程序员徐公

微信公众号:【徐公】

0%

自定义-Behavior-仿新浪微博发现页的实现

使用CoordinatorLayout打造各种炫酷的效果

自定义Behavior —— 仿知乎,FloatActionButton隐藏与展示

NestedScrolling 机制深入解析

一步步带你读懂 CoordinatorLayout 源码

自定义 Behavior -仿新浪微博发现页的实现

ViewPager,ScrollView 嵌套ViewPager滑动冲突解决

自定义 behavior - 完美仿 QQ 浏览器首页,美团商家详情页

重磅消息:小编我开始运营自己的公众号了, 目前从事于 Android 开发,除了分享 Android开发相关知识,还有职场心得,面试经验,学习心得,人生感悟等等。希望通过该公众号,让你看到程序猿不一样的一面,我们不只会敲代码,我们还会。。。。。。

有兴趣的话可以关注我的公众号 徐公码字(stormjun94),或者拿起你的手机扫一扫,期待你的参与

Android 技术人

效果图

我们先来看一下新浪微博发现页的效果:

接下来我们在来看一下我们仿照新浪微博实现的效果

仿新浪微博效果图

实现思路分析

我们这里先定义两种状态,open 和 close 状态。

  • open 状态指 Tab+ViewPager 还没有滑动到顶部的时候,header 还 没有被完全移除屏幕的时候
  • close 状态指 Tab+ViewPager 滑动到顶部的时候,Header 被移除屏幕的时候

从效果图,我们可以看到 在 open 状态下,我们向上滑动 ViewPager 里面的 RecyclerView 的 时候,RecyclerView 并不会向上移动(RecyclerView 的滑动事件交给 外部的容器处理,被被全部消费掉了),而是整个布局(指 Header + Tab +ViewPager)会向上偏移 。当 Tab 滑动到顶部的时候,我们向上滑动 ViewPager 里面的 RecyclerView 的时候,RecyclerView 可以正常向上滑动,即此时外部容器没有拦截滑动事件

同时我们可以看到在 open 状态的时候,我们是不支持下拉刷新的,这个比较容易实现,监听页面的状态,如果是 open 状态,我们设置 SwipeRefreshLayout setEnabled 为 false,这样不会 拦截事件,在页面 close 的时候,设置 SwipeRefreshLayout setEnabled 为 TRUE,这样就可以支持下拉刷新了。

基于上面的分析,我们这里可以把整个效果划分为两个部分,第一部分为 Header,第二部分为 Tab+ViewPager。下文统一把第一部分称为 Header,第二部分称为 Content 。

需要实现的效果为:在页面状态为 open 的时候,向上滑动 Header 的时候,整体向上偏移,ViewPager 里面的 RecyclerView 向上滑动的时候,消费其滑动事件,并整体向上移动。在页面状态为 close 的时候,不消耗 RecyclerView 的 滑动事件。

在上一篇博客 一步步带你读懂 CoordinatorLayout 源码 中,我们有提到在 CoordinatorLayout中,我们可以通过 给子 View 自定义 Behavior 来处理事件。它是一个容器,实现了 NestedScrollingParent 接口。它并不会直接处理事件,而是会尽可能地交给子 View 的 Behavior 进行处理。因此,为了减少依赖,我们把这两部分的关系定义为 Content 依赖于 Header。Header 移动的时候,Content 跟着 移动。所以,我们在处理滑动事件的时候,只需要处理好 Header 部分的 Behavior 就oK了,Content 部分的 Behavior 不需要处理滑动事件,只需依赖于 Header ,跟着做相应的移动即可。


Header 部分的实现

Header 部分实现的两个关键点在于

  1. 在页面状态为 open 的时候,ViewPager 里面的 RecyclerView 向上滑动的时候,消费其滑动事件,并整体向上移动。在页面状态为 close 的时候,不消耗 RecyclerView 的 滑动事件
  2. 在页面状态为 open 的时候,向上滑动 Header 的时候,整体向上偏移。

第一个关键点的实现

这里区分页面状态是 open 还是 close 状态是通过 Header 是否移除屏幕来区分的,即 child.getTranslationY() == getHeaderOffsetRange() 。

1
2
3
4
5
private boolean isClosed(View child) {
boolean isClosed = child.getTranslationY() == getHeaderOffsetRange();
return isClosed;
}

