Our Glide series is finally coming to the end. From the moment I started writing the first article in this series, I knew it was going to be a long series, but I just didn’t expect it to take this long.

Over the past six articles, we’ve learned about Glide in every aspect, including basic usage, source code parsing, caching mechanisms, callbacks and listening, image transformations, and custom modules. And today, we are going to make comprehensive use of the knowledge we have learned before, to Glide a relatively large function expansion, I hope that we have read the first six articles, and have a good understanding.

First, set your goals for functionality expansion. Glide is powerful enough on its own, but one feature that hasn’t been supported for a long time is the ability to listen to downloads.

We all know that it’s very easy to use Glide to load an image from the Web, but the frustrating thing is that we have no way of knowing the progress of the current image. If the image is small, that’s not a problem; it will load quickly anyway. But if it’s a large GIF and the user has been waiting for a long time for the image to show up, then you’ll feel that progress is necessary.

Ok, so our goal today is to extend Glide to allow it to monitor the progress of picture downloads.

Today in this article I will take you from zero to create a new project, step by step implementation, and finally complete a Glide picture loading Demo with progress. Of course, I will provide the full source code for the Demo at the end of this article, but I still want you to follow me step by step.

So let’s start by creating a new project called GlideProgressTest.

The first thing to do after the project is created is to introduce the necessary dependency libraries into the current project, and the two libraries we have to rely on so far are Glide and OkHttp. Add the following configuration to your app/build.gradle file:

Dependencies {the compile 'com. Making. Bumptech. Glide: glide: 3.7.0' compile 'com. Squareup. Okhttp3: okhttp: 3.9.0'}Copy the code

In addition, since Glide and OkHttp both require web functionality, we also need to declare web permissions in androidmanifest.xml:

<uses-permission android: />
Copy the code

Well, that completes the preparatory work.

Through the source code analysis of the second article, we know that the bottom layer of THE HTTP communication component of Glide is customized based on HttpUrlConnection. However, HttpUrlConnection’s scalability is limited and we can’t monitor the download progress based on it, so the first big move today is to replace the HTTP communication component in Glide with OkHttp.

As for the replacement principle and method of HTTP communication component, I have introduced it clearly in the sixth article, so I will not repeat it here. So let’s start making quick substitutions.

Create a new class called OkHttpFetcher and implement the DataFetcher interface 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; }}Copy the code

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

Next, create a new MyGlideModule class and implement the GlideModule interface. Then register the OkHttpGlideUrlLoader and OkHttpFetcher we just created into Glide in the registerComponents() method to replace the original HTTP communication component, 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()); }}Copy the code

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

<manifest> 
    ... 
    <application> 
        <meta-data 
            android: 
            android:value="GlideModule" /> 
        ... 
    </application> 
</manifest>
Copy the code

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

So how do we implement the ability to listen for download progress after replacing the HTTP communication component with OkHttp? This is thanks to OkHttp’s powerful interceptor mechanism.

By adding a custom interceptor to OkHttp, we can capture the entire HTTP traffic in the interceptor, and then add some of our own logic to calculate the download progress, which enables us to monitor the download progress.

Interceptors are an advanced feature of OkHttp, but even if you haven’t worked with interceptors before, I’m sure you’ll be able to make sense of this article because it’s not that hard.

Now that we know how to do it, let’s get started. Create an empty ProgressInterceptor class with no logic and implement the Interceptor interface 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; }}Copy the code

In this interceptor we’re basically doing nothing. It intercepts the OkHttp request, calls the proceed() method to process the request, and returns the server’s Response.

Next we need to enable the 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)); }}Copy the code

Here we create an okHttpClient.Builder and call addInterceptor() to add the ProgressInterceptor we just created. Finally to build new OkHttpClient object passed to OkHttpGlideUrlLoader. The Factory.

Ok, now that the custom interceptor is enabled, it’s time to implement the specific logic for downloading progress monitoring. Start by creating a New ProgressListener interface that can be used as a tool for progress listening callbacks, as shown below:

public interface ProgressListener {

    void onProgress(int progress);

}
Copy the code

Then we add methods to register and unregister download listeners to the ProgressInterceptor. Modify the ProgressInterceptor code 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; }}Copy the code

As you can see, a Map is used to hold the registered listeners, and the Map’s key is a URL address. The reason for doing this is that you might be using Glide to load many images at the same time, and in this case it is important to be able to tell which image URL each download progress callback corresponds to.

