咱們先來看一下DEMO的效果,直觀的感覺一下什麼是嵌套滾動:java
在解釋上圖涉及到哪些嵌套滑動操做以前,我先貼一下嵌套佈局的xml結構:android
<com.wzy.nesteddetail.view.NestedWebViewRecyclerViewGroup> <com.wzy.nesteddetail.view.NestedScrollWebView/> <TextView /> <android.support.v7.widget.RecyclerView /> </com.wzy.nesteddetail.view.NestedWebViewRecyclerViewGroup>
其中:git
如今咱們來講明一下,簡單的DEMO效果中包含了哪些嵌套滑動操做:github
再不知道NestedScrolling機制以前,我相信大部分人想實現上面的滑動效果都是比較頭大的,特別是滑動距離和速度要從WebView->外層容器->RecyclerView而且還要支持反向傳遞。
有了Google提供的牛逼嵌套滑動機制,再加上這篇文章粗淺的科普,我相信大部分人都可以實現這種滑動效果。這種效果最多見的應用場景就是各類新聞客戶端的詳情頁。web
Android在support.v4包中提供了用於View支持嵌套滑動的兩個接口:ide
我先用比較白話的語言介紹一下NestedScrolling的工做原理:佈局
NestedScrollingParent和NestedScrollingChild的源碼定義也是爲了配合滑動實現定義出來的:this
NestedScrollingChildspa
void setNestedScrollingEnabled(boolean enabled); // 設置是否開啓嵌套滑動 boolean isNestedScrollingEnabled(); // 得到設置開啓了嵌套滑動 boolean startNestedScroll(@ScrollAxis int axes); // 沿給定的軸線開始嵌套滾動 void stopNestedScroll(); // 中止當前嵌套滾動 boolean hasNestedScrollingParent(); // 若是有ns parent,返回true boolean dispatchNestedPreScroll(int dx , int dy , @Nullable int[] consumed , @Nullable int[] offsetInWindow); // 消費滑動時間前,先讓ns parent消費 boolean dispatchNestedScroll(int dxConsumed , int dyConsumed , int dxUnconsumed , int dyUnconsumed , @Nullable int[] offsetInWindow); // ns parent消費ns child剩餘滾動後是否還有剩餘。return true表明還有剩餘 boolean dispatchNestedPreFling(float velocityX , float velocityY); // 消費fly速度前,先讓ns parent消費 boolean dispatchNestedFling(float velocityX , float velocityY , boolean consumed); // ns parent消費ns child消費後的速度以後是否還有剩餘。return true表明還有剩餘
NestedScrollingParent.net
boolean onStartNestedScroll(@NonNull View var1 , @NonNull View var2 , int var3); // 決定是否接收子View的滾動事件 void onNestedScrollAccepted(@NonNull View var1 , @NonNull View var2 , int var3); // 響應子View的滾動 void onStopNestedScroll(@NonNull View var1); // 滾動結束的回調 void onNestedPreScroll(@NonNull View var1 , int var2 , int var3 , @NonNull int[] var4); // ns child滾動前回調 void onNestedScroll(@NonNull View var1 , int var2 , int var3 , int var4 , int var5); // ns child滾動後回調 boolean onNestedPreFling(@NonNull View var1 , float var2 , float var3); // ns child flying前回調 boolean onNestedFling(@NonNull View var1 , float var2 , float var3 , boolean var4); // ns child flying後回調 int getNestedScrollAxes(); // 返回當前佈局嵌套滾動的座標軸
Google爲了讓開發者更加方便的實現這兩個接口,提供了NestedScrollingParentHelper和NestedScrollingChildHelper這兩個輔助。因此實現NestedScrolling這兩個接口的經常使用寫法是:
ns child:
public class NestedScrollingWebView extends WebView implements NestedScrollingChild { private NestedScrollingChildHelper mChildHelper; private NestedScrollingChildHelper getNestedScrollingHelper() { if (mChildHelper == null) { mChildHelper = new NestedScrollingChildHelper(this); } return mChildHelper; } @Override public void setNestedScrollingEnabled(boolean enabled) { getNestedScrollingHelper().setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return getNestedScrollingHelper().isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return getNestedScrollingHelper().startNestedScroll(axes); } @Override public void stopNestedScroll() { getNestedScrollingHelper().stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return getNestedScrollingHelper().hasNestedScrollingParent(); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) { return getNestedScrollingHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) { return getNestedScrollingHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return getNestedScrollingHelper().dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return getNestedScrollingHelper().dispatchNestedPreFling(velocityX, velocityY); } @Override public boolean startNestedScroll(int axes, int type) { return getNestedScrollingHelper().startNestedScroll(axes, type); } @Override public void stopNestedScroll(int type) { getNestedScrollingHelper().stopNestedScroll(type); } @Override public boolean hasNestedScrollingParent(int type) { return getNestedScrollingHelper().hasNestedScrollingParent(type); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type) { return getNestedScrollingHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) { return getNestedScrollingHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); } }
ns parent:
public class NestedScrollingDetailContainer extends ViewGroup implements NestedScrollingParent { private NestedScrollingParentHelper mParentHelper; private NestedScrollingParentHelper getNestedScrollingHelper() { if (mParentHelper == null) { mParentHelper = new NestedScrollingParentHelper(this); } return mParentHelper; } @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public int getNestedScrollAxes() { return getNestedScrollingHelper().getNestedScrollAxes(); } @Override public void onNestedScrollAccepted(View child, View target, int axes) { getNestedScrollingHelper().onNestedScrollAccepted(child, target, axes); } @Override public void onStopNestedScroll(View child) { getNestedScrollingHelper().onStopNestedScroll(child); } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { // 處理預先flying事件 return false; } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { // 處理後續flying事件 return false; } @Override public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { // 處理後續scroll事件 } @Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed) { // 處理預先滑動scroll事件 } }
只知道原理你們確定是不知足的,結合原理進行實操纔是關鍵。這裏以DEMO的效果爲例,想要實現DEMO的效果,須要自定義兩個嵌套滑動容器:
外部滑動容器
在實現外部滑動的容器的時候,咱們首先須要考慮這個外部滑動容器的滑動閾值是什麼?
答: 外部滑動的滑動閾值=外部容器中全部子View的高度-外部容器的高度。同理相似WebView的滑動閾值=WebView的內容高度-WebView的容器高度。
對應代碼實現:
private int mInnerScrollHeight; // 可滑動的最大距離 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width; int widthMode = MeasureSpec.getMode(widthMeasureSpec); int measureWidth = MeasureSpec.getSize(widthMeasureSpec); int measureHeight = MeasureSpec.getSize(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY) { width = measureWidth; } else { width = mScreenWidth; } int left = getPaddingLeft(); int right = getPaddingRight(); int top = getPaddingTop(); int bottom = getPaddingBottom(); int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); LayoutParams params = child.getLayoutParams(); int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, left + right, params.width); int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, top + bottom, params.height); measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec); } setMeasuredDimension(width, measureHeight); findWebView(this); findRecyclerView(this); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childTotalHeight = 0; mInnerScrollHeight = 0; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); child.layout(0, childTotalHeight, childWidth, childHeight + childTotalHeight); childTotalHeight += childHeight; mInnerScrollHeight += childHeight; } mInnerScrollHeight -= getMeasuredHeight(); }
其次,須要考慮當WebView傳遞上滑事件和RecyclerView傳遞下滑事件時如何處理:
對應的WebView傳遞上滑速度和RecyclerView傳遞下滑速度,處理和Scroll傳遞相似。
對應代碼實現:
@Override public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) { boolean isWebViewBottom = !canWebViewScrollDown(); boolean isCenter = isParentCenter(); if (dy > 0 && isWebViewBottom && getScrollY() < getInnerScrollHeight()) { //爲了WebView滑動到底部,繼續向下滑動父控件 scrollBy(0, dy); if (consumed != null) { consumed[1] = dy; } } else if (dy < 0 && isCenter) { //爲了RecyclerView滑動到頂部時,繼續向上滑動父控件 scrollBy(0, dy); if (consumed != null) { consumed[1] = dy; } } if (isCenter && !isWebViewBottom) { //異常狀況的處理 scrollToWebViewBottom(); } } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { if (target instanceof NestedScrollingWebView) { //WebView滑到底部時,繼續向下滑動父控件和RV mCurFlyingType = FLYING_FROM_WEBVIEW_TO_PARENT; parentFling(velocityY); } else if (target instanceof RecyclerView && velocityY < 0 && getScrollY() >= getInnerScrollHeight()) { //RV滑動到頂部時,繼續向上滑動父控件和WebView,這裏用於計算到達父控件的頂部時RV的速度 mCurFlyingType = FLYING_FROM_RVLIST_TO_PARENT; parentFling((int) velocityY); } else if (target instanceof RecyclerView && velocityY > 0) { mIsRvFlyingDown = true; } return false; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { int currY = mScroller.getCurrY(); switch (mCurFlyingType) { case FLYING_FROM_WEBVIEW_TO_PARENT://WebView向父控件滑動 if (mIsRvFlyingDown) { //RecyclerView的區域的fling由本身完成 break; } scrollTo(0, currY); invalidate(); checkRvTop(); if (getScrollY() == getInnerScrollHeight() && !mIsSetFlying) { //滾動到父控件底部,滾動事件交給RecyclerView mIsSetFlying = true; recyclerViewFling((int) mScroller.getCurrVelocity()); } break; case FLYING_FROM_PARENT_TO_WEBVIEW://父控件向WebView滑動 scrollTo(0, currY); invalidate(); if (currY <= 0 && !mIsSetFlying) { //滾動父控件頂部,滾動事件交給WebView mIsSetFlying = true; webViewFling((int) -mScroller.getCurrVelocity()); } break; case FLYING_FROM_RVLIST_TO_PARENT://RecyclerView向父控件滑動,fling事件,單純用於計算速度。RecyclerView的flying事件傳遞最終會轉換成Scroll事件處理. if (getScrollY() != 0) { invalidate(); } else if (!mIsSetFlying) { mIsSetFlying = true; //滑動到頂部時,滾動事件交給WebView webViewFling((int) -mScroller.getCurrVelocity()); } break; } } }
最後,咱們須要考慮,若是用戶觸摸的是內部的一個不可滑動View時,這時事件是無法經過NestedScrolling機制傳遞給自身的。因此須要主動攔截這種事件,攔截標準:
相應代碼以下:
private int mLastY; @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mLastY = (int) event.getY(); break; case MotionEvent.ACTION_MOVE: if (mLastY == 0) { mLastY = (int) event.getY(); return true; } int y = (int) event.getY(); int dy = y - mLastY; mLastY = y; scrollBy(0, -dy); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mLastY = 0; break; } return true; } private int mLastMotionY; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: // 攔截落在不可滑動子View的MOVE事件 final int y = (int) ev.getY(); final int yDiff = Math.abs(y - mLastMotionY); boolean isInNestedChildViewArea = isTouchNestedInnerView((int)ev.getRawX(), (int)ev.getRawY()); if (yDiff > TOUCH_SLOP && !isInNestedChildViewArea) { mIsBeingDragged = true; mLastMotionY = y; final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } break; case MotionEvent.ACTION_DOWN: mLastMotionY = (int) ev.getY(); mIsBeingDragged = false; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsBeingDragged = false; break; } return mIsBeingDragged; } private boolean isTouchNestedInnerView(int x, int y) { List<View> innerView = new ArrayList<>(); if (mChildWebView != null) { innerView.add(mChildWebView); } if (mChildRecyclerView != null) { innerView.add(mChildRecyclerView); } for (View nestedView : innerView) { if (nestedView.getVisibility() != View.VISIBLE) { continue; } int[] location = new int[2]; nestedView.getLocationOnScreen(location); int left = location[0]; int top = location[1]; int right = left + nestedView.getMeasuredWidth(); int bottom = top + nestedView.getMeasuredHeight(); if (y >= top && y <= bottom && x >= left && x <= right) { return true; } } return false; }
實現一個支持嵌套滑動的WebView
自己WebView是不支持嵌套滑動的,想要支持嵌套滑動,咱們須要讓WebView實現NestedScrollingChild接口,而且處理好TouchEvent方法中的事件傳遞。
實現NestedScrollingChild接口比較簡單,上面也介紹過了,可使用Google提供的NestedScrollingChildHelper輔助類。
處理TouchEvent的思路,須要遵循如下步驟:
WebView最大滑動距離=WebView自身內容的高度-WebView容器的高度
思路比較簡單,咱們看一下對應的核心代碼:
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mWebViewContentHeight = 0; mLastY = (int) event.getRawY(); mFirstY = mLastY; if (!mScroller.isFinished()) { mScroller.abortAnimation(); } initOrResetVelocityTracker(); mIsSelfFling = false; mHasFling = false; mMaxScrollY = getWebViewContentHeight() - getHeight(); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); if (getParent() != null) { getParent().requestDisallowInterceptTouchEvent(true); } break; case MotionEvent.ACTION_MOVE: initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(event); int y = (int) event.getRawY(); int dy = y - mLastY; mLastY = y; if (getParent() != null) { getParent().requestDisallowInterceptTouchEvent(true); } if (!dispatchNestedPreScroll(0, -dy, mScrollConsumed, null)) { scrollBy(0, -dy); } if (Math.abs(mFirstY - y) > TOUCHSLOP) { //屏蔽WebView自己的滑動,滑動事件本身處理 event.setAction(MotionEvent.ACTION_CANCEL); } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: if (isParentResetScroll() && mVelocityTracker != null) { mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int yVelocity = (int) -mVelocityTracker.getYVelocity(); recycleVelocityTracker(); mIsSelfFling = true; flingScroll(0, yVelocity); } break; } super.onTouchEvent(event); return true; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { final int currY = mScroller.getCurrY(); if (!mIsSelfFling) { // parent flying scrollTo(0, currY); invalidate(); return; } if (isWebViewCanScroll()) { scrollTo(0, currY); invalidate(); } if (!mHasFling && mScroller.getStartY() < currY && !canScrollDown() && startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) && !dispatchNestedPreFling(0, mScroller.getCurrVelocity())) { //滑動到底部時,將fling傳遞給父控件和RecyclerView mHasFling = true; dispatchNestedFling(0, mScroller.getCurrVelocity(), false); } } }
NestedScrolling機制看似複雜,但其實就是實現兩個接口的事情,並且Google提供了強大的輔助類Helper來幫助咱們實現接口。這種機制將滑動事件的傳遞封裝起來,經過Helper輔助類實現ns parent和ns child之間的鏈接和交互。經過接口回調,也實現了ns parent和ns child的解耦。
DEMO項目連接:Android-NestedDetail