大家好,我又回来了!
标题好像又起的不知所云,但是貌似也想不起更好的标题,看看效果图
现在有个文件列表,每个列表标签都有一个下载的按钮,点击以下载对应的文件,如果已下载则显示“已下载”,反之显示“点击下载”。
首先我们使用okhttp框架下载文件,并且使用progressDialog显示下载进度,至于界面主列表,则是高端大气上档次的RecyclerView,啥?你还告诉我你用listView?好了不说废话,下来就一步步实现该功能吧。
一、首先新建应用,打开app的build.gradle添加常用框架的依赖
1.RecyerView(v7包默认不带,所以需要我们手动添加)
2.BaseQuickAdapter(一个搭配RecyerView很强大简洁易用的万能适配器)
3.okhttp(最常用的okhttp网络框架之一,无人不知无人不晓)
//RecycerView列表控件
implementation 'com.android.support:recyclerview-v7:28.0.0'
//okhttp网络下载框架
implementation 'com.squareup.okhttp3:okhttp:3.6.0'
//BaseQuickAdapter适配器
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.22'
之后打开project的build.gradle,在allpreject下的repositories节点下添加以下代码,供BaseQuickAdapter依赖所用
maven { url "https://jitpack.io" }
allprojects {
repositories {
google()
jcenter()
maven { url "https://jitpack.io" }
}
}
添加完毕点击Sync Project图标
等待加载完成即可。
二、完成环境的搭建,接下来我们就可以画界面啦。
首先自然是主界面了,没什么好说的,直接整个RecycerView怼上去就可。
activity_main.xml
然后是列表item界面,也没什么太复杂的东西,这里是直接整个左文字显示标题,再来个右按钮启动下载方法,因为recyclerview默认没有分割线,我们再给底部怼一个view即可,可以根据需求进行更改。
item_main.xml
三、画完了布局,我们加个列表内容bean,这个没什么难度,界面上有两个属性,textview的文字属性,button的下载状态属性。
MainBean.class
public class MainBean implements Serializable{
private String title;
private boolean isDownload;
@Override
public String toString() {
return "MainBean{" +
"title='" + title + '\'' +
", isDownload=" + isDownload +
'}';
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public boolean isDownload() {
return isDownload;
}
public void setDownload(boolean download) {
isDownload = download;
}
public MainBean(String title, boolean isDownload) {
this.title = title;
this.isDownload = isDownload;
}
}
值得一提的是,在实际开发过程中,isDownload方法服务器是不会给我们返回的,所以这里为我们手动添加,用来判断boolean值来实现button按钮上的文字显示,具体逻辑我们接下来会谈到。
四、接下来开整adapter
新建一个MainAdapter继承BaseQuickAdapter,生成convert方法,再新建一个构造函数以供MainActivity引用,同时把item布局文件塞进super里面的layoutResId参数里。
public class MainAdapter extends BaseQuickAdapter {
public MainAdapter(int layoutResId, @Nullable List data) {
super(R.layout.item_main, data);
}
@Override
protected void convert(BaseViewHolder helper, MainBean item) {
}
}
重写convert方法,先给item的textview设置title。
helper.setText(R.id.item_tv, item.getTitle());
然后给item的button设置文字,这里根据isDownload值,为true则已下载显示“已下载”,为false则未下载显示“点击下载”。
helper.setText(R.id.item_btn, item.isDownload()? "已下载": "未下载");
五.准备工作都已做好,现在可以编辑Activity了。
打开MainActivity
在onCreate()方法中
1.初始化数据
private void initData() {
MainBean bean= new MainBean("高祖提剑入咸阳,炎炎红日升扶桑", "http://10.48.78.196:8080/pdf/test.zip",false);
datalist.add(bean);
MainBean bean1= new MainBean("光武中兴续大统,金乌飞上天中央", "http://10.48.78.196:8080/pdf/test.zip",false);
datalist.add(bean1);
MainBean bean2= new MainBean("哀哉献帝绍海宇,红轮西坠咸池榜", "http://10.48.78.196:8080/pdf/test.zip",false);
datalist.add(bean2);
MainBean bean3= new MainBean("何进无谋中贵乱,凉州董卓居朝堂", "http://10.48.78.196:8080/pdf/test.zip",false);
datalist.add(bean3);
MainBean bean4= new MainBean("王允定计诛逆党,李榷郭汜兴刀枪", "http://10.48.78.196:8080/pdf/test.zip",false);
datalist.add(bean4);
MainBean bean5= new MainBean("四方盗贼如蚁聚,六合奸雄皆鹰扬", "http://10.48.78.196:8080/pdf/test.zip",false);
datalist.add(bean5);
MainBean bean6= new MainBean("孙坚孙策起江左,袁绍袁术兴河梁", "http://10.48.78.196:8080/pdf/test.zip",false);
datalist.add(bean6);
MainBean bean7= new MainBean("刘焉父子居巴蜀,刘表羁旅屯荆襄", "http://10.48.78.196:8080/pdf/test.zip",false);
datalist.add(bean7);
MainBean bean8= new MainBean("张燕张鲁霸南郑,马腾韩遂守西凉", "http://10.48.78.196:8080/pdf/test.zip",false);
datalist.add(bean8);
MainBean bean9= new MainBean("陶谦张绣公孙瓒,各逞雄才占一方", "http://10.48.78.196:8080/pdf/test.zip",false);
datalist.add(bean9);
}
2.初始化控件
/**
* 初始化控件
*/
private void initView() {
recyclerView= findViewById(R.id.main_recyclerview);
LinearLayoutManager manager= new LinearLayoutManager(this);
recyclerView.setLayoutManager(manager);
}
3.初始化适配器
/**
* 初始化适配器
*/
private void initAdapter() {
MainAdapter adapter= new MainAdapter(datalist);
recyclerView.setAdapter(adapter);
}
这时候跑起程序,界面已成功呈现。
六.遗憾的是,RecycerView并没有给我们提供item和各控件的点击监听,所以这里我们需要通过接口回调的方式完成button下载按钮的点击下载监听。
回到MainAdapter,设置自定义监听
public interface downloadClickListener {
void downloadClick(int position);
}
public void setDownloadClickListener(downloadClickListener listener){
this.listener= listener;
}
在convert()方法中设置button的点击事件,把position参数传进去以供测试。
helper.getView(R.id.item_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (listener!= null){
listener.downloadClick(helper.getAdapterPosition());
}
}
});
回到MainActivity,接口回调触发button的点击监听
//接口回调完成item上button下载按钮的点击事件
adapter.setDownloadClickListener(new MainAdapter.downloadClickListener() {
@Override
public void downloadClick(int position) {
Toast.makeText(MainActivity.this, ""+ position, Toast.LENGTH_SHORT).show();
}
});
运行程序,这里我们用Toast测试,是否position参数顺利传了进来。点击第一个item的button
点击最后一个,我们一共自定义了10条数据,所以应该Toast 9
Perfect!!!接下来我们就可以设置每个item的button的下载监听方法了。
七.下载文件方法
1.首先我们分析一下实现方法以及原理,实际开发过程中,后台一般会把文件下载路径提供给我们,测试时我们可以把文件放在tomcat服务器上以供测试,下载是通过okhttp网络框架由IO流的方式将文件下载到手机内置存储中,在手机文件管理器下的Android/data/应用包名xxxx 路径下有两个文件夹cache和file,前者cache顾名思义就是缓存文件夹,一般放置一些微型不长存的数据,如果手机因为内存不足等情况下,该文件夹经常被清除,所以不适合我们存放长时间保存的下载文件,因而我们一般在后者file文件夹下新建一个文件夹用来保存下载文件,而且因为是在包名目录下,当应用卸载时,下载的文件也会随之清除,避免了垃圾文件的残余。
原理ok了,我们来说说实现步骤吧
首先我们把要下载的文件放置在tomcat服务器上,具体环境搭建继承不详细叙述,有问题可百度。
打开tomcat的文件夹,我们会看到webapps文件夹
点击进入,新建一个文件夹放我们要下载的测试文件,我这里是新建了一个pdf文件夹,里面放了一个pdf文件
启动tomcat,获取下载路径,比如我这里
打开网络设置,获取本机IP地址 http://10.48.78.196
因为我们把需要下载的测试文件pdf_test.pdf放进了tomcat文件夹下的webapps文件夹下的pdf文件夹下,这样我们的下载路径就是 http://10.48.78.196:8080/pdf/pdf_test.pdf
在浏览器中打开如上链接,能正常打开说明我们部署成功。
perfect!这样我们通过请求该链接就能下载该pdf文件了。
2.接下来,我们决定使用okhttp下载该文件,先编写工具类。
我们之前已经添加过okhttp的依赖,所以直接引用。
主要讲几个核心方法,完整代码随后附录。
A、download()
/**
* @param url 下载连接
* @param saveDir 储存下载文件的SDCard目录
* @param listener 下载监听
*/
public void download(Context context, String fileId, final String url, final String saveDir, final OnDownloadListener listener) {
this.context= context;
Request request = new Request.Builder().url(url).build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 下载失败
listener.onDownloadFailed();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
InputStream is = null;
byte[] buf = new byte[2048];
int len = 0;
FileOutputStream fos = null;
// 储存下载文件的目录
String savePath = isExistDir(saveDir);
try {
is = response.body().byteStream();
long total = response.body().contentLength();
File file = new File(savePath, getNameFromUrl(url, fileId));
fos = new FileOutputStream(file);
long sum = 0;
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
sum += len;
int progress = (int) (sum * 1.0f / total * 100);
// 下载中
listener.onDownloading(progress);
}
fos.flush();
// 下载完成
listener.onDownloadSuccess();
} catch (Exception e) {
listener.onDownloadFailed();
} finally {
try {
if (is != null)
is.close();
} catch (IOException e) {
}
try {
if (fos != null)
fos.close();
} catch (IOException e) {
}
}
}
});
}
在该方法会产生两个回调,顾名思义,一个成功onResponse()、一个失败onFailure()
我们先看看onResponse(),当下载成功后,我们把下载的文件通过IO流的方式写入指定的手机内存路径中,因为下载和写入的进度同步进行,所以我们需要把该进度progress传递,以便我们的progressDialog显示,提升用户体验。当下载成功后,同样完成相应的处理并记得关闭IO流。
2.在如上方法中,有个isExistDir()方法。
/**
* @param saveDir
* @return
* @throws IOException
* 判断下载目录是否存在
*/
private String isExistDir(String saveDir) throws IOException {
// 下载位置
File downloadFile = new File(context.getExternalFilesDir(null), saveDir);
if (!downloadFile.mkdirs()) {
downloadFile.createNewFile();
}
String savePath = downloadFile.getAbsolutePath();
return savePath;
}
顾名思义,主要判断我们指定的手机内存路径是否存在
如果尚不存在则create一个新的文件夹目录,
if (!downloadFile.mkdirs()) {
downloadFile.createNewFile();
}
如果存在则直接返回
String savePath = downloadFile.getAbsolutePath();
return savePath;
3.在onResponse()方法中,我们注意到有一个getNameFromUrl()方法
顾名思义,这是获取下载文件的原始名称以对该文件进行下载后的命名。
因为下载文件路径都是这样的
xxxxxxxxxxxxx/我是某某某文件.xxx
所以我们把该路径最后一个/号后面的文字全部截取,就可以得到该文件的原始名称了。
return url.substring(url.lastIndexOf("/") + 1);
4.写一下相应的回调接口。以便进行相应的处理。
public interface OnDownloadListener {
/**
* 下载成功
*/
void onDownloadSuccess();
/**
* @param progress
* 下载进度
*/
void onDownloading(int progress);
/**
* 下载失败
*/
void onDownloadFailed();
}
八、准备工作基本完成,接下来我们就在列表界面Activity进行调取了。
回到Activity的列表item上的button点击事件上。去掉之前的测试toast,增加下载方法。
//接口回调完成item上button下载按钮的点击事件
adapter.setDownloadClickListener(new MainAdapter.downloadClickListener() {
@Override
public void downloadClick(int position) {
//文件下载路径
String url= "http://10.48.78.196:8080/pdf/pdf_test.pdf";
//文件在手机内存存储的路径
String saveurl= getExternalFilesDir(null)+ "/pdffile/";
//启动下载方法
DownloadUtil.get().download(MainActivity.this, url, saveurl, new DownloadUtil.OnDownloadListener() {
@Override
public void onDownloadSuccess() {
Toast.makeText(MainActivity.this, "下载成功", Toast.LENGTH_SHORT).show();
}
@Override
public void onDownloading(int progress) {
}
@Override
public void onDownloadFailed() {
Toast.makeText(MainActivity.this, "下载失败", Toast.LENGTH_SHORT).show();
}
});
}
});
在AndroidManifest.xml清单文件中添加权限。
现在我们可以先把程序跑起来,点击下载按钮
显示下载成功,我们打开相应包名下的file文件夹,看有没有该文件。
点击可以正常打开,说明我们已经成功地把服务器上的文件下载下来了。
接下来,我们给下载过程加上进度弹窗,当下载比较大和耗时的文件时显示进度,提升用户体验。
//配置progressDialog
final ProgressDialog dialog= new ProgressDialog(MainActivity.this);
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setCanceledOnTouchOutside(false);
dialog.setCancelable(true);
dialog.setTitle("正在下载中");
dialog.setMessage("请稍后...");
dialog.setProgress(0);
dialog.setMax(100);
dialog.show();
在下载的onDownloading()方法中设置进度。
@Override
public void onDownloading(int progress) {
dialog.setProgress(progress);
}
下载完成或者下载失败时隐藏掉弹窗。
dialog.dismiss();
我们现在下载个比较大的文件测试(为了进度条存在时间更长)
http://10.48.78.196:8080/pdf/test.zip
打开目录,查看是否存在
咋一看下载都成功了,突然问题来了
刚点击了好几个不同的下载按钮,为什么只有这两个文件?
原来是被同名覆盖了,这样我们要在下载命名中做点功夫了
这下就避免同名覆盖了
然后下载成功时,我们局部刷新item,将item上的下载按钮文字改为“已下载”
打开Adapter,判断文件是否存在,存在即为”已下载“,反之为“未下载”
String filePath= context.getExternalFilesDir(null)+ "/pdffile/"+ helper.getLayoutPosition()+ "_"+ item.getUrl().substring(item.getUrl().lastIndexOf("/") + 1);
if(isFileExist(filePath)){
item.setDownload(true);
}else {
item.setDownload(false);
}
helper.setText(R.id.item_btn, item.isDownload()? "已下载": "未下载");
下载成功后,局部刷新item上的button
adapter.notifyItemChanged(position);
至此全部完成,demo附上
资源下载