Android圖片加載框架最全解析(七),實現帶進度的Glide圖片加載功能

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/78357251

本文同步發表於我的微信公衆號,掃一掃文章底部的二維碼或在微信搜索 郭霖 即可關注,每天都有文章更新。

我們的Glide系列文章終於要進入收尾篇了。從我開始寫這個系列的第一篇文章時,我就知道這會是一個很長的系列,只是沒有想到竟然會寫這麼久。

在前面的六篇文章中,我們對Glide的方方面面都進行了學習,包括基本用法源碼解析緩存機制回調與監聽圖片變換以及自定義模塊。而今天,我們就要綜合利用之前所學到的知識,來對Glide進行一個比較大的功能擴展,希望大家都已經好好閱讀過了前面的六篇文章,並且有了不錯的理解。

擴展目標

首先來確立一下功能擴展的目標。雖說Glide本身就已經十分強大了,但是有一個功能卻長期以來都不支持,那就是監聽下載進度功能。

我們都知道,使用Glide來加載一張網絡上的圖片是非常簡單的,但是讓人頭疼的是,我們卻無從得知當前圖片的下載進度。如果這張圖片很小的話,那麼問題也不大,反正很快就會被加載出來。但如果這是一張比較大的GIF圖,用戶耐心等了很久結果圖片還沒顯示出來,這個時候你就會覺得下載進度功能是十分有必要的了。

好的,那麼我們今天的目標就是對Glide進行功能擴展,使其支持監聽圖片下載進度的功能。

開始

今天這篇文章我會帶着大家從零去創建一個新的項目,一步步地進行實現,最終完成一個帶進度的Glide圖片加載的Demo。當然,在本篇文章的最後我會提供這個Demo的完整源碼,但是這裏我仍然希望大家能用心跟着我一步步來編寫。

那麼我們現在就開始吧,首先創建一個新項目,就叫做GlideProgressTest吧。

項目創建完成後的第一件事就是要將必要的依賴庫引入到當前的項目當中,目前我們必須要依賴的兩個庫就是Glide和OkHttp。在app/build.gradle文件當中添加如下配置:

dependencies { 
    compile 'com.github.bumptech.glide:glide:3.7.0' 
    compile 'com.squareup.okhttp3:okhttp:3.9.0' 
}

另外,由於Glide和OkHttp都需要用到網絡功能,因此我們還得在AndroidManifest.xml中聲明一下網絡權限才行:

<uses-permission android:name="android.permission.INTERNET" />

好了,這樣準備工作就完成了。

替換通訊組件

通過第二篇文章的源碼分析,我們知道了Glide內部HTTP通訊組件的底層實現是基於HttpUrlConnection來進行定製的。但是HttpUrlConnection的可擴展性比較有限,我們在它的基礎之上無法實現監聽下載進度的功能,因此今天的第一個大動作就是要將Glide中的HTTP通訊組件替換成OkHttp。

關於HTTP通訊組件的替換原理和替換方式,我在第六篇文章當中都介紹得比較清楚了,這裏就不再贅述。下面我們就來開始快速地替換一下。

新建一個OkHttpFetcher類,並且實現DataFetcher接口,代碼如下所示:

public class OkHttpFetcher implements DataFetcher<InputStream> { 

    private final OkHttpClient client; 
    private final GlideUrl url; 
    private InputStream stream; 
    private ResponseBody responseBody; 
    private volatile boolean isCancelled; 

    public OkHttpFetcher(OkHttpClient client, GlideUrl url) { 
        this.client = client; 
        this.url = url; 
    } 

