此篇blog為譯文,原文點(diǎn)擊這里
在上一篇blog中我們簡要介紹了CoordinatorLayout和自定義Behaviour。評論中有些人詢問怎樣編寫一個可以和CoordinatorLayout還有AppBarLayout協(xié)同工作的scroll view。也有些讀者好奇為什么AppBarLayout可以和RecyclerView一起工作而和ListView卻不行。
我們先來看看AppBarLayout隨著RecyclerView上下滾動而消失和顯示的效果吧。
源代碼
查看AppBarLayout的源碼可以發(fā)現(xiàn),AppBarLayout定義了default Behaviour@DefaultBehavior(AppBarLayout.Behavior.class)
AppBarLayout.Behavior實現(xiàn)了下列和scroll事件相關(guān)的方法:
void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed)
void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes)
boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes)
void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target)
這就解釋了為什么AppBarLayout被放在CoordinatorLayout里面就能夠移動自己的位置。那么問題來了,CoordinatorLayout是怎么知道子view發(fā)生滾動了的呢。
瞅瞅NestedScrollingChild和NestedScrollingParent吧
查看源碼發(fā)現(xiàn)RecyclerView實現(xiàn)了NestedScrollingChild接口。讓我們看看文檔咋說的:
This interface should be implemented by View subclasses that wish to support dispatching nested scrolling operations to a cooperating parent ViewGroup.
翻譯一下吧:需要發(fā)送nested scrolling操作給父view的子view需要實現(xiàn)該接口。
CoordinatorLayout處理子view發(fā)送的nested scrolling操作,所以實現(xiàn)了NestedScrollingParent接口。文檔解釋如下:
This interface should be implemented by ViewGroup subclasses that wish to support scrolling operations delegated by a nested child view.
翻譯一下:需要處理子view委派的scroll操作的viewgroup都應(yīng)該實現(xiàn)該接口
接下來讓我們深入看看NestedScrollingChild和NestedScrollingParent之間是如何交流協(xié)作的。
當(dāng)child開始開始滾動的時候,會調(diào)用startNestedScroll方法。接著在每一次執(zhí)行onScroll的時候都會調(diào)用dispatchNestedPreScroll和dispatchNestedScroll方法。dispatchNestedPreScroll給父view提供了消耗部分或者全部滾動操作的機(jī)會,如果該方法返回true則子view必須在執(zhí)行滾動操作的時候減去父view消耗的部分。而dispatchNestedScroll方法用于向父view報告自身的滾動情況,包括自身消耗的部分和未消耗的部分。父view對這兩個值的處理也可能會有所不同:
An implementation may choose to use the consumed portion to match or chase scroll position of multiple child elements, for example. The unconsumed portion may be used to allow continuous dragging of multiple scrolling or draggable elements, such as scrolling a list within a vertical drawer where the drawer begins dragging once the edge of inner scrolling content is reached.
翻譯一下:例如在有的viewgroup中會根據(jù)子view消耗的部分來對齊或者追蹤其他子view的滾動位置。而沒有消耗的部分可能會被嵌套scroll或者可拖動view消耗,比如,在一個drawer里面滾動list,當(dāng)list已經(jīng)滾動到邊緣了用戶還在繼續(xù)滑動操作,此時就能拖動drawer了。
CoordinatorLayout就像是一個代理,接收子view的回調(diào),并傳遞給其他子view的Behavior對象。
當(dāng)滑動結(jié)束的時候,子view會調(diào)用stopNestedScroll方法。
自定義NestedScrollingChild View
我們已經(jīng)知道原理了,下一步就該實現(xiàn)一個簡單的NestedScrollingChild對象來監(jiān)控scroll事件并委托給CoordinatorLayout對象處理。
- 讓我們從自定義view開始
public class NestedScrollingChildView extends View implements NestedScrollingChild, OnGestureListener
為了讓我們的開發(fā)更為簡單,谷歌引入了NestedScrollingChildHelper:
Helper class for implementing nested scrolling child views compatible with Android platform versions earlier than Android 5.0 Lollipop (API 21).
我們需要做的僅僅是創(chuàng)建helper對象并將各個事件委托給helper處理。
在默認(rèn)情況下nested scroll是disabled狀態(tài),所以需要在構(gòu)造函數(shù)中開啟該功能
setNestedScrollingEnabled(true);
- 接著我們需要監(jiān)測scroll事件,為了方便我們直接使用
GestureDetectorCompat實例就行了。
@Override
public boolean onTouchEvent(MotionEvent event){
return mDetector.onTouchEvent(event);
}
- 然后在
onDown方法里,我們需要調(diào)用startNestedScroll方法并將垂直滾動標(biāo)記作為參數(shù)傳進(jìn)去。
@Override
public boolean onDown(MotionEvent e) {
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
return true;
}
- 然后是在
onScroll方法中調(diào)用dispatchNestedPreScroll方法并傳入計算所得的Y軸滾動距離;然后繼續(xù)調(diào)用dispatchNestedScroll方法,由于我們的view壓根不會滾動所以參數(shù)都傳0就可以了。
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
dispatchNestedPreScroll(0, (int) distanceY, null, null);
dispatchNestedScroll(0, 0, 0, 0, null);
return true;
}
- 最后一步就是調(diào)用
stopNestedScroll了。因為GestureDetectorCompat并沒有提供合適的回調(diào),所以我們必須自己在onTouchEvent方法中調(diào)用stopNestedScroll方法。
@Override
public boolean onTouchEvent(MotionEvent event){
final boolean handled = mDetector.onTouchEvent(event);
if (!handled && event.getAction() == MotionEvent.ACTION_UP) {
stopNestedScroll();
}
return true;
}
為了簡單,咱就沒有去處理fling的情況了。
來吧,看看效果吧。
恩,文中所有的代碼都在https://github.com/ggajews/nestedscrollingchildviewdemo