Skip to content

Instantly share code, notes, and snippets.

@TWiStErRob
Last active July 1, 2024 09:15
Show Gist options
  • Save TWiStErRob/08d5807e396740e52c90 to your computer and use it in GitHub Desktop.
Save TWiStErRob/08d5807e396740e52c90 to your computer and use it in GitHub Desktop.
Full POC for showing progress of loading in Glide v3 via OkHttp v2

How to read this gist:

  1. Take a look at usage in TestFragment.java that's the usual code for a recycler view and Glide + a custom target for progress
  2. If you want to go deeper start with OkHttpProgressGlideModule.java
  3. The GlideModule provides and interface UIProgressListener this POC uses the example implementation of ProgressTarget.
  4. All the above "glue" makes it so simple to use the progress
  5. The xml files are just examples so that we have a full working "app".

demo GIF

Note: decoding and transforming takes a while in the demo, because there's a hardcoded 1000ms delay, to make it more visible. In real uses it's much faster than this.

// TODO add <meta-data android:value="GlideModule" android:name="....OkHttpProgressGlideModule" />
// TODO add <meta-data android:value="GlideModule" tools:node="remove" android:name="com.bumptech.glide.integration.okhttp.OkHttpGlideModule" />
// or not use 'okhttp@aar' in Gradle depdendencies
public class OkHttpProgressGlideModule implements GlideModule {
@Override public void applyOptions(Context context, GlideBuilder builder) { }
@Override public void registerComponents(Context context, Glide glide) {
OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(createInterceptor(new DispatchingProgressListener()));
glide.register(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(client));
}
private static Interceptor createInterceptor(final ResponseProgressListener listener) {
return new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
return response.newBuilder()
.body(new OkHttpProgressResponseBody(request.httpUrl(), response.body(), listener))
.build();
}
};
}
public interface UIProgressListener {
void onProgress(long bytesRead, long expectedLength);
/**
* Control how often the listener needs an update. 0% and 100% will always be dispatched.
* @return in percentage (0.2 = call {@link #onProgress} around every 0.2 percent of progress)
*/
float getGranualityPercentage();
}
public static void forget(String url) {
DispatchingProgressListener.forget(url);
}
public static void expect(String url, UIProgressListener listener) {
DispatchingProgressListener.expect(url, listener);
}
private interface ResponseProgressListener {
void update(HttpUrl url, long bytesRead, long contentLength);
}
private static class DispatchingProgressListener implements ResponseProgressListener {
private static final Map<String, UIProgressListener> LISTENERS = new HashMap<>();
private static final Map<String, Long> PROGRESSES = new HashMap<>();
private final Handler handler;
DispatchingProgressListener() {
this.handler = new Handler(Looper.getMainLooper());
}
static void forget(String url) {
LISTENERS.remove(url);
PROGRESSES.remove(url);
}
static void expect(String url, UIProgressListener listener) {
LISTENERS.put(url, listener);
}
@Override public void update(HttpUrl url, final long bytesRead, final long contentLength) {
//System.out.printf("%s: %d/%d = %.2f%%%n", url, bytesRead, contentLength, (100f * bytesRead) / contentLength);
String key = url.toString();
final UIProgressListener listener = LISTENERS.get(key);
if (listener == null) {
return;
}
if (contentLength <= bytesRead) {
forget(key);
}
if (needsDispatch(key, bytesRead, contentLength, listener.getGranualityPercentage())) {
handler.post(new Runnable() {
@Override public void run() {
listener.onProgress(bytesRead, contentLength);
}
});
}
}
private boolean needsDispatch(String key, long current, long total, float granularity) {
if (granularity == 0 || current == 0 || total == current) {
return true;
}
float percent = 100f * current / total;
long currentProgress = (long)(percent / granularity);
Long lastProgress = PROGRESSES.get(key);
if (lastProgress == null || currentProgress != lastProgress) {
PROGRESSES.put(key, currentProgress);
return true;
} else {
return false;
}
}
}
private static class OkHttpProgressResponseBody extends ResponseBody {
private final HttpUrl url;
private final ResponseBody responseBody;
private final ResponseProgressListener progressListener;
private BufferedSource bufferedSource;
OkHttpProgressResponseBody(HttpUrl url, ResponseBody responseBody,
ResponseProgressListener progressListener) {
this.url = url;
this.responseBody = responseBody;
this.progressListener = progressListener;
}
@Override public MediaType contentType() {
return responseBody.contentType();
}
@Override public long contentLength() throws IOException {
return responseBody.contentLength();
}
@Override public BufferedSource source() throws IOException {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(source(responseBody.source()));
}
return bufferedSource;
}
private Source source(Source source) {
return new ForwardingSource(source) {
long totalBytesRead = 0L;
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
long fullLength = responseBody.contentLength();
if (bytesRead == -1) { // this source is exhausted
totalBytesRead = fullLength;
} else {
totalBytesRead += bytesRead;
}
progressListener.update(url, totalBytesRead, fullLength);
return bytesRead;
}
};
}
}
}
public abstract class ProgressTarget<T, Z> extends WrappingTarget<Z> implements UIProgressListener {
private T model;
private boolean ignoreProgress = true;
public ProgressTarget(Target<Z> target) {
this(null, target);
}
public ProgressTarget(T model, Target<Z> target) {
super(target);
this.model = model;
}
public final T getModel() {
return model;
}
public final void setModel(T model) {
Glide.clear(this); // indirectly calls cleanup
this.model = model;
}
/**
* Convert a model into an Url string that is used to match up the OkHttp requests. For explicit
* {@link com.bumptech.glide.load.model.GlideUrl GlideUrl} loads this needs to return
* {@link com.bumptech.glide.load.model.GlideUrl#toStringUrl toStringUrl}. For custom models do the same as your
* {@link com.bumptech.glide.load.model.stream.BaseGlideUrlLoader BaseGlideUrlLoader} does.
* @param model return the representation of the given model, DO NOT use {@link #getModel()} inside this method.
* @return a stable Url representation of the model, otherwise the progress reporting won't work
*/
protected String toUrlString(T model) {
return String.valueOf(model);
}
@Override public float getGranualityPercentage() {
return 1.0f;
}
@Override public void onProgress(long bytesRead, long expectedLength) {
if (ignoreProgress) {
return;
}
if (expectedLength == Long.MAX_VALUE) {
onConnecting();
} else if (bytesRead == expectedLength) {
onDownloaded();
} else {
onDownloading(bytesRead, expectedLength);
}
}
/**
* Called when the Glide load has started.
* At this time it is not known if the Glide will even go and use the network to fetch the image.
*/
protected abstract void onConnecting();
/**
* Called when there's any progress on the download; not called when loading from cache.
* At this time we know how many bytes have been transferred through the wire.
*/
protected abstract void onDownloading(long bytesRead, long expectedLength);
/**
* Called when the bytes downloaded reach the length reported by the server; not called when loading from cache.
* At this time it is fairly certain, that Glide either finished reading the stream.
* This means that the image was either already decoded or saved the network stream to cache.
* In the latter case there's more work to do: decode the image from cache and transform.
* These cannot be listened to for progress so it's unsure how fast they'll be, best to show indeterminate progress.
*/
protected abstract void onDownloaded();
/**
* Called when the Glide load has finished either by successfully loading the image or failing to load or cancelled.
* In any case the best is to hide/reset any progress displays.
*/
protected abstract void onDelivered();
private void start() {
OkHttpProgressGlideModule.expect(toUrlString(model), this);
ignoreProgress = false;
onProgress(0, Long.MAX_VALUE);
}
private void cleanup() {
ignoreProgress = true;
T model = this.model; // save in case it gets modified
onDelivered();
OkHttpProgressGlideModule.forget(toUrlString(model));
this.model = null;
}
@Override public void onLoadStarted(Drawable placeholder) {
super.onLoadStarted(placeholder);
start();
}
@Override public void onResourceReady(Z resource, GlideAnimation<? super Z> animation) {
cleanup();
super.onResourceReady(resource, animation);
}
@Override public void onLoadFailed(Exception e, Drawable errorDrawable) {
cleanup();
super.onLoadFailed(e, errorDrawable);
}
@Override public void onLoadCleared(Drawable placeholder) {
cleanup();
super.onLoadCleared(placeholder);
}
}
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:innerRadiusRatio="2.3"
android:shape="ring"
android:thickness="3.8sp"
android:useLevel="true">
<solid android:color="#ff0000" />
</shape>
<!-- Display indeterminate progress at the beginning and end, see setImageLevel calls inside MyProgressTarget -->
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- keep [1, 9999] range first to optimize lookup, see LevelListDrawable.LevelListState#indexOfLevel -->
<item android:drawable="@android:drawable/progress_horizontal"
android:minLevel="1" android:maxLevel="9999" />
<item android:drawable="@android:drawable/progress_indeterminate_horizontal"
android:minLevel="0" android:maxLevel="0" />
<item android:drawable="@android:drawable/progress_indeterminate_horizontal"
android:minLevel="10000" android:maxLevel="10000" />
</level-list>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_margin="4dp"
>
<!-- see interactions with MyProgressTarget.image
scaleType is fitXY because the LevelListDrawable in github_232_progress contains a fixed sized
indeterminate drawable. fitXY stretches everything out so it's screen-wide.
.centerCrop() on the Glide load will load an appropriately resized bitmap, so that won't be stretched. -->
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
tools:src="@drawable/github_232_progress"
tools:ignore="ContentDescription"
/>
<!-- see interactions with MyProgressTarget.text -->
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:padding="4dp"
android:background="#60000000"
android:textColor="#ffffff"
tools:text="progress: ??.? %"
/>
<!-- see interactions with MyProgressTarget.progress -->
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="top|end"
android:max="100"
android:progress="0"
android:progressDrawable="@drawable/github_232_circular"
/>
</FrameLayout>
public class TestFragment extends Fragment {
@Override public @Nullable View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
RecyclerView list = new RecyclerView(container.getContext());
list.setLayoutParams(new MarginLayoutParams(MarginLayoutParams.MATCH_PARENT, MarginLayoutParams.MATCH_PARENT));
list.setLayoutManager(new LinearLayoutManager(container.getContext()));
list.setAdapter(new ProgressAdapter(Arrays.asList(
// few results from https://www.google.com/search?tbm=isch&q=image&tbs=isz:lt,islt:4mp
"http://www.noaanews.noaa.gov/stories/images/goes-12%2Dfirstimage-large081701%2Ejpg",
"http://www.spektyr.com/PrintImages/Cerulean%20Cross%203%20Large.jpg",
"https://cdn.photographylife.com/wp-content/uploads/2014/06/Nikon-D810-Image-Sample-6.jpg",
"https://upload.wikimedia.org/wikipedia/commons/5/5b/Ultraviolet_image_of_the_Cygnus_Loop_Nebula_crop.jpg",
"https://upload.wikimedia.org/wikipedia/commons/c/c5/Polarlicht_2_kmeans_16_large.png",
"https://www.hq.nasa.gov/alsj/a15/M1123519889LCRC_isometric_min-8000_g0dot5_enhanced_labeled.jpg",
"http://oceanexplorer.noaa.gov/explorations/02fire/logs/hirez/octopus_hires.jpg",
"https://upload.wikimedia.org/wikipedia/commons/b/bf/GOES-13_First_Image_jun_22_2006_1730Z.jpg",
"http://www.zastavki.com/pictures/originals/2013/Photoshop_Image_of_the_horse_053857_.jpg",
"http://www.marcogiordanotd.com/blog/wp-content/uploads/2014/01/image9Kcomp.jpg",
"https://cdn.photographylife.com/wp-content/uploads/2014/06/Nikon-D810-Image-Sample-7.jpg",
"https://www.apple.com/v/imac-with-retina/a/images/overview/5k_image.jpg",
"https://www.gimp.org/tutorials/Lite_Quickies/lordofrings_hst_big.jpg",
"http://www.cesbio.ups-tlse.fr/multitemp/wp-content/uploads/2015/07/Mad%C3%A8re-022_0_1.jpg",
"https://www.spacetelescope.org/static/archives/fitsimages/large/slawomir_lipinski_04.jpg",
"https://upload.wikimedia.org/wikipedia/commons/b/b4/Mardin_1350660_1350692_33_images.jpg",
"http://4k.com/wp-content/uploads/2014/06/4k-image-tiger-jumping.jpg"
)));
return list;
}
private static class ProgressViewHolder extends ViewHolder {
private final ImageView image;
private final TextView text;
private final ProgressBar progress;
/** Cache target because all the views are tied to this view holder. */
private final ProgressTarget<String, Bitmap> target;
ProgressViewHolder(View root) {
super(root);
image = (ImageView)root.findViewById(R.id.image);
text = (TextView)root.findViewById(R.id.text);
progress = (ProgressBar)root.findViewById(R.id.progress);
target = new MyProgressTarget<>(new BitmapImageViewTarget(image), progress, image, text);
}
void bind(String url) {
target.setModel(url); // update target's cache
Glide
.with(image.getContext())
.load(url)
.asBitmap()
.placeholder(R.drawable.github_232_progress)
.centerCrop() // needs explicit transformation, because we're using a custom target
.into(target)
;
}
}
/**
* Demonstrates 3 different ways of showing the progress:
* <ul>
* <li>Update a full fledged progress bar</li>
* <li>Update a text view to display size/percentage</li>
* <li>Update the placeholder via Drawable.level</li>
* </ul>
* This last one is tricky: the placeholder that Glide sets can be used as a progress drawable
* without any extra Views in the view hierarchy if it supports levels via <code>usesLevel="true"</code>
* or <code>level-list</code>.
*
* @param <Z> automatically match any real Glide target so it can be used flexibly without reimplementing.
*/
private static class MyProgressTarget<Z> extends ProgressTarget<String, Z> {
private final TextView text;
private final ProgressBar progress;
private final ImageView image;
public MyProgressTarget(Target<Z> target, ProgressBar progress, ImageView image, TextView text) {
super(target);
this.progress = progress;
this.image = image;
this.text = text;
}
@Override public float getGranualityPercentage() {
return 0.1f; // this matches the format string for #text below
}
@Override protected void onConnecting() {
progress.setIndeterminate(true);
progress.setVisibility(View.VISIBLE);
image.setImageLevel(0);
text.setVisibility(View.VISIBLE);
text.setText("connecting");
}
@Override protected void onDownloading(long bytesRead, long expectedLength) {
progress.setIndeterminate(false);
progress.setProgress((int)(100 * bytesRead / expectedLength));
image.setImageLevel((int)(10000 * bytesRead / expectedLength));
text.setText(String.format("downloading %.2f/%.2f MB %.1f%%",
bytesRead / 1e6, expectedLength / 1e6, 100f * bytesRead / expectedLength));
}
@Override protected void onDownloaded() {
progress.setIndeterminate(true);
image.setImageLevel(10000);
text.setText("decoding and transforming");
}
@Override protected void onDelivered() {
progress.setVisibility(View.INVISIBLE);
image.setImageLevel(0); // reset ImageView default
text.setVisibility(View.INVISIBLE);
}
}
private static class ProgressAdapter extends Adapter<ProgressViewHolder> {
private final List<String> models;
public ProgressAdapter(List<String> models) {
this.models = models;
}
@Override public ProgressViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.github_232_item, parent, false);
return new ProgressViewHolder(view);
}
@Override public void onBindViewHolder(ProgressViewHolder holder, int position) {
holder.bind(models.get(position));
}
@Override public int getItemCount() {
return models.size();
}
}
}
public class WrappingTarget<Z> implements Target<Z> {
protected final Target<Z> target;
public WrappingTarget(Target<Z> target) {
this.target = target;
}
@Override public void getSize(SizeReadyCallback cb) {
target.getSize(cb);
}
@Override public void onLoadStarted(Drawable placeholder) {
target.onLoadStarted(placeholder);
}
@Override public void onLoadFailed(Exception e, Drawable errorDrawable) {
target.onLoadFailed(e, errorDrawable);
}
@Override public void onResourceReady(Z resource, GlideAnimation<? super Z> glideAnimation) {
target.onResourceReady(resource, glideAnimation);
}
@Override public void onLoadCleared(Drawable placeholder) {
target.onLoadCleared(placeholder);
}
@Override public Request getRequest() {
return target.getRequest();
}
@Override public void setRequest(Request request) {
target.setRequest(request);
}
@Override public void onStart() {
target.onStart();
}
@Override public void onStop() {
target.onStop();
}
@Override public void onDestroy() {
target.onDestroy();
}
}
@jemshit
Copy link