NestedScrolling 机制深入解析博客中,我们对 NestedScrolling 机制做了如下的总结。

  • 在 Action_Down 的时候,Scrolling child 会调用 startNestedScroll 方法,通过 childHelper 回调 Scrolling Parent 的 startNestedScroll 方法。
  • 在 Action_move 的时候,Scrolling Child 要开始滑动的时候,会调用dispatchNestedPreScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否要先于 Child 进行 滑动,若需要的话,会调用 Parent 的 onNestedPreScroll 方法,协同 Child 一起进行滑动
  • 当 ScrollingChild 滑动完成的时候,会调用 dispatchNestedScroll 方法,通过 ChildHelper 询问 Scrolling Parent 是否需要进行滑动,需要的话,会 调用 Parent 的 onNestedScroll 方法
  • 在 Action_down,Action_move 的时候,会调用 Scrolling Child 的stopNestedScroll ,通过 ChildHelper 询问 Scrolling parent 的 stopNestedScroll 方法。
  • 如果需要处理 Fling 动作,我们可以通过 VelocityTrackerCompat 获得相应的速度,并在 Action_up 的时候,调用 dispatchNestedPreFling 方法,通过 ChildHelper 询问 Parent 是否需要先于 child 进行 Fling 动作
    在 Child 处理完 Fling 动作时候,如果 Scrolling Parent 还需要处理 Fling 动作,我们可以调用 dispatchNestedFling 方法,通过 ChildHelper ,调用 Parent 的 onNestedFling 方法

而 RecyclerView 也是 Scrolling Child (实现了 NestedScrollingChild 接口),RecyclerView 在开始滑动的 时候会先调用 CoordinatorLayout 的 startNestedScroll 方法,而 CoordinatorLayout 会 调用子 View 的 Behavior 的 startNestedScroll 方法。并且只有 boolean startNestedScroll 返回 TRUE 的 时候,才会调用接下里 Behavior 中的 onNestedPreScroll 和 onNestedScroll 方法。

所以,我们在 WeiboHeaderPagerBehavior 的 onStartNestedScroll 方法可以这样写,可以确保 只拦截垂直方向上的滚动事件,且当前状态是打开的并且还可以继续向上收缩的时候还会拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View
directTargetChild, View target, int nestedScrollAxes) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "onStartNestedScroll: nestedScrollAxes=" + nestedScrollAxes);
}

boolean canScroll = canScroll(child, 0);
//拦截垂直方向上的滚动事件且当前状态是打开的并且还可以继续向上收缩
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && canScroll &&
!isClosed(child);

}


拦截事件之后,我们需要在 RecyclerView 滑动之前消耗事件,并且移动 Header,让其向上偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target,
int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
//dy>0 scroll up;dy<0,scroll down
Log.i(TAG, "onNestedPreScroll: dy=" + dy);
float halfOfDis = dy;
// 不能滑动了,直接给 Header 设置 终值,防止出错
if (!canScroll(child, halfOfDis)) {
child.setTranslationY(halfOfDis > 0 ? getHeaderOffsetRange() : 0);
} else {
child.setTranslationY(child.getTranslationY() - halfOfDis);
}
//consumed all scroll behavior after we started Nested Scrolling
consumed[1] = dy;
}


当然,我们也需要处理 Fling 事件,在页面没有完全关闭的 时候,消费所有 fling 事件。

1
2
3
4
5
6
7
8
@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {
// consumed the flinging behavior until Closed
return !isClosed(child);
}


至于滑动到顶部的动画,我是通过 mOverScroller + FlingRunnable 来实现的 。完整代码如下。

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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
public class WeiboHeaderPagerBehavior extends ViewOffsetBehavior {
private static final String TAG = "UcNewsHeaderPager";
public static final int STATE_OPENED = 0;
public static final int STATE_CLOSED = 1;
public static final int DURATION_SHORT = 300;
public static final int DURATION_LONG = 600;

private int mCurState = STATE_OPENED;
private OnPagerStateListener mPagerStateListener;

private OverScroller mOverScroller;

private WeakReference<CoordinatorLayout> mParent;
private WeakReference<View> mChild;

public void setPagerStateListener(OnPagerStateListener pagerStateListener) {
mPagerStateListener = pagerStateListener;
}

public WeiboHeaderPagerBehavior() {
init();
}

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

private void init() {
mOverScroller = new OverScroller(BaseAPP.getAppContext());
}

@Override
protected void layoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
super.layoutChild(parent, child, layoutDirection);
mParent = new WeakReference<CoordinatorLayout>(parent);
mChild = new WeakReference<View>(child);
}

