重复造轮子也是有意义的!PowerfulRecyclerView使用指导和源码分析

说起RecyclerView,大家肯定都不陌生,从名字可以知道它是只专注于recycle的一个组件,视图显示?交给LayoutManager,动画?交给SimpleItemAnimator,分割线?交给ItemDecoration,等等等等。这样的好处是各个功能之间充分解耦,用使用上来说就是list,grid,瀑布流随意切换,divider样式自定义,动画百花齐放。但是这种模式同样存在一些问题,比如谷歌并没有封装点击事件给我们调用,也没有像ListView那样有API可以直接生成header和footer。

如果你要求比较低,直接拿RecyclerView来用是没问题的,大不了点击事件什么的自己写嘛,不过作为一个有追求的程序员,这样显然是满足不了我的,所以我选择了对其进行封装。

看到这里可能有人会说,github上不是有一个封装的很好的库叫做UltimateRecyclerView了吗,为什么你还要花时间去重复造轮子呢?这里我想说,这样的第三方库直接拿来用确实很方便,但是我觉得你更应该去花时间自己封装一下,因为这期间你能学到的东西是很多的。比如说,你了解android的嵌套滑动机制吗?你知道recyclerView的工作原理吗?你明白adapter和view之间的观察者模式吗?这些如果你不了解没关系,你可以选择和我一样自己去封装一遍recyclerView,相信你会了解的,不过在这儿我会给大家带来我的分析,希望大家看完以后会有收获~

好了,下面就让我介绍下PowerfulRecyclerView的使用方式以及源码分析吧。

使用方式

大家可以先去我的github上下载代码,里面有一个sample,我会以这个sample中的代码来进行讲解。

一. 基本使用:

PowerfulSimpleRecyclerView:

该类支持直接使用,直接在xml中如下定义就可以。

1
2
3
4
5
6
7
8
<com.zjutkz.powerfulrecyclerview.ptr.PowerfulSimpleRecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:back_top_duration="350"/>

该类也支持简单的自定义(只能更换头部文字和图标)

1
2
3
4
5
private String[] headerStrings = new String[]{"下拉刷新","释放刷新","正在刷新..."};

private int[] headerDrawables = new int[]{R.drawable.down,R.drawable.up};

recycler.setHeaderStrings(headerStrings).setHeaderDrawables(headerDrawables);

PowerfulRecyclerView:

1
2
3
4
5
6
7
<com.zjutkz.powerfulrecyclerview.ptr.PowerfulRecyclerView
android:id="@+id/ptr_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:back_top_duration="350"
app:position_to_show="7"/>

该类支持完全自定义头部,不过你定义的头部必须要继承IHeaderView接口。然后通过setHeader(View header)设置。

1
2
3
4
5
6
7
8
9
10
11
12
public interface IHeaderView {

void pullToRefresh();

void releaseToRefresh();

void onRefresh();

void onReset(float distance, float fraction);

void onPull(float distance, float fraction);
}

两个类都支持设置recyclerView的头部(类似banner,运营区等等),通过addRecyclerViewHeader这个函数设置,这个header没有要求。

两个类支持的自定义属性:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PowerfulRecyclerView">

<attr name="refresh_distance" format="dimension"/>
<attr name="max_to_pull" format="dimension"/>
<attr name="back_top_duration" format="integer"/>
<attr name="position_to_show" format="integer"/>
<attr name="NoDataView" format="reference"/>
<attr name="isSwipeToRefresh" format="boolean"/>
</declare-styleable>
</resources>

refresh_distance:下拉到多少提示释放刷新
max_to_pull:最大下拉距离
back_top_duration:头部返回的时间
position_to_show:下滑多少个item之后显示返回头部按钮(针对需求)
noDataView:没有数据的时候显示的view
isSwipeToRefresh:是否使用SwipeToRefreshLayout

二. 其他使用

2.1 滑动删除和长按拖动:

1
recycler.prepareForDragAndSwipe(true, true);

第一个参数表示是否可以长按拖动,第二个参数表示是否可以滑动删除,默认不设置是不会有这两个效果的。注意如果你想使用这个效果,并且你的组件是有header的(recyclerView的header,比如banner)必须要在调用这个函数之前调用

1
recycler.addRecyclerViewHeader(header, true);

第二个参数必须要传true,不然头部会无法响应点击和滑动事件。默认的情况下你不想使用滑动删除和长按拖动直接调用

1
recycler.addRecyclerViewHeader(header);

你的Adapter要实现 ItemTouchAdapter 这个接口并且在回调中处理逻辑响应

1
2
3
4
5
6
public interface ItemTouchAdapter {

void onMove(int fromPosition, int toPosition);

void onDismiss(int position);
}

make your adapter like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ItemTouchAdapter{
@Override
public void onMove(int fromPosition, int toPosition) {
if(fromPosition < 0 || toPosition >= datas.size()){
return;
}
Collections.swap(datas, fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);
}

@Override
public void onDismiss(int position) {
if(position < 0 || position >= datas.size()){
return;
}
datas.remove(position);
notifyItemRemoved(position);
}
}

2.2 动画:

如果你想给recyclerView添加动画(指的是item的动画,例如item的移动,新增和删除),你的类要继承 BaseItemAnimator 这个类

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
public class FadeInAnimator extends BaseItemAnimator {

@Override
protected void onPreAnimateAdd(RecyclerView.ViewHolder holder) {
ViewCompat.setAlpha(holder.itemView, 0);
}

@Override
protected void onPreAnimateRemove(RecyclerView.ViewHolder holder) {
}

@Override
protected AnimatorSet generateRemoveAnimator(RecyclerView.ViewHolder holder) {
View target = holder.itemView;

AnimatorSet animator = new AnimatorSet();

animator.playTogether(
ObjectAnimator.ofFloat(target, "alpha", 1.0f, 0.0f)
);

animator.setTarget(target);
animator.setDuration(getRemoveDuration());

return animator;
}

@Override
protected AnimatorSet generateAddAnimator(RecyclerView.ViewHolder holder) {
View target = holder.itemView;

AnimatorSet animator = new AnimatorSet();

animator.playTogether(
ObjectAnimator.ofFloat(target, "alpha", 0.0f, 1.0f)
);

animator.setTarget(target);
animator.setDuration(getAddDuration());

return animator;
}
}

PowerfulRecyclerView库中提供了三个默认的实现,FadeIn,SlideIn和ZoomIn

2.3 使用SwipeRefreshLayout

如果你想使用SwipeRefreshLayout,你可以在xml中设置

