若是在之前要實現嵌套滑動,好比ScrollView嵌套RecyclerView,這時候經常使用的方法就是重寫onMeasure方法,進行從新測量。如今官方提供了兩個神奇的接口,幫助咱們實現複雜的嵌套以及更加炫酷的效果,那就是NestedScrollingChild2
和NestedScrollingParent2
。java
咱們先來看下要實現的效果:android
其實這是一個RecyclerView中一個模版是帶有RecyclerView的佈局,也就是說 RecyclerView嵌套RecyclerView。git
那麼如何才能作到像上圖的中效果呢,咱們來簡單分析一下:github
假如咱們用事件分發的思路去分析,首先當父RecylerView滑動到子RecyclerView時,父RecyclerView不該該再攔截滑動事件,當子RecyclerView從頂部往下滑動時,父RecyclerView要攔截子RecyclerView的滑動事件。可是如何判斷分發的時機呢,又要確保如此的流暢。數組
NestedScrolling機制
能夠很好的幫咱們去實現這樣的效果。bash
咱們知道RecyclerView
是實現了NestedScrollingChild2
接口的,咱們來看下它的幾個方法:app
/**
* 開啓一個嵌套滑動
*
* @param axes 支持的嵌套滑動方法,分爲水平方向,豎直方向,或不指定
* @param type 滑動事件類型
* @return 若是返回true, 表示當前子控件已經找了一塊兒嵌套滑動的view
*/
public boolean startNestedScroll(int axes, int type) {}
/**
* 在子控件滑動前,將事件分發給父控件,由父控件判斷消耗多少
*
* @param dx 水平方向嵌套滑動的子控件想要變化的距離 dx<0 向右滑動 dx>0 向左滑動
* @param dy 垂直方向嵌套滑動的子控件想要變化的距離 dy<0 向下滑動 dy>0 向上滑動
* @param consumed 子控件傳給父控件數組,用於存儲父控件水平與豎直方向上消耗的距離,consumed[0] 水平消耗的距離,consumed[1] 垂直消耗的距離
* @param offsetInWindow 子控件在當前window的偏移量
* @return 若是返回true, 表示父控件已經消耗了
*/
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) {}
/**
* 當父控件消耗事件後,子控件處理後,又繼續將事件分發給父控件,由父控件判斷是否消耗剩下的距離。
*
* @param dxConsumed 水平方向嵌套滑動的子控件滑動的距離(消耗的距離)
* @param dyConsumed 垂直方向嵌套滑動的子控件滑動的距離(消耗的距離)
* @param dxUnconsumed 水平方向嵌套滑動的子控件未滑動的距離(未消耗的距離)
* @param dyUnconsumed 垂直方向嵌套滑動的子控件未滑動的距離(未消耗的距離)
* @param offsetInWindow 子控件在當前window的偏移量
* @return 若是返回true, 表示父控件又繼續消耗了
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {}
/**
* 子控件中止嵌套滑動
*/
public void stopNestedScroll(int type) {}
/**
* 當子控件產生fling滑動時,判斷父控件是否處攔截fling,若是父控件處理了fling,那子控件就沒有辦法處理fling了。
*
* @param velocityX 水平方向上的速度 velocityX > 0 向左滑動,反之向右滑動
* @param velocityY 豎直方向上的速度 velocityY > 0 向上滑動,反之向下滑動
* @return 若是返回true, 表示父控件攔截了fling
*/
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {}
/**
* 當父控件不攔截子控件的fling,那麼子控件會調用該方法將fling,傳給父控件進行處理
*
* @param velocityX 水平方向上的速度 velocityX > 0 向左滑動,反之向右滑動
* @param velocityY 豎直方向上的速度 velocityY > 0 向上滑動,反之向下滑動
* @param consumed 子控件是否能夠消耗該fling,也能夠說是子控件是否消耗掉了該fling
* @return 父控件是否消耗了該fling
*/
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {}
/**
* 設置當前子控件是否支持嵌套滑動,若是不支持,那麼父控件是不可以響應嵌套滑動的
*
* @param enabled true 支持
*/
public void setNestedScrollingEnabled(boolean enabled) {}
/**
* 當前子控件是否支持嵌套滑動
*/
public boolean isNestedScrollingEnabled() {}
/**
* 判斷當前子控件是否擁有嵌套滑動的父控件
*/
public boolean hasNestedScrollingParent(int type) {}
複製代碼
咱們看源碼裏會發現其實裏面的實現都是經過NestedScrollingChildHelper
去實現的。ide
那麼它是怎麼調用Child2中的方法以及怎麼回調給NestedScrollingParent的各類方法呢?佈局
咱們來看下RecyclerView的onTouchEvent
方法:ui
public boolean onTouchEvent(MotionEvent e) {
.....
int nestedScrollAxis;
switch(action) {
case MotionEvent.ACTION_DOWN:
this.mScrollPointerId = e.getPointerId(0);
this.mInitialTouchX = this.mLastTouchX = (int)(e.getX() + 0.5F);
this.mInitialTouchY = this.mLastTouchY = (int)(e.getY() + 0.5F);
nestedScrollAxis = 0;
if (canScrollHorizontally) {
nestedScrollAxis |= 1;
}
if (canScrollVertically) {
nestedScrollAxis |= 2;
}
this.startNestedScroll(nestedScrollAxis, 0);
break;
case MotionEvent.ACTION_UP:
.....
if (xvel == 0.0F && yvel == 0.0F || !this.fling((int)xvel, (int)yvel)) {
this.setScrollState(0);
}
this.resetTouch();
break;
case MotionEvent.ACTION_MOVE:
nestedScrollAxis = e.findPointerIndex(this.mScrollPointerId);
if (nestedScrollAxis < 0) {
return false;
}
int x = (int)(e.getX(nestedScrollAxis) + 0.5F);
int y = (int)(e.getY(nestedScrollAxis) + 0.5F);
int dx = this.mLastTouchX - x;
int dy = this.mLastTouchY - y;
if (this.dispatchNestedPreScroll(dx, dy, this.mScrollConsumed, this.mScrollOffset, 0)) {
dx -= this.mScrollConsumed[0];
dy -= this.mScrollConsumed[1];
vtev.offsetLocation((float)this.mScrollOffset[0], (float)this.mScrollOffset[1]);
int[] var10000 = this.mNestedOffsets;
var10000[0] += this.mScrollOffset[0];
var10000 = this.mNestedOffsets;
var10000[1] += this.mScrollOffset[1];
}
...
NestedScrollingChildHelperNestedScrollingChildHelperNestedScrollingChildHelperNestedScrollingChildHelperNestedScrollingChildHelper;
}
if (!eventAddedToVelocityTracker) {
this.mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
} else {
return false;
}
}
複製代碼
從上面的代碼中能夠看出ACTION_DOWN
調用了startNestedScroll
方法;ACTION_MOVE
調用了 dispatchNestedPreScroll;ACTION_UP
調用了fling
咱們再來看下NestedScrollingChildHelper
中startNestedScroll
的實現方法:
public boolean startNestedScroll(int axes, int type) {
if (this.hasNestedScrollingParent(type)) {
// Already in progress
return true;
} else {
if (this.isNestedScrollingEnabled()) {
ViewParent p = this.mView.getParent();
for(View child = this.mView; p != null; p = p.getParent()) {
if (ViewParentCompat.onStartNestedScroll(p, child, this.mView, axes, type)) {
this.setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, this.mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View)p;
}
}
}
return false;
}
}
複製代碼
ViewParentCompat中的代碼我就不貼了,其實這段代碼就是去尋找父類的NestedScrollingParent,若是找到就會回調onStartNestedScroll和onNestedScrollAccepted
咱們簡單看下NestedScrollingParent2的實現方法:
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes, int type);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes, int type);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();
複製代碼
一樣的,咱們也能夠在NestedScrollingChildHelper看到其餘方法的實現:dispatchNestedPreScroll中會回調onNestedPreScroll方法。
在RecyclerView中的scrollByInternal
中調用了dispatchNestedScroll
, 在dispatchNestedScroll
中會回調onNestedScroll
。
fling方法中會回調onNestedPreFling和onNestedFling方法。
resetTouch方法中則會回調onStopNestedScroll。
我畫了一個簡單的示意圖來幫助梳理下我上面說到的這些:
看了上面的實現原理,咱們來實現下gif圖的效果。
首先,實現NestedScrollingParent2
接口,建立一個NestedScrollingParentHelper
實現類。
public NestedContainerRecyclerView(
@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
nestedScrollingParentHelper = new NestedScrollingParentHelper(this);
setNestedScrollingEnabled(false);
setOverScrollMode(OVER_SCROLL_NEVER);
}
複製代碼
默認不開啓嵌套滑動。
@Override
public boolean onStartNestedScroll(
@NonNull View child, @NonNull View target, int axes, int type) {
if (axes == ViewCompat.SCROLL_AXIS_HORIZONTAL) {
return false;
}
this.mTouchType = type;
targetChild = new WeakReference<View>(target);
itemChild = new WeakReference<View>(getItemChild(child));
return true;
}
複製代碼
在開始嵌套滑動的時候,判斷一下滑動的方向,若是是水平方向,不進行嵌套滑動。targetChild
用於保存當前的可滑動View,itemChild
用於保存可滑動的View當前的佈局,看下getItemChild的實現,就懂了。
private View getItemChild(View target) {
ViewParent parent = target.getParent();
if (parent == null) {
return null;
}
if (parent == this) {
return target;
} else if (parent instanceof View) {
return getItemChild((View) parent);
}
return null;
}
複製代碼
接下來就到了咱們的重頭戲登場的時候了,鐺鐺鐺:
public void onNestedScroll(
@NonNull View target,
int dxConsumed,
int dyConsumed,
int dxUnconsumed,
int dyUnconsumed,
int type) {
if (dyUnconsumed == 0) {
return;
}
if (type == ViewCompat.TYPE_TOUCH) {
if (dyUnconsumed > 0) {
if ((!target.canScrollVertically(1) || getItemChildY() != itemTargetY)
&& canScrollVertically(1)) {
scrollBy(0, dyUnconsumed);
}
} else {
if ((!target.canScrollVertically(-1) || getItemChildY() != itemTargetY)
&& canScrollVertically(-1)) {
scrollBy(0, dyUnconsumed);
}
}
} else if (type == ViewCompat.TYPE_NON_TOUCH) {
if (dyUnconsumed > 0) {
boolean canscroll = target.canScrollVertically(1);
if ((!canscroll || getItemChildY() != itemTargetY) && canScrollVertically(1)) {
scrollBy(0, dyUnconsumed);
}
} else {
boolean canscroll = target.canScrollVertically(-1);
if ((!canscroll || getItemChildY() != itemTargetY) && canScrollVertically(-1)) {
scrollBy(0, dyUnconsumed);
if (getItemChildY() > getHeight() + dyUnconsumed) {
fling(0, dyUnconsumed * VELUE);
}
}
}
}
}
複製代碼
這裏呢,補充一個小知識點,就是判斷一個view是否到頂或者到底的方法:
canScrollVertically(1)的值表示是否能向上滾動,false表示已經滾動到底部
canScrollVertically(-1)的值表示是否能向下滾動,false表示已經滾動到頂部
複製代碼
在onNestedScroll時,若是是向上滑動,當前RecyclerView並未滑動到底部而且可滑動View(之後簡稱targetView)已經滑動到底部或者targe當前全部的View的getY(相對於父控件的y軸距離)不等於itemTargetY時,當前RecyclerView消費點剩餘的距離。
itemTargetY是什麼呢,咱們在佈局的時候,targetView全部的佈局不必定充滿RecyclerView,可能上方會有必定的距離,這個距離就是itemTargetY。
那麼getItemChildY
裏是怎麼實現的呢?看下面:
private int getItemChildY() {
if (itemChild == null || itemChild.get() == null) {
return -10000;
}
return (int) (itemChild.get().getY() + 0.5f);
}
複製代碼
同理,在向下滑動的時候,咱們經過targetView是否到底以及當前RecyclerView是否到底來判斷是否消費剩餘的滑動距離。
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, int[] consumed, int type) {
if (dy != 0) {
if (type == ViewCompat.TYPE_TOUCH && itemChild != null && itemChild.get() != null) {
int y = getItemChildY();
if (dy > 0) {
if (y != itemTargetY) {
if (!canScrollVertically(1)) {
scrollBy(0, y - itemTargetY);
consumed[1] = y - itemTargetY;
} else {
scrollBy(0, dy);
y -= getItemChildY();
consumed[1] = y;
}
} else if (!target.canScrollVertically(1) && canScrollVertically(1)) {
scrollBy(0, dy);
y -= getItemChildY();
consumed[1] = y;
}
} else {
if (target instanceof RecyclerView) {
RecyclerView tRecyclerView = (RecyclerView) target;
if ((tRecyclerView.computeVerticalScrollOffset() <= 0
|| y - itemTargetY > 1)
&& canScrollVertically(-1)) {
scrollBy(0, dy);
y -= getItemChildY();
consumed[1] = y;
}
} else {
if ((!target.canScrollVertically(-1) || y - itemTargetY > 1)
&& canScrollVertically(-1)) {
scrollBy(0, dy);
y -= getItemChildY();
consumed[1] = y;
}
}
}
}
if (type == ViewCompat.TYPE_NON_TOUCH && itemChild != null && itemChild.get() != null) {
int y = getItemChildY();
if (dy > 0) {
if ((!target.canScrollVertically(1) || y != itemTargetY)
&& canScrollVertically(1)) {
scrollBy(0, dy);
y -= getItemChildY();
consumed[1] = y;
}
} else {
if ((!target.canScrollVertically(-1) || y != itemTargetY)
&& canScrollVertically(-1)) {
scrollBy(0, dy);
y -= getItemChildY();
consumed[1] = y;
}
}
}
}
}
複製代碼
在onNestedPreScoll中,咱們判斷,若是是上滑時,判斷targeView是否到底以及RecyclerView是否到底來判斷是否消費dy,若是targetView到底,RecyclerView並無到底,則消費dy,若是targetView所在View的getY不等於itemTargetY且RecyclerView到底,則消費dy-itemTargetY。
此外,還處理了onNestedFling以及dispatchNestedScroll,更詳細的代碼請見github地址。 AndroidHighlights
注意:要準確設置子RecyclerView的高度,也就是targetView的高度,推薦高度爲
recyclerView.getHeight - itemTargetY
複製代碼
若有錯誤或者不一樣意見,歡迎指出、探討。感謝閱讀到這裏的全部人。