1. 程式人生 > >Android開發丶一步步教你實現okhttp帶進度的列表下載檔案功能

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附上

資源下載