Android 嵌套滑动总结

要用 CoordinatorLayout 实现嵌套滑动非常简单,只要按如下编写布局文件:

<?xml version="1.0" encoding="utf-8"?>

<androidx.coordinatorlayout.widget.CoordinatorLayout

xmlns:android=“http://schemas.android.com/apk/res/android”

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

}

}

return false;

}

所以如果直接在 RecyclerView 的外层套 NestedScrollView 是没有办法实现完整的嵌套滑动的,你会发现在上滑的时候,没有嵌套滑动的效果,而下滑的时候有嵌套滑动的效果。

没有考虑到的问题

其实,在之前所说的内容中,默认了手指从子 Viw 开始滑动。假如手指从外层的父 View 开始滑动,当父 View fling 到顶后,子 View 是无法继续 fling,会立马停住,无法实现连贯的嵌套滑动。

这是因为嵌套滑动组件中,位移的消费只能从 NestedScrollingChildNestedScrollingParent,而不能从 NestedScrollingParentNestedScrollingChild ,因为只有 NestedScrollingChild 才能 dispatch,NestedScrollingParent 不能 dispatch。

如果想要实现从 NestedScrollingParentNestedScrollingChild 连贯的滑动,暂时没有特别好的办法,只能重写父 View 的事件分发,将父 View 滑动到顶后剩余的位移手动分发给它的子 View。(先挖个坑,看看有没有更好的办法,可以通过扩展嵌套滑动组件达到目的)

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

学习福利

【Android 详细知识点思维脑图(技能树)】

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

】**

[外链图片转存中…(img-rZoL8fIt-1712157093641)]

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

[外链图片转存中…(img-Td7CUkHe-1712157093641)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 10
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值