最近產品提了個需求,要把商品列表作成相似淘寶的樣式android
通常遇到這種需求,咱們首先會想到的是,攔截TouchEvent,而後本身來處理滑動,這種方法雖然行得通,可是代碼寫起來很是噁心,且滑動衝突會比較多,使用NestedScrolling API會簡單優雅不少。git
先上效果圖github
Parent接口共有如下幾個方法面試
public interface NestedScrollingParent { //當子View開始滑動時,會觸發這個方法,判斷接下來是否進行嵌套滑動, //返回false,則表示不使用嵌套滑動 boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes); //onStartNestedScroll若是返回true,那麼接下來就會調用這個方法,用來作一些初始化操做,通常能夠忽略 void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes); //嵌套滑動結束時會觸發這個方法 void onStopNestedScroll(@NonNull View target); //子View滑動時會觸發這個方法,dyConsumed表明子View滑動的距離,dyUnconsumed表明子View本次滑動未消耗的距離,好比RecyclerView滑到了邊界,那麼會有一部分y未消耗掉 void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed); //子View開始滑動時,會觸發這個回調,dy表示滑動的y距離,consumed數組表明父View要消耗的距離,假如consumed[1] = dy,那麼子View就不會滑動了 void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed); //當子View fling時,會觸發這個回調,consumed表明速度是否被子View消耗掉,好比RecyclerView滑動到了邊界,那麼它顯然無法消耗本次的fling boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed); //當子View要開始fling時,會先詢問父View是否要攔截本次fling,返回true表示要攔截,那麼子View就不會慣性滑動了 boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY); //表示目前正在進行的嵌套滑動的方向,值有ViewCompat.SCROLL_AXIS_HORIZONTAL 或者ViewCompat.SCROLL_AXIS_VERTICAL或者SCROLL_AXIS_NONE @ScrollAxis int getNestedScrollAxes(); }
public interface NestedScrollingChild { //設置當前子View是否支持嵌套滑動 void setNestedScrollingEnabled(boolean enabled); //當前子View是否支持嵌套滑動 boolean isNestedScrollingEnabled(); //開始嵌套滑動,對應Parent的onStartNestedScroll boolean startNestedScroll(@ScrollAxis int axes); //中止本次嵌套滑動,對應Parent的onStopNestedScroll void stopNestedScroll(); //true表示這個子View有一個支持嵌套滑動的父View boolean hasNestedScrollingParent(); //通知父View子View開始滑動了,對應父View的onNestedScroll方法 boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow); //通知父View即將開始滑動了,對應父View的onNestedPreScroll方法 boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow); //通知父View開始Fling了,對應Parent的onNestedFling方法 boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); //通知父View要開始fling了,對應Parent的onNestedPreFling方法 boolean dispatchNestedPreFling(float velocityX, float velocityY); }
總體流程描述以下(以RecyclerView爲例):數組
child.ACTION_DOWN
-> child.startNestedScroll
-> parent.onStartNestedScroll (若是返回false,則流程終止)
-> parent.onNestedScrollAccepted
-> child.ACTION_MOVE
-> child.dispatchNestedPreScroll
-> parent.onNestedPreScroll
-> child.ACTION_UP
-> chid.stopNestedScroll
-> parent.onStopNestedScroll
-> child.fling
-> child.dispatchNestedPreFling
-> parent.onNestedPreScroll
-> child.dispatchNestedFling
-> parent.onNestedFling性能優化
有興趣的朋友能夠直接查看 RecyclerView 的源碼架構
子View向上傳遞事件時,是循環向上的,即 Parent 不須要是 Child 的直接 ViewParent,具體能夠看代碼,以startNestedScroll爲例ide
public boolean startNestedScroll(int axes) { if (hasNestedScrollingParent()) { // Already in progress return true; } if (isNestedScrollingEnabled()) { ViewParent p = getParent(); View child = this; while (p != null) { try { if (p.onStartNestedScroll(child, this, axes)) { mNestedScrollingParent = p; p.onNestedScrollAccepted(child, this, axes); return true; } } catch (AbstractMethodError e) { Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " + "method onStartNestedScroll", e); // Allow the search upward to continue } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; }
RV 嵌套 RV 時,內層 RV 是沒法滑動的,然而,當外層RV在Fling時,若是咱們觸摸到子RV,那麼會有必定機率致使子RV接收到Touch事件並開始滾動,因此咱們須要同時攔截內層和外層的RV的事件。大概思路以下:性能
具體處理爲,咱們在外層RV之上嵌套一層自定義的FrameLayout,並開啓外層RV和內層RV的嵌套滑動功能,那麼咱們就能在FrameLayout中接收到RV傳遞上來的scroll和fling事件優化
public class NestedScrollLayout extends FrameLayout { private View mChildView; /** * 最外層的RecyclerView */ private RecyclerView mRootList; /** * 子RecyclerView */ private RecyclerView mChildList; @Override public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes) { //這裏表示只有在縱向滑動時,咱們才攔截事件 return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL; } @Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) { stopScroller(); //mChildView表示TabLayout和ViewPager的父View,好比說咱們用一個LinearLayout包裹住TabLayout和ViewPager if (mChildView == null) { return; } if (target == mRootList) { onParentScrolling(mChildView.getTop(), dy, consumed); } else { onChildScrolling(mChildView.getTop(), dy, consumed); } } /** * 父列表在滑動 * * @param childTop * @param dy * @param consumed */ private void onParentScrolling(int childTop, int dy, int[] consumed) { //列表已經置頂 if (childTop == 0) { if (dy > 0 && mChildList != null) { //還在向下滑動,此時滑動子列表 mChildList.scrollBy(0, dy); consumed[1] = dy; } else { if (mChildList != null && mChildList.canScrollVertically(dy)) { consumed[1] = dy; mChildList.scrollBy(0, dy); } } } else { if (childTop < dy) { consumed[1] = dy - childTop; } } } private void onChildScrolling(int childTop, int dy, int[] consumed) { if (childTop == 0) { if (dy < 0) { //向上滑動 if (!mChildList.canScrollVertically(dy)) { consumed[1] = dy; mRootList.scrollBy(0, dy); } } } else { if (dy < 0 || childTop > dy) { consumed[1] = dy; mRootList.scrollBy(0, dy); } else { //dy大於0 consumed[1] = dy; mRootList.scrollBy(0, childTop); } } } /** * 表示咱們只接收縱向的事件 * @return */ @Override public int getNestedScrollAxes() { return ViewCompat.SCROLL_AXIS_VERTICAL; } }
ViewGroup默認實現了Parent接口,這裏咱們不須要再implement一次
當列表開始 Fling 時,咱們將會接收到相應的回調,這裏咱們須要本身處理慣性滑動,使用 OverScroller 來替咱們模擬Fling
public class NestedScrollLayout extends FrameLayout { /** * 用來處理Fling */ private OverScroller mScroller; private int mLastY; @Override public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) { return false; } @Override public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) { mLastY = 0; this.mScroller.fling(0, 0, (int) velocityX, (int) velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); invalidate(); return true; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { int currY = mScroller.getCurrY(); int dy = currY - mLastY; mLastY = currY; if (dy != 0) { onFling(dy); } invalidate(); } super.computeScroll(); } private void onFling(int dy) { if (mChildView != null) { //子列表有顯示 int top = mChildView.getTop(); if (top == 0) { if (dy > 0) { if (mChildList != null && mChildList.canScrollVertically(dy)) { mChildList.scrollBy(0, dy); } else { stopScroller(); } } else { if (mChildList != null && mChildList.canScrollVertically(dy)) { mChildList.scrollBy(0, dy); } else { mRootList.scrollBy(0, dy); } } } else { if (dy > 0) { if (top > dy) { mRootList.scrollBy(0, dy); } else { mRootList.scrollBy(0, top); } } else { if (mRootList.canScrollVertically(dy)) { mRootList.scrollBy(0, dy); } else { stopScroller(); } } } } else { if (!mRootList.canScrollVertically(dy)) { stopScroller(); } else { mRootList.scrollBy(0, dy); } } } }
到這裏爲止,咱們要的效果已經實現了,mChildView 和子RV什麼時候賦值,參考Demo便可。
你覺得這樣就完了?
谷歌在 26.1.0 的 support 包中加入了兩個新的 API
這兩個接口各自繼承了NestedScrollingParent和NestedScrollingChild
public interface NestedScrollingParent2 extends NestedScrollingParent { boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type); void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type); void onStopNestedScroll(@NonNull View target, @NestedScrollType int type); void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type); void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, @NestedScrollType int type); }
public interface NestedScrollingChild2 extends NestedScrollingChild { boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type); void stopNestedScroll(@NestedScrollType int type); boolean hasNestedScrollingParent(@NestedScrollType int type); boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type); boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type); }
在新的API中去掉了 fling 回調,而且增長了 type 參數,type分爲兩種
//表示當前事件是由用戶手指觸摸產生的 public static final int TYPE_TOUCH = 0; //表示當前事件不是用戶手指觸摸產生的,通常是fling public static final int TYPE_NON_TOUCH = 1;
Parent2具體流程以下:
child.ACTION_DOWN
-> child.startNestedScroll (TYPE_TOUCH)
-> parent.onStartNestedScroll (TYPE_TOUCH) (若是返回false,則流程終止)
-> parent.onNestedScrollAccepted (TYPE_TOUCH)
-> child.ACTION_MOVE
-> child.dispatchNestedPreScroll (TYPE_TOUCH)
-> parent.onNestedPreScroll (TYPE_TOUCH)
-> child.ACTION_UP
-> chid.stopNestedScroll (TYPE_TOUCH)
-> parent.onStopNestedScroll (TYPE_TOUCH)
-> child.fling
-> child.startNestedScroll (TYPE_NON_TOUCH)
-> parent.onStartNestedScroll (TYPE_NON_TOUCH) (若是返回false,則流程終止)
-> parent.onNestedScrollAccepted (TYPE_NON_TOUCH)
-> child.dispatchNestedPreScroll (TYPE_NON_TOUCH)
-> parent.onNestedPreScroll (TYPE_NON_TOUCH)
-> child.dispatchNestedScroll (TYPE_NON_TOUCH)
-> parent.onNestedScroll (TYPE_NON_TOUCH)
-> child.stopNestedScroll (TYPE_NON_TOUCH)
-> parent.onStopNestedScroll (TYPE_NON_TOUCH)
如上所示,當 RV 開始 Fling 時,每一幀 Fling 的距離,都會通知到 Parent2,由 Parent2 判斷是否攔截處理,那麼咱們就不須要本身使用 OverScroller 來模擬慣性滑動了,代碼能夠更少。具體實現以下:
public class NestedScrollLayout2 extends FrameLayout implements NestedScrollingParent2 { private View mChildView; /** * 最外層的RecyclerView */ private RecyclerView mRootList; /** * 子RecyclerView */ private RecyclerView mChildList; private NestedViewModel mScrollViewModel; private int mAxes; public NestedScrollLayout2(@NonNull Context context) { super(context); } public NestedScrollLayout2(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public void setTarget(LifecycleOwner target) { if (target instanceof FragmentActivity) { mScrollViewModel = ViewModelProviders.of((FragmentActivity) target).get(NestedViewModel.class); } else if (target instanceof Fragment) { mScrollViewModel = ViewModelProviders.of((Fragment) target).get(NestedViewModel.class); } else { throw new IllegalArgumentException("target must be FragmentActivity or Fragment"); } mScrollViewModel.getChildView().observe(target, new Observer<View>() { @Override public void onChanged(@Nullable View view) { mChildView = view; } }); mScrollViewModel.getChildList().observe(target, new Observer<View>() { @Override public void onChanged(@Nullable View view) { mChildList = (RecyclerView) view; } }); } public void setRootList(RecyclerView recyclerView) { mRootList = recyclerView; } @Override public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) { return axes == ViewCompat.SCROLL_AXIS_VERTICAL; } @Override public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) { mAxes = axes; } @Override public void onStopNestedScroll(@NonNull View target, int type) { mAxes = SCROLL_AXIS_NONE; } @Override public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) { } @Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { if (mChildView == null) { return; } if (target == mRootList) { onParentScrolling(mChildView.getTop(), dy, consumed); } else { onChildScrolling(mChildView.getTop(), dy, consumed); } } /** * 父列表在滑動 * * @param childTop * @param dy * @param consumed */ private void onParentScrolling(int childTop, int dy, int[] consumed) { //列表已經置頂 if (childTop == 0) { if (dy > 0 && mChildList != null) { //還在向下滑動,此時滑動子列表 mChildList.scrollBy(0, dy); consumed[1] = dy; } else { if (mChildList != null && mChildList.canScrollVertically(dy)) { consumed[1] = dy; mChildList.scrollBy(0, dy); } } } else { if (childTop < dy) { consumed[1] = dy - childTop; } } } private void onChildScrolling(int childTop, int dy, int[] consumed) { if (childTop == 0) { if (dy < 0) { //向上滑動 if (!mChildList.canScrollVertically(dy)) { consumed[1] = dy; mRootList.scrollBy(0, dy); } } } else { if (dy < 0 || childTop > dy) { consumed[1] = dy; mRootList.scrollBy(0, dy); } else { //dy大於0 consumed[1] = dy; mRootList.scrollBy(0, childTop); } } } @Override public int getNestedScrollAxes() { return mAxes; } }
有人可能會問,既然有新 API,爲啥還要用 OverScroller。
由於,咱們項目工程裏的 RV 版本較低,沒有實現 NestedScrollingChild2,而新版本的 RV 已經實現了Child2,因此,你們有空必定要多升級 Support,真的好用。
最後獻上Demo地址,歡迎你們參考。
Demo地址:
https://github.com/xue5455/Ne...