一、概述
Android在support.v4包中為大家提供了兩個非常神奇的類:
如果你從未聽說過這兩個類,沒關系,聽我慢慢介紹,你就明白這兩個類可以用來干嘛了。相信大家都見識過或者使用過CoordinatorLayout,通過這個類可以非常便利的幫助我們完成一些炫麗的效果,例如下面這樣的:

這樣的效果就非常適合使用NestedScrolling機制去完成,并且CoordinatorLayout背后其實也是利用著這套機制,So,我相信你已經明白這套機制可以用來干嘛了。
但是,我相信你還有個問題
恩,我們簡單分析下:
按照上圖:
假設我們按照傳統的事件分發去理解,首先我們滑動的是下面的內容區域,而移動卻是外部的ViewGroup在移動,所以按照傳統的方式,肯定是外部的Parent攔截了內部的Child的事件;但是,上述效果圖,當Parent滑動到一定程度時,Child又開始滑動了,中間整個過程是沒有間斷的。從正常的事件分發(不手動調用分發事件,不手動去發出事件)角度去做是不可能的,因為當Parent攔截之后,是沒有辦法再把事件交給Child的,事件分發,對于攔截,相當于一錘子買賣,只要攔截了,當前手勢接下來的事件都會交給Parent(攔截者)來處理。
但是NestedScrolling機制來處理這個事情就很好辦,所以對這個機制進行深入學習,一來有助于我們編寫嵌套滑動時一些特殊的效果;二來是我為了對CoordinatorLayout做分析的鋪墊~~~
ps:具體在哪個v4版本中添加的,就不去深究了,如果你的v4中沒有上述兩個類,升級下你的v4版本。NestedScrolling機制這個詞,個人稱呼,不清楚官方有沒有這么叫,勿深究。
二、預期效果
當然講解這兩個類,肯定要有案例的支撐,不然太過于空洞了。好在,我這里有個非常好的案例可以來描述:
完全按照傳統的方式去編寫的,而且為了連續滑動,做了一些非常特殊處理,比如手動去分發DOWN事件類的,有興趣可以閱讀下。
效果圖是這樣的:

今天我們就利用這個效果,作為NestedSroll機制的案例,最后我們還會簡單分析一下源碼,其實源碼還是比較簡單的~~
ps:CoordinatorLayout可以很方便實現該效果,后續的文章也會對CoordinateLayout做一些分析。
三、實現
上述效果圖,分為3部分:頂部布局;中間的ViewPager指示器;以及底部的RecyclerView;
RecyclerView其實就是NestedSrollingChild的實現類,所以本例主要的角色是去實現NestedScrollingParent.
(1)布局文件
首先預覽下布局文件,腦子里面有個大致的布局:
<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的,并且設置的是orientation="vertical",所以直觀的就是控件按順序縱向排列,至于測量需要做一些特殊的處理,因為不是本文的重點,可以自己查看源碼,或者上面提到的文章。
(2) 實現NestedScrollingParent
NestedScrollingParent是一個接口,實現它需要實現如下方法:
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();
在寫具體的實現前,先對需要用到的上述方法做一下簡單的介紹:
主要關注的就是這三個方法~
這里內部View表示不一定非要是直接子View,只要是內部View即可。
下面看一下我們具體的實現:
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; }}以上代碼就能實現下面的效果:

對于fling方法,我們利用了OverScroll的fling的方法,對于邊界檢測,是重寫了scrollTo方法:
public void fling(int velocityY){ mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight); invalidate();}@Overridepublic void scrollTo(int x, int y){ if (y < 0) { y = 0; } if (y > mTopViewHeight) { y = mTopViewHeight; } if (y != getScrollY()) { super.scrollTo(x, y); }}詳細的解釋可以看上面提到的文章,這里就不重復了。
到這里呢,可以看到NestedScrolling機制說白了非常簡單:
就是NestedScrollingParent內部的View,在滑動到時候,會首先將dx、dy傳入給NestedScrollingParent,NestedScrollingParent可以決定是否對其進行消耗,一般會根據需求消耗部分或者全部(不過這里并沒有實際的約束,你可以隨便寫消耗多少,可能會對內部View造成一定的影響)。
用白話和原本的事件分發機制作對比就是這樣的(針對正常流程下一次手勢):
具體的源碼會比本博文復雜,因為涉及到觸摸非內部View區域的一些交互,非本博文重點,可以參考源碼。
四、原理
原理其實就是看內部View什么時候回調NestedScrollingParent各種方法的,直接定位到內部View的onTouchEvent:
@Overridepublic 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調用了startNestedScroll;ACTION_MOVE中調用了dispatchNestedPreScroll;ACTION_UP可能會觸發fling以調用resetTouch。
startNestedScroll內部實際上:
#NestedScrollingChildHelperpublic 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,然后回調onStartNestedScroll和onNestedScrollAccepted。
dispatchNestedPreScroll中會回調onNestedPreScroll方法,內部的scrollByInternal中還會回調onNestedScroll方法。
fling中會回調onNestedPreFling和onNestedFling方法。
resetTouch中則會回調onStopNestedScroll。
代碼其實沒什么貼的,大家直接找到onTouchEvent一眼就能看到,調用的方法名都是dispatchNestedXXX方法,實際內部都是通過NestedScrollingChildHelper實現的。
所以如果你需要實現和NestedScrollingParent協作的內部View,記得實現NestedScrollingChild,然后內部借助NestedScrollingChildHelper這個輔助類,核心的方法都封裝好了,你只需要在恰當的實際去傳入參數調用方法即可。
ok,這樣的一個機制一定要去試試,很多滑動相關的效果都可以借此實現;
源碼地址:
github地址:https://github.com/hongyangAndroid/Android-StickyNavLayout
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對VEVB武林網的支持。
新聞熱點
疑難解答