@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View
directTargetChild, View target, int nestedScrollAxes) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "onStartNestedScroll: nestedScrollAxes=" + nestedScrollAxes);
}

boolean canScroll = canScroll(child, 0);
//拦截垂直方向上的滚动事件且当前状态是打开的并且还可以继续向上收缩
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 && canScroll &&
!isClosed(child);

}

@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {
// consumed the flinging behavior until Closed

boolean coumsed = !isClosed(child);
Log.i(TAG, "onNestedPreFling: coumsed=" +coumsed);
return coumsed;
}

@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY, boolean consumed) {
Log.i(TAG, "onNestedFling: velocityY=" +velocityY);
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY,
consumed);

}

private boolean isClosed(View child) {
boolean isClosed = child.getTranslationY() == getHeaderOffsetRange();
return isClosed;
}

public boolean isClosed() {
return mCurState == STATE_CLOSED;
}

private void changeState(int newState) {
if (mCurState != newState) {
mCurState = newState;
if (mCurState == STATE_OPENED) {
if (mPagerStateListener != null) {
mPagerStateListener.onPagerOpened();
}

} else {
if (mPagerStateListener != null) {
mPagerStateListener.onPagerClosed();
}

}
}

}

// 表示 Header TransLationY 的值是否达到我们指定的阀值, headerOffsetRange,到达了,返回 false,
// 否则,返回 true。注意 TransLationY 是负数。
private boolean canScroll(View child, float pendingDy) {
int pendingTranslationY = (int) (child.getTranslationY() - pendingDy);
int headerOffsetRange = getHeaderOffsetRange();
if (pendingTranslationY >= headerOffsetRange && pendingTranslationY <= 0) {
return true;
}
return false;
}



@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, final View child, MotionEvent
ev) {

boolean closed = isClosed();
Log.i(TAG, "onInterceptTouchEvent: closed=" + closed);
if (ev.getAction() == MotionEvent.ACTION_UP && !closed) {
handleActionUp(parent,child);
}

return super.onInterceptTouchEvent(parent, child, ev);
}

@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target,
int dx, int dy, int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
//dy>0 scroll up;dy<0,scroll down
Log.i(TAG, "onNestedPreScroll: dy=" + dy);
float halfOfDis = dy;
// 不能滑动了,直接给 Header 设置 终值,防止出错
if (!canScroll(child, halfOfDis)) {
child.setTranslationY(halfOfDis > 0 ? getHeaderOffsetRange() : 0);
} else {
child.setTranslationY(child.getTranslationY() - halfOfDis);
}
//consumed all scroll behavior after we started Nested Scrolling
consumed[1] = dy;
}

// 需要注意的是 Header 我们是通过 setTranslationY 来移出屏幕的,所以这个值是负数
private int getHeaderOffsetRange() {
return BaseAPP.getInstance().getResources().getDimensionPixelOffset(R.dimen
.weibo_header_offset);
}

private void handleActionUp(CoordinatorLayout parent, final View child) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "handleActionUp: ");
}
if (mFlingRunnable != null) {
child.removeCallbacks(mFlingRunnable);
mFlingRunnable = null;
}
mFlingRunnable = new FlingRunnable(parent, child);
if (child.getTranslationY() < getHeaderOffsetRange() / 6.0f) {
mFlingRunnable.scrollToClosed(DURATION_SHORT);
} else {
mFlingRunnable.scrollToOpen(DURATION_SHORT);
}

}

private void onFlingFinished(CoordinatorLayout coordinatorLayout, View layout) {
changeState(isClosed(layout) ? STATE_CLOSED : STATE_OPENED);
}

public void openPager() {
openPager(DURATION_LONG);
}

/**
* @param duration open animation duration
*/
public void openPager(int duration) {
View child = mChild.get();
CoordinatorLayout parent = mParent.get();
if (isClosed() && child != null) {
if (mFlingRunnable != null) {
child.removeCallbacks(mFlingRunnable);
mFlingRunnable = null;
}
mFlingRunnable = new FlingRunnable(parent, child);
mFlingRunnable.scrollToOpen(duration);
}
}

