Skip to content

Commit

Permalink
Add android progress callback.
Browse files Browse the repository at this point in the history
  • Loading branch information
DylanVann committed Jul 5, 2017
1 parent ad4d8a5 commit c643347
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 48 deletions.
29 changes: 15 additions & 14 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
14 changes: 13 additions & 1 deletion android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@

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;
Expand All @@ -23,30 +21,35 @@
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;
}

@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>() {
Expand All @@ -57,6 +60,7 @@ public boolean onException(
Target<GlideDrawable> target,
boolean isFirstResource
) {
OkHttpProgressGlideModule.forget(uri.toStringUrl());
if (!(target instanceof ImageViewTarget)) {
return false;
}
Expand Down Expand Up @@ -95,27 +99,31 @@ public void setSrc(ImageView view, @Nullable ReadableMap source) {
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);

// 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")
Expand All @@ -128,45 +136,37 @@ public void setResizeMode(ImageView view, String resizeMode) {
public void onDropViewInstance(ImageView view) {
// This will cancel existing requests.
Glide.clear(view);
OkHttpProgressGlideModule.forget(glideUrl.toString());
super.onDropViewInstance(view);
}

@Override
@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,
MapBuilder.of("registrationName", REACT_ON_ERROR_EVENT)
);
}

// 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 cleanup() {

}

@Override
public String getId() {
return model.getCacheKey();
}
@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 cancel() {
@Override
public float getGranularityPercentage() {
return 0.5f;
}

}
};
}
};
}
Original file line number Diff line number Diff line change
@@ -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;
}
};
}
}
}
Loading

0 comments on commit c643347

Please sign in to comment.