全局實現點擊TitleBar滾動到頂部

前幾天產品拿着Android App問我們爲什麼他點擊通知欄或者TitleBar都不回滾動到頂部,這不是系統自帶的麼?這還真不是,蘋果是自帶功能,而有些安卓廠商也有自實現(例如錘子或魅族?),但畢竟不是Android系統自帶,所以我們就考慮在我們應用實現此功能。

與系統功能不同,我們是在應用裏的TitleBar裏實現點擊滾動到頂部的,單界面實現這個功能並不難,只要在TitleBar裏設置點擊/雙擊事件,然後把對應的可滾動的View傳給它,讓它去調用scrollToTop滾動方法,如ListView和GridView的smoothScrollToTop()方法就可以了。但如果要在每個界面都實現這功能,我們當然不能每個界面都這樣去設置一遍,這樣太蠢後期也不好維護,我們分析了一下,既然是所有有滾動的界面都要有此功能,那麼只能在基類裏面實現了,最好做到對所有具體界面無感,不改具體界面每一行代碼。針對我們現有的界面架構(TitleBar有抽象出來,默認會動態添加到Activity裏),我認爲實現難度應該不大,但有幾個問題需要解決:

  1. 如何獲取當前界面可滾動的View,如果有多個可滾動的View那麼該滾動哪個?

  2. 我們的Activity與Fragment的界面搭配較多,一個Activity裏的不同Fragment來回切換時怎麼把當前的Fragment裏可滾動View傳給Titlebar

  3. Fragment裏面如果再多次嵌套Fragment,如何實現上面遇到的問題

  4. 如果是滑動的item數較多時,怎麼一下子就滑動到頂部,而不是按勻速緩慢的移動到頂部

帶着上面這些問題,我撲哧撲哧的幹了起來,一個個將它們擊破:

問題1:

由於我們界面沒有多塊區域可滾動的情況,所以我們只要從界面的最下層遍歷上來,以第一個可滾動的View爲準就可以,實現代碼如下:

/**
 * 遍歷找到viewGroup下面的第一個可滑動的View
 * @param viewGroup
 * @return 第一個可滑動的子View
 */
public static View findFirstCanScrollView(ViewGroup viewGroup) {
    if (viewGroup != null) {
        for (int i = 0, N = viewGroup.getChildCount(); i < N; i++) {
            View child = viewGroup.getChildAt(i);
            if (child instanceof AbsListView) {
                Log.i("findFirstCanScrollView", "get AbsListView");
                return child;
            } else if (child instanceof ScrollView) {
                Log.i("findFirstCanScrollView", "get ScrollView");
                return child;
            } else if (child instanceof WebView) {
                Log.i("findFirstCanScrollView", "get WebView");
                return child;
            } else if (child instanceof ViewGroup) {
                View scrollView = findFirstCanScrollView((ViewGroup) child);
                if (scrollView != null) {
                    return scrollView;
                }
            }
        }
    }
    return null;
}

從上面的代碼可以看到,只要把我們界面的最下層的ViewGroup傳進來,只要找到任意一個可滑動的View直接就返回,我們現在用到的可滾動的View總共有幾類,ListView/GridView/ScrollView/WebView,現在RecyclerView用得還比較少,等用到了再加上去。這種方式實現唯一的一個缺點就是可能引起性能問題,但由於我們界面還不算特別複雜,從我多次測試結果上看,遍歷一次最多耗時不回超過5ms,大部分情況是0/1/2ms,且一般只會在初始化遍歷一次,所以此方法是可行的。

問題2:

下圖是我們界面最爲複雜的一個界面框架草圖:

app概況圖

先說一下Activity和Fragment搭配的界面,由於我們的TitleBar是依附於Activity,所以當前的Fragment就得去設置Activity裏TitleBar的可滑動View,這只需要在BaseFragment裏做就可以:

 /**
     * 設置Activity的滑動View
     * @param canScrollView
     */
    protected void setSmoothToTopView(View canScrollView) {
        if (getActivity() instanceof BaseActivity) {
            ((BaseActivity)getActivity()).setSmoothToTopView(canScrollView);
        }
    }