1
app:isSwipeToRefresh="true"

或者在代码中调用

1
recycle.useSwipeRefreshLayout();

SwipeRefreshLayout有一些属性设置,比如圆圈的大小,颜色等等,你可以直接通过我这个组件进行设置

1
2
3
4
recycle.setColorSchemeResources(android.R.color.holo_blue_bright,
android.R.color.holo_green_light,
android.R.color.holo_orange_light,
android.R.color.holo_red_light);

2.4 其他支持

支持获取滚动高度,首个和最后一个可见item的位置等等。

三. 点击,刷新事件

通过实现OnRefreshListener和OnLoadMoreListener接口来回调刷新和加载事件。

like this:

1
2
3
4
5
6
7
8
9
10
11
public class RecyclerListViewActivity extends AppCompatActivity implements OnRefreshListener,OnLoadMoreListener {
@Override
public void onRefresh() {

}

@Override
public void onLoadMore() {

}
}

通过setOnItemClickListener和setOnItemLongClickListener去实现点击事件。

like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
recycler.setOnItemClickListener(new PowerfulRecyclerView.OnItemClickListener() {
@Override
public void onItemClick(RecyclerView parent, RecyclerView.ViewHolder holder, int position) {
Log.d(TAG, "onItemClick: " + position);
}
});

//如果设置了可以drag和swipe,不要设置长按监听器
/*recycler.setOnItemLongClickListener(new WdPtrrecycler.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(RecyclerView parent, RecyclerView.ViewHolder holder, int position) {

return true;
}
});*/

这里的position是去掉了头部以后的位置,和model层的list是一致的

四. 刷新逻辑

ListView中如果想刷新的话直接调用notifyDataSetChanged()就可以了,而recyclerView提供了更多的刷新函数,用来配合局部刷新和动画。

五. 注意事项

1.别忘了调用setLayoutManager(LayoutManager manager)函数。
2.如果你使用的是grid,要在setAdapter(Adapter adapter)函数之前调用setLayoutManager(LayoutManager manager),不然recyclerHeader的宽度会不对。
3.该UI库是支持嵌套滑动的,可以配合CoordinatorLayout来使用,比如加上AppBarLayout。
4.支持横向滑动,包括header和footer。

六. 改进

当设置了可以滑动删除并且存在recyclerHeader的时候滑动逻辑不是太好。
使用瀑布流的时候,当从底部滑动到最上面时,会有一个item交换的视觉效果。

源码分析

一. 嵌套滑动

说起下拉刷新库的滑动机制,大家第一时间想到的是什么?onTouchEvent, onInterceptTouchEvent和dispatchTouchEvent三个函数?来看看PowerfulRecyclerView是什么。

1
2
3
4
public class PowerfulRecyclerView extends LinearLayout implements OnViewClick,SwipeRefreshLayout.OnRefreshListener,NestedScrollingParent, NestedScrollingChild{
.........
.........
}

暂且忽略后面的一堆接口,它的本质是一个LinearLayout,这是为什么呢,因为我要在recyclerView的基础上添加一个刷新header啊,为了图方便,直接继承了一个LinearLayout,orientation是vertical,然后有两个child,一个header,一个RecyclerView,这样的好处是支持切换成SwipeRefreshLayout。因为只要把LinearLayout的child换掉就可以了。好了,既然是一个LinearLayout,一般情况下怎么处理呢。简单的说就是在onInterceptTouchEvent函数中判断滑动的前置条件,在onTouchEvent中处理滑动。这样其实已经够烦琐了,因为你得写一堆的函数去判断,如果你想要支持嵌套滑动呢?自己去写逻辑简直是一个噩梦,不信大家可以去试试,反正我是吃够了这种苦了。那PowerfulRecyclerView是怎么做的呢?可以说我在谷歌推出的机制上,用了一种很清晰的方式去完成逻辑的处理。

在分析源码之前,为了保证大家都明白什么是嵌套滑动,我推荐大家去看这篇文章这篇文章,它里面详细的介绍了嵌套滑动你所需要了解的东西。

ok,talk is cheap,show you the code.

首先,先看一下和嵌套滑动有关的三个变量:

1
2
3
4
5
6
7
8
/**
* recyclerView在坐标上未消费的总距离
*/

protected float mTotalUnconsumed;

protected final int[] mParentScrollConsumed = new int[2];

protected final int[] mParentOffsetInWindow = new int[2];

如果你看过我刚才提到的那篇文章,相信对这三个变量都不会陌生。

接着我们看PowerfulRecyclerView的定义:

1
public class PowerfulRecyclerView extends LinearLayout implements OnViewClick,SwipeRefreshLayout.OnRefreshListener,NestedScrollingParent, NestedScrollingChild

为什么既要实现NestedScrollingParent接口,又要实现NestedScrollingChild接口呢。这是因为PowerfulRecyclerView作为真正的RecyclerView的父组件,一般情况下它会作为CoordinatorLayout的子组件去进行使用,这句话有点拗口,大家可以看下面这个layout.xml

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
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true">


<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:fitsSystemWindows="true">


<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
app:title="App Title"
app:subtitle="Sub Title"
app:navigationIcon="@drawable/ic_toc_white_24dp"
app:layout_scrollFlags="scroll|enterAlways"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"/>


</android.support.design.widget.AppBarLayout>

<com.zjutkz.powerfulrecyclerview.ptr.PowerfulRecyclerView
android:id="@+id/ptr_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:back_top_duration="350"
app:position_to_show="7"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>



<ImageView
android:id="@+id/btn_return_to_top"
android:layout_width="54dp"
android:layout_height="54dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginBottom="10dp"
android:layout_marginRight="10dp"
android:background="@mipmap/ic_launcher"
android:visibility="gone"
app:layout_anchor="@id/main_content"
app:layout_anchorGravity="bottom|right"/>


</android.support.design.widget.CoordinatorLayout>

所以PowerfulRecycler对于内部的RecyclerView来说,它是NestedScrollingParent,但是对于根节点的CoordinatorLayout来说,它却是NestedScrollingChild,实际是起到了一个承上启下的作用,用于连接所有的组件进行嵌套滑动。那具体是怎么实现的呢?

下面是关于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
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
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
if(orientationMode == ORIENTATION_HORIZONTAL){
return isEnabled() && !(refreshState == STATE_ON_REFRESH) && !(refreshState == STATE_ON_RESET)
&& (nestedScrollAxes & ViewCompat.SCROLL_AXIS_HORIZONTAL) != 0;
}
return isEnabled() && !(refreshState == STATE_ON_REFRESH) && !(refreshState == STATE_ON_RESET)
&& (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}