public void closePager() {
closePager(DURATION_LONG);
}

/**
* @param duration close animation duration
*/
public void closePager(int duration) {
View child = mChild.get();
CoordinatorLayout parent = mParent.get();
if (!isClosed()) {
if (mFlingRunnable != null) {
child.removeCallbacks(mFlingRunnable);
mFlingRunnable = null;
}
mFlingRunnable = new FlingRunnable(parent, child);
mFlingRunnable.scrollToClosed(duration);
}
}

private FlingRunnable mFlingRunnable;

/**
* For animation , Why not use {@link android.view.ViewPropertyAnimator } to play animation
* is of the
* other {@link CoordinatorLayout.Behavior} that depend on this could not receiving the
* correct result of
* {@link View#getTranslationY()} after animation finished for whatever reason that i don't know
*/
private class FlingRunnable implements Runnable {
private final CoordinatorLayout mParent;
private final View mLayout;

FlingRunnable(CoordinatorLayout parent, View layout) {
mParent = parent;
mLayout = layout;
}

public void scrollToClosed(int duration) {
float curTranslationY = ViewCompat.getTranslationY(mLayout);
float dy = getHeaderOffsetRange() - curTranslationY;
if (BuildConfig.DEBUG) {
Log.d(TAG, "scrollToClosed:offest:" + getHeaderOffsetRange());
Log.d(TAG, "scrollToClosed: cur0:" + curTranslationY + ",end0:" + dy);
Log.d(TAG, "scrollToClosed: cur:" + Math.round(curTranslationY) + ",end:" + Math
.round(dy));
Log.d(TAG, "scrollToClosed: cur1:" + (int) (curTranslationY) + ",end:" + (int) dy);
}
mOverScroller.startScroll(0, Math.round(curTranslationY - 0.1f), 0, Math.round(dy +
0.1f), duration);
start();
}

public void scrollToOpen(int duration) {
float curTranslationY = ViewCompat.getTranslationY(mLayout);
mOverScroller.startScroll(0, (int) curTranslationY, 0, (int) -curTranslationY,
duration);
start();
}

private void start() {
if (mOverScroller.computeScrollOffset()) {
mFlingRunnable = new FlingRunnable(mParent, mLayout);
ViewCompat.postOnAnimation(mLayout, mFlingRunnable);
} else {
onFlingFinished(mParent, mLayout);
}
}

@Override
public void run() {
if (mLayout != null && mOverScroller != null) {
if (mOverScroller.computeScrollOffset()) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "run: " + mOverScroller.getCurrY());
}
ViewCompat.setTranslationY(mLayout, mOverScroller.getCurrY());
ViewCompat.postOnAnimation(mLayout, this);
} else {
onFlingFinished(mParent, mLayout);
}
}
}
}

/**
* callback for HeaderPager 's state
*/
public interface OnPagerStateListener {
/**
* do callback when pager closed
*/
void onPagerClosed();

/**
* do callback when pager opened
*/
void onPagerOpened();
}

}

第二个关键点的实现

在页面状态为 open 的时候,向上滑动 Header 的时候,整体向上偏移。

在第一个关键点的实现上,我们是通过自定义 Behavior 来处理 ViewPager 里面 RecyclerView 的移动的,那我们要怎样监听整个 Header 的滑动了。

那就是重写 LinearLayout,将滑动事件交给 ScrollingParent(这里是CoordinatorLayout) 去处理,CoordinatorLayout 再交给子 View 的 behavior 去处理。

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
public class NestedLinearLayout extends LinearLayout implements NestedScrollingChild {

private static final String TAG = "NestedLinearLayout";

private final int[] offset = new int[2];
private final int[] consumed = new int[2];

private NestedScrollingChildHelper mScrollingChildHelper;
private int lastY;

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

public NestedLinearLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public NestedLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initData();
}

private void initData() {
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
mScrollingChildHelper.setNestedScrollingEnabled(true);
}
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastY = (int) event.getRawY();
// 当开始滑动的时候,告诉父view
startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL
| ViewCompat.SCROLL_AXIS_VERTICAL);
break;
case MotionEvent.ACTION_MOVE:

