nestedscollin原理

Android在发布 5.0(Lollipop)版本之后,Google为我们提供了嵌套滑动(NestedScrolling) 的特性,接下来主要介绍嵌套滑动机制是怎样的原理?

首先,NestedScrolling特性实现时使用到的4个类

  接下来从这四个类分析看看具体做什么的:

  1. NestedScrollingChild
    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
    public interface NestedScrollingChild {  
    /**
    * 设置嵌套滑动是否能用
    *
    * @param enabled true to enable nested scrolling, false to disable
    */
    public void setNestedScrollingEnabled(boolean enabled);

    /**
    * 判断嵌套滑动是否可用
    *
    * @return true if nested scrolling is enabled
    */
    public boolean isNestedScrollingEnabled();

    /**
    * 开始嵌套滑动
    *
    * @param axes 表示方向轴,有横向和竖向
    */
    public boolean startNestedScroll(int axes);

    /**
    * 停止嵌套滑动
    */
    public void stopNestedScroll();

    /**
    * 判断是否有父View 支持嵌套滑动
    * @return whether this view has a nested scrolling parent
    */
    public boolean hasNestedScrollingParent();

    /**
    * 在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离
    *
    * @param dx x轴上滑动的距离
    * @param dy y轴上滑动的距离
    * @param consumed 父view消费掉的scroll长度
    * @param offsetInWindow 子View的窗体偏移量
    * @return 支持的嵌套的父View 是否处理了 滑动事件
    */
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    /**
    * 子view处理scroll后调用
    *
    * @param dxConsumed x轴上被消费的距离(横向)
    * @param dyConsumed y轴上被消费的距离(竖向)
    * @param dxUnconsumed x轴上未被消费的距离
    * @param dyUnconsumed y轴上未被消费的距离
    * @param offsetInWindow 子View的窗体偏移量
    * @return true if the event was dispatched, false if it could not be dispatched.
    */
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
    int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);



    /**
    * 滑行时调用
    *
    * @param velocityX x 轴上的滑动速率
    * @param velocityY y 轴上的滑动速率
    * @param consumed 是否被消费
    * @return true if the nested scrolling parent consumed or otherwise reacted to the fling
    */
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    /**
    * 进行滑行前调用
    *
    * @param velocityX x 轴上的滑动速率
    * @param velocityY y 轴上的滑动速率
    * @return true if a nested scrolling parent consumed the fling
    */
    public boolean dispatchNestedPreFling(float velocityX, float velocityY);

  类的方法虽然不多,但主要还是他的作用?我们想想一种场景:CoordinatorLayout里嵌套着RecyclerView和Toolbar,我们上下滑动RecyclerView的时候,Toolbar会随之显现隐藏,这是典型的嵌套滑动机制情景。这里,RecyclerView作为嵌套的子View,我们猜测,它一定实现了NestedScrollingChild 接口,上代码:

1
2
3
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
..................................................................................
}

可以看出RecyclerView 实现了NestedScrollingChild 接口里的方法,我们在跟进去看看各个方法是怎么实现的?

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
@Override
public void setNestedScrollingEnabled(boolean enabled) {
getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}

@Override
public boolean isNestedScrollingEnabled() {
return getScrollingChildHelper().isNestedScrollingEnabled();
}

@Override
public boolean startNestedScroll(int axes) {
return getScrollingChildHelper().startNestedScroll(axes);
}

@Override
public void stopNestedScroll() {
getScrollingChildHelper().stopNestedScroll();
}

@Override
public boolean hasNestedScrollingParent() {
return getScrollingChildHelper().hasNestedScrollingParent();
}

@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}

@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
}

@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
}

  通过代码我们知道,全部都是由getScrollingChildHelper()这个方法的返回对象处理了,看看这个方法是怎么实现的。

1
2
3
4
5
6
private NestedScrollingChildHelper getScrollingChildHelper() {
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
}
return mScrollingChildHelper;
}

  NestedScrollingChild 接口的方法都交给NestedScrollingChildHelper这个代理对象处理了。现在我们继续深入,分析下NestedScrollingChildHelper中开始嵌套滑动startNestedScroll(int axes)方法是怎么实现的。