    @Override 
    public InputStream loadData(Priority priority) throws Exception { 
        Request.Builder requestBuilder = new Request.Builder() 
                .url(url.toStringUrl()); 
        for (Map.Entry<String, String> headerEntry : url.getHeaders().entrySet()) {
            String key = headerEntry.getKey(); 
            requestBuilder.addHeader(key, headerEntry.getValue()); 
        } 
        Request request = requestBuilder.build(); 
        if (isCancelled) { 
            return null; 
        } 
        Response response = client.newCall(request).execute(); 
        responseBody = response.body(); 
        if (!response.isSuccessful() || responseBody == null) { 
            throw new IOException("Request failed with code: " + response.code());
        } 
        stream = ContentLengthInputStream.obtain(responseBody.byteStream(), 
                responseBody.contentLength()); 
        return stream; 
    } 

    @Override 
    public void cleanup() { 
        try { 
            if (stream != null) { 
                stream.close(); 
            } 
            if (responseBody != null) { 
                responseBody.close(); 
            } 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
    } 

    @Override 
    public String getId() { 
        return url.getCacheKey(); 
    } 

    @Override 
    public void cancel() { 
        isCancelled = true; 
    } 
}

然後新建一個OkHttpGlideUrlLoader類,並且實現ModelLoader

public class OkHttpGlideUrlLoader implements ModelLoader<GlideUrl, InputStream> { 

    private OkHttpClient okHttpClient; 

    public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> { 

        private OkHttpClient client; 

        public Factory() { 
        } 

        public Factory(OkHttpClient client) { 
            this.client = client; 
        } 

        private synchronized OkHttpClient getOkHttpClient() { 
            if (client == null) { 
                client = new OkHttpClient(); 
            } 
            return client; 
        } 

        @Override 
        public ModelLoader<GlideUrl, InputStream> build(Context context, GenericLoaderFactory factories) {
            return new OkHttpGlideUrlLoader(getOkHttpClient()); 
        } 

        @Override 
        public void teardown() { 
        } 
    } 

    public OkHttpGlideUrlLoader(OkHttpClient client) { 
        this.okHttpClient = client; 
    } 

    @Override 
    public DataFetcher<InputStream> getResourceFetcher(GlideUrl model, int width, int height) { 
        return new OkHttpFetcher(okHttpClient, model); 
    } 
}

接下來,新建一個MyGlideModule類並實現GlideModule接口,然後在registerComponents()方法中將我們剛剛創建的OkHttpGlideUrlLoader和OkHttpFetcher註冊到Glide當中,將原來的HTTP通訊組件給替換掉,如下所示:

public class MyGlideModule implements GlideModule { 
    @Override 
    public void applyOptions(Context context, GlideBuilder builder) { 
    } 

    @Override 
    public void registerComponents(Context context, Glide glide) { 
        glide.register(GlideUrl.class, InputStream.class, new OkHttpGlideUrlLoader.Factory());
    } 
}

最後,爲了讓Glide能夠識別我們自定義的MyGlideModule,還得在AndroidManifest.xml文件當中加入如下配置才行:

<manifest> 
    ... 
    <application> 
        <meta-data 
            android:name="com.example.glideprogresstest.MyGlideModule" 
            android:value="GlideModule" /> 
        ... 
    </application> 
</manifest>

OK,這樣我們就把Glide中的HTTP通訊組件成功替換成OkHttp了。

實現下載進度監聽

那麼,將HTTP通訊組件替換成OkHttp之後,我們又該如何去實現監聽下載進度的功能呢?這就要依靠OkHttp強大的攔截器機制了。

我們只要向OkHttp中添加一個自定義的攔截器,就可以在攔截器中捕獲到整個HTTP的通訊過程,然後加入一些自己的邏輯來計算下載進度,這樣就可以實現下載進度監聽的功能了。

攔截器屬於OkHttp的高級功能,不過即使你之前並沒有接觸過攔截器,我相信你也能輕鬆看懂本篇文章的,因爲它本身並不難。

確定了實現思路之後,那我們就開始動手吧。首先創建一個沒有任何邏輯的空攔截器,新建ProgressInterceptor類並實現Interceptor接口,代碼如下所示:

public class ProgressInterceptor implements Interceptor { 

    @Override 
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request(); 
        Response response = chain.proceed(request); 
        return response; 
    } 

}

