Android開發丶一步步教你實現okhttp帶進度的列表下載檔案功能
大家好,我又回來了!
標題好像又起的不知所云,但是貌似也想不起更好的標題,話不多少,先來張效果圖
根據上圖就很明顯標題的含義了,每個列表標籤都有一個下載的按鈕,點選以下載對應的檔案,如果已下載則顯示“已下載”,反之顯示“點選下載”。
首先我們使用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
<?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>
三、畫完了佈局,我們加個列表內容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<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(); }
八、準備工作基本完成,接下來我們就在列表介面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附上