0%

Android NestedScrolling

NestedScrolling 是谷歌推出的用于解决嵌套滑动的解决方案,在CoordinatorLayout中有使用。在 sdk api 21 之后,直接更新到了 View 和 ViewGroup源码里面,同时也在android.support.v4 包中提供了两个接口NestedScrollingChild和NestedScrollingParent, 还和两个辅助类 NestedScrollingChildHelper 和 NestedScrollingParentHelper 用来帮助开发者实现相关功能. 因此在 sdk21之后 系统原生控件是直接支持嵌套滑动的,如果自己需要实现,用那几个相关类就可以了。

原理

在Android触摸事件的处理中,无论是父类还是子类,一旦拦截了 Action_DOWN ,后续的事件都会发给这个控件,出于兼容的目的,这个流程不会大改,想要在滑动的过程中父类和子类联动,于是新增了接口方法,在 ACITION_MOVE 中进行调用,并且不改变整个onInterceptTouchEvent()和onTouchEvent()的返回值,这样既不会影响到原有流程,又可以让父类和子类进行交互。对于 Fling 的处理则是在 ACTION_UP 中,这个应该很好理解~

主要的过程则是子控件接收到滑动一段距离的请求时, 先询问父控件是否要滑动, 如果滑动了父控件就通知子控件它消耗了一部分滑动距离, 子控件就处理剩下的滑动距离, 然后子控件滑动完毕后再把剩余的滑动距离传给父控件.这个过程由子控件发起。

主要类和方法

NestedScrollingChild

  • startNestedScroll : 起始方法, 主要作用是找到接收滑动距离信息的父控件.
  • dispatchNestedPreScroll : 在内控件处理滑动前把滑动信息分发给父控件.
  • dispatchNestedScroll : 在内控件处理完滑动后把剩下的滑动距离信息分发给父控件.
  • stopNestedScroll : 结束方法, 主要作用就是清空嵌套滑动的相关状态

NestedScrollingChildHelper是对NestedScrollingChild的方法的实现

NestedScrollingParent

父控件接口主要是定义了一些响应子控件的方法,以onXXXXX命名和子控件的方法一一对应。

