

中高端软件定制开发服务商

13245491521 13245491521
图解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 先感知到用户的滑动需求,进而决定是自己处理还是分发该事件。 ?NestedScrollingChildpublic 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 类,可以实现这套嵌套联动逻辑」。 ?NestedScrollingChildHelperNestedScrollingChildHelper 对 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 便是嵌套滑动的子控件。 setNestedScrollingEnabledpublic void setNestedScrollingEnabled(boolean enabled) { if (mIsNestedScrollingEnabled) { ViewCompat.stopNestedScroll(mView); } mIsNestedScrollingEnabled = enabled;} public boolean isNestedScrollingEnabled() { return mIsNestedScrollingEnabled;}用来设置当前子控件是否要支持嵌套滑动。 startNestedScrollpublic 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; // 逐一往上寻找能配合处理嵌套滑动的 NestedScrollingParent while (p != null) { // ViewParentCompat.onStartNestedScroll() 会判断 p 是否实现 NestedScrollingParent 接口 // 如果是则调用其 onStartNestedScroll 方法,并返回 onStartNestedScroll 的 Boolean 结果 // 否则返回 false if (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,也可能不是。dispatchNestedPreScrollpublic 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 数组是个引用,且初始化 0 ViewParentCompat.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 逻辑都比较类似,都是按照前面说的嵌套滑动的逻辑流程,把事件分发给父控件,不再一一分析。 NestedScrollingParentHelperpublic 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; } } @ScrollAxis public 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.NestedBottomView android: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" / View android: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 = 0f private 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 - dy if (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: View private lateinit var bottomView: View private 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 topMinHeight val canScrollDown = dy 0 && topView.height topMaxHeight && bottomView.translationY == 0f if (canScrollUp || canScrollDown) { // 父 View 需要消费 val newHeight = (topView.height - dy).coerceIn(topMinHeight, topMaxHeight) topView.layoutParams.height = newHeight topView.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" View android: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.RecyclerView android: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: View private lateinit var bottomView: RecyclerView private 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() == 0 val canScrollUp = dy 0 && topView.height topMinHeight val canScrollDown = dy 0 && topView.height topMaxHeight && isBottomAtTop if (canScrollUp || canScrollDown) { // 父 View 需要消费 val newHeight = (topView.height - dy).coerceIn(topMinHeight, topMaxHeight) topView.layoutParams.height = newHeight topView.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 { // 可以根据实际需求决定要不要响应,这里先简单处理,直接拦截,不处理 fling return 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 等组件原理。 ?点击关注公众号,“技术干货”及时达! 阅读原文
| 上一篇:2025-03-31_第一个免费可用的智能Agent产品全量上线,中国公司智谱打造,推理模型比肩R1 | 下一篇:2025-07-02_「转」建议LABUBU和老铺黄金联名 |
TAG标签: |
16 |
|
我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!
|
|
不达标就退款 高性价比建站 免费网站代备案 1对1原创设计服务 7×24小时售后支持 |
|
|
