The most complete analysis of the Android image loading framework (7), to realize the Glide image loading function with progress

Our Glide series is finally wrapping up. From the time I started writing the first article in this series, I knew it was going to be a long series, I just didn't expect it to be this long.

In the previous six articles, we have learned all aspects of Glide, including basic usage , source code parsing , caching mechanism , callback and monitoring , image transformation , and custom modules . Today, we will comprehensively use the knowledge we have learned before to expand Glide's functions. I hope you have read the previous six articles and have a good understanding.

Scaling target

First of all, let's establish the goal of function expansion. Although Glide itself is already very powerful, there is one function that has not been supported for a long time, that is, the function of monitoring download progress.

We all know that it is very simple to use Glide to load a picture on the Internet, but the troublesome thing is that we have no way of knowing the download progress of the current picture. If the image is small, then it's not a big problem, it will be loaded quickly anyway. But if this is a relatively large GIF image, and the user waits patiently for a long time and the image has not been displayed, you will feel that the download progress function is very necessary at this time.

Ok, so our goal today is to extend the function of Glide to support the function of monitoring the progress of image download.

Start

In this article today, I will take you to create a new project from scratch, implement it step by step, and finally complete a demo with progressed Glide image loading. Of course, at the end of this article, I will provide the complete source code of this Demo, but here I still hope that everyone can follow me step by step.

So let's get started now, by creating a new project called GlideProgressTest.

The first thing after the project is created is to introduce the necessary dependent libraries into the current project. At present, the two libraries we must depend on are Glide and OkHttp. Add the following configuration to the app/build.gradle file:

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

 

In addition, since both Glide and OkHttp need to use network functions, we also have to declare network permissions in AndroidManifest.xml:

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

 

Well, the preparation work is complete.

Replace communication components

Through the source code analysis of the second article, we know that the underlying implementation of Glide's internal HTTP communication components is customized based on HttpUrlConnection. However, the scalability of HttpUrlConnection is relatively limited, and we cannot implement the function of monitoring the download progress based on it. Therefore, the first big move today is to replace the HTTP communication component in Glide with OkHttp.

Regarding the replacement principle and replacement method of HTTP communication components, I have introduced them more clearly in the sixth article, so I will not repeat them here. Let's start with a quick replacement.

Create a new OkHttpFetcher class and implement the DataFetcher interface. The code is as follows:

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; } }

 

Then create a new OkHttpGlideUrlLoader class and implement 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); } }

 

Next, create a new MyGlideModule class and implement the GlideModule interface, and then register the OkHttpGlideUrlLoader and OkHttpFetcher we just created to Glide in the registerComponents() method, and replace the original HTTP communication components, as shown below:

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()); } }

 

Finally, in order for Glide to recognize our custom MyGlideModule, we have to add the following configuration to the AndroidManifest.xml file:

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

 

OK, so we have successfully replaced the HTTP communication component in Glide with OkHttp.

Implement download progress monitoring

Then, after replacing the HTTP communication component with OkHttp, how can we implement the function of monitoring the download progress? This depends on the powerful interceptor mechanism of OkHttp.

As long as we add a custom interceptor to OkHttp, we can capture the entire HTTP communication process in the interceptor, and then add some own logic to calculate the download progress, so that the download progress monitoring function can be realized.

Interceptor is an advanced function of OkHttp, but even if you have not touched interceptor before, I believe you can easily understand this article, because it is not difficult in itself.

After determining the implementation idea, let's get started. First create an empty interceptor without any logic, create a new ProgressInterceptor class and implement the Interceptor interface, the code is as follows:

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

 

In this interceptor we can say that nothing is done. It is to intercept the OkHttp request, then call the proceed() method to process the request, and finally return the Response of the server response.

Next we need to enable this interceptor and modify the code in MyGlideModule as follows:

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)); } }

 

Here we create an OkHttpClient.Builder, then call the addInterceptor() method to add the ProgressInterceptor just created, and finally pass the constructed new OkHttpClient object into OkHttpGlideUrlLoader.Factory.

Ok, now that the custom interceptor has been enabled, we can start to implement the specific logic of download progress monitoring. First, create a new ProgressListener interface, which is used as a tool for progress monitoring callback, as shown below:

public interface ProgressListener {

    void onProgress(int progress); }

 

Then we add the methods of registering download monitoring and unregistering download monitoring in ProgressInterceptor. Modify the code in ProgressInterceptor as follows:

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; } }

 

As you can see, a Map is used here to save the registered listeners, and the key of the Map is a URL address. The reason for this is that you may use Glide to load many images at the same time, and in this case, you must be able to distinguish which image URL address the callback of each download progress corresponds to.