jemshit commented Oct 4, 2016

Made it work with version 1.5.0-SNAPSHOT without using exclude group: 'glide-parent' , without @aar

@gaara87
Copy link

gaara87 commented Dec 20, 2016

@TWiStErRob thank you for that supportapp module! You should definitely consider having this linked up directly to the glide readme. I found these so very helpful!

@sunyz2016
Copy link

@MasterJohnson@ladddd
I face the same problem. How can you solve it?

@dakun666
Copy link

dakun666 commented May 4, 2017

It‘s not work. onDownloaded and onDownloading can't call...

compile 'com.github.bumptech.glide:glide:3.7.0'
compile 'com.squareup.okhttp3:okhttp:3.7.0'
compile('com.github.bumptech.glide:okhttp3-integration:1.5.0') {
    exclude group: 'glide-parent'
}

@TWiStErRob what should I do to solve it ?

update:
it worked!
because my manifests is wrong,
old❌:

<meta-data android:name="com.bumptech.glide.integration.okhttp3.OkHttpGlideModule" android:value="GlideModule" />

new✅:

<meta-data android:name="com.bumptech.glide.integration.okhttp3.OkHttpGlideModule"
            tools:node="remove" />
<meta-data android:name="my OkHttpProgressGlideModule"
            android:value="GlideModule" />

@ParkerK
Copy link

ParkerK commented Jul 23, 2017

FYI to anyone else porting this to Glide4: in @Override public void registerComponents(Context context, Registry registry) use registry.replace(...) DO NOT use registry.append(...)

Hopefully this will save someone the pointless hours of debugging I did to try to see why it wasn't working :)

@saket
Copy link

saket commented Aug 7, 2017

@TWiStErRob is ParkerK's solution correct? Also, there's a slight delay before the first progress callback is received on Glide v4. Is there anything that needs to be changed in this POC for v4?

@frozenex
Copy link

@TWiStErRob How can we use a thumbnail image (from network) inside this ?. If i add a thumbnail image then the progress is not working

@wonsuc
Copy link

wonsuc commented Jan 17, 2018

Try this gist, if you use FirebaseUI Storage.

@wuwenbo6
Copy link

wuwenbo6 commented Mar 5, 2018

@HungTDO I face the same problem! can not find a way to solve it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment