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 的 onNestedPreSroll
和 onNestedScroll
的呢?分析一下 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 处理 onNestedPreScroll
。ViewCompat
的主要作用是用来兼容不同版本的滑动接口。
实现 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,会立马停住,无法实现连贯的嵌套滑动。
这是因为嵌套滑动组件中,位移的消费只能从 NestedScrollingChild
到 NestedScrollingParent
,而不能从 NestedScrollingParent
到 NestedScrollingChild
,因为只有 NestedScrollingChild
才能 dispatch,NestedScrollingParent
不能 dispatch。
如果想要实现从 NestedScrollingParent
到 NestedScrollingChild
连贯的滑动,暂时没有特别好的办法,只能重写父 View 的事件分发,将父 View 滑动到顶后剩余的位移手动分发给它的子 View。(先挖个坑,看看有没有更好的办法,可以通过扩展嵌套滑动组件达到目的)
Tips
NestedScrollingParent
和 NestedScrollingChild
一共有 3 个版本。
最早的是 NestedScrollingParent
和 NestedScrollingChild
,这一套接口把 scroll 和 fling 分别进行处理,造成了不必要的复杂性。
后来有了 NestedScrollingParent2
和 NestedScrollingChild2
继承自一代,不过它将 fling 转化为 scroll 的距离统一进行处理。上述的嵌套滑动组件均指二代。
再后来又有了 NestedScrollingParent3
和 NestedScrollingChild
继承自二代,它们相比于 2 代增加了 dispatchNestedScroll
和 onNestedScroll
消耗部分滑动位移的功能,即在父 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 的测量布局流程,造成卡顿。这是我在项目中实际遇到的问题。
三、多级嵌套滑动
我们知道了 NestedScrollingParent
和 NestedScrollingChild
可以用来定制化地实现自己地嵌套滑动。很容易想到,如果一个 View 同时实现了两个接口,那么它既可以接受 child 的滑动,又可以分发滑动给 parent,这样就形成了一个链条。而多级嵌套滑动的核心原理就来自于此,如图:
原理其实并不复杂,下面用伪代码表示一下:
- 对于
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;
}
- 对于中介者,即同时实现了
NestedScrollingParent
和NestedScrollingChild
的中间 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 多级嵌套滑动的图:
四、嵌套滑动组件中使用的设计模式
作为总结,讨论一下。
- 策略模式
NestedScrollingParent
和 NestedScrollingChild
是一对接口,不同的 View 通过实现这对接口来达到不同的嵌套滑动效果。同时使用接口也保证了扩展性。
- 代理模式
如前述,当一个 View 实现嵌套滑动接口中的方法时,滑动的具体传递都交给了代理的 NestedScrollingParentHelper
和 NestedScrollingChildHelper
来实现,这两个类是由 sdk 提供的,在 NestedScrollingParent
和 NestedScrollingChild
接口中有如下说明:
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移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
总结
其实要轻松掌握很简单,要点就两个:
- 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
- 多练。 (视频优势是互动感强,容易集中注意力)
你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。下面资料部分截图是我花费几个月时间整理的,诚意满满:特别适合有3-5年开发经验的Android程序员们学习。
识点,真正体系化!**
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-8yoAZIgN-1711729383624)]
总结
其实要轻松掌握很简单,要点就两个:
- 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
- 多练。 (视频优势是互动感强,容易集中注意力)
你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。下面资料部分截图是我花费几个月时间整理的,诚意满满:特别适合有3-5年开发经验的Android程序员们学习。
[外链图片转存中…(img-p25mxVIv-1711729383624)]