diff --git a/FastImage.js b/FastImage.js index d6de8e5..f669fcd 100644 --- a/FastImage.js +++ b/FastImage.js @@ -1,5 +1,5 @@ import React, { Component } from 'react' -import PropTypes from 'prop-types'; +import PropTypes from 'prop-types' import { Image, NativeModules, @@ -16,7 +16,7 @@ class FastImage extends Component { this._root.setNativeProps(nativeProps) } - render () { + render() { const { source, onLoadStart, @@ -24,7 +24,7 @@ class FastImage extends Component { onLoad, onError, onLoadEnd, - ...props, + ...props } = this.props // If there's no source or source uri just fallback to Image. diff --git a/README.md b/README.md index 278a645..5c3df80 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,20 @@ Headers to load the image with. e.g. `{ Authorization: 'someAuthToken' }`. --- +### `onLoadStart?: () => void` + +Called when the image starts to load. + +--- + +### `onProgress?: (event) => void` + +Called when the image is loading. + +e.g. `onProgress={e => console.log(e.nativeEvent.loaded / e.nativeEvent.total)}` + +--- + ### `onLoad?: () => void` Called on a successful image fetch. @@ -111,6 +125,12 @@ Called on an image fetching error. --- +### `onLoadEnd?: () => void` + +Called when the image finishes loading, whether it was successful or an error. + +--- + ### `children` `FastImage` does not currently support children. diff --git a/__snapshots__/FastImage.test.js.snap b/__snapshots__/FastImage.test.js.snap index cb172c7..0e91b52 100644 --- a/__snapshots__/FastImage.test.js.snap +++ b/__snapshots__/FastImage.test.js.snap @@ -2,9 +2,11 @@ exports[`FastImage renders correctly. 1`] = ` implements UIProgressListener { +class ImageViewWithUrl extends ImageView { + public GlideUrl glideUrl; + + public ImageViewWithUrl(Context context) { + super(context); + } +} + +class FastImageViewManager extends SimpleViewManager implements ProgressListener { private static final String REACT_CLASS = "FastImageView"; - + private static final String REACT_ON_LOAD_START_EVENT = "onFastImageLoadStart"; 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; + private static final String REACT_ON_LOAD_EVENT = "onFastImageLoad"; + private static final String REACT_ON_LOAD_END_EVENT = "onFastImageLoadEnd"; + private static final Drawable TRANSPARENT_DRAWABLE = new ColorDrawable(Color.TRANSPARENT); + private static final Map> VIEWS_FOR_URLS = new HashMap<>(); @Override public String getName() { @@ -47,9 +54,8 @@ class FastImageViewManager extends SimpleViewManager implements UIPro } @Override - protected ImageView createViewInstance(ThemedReactContext reactContext) { - imageView = new ImageView(reactContext); - return imageView; + protected ImageViewWithUrl createViewInstance(ThemedReactContext reactContext) { + return new ImageViewWithUrl(reactContext); } private static RequestListener LISTENER = new RequestListener() { @@ -64,12 +70,12 @@ class FastImageViewManager extends SimpleViewManager implements UIPro if (!(target instanceof ImageViewTarget)) { return false; } - ImageView view = (ImageView) ((ImageViewTarget) target).getView(); - WritableMap event = new WritableNativeMap(); + ImageViewWithUrl view = (ImageViewWithUrl) ((ImageViewTarget) target).getView(); ThemedReactContext context = (ThemedReactContext) view.getContext(); RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); int viewId = view.getId(); - eventEmitter.receiveEvent(viewId, REACT_ON_ERROR_EVENT, event); + eventEmitter.receiveEvent(viewId, REACT_ON_ERROR_EVENT, new WritableNativeMap()); + eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_END_EVENT, new WritableNativeMap()); return false; } @@ -84,29 +90,32 @@ class FastImageViewManager extends SimpleViewManager implements UIPro if (!(target instanceof ImageViewTarget)) { return false; } - ImageView view = (ImageView) ((ImageViewTarget) target).getView(); - WritableMap event = new WritableNativeMap(); + ImageViewWithUrl view = (ImageViewWithUrl) ((ImageViewTarget) target).getView(); ThemedReactContext context = (ThemedReactContext) view.getContext(); RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); int viewId = view.getId(); - eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_EVENT, event); + eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_EVENT, new WritableNativeMap()); + eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_END_EVENT, new WritableNativeMap()); return false; } }; @ReactProp(name = "source") - public void setSrc(ImageView view, @Nullable ReadableMap source) { + public void setSrc(ImageViewWithUrl view, @Nullable ReadableMap source) { if (source == null) { // Cancel existing requests. Glide.clear(view); - OkHttpProgressGlideModule.forget(glideUrl.toStringUrl()); + if (view.glideUrl != null) { + OkHttpProgressGlideModule.forget(view.glideUrl.toStringUrl()); + } // Clear the image. view.setImageDrawable(null); return; } // Get the GlideUrl which contains header info. - glideUrl = FastImageViewConverter.glideUrl(source); + GlideUrl glideUrl = FastImageViewConverter.glideUrl(source); + view.glideUrl = glideUrl; // Get priority. final Priority priority = FastImageViewConverter.priority(source); @@ -116,6 +125,17 @@ class FastImageViewManager extends SimpleViewManager implements UIPro String key = glideUrl.toStringUrl(); OkHttpProgressGlideModule.expect(key, this); + List viewsForKey = VIEWS_FOR_URLS.get(key); + if (viewsForKey != null && !viewsForKey.contains(view)) { + viewsForKey.add(view); + } else if (viewsForKey == null) { + VIEWS_FOR_URLS.put(key, Collections.singletonList(view)); + } + + ThemedReactContext context = (ThemedReactContext) view.getContext(); + RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); + int viewId = view.getId(); + eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_START_EVENT, new WritableNativeMap()); Glide .with(view.getContext()) @@ -123,20 +143,26 @@ class FastImageViewManager extends SimpleViewManager implements UIPro .priority(priority) .placeholder(TRANSPARENT_DRAWABLE) .listener(LISTENER) - .into(imageView); + .into(view); } @ReactProp(name = "resizeMode") - public void setResizeMode(ImageView view, String resizeMode) { - final ImageView.ScaleType scaleType = FastImageViewConverter.scaleType(resizeMode); + public void setResizeMode(ImageViewWithUrl view, String resizeMode) { + final ImageViewWithUrl.ScaleType scaleType = FastImageViewConverter.scaleType(resizeMode); view.setScaleType(scaleType); } @Override - public void onDropViewInstance(ImageView view) { + public void onDropViewInstance(ImageViewWithUrl view) { // This will cancel existing requests. Glide.clear(view); - OkHttpProgressGlideModule.forget(glideUrl.toString()); + final String key = view.glideUrl.toString(); + OkHttpProgressGlideModule.forget(key); + List viewsForKey = VIEWS_FOR_URLS.get(key); + if (viewsForKey != null) { + viewsForKey.remove(view); + if (viewsForKey.size() == 0) VIEWS_FOR_URLS.remove(key); + } super.onDropViewInstance(view); } @@ -144,24 +170,33 @@ class FastImageViewManager extends SimpleViewManager implements UIPro @Nullable public Map getExportedCustomDirectEventTypeConstants() { return MapBuilder.of( + REACT_ON_LOAD_START_EVENT, + MapBuilder.of("registrationName", REACT_ON_LOAD_START_EVENT), 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) + MapBuilder.of("registrationName", REACT_ON_ERROR_EVENT), + REACT_ON_LOAD_END_EVENT, + MapBuilder.of("registrationName", REACT_ON_LOAD_END_EVENT) ); } @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); + public void onProgress(String key, long bytesRead, long expectedLength) { + List viewsForKey = VIEWS_FOR_URLS.get(key); + if (viewsForKey != null) { + for (ImageViewWithUrl view: viewsForKey) { + WritableMap event = new WritableNativeMap(); + event.putInt("loaded", (int) bytesRead); + event.putInt("total", (int) expectedLength); + ThemedReactContext context = (ThemedReactContext) view.getContext(); + RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); + int viewId = view.getId(); + eventEmitter.receiveEvent(viewId, REACT_ON_PROGRESS_EVENT, event); + } + } } @Override diff --git a/android/src/main/java/com/dylanvann/fastimage/OkHttpProgressGlideModule.java b/android/src/main/java/com/dylanvann/fastimage/OkHttpProgressGlideModule.java index d3a291b..87ec45c 100644 --- a/android/src/main/java/com/dylanvann/fastimage/OkHttpProgressGlideModule.java +++ b/android/src/main/java/com/dylanvann/fastimage/OkHttpProgressGlideModule.java @@ -56,12 +56,12 @@ public class OkHttpProgressGlideModule implements GlideModule { }; } - public static void forget(String url) { - DispatchingProgressListener.forget(url); + public static void forget(String key) { + DispatchingProgressListener.forget(key); } - public static void expect(String url, UIProgressListener listener) { - DispatchingProgressListener.expect(url, listener); + public static void expect(String key, ProgressListener listener) { + DispatchingProgressListener.expect(key, listener); } private interface ResponseProgressListener { @@ -69,7 +69,7 @@ public class OkHttpProgressGlideModule implements GlideModule { } private static class DispatchingProgressListener implements ResponseProgressListener { - private static final Map LISTENERS = new HashMap<>(); + private static final Map LISTENERS = new HashMap<>(); private static final Map PROGRESSES = new HashMap<>(); private final Handler handler; @@ -78,18 +78,18 @@ public class OkHttpProgressGlideModule implements GlideModule { this.handler = new Handler(Looper.getMainLooper()); } - static void forget(String url) { - LISTENERS.remove(url); - PROGRESSES.remove(url); + static void forget(String key) { + LISTENERS.remove(key); + PROGRESSES.remove(key); } - static void expect(String url, UIProgressListener listener) { - LISTENERS.put(url, listener); + static void expect(String key, ProgressListener listener) { + LISTENERS.put(key, listener); } @Override - public void update(String key, final long bytesRead, final long contentLength) { - final UIProgressListener listener = LISTENERS.get(key); + public void update(final String key, final long bytesRead, final long contentLength) { + final ProgressListener listener = LISTENERS.get(key); if (listener == null) { return; } @@ -100,7 +100,7 @@ public class OkHttpProgressGlideModule implements GlideModule { handler.post(new Runnable() { @Override public void run() { - listener.onProgress(bytesRead, contentLength); + listener.onProgress(key, bytesRead, contentLength); } }); } diff --git a/example/FastImageExample.js b/example/FastImageExample.js index a885e98..8a09932 100644 --- a/example/FastImageExample.js +++ b/example/FastImageExample.js @@ -111,7 +111,8 @@ class FastImageExample extends Component { priority: FastImage.priority.high, }} onLoadStart={() => console.log('onLoadStart')} - onProgress={e => console.log(e.nativeEvent.progress)} + onProgress={e => + console.log(e.nativeEvent.loaded / e.nativeEvent.total)} onLoad={() => console.log('onLoad')} onError={() => console.log('onError')} onLoadEnd={() => console.log('onLoadEnd')} diff --git a/ios/FastImage/FFFastImageView.m b/ios/FastImage/FFFastImageView.m index 587e20d..7dd545f 100644 --- a/ios/FastImage/FFFastImageView.m +++ b/ios/FastImage/FFFastImageView.m @@ -61,9 +61,11 @@ progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) { double progress = MIN(1, MAX(0, (double) receivedSize / (double) expectedSize)); if (_onFastImageProgress) { - _onFastImageProgress(@{ @"progress": @(progress) }); + _onFastImageProgress(@{ + @"loaded": @(receivedSize), + @"total": @(expectedSize) + }); } - } completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType,