在列表中显示多个下载进度条是一个很常见的需求了,
这个需求主要涉及到以下两个技术点:
1.Handler异步更新UI
2.ListView进行局部更新
今天来看一下这一功能最简单的实现——模仿多个APP下载更新进度条。
为了让代码简单一些,在这里使用了ListView显示列表,直接使用线程控制进度更新。
首先,来创建一个AppItem的类在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
28public class AppItem {
private String appName;
private int currentProgress;
private int appIndex;
public AppItem(String name, int index) {
appName = name;
currentProgress = 0;
appIndex = index;
}
public String getAppName() {
return appName;
}
public void setCurrentProgress(int progress) {
currentProgress = progress;
}
public int getCurrentProgress() {
return currentProgress;
}
public int getAppIndex() {
return appIndex;
}
}
为简单说明,列表项设定的很简单,仅有应用名称,下载进度和项目在列表中的index这三个元素。
下载进度默认为0,在创建项目时必须提供应用名称和index数值。
然后来编写ListView的Adapter.
在这里,考虑到应用下载时进度条时刻变化,并且各项之间没有同步关系,因此需要提供一个方法来允许我们更新ListView中的某一项,而不是每次调用notifyDataSetChanged更新整个list,那样会非常耗费资源。
同时,还需要创建一个ViewHolder并使用缓存机制节省内存。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
62class AppListAdapter extends BaseAdapter {
//为描述简便,将Adapter直接定义在MainActivity,并且直接读取Activity中的mData(ArrayList)
LayoutInflater mInflater;
public AppListAdapter(Context context) {
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public int getCount() {
return mData.size();
}
public Object getItem(int position) {
return mData.get(position);
}
public long getItemId(int position) {
return position;
}
public View getView(int position, View convertView, ViewGroup parent) {
// 使用ViewHolder和复用机制节省内存
ViewHolder holder;
if(convertView == null) {
holder = new ViewHolder();
convertView = mInflater.inflate(R.layout.layout_item, parent, false);
holder.appNameText = (TextView) convertView.findViewById(R.id.app_name_text);
holder.downloadProgress = (ProgressBar) convertView.findViewById(R.id.app_progress);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.appNameText.setText(mData.get(position).getAppName());
holder.downloadProgress.setProgress(mData.get(position).getCurrentProgress());
return convertView;
}
// 通过提供单个View何其所在位置,更新ListView中的某一项
public void updateView(View view, int position) {
if (view == null) {
return;
}
Log.i(TAG, "view index is : " + mListView.getPositionForView(view));
ViewHolder holder = (ViewHolder)view.getTag();
holder.downloadProgress.setProgress(mData.get(position).getCurrentProgress());
}
}
static class ViewHolder {
TextView appNameText;
ProgressBar downloadProgress;
}
在这里通过updateView来实现单个Item的更新,为何需要传入View对象和其对应的位置,后面来说明。
接下来,为了模拟应用下载,我们新建一个线程类,定时更新AppItem中的progress数值。这样刷新Item时,直接读取并更新View上对应AppItem中的progressbar数值,就可以达到这一效果了。
同时,由于我们使用的线程独立于UI主线程,因此这个线程中需要一个来自Activity中的Handler对象(使用类似LocalBroadcast也可以),来通知Activity刷新UI。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
41public class DownLoadThread extends Thread {
private int interval = 500;
private AppItem mApp;
private MainActivity.DownloadHandler mHandler;
// 构造函数中传入一个间隔时间值,用来控制每一个Item的刷新速度,模拟不同应用下载速度不同
public DownLoadThread(int time, AppItem app, MainActivity.DownloadHandler handler) {
interval = time;
mApp = app;
mHandler = handler;
}
public void run() {
try {
for (int i = 0; i < 101; i++) {
Thread.sleep(interval);
// 不断更新AppItem中的进度百分比
mApp.setCurrentProgress(i);
// 在这里可以设置一些条件来控制发送UI更新消息的频率,以免每变化1%就刷新UI
if (someCondition) {
continue;
}
// 获取Item对应的index,并向Handler发送消息
int index = mApp.getAppIndex();
Message msg = mHandler.obtainMessage(MainActivity.UPDATE_PROGRESS);
msg.what = MainActivity.UPDATE_PROGRESS;
msg.arg1 = index;
mHandler.sendMessage(msg);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
然后来编写消息处理最核心的Handler部分,
1.采用Android推荐方式,将Handler声明为静态内部类,并使用弱引用,以避免内存泄漏
2.通过Message中的arg1来得到Item的index数值,通过listView的getFirstVisiblePosition与getLastVisiblePosition方法,来判断要更新的Item是否在可见区域内,如果当前不可见,就无需更新,这样提高效率。
3.通过Item的index与firstViewIndex差值,来得到View对象的偏移量。接下来使用getChildAt来获取到屏幕中对应的View
4.调用我们在Adapter中添加的updateView函数来更新特定View,而不是刷新整个ListView1
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
35static class DownloadHandler extends Handler {
WeakReference<MainActivity> activity;
public DownloadHandler(MainActivity mainActivity) {
activity = new WeakReference<MainActivity>(mainActivity);
}
public void handleMessage(Message msg) {
switch (msg.what) {
case UPDATE_PROGRESS:
// 获取到我们传递来的index
int index = msg.arg1;
int firstViewIndex = activity.get().getListView().getFirstVisiblePosition();
int lastViewIndex = activity.get().getListView().getLastVisiblePosition();
// 仅当需要更新的Item可见,才进行更新,否则直接跳出
if (index >= firstViewIndex && index <= lastViewIndex) {
// 计算index与可见view的偏移量,获取到真正的view对象
// 由于getChildAt接口获取的是屏幕显示的所有view中的第i个:
// 例如ListView区域最多可以显示5个item,getChildAt(2)拿到的是第2个view,并不一定是整个list中的第2个。
int offset = index - firstViewIndex;
View view = activity.get().getListView().getChildAt(offset);
activity.get().getListAdapter().updateView(view, index);
}
// 并不采用notifyDataSetChanged的方式,因为会刷新整个列表,效率很低
//activity.get().getListAdapter().notifyDataSetChanged();
break;
default:
break;
}
}
}
最后为了模拟这一过程,在一个按钮的点击事件中创建并启动若干线程,来模拟下载过程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18mButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
startDownload();
}
});
private void startDownload() {
// 100ms更新间隔
DownLoadThread threadA = new DownLoadThread(100, mData.get(0), mHandler);
threadA.start();
// 200ms
DownLoadThread threadB = new DownLoadThread(200, mData.get(2), mHandler);
threadB.start();
//300ms
DownLoadThread threadC = new DownLoadThread(500, mData.get(11), mHandler);
threadC.start();
}
直接使用线程比较简陋,这块可考虑AsyncTask或线程池实现,来更好控制多线程的处理。同时线程中使用while循环+退出标志的方式,以在异常情况下迅速停止掉线程。