From c643347d4c80e532017f448be93dd46b58e6fdd1 Mon Sep 17 00:00:00 2001 From: Dylan Vann Date: Wed, 5 Jul 2017 04:10:21 -0400 Subject: [PATCH] Add android progress callback. --- android/build.gradle | 29 +-- android/src/main/AndroidManifest.xml | 14 +- .../fastimage/FastImageViewManager.java | 66 +++---- .../fastimage/OkHttpProgressGlideModule.java | 179 ++++++++++++++++++ .../fastimage/UIProgressListener.java | 14 ++ 5 files changed, 254 insertions(+), 48 deletions(-) create mode 100644 android/src/main/java/com/dylanvann/fastimage/OkHttpProgressGlideModule.java create mode 100644 android/src/main/java/com/dylanvann/fastimage/UIProgressListener.java diff --git a/android/build.gradle b/android/build.gradle index 5449c14..1a99752 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -10,26 +10,27 @@ buildscript { apply plugin: 'com.android.library' android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" + compileSdkVersion 23 + buildToolsVersion "23.0.1" - defaultConfig { - minSdkVersion 16 - targetSdkVersion 22 - versionCode 1 - versionName "1.0" - lintOptions { - abortOnError false + defaultConfig { + minSdkVersion 16 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + lintOptions { + abortOnError false + } } - } } repositories { - mavenCentral() + mavenCentral() } dependencies { - compile 'com.facebook.react:react-native:+' - compile 'com.github.bumptech.glide:glide:3.7.0' - compile 'com.android.support:support-v4:19.1.0' + compile 'com.facebook.react:react-native:+' + compile group: 'com.github.bumptech.glide', name: 'glide', version: '3.8.0' + compile group: 'com.github.bumptech.glide', name: 'okhttp3-integration', version: '1.5.0' + compile 'com.android.support:support-v4:19.1.0' } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index d69105c..178014a 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,5 +1,17 @@ + > + + + + diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java b/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java index 7d7e6e4..91062a9 100644 --- a/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +++ b/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java @@ -7,9 +7,7 @@ import android.widget.ImageView; import com.bumptech.glide.Glide; import com.bumptech.glide.Priority; -import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.model.GlideUrl; -import com.bumptech.glide.load.model.stream.StreamModelLoader; import com.bumptech.glide.load.resource.drawable.GlideDrawable; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.ImageViewTarget; @@ -23,22 +21,26 @@ import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.events.RCTEventEmitter; -import java.io.IOException; -import java.io.InputStream; import java.util.Map; import javax.annotation.Nullable; -class FastImageViewManager extends SimpleViewManager { +class FastImageViewManager extends SimpleViewManager implements UIProgressListener { private static final String REACT_CLASS = "FastImageView"; + private static final String REACT_ON_PROGRESS_EVENT = "onFastImageProgress"; + private static final String REACT_ON_LOAD_EVENT = "onFastImageLoad"; private static final String REACT_ON_ERROR_EVENT = "onFastImageError"; private static Drawable TRANSPARENT_DRAWABLE = new ColorDrawable(Color.TRANSPARENT); + private ImageView imageView; + + private GlideUrl glideUrl; + @Override public String getName() { return REACT_CLASS; @@ -46,7 +48,8 @@ class FastImageViewManager extends SimpleViewManager { @Override protected ImageView createViewInstance(ThemedReactContext reactContext) { - return new ImageView(reactContext); + imageView = new ImageView(reactContext); + return imageView; } private static RequestListener LISTENER = new RequestListener() { @@ -57,6 +60,7 @@ class FastImageViewManager extends SimpleViewManager { Target target, boolean isFirstResource ) { + OkHttpProgressGlideModule.forget(uri.toStringUrl()); if (!(target instanceof ImageViewTarget)) { return false; } @@ -95,13 +99,14 @@ class FastImageViewManager extends SimpleViewManager { if (source == null) { // Cancel existing requests. Glide.clear(view); + OkHttpProgressGlideModule.forget(glideUrl.toStringUrl()); // Clear the image. view.setImageDrawable(null); return; } // Get the GlideUrl which contains header info. - final GlideUrl glideUrl = FastImageViewConverter.glideUrl(source); + glideUrl = FastImageViewConverter.glideUrl(source); // Get priority. final Priority priority = FastImageViewConverter.priority(source); @@ -109,13 +114,16 @@ class FastImageViewManager extends SimpleViewManager { // Cancel existing request. Glide.clear(view); + String key = glideUrl.toStringUrl(); + OkHttpProgressGlideModule.expect(key, this); + Glide .with(view.getContext()) .load(glideUrl) .priority(priority) .placeholder(TRANSPARENT_DRAWABLE) .listener(LISTENER) - .into(view); + .into(imageView); } @ReactProp(name = "resizeMode") @@ -128,6 +136,7 @@ class FastImageViewManager extends SimpleViewManager { public void onDropViewInstance(ImageView view) { // This will cancel existing requests. Glide.clear(view); + OkHttpProgressGlideModule.forget(glideUrl.toString()); super.onDropViewInstance(view); } @@ -135,6 +144,8 @@ class FastImageViewManager extends SimpleViewManager { @Nullable public Map getExportedCustomDirectEventTypeConstants() { return MapBuilder.of( + REACT_ON_PROGRESS_EVENT, + MapBuilder.of("registrationName", REACT_ON_PROGRESS_EVENT), REACT_ON_LOAD_EVENT, MapBuilder.of("registrationName", REACT_ON_LOAD_EVENT), REACT_ON_ERROR_EVENT, @@ -142,31 +153,20 @@ class FastImageViewManager extends SimpleViewManager { ); } - // Used to attempt to load from cache only. - private static final StreamModelLoader cacheOnlyStreamLoader = new StreamModelLoader() { - @Override - public DataFetcher getResourceFetcher(final GlideUrl model, int width, int height) { - return new DataFetcher() { - @Override - public InputStream loadData(Priority priority) throws Exception { - throw new IOException(); - } + @Override + public void onProgress(long bytesRead, long expectedLength) { + WritableMap event = new WritableNativeMap(); + double progress = ((float) bytesRead / (float) expectedLength) * 100; + event.putDouble("progress", progress); + ThemedReactContext context = (ThemedReactContext) imageView.getContext(); + RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); + int viewId = imageView.getId(); + eventEmitter.receiveEvent(viewId, REACT_ON_PROGRESS_EVENT, event); + } - @Override - public void cleanup() { + @Override + public float getGranularityPercentage() { + return 0.5f; + } - } - - @Override - public String getId() { - return model.getCacheKey(); - } - - @Override - public void cancel() { - - } - }; - } - }; } diff --git a/android/src/main/java/com/dylanvann/fastimage/OkHttpProgressGlideModule.java b/android/src/main/java/com/dylanvann/fastimage/OkHttpProgressGlideModule.java new file mode 100644 index 0000000..d3a291b --- /dev/null +++ b/android/src/main/java/com/dylanvann/fastimage/OkHttpProgressGlideModule.java @@ -0,0 +1,179 @@ +package com.dylanvann.fastimage; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.GlideBuilder; +import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.module.GlideModule; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; +import okio.ForwardingSource; +import okio.Okio; +import okio.Source; + +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 + .Builder() + .addInterceptor(createInterceptor(new DispatchingProgressListener())) + .build(); + 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); + final String key = request.url().toString(); + return response + .newBuilder() + .body(new OkHttpProgressResponseBody(key, response.body(), listener)) + .build(); + } + }; + } + + 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(String key, long bytesRead, long contentLength); + } + + private static class DispatchingProgressListener implements ResponseProgressListener { + private static final Map LISTENERS = new HashMap<>(); + private static final Map 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(String key, final long bytesRead, final long contentLength) { + final UIProgressListener listener = LISTENERS.get(key); + if (listener == null) { + return; + } + if (contentLength <= bytesRead) { + forget(key); + } + if (needsDispatch(key, bytesRead, contentLength, listener.getGranularityPercentage())) { + 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 String key; + private final ResponseBody responseBody; + private final ResponseProgressListener progressListener; + private BufferedSource bufferedSource; + + OkHttpProgressResponseBody( + String key, + ResponseBody responseBody, + ResponseProgressListener progressListener + ) { + this.key = key; + this.responseBody = responseBody; + this.progressListener = progressListener; + } + + @Override + public MediaType contentType() { + return responseBody.contentType(); + } + + @Override + public long contentLength() { + return responseBody.contentLength(); + } + + @Override + public BufferedSource source() { + 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(key, totalBytesRead, fullLength); + return bytesRead; + } + }; + } + } +} diff --git a/android/src/main/java/com/dylanvann/fastimage/UIProgressListener.java b/android/src/main/java/com/dylanvann/fastimage/UIProgressListener.java new file mode 100644 index 0000000..25e92fb --- /dev/null +++ b/android/src/main/java/com/dylanvann/fastimage/UIProgressListener.java @@ -0,0 +1,14 @@ +package com.dylanvann.fastimage; + +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 getGranularityPercentage(); + +}