NestedScrollingChildHelper#startNestedScroll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
return true;
}
if (isNestedScrollingEnabled()) {//判断是否可以滑动
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {//回调了父View的onStartNestedScroll方法
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}

方法主要做了:

  1. 判断是否有嵌套滑动的父View,返回值 true 表示找到了嵌套滑动的父View和同意一起处理 Scroll 事件。
  2. 用While的方式寻找最近嵌套滑动的父View ,如果找到调用父view的onNestedScrollAccepted.

从这里至少可以得出 子view在调用某个方法都会回调嵌套父view相应的方法,比如子view开始了startNestedScroll,如果嵌套父view存在,就会回调父view的onStartNestedScroll、onNestedScrollAccepted方法。

接下来,在来看看嵌套滑动父view NestedScrollingParent,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface NestedScrollingParent {

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

public void onStopNestedScroll(View target);

public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

public boolean onNestedFling(View target, float velocityX,
float velocityY,boolean consumed);

public boolean onNestedPreFling(View target, float velocityX, float velocityY);

public int getNestedScrollAxes();
}

仔细对比一下会发现,其实和子view差不多的方法,大致一一对应关系,而且它的具体实现也交给了NestedScrollingParentHelper这个代理类。

大致流程

  1. 当 NestedScrollingChild(下文用Child代替) 要开始滑动的时候会调用 onStartNestedScroll ,然后交给代理类NestedScrollingChildHelper(下文ChildHelper代替)的onStartNestedScroll请求给最近的NestedScrollingParent(下文Parent代替).
  2. 当ChildHelper的onStartNestedScroll方法 返回 true 表示同意一起处理 Scroll 事件的时候时候,ChildHelper会通知Parent回调onNestedScrollAccepted 做一些准备动作
  3. 当Child 要开始滑动的时候,会先发送onNestedPreScroll,交给ChildHelper的onNestedPreScroll 请求给Parent ,告诉它我现在要滑动多少距离,你觉得行不行,这时候Parent 根据实际情况告诉Child 现在只允许你滑动多少距离.然后 ChildHelper根据 onNestedPreScroll 中回调回来的信息对滑动距离做相对应的调整.
  4. 在滑动的过程中 Child 会发送onNestedScroll通知ChildeHelpaer的onNestedScroll告知Parent 当前 Child 的滑动情况.
  5. 当要进行滑行的时候,会先发送onNestedFling 请求给Parent,告诉它 我现在要滑行了,你说行不行, 这时候Parent会根据情况告诉 Child 你是否可以滑行.
  6. Child 通过onNestedFling 返回的 Boolean 值来觉得是否进行滑行.如果要滑行的话,会在滑行的时候发送onNestedFling 通知告知 Parent 滑行情况.
  7. 当滑动事件结束就会child发送onStopNestedScroll通知 Parent 去做相关操作.
    nestedScrolling流程图

小结
  通过上面可以知道,要实现NestedScrolling特性,我们需要使用到开始提到四个的类,并在需要一个父view与子view(父view与子view也可能为同一个view)配合来实现。

实例演示

  5.0之前的ListView是没有实现NestedScrollingChild这个接口的,如果要实现CoordinatorLayout里嵌套着ListView和Toolbar,在上下滑动ListView的时候,Toolbar会随之显现隐藏,就必须重写ListView。show code

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package com.cjj.nestedlistview;

import android.content.Context;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ListView;

public class NestedListView extends ListView implements NestedScrollingChild {

private NestedScrollingChildHelper mChildHelper;
private int mLastY;
private final int[] mScrollOffset = new int[2];
private final int[] mScrollConsumed = new int[2];
private int mNestedOffsetY;

public NestedListView(Context context) {
super(context);
init();
}

public NestedListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public NestedListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
mChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}


@Override
public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
}

@Override
public boolean startNestedScroll(int axes) {
return mChildHelper.startNestedScroll(axes);
}

@Override
public void stopNestedScroll() {
mChildHelper.stopNestedScroll();
}

@Override
public boolean hasNestedScrollingParent() {
return mChildHelper.hasNestedScrollingParent();
}

@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}

@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}

@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}


@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = MotionEventCompat.getActionMasked(event);

int y = (int) event.getY();
event.offsetLocation(0, mNestedOffsetY);
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastY = y;
mNestedOffsetY = 0;
break;
case MotionEvent.ACTION_MOVE:

int dy = mLastY - y;
int oldY = getScrollY();

startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
if (dispatchNestedPreScroll(0, dy, mScrollConsumed, mScrollOffset)) {
dy -= mScrollConsumed[1];
event.offsetLocation(0, -mScrollOffset[1]);
mNestedOffsetY += mScrollOffset[1];
}
mLastY = y - mScrollOffset[1];
if (dy < 0) {
int newScrollY = Math.max(0, oldY + dy);
dy -= newScrollY - oldY;
if (dispatchNestedScroll(0, newScrollY - dy, 0, dy, mScrollOffset)) {
event.offsetLocation(0, mScrollOffset[1]);
mNestedOffsetY += mScrollOffset[1];
mLastY -= mScrollOffset[1];
}
}
stopNestedScroll();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:

stopNestedScroll();

break;
}
return super.onTouchEvent(event);
}
}

效果:
嵌套滚动视图效果

来源: