项目由于历史原因,很多同事可能基于当时的需求引入了众多不同的下拉刷新库。这就造成项目中下拉刷新控件的繁杂混乱,而且后期维护也变得复杂困难。那么,统一控件自然成了亟待解决的问题。而我期望的下拉刷新库是小巧轻量,只提供基本的功能。但又开放接口,易于扩展。偶然逛Gay网,看到了一个刷新库,本篇文章是对该库的源码解析。
介绍
一个支持横向纵向,侵入非侵入,自定义刷新头、刷新尾以及包裹任意刷新内容(
ListView
、RecyclerView
、ViewPager
等等)的类库。最关键的是它是一个非常轻量级的类库!整个类库只有一个文件,代码不到500行。
用法
布局
在需要刷新的地方用
EasyPullLayout
包裹起来(例如根布局),并为EasyPullLayout
下的子View声明layout_type
属性,使得子View可以被EasyPullLayout
识别,分别可以为content
(必选)、edge_top
、edge_bottom
、edge_left
、edge_right
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<com.hzn.lib.EasyPullLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/epl"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--刷新头 由edge_top指定-->
<com.hzn.easypulllayout.TransformerView
android:id="@+id/topView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_type="edge_top" />
<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_type="content"
tools:listitem="@layout/item"/>
</com.hzn.lib.EasyPullLayout>EasyPullLayout
提供的布局属性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<declare-styleable name="EasyPullLayoutJ">
<!--左侧边缘视图触发刷新的拖拽距离 默认为左侧边缘试图的宽度的一半-->
<attr name="trigger_offset_left" format="dimension" />
<attr name="trigger_offset_top" format="dimension" />
<attr name="trigger_offset_right" format="dimension" />
<attr name="trigger_offset_bottom" format="dimension" />
<!--左侧边缘试图能够拖拽的最大距离 默认为左侧边缘试图的宽度-->
<attr name="max_offset_left" format="dimension" />
<attr name="max_offset_top" format="dimension" />
<attr name="max_offset_right" format="dimension" />
<attr name="max_offset_bottom" format="dimension" />
<!--左侧边缘视图是否固定 刷新时类似于swipeRefreshLayout的样式 侵入式效果 默认false-->
<attr name="fixed_content_left" format="boolean" />
<attr name="fixed_content_top" format="boolean" />
<attr name="fixed_content_right" format="boolean" />
<attr name="fixed_content_bottom" format="boolean" />
<!--刷新试图恢复原位动画的时间 默认300ms-->
<attr name="roll_back_duration" format="integer" />
<!--自动刷新拖拽动画的时间 默认300ms-->
<attr name="auto_refresh_rolling_duration" format="integer" />
<!--拖拽的阻尼系数? 系数越大,拖拽的难度越大 默认0.66-->
<attr name="sticky_factor" format="float" />
</declare-styleable>EasyPullLayout
提供的布局参数1
2
3
4
5
6
7
8
9
10
11
12<declare-styleable name="EasyPullLayout_LayoutParams">
<attr name="layout_type" format="enum">
<!--声明是左侧的刷新头布局-->
<enum name="edge_left" value="0" />
<enum name="edge_top" value="1" />
<enum name="edge_right" value="2" />
<enum name="edge_bottom" value="3" />
<!--声明要包裹的刷新布局 这个必须提供 以上可以不提供-->
<enum name="content" value="4" />
</attr>
<attr name="layout_content_fixed" format="boolean" />
</declare-styleable>
监听
EasyPullLayout
的设计遵从单一职责原则,只负责处理拉拽相关的操作,其他的均交给外部进行处理,因此其子View可以是任何一种View。EasyPullLayout
有2个监听可以设置,分别为:OnPullListenerAdapter
:用于EasyPullLatout
对外通知当前拉拽的一些参数(例如拉拽进度),我们可以利用这些参数来改变我们的边缘视图的行为。一般用这个就足够了1
2
3
4
5
6
7
8public abstract static class OnPullListenerAdapter {
//拖拽进度回调
public void onPull(int type, float fraction, boolean changed) {
}
//刷新边缘视图恢复初始位置的回调
public void onRollBack(int rollBackType) {
}
}OnEdgeListener
: 用于通知EasyPullLayout
当前是否到达边缘,到达边缘后EasyPullLayout
会拦截触摸事件,开始拖拽行为,默认会自动监听layout_type
为content
的子View是否到达边缘。一般不用自定义1
2
3public interface OnEdgeListener {
int onEdge();
}具体使用
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
37easyPullLayout.addOnPullListenerAdapter(new EasyPullLayoutJ.OnPullListenerAdapter() {
public void onPull(int type, float fraction, boolean changed) {
//是否经过触发位置 使用该参数可以只在经过触发位置时更新
if (!changed)
return;
//只有刷新头 表明是下拉刷新
if (type == EasyPullLayoutJ.TYPE_EDGE_TOP) {
//进度为1,说明需要刷新头要准备刷新否则就是空闲状态
if (fraction == 1f)
topRefreshView.ready();
else
topRefreshView.idle();
}
}
public void onTriggered(int type) {
//下拉刷新 此时表明已经触发了刷新,刷新头要显示为刷新状态
if (type == EasyPullLayoutJ.TYPE_EDGE_TOP) {
topRefreshView.triggered();
doRefresh();
}
});
private void doRefresh() {
//耗时操作 如加载网络数据
easyPullLayout.postDelayed(new Runnable() {
public void run() {
// 加载完成后必须要调用该方法,让easyPullLayout重置状态
easyPullLayout.stop();
}
}, 2000);
}
//如果有强制自动刷新的需求,调用这个
easyPullLayout.autoRefresh(EasyPullLayoutJ.TYPE_EDGE_TOP);
源码解析
大致原理:类似于
SwipeRefreshLayout
。- 首先为子View提供
layout_type
属性,用以区分刷新内容View及刷新头View,刷新尾View - 确定如何摆放这些子view
- 处理触摸事件
- 首先为子View提供
源码
构造方法
初始化上面的布局属性。
确定布局参数
自定义
ViewGroup
如果有一些自定义控制布局的属性设置(包括margin),就会通过继承View.MarginParams
来扩展布局设置。然后重写generateLayoutParams
方法,这样系统框架就会自动使用该布局读取在xml
中配置的布局属性来控制我们的VIew
的位置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//生成布局参数时会调用该方法
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return null != p && p instanceof LayoutParams;
}
//通过addView(View)添加会调用这个方法
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
//布局文件中设置了属性,在初始化子控件时,会调用该方法来为子控件生成对应的布局属性,
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
//
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
//默认只是生成layout_width和layout_height所以对应的布局参数,
public class LayoutParams extends MarginLayoutParams {
int type = TYPE_NONE;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.getTheme().obtainStyledAttributes(attrs, R.styleable.EasyPullLayout_LayoutParams, 0, 0);
//在内部获取type值并保存起来
type = a.getInt(R.styleable.EasyPullLayout_LayoutParams_layout_type, TYPE_NONE);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}摆放子View
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//完成xml解析后调用
protected void onFinishInflate() {
super.onFinishInflate();
int i = 0;
final int childCount = getChildCount();
//遍历子View
while (i < childCount) {
View child = getChildAt(i++);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
//获取到子View后用一个HashMap来存储,key对应View,value对应View的一些参数,
//类型不能重复!!
if (null != getByType(childViews, lp.type))
throw new IllegalArgumentException("Each child type can only be defined once!");
else
childViews.put(child, new ChildViewAttr());
}
//确保主内容View必须存在
final View contentView = getByType(childViews, TYPE_CONTENT);
if (null == contentView)
throw new IllegalArgumentException("Child type \"content\" must be defined!");
//设置默认的触发临界的监听
setOnEdgeListener(new OnEdgeListener() {
public int onEdge() {
//左侧边缘视图并且手指不能向右滑动(即内容布局不能向左滑动) 表明左侧视图到边缘了
//下面几个同理
if (null != getByType(childViews, TYPE_EDGE_LEFT) && !contentView.canScrollHorizontally(-1))
return TYPE_EDGE_LEFT;
else if (null != getByType(childViews, TYPE_EDGE_RIGHT) && !contentView.canScrollHorizontally(1))
return TYPE_EDGE_RIGHT;
//顶部边缘视图不能向上滑动了,意思就是类似listview已经滚动到顶部了
else if (null != getByType(childViews, TYPE_EDGE_TOP) && !contentView.canScrollVertically(-1))
return TYPE_EDGE_TOP;
else if (null != getByType(childViews, TYPE_EDGE_BOTTOM) && !contentView.canScrollVertically(1))
return TYPE_EDGE_BOTTOM;
else
return TYPE_NONE;
}
});
}接着就是自定义
ViewGroup
的常见的流程 测量 摆放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//测量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//从保存的子View集合中遍历子View,
for (Map.Entry<View, ChildViewAttr> entry : childViews.entrySet()) {
View childView = entry.getKey();
ChildViewAttr childViewAttr = entry.getValue();
//对每个子View进行测量 这里因为继承了MarginLayoutParams,把margin及padding 也作为子视图大小的一部分
easureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
//记录下边缘视图的一些参数,以及根据这些参数初始化EasyPullLayout自身的一些参数:
//把子View的size值记录下来,在摆放子View时会用到
LayoutParams lp = (LayoutParams) childView.getLayoutParams();
switch (lp.type) {
case TYPE_EDGE_LEFT:
case TYPE_EDGE_RIGHT:
//横向size就是宽度加左右margin
childViewAttr.size = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
//初始化EasyPullLayout的属性
//如果trigger_offset_left未在xml指定,在这里初始化 设为左侧边缘试图宽度的一半
trigger_offset_left = trigger_offset_left < 0 ? childViewAttr.size / 2 : trigger_offset_left;
trigger_offset_right = trigger_offset_right < 0 ? childViewAttr.size / 2 : trigger_offset_right;
//未指定则为左侧边缘试图的宽度
max_offset_left = max_offset_left < 0 ? childViewAttr.size : max_offset_left;
max_offset_right = max_offset_right < 0 ? childViewAttr.size : max_offset_right;
break;
case TYPE_EDGE_TOP:
case TYPE_EDGE_BOTTOM:
// 纵向size就是高度加上下margin
childViewAttr.size = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
trigger_offset_top = trigger_offset_top < 0 ? childViewAttr.size / 2 : trigger_offset_top;
trigger_offset_bottom = trigger_offset_bottom < 0 ? childViewAttr.size / 2 : trigger_offset_bottom;
max_offset_top = max_offset_top < 0 ? childViewAttr.size : max_offset_top;
max_offset_bottom = max_offset_bottom < 0 ? childViewAttr.size : max_offset_bottom;
break;
}
}
}
//摆放
protected void onLayout(boolean changed, int l, int t, int r, int b) {
View contentView = getByType(childViews, TYPE_CONTENT);
//contentView不能为空!!!
if (null == contentView)
throw new IllegalArgumentException("EasyPullLayout must have and only have one layout_type \"content\"!");
//获取内容布局的宽高
int contentWidth = contentView.getMeasuredWidth();
int contentHeight = contentView.getMeasuredHeight();
//遍历子View
for (Map.Entry<View, ChildViewAttr> entry : childViews.entrySet()) {
//首先计算出子View的位置,此时都还未偏移,左上角位于(0,0) left = 0, top = 0
View childView = entry.getKey();
ChildViewAttr childViewAttr = entry.getValue();
LayoutParams lp = (LayoutParams) childView.getLayoutParams();
int left = getPaddingLeft() + lp.leftMargin;
int top = getPaddingTop() + lp.topMargin;
int right = left + childView.getMeasuredWidth();
int bottom = top + childView.getMeasuredHeight();
//将view摆放在合适的位置
switch (lp.type) {
case TYPE_EDGE_LEFT:
//左侧刷新view向左移动-childViewAttr.size,刚好隐藏view
//以下同理
left -= childViewAttr.size;
right -= childViewAttr.size;
break;
case TYPE_EDGE_TOP:
top -= childViewAttr.size;
bottom -= childViewAttr.size;
break;
case TYPE_EDGE_RIGHT:
left += contentWidth;
right += contentWidth;
break;
case TYPE_EDGE_BOTTOM:
top += contentHeight;
bottom += contentHeight;
break;
}
//将iew的当前摆放位置记录下来,因为EasyPullLayout是通过改变View的x和y属性来达到位移效果的, 因此需要参考子View的初始位置
//并且可以不通过onLayout来重置位置,避免回调onLayout。
childViewAttr.setBounds(left, top, right, bottom);
childView.layout(left, top, right, bottom);
}处理触摸事件
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//触摸事件拦截处理
public boolean onInterceptTouchEvent(MotionEvent ev) {
//闲置状态不做任何处理
if (currentState != STATE_IDLE)
return false;
switch (ev.getAction()) {
//手指按下的时候记录当前位置
case MotionEvent.ACTION_DOWN:
downX = ev.getX();
downY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//回掉onEdge,判断当前触发边缘的位置
//把是否进行拦截的判断操作交给了外部进行处理,只要返回正确的类型,则开始对触摸事件进行拦截
int type = onEdgeListener.onEdge();
//计算x y偏移量 deltaX deltaY
float dx = ev.getX() - downX;
float dy = ev.getY() - downY;
currentType = type;
//如果当前是左侧边缘View到了边缘,并且允许向左滑动,横向滑动距离大于纵向滑动距离开始拦截
if (type == TYPE_EDGE_LEFT && (pullTypeMask & PULL_TYPE_LEFT) != 0)
return ev.getX() > downX && Math.abs(dx) > Math.abs(dy);
else if (type == TYPE_EDGE_RIGHT && (pullTypeMask & TYPE_EDGE_RIGHT) != 0)
return ev.getX() < downX && Math.abs(dx) > Math.abs(dy);
else if (type == TYPE_EDGE_TOP && (pullTypeMask & TYPE_EDGE_TOP) != 0)
return ev.getY() > downY && Math.abs(dy) > Math.abs(dx);
else if (type == TYPE_EDGE_BOTTOM && (pullTypeMask & TYPE_EDGE_BOTTOM) != 0)
return ev.getY() < downY && Math.abs(dy) > Math.abs(dx);
else
return false;
}
return false;
}
//拦截了触摸事件后,由EasyPullLayout自身进行消费处理
public boolean onTouchEvent(MotionEvent event) {
//闲置状态不做任何处理
if (currentState != STATE_IDLE)
return false;
// 检查滑动到边缘视图类型
if (currentType == TYPE_EDGE_LEFT && (pullTypeMask & PULL_TYPE_LEFT) == 0 ||
currentType == TYPE_EDGE_TOP && (pullTypeMask & PULL_TYPE_TOP) == 0 ||
currentType == TYPE_EDGE_RIGHT && (pullTypeMask & PULL_TYPE_RIGHT) == 0 ||
currentType == TYPE_EDGE_BOTTOM && (pullTypeMask & PULL_TYPE_BOTTOM) == 0)
return false;
//因为自己要处理,所以一定要请求父view不要拦截
getParent().requestDisallowInterceptTouchEvent(true);
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
float x = event.getX();
float y = event.getY();
//计算x,y 偏移量 = 滑动的距离 * (1- 阻尼系数)
//从这里可以得出阻尼系数与滑动的距离有反比例关系
//阻尼系数可以使拖拽时有黏着效果
offsetX = (x - downX) * (1 - sticky_factor * 0.75f);
offsetY = (y - downY) * (1 - sticky_factor * 0.75f);
//对必要的子View进行偏移(设置了对应的fixed选项后,content不会进行偏移,达到侵入式效果)
move();
break;
//手指释放时,通过ValueAnimator,在一段时间内将子View还原 达到缓缓还原的效果
//还原后的位置分2种情况
//1还没超过触发偏移量,则还原回到初始位置
//2已经超过了触发偏移量,则回到触发偏移量的位置(即刷新位置) 再经由外部调用Stop方法,再次将位置还原的初始位置
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//更改状态为滑动中
currentState = STATE_ROLLING;
switch (currentType) {
case TYPE_EDGE_LEFT:
case TYPE_EDGE_RIGHT:
rollBackHorizontal();
break;
case TYPE_EDGE_TOP:
case TYPE_EDGE_BOTTOM:
rollBackVertical();
break;
}
break;
}
return true;
}
private void move() {
//定义当前拖拽的进度
float pullFraction = 0f;
// 根据max_offset_left 限制拖拽距离 计算当前拖拽完成的百分比
switch (currentType) {
case TYPE_EDGE_LEFT:
//限制x偏移量的边界值
offsetX = offsetX < 0 ? 0f : offsetX > max_offset_left ? max_offset_left : offsetX;
//计算当前拖拽的进度 同时也限制了边界 进度 = 当前的x偏移量 / 触发刷新位置的x值
pullFraction = offsetX == 0f ? 0f : trigger_offset_left > offsetX ? offsetX / trigger_offset_left : 1f;
break;
case TYPE_EDGE_RIGHT:
offsetX = offsetX > 0 ? 0f : offsetX < -max_offset_right ? -max_offset_right : offsetX;
pullFraction = offsetX == 0f ? 0f : -trigger_offset_right < offsetX ? offsetX / -trigger_offset_right : 1f;
break;
case TYPE_EDGE_TOP:
offsetY = offsetY < 0 ? 0f : offsetY > max_offset_top ? max_offset_top : offsetY;
pullFraction = offsetY == 0f ? 0f : trigger_offset_top > offsetY ? offsetY / trigger_offset_top : 1f;
break;
case TYPE_EDGE_BOTTOM:
offsetY = offsetY > 0 ? 0f : offsetY < -max_offset_bottom ? -max_offset_bottom : offsetY;
pullFraction = offsetY == 0f ? 0f : -trigger_offset_bottom < offsetY ? offsetY / -trigger_offset_bottom : 1f;
break;
}
//是否经过触发位置
//changed为true时,两者不能同时为1 也不能同时小于1
//其实就是为了保证上下滑动只在经过触发位置时更新
boolean changed = !(lastPullFraction < 1f && pullFraction < 1f || lastPullFraction == 1f && pullFraction == 1f);
//将当前的滑动边缘类型,及滑动进度等相关信息回调
onPullListenerAdapter.onPull(currentType, pullFraction, changed);
//记录上次的滑动进度
lastPullFraction = pullFraction;
//遍历子View,根据拖拽的距离摆放子View
//横向的就是设置X坐标值
//纵向的就是设置Y坐标值
// 如果设置了对应的fixed,且为content,则不偏移
for (Map.Entry<View, ChildViewAttr> entry : childViews.entrySet()) {
View childView = entry.getKey();
ChildViewAttr childViewAttr = entry.getValue();
if (currentType == TYPE_EDGE_LEFT &&
(((LayoutParams) childView.getLayoutParams()).type != TYPE_CONTENT || !fixed_content_left)) {
childView.setX(childViewAttr.left + offsetX);
} else if (currentType == TYPE_EDGE_RIGHT &&
(((LayoutParams) childView.getLayoutParams()).type != TYPE_CONTENT || !fixed_content_right)) {
childView.setX(childViewAttr.left + offsetX);
} else if (currentType == TYPE_EDGE_TOP &&
((LayoutParams) childView.getLayoutParams()).type != TYPE_CONTENT || !fixed_content_top) {
childView.setY(childViewAttr.top + offsetY);
} else if (currentType == TYPE_EDGE_BOTTOM &&
(((LayoutParams) childView.getLayoutParams()).type != TYPE_CONTENT || !fixed_content_bottom)) {
childView.setY(childViewAttr.top + offsetY);
}
}
}
//这里只看下纵向的 横向的原理一样
private void rollBackVertical() {
//要还原的偏移量
//下拉 offset是正值
//1 trigger_offset_bottom < offsetY <= trigger_offset_top即未拉到触发位置的,还原的偏移量就是下拉的偏移量offsetY
//2 offsetY > trigger_offset_top即超过触发位置的,还原偏移量是超过触发位置的偏移量 offsetY - trigger_offset_top
//上拉 offset是负值
//1 offsetY >= -trigger_offset_bottom 即上拉未拉到触发位置的,还原的偏移量就是上拉的偏移量offsetY
//2 offsetY < -trigger_offset_bottom 即上拉超过触发位置的,还原偏移量是超过触发位置的偏移量
//offsetY(负值) + trigger_offset_bottom(正值)
final float rollBackOffset =
offsetY > trigger_offset_top ? offsetY - trigger_offset_top
: offsetY < -trigger_offset_bottom ? offsetY + trigger_offset_bottom
: offsetY;
Log.e("roll", "offsetY: " + offsetY + "-->trigger_offset_top: " + trigger_offset_top +
"--> -trigger_offset_bottom: " + (-trigger_offset_bottom) + "--rollBackOffset: " + rollBackOffset);
//触发刷新位置的偏移量 如果尚未拉到触发位置的,则为0
//超过触发位置的, 判断当前视图边缘位置,是下拉刷新,则未trigger_offset_top
//上拉加载的则未-trigger_offset_bottom,否则未0
final float triggerOffset =
rollBackOffset != offsetY ?
currentType == TYPE_EDGE_TOP ? trigger_offset_top :
currentType == TYPE_EDGE_BOTTOM ? -trigger_offset_bottom :
0
: 0;
Log.e("roll", "triggerOffset: " + triggerOffset);
// 通过ValueAnimator,在一段时间内将子View还原 达到缓缓还原 动画,值从1->0
verticalAnimator = ValueAnimator.ofFloat(1f, 0f);
//设置动画时间
verticalAnimator.setDuration(roll_back_duration)
.setInterpolator(new DecelerateInterpolator());
//动画更新监听
verticalAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
switch (currentType) {
//当前是下拉刷新 遍历子View
case TYPE_EDGE_TOP:
for (Map.Entry<View, ChildViewAttr> entry : childViews.entrySet()) {
View childView = entry.getKey();
ChildViewAttr childViewAttr = entry.getValue();
//为content且设置了fixed 不偏移 否则通过rollBackOffset和triggerOffset,以及animatedValue计算得出y
if (((LayoutParams) childView.getLayoutParams()).type != TYPE_CONTENT || !fixed_content_top)
childView.setY(childViewAttr.top + triggerOffset + rollBackOffset *
(float) animation.getAnimatedValue());
}
break;
case TYPE_EDGE_BOTTOM:
for (Map.Entry<View, ChildViewAttr> entry : childViews.entrySet()) {
View childView = entry.getKey();
ChildViewAttr childViewAttr = entry.getValue();
if (((LayoutParams) childView.getLayoutParams()).type != TYPE_CONTENT || !fixed_content_bottom)
childView.setY(childViewAttr.top + triggerOffset + rollBackOffset *
(float) animation.getAnimatedValue());
}
break;
}
}
});
// 动画结束后,还原一些参数,回调监听
verticalAnimator.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
//此时是超过触发位置的,动画将之还原到触发位置
if (triggerOffset != 0 && currentState == STATE_ROLLING) {
//更改状态为触发状态
currentState = STATE_TRIGGERING;
//记录此时的偏移量= triggerOffset 因为在外部刷新后调用stop()方法还需要再次还原到初始位置
offsetY = triggerOffset;
//回调触发刷新的监听
onPullListenerAdapter.onTriggered(currentType);
} else {
//更改状态为空闲状态
currentState = STATE_IDLE;
offsetY = 0f;
//回调通知
rollBackEnd();
}
}
});
verticalAnimator.start();
}
//在刷新完由外部调用,再次将子View位置由触发位置还原到初始位置
public void stop() {
switch (currentType) {
case TYPE_EDGE_LEFT:
case TYPE_EDGE_RIGHT:
rollBackHorizontal();
break;
case TYPE_EDGE_TOP:
case TYPE_EDGE_BOTTOM:
rollBackVertical();
break;
}
}自动刷新
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//自动更新
//根据要更新的刷新类型计算出要自动滑动的距离,通过ValueAnimator不断更新子View的X或者Y属性,达到缓缓平移的效果
//动画结束后,再次通过ValueAnimator还原到刷新位置
public void autoRefresh(int typeEdge) {
//检查状态
if (currentState != STATE_IDLE) return;
//检查类型
if (typeEdge != TYPE_EDGE_LEFT &&
typeEdge != TYPE_EDGE_TOP &&
typeEdge != TYPE_EDGE_RIGHT &&
typeEdge != TYPE_EDGE_BOTTOM) return;
//更改状态为滑动
currentState = STATE_ROLLING;
//记录当前要刷新的类型
currentType = typeEdge;
//要自动滑动的距离
float end;
switch (currentType) {
case TYPE_EDGE_LEFT:
end = max_offset_left;
break;
case TYPE_EDGE_TOP:
end = max_offset_top;
break;
case TYPE_EDGE_RIGHT:
end = -max_offset_right;
break;
case TYPE_EDGE_BOTTOM:
end = -max_offset_bottom;
break;
default:
end = 0;
break;
}
verticalAnimator = ValueAnimator.ofFloat(0f, end);
//设置动画时间
verticalAnimator.setDuration(auto_refresh_rolling_duration);
verticalAnimator.setInterpolator(new DecelerateInterpolator());
verticalAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
//计算每次更新拖拽的偏移量
if (currentType == TYPE_EDGE_LEFT || currentState == TYPE_EDGE_RIGHT) {
offsetX = (float) animation.getAnimatedValue();
} else if (currentType == TYPE_EDGE_BOTTOM || currentType == TYPE_EDGE_TOP) {
offsetY = (float) animation.getAnimatedValue();
}
move();
}
});
verticalAnimator.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
//动画结束时 还原位置到触发刷新位置
if (currentType == TYPE_EDGE_LEFT || currentState == TYPE_EDGE_RIGHT) {
rollBackHorizontal();
} else if (currentType == TYPE_EDGE_BOTTOM || currentType == TYPE_EDGE_TOP) {
rollBackVertical();
}
}
});
verticalAnimator.start();
}
至此,源码已分析完毕。