@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
// 通知父view开始滚动

if(orientationMode == ORIENTATION_HORIZONTAL){
startNestedScroll(axes & ViewCompat.SCROLL_AXIS_HORIZONTAL);
}else{
startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);
}

mTotalUnconsumed = 0;
}

@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if(orientationMode == ORIENTATION_HORIZONTAL){
if (dx > 0 && mTotalUnconsumed > 0) {
if (dx > mTotalUnconsumed) {
consumed[0] = dx - (int) mTotalUnconsumed;
mTotalUnconsumed = 0;
} else {
mTotalUnconsumed -= dx;
consumed[0] = dx;

}
updateHeaderHeight(mTotalUnconsumed);
}

int[] parentConsumed = mParentScrollConsumed;
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}

return;
}
if (dy > 0 && mTotalUnconsumed > 0) {
if (dy > mTotalUnconsumed) {
consumed[1] = dy - (int) mTotalUnconsumed;
mTotalUnconsumed = 0;
} else {
mTotalUnconsumed -= dy;
consumed[1] = dy;

}
updateHeaderHeight(mTotalUnconsumed);
}

int[] parentConsumed = mParentScrollConsumed;
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}
}

@Override
public int getNestedScrollAxes() {
return mNestedScrollingParentHelper.getNestedScrollAxes();
}

@Override
public void onStopNestedScroll(View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);

//头部回滚
if (mTotalUnconsumed > 0) {
refreshByState();

mTotalUnconsumed = 0;
}

autoRefreshHeight = 0;

stopNestedScroll();
}

@Override
public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed,
final int dxUnconsumed, final int dyUnconsumed)
{

if(orientationMode == ORIENTATION_HORIZONTAL){
// 让父view先尝试滚动
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
mParentOffsetInWindow);

final int dx = dxUnconsumed + mParentOffsetInWindow[0];
if (dx < 0) {
mTotalUnconsumed += Math.abs(dx);
updateHeaderHeight(mTotalUnconsumed);
}

return;
}

// 让父view先尝试滚动
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
mParentOffsetInWindow);

final int dy = dyUnconsumed + mParentOffsetInWindow[1];
if (dy < 0) {
mTotalUnconsumed += Math.abs(dy);
updateHeaderHeight(mTotalUnconsumed);
}
}

下面是关于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
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
}

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

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

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

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

@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow)
{

return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}

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

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

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

@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
return dispatchNestedFling(velocityX, velocityY, consumed);
}

@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}

(1)我们要在合适的地方去调用setNestedScrollingEnabled这个函数,保证我们的组件是可以支持嵌套滑动的。

1
2
3
4
5
6
7
8
9
10
11
public PowerfulRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}

private void init(Context context, AttributeSet attrs, int defStyleAttr) {
setOrientation(VERTICAL);
.........无关代码
setNestedScrollingEnabled(true);
.........无关代码
}

直接在构造函数中调用就可以了。

(2)PRV(PowerfulRecyclerView)作为内部RecyclerView的父组件,它会接受到RecyclerView传递过来的滑动事件,我们看看RecyclerView内部是怎么开启滑动事件的。

1
2
3
4
5
6
7
8
9
10
@Override
public boolean onTouchEvent(MotionEvent e) {
..........无关代码
switch (action) {
case MotionEvent.ACTION_DOWN:
..........无关代码
startNestedScroll(nestedScrollAxis);
break;
..........无关代码
}

直接在onTouchEvent的ACTION_DOWN中开启嵌套滑动,那startNestedScroll具体做的是什么呢。

1
2
3
4
@Override
public boolean startNestedScroll(int axes) {
return mScrollingChildHelper.startNestedScroll(axes);
}

直接交给了mScrollingChildHelper,那我们再看看mScrollingChildHelper做了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}

其实就是找到组件的父组件通过调用父组件的onStartNestedScroll来判断其是否支持嵌套滑动,如果支持就调用其onNestedScrollAccepted。那在这里面RecyclerView的父组件就是我们的PRV,来看看PRV的onStartNestedScroll做了什么。

1
2
3
4
5
6
7
8
9
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
if(orientationMode == ORIENTATION_HORIZONTAL){
return isEnabled() && !(refreshState == STATE_ON_REFRESH) && !(refreshState == STATE_ON_RESET)
&& (nestedScrollAxes & ViewCompat.SCROLL_AXIS_HORIZONTAL) != 0;
}
return isEnabled() && !(refreshState == STATE_ON_REFRESH) && !(refreshState == STATE_ON_RESET)
&& (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}

如果是横向滑动模式,就支持横向嵌套滑动,如果是纵向模式,就支持纵向嵌套滑动。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
// 通知父view开始滚动

if(orientationMode == ORIENTATION_HORIZONTAL){
startNestedScroll(axes & ViewCompat.SCROLL_AXIS_HORIZONTAL);
}else{
startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);
}

mTotalUnconsumed = 0;
}

在onNestedScrollAccepted中,我们调用了startNestedScroll这个函数,这是为什么呢?还记得我们前面说的吗?PRV是作为一个中间件存在的,当内部的RecyclerView将嵌套滑动事件传递给我们之后,我们得去通知PRV的父组件(比如CoordinatorLayout)去开启嵌套滑动,就像RecyclerView对PRV做的。

1
2
3
4
@Override
public boolean startNestedScroll(int axes) {
return mNestedScrollingChildHelper.startNestedScroll(axes);
}

到这里,我们把如何开启嵌套滑动分析好了,流程大致是这样的。

RecyclerView的onTouchEvent中调用它的startNestedScroll ->

通过childHelper内部调用父组件(PRV)的onStartNestedScroll ->

PRV在onStartNestedScroll中判断是否支持滑动 ->

如果支持,childHelper内部调用PRV的onNestedScrollAccepted ->

PRV在onNestedScrollAccepted调用自己的startNestedScroll ->

通过PRV的childHelper去通知PRV的父组件(比如CoordinatorLayout)开启嵌套滑动,逻辑同上。

好了,分析完滑动的准备工作,下面继续分析真正的滑动过程。

作为滑动事件的初始点,我们来看看RecyclerView是怎么做的。

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
@Override
public boolean onTouchEvent(MotionEvent e) {
.......无关代码
case MotionEvent.ACTION_MOVE: {
.......无关代码

final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;

if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
dx -= mScrollConsumed[0];
dy -= mScrollConsumed[1];
vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
}

.......无关代码

if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];

