图解Android嵌套滑动一|NestedScrollingParent和Child
概述
在传统的事件分发机制中,通常来说父控件和子控件们只会有一个控件去处理事件流,但我们时常会有一些特殊的需求,比如说多个控件之间的滑动需要联动起来,这时候就需要用到嵌套滑动机制了。
NestedScrolling 是 Android 5.0 推出的嵌套滑动机制,它可以指定在一个滑动事件流中,父控件和子控件分别消费多少。比如,在一个顶部为 HeaderView,下方是 RecyclerView 列表的布局中,如果手指在 RecyclerView 上产生了向上的滑动事件,我们可以让 HeaderView 先消费滑动事件(比如把 HeaderView 的高度从 100 缩小到 40),然后列表再向上滑动。即 HeaderView 消费 60 的滑动事件,剩下的再交给列表处理。
❝NestedScrolling 机制是在原有的事件分发基础上,在 View 和 ViewGroup 的触摸/滑动过程中新增了一系列方法调用,达到嵌套滑动的效果,本质上还是依托 Android View 事件分发机制的。
❞
一提到嵌套滑动,我们可能会想到 CoordinatorLayout, AppBarLayout, Behavior 这些组件,在分析这些组件是怎么实现嵌套效果前,先来看看嵌套滑动的两个核心接口:NestedScrollingParent 和 NestedScrollingChild。掌握这俩组件后,即使对 CoordinatorLayout, AppBarLayout, Behavior 这些不了解,也足够处理嵌套滑动的场景了。
嵌套滑动的逻辑流程
首先看一下父控件(NestedScrollingParent) 和子控件(NestedScrollingChild) 之间嵌套滑动的流程:
事件分发时,父控件不拦截,由子控件处理; 子控件开始滑动前(收到 DOWN 事件),询问父控件是否要配合嵌套滑动,如果父控件返回不配合,则不会继续下面的步骤,否则继续; 子控件接收到 MOVE 事件后,先把滑动信息传给父控件,父控件消费部分/全部滑动距离,并通知子控件它消费的滑动距离; 子控件处理剩下的滑动距离,它消费全部/部分剩下的滑动距离后,把还剩下的滑动距离传给父控件处理; 如果子控件在滑动过程中还发生了惯性滑动,就先把惯性速度信息传给父控件,父控件可以选择消费/不消费惯性滑动,并告诉子控件它的消费结果; 如果父控件没有消费惯性事件,则子控件来决定消不消费,并把这个消费结果再次传给父控件,父控件根据需要返回消费结果。 触摸/Fling 事件结束后,通知父控件嵌套滑动流程结束。
流程图如下:
通过上述嵌套滑动机制,在一次滑动操作过程中父控件和子控件都可以对滑动事件作出响应。
NestedScrollingParent&NestedScrollingChild
接下来我们看看上面嵌套滑动流程图中,NestedScrollingParent 和 NestedScrollingChild 角色相关的方法,它们是 NestedScrolling 机制的基础。在 Android 中提供了一系列实现了这俩接口的控件:
NestedScrollingParent: 如 CoordinatorLayout,NestedScrollView 等,此接口应由希望支持嵌套滑动事件的父控件实现,此外还提供了 NestedScrollingParent2 和 NestedScrollingParent3 扩展功能的接口。 NestedScrollingChild: 如 RecyclerView,NestedScrollView 等,此接口应由希望支持嵌套滑动事件的子控件实现,此外还提供了 NestedScrollingChild2 和 NestedScrollingChild3 扩展功能的接口。
既然子 View 控件是嵌套滑动事件分发的起点,那么下面我们就从 NestedScrollingChild 接口看起。
❝嵌套滑动事件为什么是从子 View 开始的?从交互上来看,滑动事件是由用户的手势触发的,而这些手势最先作用在子 View 上。因此,子 View 先感知到用户的滑动需求,进而决定是自己处理还是分发该事件。
❞
NestedScrollingChild
public interface NestedScrollingChild {void setNestedScrollingEnabled(boolean enabled);boolean isNestedScrollingEnabled();boolean startNestedScroll(@ScrollAxis int axes);void stopNestedScroll();boolean hasNestedScrollingParent();boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow);boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);boolean dispatchNestedPreFling(float velocityX, float velocityY);}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);}public interface NestedScrollingChild3 extends NestedScrollingChild2 {void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @ViewCompat.NestedScrollType int type, @NonNull int[] consumed);}
主要方法如下:
setNestedScrollingEnabled 和 isNestedScrollingEnabled: 一对 get/set 方法,用来设置/判断子控件是否支持嵌套滑动。 startNestedScroll: 嵌套滑动起始方法,找到接收滑动信息的父控件,返回值表示父控件是否接受嵌套滑动流程。 stopNestedScroll: 嵌套滑动结束方法,清空嵌套滑动相关的状态。 dispatchNestedPreScroll: 在「子控件消费滑动事件前」把滑动信息分发给父控件。 dispatchNestedScroll: 在「子控件消费滑动事件后」把剩下的滑动距离信息分发给父控件。 dispatchNestedPreFling 和 dispatchNestedFling: 跟 Scroll 对应方法作用类似,不过分发的不是滑动信息而是 Fling 信息(惯性滑动)。
NestedScrollingChild2 和 NestedScrollingChild3 在 NestedScrollingChild 基础上有所扩展,比如新增了 type 类型参数,表示滑动的类型,取值有 TYPE_TOUCH 和 TYPE_NON_TOUCH:
TYPE_TOUCH: 输入类型来自用户触摸屏幕。 TYPE_NON_TOUCH: 输入类型不是由用户触摸屏幕引起的,一般来自 Fling 动作(惯性)。
NestedScrollingParent
public interface NestedScrollingParent {boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);void onStopNestedScroll(@NonNull View target);void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);int getNestedScrollAxes();}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 NestedScrollingParent3 extends NestedScrollingParent2 {void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @ViewCompat.NestedScrollType int type, @NonNull int[] consumed);}
父控件的大部分方法都是被子控件的对应方法回调的:
onStartNestedScroll: 子控件调用 startNestedScroll 方法时,会找到接收滑动信息的父控件,然后调用父控件的这个方法来确定父控件是否接收滑动信息,返回值表示父控件是否接受嵌套滑动流程。这个方法通常会根据 axes 来判断是垂直还是水平方向,进而返回 true 或 false。 onStopNestedScroll: 子控件调用 stopNestedScroll 方法时,会调用到父控件的这个方法,用来做一些收尾工作。 onNestedScrollAccepted: 当父控件确定接受嵌套滑动流程后该方法会被回调,可以让父控件针对嵌套滑动做一些前期工作。 onNestedPreScroll: 嵌套滑动的关键方法,子控件通过 dispatchNestedPreScroll 分发滑动信息后,该方法被调用,它用来接收「子控件处理滑动前」的滑动距离信息,在这里父控件可以优先响应滑动操作,可以根据需要来消费滑动距离,然后通过 consumed 参数把它消费的距离传回给子控件。 onNestedScroll: 子控件调用 dispatchNestedScroll 后,该方法被调用,它用来接收「子控件处理完滑动后」的滑动距离信息,父控件可以在这个方法里消费剩余的滑动距离。 getNestedScrollAxes: 返回嵌套滑动的方向,横向或竖向滑动。 onNestedPreFling 和 onNestedFling: 同上。
父控件通过 onNestedPreScroll 和 onNestedScroll 来接收子控件响应滑动前后的滑动距离信息,这两个方法是实现嵌套滑动效果的关键方法。
解释一下上面方法里的 child 和 target 分别是啥(在下文会通过源码解析其传值):
target: 当前的 NestedScrollingChild 控件; child: 包含 target 的 NestedScrollingParent 的直接子控件,所以 child 可能就是 target,也可能不是。
与 NestedScrollingChild 类似,NestedScrollingParent2 和 NestedScrollingParent3 也是对 NestedScrollingParent 功能的扩展,比如新增了 type 参数。
嵌套滑动的接口流程
结合具体的接口方法,看看上面的流程图:
❝需要注意的是上面的 NestedScrollingChild 和 NestedScrollingParent 都只是接口,具体的分发逻辑需要自己实现,但「官方提供了两个实现了相关逻辑的帮助类:NestedScrollingChildHelper 和 NestedScrollingParentHelper。通过调用这两个 Helper 类,可以实现这套嵌套联动逻辑」。
❞
NestedScrollingChildHelper
NestedScrollingChildHelper 对 NestedScrollingChild 接口的相关方法做了一个实现,一般来说,子控件可以直接调用 NestedScrollingChildHelper 内部的方法,来实现嵌套滑动事件的分发。
public class NestedScrollingChildHelper {public void setNestedScrollingEnabled(boolean enabled) {// ...}public boolean isNestedScrollingEnabled() {// ...}public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {// ...}public void stopNestedScroll(@NestedScrollType int type) {// ...}public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type,@Nullable int[] consumed) {// ...}public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,@Nullable int[] offsetInWindow, @NestedScrollType int type) {// ...}// ...}
构造方法
public NestedScrollingChildHelper(@NonNull View view) {mView = view;}
创建 NestedScrollingChildHelper 实例需要传入一个 View,这个 View 便是嵌套滑动的子控件。
setNestedScrollingEnabled
public void setNestedScrollingEnabled(boolean enabled) {if (mIsNestedScrollingEnabled) {ViewCompat.stopNestedScroll(mView);}mIsNestedScrollingEnabled = enabled;}public boolean isNestedScrollingEnabled() {return mIsNestedScrollingEnabled;}
用来设置当前子控件是否要支持嵌套滑动。
startNestedScroll
public boolean startNestedScroll(@ScrollAxis int axes) {return startNestedScroll(axes, TYPE_TOUCH);}public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {if (hasNestedScrollingParent(type)) {// 如果没有配合处理的 NestedScrollingParent,则直接返回return true;}// 如果当前控件开启了嵌套滑动支持,才会继续if (isNestedScrollingEnabled()) {ViewParent p = mView.getParent();View child = mView;// 逐一往上寻找能配合处理嵌套滑动的 NestedScrollingParentwhile (p != null) {// ViewParentCompat.onStartNestedScroll() 会判断 p 是否实现 NestedScrollingParent 接口// 如果是则调用其 onStartNestedScroll 方法,并返回 onStartNestedScroll 的 Boolean 结果// 否则返回 falseif (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {setNestedScrollingParentForType(type, p);// 如果 NestedScrollingParent 接受嵌套滑动事件,则调用其 onNestedScrollAccepted 方法ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);return true;}if (p instanceof View) {child = (View) p;}p = p.getParent();}}return false;}
因此可以看出 NestedScrollingParent 接口方法里的 child 和 target 分别是啥:
target: 当前的 NestedScrollingChild 控件; child: 包含 target 的 NestedScrollingParent 的直接子控件,所以 child 可能就是 target,也可能不是。
dispatchNestedPreScroll
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,@Nullable int[] offsetInWindow) {return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, TYPE_TOUCH);}public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,@Nullable int[] offsetInWindow, @NestedScrollType int type) {// 如果当前控件开启了嵌套滑动支持,才会继续if (isNestedScrollingEnabled()) {final ViewParent parent = getNestedScrollingParentForType(type);// 如果没有配合处理的 NestedScrollingParent,则直接返回if (parent == null) {return false;}if (dx != 0 || dy != 0) {int startX = 0;int startY = 0;if (offsetInWindow != null) {mView.getLocationInWindow(offsetInWindow);startX = offsetInWindow[0];startY = offsetInWindow[1];}if (consumed == null) {consumed = getTempNestedScrollConsumed();}consumed[0] = 0;consumed[1] = 0;// 调用 Parent 的 onNestedPreScroll 方法// 传入的 consumed 数组是个引用,且初始化 0ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);if (offsetInWindow != null) {mView.getLocationInWindow(offsetInWindow);offsetInWindow[0] -= startX;offsetInWindow[1] -= startY;}// consumed 有一个元素不为 0 则返回 true// 说明 Parent 有消费滑动距离return consumed[0] != 0 || consumed[1] != 0;} else if (offsetInWindow != null) {offsetInWindow[0] = 0;offsetInWindow[1] = 0;}}return false;}
主要作用是把滑动距离分发给 NestedScrollingParent 父控件,父控件需要把自己消费的滑动距离赋值给 consumed 数组。
其他 dispatchNestedScroll, dispatchNestedFling, dispatchNestedPreFling 逻辑都比较类似,都是按照前面说的嵌套滑动的逻辑流程,把事件分发给父控件,不再一一分析。
NestedScrollingParentHelper
public class NestedScrollingParentHelper {private int mNestedScrollAxesTouch;private int mNestedScrollAxesNonTouch;public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,@ScrollAxis int axes) {onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH);}public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,@ScrollAxis int axes, @NestedScrollType int type) {if (type == ViewCompat.TYPE_NON_TOUCH) {mNestedScrollAxesNonTouch = axes;} else {mNestedScrollAxesTouch = axes;}}@ScrollAxispublic int getNestedScrollAxes() {return mNestedScrollAxesTouch | mNestedScrollAxesNonTouch;}public void onStopNestedScroll(@NonNull View target) {onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);}public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) {if (type == ViewCompat.TYPE_NON_TOUCH) {mNestedScrollAxesNonTouch = ViewGroup.SCROLL_AXIS_NONE;} else {mNestedScrollAxesTouch = ViewGroup.SCROLL_AXIS_NONE;}}}
NestedScrollingParentHelper 逻辑比较简单,只提供对应 NestedScrollingParent 父控件相关的 axes 滑动方向字段的管理。
示例
接下来分别用「自定义嵌套滑动子控件」和「系统 RecyclerView」 实现下面的效果:
| 自定义子控件 | RecyclerView |
|---|---|
示例一:自定义子控件
在了解嵌套滑动的流程及父子控件接口的逻辑后,可以通过它来实现一个嵌套滑动的Demo: 顶部是个普通 View,底部是嵌套滑动子控件,父布局是嵌套滑动的父控件。 子控件接收到滑动事件后,分发给父控件,父控件控制顶部 View 的高度,直到顶部 View 高度达到最小高度后,父控件不再消费滑动距离,交给底部控件消费。 嵌套滑动效果比较简单,可以自己加一下其他效果,比如松手后自动吸附,响应fling事件等。 NestedScrollingChild 和 NestedScrollingParent 其实只是接口,定义了嵌套滑动的事件分发规范,但实际上的逻辑需要我们自己实现。 对于 NestedScrollingChild 的接口实现,官方有提供一个 NestedScrollingChildHelper 类,一般直接使用它就行。NestedScrollingChildHelper 内部会和 NestedScrollingParent 进行交互。 对于 NestedScrollingParent 的实现,官方也有提供一个 NestedScrollingParentHelper 类,内部维护了一些状态。 具体的滑动行为,开发者可以根据具体需求自己来实现。
「布局 XML」:
<?xml version="1.0" encoding="utf-8"?><com.hearing.demo.nested.v1.NestedContainerView xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"><com.hearing.demo.nested.v1.NestedBottomViewandroid:id="@+id/bottom_view"android:layout_width="match_parent"android:layout_height="0dp"android:background="@drawable/bg_gradient"app:layout_constraintTop_toBottomOf="@+id/top_view" /><Viewandroid:id="@+id/top_view"android:layout_width="match_parent"android:layout_height="200dp"android:alpha="0.5"android:background="@drawable/ic_test1"app:layout_constraintBottom_toTopOf="@+id/bottom_view"app:layout_constraintTop_toTopOf="parent" /></com.hearing.demo.nested.v1.NestedContainerView>
「底部子控件 NestedBottomView」:
class NestedBottomView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr), NestedScrollingChild {private val childHelper by lazy { NestedScrollingChildHelper(this) }private var lastY = 0fprivate val consumed = intArrayOf(0, 0)init {// 设置支持嵌套滑动isNestedScrollingEnabled = true}override fun dispatchTouchEvent(event: MotionEvent?): Boolean {when (event?.action) {MotionEvent.ACTION_DOWN -> {lastY = event.y// 起始方法if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)) {return true}}MotionEvent.ACTION_MOVE -> {var dy = (lastY - event.y).toInt()lastY = event.y// 首先分发给 NestedScrollingParent 处理if (dispatchNestedPreScroll(0, dy, consumed, null)) {dy -= consumed[1]}if (dy != 0) {// NestedScrollingParent 处理后,剩下未消费的交给自己处理var translation = translationY - dyif (translation > 0) {translation = 0f}translationY = translation// 简单处理一下高度layoutParams.height = SizeUtil.getScreenHeight(context) - translation.toInt()requestLayout()}// 剩下的再交给 NestedScrollingParent 处理,这里直接传 0,也可以不调dispatchNestedScroll(0, dy, 0, 0, null)return true}MotionEvent.ACTION_UP -> {// 结束方法stopNestedScroll()}}return super.dispatchTouchEvent(event)}override fun setNestedScrollingEnabled(enabled: Boolean) {childHelper.isNestedScrollingEnabled = enabled}override fun isNestedScrollingEnabled(): Boolean {return childHelper.isNestedScrollingEnabled}override fun startNestedScroll(axes: Int): Boolean {return childHelper.startNestedScroll(axes)}override fun stopNestedScroll() {childHelper.stopNestedScroll()}override fun hasNestedScrollingParent(): Boolean {return childHelper.hasNestedScrollingParent()}override fun dispatchNestedScroll(dxConsumed: Int,dyConsumed: Int,dxUnconsumed: Int,dyUnconsumed: Int,offsetInWindow: IntArray?): Boolean {return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)}override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?): Boolean {return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)}}
「父控件 NestedContainerView」:
class NestedContainerView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr), NestedScrollingParent {private val parentHelper by lazy { NestedScrollingParentHelper(this) }private lateinit var topView: Viewprivate lateinit var bottomView: Viewprivate val topMinHeight = SizeUtil.dp2px(50f)private val topMaxHeight = SizeUtil.dp2px(200f)override fun onFinishInflate() {super.onFinishInflate()// 简单处理topView = findViewById(R.id.top_view)bottomView = findViewById(R.id.bottom_view)}override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean {return (nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL) != 0}override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {parentHelper.onNestedScrollAccepted(child, target, axes)Log.i(TAG, "onNestedScrollAccepted: $this")}override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {val canScrollUp = dy > 0 && topView.height > topMinHeightval canScrollDown = dy < 0 && topView.height < topMaxHeight && bottomView.translationY == 0fif (canScrollUp || canScrollDown) {// 父 View 需要消费val newHeight = (topView.height - dy).coerceIn(topMinHeight, topMaxHeight)topView.layoutParams.height = newHeighttopView.requestLayout()consumed[1] = dy} else {// 父 View 不需要消费consumed[1] = 0}}override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) {}override fun onStopNestedScroll(child: View) {parentHelper.onStopNestedScroll(child)}override fun getNestedScrollAxes(): Int {return parentHelper.nestedScrollAxes}}
代码里有注释,按照前面的流程图步骤来实现就行。
示例二:RecyclerView
上面是完全自定义的 NestedScrollingChild 子控件,但 Android 系统已经有很多实现了 NestedScrollingChild 接口的控件了,比如说 RecyclerView。 使用 RecyclerView 来实现类似上个示例的嵌套滑动效果。
「布局 XML」:
<?xml version="1.0" encoding="utf-8"?><com.hearing.demo.nested.v2.NestedContainerView xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"><Viewandroid:id="@+id/top_view"android:layout_width="match_parent"android:layout_height="200dp"android:alpha="0.5"android:background="@drawable/ic_test1"app:layout_constraintBottom_toTopOf="@+id/bottom_view"app:layout_constraintTop_toTopOf="parent" /><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/bottom_view"android:layout_width="match_parent"android:layout_height="0dp"app:layout_constraintTop_toBottomOf="@+id/top_view" /></com.hearing.demo.nested.v2.NestedContainerView>
「父控件逻辑基本不变」:
class NestedContainerView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr), NestedScrollingParent {private val parentHelper by lazy { NestedScrollingParentHelper(this) }private lateinit var topView: Viewprivate lateinit var bottomView: RecyclerViewprivate val topMinHeight = SizeUtil.dp2px(50f)private val topMaxHeight = SizeUtil.dp2px(200f)override fun onFinishInflate() {super.onFinishInflate()// 简单处理topView = findViewById(R.id.top_view)bottomView = findViewById(R.id.bottom_view)bottomView.setViewHeight(SizeUtil.getScreenHeight(context) - topMinHeight)}override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean {return (nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL) != 0}override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {parentHelper.onNestedScrollAccepted(child, target, axes)Log.i(TAG, "onNestedScrollAccepted: $this")}override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {val isBottomAtTop = (bottomView.layoutManager as? LinearLayoutManager)?.findFirstCompletelyVisibleItemPosition() == 0val canScrollUp = dy > 0 && topView.height > topMinHeightval canScrollDown = dy < 0 && topView.height < topMaxHeight && isBottomAtTopif (canScrollUp || canScrollDown) {// 父 View 需要消费val newHeight = (topView.height - dy).coerceIn(topMinHeight, topMaxHeight)topView.layoutParams.height = newHeighttopView.requestLayout()consumed[1] = dy} else {// 父 View 不需要消费consumed[1] = 0}}override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) {}override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {// 可以根据实际需求决定要不要响应,这里先简单处理,直接拦截,不处理 flingreturn true}override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {return true}override fun onStopNestedScroll(child: View) {parentHelper.onStopNestedScroll(child)}override fun getNestedScrollAxes(): Int {return parentHelper.nestedScrollAxes}}
小结
本篇文章主要介绍了 NestedScrolling 嵌套滑动机制的流程,以及 NestedScrollingParent 和 NestedScrollingChild 的用法:
❝上面的示例比较简单,接下来的文章会通过一个比较复杂的嵌套滑动示例,再次介绍 NestedScrollingParent 和 NestedScrollingChild 的用法;以及解析 Android 系统提供的 CoordinatorLayout 和 Behavior 等组件原理。
❞