Next comes the most complicated part of the day, which is the specific calculation of the download progress. We need to create a ProgressResponseBody class that inherits from OkHttp’s ResponseBody and write the logic that listens to the download progress as follows: ProgressResponseBody

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; }}}Copy the code

This code isn’t that hard, so let me explain it briefly. First, we define a ProgressResponseBody constructor that requires passing in a URL parameter and a ResponseBody parameter. So obviously, the URL argument is the url of the image, and the ResponseBody argument is the original ResponseBody object that OkHttp intercepted. Then, in the constructor, we call LISTENER_MAP in the ProgressInterceptor to get the listener callback object corresponding to the URL, which we can call back to the calculated download progress later.

Since you must override contentType(), contentLength(), and source() after inheriting the ResponseBody class, We call the contentType() and contentLength() methods of the original ResponseBody directly in the contentType() and contentLength() methods, which act as a delegate. In the source() method, however, we have to add a bit of logic of our own, because there is a specific download progress calculation involved.

So let’s look at the source() method. We call the source() method of the original ResponseBody to get the source object. We then wrap the source object in a ProgressSource object. Okio’s buffer() method is finally returned 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 pattern, which does not handle any specific logic, but merely takes care of passing in the original Source object. However, we use the ProgressSource to inherit from the ForwardingSource, so we can add our own logic during the transition.

As you can see, in ProgressSource we override the read() method. We then get the number of bytes read and the total number of bytes downloaded in the read() method, and do some simple math to calculate the current download progress. Here I printed the results using the Log tool and called back the results using the previously obtained callback listener object.

Ok, now that the logic to calculate the download progress is complete, let’s use it in the interceptor. Modify the ProgressInterceptor code 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; }}Copy the code

Here are some simple uses of OkHttp as well. We use the newBuilder() method of Response to create a new Response object, replace its body with the ProgressResponseBody we just implemented, and finally return the new Response object. This allows the logic to calculate the download progress to take effect.

So that’s where the code comes in, and we’re ready to run the program. It should now be possible to monitor the download progress of any image loaded on the Internet.

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>
Copy the code

Very simple, here we use a 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); }}Copy the code

Now you can run the program, as shown below.

OK, the image has been loaded. So how to verify the success of monitoring the download progress of the picture? Remember the print log we just added to ProgressResponseBody? Now just go to logcat and look, as shown below:

This shows that the download progress monitoring function has been successfully implemented.

Although we have been able to monitor the download progress of pictures, this progress can only be displayed in the console printing, which is meaningless to users, so our next step is to find a way to display the download progress on 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 (" load "); } 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); }}); }Copy the code

The code is not complicated. Here we added a ProgressDialog is used to show the download progress, then the loadImage () method, called the ProgressInterceptor. The addListener () method to register a listener, And update the current download progress in the onProgress() callback method.

Finally, Glide into () method is also done to modify, this time into to a GlideDrawableImageViewTarget. We rewrote its onLoadStarted() and onResourceReady() methods to display the progress dialog when the image starts loading and to close it when it finishes loading.

Now run the program again. The result should look like the following image.

Of course, not only static images, but also large GIF images can successfully monitor the download progress. For example, if we change the url of the image to guolin.tech/test.gif and rerun the application, the result will look like the following image.

Ok, so we have the Glide picture loading function with the progress of a complete realization. Although the interface in this example is relatively rough, download progress box is also the most simple, but as long as you learn the function, the interface is not a matter, we can later carry out a variety of interface optimization.

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

After writing a series for half a year, I suddenly felt a little reluctant to give up. If you can grasp the whole series of seven articles well, then it is not too much to call yourself Glide master now.

In fact, when I started writing this series, I was going to write eight articles, but I ended up writing only seven. So in order to fulfill my original promise of eight articles, I plan to write about the usage of Glide 4.0 version in the last article, and let me find an opportunity to study the new version. This is not to say that Glide 3.7 is dead, of course. In fact, Glide 3.7 is a very stable version that meets almost all of my usual development needs and is something that can be used for a long time.

If you are interested, please continue to read the most complete version of the Android Picture loading framework (8), which gives you a comprehensive understanding of the use of Glide 4.

Pay attention to my technical public account “Guo Lin”, there are high-quality technical articles pushed every day. Pay attention to my entertainment public number, work, study tired when relax yourself.