if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
} break;

return true;
}

可以看到在RecyclerView的onTouchEvent的ACTION_MOVE中调用了dispatchNestedPreScroll函数,而真正RecyclerView自己的滑动是在scrollByInternal这个函数中执行的,这个函数我们等等再看。先看看dispatchNestedPreScroll这个函数做了什么。

1
2
3
4
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}

同样的,直接调用了childHelper的同名函数,跟踪进去看看。

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
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.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;
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

if (offsetInWindow != null) {
mView.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;
}
1
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

这句话表示调用了父组件的onNestedPreScroll函数,也就是PRV的onNestedPreScroll函数,我们来看一下。

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
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if(orientationMode == ORIENTATION_HORIZONTAL){
if (dx > 0 && mTotalUnconsumed > 0) {
if (dx > mTotalUnconsumed) {
consumed[0] = dx - (int) mTotalUnconsumed;
mTotalUnconsumed = 0;
} else {
mTotalUnconsumed -= dx;
consumed[0] = dx;

}
updateHeaderHeight(mTotalUnconsumed);
}

int[] parentConsumed = mParentScrollConsumed;
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}

return;
}
if (dy > 0 && mTotalUnconsumed > 0) {
if (dy > mTotalUnconsumed) {
consumed[1] = dy - (int) mTotalUnconsumed;
mTotalUnconsumed = 0;
} else {
mTotalUnconsumed -= dy;
consumed[1] = dy;

}
updateHeaderHeight(mTotalUnconsumed);
}

int[] parentConsumed = mParentScrollConsumed;
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}
}

这个函数是重点了。

首先判断滑动模式是横向还是纵向,我们就挑纵向看好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (dy > 0 && mTotalUnconsumed > 0) {
if (dy > mTotalUnconsumed) {
consumed[1] = dy - (int) mTotalUnconsumed;
mTotalUnconsumed = 0;
} else {
mTotalUnconsumed -= dy;
consumed[1] = dy;

}
updateHeaderHeight(mTotalUnconsumed);
}

int[] parentConsumed = mParentScrollConsumed;
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}

首先,dy是RecyclerView传递过来的,意思是纵向上滑动的距离。mTotalUnconsumed表示的意思是RecyclerView在坐标上未消费的总距离,它在初始化嵌套滑动的时候被设置为0。所以在现在这种情况下(刚开始滑动),这个if语句是不会执行的,真正有用的是下面这句。

1
2
3
4
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}

是不是很熟悉,又执行了dispatchNestedPreScroll函数。

1
2
3
4
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}

它和之前RecyclerView的逻辑一样,会调用childHelper的dispatchNestedPreScroll,然后就把滑动事件分发到了PRV的父组件上。为什么要这么做呢?因为嵌套滑动的一个理念就是“子组件在滑动前要通知父组件从而使其有机会先滑动“,具体来说,比如你使用了Toolbar,下面是PRV,向上滚动隐藏Toolbar,向下滚动显示Toolbar。我们希望在PRV滚动之前,隐藏的Toolbar能显示出来,这就要求Toolbar的滚动优先于PRV,从代码的角度来说就是我们上面的那个逻辑,PRV作为一个中转,在RecyclerView的滑动事件分发到自己这儿以后,直接分发给父组件,比如CoordinatorLayout,询问它是否要处理滑动逻辑。

好了,讲完了dispatchNestedPreScroll,接下去分析RecyclerView真正处理滑动的函数:

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
boolean scrollByInternal(int x, int y, MotionEvent ev) {
int unconsumedX = 0, unconsumedY = 0;
int consumedX = 0, consumedY = 0;

consumePendingUpdateOperations();
if (mAdapter != null) {
eatRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
TraceCompat.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
resumeRequestLayout(false);
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}

if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
} else if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) {
if (ev != null) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
if (!awakenScrollBars()) {
invalidate();
}
return consumedX != 0 || consumedY != 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
consumePendingUpdateOperations();
if (mAdapter != null) {
eatRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
if (x != 0) {
consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
unconsumedX = x - consumedX;
}
if (y != 0) {
consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
unconsumedY = y - consumedY;
}
TraceCompat.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
resumeRequestLayout(false);
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}

这一段函数是RecyclerView本身滑动的逻辑,通过LayoutManager去处理滑动,这也符合RecyclerView的设计,功能解耦。在这儿之后,就是嵌套滑动了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
} else if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) {
if (ev != null) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
if (!awakenScrollBars()) {
invalidate();
}

可以看到调用了dispatchNestedScroll函数。

1
2
3
4
5
6
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow)
{

return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}

和之前的函数一样,也是调用的childHelper的同名函数。看看childHelper做了什么。

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
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)
{

if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}

ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
dyConsumed, dxUnconsumed, dyUnconsumed);

if (offsetInWindow != null) {
mView.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;
}

可以看到调用了parent的onNestedScroll函数,也就是PRV的onNestedScroll函数。

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
@Override
public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed,
final int dxUnconsumed, final int dyUnconsumed) {
if(orientationMode == ORIENTATION_HORIZONTAL){
// 让父view先尝试滚动
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
mParentOffsetInWindow);

final int dx = dxUnconsumed + mParentOffsetInWindow[0];
if (dx < 0) {
mTotalUnconsumed += Math.abs(dx);
updateHeaderHeight(mTotalUnconsumed);
}

return;
}

// 让父view先尝试滚动
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
mParentOffsetInWindow);

final int dy = dyUnconsumed + mParentOffsetInWindow[1];
if (dy < 0) {
mTotalUnconsumed += Math.abs(dy);
updateHeaderHeight(mTotalUnconsumed);
}
}

开始还是判断滑动模式,我们照旧看纵向模式。

1
2
3
4
5
6
7
8
9
// 让父view先尝试滚动
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
mParentOffsetInWindow);

final int dy = dyUnconsumed + mParentOffsetInWindow[1];
if (dy < 0) {
mTotalUnconsumed += Math.abs(dy);
updateHeaderHeight(mTotalUnconsumed);
}

开始还是一样的逻辑,让父组件先尝试滚动,这点真的很重要,因为嵌套滑动解决的问题就是“如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下。“这句话哪里来呢,当然是照搬了前面提到的这篇文章啦,哈哈,不过我个人是很认同这点的。好了言归正传,在父组件尝试滚动完以后,就是RPV本身的滚动了。

1
2
3
4
if (dy < 0) {
mTotalUnconsumed += Math.abs(dy);
updateHeaderHeight(mTotalUnconsumed);
}

