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


我們的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的用法


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM