你们好,我又回来了!html
标题好像又起的不知所云,可是貌似也想不起更好的标题,看看效果图android
如今有个文件列表,每一个列表标签都有一个下载的按钮,点击如下载对应的文件,若是已下载则显示“已下载”,反之显示“点击下载”。git
首先咱们使用okhttp框架下载文件,而且使用progressDialog显示下载进度,至于界面主列表,则是高端大气上档次的RecyclerView,啥?你还告诉我你用listView?好了不说废话,下来就一步步实现该功能吧。github
1、首先新建应用,打开app的build.gradle添加经常使用框架的依赖web
1.RecyerView(v7包默认不带,因此须要咱们手动添加)浏览器
2.BaseQuickAdapter(一个搭配RecyerView很强大简洁易用的万能适配器)缓存
3.okhttp(最经常使用的okhttp网络框架之一,无人不知无人不晓)tomcat
//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图标网络
等待加载完成便可。
2、完成环境的搭建,接下来咱们就能够画界面啦。
首先天然是主界面了,没什么好说的,直接整个RecycerView怼上去就可。
activity_main.xml <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <android.support.v7.widget.RecyclerView android:id="@+id/main_recyclerview" android:layout_width="match_parent" android:layout_height="match_parent"/> </RelativeLayout>
而后是列表item界面,也没什么太复杂的东西,这里是直接整个左文字显示标题,再来个右按钮启动下载方法,由于recyclerview默认没有分割线,咱们再给底部怼一个view便可,能够根据需求进行更改。
item_main.xml <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="55dp" > <TextView android:layout_width="wrap_content" android:id="@+id/tv" android:padding="15dp" android:layout_centerVertical="true" android:text="11111" android:layout_height="wrap_content" /> <Button android:id="@+id/item_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_marginRight="15dp" android:padding="10dp" android:layout_centerVertical="true"/> <View android:layout_width="match_parent" android:background="@color/colorPrimary" android:layout_alignParentBottom="true" android:layout_height="1dp"/> </RelativeLayout>
3、画完了布局,咱们加个列表内容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按钮上的文字显示,具体逻辑咱们接下来会谈到。
4、接下来开整adapter
新建一个MainAdapter继承BaseQuickAdapter,生成convert方法,再新建一个构造函数以供MainActivity引用,同时把item布局文件塞进super里面的layoutResId参数里。
public class MainAdapter extends BaseQuickAdapter<MainBean, BaseViewHolder> { public MainAdapter(int layoutResId, @Nullable List<MainBean> 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(); }
8、准备工做基本完成,接下来咱们就在列表界面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清单文件中添加权限。
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
如今咱们能够先把程序跑起来,点击下载按钮
显示下载成功,咱们打开相应包名下的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附上