如果dy<0,也就是手指是向下的,就去调用updateHeaderHeight函数。

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
private void updateHeaderHeight(float distance) {
if(orientationMode == ORIENTATION_HORIZONTAL){
if(!isSwipeToRefresh && mHeaderLayoutParams != null && mHeaderViewContainer != null && refreshEnable){
updateHeaderState();

distance = Math.min(pullMax * 2, distance);
distance = Math.max(0, distance);
//为了产生阻尼效果,实际下拉的距离为手指下拉距离的一半
float offsetX = (int)(distance / 2);

mHeaderLayoutParams.leftMargin = (int)offsetX - pullMax;
mHeaderViewContainer.setLayoutParams(mHeaderLayoutParams);

if (mHeaderLayoutParams.leftMargin >= (refreshDistance - pullMax)) {
refreshState = STATE_RELEASE_TO_REFRESH;
}else{
refreshState = STATE_PULL_TO_REFRESH;
}

if(mHeaderViews != null && mHeaderViews.size() > 0){

for (IHeaderView headerView : mHeaderViews) {

headerView.onPull(offsetX,offsetX / mHeaderViewContainer.getWidth());
}
}
}
return;
}

if(!isSwipeToRefresh && mHeaderLayoutParams != null && mHeaderViewContainer != null && refreshEnable){
updateHeaderState();

distance = Math.min(pullMax * 2, distance);
distance = Math.max(0, distance);
//为了产生阻尼效果,实际下拉的距离为手指下拉距离的一半
float offsetY = (int)(distance / 2);

mHeaderLayoutParams.topMargin = (int)offsetY - pullMax;
mHeaderViewContainer.setLayoutParams(mHeaderLayoutParams);

if (mHeaderLayoutParams.topMargin >= (refreshDistance - pullMax)) {
refreshState = STATE_RELEASE_TO_REFRESH;
}else{
refreshState = STATE_PULL_TO_REFRESH;
}

if(mHeaderViews != null && mHeaderViews.size() > 0){

for (IHeaderView headerView : mHeaderViews) {

headerView.onPull(offsetY,offsetY / mHeaderViewContainer.getHeight());
}
}
}
}

照旧看纵向模式。。

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
 if(!isSwipeToRefresh && mHeaderLayoutParams != null && mHeaderViewContainer != null && refreshEnable){
updateHeaderState();

distance = Math.min(pullMax * 2, distance);
distance = Math.max(0, distance);
//为了产生阻尼效果,实际下拉的距离为手指下拉距离的一半
float offsetY = (int)(distance / 2);

mHeaderLayoutParams.topMargin = (int)offsetY - pullMax;
mHeaderViewContainer.setLayoutParams(mHeaderLayoutParams);

if (mHeaderLayoutParams.topMargin >= (refreshDistance - pullMax)) {
refreshState = STATE_RELEASE_TO_REFRESH;
}else{
refreshState = STATE_PULL_TO_REFRESH;
}

if(mHeaderViews != null && mHeaderViews.size() > 0){

for (IHeaderView headerView : mHeaderViews) {

headerView.onPull(offsetY,offsetY / mHeaderViewContainer.getHeight());
}
}
}
}

这段代码相信大家都懂,就是将refresh header显示出来并且判断刷新状态而已,没啥特别的。

滑动完以后,停止滑动逻辑是怎样的呢?先来看RecyclerView的源码。

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
@Override
public boolean onTouchEvent(MotionEvent e) {
......无关代码

case MotionEvent.ACTION_UP: {
......无关代码

resetTouch();
} break;

case MotionEvent.ACTION_CANCEL: {
cancelTouch();
} break;
}

......无关代码
return true;
}

private void resetTouch() {
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
stopNestedScroll();
releaseGlows();
}

private void cancelTouch() {
resetTouch();
setScrollState(SCROLL_STATE_IDLE);
}

可以看到在onTouchEvent的ACTION_UP和ACTION_CANCEL中分别调用了resetTouch和cancelTouch函数,而在cancelTouch中调用了resetTouch,最终都会调用stopNestedScroll。

1
2
3
4
@Override
public void stopNestedScroll() {
mScrollingChildHelper.stopNestedScroll();
}

好吧,我承认这步多余了。。不用看也知道就是调用了childHelper的同名函数,看到这儿可能有人会有疑问,为什么嵌套滑动作为child的时候,都只要调用childHelper的同名函数呢?其实仔细想想,这是合理的,因为作为嵌套滑动的child,你所需要做的事只是告诉parent,有个滑动事件唉,你要不要用?至于各种逻辑判断都是一样的,直接封装好就行了,还省得我们自己写。

1
2
3
4
5
6
public void stopNestedScroll() {
if (mNestedScrollingParent != null) {
ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
mNestedScrollingParent = null;
}
}

childHelper的stopNestedScroll很简单,调用父组件的onStopNestedScroll并且把scrollingParent设置为null。我们来看一看PRV做了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void onStopNestedScroll(View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);

//头部回滚
if (mTotalUnconsumed > 0) {
refreshByState();

mTotalUnconsumed = 0;
}

autoRefreshHeight = 0;

stopNestedScroll();
}

首先如果refresh header是显示出来的,将其回滚,接着通知父组件。至于refreshByState这个函数就不跟进去看了。

好了,嵌套滑动的滑动逻辑到这儿就分析完了,做个总结吧。以前面那个layout.xml为例。

RecyclerView的onTouchEvent的ACTION_MOVE中调用dispatchNestedPreScroll ->

RecyclerView的childHelper的dispatchNestedPreScroll ->

PRV的onNestedPreScroll,如果refresh header是显示的,操作refresh header,之后调用dispatchNestedPreScroll ->

PRV的childHelper的dispatchNestedPreScroll ->

CoordinatorLayout的onNestedPreScroll,处理其子附件的滑动,比如Toolbar的显示和隐藏。

RecyclerView的scrollByInternal,先执行RecyclerView本身的滑动的逻辑(通过LayoutManager),然后调用dispatchNestedScroll ->

RecyclerView的childHelper的dispatchNestedScroll ->

PRV的onNestedScroll,先让父组件尝试滚动,接着执行refresh head ->

RecyclerView的onTouchEvent的ACTION_UP或ACTION_CANCEL中调用stopNestedScroll ->

RecyclerView的childHelper的stopNestedScroll ->

PRV的onStopNestedScroll,如果refresh header显示的话将其隐藏,调用stopNestedScroll->

PRV的childHelper的stopNestedScroll ->