從上圖可以看到,現在APP一般首頁界面都由幾個Tab,當點擊底部不同Tab時,切換到不同的Fragment,這種情況下所以每次切換Tab時,都要重新設置Activity裏的ScrollView。在Android系統裏,不同Fragment的切換通常情況下有兩種方式:

  • 一種是用FragmentManager來hide和show不同的Fragment,這種情況每次show和hide都會回調Fragment裏的onHiddenChanged(boolean hidden)方法,只要複寫這個方法就可以。

  • 一種是用ViewPager配合Fragment的切換,這種情況下會回調Fragment裏面的setUserVisibleHint(boolean isVisibleToUser),在顯示的Fragment傳入true,在隱藏的Fragment傳入false

問題3:

從上面的app草圖裏面可看到,在Titlebar的下面還有另外一排tab,這樣的界面在安卓的app上也能經常看到,但這種界面會碰到新的問題:例如你在這個界面點擊頂部的了TAB3,但是你又點擊了底部其他的TAB,待會重新點回有頭部tab的fragment,你會發現,這時並不會調用上面tab相應fragment的setUserVisibleHint(boolean isVisibleToUser)方法(一般上面的tab用ViewPager來實現),這也能理解,因爲頂部tab是切換頂部纔會調用,切換底部跟它沒關係。

此時我們能做的辦法就是,主動調用一次剛纔頂部顯示的fragment 的setUserVisibleHint(boolean isVisibleToUser)方法,代碼如下:

  /**
     * viewPager顯示或隱藏Fragment時的回調
     * @param isVisibleToUser
     */
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        //去遍歷找
        processFirstVisibleToUser(isVisibleToUser);

        if (isVisibleToUser) {
            /**
             * 用於fragment嵌套fragment的情況,當點擊父fragment時,父fragment也要回調子fragment的setUserVisibleHint方法
             */
            List<Fragment> fragments = getChildFragmentManager().getFragments();
            if (fragments != null && fragments.size() > 0) {
                for (Fragment fragment : fragments) {
                    if (fragment != null && fragment.getUserVisibleHint()) {
                        fragment.setUserVisibleHint(true);
                    }
                }
            }
        }
    }

關於Activity和Fragment的搭配還碰到一個問題,是當Fragment只是佔Activity的一小塊區域,而此時的Activity裏的主要區域又是可滑動的,這時Fragment會把Activity的ScrollView沖掉,這種情況我們只能提供一個特殊處理辦法,對於這種沒有ScrollView或者那些不想滾動到頂部的Fragement,讓它們去複寫父類的方法:

 /**
     * 有些界面不需要滑動到頂部,可複寫此方法,返回false
     * @return
     */
    protected boolean isScrollToTop() {
        return true;
    }

問題4:

對於滾動緩慢的問題比較好辦,AbsListView這種類型,比如你已經滑動到100行了,想一下子滾動到頂部,可以先調用setSelection(10),然後再調用smoothScrollToPosition(0),從滑動效果上看沒破綻。對於WebView,由於它沒有smoothScrollToTop之類的方法,可以用ObjectAnimator來動態改變它的ScrollY軸,這種方式只要設置duration就好。TitleBar裏的調用滾動的方法如下:

   /**
     * 雙擊操作,目前只對WebView,ScrollView,AbsListView做操作
     */
    private void doubleClick() {
        if (mSmoothToTopView != null) {
            if (mSmoothToTopView instanceof AbsListView) {
                AbsListView smoothToTopView = (AbsListView) mSmoothToTopView;
                int firstVisiblePosition = smoothToTopView.getFirstVisiblePosition();
                if (firstVisiblePosition < 11) {
                    smoothToTopView.smoothScrollToPosition(0);
                } else {
                    //太多item時加快速度
                    smoothToTopView.setSelection(10);
                    smoothToTopView.smoothScrollToPosition(0);
                }
                smoothToTopView.clearFocus();
            } else if (mSmoothToTopView instanceof ScrollView) {
                ScrollView scrollView = ((ScrollView) mSmoothToTopView);
                scrollView.smoothScrollTo(scrollView.getScrollX(), 0);
            } else if (mSmoothToTopView instanceof WebView) {
                ObjectAnimator anim = ObjectAnimator.ofInt(mSmoothToTopView, "scrollY", mSmoothToTopView.getScrollY(), 0);
                anim.setDuration(200).start();
            }
        }
    }

看到這裏你可能會發現,把細節封裝的基類雖然很好,但要處理和兼容很多特殊情況,這也是一個框架魯棒性的體現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章