NestedScrollingParent 实现复杂交互效果

最终效果:
上下排列 3 个 view, 依次为 RecyclerView ,NestedScrollView,RecyclerView,规则:子 View 可以滚动时 Touch 事件给子 view 消费,否则给父布局。

img

1. 接口

NestedScrollingParent 用来配置子视图( NestedScrollingChild )嵌套滚动。

NestedScrollingParent 简称 NP
NestedScrollingChild 简称 NC

  1. NC 产生一个 touch 事件,调用 startNestedScroll,表示开始分享出去 touch 事件, NP 的 onStartNestedScroll 判断是否需要跟 NC 配合,返回 true 表示接受

  2. NC 调用 dispatchNestedPreScroll ,通知 NP 将要进行一个 Touch 事件,如移动 5个像素,NP 的 onNestedPreScroll 中收到通知,知道 NC 要准备移动5个像素,这时候 NP 正好也需要滑动 2 个像素,然后在 onNestedPreScroll 的参数 consumed[] 数组对应的方向(下标为0的x轴,下标为1的y轴)赋值 2,表示消费 2 个像素

  3. NC 拿到 consumed[] 数组,知道 NP 消费了 2 个像素,剩下了 3 个像素,然后自己根据需要再移动 3 个像素的距离,如果这时候 NC 由于某个原因只移动了 2 个像素,那么剩下的 1 个像素距离会调用 dispatchNestedScroll 给 NP,NP 在 onNestedScroll 处理剩下的未消费的 1 像素。最后,NC 调用 stopNestedScroll ,最后 NP 调用 onStopNestedScroll 结束。

  4. 同 NestedScroll 类似的还有一套 NestedFling 操作,整体流程类似,NC 快速滑动产生一个 fling 事件,处理流程 NC.dispatchNestedPreFling -> NP.onNestedPreFling -> NC.dispatchNestedFling -> NP.onNestedFling

为什么 NC 和 NP 能相互配合滚动? 主要的源码在 NestedScrollingChildHelperNestedScrollingParentHelper

效果实现

自定 ViewGroup ,实现 NestedScrollingParent 接口,一个 NestedScrollingParentHelper 成员变量作为辅助

布局

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
<?xml version="1.0" encoding="utf-8"?>
<xyz.hanks.nestedwebview.nestedscroll.NScrollView
xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView0"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#20f0"
android:scrollbarSize="3dp"
android:scrollbars="vertical"/>

<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#e1e1e1"
android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="cdsadadlick"/>

<Button
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="asddasda"/>

<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="asdadarewe"/>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>

<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#2f00"
android:scrollbarStyle="insideInset"
android:scrollbars="vertical"/>
</xyz.hanks.nestedwebview.nestedscroll.NScrollView>

基础的 onMeasure ,onLayout

onLayout

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191

/**
* NestedScrollView
* Created by hanks on 16/8/22.
*/
public class NScrollView extends ViewGroup implements NestedScrollingParent {

private static final String TAG = "NScrollView";
private int maxDistance = 0;
private int totalHeight = 0;
private NestedScrollingParentHelper mParentHelper;
private List<View> nestedScrollingChildList = new ArrayList<>();
private ScrollerCompat scroller;

public NScrollView(Context context) {
this(context, null);
}

public NScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public NScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOverScrollMode(View.OVER_SCROLL_NEVER);
mParentHelper = new NestedScrollingParentHelper(this);
scroller = ScrollerCompat.create(context);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {

int parentHeight = getMeasuredHeight();
int top = t;
int lastChildHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
LayoutParams layoutParams = child.getLayoutParams();
if (layoutParams.height == LayoutParams.MATCH_PARENT) {
layoutParams.height = parentHeight;
} else {
int childMeasuredHeight = child.getMeasuredHeight();
layoutParams.height = childMeasuredHeight;
}
log(child + "," + layoutParams.height);
child.setLayoutParams(layoutParams);
child.layout(l, top, r, top + layoutParams.height);
top += layoutParams.height;
lastChildHeight = layoutParams.height;
}
maxDistance = top - lastChildHeight;
totalHeight = top;
//viewGroup.layout(l, t, r, top);
}

@Override
protected void onFinishInflate() {
super.onFinishInflate();

nestedScrollingChildList.clear();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
nestedScrollingChildList.add(child);
}
}

private void consumeEvent(int dx, int dy, int[] consumed) {
scrollBy(dx, dy);
consumed[0] = 0;
consumed[1] = dy;
log("parent consumed pre : " + consumed[1]);
}

public int getCurrentWrapline(View target) {
int line = 0;
for (View view : nestedScrollingChildList) {
if (view == target) {
return line;
}
line += view.getHeight();
}
return line;
}

@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0;
}

@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
mParentHelper.onNestedScrollAccepted(child, target, axes);
return;
}

@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
logi("======== onStartNestedScroll =======");
int scrollY = getScrollY();
int line = getCurrentWrapline(target);
boolean targetScrollDown = ScrollUtils.canChildScrollDown(target);
boolean targetScrollUp = ScrollUtils.canChildScrollUp(target);
log("onNestedPreScroll: target = [" + target.getClass().getName() + "], dx = [" + dx + "], dy = [" + dy + "], consumed = [" + consumed + "]");
log("scrollY:" + scrollY + ",lien:" + line + ",scrollDown:" + targetScrollDown + "scrollUp:" + targetScrollUp + ",maxDistance" + maxDistance);

if (scrollY == line
&& ((dy > 0 && targetScrollDown) || (dy < 0 && targetScrollUp))) {
return;
}

if (scrollY + dy < 0 || scrollY + dy > maxDistance) {
return;
}
consumeEvent(0, dy, consumed);
}

@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
int scrollY = getScrollY();
if (scrollY + dyUnconsumed < 0 || scrollY + dyUnconsumed > maxDistance) {
return;
}
scrollBy(dxUnconsumed, dyUnconsumed);
log("onNestedScroll: target = [" + target.getClass().getName() + "], dxConsumed = [" + dxConsumed + "], dyConsumed = [" + dyConsumed + "], dxUnconsumed = [" + dxUnconsumed + "], dyUnconsumed = [" + dyUnconsumed + "]");
}

@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
int scrollY = getScrollY();
log("onNestedPreFling: target = [" + target.getClass().getName() + "], velocityX = [" + velocityX + "], velocityY = [" + velocityY + "]" + ",scrollY:" + scrollY);
int line = getCurrentWrapline(target);
if (scrollY != line) {
fling((int) velocityY, scrollY);
return true;
}
return false;
}

private void fling(int velocityY, int scrollY) {
if (getChildCount() > 0) {
int height = getHeight() - getPaddingTop() - getPaddingBottom();
int bottom = totalHeight;
scroller.fling(0, scrollY, 0, velocityY, 0, 0, 0,
Math.max(0, bottom - height));
invalidate();
}
}

@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
int y = scroller.getCurrY();
scrollTo(0, y);
invalidate();
}
}

@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
log("onNestedFling: target = [" + target.getClass().getName() + "], velocityX = [" + velocityX + "], velocityY = [" + velocityY + "], consumed = [" + consumed + "]");
return false;
}

@Override
public void onStopNestedScroll(View target) {
logi("======== onStopNestedScroll =======");
mParentHelper.onStopNestedScroll(target);
}

public void log(String s) {
Log.e(TAG, s);
}

public void logi(String s) {
Log.i(TAG, s);
}

public void logw(String s) {
Log.w(TAG, s);
}
}

文章来自: https://hanks.pub