編輯:Android資訊
從 Android 5.0 Lollipop 開始提供一套 API 來支持嵌入的滑動效果。同樣在最新的 Support V4 包中也提供了前向的兼容。有了嵌入滑動機制,就能實現很多很復雜的滑動效果。在 Android Design Support 庫中非常總要的 CoordinatorLayout 組件就是使用了這套機制,實現了 Toolbar 的收起和展開功能,如下圖所示:
NestedScrolling提供了一套父 View 和子 View 滑動交互機制。要完成這樣的交互,父 View 需要實現 NestedScrollingParent 接口,而子 View 需要實現 NestedScrollingChild 接口。
首先來說NestedScrollingChild。如果你有一個可以滑動的 View,需要被用來作為嵌入滑動的子 View,就必須實現本接口。在此 View 中,包含一個 NestedScrollingChildHelper 輔助類。NestedScrollingChild接口的實現,基本上就是調用本 Helper 類的對應的函數即可,因為 Helper 類中已經實現好了 Child 和 Parent 交互的邏輯。原來的 View 的處理 Touch 事件,並實現滑動的邏輯大體上不需要改變。
需要做的就是,如果要准備開始滑動了,需要告訴 Parent,你要准備進入滑動狀態了,調用startNestedScroll()。你在滑動之前,先問一下你的 Parent 是否需要滑動,也就是調用dispatchNestedPreScroll()。如果父類滑動了一定距離,你需要重新計算一下父類滑動後剩下給你的滑動距離余量。然後,你自己進行余下的滑動。最後,如果滑動距離還有剩余,你就再問一下,Parent 是否需要在繼續滑動你剩下的距離,也就是調用dispatchNestedScroll()。
以上是一些基本原理,有了上面的基本思路,可以參考這篇 文章 ,這裡面有原理的詳細解析。如果還是不清楚, 這裡 有對應的代碼可以參考。
作為一個可以嵌入 NestedScrollingChild 的父 View,需要實現NestedScrollingParent,這個接口方法和NestedScrollingChild大致有一一對應的關系。同樣,也有一個 NestedScrollingParentHelper 輔助類來默默的幫助你實現和 Child 交互的邏輯。滑動動作是 Child 主動發起,Parent 就收滑動回調並作出響應。
從上面的 Child 分析可知,滑動開始的調用startNestedScroll(),Parent 收到onStartNestedScroll()回調,決定是否需要配合 Child 一起進行處理滑動,如果需要配合,還會回調onNestedScrollAccepted()。
每次滑動前,Child 先詢問 Parent 是否需要滑動,即dispatchNestedPreScroll(),這就回調到 Parent 的onNestedPreScroll(),Parent 可以在這個回調中“劫持”掉 Child 的滑動,也就是先於 Child 滑動。
Child 滑動以後,會調用onNestedScroll(),回調到 Parent 的onNestedScroll(),這裡就是 Child 滑動後,剩下的給 Parent 處理,也就是 後於 Child 滑動。
最後,滑動結束,調用onStopNestedScroll()表示本次處理結束。
其實,除了上面的 Scroll 相關的調用和回調,還有 Fling 相關的調用和回調,處理邏輯基本一致。
有了這一套官方的嵌套滑動的解決方案,打算把我的 FlyRefresh 的滑動和下來部分用 NestedScrolling 來實現。我在這篇博客中講了,之前是通過在PullHeaderLayout的dispatchTouchEvent()中小心處理 Touch 事件來實現的。現在回想起來,這種方法相對復雜,需要清楚知道 Parent 和 Child 的滑動狀態,這就導致了,只能支持有限的 Child 類型,例如當時只支持 ListView 和 RecyclerView,為了支持更多的類型,還定義了一個IScrollHandler接口來支持。
讓 FlyRefresh 實現NestedScrollingParent,就可以支持所有的NestedScrollingChild作為FlyRefreshLayout的子 View。另外,因為CoordinatorLayout是如此的重要,大部分的 App 都需要使用它作為頂層的 Layout,為了讓FlyRefreshLayout能夠在 CoordinatorLayout 也能使用,所以我還打算同時實現NestedScrollingChild接口。關鍵實現代碼如下:
public class PullHeaderLayout extends ViewGroup implements NestedScrollingParent, NestedScrollingChild { private final int[] mScrollOffset = new int[2]; private final int[] mScrollConsumed = new int[2]; private final NestedScrollingParentHelper mParentHelper; private final NestedScrollingChildHelper mChildHelper; ... // NestedScrollingChild @Override public void setNestedScrollingEnabled(boolean enabled) { mChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return mChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return mChildHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { mChildHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return mChildHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); } // NestedScrollingParent @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); } @Override public void onStopNestedScroll(View target) { stopNestedScroll(); } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { final int myConsumed = moveBy(dyUnconsumed); final int myUnconsumed = dyUnconsumed - myConsumed; dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { if (dy > 0 && mHeaderController.canScrollUp()) { final int delta = moveBy(dy); consumed[0] = 0; consumed[1] = delta; //dispatchNestedScroll(0, myConsumed, 0, consumed[1], null); } } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { if (!consumed) { flingWithNestedDispatch((int) velocityY); return true; } return false; } private boolean flingWithNestedDispatch(int velocityY) { final boolean canFling = (mHeaderController.canScrollUp() && velocityY > 0) || (mHeaderController.canScrollDown() && velocityY < 0); if (!dispatchNestedPreFling(0, velocityY)) { dispatchNestedFling(0, velocityY, canFling); if (canFling) { fling(velocityY); } } return canFling; } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return flingWithNestedDispatch((int) velocityY); } @Override public int getNestedScrollAxes() { return mParentHelper.getNestedScrollAxes(); } // Touch event hanlder @Override public boolean onTouchEvent(MotionEvent ev) { MotionEvent vtev = MotionEvent.obtain(ev); final int actionMasked = MotionEventCompat.getActionMasked(ev); if (actionMasked == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0; } vtev.offsetLocation(0, mNestedYOffset); switch (actionMasked) { ... case MotionEvent.ACTION_MOVE: ... final int y = (int) MotionEventCompat.getY(ev, activePointerIndex); int deltaY = mLastMotionY - y; if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { deltaY -= mScrollConsumed[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } mIsBeingDragged = true; if (deltaY > 0) { deltaY -= mTouchSlop; } else { deltaY += mTouchSlop; } } if (mIsBeingDragged) { // Scroll to follow the motion event mLastMotionY = y - mScrollOffset[1]; final int scrolledDeltaY = moveBy(deltaY); final int unconsumedY = deltaY - scrolledDeltaY; if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { mLastMotionY -= mScrollOffset[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } } break; ... } ... return true; } ... }
完整的修改,可以看這個 commit 。整個修改下來,代碼減少了不少,而且更加整潔了。
總體來說, NestedScroll 初看起來有些讓人費解,但是真的理解以後,就發現這種設計的優秀之處。把滑動整體封裝起來,通過 Helper 來實現 Child 和 Parent 之間的連接和交互。通過接口來回調,實現了 Child 和 Parent 的邏輯獨立。
Android 5.0的大部分可以滑動的控件都支持了 NestScrolling 接口,最新的 Support V4 中也一樣,相信以後越來越多的第三方庫都會支持,到時候各種控件的嵌套滑動就能無縫集成了。
最近看到React Native好像好厲害的樣子,好奇心驅使之下體驗了一下並將在Window下搭建React Natvie Android環境的步驟記錄下來,並
Button,就是按鈕,是android中應用最多的組件之一,Button有兩種用法,一種是XML中配置,另一種是在程序中直接使用 在XML布局文件裡,會遇到如下
以前一直想寫一篇總結 Android 開發經驗的文章,估計當時的我還達不到某種水平,所以思路跟不上,下筆又捉襟見肘。近日,思路較為明朗,於是重新操起鍵盤開始碼字一
本文由碼農網 – 小峰原創,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃! Android開發是目前最熱門的移動開發技術之一,隨著開發者的不斷努力