刚接触Android开发的时候,记得当时要实现一个下载功能的任务。鉴于当时对Android的掌握不够,实现起来感觉很有难度,只能参照当时其他项目里的下载代码,过后对这部分内容仍旧比较模糊,只知道执行的流程和实现的方式,想不到更合理的方式去优化。虽然这部分功能一直用着比较稳定,但是变更需求的时候去修改逻辑难度相当大。最近从网上找了些稳定易用的下载库,试着用到我的项目中。
这里先介绍一下MultiThreadDownloader,出自Aspsine。项目中给出的Demo相当简单,这里按照Demo对该库的使用进行分析。
首先看一下Demo的结构:
(BTW:使用简书插图时,直接将图片拖进来即可完成上传并生成链接。然而图片的尺寸不一定合适,如何定义图片的大小或比例,知乎上有一个讨论markdown中插入图片怎么定义图片的大小或比例?,简单总结一下三种方式:一、嵌入HTML代码(img标签),二、使用支持图片大小更改操作的Mou编辑器(图片链接后加" =100x100"),三、使用支持参数的图床,如七牛。使用过程中发现简书默认支持七牛的图床接口,简直太方便了!执行缩放图片操作只需配置/thumbnail/参数即可,具体可查看七牛的图片处理高级文档。
另:CloudApp也是相当好用,奈何被墙,在我已使用VPN的情况下在简书里书写图片的链接仍然无法显示,太遗憾了!)
言归正传,接着看项目结构,这里采用Module dependency方式依赖下载库MultiThreadDownloader,先啰嗦一下GitHub上的使用说明:
- Step 1: Add permission in 'AndroidManifest.xml'
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Step 2: Add "compile project(':library')" in your 'build.gradle' file.(Maven or jCenter support will be coming soon)
Step 3: Config it in your Application class
private void initDownloader() {
DownloadConfiguration configuration = new DownloadConfiguration();
configuration.setMaxThreadNum(10);
configuration.setThreadNum(3);
DownloadManager.getInstance().init(getApplicationContext(), configuration);
}
- Step 4: Just use it!
// first: build a DownloadRequest:
final DownloadRequest request = new DownloadRequest.Builder()
.setTitle(appInfo.getName() + ".apk")
.setUri(appInfo.getUrl())
.setFolder(mDownloadDir)
.build();
// download:
// the tag here, you can simply use download uri as your tag;
DownloadManager.getInstance().download(request, tag, new CallBack() {
@Override
public void onStarted() {
}
@Override
public void onConnecting() {
}
@Override
public void onConnected(long total, boolean isRangeSupport) {
}
@Override
public void onProgress(long finished, long total, int progress) {
}
@Override
public void onCompleted() {
}
@Override
public void onDownloadPaused() {
}
@Override
public void onDownloadCanceled() {
}
@Override
public void onFailed(DownloadException e) {
}
});
//pause
DownloadManager.getInstance().pause(tag);
//pause all
DownloadManager.getInstance().pauseAll();
//cancel
DownloadManager.getInstance().cancel(tag);
//cancel all
DownloadManager.getInstance().cancelAll();
完成这些之后就能够使用下载功能了,然而这种方式并不适用于我们通常的业务场景,下面我将对涉及ListView的下载业务场景中使用该库进行一些说明。
本文主要是针对下载业务做介绍,对于下载所用到的URL的集合,采用常量String[]形式放到类DataSource中,作为数据源,其他相关的NAMES、IMAGES也采用这种方式,并暴露获取数据的方法:
(现实情况中URL基本都要通过服务器交互来获取,有机会再去介绍这部分内容)
private static final String[] URLS = {
"http://s1.music.126.net/download/android/CloudMusic_2.8.1_official_4.apk",
"http://dl.m.cc.youku.com/android/phone/Youku_Phone_youkuweb.apk",
"http://dldir1.qq.com/qqmi/TencentVideo_V4.1.0.8897_51.apk",
"http://wap3.ucweb.com/files/UCBrowser/zh-cn/999/UCBrowser_V10.6.0.620_android_pf145_(Build150721222435).apk",
"http://msoftdl.360.cn/mobilesafe/shouji360/360safesis/360MobileSafe_6.2.3.1060.apk",
"http://www.51job.com/client/51job_51JOB_1_AND2.9.3.apk",
"http://upgrade.m.tv.sohu.com/channels/hdv/5.0.0/SohuTV_5.0.0_47_201506112011.apk",
"http://dldir1.qq.com/qqcontacts/100001_phonebook_4.0.0_3148.apk",
"http://download.alicdn.com/wireless/taobao4android/latest/702757.apk",
"http://apps.wandoujia.com/apps/com.jm.android.jumei/download",
"http://download.3g.fang.com/soufun_android_30001_7.9.0.apk"
};
……
……
……
public List<AppInfo> getData() {
List<AppInfo> appInfos = new ArrayList<AppInfo>();
for (int i = 0; i < NAMES.length; i++) {
AppInfo appInfo = new AppInfo(String.valueOf(i), NAMES[i], IMAGES[i], URLS[i]);
appInfos.add(appInfo);
}
return appInfos;
}
如此一来,数据源的问题解决了,下面看一下Demo的效果(这时候看效果似乎有点晚了):
采用ListView必然会用到Adapter、ListView的Item对应的Entity等,这里每个Item显示的是一个应用的相关信息,对应的Entity类AppInfo为:
这里定义的静态的下载状态字段值和成员变量status
相关联,通过status
字段我们灵活设置ListView中Item的下载状态,并能更新Item对应的可操作方式,若当前Item的status
为STATUS_NOT_DOWNLOAD(0)
,则Item对应的状态为未下载,该Item的可操作方式为可执行下载。AppInfo类提供了两个方法getStatusText()
和getButtonText()
映射这种关系。
下面看一下UI中的ListViewFragment,主要成员变量为:
private List<AppInfo> mAppInfos;
private ListViewAdapter mAdapter;
private File mDownloadDir;
private DownloadReceiver mReceiver;
onCreate()中的操作:
mDownloadDir = new File(Environment.getExternalStorageDirectory(), "Download");
mAdapter = new ListViewAdapter();
mAdapter.setOnItemClickListener(this);
mAppInfos = DataSource.getInstance().getData();
for (AppInfo info : mAppInfos) {
DownloadInfo downloadInfo = DownloadManager.getInstance().getDownloadProgress(info.getUrl());
if (downloadInfo != null) {
info.setProgress(downloadInfo.getProgress());
info.setDownloadPerSize(Utils.getDownloadPerSize(downloadInfo.getFinished(), downloadInfo.getLength()));
info.setStatus(AppInfo.STATUS_PAUSED);
}
}
设定下载的路径 mDownloadDir
,实例化mAdapter
,绑定mAdapter
的点击监听器,获取数据源mAppInfos
, ** 最重要的一步:给数据源中每一项的下载状态赋值。**
这里的 progress
,downloadPersize
和 status
是由MultiThreadDownloader库为我们维护,只需要每一项的url值即可。
onActivityCreated()中完成ListView和Adapter的绑定及Adapter的数据填充。
每条Item下载状态的更新是通过ListViewFragment中的广播接收器 DownloadReceiver
实现的,则一定存在发送广播的地方。的确,广播发送是在Service中执行的,Service的内容稍后介绍;发送时会将AppInfo对象一起发送(仔细观察可以发现AppInfo类实现了Serializable接口,就是为了将其通过广播的Intent传递)。 DownloadReceiver
中可以获取Item的position信息,并获取AppInfo对象,并根据AppInfo的status字段,使用:
ListViewAdapter.ViewHolder holder = getViewHolder(position)
的方式设置Item的显示状态和对应的内容。
BroadcastReceiver的使用需要注册和注销操作,分别在onResume()和onPause()方法中调用。
private void register() {
mReceiver = new DownloadReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(DownloadService.ACTION_DOWNLOAD_BROAD_CAST);
LocalBroadcastManager.getInstance(getContext()).registerReceiver(mReceiver, intentFilter);
}
private void unRegister() {
if (mReceiver != null) {
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mReceiver);
}
}
在项目结构中有一个listener包,这里面定义了一个接口:OnItemClickListener
,它接收一个泛型参数,在后边的使用中会具体化成AppInfo。
public interface OnItemClickListener<T> {
void onItemClick(View v, int position, T t);
}
ListViewFragment实现了OnItemClickListener接口,并将泛型参数具体化为AppInfo,所以在ListViewFragment中还要重写onItemClick()
方法,这是为了处理Item的下载按钮点击处理下载操作的问题;ListViewAdapter中Item的下载按钮设置点击的监听器,并用OnItemClickListener的onItemClick()
方法实现,最终实现对ListViewFragment中覆写的onItemClick()
方法的调用(这部分的知识应该再看一下);这里只简单处理了下载和暂停操作。
@Override
public void onItemClick(View v, final int position, final AppInfo appInfo) {
if (appInfo.getStatus() == AppInfo.STATUS_DOWNLOADING || appInfo.getStatus() == AppInfo.STATUS_CONNECTING) {
pause(appInfo.getUrl());
} else {
download(position, appInfo.getUrl(), appInfo);
}
}
对于下载和暂停的方法在ListViewFragment中实现,分别调用了DownloadService中的方法:
private void download(int position, String tag, AppInfo info) {
DownloadService.intentDownload(getActivity(), position, tag, info);
}
private void pause(String tag)
DownloadService.intentPause(getActivity(), tag);
}
下面具体分析一下DownloadService中的内容。
DownloadService中提供了启动service的方法:intentDownload()、intentPause()
,传递ACTION_DOWNLOAD、EXTRA_POSITION、EXTRA_POSITION
和EXTRA_APP_INFO
作为参数。
public static void intentDownload(Context context, int position, String tag, AppInfo info) {
Intent intent = new Intent(context, DownloadService.class);
intent.setAction(ACTION_DOWNLOAD);
intent.putExtra(EXTRA_POSITION, position);
intent.putExtra(EXTRA_POSITION, tag);
intent.putExtra(EXTRA_APP_INFO, info);
context.startService(intent);
}
public static void intentPause(Context context, String tag) {
Intent intent = new Intent(context, DownloadService.class);
intent.setAction(ACTION_PAUSE);
intent.putExtra(EXTRA_TAG, tag);
context.startService(intent);
}
onStartCommand()方法中判断传进来的action名字,调用对应的方法;下载操作的开始、暂停和取消都是通过DownloadManager处理,以下是下载方法:
private void download(final int position, final AppInfo appInfo, String tag) {
final DownloadRequest request = new DownloadRequest.Builder()
.setTitle(appInfo.getName() + ".apk")
.setUri(appInfo.getUrl())
.setFolder(mDownloadDir)
.build();
mDownloadManager.download(request, tag, new DownloadCallBack(position, appInfo, mNotificationManager, getApplicationContext()));
}
注意DownloadManager.download()方法中传入了一个匿名的DownloadCallBack对象,它是下载过程的方法回调,发送广播并更新Notification。
通过DownloadService中监听下载的状态发送广播,并在ListViewFragment中通过DownloadReceiver接收广播消息,实现ListView每一项的状态更新。
在ListViewFragment中有两个方法:isCurrentListViewItemVisible()
和getViewHolder()
:
private boolean isCurrentListViewItemVisible(int position) {
int first = listView.getFirstVisiblePosition();
int last = listView.getLastVisiblePosition();
return first <= position && position <= last;
}
private ListViewAdapter.ViewHolder getViewHolder(int position) {
int childPosition = position - listView.getFirstVisiblePosition();
View view = listView.getChildAt(childPosition);
return (ListViewAdapter.ViewHolder) view.getTag();
}
ViewHolder的获取是通过position参数,获取ListView的该位置Item的View;在DownloadReceiver接收到广播消息更新View的状态时,首先要判断该View当前是否可见,传入position参数,看它是否在ListView当前所有可见Item的起止position范围内。
这是相当简洁的一个下载库,在我看来使用起来也较为清晰。这里所做的分析只是我在使用它的过程中的一些总结,理解有误或者总结不当的地方欢迎指正!