這個攔截器中我們可以說是什麼都沒有做。就是攔截到了OkHttp的請求,然後調用proceed()方法去處理這個請求,最終將服務器響應的Response返回。

接下來我們需要啓用這個攔截器,修改MyGlideModule中的代碼,如下所示:

public class MyGlideModule implements GlideModule { 
    @Override 
    public void applyOptions(Context context, GlideBuilder builder) { 
    } 

    @Override 
    public void registerComponents(Context context, Glide glide) { 
        OkHttpClient.Builder builder = new OkHttpClient.Builder(); 
        builder.addInterceptor(new ProgressInterceptor()); 
        OkHttpClient okHttpClient = builder.build(); 
        glide.register(GlideUrl.class, InputStream.class, new OkHttpGlideUrlLoader.Factory(okHttpClient));
    } 
}

這裏我們創建了一個OkHttpClient.Builder,然後調用addInterceptor()方法將剛纔創建的ProgressInterceptor添加進去,最後將構建出來的新OkHttpClient對象傳入到OkHttpGlideUrlLoader.Factory中即可。

好的,現在自定義的攔截器已經啓用了,接下來就可以開始去實現下載進度監聽的具體邏輯了。首先新建一個ProgressListener接口,用於作爲進度監聽回調的工具,如下所示:

public interface ProgressListener {

    void onProgress(int progress);

}

然後我們在ProgressInterceptor中加入註冊下載監聽和取消註冊下載監聽的方法。修改ProgressInterceptor中的代碼,如下所示:

public class ProgressInterceptor implements Interceptor { 

    static final Map<String, ProgressListener> LISTENER_MAP = new HashMap<>();

    public static void addListener(String url, ProgressListener listener) {
        LISTENER_MAP.put(url, listener); 
    } 

    public static void removeListener(String url) { 
        LISTENER_MAP.remove(url); 
    } 

    @Override 
    public Response intercept(Chain chain) throws IOException { 
        Request request = chain.request(); 
        Response response = chain.proceed(request); 
        return response; 
    } 

}

可以看到,這裏使用了一個Map來保存註冊的監聽器,Map的鍵是一個URL地址。之所以要這麼做,是因爲你可能會使用Glide同時加載很多張圖片,而這種情況下,必須要能區分出來每個下載進度的回調到底是對應哪個圖片URL地址的。

接下來就要到今天最複雜的部分了,也就是下載進度的具體計算。我們需要新建一個ProgressResponseBody類,並讓它繼承自OkHttp的ResponseBody,然後在這個類當中去編寫具體的監聽下載進度的邏輯,代碼如下所示:

public class ProgressResponseBody extends ResponseBody {

    private static final String TAG = "ProgressResponseBody";

    private BufferedSource bufferedSource;

    private ResponseBody responseBody;

    private ProgressListener listener;

    public ProgressResponseBody(String url, ResponseBody responseBody) {
        this.responseBody = responseBody;
        listener = ProgressInterceptor.LISTENER_MAP.get(url);
    }

    @Override
    public MediaType contentType() {
        return responseBody.contentType();
    }

    @Override
    public long contentLength() {
        return responseBody.contentLength();
    }

    @Override 
    public BufferedSource source() {
        if (bufferedSource == null) {
            bufferedSource = Okio.buffer(new ProgressSource(responseBody.source()));
        }
        return bufferedSource;
    }

    private class ProgressSource extends ForwardingSource {

        long totalBytesRead = 0;

        int currentProgress;

        ProgressSource(Source source) {
            super(source);
        }

        @Override 
        public long read(Buffer sink, long byteCount) throws IOException {
            long bytesRead = super.read(sink, byteCount);
            long fullLength = responseBody.contentLength();
            if (bytesRead == -1) {
                totalBytesRead = fullLength;
            } else {
                totalBytesRead += bytesRead;
            }
            int progress = (int) (100f * totalBytesRead / fullLength);
            Log.d(TAG, "download progress is " + progress);
            if (listener != null && progress != currentProgress) {
                listener.onProgress(progress);
            }
            if (listener != null && totalBytesRead == fullLength) {
                listener = null;
            }
            currentProgress = progress;
            return bytesRead;
        }
    }

}

其實這段代碼也不是很難,下面我來簡單解釋一下。首先,我們定義了一個ProgressResponseBody的構造方法,該構造方法中要求傳入一個url參數和一個ResponseBody參數。那麼很顯然,url參數就是圖片的url地址了,而ResponseBody參數則是OkHttp攔截到的原始的ResponseBody對象。然後在構造方法中,我們調用了ProgressInterceptor中的LISTENER_MAP來去獲取該url對應的監聽器回調對象,有了這個對象,待會就可以回調計算出來的下載進度了。

由於繼承了ResponseBody類之後一定要重寫contentType()、contentLength()和source()這三個方法,我們在contentType()和contentLength()方法中直接就調用傳入的原始ResponseBody的contentType()和contentLength()方法即可,這相當於一種委託模式。但是在source()方法中,我們就必須加入點自己的邏輯了,因爲這裏要涉及到具體的下載進度計算。

那麼我們具體看一下source()方法,這裏先是調用了原始ResponseBody的source()方法來去獲取Source對象,接下來將這個Source對象封裝到了一個ProgressSource對象當中,最終再用Okio的buffer()方法封裝成BufferedSource對象返回。

那麼這個ProgressSource是什麼呢?它是一個我們自定義的繼承自ForwardingSource的實現類。ForwardingSource也是一個使用委託模式的工具,它不處理任何具體的邏輯,只是負責將傳入的原始Source對象進行中轉。但是,我們使用ProgressSource繼承自ForwardingSource,那麼就可以在中轉的過程中加入自己的邏輯了。

可以看到,在ProgressSource中我們重寫了read()方法,然後在read()方法中獲取該次讀取到的字節數以及下載文件的總字節數,並進行一些簡單的數學計算就能算出當前的下載進度了。這裏我先使用Log工具將算出的結果打印了一下,再通過前面獲取到的回調監聽器對象將結果進行回調。

好的,現在計算下載進度的邏輯已經完成了,那麼我們快點在攔截器當中使用它吧。修改ProgressInterceptor中的代碼,如下所示:

public class ProgressInterceptor implements Interceptor { 

    ... 

    @Override 
    public Response intercept(Chain chain) throws IOException { 
        Request request = chain.request(); 
        Response response = chain.proceed(request); 
        String url = request.url().toString(); 
        ResponseBody body = response.body(); 
        Response newResponse = response.newBuilder().body(new ProgressResponseBody(url, body)).build();
        return newResponse; 
    } 

}

這裏也都是一些OkHttp的簡單用法。我們通過Response的newBuilder()方法來創建一個新的Response對象,並把它的body替換成剛纔實現的ProgressResponseBody,最終將新的Response對象進行返回,這樣計算下載進度的邏輯就能生效了。

代碼寫到這裏,我們就可以來運行一下程序了。現在無論是加載任何網絡上的圖片,都應該是可以監聽到它的下載進度的。

修改activity_main.xml中的代碼,如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="match_parent" 
    android:layout_height="match_parent" 
    android:orientation="vertical"> 

    <Button 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:text="Load Image" 
        android:onClick="loadImage" 
        /> 

    <ImageView 
        android:id="@+id/image" 
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" /> 
</LinearLayout>

很簡單,這裏使用了一個Button按鈕來加載圖片,使用了一個ImageView來展示圖片。

然後修改MainActivity中的代碼,如下所示:

public class MainActivity extends AppCompatActivity { 

    String url = "http://guolin.tech/book.png"; 

    ImageView image; 

    @Override 
    protected void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main); 
        image = (ImageView) findViewById(R.id.image); 
    } 

    public void loadImage(View view) { 
        Glide.with(this) 
             .load(url) 
             .diskCacheStrategy(DiskCacheStrategy.NONE)
             .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
             .into(image); 
    } 
}