CoordinatorLayout的onStopNestedScroll

二. 点击事件

对于点击事件,最简单的方式就是在我PowerfulRecyclerView内部的那个adapter的onBindViewHolder函数中设置,就像这样

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
@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
if(hasFootView() && hasHeaderView()){
if(position < getHeaderViewCount() || position == getItemCount() - 1){
//do nothing
}else{
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
trueAdapter.onClick(holder,position - getHeaderViewCount());
}
});

holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return trueAdapter.onLongClick(holder,position - getHeaderViewCount());
}
});

mPlugAdapter.onBindViewHolder(holder, position - getHeaderViewCount());
}
}else if(hasFootView() && !hasHeaderView()){
if(position == getItemCount() - 1){
//do nothing
}else{

holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
trueAdapter.onClick(holder, position);
}
});

holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return trueAdapter.onLongClick(holder, position);
}
});

mPlugAdapter.onBindViewHolder(holder,position);
}
}else if(!hasFootView() && hasHeaderView()){
if(position < getHeaderViewCount()){
//do nothing
}else{

holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
trueAdapter.onClick(holder,position - getHeaderViewCount());
}
});

holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return trueAdapter.onLongClick(holder,position - getHeaderViewCount());
}
});

mPlugAdapter.onBindViewHolder(holder,position - getHeaderViewCount());
}
}else{

holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
trueAdapter.onClick(holder,position);
}
});

holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return trueAdapter.onLongClick(holder,position);
}
});

mPlugAdapter.onBindViewHolder(holder, position);
}
}

忽略我写的乱七八糟的逻辑。。。汗。但是我这里提供了另外一思路去实现点击事件,就是使用RecyclerView提供的OnItemTouchListener,这是一个什么东西呢?还是来看RecyclerView源码吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Add an {@link OnItemTouchListener} to intercept touch events before they are dispatched
* to child views or this view's standard scrolling behavior.
*
* <p>Client code may use listeners to implement item manipulation behavior. Once a listener
* returns true from
* {@link OnItemTouchListener#onInterceptTouchEvent(RecyclerView, MotionEvent)} its
* {@link OnItemTouchListener#onTouchEvent(RecyclerView, MotionEvent)} method will be called
* for each incoming MotionEvent until the end of the gesture.</p>
*
* @param listener Listener to add
* @see SimpleOnItemTouchListener
*/

public void addOnItemTouchListener(OnItemTouchListener listener) {
mOnItemTouchListeners.add(listener);
}

这里我把注释也写了进来,看注释应该很清楚了,就是给RecyclerView一个”intercept touch events before they are dispatched”的机会。下面看看它具体在哪儿调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if (mLayoutFrozen) {
// When layout is frozen, RV does not intercept the motion event.
// A child view e.g. a button may still get the click.
return false;
}
if (dispatchOnItemTouchIntercept(e)) {
cancelTouch();
return true;
}

if (mLayout == null) {
return false;
}

.......siwtch各种MotionEvent
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public boolean onTouchEvent(MotionEvent e) {
if (mLayoutFrozen || mIgnoreMotionEventTillDown) {
return false;
}
if (dispatchOnItemTouch(e)) {
cancelTouch();
return true;
}

if (mLayout == null) {
return false;
}

.......siwtch各种MotionEvent
}
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
private boolean dispatchOnItemTouchIntercept(MotionEvent e) {
final int action = e.getAction();
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_DOWN) {
mActiveOnItemTouchListener = null;
}

final int listenerCount = mOnItemTouchListeners.size();
for (int i = 0; i < listenerCount; i++) {
final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) {
mActiveOnItemTouchListener = listener;
return true;
}
}
return false;
}

private boolean dispatchOnItemTouch(MotionEvent e) {
final int action = e.getAction();
if (mActiveOnItemTouchListener != null) {
if (action == MotionEvent.ACTION_DOWN) {
// Stale state from a previous gesture, we're starting a new one. Clear it.
mActiveOnItemTouchListener = null;
} else {
mActiveOnItemTouchListener.onTouchEvent(this, e);
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
// Clean up for the next gesture.
mActiveOnItemTouchListener = null;
}
return true;
}
}

// Listeners will have already received the ACTION_DOWN via dispatchOnItemTouchIntercept
// as called from onInterceptTouchEvent; skip it.
if (action != MotionEvent.ACTION_DOWN) {
final int listenerCount = mOnItemTouchListeners.size();
for (int i = 0; i < listenerCount; i++) {
final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
if (listener.onInterceptTouchEvent(this, e)) {
mActiveOnItemTouchListener = listener;
return true;
}
}
}
return false;
}

看清楚了吧,就是在onInterceptTouchEvent和onTouchEvent函数的最开始调用,来实现”intercept touch events before they are dispatched”功能。具体dispatchOnItemTouchIntercept和dispatchOnItemTouch的逻辑应该是很好理解的,牵扯到的事件分发机制大家如果不了解可以去看我之前提到的这篇文章

哦对了,说个题外话,文章的作者郭大侠结婚了。。记得刚学Android那会儿天天看他和hongyang的博客,转眼他俩都结婚了还在博客里秀恩爱。。恭喜恭喜~

咳咳,好了好了回归正题,其实也没啥好说的了,具体看下我实现的OnItemTouchListener吧。

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
public class ItemTouchListenerAdapter extends GestureDetector.SimpleOnGestureListener implements RecyclerView.OnItemTouchListener {

private OnViewClick listener;
private RecyclerView recyclerView;
private GestureDetector gestureDetector;

public ItemTouchListenerAdapter(RecyclerView recyclerView, OnViewClick listener) {
if (recyclerView == null || listener == null) {
throw new IllegalArgumentException("RecyclerView and Listener arguments can not be null");
}
this.recyclerView = recyclerView;
this.listener = listener;
this.gestureDetector = new GestureDetector(recyclerView.getContext(), this);
}

@Override
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {
gestureDetector.onTouchEvent(motionEvent);
return false;
}

@Override
public void onTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {
}

@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

}
@Override
public void onShowPress(MotionEvent e) {
View view = getChildViewUnder(e);
if (view != null) {
view.setPressed(true);
}
}

@Override
public boolean onSingleTapUp(MotionEvent e) {
View view = getChildViewUnder(e);
if (view == null) return false;

view.setPressed(false);

int position = recyclerView.getChildAdapterPosition(view);

RecyclerView.ViewHolder holder = getViewHolderUnder(position);

RecyclerView.Adapter mAdapter = recyclerView.getAdapter();
if(mAdapter instanceof PowerfulRecyclerAdapter){
position -= ((PowerfulRecyclerAdapter) mAdapter).getHeaderViewCount();
}

if(position < 0){
return false;
}

listener.onClick(holder, position);
return true;
}

public void onLongPress(MotionEvent e) {
View view = getChildViewUnder(e);
if (view == null) return;

int position = recyclerView.getChildAdapterPosition(view);

RecyclerView.Adapter mAdapter = recyclerView.getAdapter();
if(mAdapter instanceof PowerfulRecyclerAdapter){
position -= ((PowerfulRecyclerAdapter) mAdapter).getHeaderViewCount();
}

RecyclerView.ViewHolder holder = getViewHolderUnder(position);

if(position < 0){
return;
}

listener.onLongClick(holder, position);
view.setPressed(false);
}

@Nullable
private View getChildViewUnder(MotionEvent e) {
return recyclerView.findChildViewUnder(e.getX(), e.getY());
}

private RecyclerView.ViewHolder getViewHolderUnder(int position){
return recyclerView.findViewHolderForAdapterPosition(position);
}
}

很简单对吧,就是通过SimpleOnGestureListener去捕捉点击事件并且分发。

如果大家看过PowerfulRecyclerView的源码的话,会发现针对于RecyclerView header,比如banner的滑动,我也是通过这种设置OnItemTouchListener的方式去实现的,不过由于个人才疏学浅,对于其中一个小细节始终找不到完美的解决方案,只能用了个比较偷懒的方式,如果有大神有好的方法的话,请务必不吝赐教,谢谢了!

说到这里我想说,大家如果阅读过源码就会发现RecyclerView提供了很多很方便的机制,比如上面我说的这个,如果下次大家对RecyclerView的滑动,点击事件要做处理的话,不妨试试看OnItemTouchListener吧。

三. RecyclerView源码分析

对于RecyclerView源码的分析,其实我想了蛮久的,到底要不要把所有的都写出来,后来决定只写两部分——Adapter的观察者模式,RecyclerView的recycler机制。为什么只写两部分呢,主要原因是RecyclerView的源码实在太多了。。。光RecyclerView.java这个类就有整整11038行代码,这是什么概念啊同志们,将近10W行啊!咳咳。。开个玩笑,不过如果把这10000行代码分析完,加上其他各种类,比如LayoutManager,ChildHelper等等,估计得几天几夜吧。。所以我挑了两个我个人觉得值得关注的点。

Adapter的观察者模式

大家都知道对于ListView来说,如果adapter的数据改变了,只需要调用notifyDataSetChanged方法就可以了,而对于RecyclerView来说,它提供了其他很多方法,比如notifyItemInsert,notifyItemRemove等等,这些主要是用于局部刷新和动画的,但是为什么调用了这样的方法,滑动组件就能更新视图呢?

Let’s read the source code

1
2
3
4
5
6
public void setAdapter(Adapter adapter) {
// bail out if layout is frozen
setLayoutFrozen(false);
setAdapterInternal(adapter, false, true);
requestLayout();
}

可以看到重要的最后两行,先调用了internal,再requestLayout,而requestLayout会使RecyclerView重新执行measure,layout,draw。来看看internal做了什么。

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
private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews)
{

if (mAdapter != null) {
mAdapter.unregisterAdapterDataObserver(mObserver);
mAdapter.onDetachedFromRecyclerView(this);
}
if (!compatibleWithPrevious || removeAndRecycleViews) {
// end all running animations
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
}
// Since animations are ended, mLayout.children should be equal to
// recyclerView.children. This may not be true if item animator's end does not work as
// expected. (e.g. not release children instantly). It is safer to use mLayout's child
// count.
if (mLayout != null) {
mLayout.removeAndRecycleAllViews(mRecycler);
mLayout.removeAndRecycleScrapInt(mRecycler);
}
// we should clear it here before adapters are swapped to ensure correct callbacks.
mRecycler.clear();
}
mAdapterHelper.reset();
final Adapter oldAdapter = mAdapter;
mAdapter = adapter;
if (adapter != null) {
adapter.registerAdapterDataObserver(mObserver);
adapter.onAttachedToRecyclerView(this);
}
if (mLayout != null) {
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
mState.mStructureChanged = true;
markKnownViewsInvalid();
}

其实最重要的是

1
2
3
4
5
6
7
8
9
10
final Adapter oldAdapter = mAdapter;
mAdapter = adapter;
if (adapter != null) {
adapter.registerAdapterDataObserver(mObserver);
adapter.onAttachedToRecyclerView(this);
}
if (mLayout != null) {
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);

可以看到调用了adapter的registerAdapterDataObserver方法。

1
2
3
public void registerAdapterDataObserver(AdapterDataObserver observer) {
mObservable.registerObserver(observer);
}

这个mObservable是什么呢?

1
2
3
4
public static abstract class Adapter<VH extends ViewHolder> {
private final AdapterDataObservable mObservable = new AdapterDataObservable();
........
}

是一个AdapterDataObservable,那AdapterDataObservable又是啥呢?

1
2
3
static class AdapterDataObservable extends Observable<AdapterDataObserver> {
...........
}

可以看到是继承了Observable的。

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
/**
* Provides methods for registering or unregistering arbitrary observers in an {@link ArrayList}.
*
* This abstract class is intended to be subclassed and specialized to maintain
* a registry of observers of specific types and dispatch notifications to them.
*
* @param T The observer type.
*/

public abstract class Observable<T> {
/**
* The list of observers. An observer can be in the list at most
* once and will never be null.
*/

protected final ArrayList<T> mObservers = new ArrayList<T>();

/**
* Adds an observer to the list. The observer cannot be null and it must not already
* be registered.
* @param observer the observer to register
* @throws IllegalArgumentException the observer is null
* @throws IllegalStateException the observer is already registered
*/

public void registerObserver(T observer) {
if (observer == null) {
throw new IllegalArgumentException("The observer is null.");
}
synchronized(mObservers) {
if (mObservers.contains(observer)) {
throw new IllegalStateException("Observer " + observer + " is already registered.");
}
mObservers.add(observer);
}
}

/**
* Removes a previously registered observer. The observer must not be null and it
* must already have been registered.
* @param observer the observer to unregister
* @throws IllegalArgumentException the observer is null
* @throws IllegalStateException the observer is not yet registered
*/

public void unregisterObserver(T observer) {
if (observer == null) {
throw new IllegalArgumentException("The observer is null.");
}
synchronized(mObservers) {
int index = mObservers.indexOf(observer);
if (index == -1) {
throw new IllegalStateException("Observer " + observer + " was not registered.");
}
mObservers.remove(index);
}
}

/**
* Remove all registered observers.
*/

public void unregisterAll() {
synchronized(mObservers) {
mObservers.clear();
}
}
}

