前幾天產品拿着Android App問我們爲什麼他點擊通知欄或者TitleBar都不回滾動到頂部,這不是系統自帶的麼?這還真不是,蘋果是自帶功能,而有些安卓廠商也有自實現(例如錘子或魅族?),但畢竟不是Android系統自帶,所以我們就考慮在我們應用實現此功能。
與系統功能不同,我們是在應用裏的TitleBar裏實現點擊滾動到頂部的,單界面實現這個功能並不難,只要在TitleBar裏設置點擊/雙擊事件,然後把對應的可滾動的View傳給它,讓它去調用scrollToTop滾動方法,如ListView和GridView的smoothScrollToTop()方法就可以了。但如果要在每個界面都實現這功能,我們當然不能每個界面都這樣去設置一遍,這樣太蠢後期也不好維護,我們分析了一下,既然是所有有滾動的界面都要有此功能,那麼只能在基類裏面實現了,最好做到對所有具體界面無感,不改具體界面每一行代碼。針對我們現有的界面架構(TitleBar有抽象出來,默認會動態添加到Activity裏),我認爲實現難度應該不大,但有幾個問題需要解決:
如何獲取當前界面可滾動的View,如果有多個可滾動的View那麼該滾動哪個?
我們的Activity與Fragment的界面搭配較多,一個Activity裏的不同Fragment來回切換時怎麼把當前的Fragment裏可滾動View傳給Titlebar
Fragment裏面如果再多次嵌套Fragment,如何實現上面遇到的問題
如果是滑動的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:
下圖是我們界面最爲複雜的一個界面框架草圖:
先說一下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();
}
}
}
看到這裏你可能會發現,把細節封裝的基類雖然很好,但要處理和兼容很多特殊情況,這也是一個框架魯棒性的體現。