現在就可以運行一下程序了,效果如下圖所示。

OK,圖片已經加載出來了。那麼怎麼驗證有沒有成功監聽到圖片的下載進度呢?還記得我們剛纔在ProgressResponseBody中加的打印日誌嗎?現在只要去logcat中觀察一下就知道了,如下圖所示:

由此可見,下載進度監聽功能已經成功實現了。

進度顯示

雖然現在我們已經能夠監聽到圖片的下載進度了,但是這個進度目前還只能顯示在控制檯打印當中,這對於用戶來說是沒有任何意義的,因此我們下一步就是要想辦法將下載進度顯示到界面上。

現在修改MainActivity中的代碼,如下所示:

public class MainActivity extends AppCompatActivity {

    String url = "http://guolin.tech/book.png";

    ImageView image;

    ProgressDialog progressDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        image = (ImageView) findViewById(R.id.image);
        progressDialog = new ProgressDialog(this);
        progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        progressDialog.setMessage("加載中"); 
    }

    public void loadImage(View view) {
        ProgressInterceptor.addListener(url, new ProgressListener() {
            @Override
            public void onProgress(int progress) {
                progressDialog.setProgress(progress);
            }
        });
        Glide.with(this)
             .load(url)
             .diskCacheStrategy(DiskCacheStrategy.NONE)
             .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
             .into(new GlideDrawableImageViewTarget(image) {
                 @Override
                 public void onLoadStarted(Drawable placeholder) {
                     super.onLoadStarted(placeholder);
                     progressDialog.show();
                 }

                 @Override 
                 public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> animation) {
                     super.onResourceReady(resource, animation);
                     progressDialog.dismiss();
                     ProgressInterceptor.removeListener(url);
                 }
             });
    }

代碼並不複雜。這裏我們新增了一個ProgressDialog用來顯示下載進度,然後在loadImage()方法中,調用了ProgressInterceptor.addListener()方法來去註冊一個下載監聽器,並在onProgress()回調方法中更新當前的下載進度。

最後,Glide的into()方法也做了修改,這次是into到了一個GlideDrawableImageViewTarget當中。我們重寫了它的onLoadStarted()方法和onResourceReady()方法,從而實現當圖片開始加載的時候顯示進度對話框,當圖片加載完成時關閉進度對話框的功能。

現在重新運行一下程序,效果如下圖所示。

當然,不僅僅是靜態圖片,體積比較大的GIF圖也是可以成功監聽到下載進度的。比如我們把圖片的url地址換成http://guolin.tech/test.gif,重新運行程序,效果如下圖所示。

好了,這樣我們就把帶進度的Glide圖片加載功能完整地實現了一遍。雖然這個例子當中的界面都比較粗糙,下載進度框也是使用的最簡陋的,不過只要將功能學會了,界面那都不是事,大家後期可以自己進行各種界面優化。

最後,如果你想要下載完整的Demo,請點擊這裏

寫了大半年的一個系列就這麼要結束了,突然還有一點點小不捨。如果大家能將整個系列的七篇文章都很好地掌握了,那麼現在自稱爲Glide高手應該不算過分。

其實在剛打算寫這個系列的時候,我是準備寫八篇文章,結果最後滿打滿算就只寫出了七篇。那麼爲了兌現自己當初八篇的承諾,我準備最後一篇寫一下關於Glide 4.0版本的用法,順便讓我自己也找個契機去研究一下新版本。當然,這並不是說Glide 3.7版本就已經淘汰了,事實上,Glide 3.7版本十分穩定,而且還能幾乎完全滿足我平時開發的所有需求,是可以長期使用下去的一個版本。

感興趣的朋友請繼續閱讀 Android圖片加載框架最全解析(八),帶你全面瞭解Glide 4的用法

關注我的技術公衆號,每天都有優質技術文章推送。關注我的娛樂公衆號,工作、學習累了的時候放鬆一下自己。

微信掃一掃下方二維碼即可關注:

        

我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章