文章内容如有错误欢迎探讨指正!
概述
在传统的事件分发机制中,通常来说父控件和子控件们只会有一个控件去处理事件流,但我们时常会有一些特殊的需求,比如说多个控件之间的滑动需要联动起来,这时候就需要用到嵌套滑动机制了。
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
Java
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
less
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 内部的方法,来实现嵌套滑动事件的分发。
Java
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) {
// ...
}
// ...
}
构造方法
Java
public NestedScrollingChildHelper(@NonNull View view) {
mView = view;
}
创建 NestedScrollingChildHelper 实例需要传入一个 View,这个 View 便是嵌套滑动的子控件。
setNestedScrollingEnabled
Java
public void setNestedScrollingEnabled(boolean enabled) {
if (mIsNestedScrollingEnabled) {
ViewCompat.stopNestedScroll(mView);
}
mIsNestedScrollingEnabled = enabled;
}
public boolean isNestedScrollingEnabled() {
return mIsNestedScrollingEnabled;
}
用来设置当前子控件是否要支持嵌套滑动。
startNestedScroll
Java
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
Java
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
Java
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 |
---|---|
示例一:自定义子控件
- 在了解嵌套滑动的流程及父子控件接口的逻辑后,可以通过它来实现一个嵌套滑动的Demo:
- 顶部是个普通 View,底部是嵌套滑动子控件,父布局是嵌套滑动的父控件。
- 子控件接收到滑动事件后,分发给父控件,父控件控制顶部 View 的高度,直到顶部 View 高度达到最小高度后,父控件不再消费滑动距离,交给底部控件消费。
- 嵌套滑动效果比较简单,可以自己加一下其他效果,比如松手后自动吸附,响应fling事件等。
- NestedScrollingChild 和 NestedScrollingParent 其实只是接口,定义了嵌套滑动的事件分发规范,但实际上的逻辑需要我们自己实现。
- 对于 NestedScrollingChild 的接口实现,官方有提供一个 NestedScrollingChildHelper 类,一般直接使用它就行。NestedScrollingChildHelper 内部会和 NestedScrollingParent 进行交互。
- 对于 NestedScrollingParent 的实现,官方也有提供一个 NestedScrollingParentHelper 类,内部维护了一些状态。
- 具体的滑动行为,开发者可以根据具体需求自己来实现。
布局 XML:
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:
Kotlin
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:
kotlin
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:
ini
<?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>
父控件逻辑基本不变:
kotlin
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 等组件原理。
觉得内容不错的同学可以点点赞,评论区交流。
之前在掘金上对Kotlin协程的解析比较零散,小小地推荐一下《深入理解Kotlin协程》,从源码和实例出发,结合图解,系统地分析 Kotlin 协程启动,挂起,恢复,异常处理,线程切换,并发等流程,只用一顿饭钱!感兴趣的朋友可以了解下,互相交流,不喜勿喷。