稀土掘金技术社区

图解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) 之间嵌套滑动的流程:

  1. 事件分发时,父控件不拦截,由子控件处理;
  2. 子控件开始滑动前(收到 DOWN 事件),询问父控件是否要配合嵌套滑动,如果父控件返回不配合,则不会继续下面的步骤,否则继续;
  3. 子控件接收到 MOVE 事件后,先把滑动信息传给父控件,父控件消费部分/全部滑动距离,并通知子控件它消费的滑动距离;
  4. 子控件处理剩下的滑动距离,它消费全部/部分剩下的滑动距离后,把还剩下的滑动距离传给父控件处理;
  5. 如果子控件在滑动过程中还发生了惯性滑动,就先把惯性速度信息传给父控件,父控件可以选择消费/不消费惯性滑动,并告诉子控件它的消费结果;
  6. 如果父控件没有消费惯性事件,则子控件来决定消不消费,并把这个消费结果再次传给父控件,父控件根据需要返回消费结果。
  7. 触摸/Fling 事件结束后,通知父控件嵌套滑动流程结束。

流程图如下:

Image

通过上述嵌套滑动机制,在一次滑动操作过程中父控件和子控件都可以对滑动事件作出响应。

NestedScrollingParent&NestedScrollingChild

接下来我们看看上面嵌套滑动流程图中,NestedScrollingParent 和 NestedScrollingChild 角色相关的方法,它们是 NestedScrolling 机制的基础。在 Android 中提供了一系列实现了这俩接口的控件:

  1. NestedScrollingParent: 如 CoordinatorLayout,NestedScrollView 等,此接口应由希望支持嵌套滑动事件的父控件实现,此外还提供了 NestedScrollingParent2 和 NestedScrollingParent3 扩展功能的接口。
  2. 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 参数。

嵌套滑动的接口流程

结合具体的接口方法,看看上面的流程图:

Image

❝

需要注意的是上面的 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; // 逐一往上寻找能配合处理嵌套滑动的 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,也可能不是。

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 数组是个引用,且初始化 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 逻辑都比较类似,都是按照前面说的嵌套滑动的逻辑流程,把事件分发给父控件,不再一一分析。

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; } }
@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

Image

Image

示例一:自定义子控件

  1. 在了解嵌套滑动的流程及父子控件接口的逻辑后,可以通过它来实现一个嵌套滑动的Demo:
    1. 顶部是个普通 View,底部是嵌套滑动子控件,父布局是嵌套滑动的父控件。
    2. 子控件接收到滑动事件后,分发给父控件,父控件控制顶部 View 的高度,直到顶部 View 高度达到最小高度后,父控件不再消费滑动距离,交给底部控件消费。
    3. 嵌套滑动效果比较简单,可以自己加一下其他效果,比如松手后自动吸附,响应fling事件等。
  2. NestedScrollingChild 和 NestedScrollingParent 其实只是接口,定义了嵌套滑动的事件分发规范,但实际上的逻辑需要我们自己实现。
  3. 对于 NestedScrollingChild 的接口实现,官方有提供一个 NestedScrollingChildHelper 类,一般直接使用它就行。NestedScrollingChildHelper 内部会和 NestedScrollingParent 进行交互。
  4. 对于 NestedScrollingParent 的实现,官方也有提供一个 NestedScrollingParentHelper 类,内部维护了一些状态。
  5. 具体的滑动行为,开发者可以根据具体需求自己来实现。

「布局 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

  1. 上面是完全自定义的 NestedScrollingChild 子控件,但 Android 系统已经有很多实现了 NestedScrollingChild 接口的控件了,比如说 RecyclerView。
  2. 使用 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 的用法:

Image

❝

上面的示例比较简单,接下来的文章会通过一个比较复杂的嵌套滑动示例,再次介绍 NestedScrollingParent 和 NestedScrollingChild 的用法;以及解析 Android 系统提供的 CoordinatorLayout 和 Behavior 等组件原理。

❞