Android自定义控件下拉刷新实例代码

实现效果:

图片素材:

--> 首先, 写先下拉刷新时的刷新布局 pull_to_refresh.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
53
54
55
56
57
58
59
<resources>
   <string name= "app_name" >PullToRefreshTest</string>
   <string name= "pull_to_refresh" >下拉可以刷新</string>
   <string name= "release_to_refresh" >释放立即刷新</string>
   <string name= "refreshing" >正在刷新...</string>
   <string name= "not_updated_yet" >暂未更新过</string>
   <string name= "updated_at" >上次更新于%1$s前</string>
   <string name= "updated_just_now" >刚刚更新</string>
   <string name= "time_error" >时间有问题</string>
</resources>
<?xml version= "1.0" encoding= "utf-8" ?>
<RelativeLayout xmlns:android= "http://schemas.android.com/apk/res/android"
   android:id= "@+id/pull_to_refresh_head"
   android:layout_width= "match_parent"
   android:layout_height= "60dp" >
   <LinearLayout
     android:layout_width= "200dp"
     android:layout_height= "60dp"
     android:layout_centerInParent= "true"
     android:orientation= "horizontal" >
     <RelativeLayout
       android:layout_width= "0dp"
       android:layout_height= "60dp"
       android:layout_weight= "3" >
       <ImageView
         android:id= "@+id/arrow"
         android:layout_width= "wrap_content"
         android:layout_height= "wrap_content"
         android:layout_centerInParent= "true"
         android:src= "@mipmap/indicator_arrow" />
       <ProgressBar
         android:id= "@+id/progress_bar"
         android:layout_width= "30dp"
         android:layout_height= "30dp"
         android:layout_centerInParent= "true"
         android:visibility= "gone" />
     </RelativeLayout>
     <LinearLayout
       android:layout_width= "0dp"
       android:layout_height= "60dp"
       android:layout_weight= "12"
       android:orientation= "vertical" >
       <TextView
         android:id= "@+id/description"
         android:layout_width= "match_parent"
         android:layout_height= "0dp"
         android:layout_weight= "1"
         android:gravity= "center_horizontal|bottom"
         android:text= "@string/pull_to_refresh" />
       <TextView
         android:id= "@+id/updated_at"
         android:layout_width= "match_parent"
         android:layout_height= "0dp"
         android:layout_weight= "1"
         android:gravity= "center_horizontal|top"
         android:text= "@string/updated_at" />
     </LinearLayout>
   </LinearLayout>
