写点什么

Android 嵌套滑动总结,android 项目驱动式开发教程

用户头像
Android架构
关注
发布于: 20 分钟前

xmlns:app="http://schemas.android.com/apk/res-auto"


xmlns:tools="http://schemas.android.com/tools"


android:layout_width="match_parent"


android:layout_height="match_parent">


<com.google.android.material.appbar.AppBarLayout


android:layout_height="300dp"


android:layout_width="match_parent">


// 可滑动部分


<View


android:layout_width="match_parent"


android:layout_height="0dp"


android:layout_weight="1"


app:layout_scrollFlags="scroll"/>


<TextView


android:layout_width="match_parent"


android:layout_height="64dp"


android:layout_gravity="bottom"


android:text="Top"


android:textSize="32sp"


android:textColor="@color/white"


android:gravity="center"


android:textStyle="bold"/>


</com.google.android.material.appbar.AppBarLayout>


<androidx.recyclerview.widget.RecyclerView


android:id="@+id/rv"


android:layout_width="match_parent"


android:layout_height="wrap_content"


app:layout_behavior="@string/appbar_scrolling_view_behavior"/>


</androidx.coordinatorlayout.widget.CoordinatorLayout>


AppBarLayout 中需要上滑隐藏的部分的 scrollFlag 指定为 scroll ,在 RecyclerView 中指定 behaviorappbar_scrolling_view_behavior 就可以实现最简单的吸顶嵌套滑动,如下:



看起来像带有 header 的 RecyclerView 在滑动,但其实是嵌套滑动。


layout_scrollFlagslayout_behavior 有很多可选值,配合起来可以实现多种效果,不只限于嵌套滑动。具体可以参考 API 文档。


使用 CoordinatorLayout 实现嵌套滑动比手动实现要好得多,既可以实现连贯的吸顶嵌套滑动,又支持 fling。而且是官方提供的布局,可以放心使用,出 bug 的几率很小,性能也不会有问题。不过也正是因为官方将其封装得很好,使用 CoordinatorLayout 很难实现比较复杂的嵌套滑动布局,比如多级嵌套滑动。

[](

)3、嵌套滑动组件 NestedScrollingParent 和 NestedScrollingChild


NestedScrollingParentNestedScrollingChild 是 google 官方提供地一套专门用来解决嵌套滑动地组件。它们是两个接口,代码如下:


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);


}


需要嵌套滑动的 View 可以实现这两个接口,复写其中的方法。这套组件实现嵌套滑动的核心原理很简单,主要是以下三步:


  • NestedScrollingChildonTouchEvent 方法中先将 ACITON_MOVE 事件产生的位移 dx 和 dy 通过 dispatchNestedPreScroll 传递给 NestedScrollingParent

  • NestedScrollingParentonNestedPreScroll 中接受到 dx 和 dy 并进行消费。并将消费掉的位移放入 int[] consumed 中,consumed 数组是一个长度为 2 的 int 类型数组,consumed[0] 代表 x 轴的消耗,consumed[1] 代表 y 轴的消耗

  • NestedScrollingChild 之后从 int[] consumed 数组中拿到 NestedScrollingParent 已经消费掉的位移,减去之后得到剩余的位移,再由自己消费


滑动位移传递方向由 child -> parent -> child,如下图。如果 child 是 Recyclerview ,它会先把位移给父布局消费,这时父布局滑动。当父布局滑动顶到不能滑动时,Recyclerview 这时会消费全部位移,这时它自己开始滑动,这样就形成了嵌套滑动,效果正如之前的例子中所看到的。



dispatchNestedScrollonNestedScroll 的作用原理上述 preScroll 的方法类似,只不过这两个方法构造的嵌套滑动顺序和 preScroll 的相反,是子 View 先消费,子 View 消费不了的时候,再由父 View 再消费。


这套机制还支持 fling,在手指离开 view 的时候,即产生 ACITON_UP 事件时,child 将此时的 Velocity 转化为位移 dxdy,并重复之前的流程。通过 @NestedScrollType int type 的值来判断是 TYPE_TOUCH 还是 TYPE_NON_TOUCHTYPE_TOUCH 就是滑动, TYPE_NON_TOUCH 就是 fling。

[](

)Android 中哪些 View 使用了这套滑动机制?


  • 实现 NestedScrollingParent 接口的 View 有:NestedScrollViewCoordinatorLayoutMotionLayout

  • 实现 NestedScrollingChild 接口的 View 有:NestedScrollViewRecyclerView

  • NestedScrollView 是唯一同时实现两个接口的 View,这意味着它可以用作中介来实现多级嵌套滑动,后面会说到。


从上面可以看到,实际上,之前提到的 CoordinatorLayout 实现的嵌套滑动,本质上也是通过这套 NestedScrolling 接口来实现的。但是由于它封装得太好,我们没办法做过多定制。而直接使用这套接口,就可以根据自己的需求做定制。


大部分的场景中,我们不需要去实现 NestedScrollingChild 接口,因为 RecyclerView 已经做了这个实现,而涉及到嵌套滑动场景的子 View 基本也都是 RecyclerView。我们看看 RecyclerView 的相关源码:


public boolean onTouchEvent(MotionEvent e) {


...


case MotionEvent.ACTION_MOVE: {


...


// 计算 dx,dy


int dx = mLastTouchX - x;


int dy = mLastTouchY - y;


...


mReusableIntPair[0] = 0;


mReusableIntPair[1] = 0;


...


// 分发 preScroll


if (dispatchNestedPreScroll(


canScrollHorizontally ? dx : 0,


canScrollVertically ? dy : 0,


mReusableIntPair, mScrollOffset, TYPE_TOUCH


)) {


// 减去父 view 消费掉的位移


dx -= mReusableIntPair[0];


dy -= mReusableIntPair[1];


mNestedOffsets[0] += mScrollOffset[0];


mNestedOffsets[1] += mScrollOffset[1];


getParent().requestDisallowInterceptTouchEvent(true);


}


...


} break;


...


}


boolean scrollByInternal(int x, int y, MotionEvent ev) {


int unconsumedX = 0;


int unconsumedY = 0;


int consumedX = 0;


int consumedY = 0;


if (mAdapter != null) {


mReusableIntPair[0] = 0;


mReusableIntPair[1] = 0;


// 先消耗掉自己的 scroll


scrollStep(x, y, mReusableIntPair);


consumedX = mReusableIntPair[0];


consumedY = mReusableIntPair[1];


// 计算剩余的量


unconsumedX = x - consumedX;


unconsumedY = y - consumedY;


}


mReusableIntPair[0] = 0;


mReusableIntPair[1] = 0;


// 分发 nestedScroll 给父 View,顺序和 preScroll 刚好相反


dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,


TYPE_TOUCH, mReusableIntPair);


unconsumedX -= mReusableIntPair[0];


unconsumedY -= mReusableIntPair[1];


...


}


RecyclerView 是怎么调到父 View 的 onNestedPreSrollonNestedScroll 的呢?分析一下 dispatchNestedPreScroll 的代码,如下,dispatchNestedScroll 的代码原理和此类似,不再贴出:


public boolean dispatchNestedPre


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


Scroll(int dx, int dy, int[] consumed, int[] offsetInWindow,


int type) {


return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow,type);


}


// NestedScrollingChildHelper.java


public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,


@Nullable int[] offsetInWindow, @NestedScrollType int type) {


if (isNestedScrollingEnabled()) {


final ViewParent parent = getNestedScrollingParentForType(type);


if (parent == null) {


return false;


}


if (dx != 0 || dy != 0) {


...


consumed[0] = 0;


consumed[1] = 0;


ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);


...


}


...


}


return false;


}


// ViewCompat.java


public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,


int[] consumed, int type) {


if (parent instanceof NestedScrollingParent2) {


// First try the NestedScrollingParent2 API


((NestedScrollingParent2) parent).onNestedPreScroll(target, dx, dy, consumed, type);


} else if (type == ViewCompat.TYPE_TOUCH) {


// Else if the type is the default (touch), try the NestedScrollingParent API


if (Build.VERSION.SDK_INT >= 21) {


try {


parent.onNestedPreScroll(target, dx, dy, consumed);


} catch (AbstractMethodError e) {


Log.e(TAG, "ViewParent " + parent + " does not implement interface "


  • "method onNestedPreScroll", e);


}


} else if (parent instanceof NestedScrollingParent) {


((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);


}


}


}


可以看到,RecyclerView 通过一个代理类 NestedScrollingChildHelper 完成滑动分发,最后交给 ViewCompat 的静态方法来让父 View 处理 onNestedPreScrollViewCompat 的主要作用是用来兼容不同版本的滑动接口。

[](

)实现 onNestedPreScroll 方法


从上面的代码可以清楚地看到 RecyclerView 对于 NestedScrollingChild 的实现,以及触发嵌套滑动的时机。如果我们要实现嵌套滑动,并且内部的滑动子 View 是 RecyclerView,那么只需要让外层的父 View 实现 NestedScrollingParent 的方法就行了,比如在 onNestedPreScroll 方法中,


@Override


public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {


// 滑动 dy 距离


scrollBy(0, dy);


// 将消耗掉的 dy 放入 consumed 数组通知子 view


consumed[1] = dy;


}


这样就实现了最简单的嵌套滑动。当然,实际情况中,还要对滑动距离进行判断,不能让父 View 一直消费子 View 的位移。

[](

)关于 NestedScrollView


NestedScrollView 这样的类,由于它内部实现了 onNestedScroll,所以在下滑时,它能在内部的 RecyclerView 下滑直到列表顶端时,外层继续下滑而不用抬起手指。另外也实现了 onNestedPreScroll方法,只不过它在该方法中把滑动继续向上传递,自己没有消费,如下代码:


// NestedScrollView.java


public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,


int type) {


// 只分发了 preScroll 自己并没有消费。之所以能分发是因为 NestedScrollView 同时实现了 NestedScrollingChild 接口


dispatchNestedPreScroll(dx, dy, consumed, null, type);


}


@Override


public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,


int type) {


return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);


}


// NestedScrollingChildHelper.java


public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,


@Nullable int[] offsetInWindow, @NestedScrollType int type) {


if (isNestedScrollingEnabled()) {


final ViewParent parent = getNestedScrollingParentForType(type);


if (parent == null) {


return false;


}


if (dx != 0 || dy != 0) {


...


consumed[0] = 0;


consumed[1] = 0;


ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android 嵌套滑动总结,android项目驱动式开发教程