return true;
}
return super.onInterceptTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "onTouchEvent: ACTION_MOVE=");
int y = (int) (event.getRawY());
int dy =lastY- y;
lastY = y;
Log.i(TAG, "onTouchEvent: lastY=" + lastY);
Log.i(TAG, "onTouchEvent: dy=" + dy);
// dy < 0 下拉, dy>0 赏花
if (dy >0) { // 上滑的时候才交给父类去处理
if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 如果找到了支持嵌套滚动的父类
&& dispatchNestedPreScroll(0, dy, consumed, offset)) {//
// 父类进行了一部分滚动
}
}else{
if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) // 如果找到了支持嵌套滚动的父类
&& dispatchNestedScroll(0, 0, 0,dy, offset)) {//
// 父类进行了一部分滚动
}
}
break;
}
return true;
}



private NestedScrollingChildHelper getScrollingChildHelper() {
return mScrollingChildHelper;
}

// 接口实现--------------------------------------------------

@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);
}
}


Content 部分的实现

Content 部分的实现也主要有两个关键点

  • 整体置于 Header 之下
  • Content 跟着 Header 移动。即 Header 位置发生变化的时候,Content 也需要随着调整位置。

第一个关键点的实现

整体置于 Header 之下。这个我们可以参考 APPBarLayout 的 behavior,它是这样处理的。

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
/**
* Copy from Android design library
* <p/>
* Created by xujun
*/
public abstract class HeaderScrollingViewBehavior extends ViewOffsetBehavior<View> {
private final Rect mTempRect1 = new Rect();
private final Rect mTempRect2 = new Rect();

private int mVerticalLayoutGap = 0;
private int mOverlayTop;

public HeaderScrollingViewBehavior() {
}

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

@Override
public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final int childLpHeight = child.getLayoutParams().height;
if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
// If the menu's height is set to match_parent/wrap_content then measure it
// with the maximum visible height

final List<View> dependencies = parent.getDependencies(child);
final View header = findFirstDependency(dependencies);
if (header != null) {
if (ViewCompat.getFitsSystemWindows(header) && !ViewCompat.getFitsSystemWindows(child)) {
// If the header is fitting system windows then we need to also,
// otherwise we'll get CoL's compatible measuring
ViewCompat.setFitsSystemWindows(child, true);

if (ViewCompat.getFitsSystemWindows(child)) {
// If the set succeeded, trigger a new layout and return true
child.requestLayout();
return true;
}
}

if (ViewCompat.isLaidOut(header)) {
int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
if (availableHeight == 0) {
// If the measure spec doesn't specify a size, use the current height
availableHeight = parent.getHeight();
}

final int height = availableHeight - header.getMeasuredHeight() + getScrollRange(header);
final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT ? View.MeasureSpec.EXACTLY : View.MeasureSpec.AT_MOST);

// Now measure the scrolling view with the correct height
parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);

return true;
}
}
}
return false;
}

@Override
protected void layoutChild(final CoordinatorLayout parent, final View child, final int layoutDirection) {
final List<View> dependencies = parent.getDependencies(child);
final View header = findFirstDependency(dependencies);

if (header != null) {
final CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
final Rect available = mTempRect1;
available.set(parent.getPaddingLeft() + lp.leftMargin, header.getBottom() + lp.topMargin,
parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
parent.getHeight() + header.getBottom() - parent.getPaddingBottom() - lp.bottomMargin);

final Rect out = mTempRect2;
GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(), child.getMeasuredHeight(), available, out, layoutDirection);

final int overlap = getOverlapPixelsForOffset(header);

child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);
mVerticalLayoutGap = out.top - header.getBottom();
} else {
// If we don't have a dependency, let super handle it
super.layoutChild(parent, child, layoutDirection);
mVerticalLayoutGap = 0;
}
}

float getOverlapRatioForOffset(final View header) {
return 1f;
}

final int getOverlapPixelsForOffset(final View header) {
return mOverlayTop == 0
? 0
: MathUtils.constrain(Math.round(getOverlapRatioForOffset(header) * mOverlayTop),
0, mOverlayTop);

}

private static int resolveGravity(int gravity) {
return gravity == Gravity.NO_GRAVITY ? GravityCompat.START | Gravity.TOP : gravity;
}

protected abstract View findFirstDependency(List<View> views);

protected int getScrollRange(View v) {
return v.getMeasuredHeight();
}

/**
* The gap between the top of the scrolling view and the bottom of the header layout in pixels.
*/
final int getVerticalLayoutGap() {
return mVerticalLayoutGap;
}