NestedScrollingParentHelper是对NestedScrollingParent的方法的实现

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* Begin a nestable scroll operation along the given axes.
*
* <p>A view starting a nested scroll promises to abide by the following contract:</p>
*
* <p>The view will call startNestedScroll upon initiating a scroll operation. In the case
* of a touch scroll this corresponds to the initial {@link MotionEvent#ACTION_DOWN}.
* In the case of touch scrolling the nested scroll will be terminated automatically in
* the same manner as {@link ViewParent#requestDisallowInterceptTouchEvent(boolean)}.
* In the event of programmatic scrolling the caller must explicitly call
* {@link #stopNestedScroll()} to indicate the end of the nested scroll.</p>
*
* <p>If <code>startNestedScroll</code> returns true, a cooperative parent was found.
* If it returns false the caller may ignore the rest of this contract until the next scroll.
* Calling startNestedScroll while a nested scroll is already in progress will return true.</p>
*
* <p>At each incremental step of the scroll the caller should invoke
* {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll}
* once it has calculated the requested scrolling delta. If it returns true the nested scrolling
* parent at least partially consumed the scroll and the caller should adjust the amount it
* scrolls by.</p>
*
* <p>After applying the remainder of the scroll delta the caller should invoke
* {@link #dispatchNestedScroll(int, int, int, int, int[]) dispatchNestedScroll}, passing
* both the delta consumed and the delta unconsumed. A nested scrolling parent may treat
* these values differently. See {@link ViewParent#onNestedScroll(View, int, int, int, int)}.
* </p>
*
* @param axes Flags consisting of a combination of {@link #SCROLL_AXIS_HORIZONTAL} and/or
* {@link #SCROLL_AXIS_VERTICAL}.
* @return true if a cooperative parent was found and nested scrolling has been enabled for
* the current gesture.
*
* @see #stopNestedScroll()
* @see #dispatchNestedPreScroll(int, int, int[], int[])
* @see #dispatchNestedScroll(int, int, int, int, int[])
*/
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = getParent();
View child = this;
while (p != null) {
try {
if (p.onStartNestedScroll(child, this, axes)) {
mNestedScrollingParent = p;
p.onNestedScrollAccepted(child, this, axes);
return true;
}
} catch (AbstractMethodError e) {
Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " +
"method onStartNestedScroll", e);
// Allow the search upward to continue
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}

注释说的很清楚了。。
axes: SCROLL_AXIS_HORIZONTALSCROLL_AXIS_VERTICAL的值之一。

返回值 : 如果父控件可以响应嵌套滑动并且是enabled状态就返回true

在Action_DWON和onInterceptTouchEvent中调用,表示嵌套滑动的开始,从代码来看,做的仅仅是一直getParent(),如果父类的onStartNestedScroll()返回true,就接着调用onNestedScrollAccepted()进行初始化,否则继续向上寻找,没找到返回false。

后续应该调用 dispatchNestedPreScroll() ,如果它返回 true 则表示父控件至少消耗的部分或者全部的滑动距离

接着应该调用 dispatchNestedScroll() , 自己处理后再返回给父控件去处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
* 在子控件消耗任何部分的滑动之前分发嵌套滑动,这个方法给父控件提供了预先处理的机会
* @param dx Horizontal scroll distance in pixels
* @param dy Vertical scroll distance in pixels
* @param consumed 用于输出的数组。如果不为空,consumed[0]会包含dx的消耗值,consumed[1]是dy的消耗值
* @param offsetInWindow 可选,如果不为空,则返回的值是view在这个方法操作前后的坐标的插值。View可能会用这个值来调整输入坐标值
* @return 只要父控件消耗了滑动的坐标就会返回true
*/
public boolean dispatchNestedPreScroll(int dx, int dy,
@Nullable @Size(2) int[] consumed, @Nullable @Size(2) int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}

if (consumed == null) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
consumed[0] = 0;
consumed[1] = 0;
mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed);

if (offsetInWindow != null) {
getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}


/**
* 在子控件消耗任何部分的滑动的时候 分发嵌套滑动,这个方法给父控件提供了后处理的机会,逻辑和上面类似
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable @Size(2) int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}

mNestedScrollingParent.onNestedScroll(this, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed);

if (offsetInWindow != null) {
getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return true;
} else if (offsetInWindow != null) {
// No motion, no dispatch. Keep offsetInWindow up to date.
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}

使用

上面说的断断续续,其实用起来很容易。

NestedScrollingChild

对于 NestedScrollingChild 来说,它需要做的是 :

  1. setNestedScrollingEnabled()设置为true
  2. 在ACTION_DOWN中调用 startNestedScroll()
  3. 在ACTION_MOVE中调用 dispatchNestedPreScroll() 和 dispatchNestedScroll()
  4. 在ACTOPN_UP|ACTION_CANCEL 中看情况调用 stopNestedScroll()

在sdk21之后,View类里面 onTouchEvent() 默认实现了这些步骤,如果不需要重写 onTouchEvent() 的话,本身就是支持这个功能的,如果重写的话,则需要自己看情况加入这些流程的调用。具体的例子可以看 RecyclerView 。作为support包的类,为了兼容性它自己实现了NestedScrollingChild接口,其实也就是调用 NestedScrollingChildHelper 类的相关方法,逻辑和sdk21之后的View的默认实现是类似的。

NestedScrollingParent

对于 NestedScrollingParent 来说,它需要做的就是 重写onXXXScroll()方法 ,这个根据不同的控件会有不同的效果,都需要自己去实现,对于sdk21之后的ViewGroup,提供了默认实现,就是直接调用 View的默认实现 dispatchNestedPreScroll() 和 dispatchNestedScroll() 继续向父控件分发,不满足条件则直接返回false,什么也不做。具体的例子可以看 ActionBarOverlayLayout 这个类,不过鉴于这个类不是很熟悉,可以看 ScrollView,SwipeRefreshLayout ,但这两个类都不仅仅可以作为 NestedScrollingParent,也可以作为NestedScrollingChild,看的时候不要弄混了。还有个 CoordinatorLayout,这个又做了一层封装,可以自己选择哪个类把。

后面有空可以自己实现一个来看看效果