</RelativeLayout>
?
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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
strings
pull_to_refresh
--> 然后, 也是主要的, 自定义下拉刷新的 View (包含下拉刷新所有操作) RefreshView.java:
package com.dragon.android.tofreshlayout;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
public class RefreshView extends LinearLayout implements View.OnTouchListener {
private static final String TAG = RefreshView. class .getSimpleName();
public enum PULL_STATUS {
STATUS_PULL_TO_REFRESH( 0 ), // 下拉状态
STATUS_RELEASE_TO_REFRESH( 1 ), // 释放立即刷新状态
STATUS_REFRESHING( 2 ), // 正在刷新状态
STATUS_REFRESH_FINISHED( 3 ); // 刷新完成或未刷新状态
private int status; // 状态
PULL_STATUS( int value) {
this .status = value;
}
public int getValue() {
return this .status;
}
}
// 下拉头部回滚的速度
public static final int SCROLL_SPEED = - 20 ;
// 一分钟的毫秒值,用于判断上次的更新时间
public static final long ONE_MINUTE = 60 * 1000 ;
// 一小时的毫秒值,用于判断上次的更新时间
public static final long ONE_HOUR = 60 * ONE_MINUTE;
// 一天的毫秒值,用于判断上次的更新时间
public static final long ONE_DAY = 24 * ONE_HOUR;
// 一月的毫秒值,用于判断上次的更新时间
public static final long ONE_MONTH = 30 * ONE_DAY;
// 一年的毫秒值,用于判断上次的更新时间
public static final long ONE_YEAR = 12 * ONE_MONTH;
// 上次更新时间的字符串常量,用于作为 SharedPreferences 的键值
private static final String UPDATED_AT = "updated_at" ;
// 下拉刷新的回调接口
private PullToRefreshListener mListener;
private SharedPreferences preferences; // 用于存储上次更新时间
private View header; // 下拉头的View
private ListView listView; // 需要去下拉刷新的ListView
private ProgressBar progressBar; // 刷新时显示的进度条
private ImageView arrow; // 指示下拉和释放的箭头
private TextView description; // 指示下拉和释放的文字描述
private TextView updateAt; // 上次更新时间的文字描述
private MarginLayoutParams headerLayoutParams; // 下拉头的布局参数
private long lastUpdateTime; // 上次更新时间的毫秒值
// 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,使用id来做区分
private int mId = - 1 ;
private int hideHeaderHeight; // 下拉头的高度
/**
* 当前处理什么状态,可选值有 STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH, STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED
*/
private PULL_STATUS currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED;
// 记录上一次的状态是什么,避免进行重复操作
private PULL_STATUS lastStatus = currentStatus;
private float yDown; // 手指按下时的屏幕纵坐标
private int touchSlop; // 在被判定为滚动之前用户手指可以移动的最大值。
private boolean loadOnce; // 是否已加载过一次layout,这里onLayout中的初始化只需加载一次
private boolean ableToPull; // 当前是否可以下拉,只有ListView滚动到头的时候才允许下拉
/**
* 下拉刷新控件的构造函数,会在运行时动态添加一个下拉头的布局
*/
public RefreshView(Context context, AttributeSet attrs) {
super (context, attrs);
preferences = PreferenceManager.getDefaultSharedPreferences(context);
header = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh, null , true );
progressBar = (ProgressBar) header.findViewById(R.id.progress_bar);
arrow = (ImageView) header.findViewById(R.id.arrow);
description = (TextView) header.findViewById(R.id.description);
updateAt = (TextView) header.findViewById(R.id.updated_at);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
refreshUpdatedAtValue();
setOrientation(VERTICAL);
addView(header, 0 );
//Log.d(TAG, "RefreshView Constructor() getChildAt(0): " + getChildAt(0));
//Log.d(TAG, "RefreshView Constructor() getChildAt(0): " + getChildAt(1));
// listView = (ListView) getChildAt(1);
// listView.setOnTouchListener(this);
}
/**
* 进行一些关键性的初始化操作,比如:将下拉头向上偏移进行隐藏,给 ListView 注册 touch 事件
*/
@Override
protected void onLayout( boolean changed, int l, int t, int r, int b) {
super .onLayout(changed, l, t, r, b);
if (changed && !loadOnce) {
hideHeaderHeight = -header.getHeight();
headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();
headerLayoutParams.topMargin = hideHeaderHeight;
listView = (ListView) getChildAt( 1 );
//Log.d(TAG, "onLayout() getChildAt(0): " + getChildAt(0));
//Log.d(TAG, "onLayout() listView: " + listView);
listView.setOnTouchListener( this );
loadOnce = true ;
}
}
/**
* 当 ListView 被触摸时调用,其中处理了各种下拉刷新的具体逻辑
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
setCanAbleToPull(event); // 判断是否可以下拉
if (ableToPull) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
yDown = event.getRawY();
break ;
case MotionEvent.ACTION_MOVE:
// 获取移动中的 Y 轴的位置
float yMove = event.getRawY();
// 获取从按下到移动过程中移动的距离
int distance = ( int ) (yMove - yDown);
// 如果手指是上滑状态,并且下拉头是完全隐藏的,就屏蔽下拉事件
if (distance <= 0 && headerLayoutParams.topMargin <= hideHeaderHeight) {
return false ;
}
if (distance < touchSlop) {
return false ;
}
// 判断是否已经在刷新状态
if (currentStatus != PULL_STATUS.STATUS_REFRESHING) {
// 判断设置的 topMargin 是否 > 0, 默认初始设置为 -header.getHeight()
if (headerLayoutParams.topMargin > 0 ) {
currentStatus = PULL_STATUS.STATUS_RELEASE_TO_REFRESH;
} else {
// 否则状态为下拉中的状态
currentStatus = PULL_STATUS.STATUS_PULL_TO_REFRESH;
}
// 通过偏移下拉头的 topMargin 值,来实现下拉效果
headerLayoutParams.topMargin = (distance / 2 ) + hideHeaderHeight;
header.setLayoutParams(headerLayoutParams);
}
break ;
case MotionEvent.ACTION_UP:
default :
if (currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {
// 松手时如果是释放立即刷新状态,就去调用正在刷新的任务
new RefreshingTask().execute();
} else if (currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH) {
// 松手时如果是下拉状态,就去调用隐藏下拉头的任务
new HideHeaderTask().execute();
}
break ;
}
// 时刻记得更新下拉头中的信息
if (currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH
|| currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {
updateHeaderView();
// 当前正处于下拉或释放状态,要让 ListView 失去焦点,否则被点击的那一项会一直处于选中状态
listView.setPressed( false );
listView.setFocusable( false );
listView.setFocusableInTouchMode( false );
lastStatus = currentStatus;
// 当前正处于下拉或释放状态,通过返回 true 屏蔽掉 ListView 的滚动事件
return true ;
}
}
return false ;
}
/**
* 给下拉刷新控件注册一个监听器
*
* @param listener 监听器的实现
* @param id 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,不同界面在注册下拉刷新监听器时一定要传入不同的 id
*/
public void setOnRefreshListener(PullToRefreshListener listener, int id) {
mListener = listener;
mId = id;
}
/**
* 当所有的刷新逻辑完成后,记录调用一下,否则你的 ListView 将一直处于正在刷新状态
*/
public void finishRefreshing() {
currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED;
preferences.edit().putLong(UPDATED_AT + mId, System.currentTimeMillis()).commit();
new HideHeaderTask().execute();
}
/**
* 根据当前 ListView 的滚动状态来设定 {@link #ableToPull}
* 的值,每次都需要在 onTouch 中第一个执行,这样可以判断出当前应该是滚动 ListView,还是应该进行下拉
*/
private void setCanAbleToPull(MotionEvent event) {
View firstChild = listView.getChildAt( 0 );
if (firstChild != null ) {
// 获取 ListView 中第一个Item的位置
int firstVisiblePos = listView.getFirstVisiblePosition();
// 判断第一个子控件的 Top 是否和第一个 Item 位置相等
if (firstVisiblePos == 0 && firstChild.getTop() == 0 ) {
if (!ableToPull) {
// getRawY() 获得的是相对屏幕 Y 方向的位置
yDown = event.getRawY();
}
// 如果首个元素的上边缘,距离父布局值为 0,就说明 ListView 滚动到了最顶部,此时应该允许下拉刷新
ableToPull = true ;
} else {
if (headerLayoutParams.topMargin != hideHeaderHeight) {
headerLayoutParams.topMargin = hideHeaderHeight;
header.setLayoutParams(headerLayoutParams);
}
ableToPull = false ;
}
} else {
// 如果 ListView 中没有元素,也应该允许下拉刷新
ableToPull = true ;
}
}
/**
* 更新下拉头中的信息
*/
private void updateHeaderView() {
if (lastStatus != currentStatus) {
if (currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH) {
description.setText(getResources().getString(R.string.pull_to_refresh));
arrow.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
rotateArrow();
} else if (currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {
description.setText(getResources().getString(R.string.release_to_refresh));
arrow.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
rotateArrow();
} else if (currentStatus == PULL_STATUS.STATUS_REFRESHING) {
description.setText(getResources().getString(R.string.refreshing));
progressBar.setVisibility(View.VISIBLE);
arrow.clearAnimation();
arrow.setVisibility(View.GONE);
}
refreshUpdatedAtValue();
}
}
/**
* 根据当前的状态来旋转箭头
*/
private void rotateArrow() {
float pivotX = arrow.getWidth() / 2f;
float pivotY = arrow.getHeight() / 2f;
float fromDegrees = 0f;
float toDegrees = 0f;
if (currentStatus == PULL_STATUS.STATUS_PULL_TO_REFRESH) {
fromDegrees = 180f;
toDegrees = 360f;
} else if (currentStatus == PULL_STATUS.STATUS_RELEASE_TO_REFRESH) {
fromDegrees = 0f;
toDegrees = 180f;
}
RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);
animation.setDuration( 100 );
animation.setFillAfter( true );
arrow.startAnimation(animation);
}
/**
* 刷新下拉头中上次更新时间的文字描述
*/
private void refreshUpdatedAtValue() {
lastUpdateTime = preferences.getLong(UPDATED_AT + mId, - 1 );
long currentTime = System.currentTimeMillis();
long timePassed = currentTime - lastUpdateTime;
long timeIntoFormat;
String updateAtValue;
if (lastUpdateTime == - 1 ) {
updateAtValue = getResources().getString(R.string.not_updated_yet);
} else if (timePassed < 0 ) {
updateAtValue = getResources().getString(R.string.time_error);
} else if (timePassed < ONE_MINUTE) {
updateAtValue = getResources().getString(R.string.updated_just_now);
} else if (timePassed < ONE_HOUR) {
timeIntoFormat = timePassed / ONE_MINUTE;
String value = timeIntoFormat + "分钟" ;
updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
} else if (timePassed < ONE_DAY) {
timeIntoFormat = timePassed / ONE_HOUR;
String value = timeIntoFormat + "小时" ;
updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
} else if (timePassed < ONE_MONTH) {
timeIntoFormat = timePassed / ONE_DAY;
String value = timeIntoFormat + "天" ;
updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
} else if (timePassed < ONE_YEAR) {
timeIntoFormat = timePassed / ONE_MONTH;
String value = timeIntoFormat + "个月" ;
updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
} else {
timeIntoFormat = timePassed / ONE_YEAR;
String value = timeIntoFormat + "年" ;
updateAtValue = String.format(getResources().getString(R.string.updated_at), value);
}
updateAt.setText(updateAtValue);
}
/**
* 正在刷新的任务,在此任务中会去回调注册进来的下拉刷新监听器
*/
class RefreshingTask extends AsyncTask<Void, Integer, Void> {
@Override
protected Void doInBackground(Void... params) {
int topMargin = headerLayoutParams.topMargin;
while ( true ) {
topMargin = topMargin + SCROLL_SPEED;
if (topMargin <= 0 ) {
topMargin = 0 ;
break ;
}
publishProgress(topMargin);
SystemClock.sleep( 10 );
}
currentStatus = PULL_STATUS.STATUS_REFRESHING;
publishProgress( 0 );
if (mListener != null ) {
mListener.onRefresh();
}
return null ;
}
@Override
protected void onProgressUpdate(Integer... topMargin) {
updateHeaderView();
headerLayoutParams.topMargin = topMargin[ 0 ];
header.setLayoutParams(headerLayoutParams);
}
}
/**
* 隐藏下拉头的任务,当未进行下拉刷新或下拉刷新完成后,此任务将会使下拉头重新隐藏
*/
class HideHeaderTask extends AsyncTask<Void, Integer, Integer> {
@Override
protected Integer doInBackground(Void... params) {
int topMargin = headerLayoutParams.topMargin;
while ( true ) {
topMargin = topMargin + SCROLL_SPEED;
if (topMargin <= hideHeaderHeight) {
topMargin = hideHeaderHeight;
break ;
}
publishProgress(topMargin);
SystemClock.sleep( 10 );
}
return topMargin;
}
@Override
protected void onProgressUpdate(Integer ... topMargin) {
headerLayoutParams.topMargin = topMargin[ 0 ];
header.setLayoutParams(headerLayoutParams);
}
@Override
protected void onPostExecute(Integer topMargin) {
headerLayoutParams.topMargin = topMargin;
header.setLayoutParams(headerLayoutParams);
currentStatus = PULL_STATUS.STATUS_REFRESH_FINISHED;
}
}
/**
* 下拉刷新的监听器,使用下拉刷新的地方应该注册此监听器来获取刷新回调
*/
public interface PullToRefreshListener {
// 刷新时会去回调此方法,在方法内编写具体的刷新逻辑。注意此方法是在子线程中调用的, 可以不必另开线程来进行耗时操作
void onRefresh();
}
}

