Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save Rainer-Lang/219e39b1324d6b731c9bd52692ebb8a5 to your computer and use it in GitHub Desktop.

Select an option

Save Rainer-Lang/219e39b1324d6b731c9bd52692ebb8a5 to your computer and use it in GitHub Desktop.

Revisions

  1. @TWiStErRob TWiStErRob revised this gist Jan 22, 2016. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion OkHttpProgressGlideModule.java
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,6 @@
    // TODO <meta-data android:name="....OkHttpProgressGlideModule" android:value="GlideModule" />
    // 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) {
  2. @TWiStErRob TWiStErRob revised this gist Jan 18, 2016. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -8,4 +8,5 @@ How to read this gist:
    5. The xml files are just examples so that we have a full working "app".

    ![demo GIF](https://cloud.githubusercontent.com/assets/2906988/12400279/5f5fe0d2-be1f-11e5-9769-8623e1d81560.gif)
    Note: decoding and transforming takes a while in the demo, because there's a hardcoded 1000mns delay, to make it more visible. In real uses it's much faster than this.

    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.
  3. @TWiStErRob TWiStErRob revised this gist Jan 18, 2016. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -7,4 +7,5 @@ How to read this gist:
    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](https://cloud.githubusercontent.com/assets/2906988/12400279/5f5fe0d2-be1f-11e5-9769-8623e1d81560.gif)
    ![demo GIF](https://cloud.githubusercontent.com/assets/2906988/12400279/5f5fe0d2-be1f-11e5-9769-8623e1d81560.gif)
    Note: decoding and transforming takes a while in the demo, because there's a hardcoded 1000mns delay, to make it more visible. In real uses it's much faster than this.
  4. @TWiStErRob TWiStErRob revised this gist Jan 18, 2016. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -6,3 +6,5 @@ How to read this gist:
    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](https://cloud.githubusercontent.com/assets/2906988/12400279/5f5fe0d2-be1f-11e5-9769-8623e1d81560.gif)
  5. @TWiStErRob TWiStErRob revised this gist Jan 18, 2016. 1 changed file with 8 additions and 0 deletions.
    8 changes: 8 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    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".
  6. @TWiStErRob TWiStErRob created this gist Jan 18, 2016.
    139 changes: 139 additions & 0 deletions OkHttpProgressGlideModule.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,139 @@
    // TODO <meta-data android:name="....OkHttpProgressGlideModule" android:value="GlideModule" />
    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;
    }
    };
    }
    }
    }
    101 changes: 101 additions & 0 deletions ProgressTarget.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,101 @@
    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);
    }
    }
    125 changes: 125 additions & 0 deletions TestFragment.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,125 @@
    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();
    }
    }
    }
    40 changes: 40 additions & 0 deletions WrappingTarget.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,40 @@
    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();
    }
    }
    7 changes: 7 additions & 0 deletions res-drawable-github_232_circular.xml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,7 @@
    <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>
    10 changes: 10 additions & 0 deletions res-drawable-github_232_progress.xml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,10 @@
    <!-- 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>
    42 changes: 42 additions & 0 deletions res-layout-github_232_item.xml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,42 @@
    <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>