詳解Android中的NestedScrolling機(jī)制帶你玩轉(zhuǎn)嵌套滑動(dòng)
一、概述
Android在support.v4包中為大家提供了兩個(gè)非常神奇的類:
- NestedScrollingParent
- NestedScrollingChild
如果你從未聽說過這兩個(gè)類,沒關(guān)系,聽我慢慢介紹,你就明白這兩個(gè)類可以用來干嘛了。相信大家都見識(shí)過或者使用過CoordinatorLayout,通過這個(gè)類可以非常便利的幫助我們完成一些炫麗的效果,例如下面這樣的:
這樣的效果就非常適合使用NestedScrolling機(jī)制去完成,并且CoordinatorLayout背后其實(shí)也是利用著這套機(jī)制,So,我相信你已經(jīng)明白這套機(jī)制可以用來干嘛了。
但是,我相信你還有個(gè)問題
- 這個(gè)機(jī)制相比傳統(tǒng)的自定義ViewGroup事件分發(fā)處理有什么優(yōu)越的地方嗎?
恩,我們簡單分析下:
按照上圖:
假設(shè)我們按照傳統(tǒng)的事件分發(fā)去理解,首先我們滑動(dòng)的是下面的內(nèi)容區(qū)域,而移動(dòng)卻是外部的ViewGroup在移動(dòng),所以按照傳統(tǒng)的方式,肯定是外部的Parent攔截了內(nèi)部的Child的事件;但是,上述效果圖,當(dāng)Parent滑動(dòng)到一定程度時(shí),Child又開始滑動(dòng)了,中間整個(gè)過程是沒有間斷的。從正常的事件分發(fā)(不手動(dòng)調(diào)用分發(fā)事件,不手動(dòng)去發(fā)出事件)角度去做是不可能的,因?yàn)楫?dāng)Parent攔截之后,是沒有辦法再把事件交給Child的,事件分發(fā),對(duì)于攔截,相當(dāng)于一錘子買賣,只要攔截了,當(dāng)前手勢(shì)接下來的事件都會(huì)交給Parent(攔截者)來處理。
但是NestedScrolling機(jī)制來處理這個(gè)事情就很好辦,所以對(duì)這個(gè)機(jī)制進(jìn)行深入學(xué)習(xí),一來有助于我們編寫嵌套滑動(dòng)時(shí)一些特殊的效果;二來是我為了對(duì)CoordinatorLayout做分析的鋪墊~~~
ps:具體在哪個(gè)v4版本中添加的,就不去深究了,如果你的v4中沒有上述兩個(gè)類,升級(jí)下你的v4版本。NestedScrolling機(jī)制這個(gè)詞,個(gè)人稱呼,不清楚官方有沒有這么叫,勿深究。
二、預(yù)期效果
當(dāng)然講解這兩個(gè)類,肯定要有案例的支撐,不然太過于空洞了。好在,我這里有個(gè)非常好的案例可以來描述:
很久以前,我寫過這樣一篇文章:
Android通過自定義控件實(shí)現(xiàn)360軟件詳情頁效果
完全按照傳統(tǒng)的方式去編寫的,而且為了連續(xù)滑動(dòng),做了一些非常特殊處理,比如手動(dòng)去分發(fā)DOWN事件類的,有興趣可以閱讀下。
效果圖是這樣的:
今天我們就利用這個(gè)效果,作為NestedSroll機(jī)制的案例,最后我們還會(huì)簡單分析一下源碼,其實(shí)源碼還是比較簡單的~~
ps:CoordinatorLayout可以很方便實(shí)現(xiàn)該效果,后續(xù)的文章也會(huì)對(duì)CoordinateLayout做一些分析。
三、實(shí)現(xiàn)
上述效果圖,分為3部分:頂部布局;中間的ViewPager指示器;以及底部的RecyclerView;
RecyclerView其實(shí)就是NestedSrollingChild的實(shí)現(xiàn)類,所以本例主要的角色是去實(shí)現(xiàn)NestedScrollingParent.
(1)布局文件
首先預(yù)覽下布局文件,腦子里面有個(gè)大致的布局:
<com.zhy.view.StickyNavLayout xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <RelativeLayout android:id="@id/id_stickynavlayout_topview" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#4400ff00" > <TextView android:layout_width="match_parent" android:layout_height="256dp" android:gravity="center" android:text="軟件介紹" android:textSize="30sp" android:textStyle="bold" /> </RelativeLayout> <com.zhy.view.SimpleViewPagerIndicator android:id="@id/id_stickynavlayout_indicator" android:layout_width="match_parent" android:layout_height="50dp" android:background="#ffffffff" > </com.zhy.view.SimpleViewPagerIndicator> <android.support.v4.view.ViewPager android:id="@id/id_stickynavlayout_viewpager" android:layout_width="match_parent" android:layout_height="match_parent" > </android.support.v4.view.ViewPager> </com.zhy.view.StickyNavLayout>
StickyNavLayout是直接繼承自LinearLayout的,并且設(shè)置的是orientation="vertical",所以直觀的就是控件按順序縱向排列,至于測(cè)量需要做一些特殊的處理,因?yàn)椴皇潜疚牡闹攸c(diǎn),可以自己查看源碼,或者上面提到的文章。
(2) 實(shí)現(xiàn)NestedScrollingParent
NestedScrollingParent是一個(gè)接口,實(shí)現(xiàn)它需要實(shí)現(xiàn)如下方法:
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes); public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes); public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed); public void onNestedPreScroll(View target, int dx, int dy, int[] consumed); public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed); public boolean onNestedPreFling(View target, float velocityX, float velocityY); public int getNestedScrollAxes();
在寫具體的實(shí)現(xiàn)前,先對(duì)需要用到的上述方法做一下簡單的介紹:
- onStartNestedScroll該方法,一定要按照自己的需求返回true,該方法決定了當(dāng)前控件是否能接收到其內(nèi)部View(非并非是直接子View)滑動(dòng)時(shí)的參數(shù);假設(shè)你只涉及到縱向滑動(dòng),這里可以根據(jù)nestedScrollAxes這個(gè)參數(shù),進(jìn)行縱向判斷。
- onNestedPreScroll該方法的會(huì)傳入內(nèi)部View移動(dòng)的dx,dy,如果你需要消耗一定的dx,dy,就通過最后一個(gè)參數(shù)consumed進(jìn)行指定,例如我要消耗一半的dy,就可以寫consumed[1]=dy/2
- onNestedFling你可以捕獲對(duì)內(nèi)部View的fling事件,如果return true則表示攔截掉內(nèi)部View的事件。
主要關(guān)注的就是這三個(gè)方法~
這里內(nèi)部View表示不一定非要是直接子View,只要是內(nèi)部View即可。
下面看一下我們具體的實(shí)現(xiàn):
public class StickyNavLayout extends LinearLayout implements NestedScrollingParent { @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { boolean hiddenTop = dy > 0 && getScrollY() < mTopViewHeight; boolean showTop = dy < 0 && getScrollY() > 0 && !ViewCompat.canScrollVertically(target, -1); if (hiddenTop || showTop) { scrollBy(0, dy); consumed[1] = dy; } } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { if (getScrollY() >= mTopViewHeight) return false; fling((int) velocityY); return true; } }
- onStartNestedScroll中,我們判斷了如果是縱向返回true,這個(gè)一般是需要內(nèi)部的View去傳入的,你要是不確定,或者擔(dān)心內(nèi)部View編寫的不規(guī)范,你可以直接return true;
- onNestedPreScroll中,我們判斷,如果是上滑且頂部控件未完全隱藏,則消耗掉dy,即consumed[1]=dy;如果是下滑且內(nèi)部View已經(jīng)無法繼續(xù)下拉,則消耗掉dy,即consumed[1]=dy,消耗掉的意思,就是自己去執(zhí)行scrollBy,實(shí)際上就是我們的StickNavLayout滑動(dòng)。
- 此外,這里還處理了fling,通過onNestedPreFling方法,這個(gè)可以根據(jù)自己需求定了,當(dāng)頂部控件顯示時(shí),fling可以讓頂部控件隱藏或者顯示。
以上代碼就能實(shí)現(xiàn)下面的效果:
對(duì)于fling方法,我們利用了OverScroll的fling的方法,對(duì)于邊界檢測(cè),是重寫了scrollTo方法:
public void fling(int velocityY) { mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight); invalidate(); } @Override public void scrollTo(int x, int y) { if (y < 0) { y = 0; } if (y > mTopViewHeight) { y = mTopViewHeight; } if (y != getScrollY()) { super.scrollTo(x, y); } }
詳細(xì)的解釋可以看上面提到的文章,這里就不重復(fù)了。
到這里呢,可以看到NestedScrolling機(jī)制說白了非常簡單:
就是NestedScrollingParent內(nèi)部的View,在滑動(dòng)到時(shí)候,會(huì)首先將dx、dy傳入給NestedScrollingParent,NestedScrollingParent可以決定是否對(duì)其進(jìn)行消耗,一般會(huì)根據(jù)需求消耗部分或者全部(不過這里并沒有實(shí)際的約束,你可以隨便寫消耗多少,可能會(huì)對(duì)內(nèi)部View造成一定的影響)。
用白話和原本的事件分發(fā)機(jī)制作對(duì)比就是這樣的(針對(duì)正常流程下一次手勢(shì)):
- 事件分發(fā)是這樣的:子View首先得到事件處理權(quán),處理過程中,父View可以對(duì)其攔截,但是攔截了以后就無法再還給子View(本次手勢(shì)內(nèi))。
- NestedScrolling機(jī)制是這樣的:內(nèi)部View在滾動(dòng)的時(shí)候,首先將dx,dy交給NestedScrollingParent,NestedScrollingParent可對(duì)其進(jìn)行部分消耗,剩余的部分還給內(nèi)部View。
具體的源碼會(huì)比本博文復(fù)雜,因?yàn)樯婕暗接|摸非內(nèi)部View區(qū)域的一些交互,非本博文重點(diǎn),可以參考源碼。
四、原理
原理其實(shí)就是看內(nèi)部View什么時(shí)候回調(diào)NestedScrollingParent各種方法的,直接定位到內(nèi)部View的onTouchEvent:
@Override public boolean onTouchEvent(MotionEvent e) { switch (action) { case MotionEvent.ACTION_DOWN: { int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; if (canScrollHorizontally) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; } if (canScrollVertically) { nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; } startNestedScroll(nestedScrollAxis); } break; case MotionEvent.ACTION_MOVE: { if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) { dx -= mScrollConsumed[0]; dy -= mScrollConsumed[1]; vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); } } break; case MotionEvent.ACTION_UP: { fling((int) xvel, (int) yvel); resetTouch(); } break; case MotionEvent.ACTION_CANCEL: { cancelTouch(); } break; } return true; }
可以看到:
ACTION_DOWN調(diào)用了startNestedScroll;ACTION_MOVE中調(diào)用了dispatchNestedPreScroll;ACTION_UP可能會(huì)觸發(fā)fling以調(diào)用resetTouch。
startNestedScroll內(nèi)部實(shí)際上:
#NestedScrollingChildHelper public boolean startNestedScroll(int axes) { if (hasNestedScrollingParent()) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = mView.getParent(); View child = mView; while (p != null) { if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) { mNestedScrollingParent = p; ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes); return true; } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; }
去尋找NestedScrollingParent,然后回調(diào)onStartNestedScroll和onNestedScrollAccepted。
dispatchNestedPreScroll中會(huì)回調(diào)onNestedPreScroll方法,內(nèi)部的scrollByInternal中還會(huì)回調(diào)onNestedScroll方法。
fling中會(huì)回調(diào)onNestedPreFling和onNestedFling方法。
resetTouch中則會(huì)回調(diào)onStopNestedScroll。
代碼其實(shí)沒什么貼的,大家直接找到onTouchEvent一眼就能看到,調(diào)用的方法名都是dispatchNestedXXX方法,實(shí)際內(nèi)部都是通過NestedScrollingChildHelper實(shí)現(xiàn)的。
所以如果你需要實(shí)現(xiàn)和NestedScrollingParent協(xié)作的內(nèi)部View,記得實(shí)現(xiàn)NestedScrollingChild,然后內(nèi)部借助NestedScrollingChildHelper這個(gè)輔助類,核心的方法都封裝好了,你只需要在恰當(dāng)?shù)膶?shí)際去傳入?yún)?shù)調(diào)用方法即可。
ok,這樣的一個(gè)機(jī)制一定要去試試,很多滑動(dòng)相關(guān)的效果都可以借此實(shí)現(xiàn);
源碼地址:
github地址:https://github.com/hongyangAndroid/Android-StickyNavLayout
本地下載:http://xiazai.jb51.net/201705/yuanma/Android-StickyNavLayout(jb51.net).rar
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Kotlin RadioGroup與ViewPager實(shí)現(xiàn)底層分頁按鈕方法
安卓的控件是挺多的,沒有辦法一個(gè)一個(gè)的來說明,我們挑出了一些重點(diǎn)的控件,組成一些常見的布局,這樣以后在遇到相同功能的界面時(shí),就會(huì)有自己的思路,或者進(jìn)行復(fù)用2022-12-12Android實(shí)現(xiàn)頁面跳轉(zhuǎn)
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)頁面跳轉(zhuǎn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06Android 使用jQuery實(shí)現(xiàn)item點(diǎn)擊顯示或隱藏的特效的示例
本篇文章主要介紹了Android 使用jQuery實(shí)現(xiàn)item點(diǎn)擊顯示或隱藏的特效的示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03Android通過手勢(shì)實(shí)現(xiàn)的縮放處理實(shí)例代碼
Android通過手勢(shì)實(shí)現(xiàn)的縮放處理實(shí)例代碼,需要的朋友可以參考一下2013-05-05Android開發(fā)四大組件之實(shí)現(xiàn)電話攔截和電話錄音
這篇文章給大家介紹Android開發(fā)四大組件之實(shí)現(xiàn)電話攔截和電話錄音,涉及到android四大基本組件在程序中的應(yīng)用,對(duì)android四大基本組件感興趣的朋友可以參考下本篇文章2015-10-10