Next comes the most complicated part today, which is the specific calculation of the download progress. We need to create a new ProgressResponseBody class, and let it inherit from OkHttp's ResponseBody, and then write the specific logic to monitor the download progress in this class. The code is as follows:

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; } } }

 

In fact, this code is not difficult, let me explain it briefly. First, we define a ProgressResponseBody constructor, which requires a url parameter and a ResponseBody parameter to be passed in. So obviously, the url parameter is the url address of the image, and the ResponseBody parameter is the original ResponseBody object intercepted by OkHttp. Then in the construction method, we call LISTENER_MAP in ProgressInterceptor to get the listener callback object corresponding to the url. With this object, we can call back the calculated download progress later.

Since the three methods of contentType(), contentLength() and source() must be rewritten after inheriting the ResponseBody class, we directly call contentType() and contentType() of the incoming original ResponseBody in the contentType() and contentLength() methods. The contentLength() method is enough, which is equivalent to a delegate mode. But in the source() method, we must add our own logic, because it involves the specific download progress calculation.

Then let's take a look at the source() method in detail. Here, we first call the source() method of the original ResponseBody to obtain the Source object, then encapsulate the Source object into a ProgressSource object, and finally encapsulate it with Okio's buffer() method. Returns as a BufferedSource object.

So what is this ProgressSource? It is a custom implementation class that inherits from ForwardingSource. ForwardingSource is also a tool that uses the delegate mode. It does not handle any specific logic, but is only responsible for forwarding the incoming original Source object. However, we use ProgressSource to inherit from ForwardingSource, then we can add our own logic in the transfer process.

It can be seen that in ProgressSource we rewrite the read() method, and then obtain the number of bytes read this time and the total number of bytes of the downloaded file in the read() method, and perform some simple mathematical calculations. Can calculate the current download progress. Here I first use the Log tool to print the calculated results, and then call back the results through the callback listener object obtained earlier.

Ok, now that the logic for calculating the download progress is complete, let's use it in the interceptor. Modify the code in ProgressInterceptor as follows:

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; } }

 

Here are some simple usages of OkHttp. We create a new Response object through the newBuilder() method of Response, and replace its body with the ProgressResponseBody just implemented, and finally return the new Response object, so that the logic for calculating the download progress can take effect.

The code is written here, we can run the program. Now whether you are loading any image on the network, you should be able to monitor its download progress.

Modify the code in activity_main.xml as follows:

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

 

Very simple, here we use a Button button to load the image and an ImageView to display the image.

Then modify the code in MainActivity as follows:

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); } }

 

Now you can run the program, the effect is as shown below.

OK, the picture has been loaded. So how to verify whether the download progress of the image has been successfully monitored? Remember the print log we just added to the ProgressResponseBody? Now just go to the logcat to observe it, as shown in the following figure:

It can be seen that the download progress monitoring function has been successfully implemented.

progress display

Although we can now monitor the download progress of the image, this progress can only be displayed in the console print, which is meaningless to the user, so our next step is to find a way to display the download progress. to the interface.

Now modify the code in MainActivity as follows:

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); } }); }

 

The code is not complicated. Here we add a ProgressDialog to display the download progress, and then in the loadImage() method, call the ProgressInterceptor.addListener() method to register a download listener, and update the current download progress in the onProgress() callback method .

Finally, Glide's into() method has also been modified, this time into a GlideDrawableImageViewTarget. We rewrite its onLoadStarted() method and onResourceReady() method to display the progress dialog when the image starts to load, and close the progress dialog when the image is loaded.

Now re-run the program, the effect is as shown below.

Of course, not only static images, but also large GIF images can successfully monitor the download progress. For example, we change the url address of the picture to http://guolin.tech/test.gif , and re-run the program, the effect is shown in the following figure.

Ok, so we have fully implemented the Glide image loading function with progress. Although the interface in this example is relatively rough, and the download progress box is also the most simple to use, but as long as the function is learned, the interface is not a problem, and you can perform various interface optimizations by yourself later.

Finally, if you want to download the full demo, please click here .

A series that has been written for more than half a year is coming to an end, and suddenly there is still a little bit of reluctance. If you have a good grasp of the entire series of seven articles, then calling yourself a Glide master now should not be too much.

In fact, when I first planned to write this series, I was going to write eight articles, but in the end I only wrote seven articles. So in order to fulfill the promise of my original eight articles, I am going to write the last article about the usage of Glide version 4.0, and by the way, let me find an opportunity to study the new version. Of course, this does not mean that the Glide 3.7 version has been eliminated. In fact, the Glide 3.7 version is very stable, and can almost fully meet all the needs of my usual development. It is a version that can be used for a long time.

Interested friends, please continue to read the most complete analysis of the Android image loading framework (8), which will give you a comprehensive understanding of the usage of Glide 4 .

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325260726&siteId=291194637