/**
* Set the distance that this view should overlap any {@link AppBarLayout}.
*
* @param overlayTop the distance in px
*/
public final void setOverlayTop(int overlayTop) {
mOverlayTop = overlayTop;
}

/**
* Returns the distance that this view should overlap any {@link AppBarLayout}.
*/
public final int getOverlayTop() {
return mOverlayTop;
}

}

这个基类的代码还是很好理解的,因为之前就说过了,正常来说被依赖的 View 会优先于依赖它的 View 处理,所以需要依赖的 View 可以在 measure/layout 的时候,找到依赖的 View 并获取到它的测量/布局的信息,这里的处理就是依靠着这种关系来实现的.

我们的实现类,需要重写的除了抽象方法 findFirstDependency 外,还需要重写 getScrollRange,我们把 Header
的 Id id_weibo_header 定义在 ids.xml 资源文件内,方便依赖的判断.

至于缩放的高度,根据 结果图 得知是 0,得出如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private int getFinalHeight() {
Resources resources = BaseAPP.getInstance().getResources();

return 0;
}

@Override
protected int getScrollRange(View v) {
if (isDependOn(v)) {
return Math.max(0, v.getMeasuredHeight() - getFinalHeight());
} else {
return super.getScrollRange(v);
}
}

第二个关键点的实现:

Content 跟着 Header 移动。即 Header 位置发生变化的时候,Content 也需要随着调整位置。

主要的逻辑就是 在 layoutDependsOn 方法里面,判断 dependcy 是不是 HeaderView ,是的话,返回TRUE,这样在 Header 位置发生变化的时候,会回调 onDependentViewChanged 方法,在该方法里面,做相应的偏移。TranslationY 是根据比例算出来的 translationY = (int) (-dependencyTranslationY / (getHeaderOffsetRange() * 1.0f) * getScrollRange(dependency));

完整代码如下:

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
public class WeiboContentBehavior extends HeaderScrollingViewBehavior {
private static final String TAG = "WeiboContentBehavior";

public WeiboContentBehavior() {
}

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

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return isDependOn(dependency);
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "onDependentViewChanged");
}
offsetChildAsNeeded(parent, child, dependency);
return false;
}

private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
float dependencyTranslationY = dependency.getTranslationY();
int translationY = (int) (-dependencyTranslationY / (getHeaderOffsetRange() * 1.0f) *
getScrollRange(dependency));
Log.i(TAG, "offsetChildAsNeeded: translationY=" + translationY);
child.setTranslationY(translationY);

}

@Override
protected View findFirstDependency(List<View> views) {
for (int i = 0, z = views.size(); i < z; i++) {
View view = views.get(i);
if (isDependOn(view)) return view;
}
return null;
}

@Override
protected int getScrollRange(View v) {
if (isDependOn(v)) {
return Math.max(0, v.getMeasuredHeight() - getFinalHeight());
} else {
return super.getScrollRange(v);
}
}

private int getHeaderOffsetRange() {
return BaseAPP.getInstance().getResources().getDimensionPixelOffset(R.dimen
.weibo_header_offset);
}

private int getFinalHeight() {
Resources resources = BaseAPP.getInstance().getResources();

return 0;
}

private boolean isDependOn(View dependency) {
return dependency != null && dependency.getId() == R.id.id_weibo_header;
}
}


题外话

  • NestedScrolling 机制,对比传统的事件分发机制真的很强大。这种仿新浪微博发现页效果, 如果用传统的事件分发机制来做,估计很难实现,处理起来会有一大堆坑。
  • 看完了这种仿新浪微博发现页的效果,你是不是学到了什么?如果让你 模仿 仿 QQ 浏览器首页效果,你能实现话。

最后,特别感谢写这篇博客 自定义Behavior的艺术探索-仿UC浏览器主页 的开发者,没有这篇博客作为参考,这种效果我很大几率是实现 不了的。大家觉得效果还不错的话,顺手到 github 上面给我 star,谢谢。github 地址


参考文章:

自定义Behavior的艺术探索-仿UC浏览器主页

github 地址

最后的最后,卖一下广告,欢迎大家关注我的微信公众号 徐公码字,扫一扫下方二维码或搜索微信号 stormjun94,即可关注。 目前专注于 Android 开发,主要分享 Android开发相关知识和一些相关的优秀文章,包括个人总结,职场经验等。