可以看到,这是一个很典型的观察者模式。对于观察者模式不清楚的同学可以看看这篇文章

再回到上面,setAdapterInternal函数中把oberser注册到了observable中,我们已经知道observable是RecyclerView内部的AdapterDataObservable,那observer是什么呢?

1
private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();
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
private class RecyclerViewDataObserver extends AdapterDataObserver {
@Override
public void onChanged() {
assertNotInLayoutOrScroll(null);
if (mAdapter.hasStableIds()) {
// TODO Determine what actually changed.
// This is more important to implement now since this callback will disable all
// animations because we cannot rely on positions.
mState.mStructureChanged = true;
setDataSetChangedAfterLayout();
} else {
mState.mStructureChanged = true;
setDataSetChangedAfterLayout();
}
if (!mAdapterHelper.hasPendingUpdates()) {
requestLayout();
}
}

@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
triggerUpdateProcessor();
}
}

@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
triggerUpdateProcessor();
}
}

@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
triggerUpdateProcessor();
}
}

@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeMoved(fromPosition, toPosition, itemCount)) {
triggerUpdateProcessor();
}
}

void triggerUpdateProcessor() {
if (mPostUpdatesOnAnimation && mHasFixedSize && mIsAttached) {
ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
} else {
mAdapterUpdateDuringMeasure = true;
requestLayout();
}
}
}

怎么样,这个类里面的方法大家有没有很熟悉,好像和Adapter的那些notify方法很像啊,让我们看看那些notify方法做了什么,以insert为例。

1
2
3
public final void notifyItemInserted(int position) {
mObservable.notifyItemRangeInserted(position, 1);
}

调用了mObservable,也就是我们上面分析的AdapterDataObservable的notifyItemRangeInserted方法。看看这个方法具体做了什么。

1
2
3
4
5
6
7
8
9
public void notifyItemRangeInserted(int positionStart, int itemCount) {
// since onItemRangeInserted() is implemented by the app, it could do anything,
// including removing itself from {@link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {@link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
}
}

把它内部的mObservers遍历一遍,调用onItemRangeInserted方法,这下大家明白了吧。其实就是obeservable去通知oberser,调用onItemRangeInserted。

1
2
3
4
5
6
7
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
triggerUpdateProcessor();
}
}

这是我们刚刚看过的RecyclerViewDataObserver的代码,内部具体做了什么我们不做分析了,其实就是根据不同的类型(Insert,Remove等等)注册信息并且执行动画。

让我们做个总结吧。

当我们setAdapter的时候,RecyclerView内部会吧自己的observer注册到observable中,而在adapter调用对应的notify函数的时候,observable会去通知那些注册到它这儿的observer去执行相信的动作。一个典型的观察者模式有木有。其实PoewerfulRecyclerView中有用到这个机制,如果大家看过PRV的源码会发现其实我在内部有一个PowerfulRecyclerViewAdapter用来封装上层app自己的adapter,但是这个PowerfulRecyclerViewAdapter对上层app是透明的,也就是说上层app的数据发生了变化,他们调用的是他们自己的adapter,那我的PowerfulRecyclerViewAdapter是怎么捕捉到这个变化的呢?还是看源码吧。

下面这个是PowerfulRecyclerView的setAdapter方法。

1
2
3
4
5
6
7
8
9
10
public void setAdapter(RecyclerView.Adapter adapter){

if(mPowerfulRecyclerAdapter == null){
mPowerfulRecyclerAdapter = new PowerfulRecyclerAdapter(adapter);
}

if(mRecyclerView != null){
mRecyclerView.setAdapter(mPowerfulRecyclerAdapter);
}
}

可以看到内部创建了一个PowerfulRecyclerAdapter并且封装。来看看PowerfulRecyclerAdapter的构造函数。

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
public PowerfulRecyclerAdapter(final RecyclerView.Adapter mPlugAdapter){
this.mPlugAdapter = mPlugAdapter;

mHeaderViews = new ArrayList<View>();

mPlugAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
PowerfulRecyclerAdapter.this.notifyDataSetChanged();
}

@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
PowerfulRecyclerAdapter.this.notifyItemRangeChanged(positionStart + getHeaderViewCount(), itemCount);
}

@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
int truePositionStart = positionStart + getHeaderViewCount();

PowerfulRecyclerAdapter.this.notifyItemRangeInserted(truePositionStart, itemCount);
}

@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
int truePositionStart = positionStart + getHeaderViewCount();

PowerfulRecyclerAdapter.this.notifyItemRangeRemoved(truePositionStart, itemCount);
}

@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
int truePositionStart = fromPosition + getHeaderViewCount();
int truePositionEnd = toPosition + getHeaderViewCount();

PowerfulRecyclerAdapter.this.notifyItemMoved(truePositionStart, truePositionEnd);
}
});
}

PowerfulRecyclerViewAdapter的构造函数内部自己去新建了一个observer,并且注册到mPlugAdapter中。

1
2
3
4
mPlugAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
..........
..........
}

结合上面对RecyclerView的源码分析,这样做的意思就是,当上层app的adapter调用了notify函数的时候,它内部的observable(这里补充一点,oberservable是Adapter的内部类)会去通知注册过的observer,就顺理成章的调用了我们PowerfulRecyclerViewAdapter构造函数里注册的这个observer,然后observer会去调用PowerfulRecyclerViewAdapter对应的notify函数。

好了,到这儿对于RecyclerView#Adapter的观察者模式就分析完了。其实这是一个很简单但是很使用的技巧,大家可以参考。

Recycler机制

这里笔者得和大家说声对不起,因为我可能要跳票了。。。主要公司有点事情要忙,加上这个的分析需要花点时间,所以。。你懂的,sorry!不过大家可以去看看这篇文章,其实作者已经对RecyclerView的源码进行了比较细致的分析了。有空我一定给大家带来自己的分析,再次说声抱歉!

下期预告

下篇文章准备给大家带来的时候关于Android中apt的应用,注意不是编译资源的那个aapt,而是apt(Annotation Process Tool)。大家了解ButterKnife,Dagger这种大名鼎鼎的注解库的工作原理吗?了解如何动态生成一个java文件吗?知道编译时注解和运行时注解有哪些区别吗?See you next week.