- [Android]触摸、滑动与嵌套滑动(一)事情与翻滚 – ()
- [Android]触摸、滑动与嵌套滑动(二)几种场景下的事情处理分析和调试 – ()
- [Android]触摸、滑动与嵌套滑动(三)ScrollView与滑动冲突 – ()
怎样完成舒畅的嵌套滑动
当咱们ScrollView套着ScrollView会出现一个问题,假设手指一次性滑动了500pix,内层ScrollView耗费了300pix,即便还余下200pix也不能持续滑动了,而是必须抬起手指,再滑动一次才能够让外层的ScrollView被滑动:
而咱们希望的是,当ScrollView滑动了300pix之后,将余下的200pix交给父View,也便是外层的ScrollView进行滑动:
这按照传统的事情传递机制似乎是不可行的,由于现行的事情传递机制规定了:一组事情(DOWN/MOVE/UP)必须由一个View消费完结,除非外层的视图自动阻拦,才会出现外层视图不承受ACTION_DOWN只承受ACTION_MOVE的状况。
而现在的问题是,ScrollView嵌套ScrollView的时分,内层的ScrollView底子无法滑动:
所以在之前的内容中,咱们的处理办法是,在外层的ScrollView的onInterceptTouchEvent()
依据必定状况回来false
,旨在告知外层ScollView假如内层视图能够滑动则不阻拦事情,可是咱们难以操控什么时分能够让外层View从头取得事情。
假设,咱们希望在内层ScrollView无法再向上滑动的时分,即滑动量达到300pix的时分,将事情交给父View处理,咱们能够有两种解决方案:
- 父View在收到子View某种信号的时分自动去阻拦子View的事情。就和之前ScrollView和Button嵌套的时分,Button收到ACTION_DOWN的时分那样。
- 不上交事情,只把滑动量交给父View来处理,余下的200pix经过某种办法让父View换一种办法来消费这个滑动量,而不是事情。
显然,2是更为合理的,咱们看看之前的这一种效果图,咱们能够发现在父View接收了余下的滑动量之后,咱们从头下滑,此刻立即呼应的是子View下滑,而不是父View下滑。而1中,假如咱们去阻拦子View事情后,再从头下发,就目前的滑动模型来说,是非常困难的。
1. NestedScrollView是怎样做的
咱们能够先监听一下官方NestedScrollView嵌套时,它们的事情是怎样传递的,就能够知道在派发剩余滑动量时,消费事情的究竟是子View仍是父View。
仍是比较明显的,即便嵌套滑动发生的时分,消费事情的View仍然是内层的NestedScrollView:
Out::dispatchTouchEvent,true
// 以下为一组(由于是在super.onTouchEvent之后调用的打印,所以看着是反着的)
Inner::onTouchEvent,true
Inner::dispatchTouchEvent,true
Out::dispatchTouchEvent,true
//
D/rEd: Inner::onTouchEvent,true
D/rEd: Inner::dispatchTouchEvent,true
Out::dispatchTouchEvent,true
由于是先调用的super,再打印的日志,所以看着是反着的。可是要害是在Inner::onTouchEvent中回来的true,这就说明了,嵌套滑动子View只是将剩余的滑动量派发到父View,并没有抛弃事情。
假如是子View滑究竟,然后抬起手指,再在子View上滑动呢?此刻承受事情的是谁?
此刻子View仍然是无法向上滑动的(由于究竟了),翻滚的是父View,可是承受事情的仍是子View,也便是说,父View在任何状况下都就将事情派发给子View了,而不是自己去滑动(当然点击事情得发生在父View和子View重合的区域)。
这就说明了,嵌套滑动相关的工具类:NestedScrollParent、NestedScrollChild等等,要解决的核心问题,便是:NestedScrollChild向NestedScrollParent派发剩余的滑动量,而Child需求处理:
- 什么时分发生剩余滑动量(究竟/顶之后剩余的滑动量);
- 怎样派发、怎样派发;
Parent需求处理:
- 怎样承受剩余的滑动量;
- 在事情传递机制大框架之外额外进行滑动;
2. NestedScrollChild与NestedScrollParent
咱们看看这两个接口对应的一些办法,首先是NestedScrollingChild3
,假如你直接承继它,你要重写如下的办法:
override fun startNestedScroll(axes: Int, type: Int): Boolean
override fun stopNestedScroll(type: Int)
override fun hasNestedScrollingParent(type: Int): Boolean
这三个看着还算正常,也比较好理解。下面前两个显然是用来派发滑动量的,第三个dispatchNestedPreScroll,则为嵌套滑动操作中的父View供给了,在子View运用滑动操作之前运用部分或悉数翻滚操作的机会。
override fun dispatchNestedScroll(
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
offsetInWindow: IntArray?,
type: Int,
consumed: IntArray
)
override fun dispatchNestedScroll(
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
offsetInWindow: IntArray?,
type: Int
): Boolean
override fun dispatchNestedPreScroll(
dx: Int,
dy: Int,
consumed: IntArray?,
offsetInWindow: IntArray?,
type: Int
): Boolean
而Parent的也基本上能够从名称中看出大致的效果。
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int)
override fun onStopNestedScroll(target: View, type: Int)
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray
)
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int
)
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int)
常用的组件中,NestedScrollView分别完成了NestedScrollParent和NestedScrollChildren;RecyclerView则完成了NestedScrollChild,而CoordinatorLayout则完成了NestedScrollParent,后续的阅读能够以这三个组件的对这俩个接口的运用为主;
此外,还有两个重要的类,在上述两个接口的注释中,告知咱们:
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.
大致意思是,咱们假如要运用NestedScrollParent,咱们需求创立一个final的NestedScrollingParentHelper的实例,运用其间的一些办法来代理掉原先View、ViewGroup中的同名办法,同样地,NestedScrollChild也有这么段话,只不过实例变成了:NestedScrollingChildHelper。
3. RecyclerView是怎样完成NestedScrollChild的
RecyclerView在大多数的场景下以LinearLayoutManager的形式出现,偶尔也以表格、瀑布流的形式出现,详细的取决于LayoutManager中的自定义,它是支持嵌套滑动的,也便是说,它会将剩余的偏移量下发给NestedScrollParent。
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3
NestedScrollingChild2和3一共有6个办法需求重写,可是重写的内容悉数是运用NestedScrollingChildHelper中的同签名办法替换掉了,比如:
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow, int type) {
return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow, type);
}
@Override
public final void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow, int type, @NonNull int[] consumed) {
getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
}
……
已然悉数托付给了NestedScrollingChildHelper
,那么咱们滑动量的派发的关注点,又能够缩小一些了。
4. 一次嵌套滑动
NestedScrollingChildHelper中,主要是针对NestedScrollChild中的重写的办法,进行一些逻辑处理,以NestedcScrollChild开端嵌套滑动的startNestedScroll为例:
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
完成判别一下是否已经在嵌套滑动的进程中,假如已经在了则直接回来。
否则,从View Hierarchy上的当时节点开端,去查找嵌套滑动的:NestedScrollParent,并企图让它进行消费事情。
这儿又来了一个新的类:ViewParentCompat,其间声明了很多的静态办法,用于处理一些兼容性的滑动。以ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)
的调用为例:
public static boolean onStartNestedScroll(@NonNull ViewParent parent, @NonNull View child,
@NonNull View target, int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, 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 {
return Api21Impl.onStartNestedScroll(parent, child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onStartNestedScroll", e);
}
} else if (parent instanceof NestedScrollingParent) {
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
}
return false;
}
其间,以parent可能为NestedScrollParent2选择直接调用2中对应的parent的onStartNestedScroll进行嵌套滑动;或者是NestedScrollParent来处理嵌套滑动。Api21Impl中,由于后续的ViewParent做了兼容性的处理,直接调用ViewParent下的同名办法即可:
@DoNotInline
static boolean onStartNestedScroll(ViewParent viewParent, View view, View view1, int i) {
return viewParent.onStartNestedScroll(view, view1, i);
}
而其他状况下,则去调用NestedScrollingParent
接口完成类下的同名办法。
回到主干部分,ViewParentCompat.onNestedScrollAccepted中的调用和上面的onStartNestedScroll
大同小异。
……
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
……
onNestedScrollAccepted
办法为视图及其超类供给了为嵌套翻滚履行初始配置的机会,比如在NestedScrollView这个NestedScrollParent中:
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes,
int type) {
mParentHelper.onNestedScrollAccepted(child, target, axes, type);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type);
}
注意,这儿又调用到了最开端**startNestedScroll
** ,(以A->B->C为例)由于嵌套滑动的进程,并不是只在BC之间进行的,也可能是C和A之间的嵌套。
剩余滑动量的派发的进程如下:NestedScrollingChildHelper的dispatchNestedScrollInternal
中:
// 有删减
private boolean dispatchNestedScrollInternal(
int dxConsumed,
int dyConsumed,
int dxUnconsumed,
int dyUnconsumed,
@Nullable int[] offsetInWindow,
@NestedScrollType int type,
@Nullable int[] consumed
) {
if(!滑动可用){
return false;
}
final ViewParent parent = getNestedScrollingParentForType(type);
// 记录View和Window的初始偏移量
val startX = offsetInWindow[0]
val startY = offsetInWindow[1];
// 开端滑动
ViewParentCompat.onNestedScroll(parent, mView,
dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
// 重置滑动偏移量
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return true;
}
要害就在于**ViewParentCompat.onNestedScroll(parent, mView,dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
** ,假如你认真看过之前的start是怎样运作的,那么这儿面的内容你应该能猜到,咱们直接看NestedScrollParent怎样处理的:
// NestedScrollView中:
private void onNestedScrollInternal(int dyUnconsumed, int type, @Nullable int[] consumed) {
final int oldScrollY = getScrollY();
scrollBy(0, dyUnconsumed);
final int myConsumed = getScrollY() - oldScrollY;
if (consumed != null) {
consumed[1] += myConsumed;
}
final int myUnconsumed = dyUnconsumed - myConsumed;
mChildHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed);
}
无非便是依据未消费的滑动量:dyUnconsumed,运用scrollBy对当时位置进行一个相对距离的滑动。
5. 总结
上面已经简略的分析过NestedScrollXXX组件是怎样完成一次嵌套滑动的了,作为官方供给的组件,由于要兼容非常多种状况和api,所以现在读起来会显得很『厚』,这是代码迭代的必然结果,所以怎样从中提炼出一个详细的,嵌套滑动的模型才是要害的:
总结的几件事情是:
- 嵌套滑动的Child要处理滑动,一起需求将剩余的滑动上发给Parent;
- Parent默许状况下将所有的事情都下发给Child处理;
- Parent要接收额外的滑动状况,使用scrollBy在onTouchEvent以外进行视图的滑动;
归根到底地,嵌套滑动用一句话总结,便是:父View不阻拦ACTION_DOWN事情,子View全权消费事情,而且子View会依据必定的状况下发剩余的滑动偏移量给父View,而父View则经过子View上发的「偏移量」而不是「事情」凭借scrollBy办法对「偏移量」进行滑动。 这儿的偏移量通常是子View触底未消费完的滑动量,亦或者是其它的事务交互下的偏移量,详细是什么,能够依据详细需求的UI完成来确定。
~End