--> 第三步, 写主布局:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version= "1.0" encoding= "utf-8" ?>
<RelativeLayout xmlns:android= "http://schemas.android.com/apk/res/android"
   xmlns:tools= "http://schemas.android.com/tools"
   android:layout_width= "match_parent"
   android:layout_height= "match_parent"
   tools:context= ".MainActivity" >
   <com.dragon.android.tofreshlayout.RefreshView
     android:id= "@+id/refreshable_view"
     android:layout_width= "match_parent"
     android:layout_height= "match_parent" >
     <ListView
       android:id= "@+id/list_view"
       android:layout_width= "match_parent"
       android:layout_height= "match_parent" >
     </ListView>
   </com.dragon.android.tofreshlayout.RefreshView>
</RelativeLayout>

--> 最后, Java 代码添加 ListView 的数据:

?
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
package com.dragon.android.tofreshlayout;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.webkit.WebView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
public class MainActivity extends AppCompatActivity {
   RefreshView refreshableView;
   ListView listView;
   ArrayAdapter<String> adapter;
   private WebView webView;
   private static int NUM = 30 ;
   String[] items = new String[NUM];
   @Override
   protected void onCreate(Bundle savedInstanceState) {
     super .onCreate(savedInstanceState);
     setContentView(R.layout.activity_main);
     getSupportActionBar().hide();
     for ( int i = 0 ; i < items.length; i++) {
       items[i] = "列表项" + i;
     }
     refreshableView = (RefreshView) findViewById(R.id.refreshable_view);
     listView = (ListView) findViewById(R.id.list_view);
     adapter = new ArrayAdapter<>( this , android.R.layout.simple_list_item_1, items);
     listView.setAdapter(adapter);
     refreshableView.setOnRefreshListener( new RefreshView.PullToRefreshListener() {
       @Override
       public void onRefresh() {
         SystemClock.sleep( 3000 );
         refreshableView.finishRefreshing();
       }
     }, 0 );
   }
}

程序 Demo: 链接:http://pan.baidu.com/s/1ge6Llw3 密码:skna

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值