Add android progress callback.

This commit is contained in:
Dylan Vann 2017-07-05 04:10:21 -04:00
parent ad4d8a5fe6
commit c643347d4c
5 changed files with 254 additions and 48 deletions

View File

@ -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'
}

View File

@ -1,5 +1,17 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.dylanvann.fastimage"
>
>
<application>
<meta-data
android:name="com.dylanvann.fastimage.OkHttpProgressGlideModule"
android:value="GlideModule"
/>
<meta-data
android:name="com.bumptech.glide.integration.okhttp.OkHttpGlideModule"
tools:node="remove"
android:value="GlideModule"
/>
</application>
</manifest>

View File

@ -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<ImageView> {
class FastImageViewManager extends SimpleViewManager<ImageView> 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<ImageView> {
@Override
protected ImageView createViewInstance(ThemedReactContext reactContext) {
return new ImageView(reactContext);
imageView = new ImageView(reactContext);
return imageView;
}
private static RequestListener<GlideUrl, GlideDrawable> LISTENER = new RequestListener<GlideUrl, GlideDrawable>() {
@ -57,6 +60,7 @@ class FastImageViewManager extends SimpleViewManager<ImageView> {
Target<GlideDrawable> target,
boolean isFirstResource
) {
OkHttpProgressGlideModule.forget(uri.toStringUrl());
if (!(target instanceof ImageViewTarget)) {
return false;
}
@ -95,13 +99,14 @@ class FastImageViewManager extends SimpleViewManager<ImageView> {
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<ImageView> {
// 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<ImageView> {
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<ImageView> {
@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<ImageView> {
);
}
// Used to attempt to load from cache only.
private static final StreamModelLoader<GlideUrl> cacheOnlyStreamLoader = new StreamModelLoader<GlideUrl>() {
@Override
public DataFetcher<InputStream> getResourceFetcher(final GlideUrl model, int width, int height) {
return new DataFetcher<InputStream>() {
@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() {
}
};
}
};
}

View File

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

View File

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