Android 嵌套滑动总结,赶紧学起来

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。(先挖个坑,看看有没有更好的办法,可以通过扩展嵌套滑动组件达到目的)

Tips

NestedScrollingParentNestedScrollingChild 一共有 3 个版本。

最早的是 NestedScrollingParentNestedScrollingChild,这一套接口把 scroll 和 fling 分别进行处理,造成了不必要的复杂性。

后来有了 NestedScrollingParent2NestedScrollingChild2 继承自一代,不过它将 fling 转化为 scroll 的距离统一进行处理。上述的嵌套滑动组件均指二代。

再后来又有了 NestedScrollingParent3NestedScrollingChild 继承自二代,它们相比于 2 代增加了 dispatchNestedScrollonNestedScroll 消耗部分滑动位移的功能,即在父 View 消耗位移之后,将消耗值放入 consumed 数组通知子 View。而二代是不会让子 View 知道父 View 的消耗值的。一般来说,要自己实现嵌套滑动,只需要实现 2 代及以上接口即可。一代基本不再使用。

注意:使用 NestedScrollView 有一个地方需要注意,当它的子 View 是 RecyclerView 这种 content 可以无限长的布局时,要注意限制这些子 View 的高度,不要使用 wrap_content 设置 RecyclerView 的高度。因为 NestedScrollView 在测量时给子 View 的限制是 UNSPECIFIED ,即不做限制,RecyclerView 想要多高就有多高。像 RecyclerView 如果内部 item 数量太多,RecyclerView 在 wrap_content 的情况下会把所有 item 都显示出来,相当于没有回收。这样会对内存造成很大消耗。如果调用 setVisibility 改变可见性的话,当从不可见到可见,更是会瞬间调用所有 item 的测量布局流程,造成卡顿。这是我在项目中实际遇到的问题。

三、多级嵌套滑动

我们知道了 NestedScrollingParentNestedScrollingChild可以用来定制化地实现自己地嵌套滑动。很容易想到,如果一个 View 同时实现了两个接口,那么它既可以接受 child 的滑动,又可以分发滑动给 parent,这样就形成了一个链条。而多级嵌套滑动的核心原理就来自于此,如图:

image-20210304170155409.png

原理其实并不复杂,下面用伪代码表示一下:

  • 对于 NestedScrollingParent

@Override

public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {

scrollByMe(dx, dy);

consumed[0] = dxConsumed;

consumed[1] = dyConsumed;

}

  • 对于中介者,即同时实现了 NestedScrollingParentNestedScrollingChild 的中间 View 来说

@Override

public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {

// 先分发,再消费。当然也可以先以先消费,再分发,这取决于业务

dispatchNestedPreScroll(dispatchNestedPreScroll(dx, dy, consumed, null, type);

int dx -= consumed[0];

int dy -= consumed[1];

scrollByMe(dx, dy);

consumed[0] = dxConsumed;

consumed[1] = dyConsumed;

}

  • 对于最内层的 NestedScrollingChild,一般使用 RecyclerView 就可以。

在多级嵌套滑动中,可以根据业务自己设置各层在上滑与下滑过程中的优先级。

工作的项目因为还没发布就不放上来了,这里上一个从网上找到的即刻 App 多级嵌套滑动的图:

四、嵌套滑动组件中使用的设计模式

作为总结,讨论一下。

  • 策略模式

NestedScrollingParentNestedScrollingChild是一对接口,不同的 View 通过实现这对接口来达到不同的嵌套滑动效果。同时使用接口也保证了扩展性。

  • 代理模式

如前述,当一个 View 实现嵌套滑动接口中的方法时,滑动的具体传递都交给了代理的 NestedScrollingParentHelperNestedScrollingChildHelper来实现,这两个类是由 sdk 提供的,在 NestedScrollingParentNestedScrollingChild 接口中有如下说明:

This interface should be implemented by ViewGroup subclasses

that wish to support scrolling operations delegated by a nested child view.

Classes implementing this interface should create a final instance of a

NestedScrollingParentHelper as a field and delegate any View or ViewGroup methods

to the NestedScrollingParentHelper methods of the same signature.

  • 适配器模式 / 外观模式

RecyclerView 实现了 NestedScrollingChild2 接口,但是如果它的父 view 实现的是一代的 NestedScrollingParent 接口怎么办?这就不同版本的嵌套滑动组件需要兼容。怎样实现兼容呢,使用 ViewCompat ,如下:

// 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) {

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

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

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

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

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

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

总结

其实要轻松掌握很简单,要点就两个:

  1. 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
  2. 多练。 (视频优势是互动感强,容易集中注意力)

你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。下面资料部分截图是我花费几个月时间整理的,诚意满满:特别适合有3-5年开发经验的Android程序员们学习。

识点,真正体系化!**

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

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-8yoAZIgN-1711729383624)]

总结

其实要轻松掌握很简单,要点就两个:

  1. 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
  2. 多练。 (视频优势是互动感强,容易集中注意力)

你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。下面资料部分截图是我花费几个月时间整理的,诚意满满:特别适合有3-5年开发经验的Android程序员们学习。

[外链图片转存中…(img-p25mxVIv-1711729383624)]

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

  • 22
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值