Make android and iOS view loading events conform to API of React Native's Image.

This will make using this library as a drop in replacement easier.
This commit also fixes some issues in the android progress implementation.
This commit is contained in:
Dylan Vann 2017-07-25 00:38:34 -04:00
parent 6c720f5020
commit 1666f7c4e5
7 changed files with 119 additions and 59 deletions

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types'; import PropTypes from 'prop-types'
import { import {
Image, Image,
NativeModules, NativeModules,
@ -16,7 +16,7 @@ class FastImage extends Component {
this._root.setNativeProps(nativeProps) this._root.setNativeProps(nativeProps)
} }
render () { render() {
const { const {
source, source,
onLoadStart, onLoadStart,
@ -24,7 +24,7 @@ class FastImage extends Component {
onLoad, onLoad,
onError, onError,
onLoadEnd, onLoadEnd,
...props, ...props
} = this.props } = this.props
// If there's no source or source uri just fallback to Image. // If there's no source or source uri just fallback to Image.

View File

@ -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` ### `onLoad?: () => void`
Called on a successful image fetch. 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` ### `children`
`FastImage` does not currently support children. `FastImage` does not currently support children.

View File

@ -2,9 +2,11 @@
exports[`FastImage renders correctly. 1`] = ` exports[`FastImage renders correctly. 1`] = `
<FastImageView <FastImageView
onFastImageError={[Function]} onFastImageError={undefined}
onFastImageLoad={[Function]} onFastImageLoad={undefined}
onFastImageProgress={[Function]} onFastImageLoadEnd={undefined}
onFastImageLoadStart={undefined}
onFastImageProgress={undefined}
resizeMode="cover" resizeMode="cover"
source={ source={
Object { Object {

View File

@ -1,5 +1,6 @@
package com.dylanvann.fastimage; package com.dylanvann.fastimage;
import android.content.Context;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
@ -21,25 +22,31 @@ import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.events.RCTEventEmitter; import com.facebook.react.uimanager.events.RCTEventEmitter;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import javax.annotation.Nullable; import javax.annotation.Nullable;
class FastImageViewManager extends SimpleViewManager<ImageView> implements UIProgressListener { class ImageViewWithUrl extends ImageView {
public GlideUrl glideUrl;
public ImageViewWithUrl(Context context) {
super(context);
}
}
class FastImageViewManager extends SimpleViewManager<ImageViewWithUrl> implements ProgressListener {
private static final String REACT_CLASS = "FastImageView"; 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_PROGRESS_EVENT = "onFastImageProgress";
private static final String REACT_ON_LOAD_EVENT = "onFastImageLoad";
private static final String REACT_ON_ERROR_EVENT = "onFastImageError"; private static final String REACT_ON_ERROR_EVENT = "onFastImageError";
private static final String REACT_ON_LOAD_EVENT = "onFastImageLoad";
private static Drawable TRANSPARENT_DRAWABLE = new ColorDrawable(Color.TRANSPARENT); private static final String REACT_ON_LOAD_END_EVENT = "onFastImageLoadEnd";
private static final Drawable TRANSPARENT_DRAWABLE = new ColorDrawable(Color.TRANSPARENT);
private ImageView imageView; private static final Map<String, List<ImageViewWithUrl>> VIEWS_FOR_URLS = new HashMap<>();
private GlideUrl glideUrl;
@Override @Override
public String getName() { public String getName() {
@ -47,9 +54,8 @@ class FastImageViewManager extends SimpleViewManager<ImageView> implements UIPro
} }
@Override @Override
protected ImageView createViewInstance(ThemedReactContext reactContext) { protected ImageViewWithUrl createViewInstance(ThemedReactContext reactContext) {
imageView = new ImageView(reactContext); return new ImageViewWithUrl(reactContext);
return imageView;
} }
private static RequestListener<GlideUrl, GlideDrawable> LISTENER = new RequestListener<GlideUrl, GlideDrawable>() { private static RequestListener<GlideUrl, GlideDrawable> LISTENER = new RequestListener<GlideUrl, GlideDrawable>() {
@ -64,12 +70,12 @@ class FastImageViewManager extends SimpleViewManager<ImageView> implements UIPro
if (!(target instanceof ImageViewTarget)) { if (!(target instanceof ImageViewTarget)) {
return false; return false;
} }
ImageView view = (ImageView) ((ImageViewTarget) target).getView(); ImageViewWithUrl view = (ImageViewWithUrl) ((ImageViewTarget) target).getView();
WritableMap event = new WritableNativeMap();
ThemedReactContext context = (ThemedReactContext) view.getContext(); ThemedReactContext context = (ThemedReactContext) view.getContext();
RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class);
int viewId = view.getId(); 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; return false;
} }
@ -84,29 +90,32 @@ class FastImageViewManager extends SimpleViewManager<ImageView> implements UIPro
if (!(target instanceof ImageViewTarget)) { if (!(target instanceof ImageViewTarget)) {
return false; return false;
} }
ImageView view = (ImageView) ((ImageViewTarget) target).getView(); ImageViewWithUrl view = (ImageViewWithUrl) ((ImageViewTarget) target).getView();
WritableMap event = new WritableNativeMap();
ThemedReactContext context = (ThemedReactContext) view.getContext(); ThemedReactContext context = (ThemedReactContext) view.getContext();
RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class);
int viewId = view.getId(); 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; return false;
} }
}; };
@ReactProp(name = "source") @ReactProp(name = "source")
public void setSrc(ImageView view, @Nullable ReadableMap source) { public void setSrc(ImageViewWithUrl view, @Nullable ReadableMap source) {
if (source == null) { if (source == null) {
// Cancel existing requests. // Cancel existing requests.
Glide.clear(view); Glide.clear(view);
OkHttpProgressGlideModule.forget(glideUrl.toStringUrl()); if (view.glideUrl != null) {
OkHttpProgressGlideModule.forget(view.glideUrl.toStringUrl());
}
// Clear the image. // Clear the image.
view.setImageDrawable(null); view.setImageDrawable(null);
return; return;
} }
// Get the GlideUrl which contains header info. // Get the GlideUrl which contains header info.
glideUrl = FastImageViewConverter.glideUrl(source); GlideUrl glideUrl = FastImageViewConverter.glideUrl(source);
view.glideUrl = glideUrl;
// Get priority. // Get priority.
final Priority priority = FastImageViewConverter.priority(source); final Priority priority = FastImageViewConverter.priority(source);
@ -116,6 +125,17 @@ class FastImageViewManager extends SimpleViewManager<ImageView> implements UIPro
String key = glideUrl.toStringUrl(); String key = glideUrl.toStringUrl();
OkHttpProgressGlideModule.expect(key, this); OkHttpProgressGlideModule.expect(key, this);
List<ImageViewWithUrl> 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 Glide
.with(view.getContext()) .with(view.getContext())
@ -123,20 +143,26 @@ class FastImageViewManager extends SimpleViewManager<ImageView> implements UIPro
.priority(priority) .priority(priority)
.placeholder(TRANSPARENT_DRAWABLE) .placeholder(TRANSPARENT_DRAWABLE)
.listener(LISTENER) .listener(LISTENER)
.into(imageView); .into(view);
} }
@ReactProp(name = "resizeMode") @ReactProp(name = "resizeMode")
public void setResizeMode(ImageView view, String resizeMode) { public void setResizeMode(ImageViewWithUrl view, String resizeMode) {
final ImageView.ScaleType scaleType = FastImageViewConverter.scaleType(resizeMode); final ImageViewWithUrl.ScaleType scaleType = FastImageViewConverter.scaleType(resizeMode);
view.setScaleType(scaleType); view.setScaleType(scaleType);
} }
@Override @Override
public void onDropViewInstance(ImageView view) { public void onDropViewInstance(ImageViewWithUrl view) {
// This will cancel existing requests. // This will cancel existing requests.
Glide.clear(view); Glide.clear(view);
OkHttpProgressGlideModule.forget(glideUrl.toString()); final String key = view.glideUrl.toString();
OkHttpProgressGlideModule.forget(key);
List<ImageViewWithUrl> viewsForKey = VIEWS_FOR_URLS.get(key);
if (viewsForKey != null) {
viewsForKey.remove(view);
if (viewsForKey.size() == 0) VIEWS_FOR_URLS.remove(key);
}
super.onDropViewInstance(view); super.onDropViewInstance(view);
} }
@ -144,24 +170,33 @@ class FastImageViewManager extends SimpleViewManager<ImageView> implements UIPro
@Nullable @Nullable
public Map getExportedCustomDirectEventTypeConstants() { public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of( return MapBuilder.of(
REACT_ON_LOAD_START_EVENT,
MapBuilder.of("registrationName", REACT_ON_LOAD_START_EVENT),
REACT_ON_PROGRESS_EVENT, REACT_ON_PROGRESS_EVENT,
MapBuilder.of("registrationName", REACT_ON_PROGRESS_EVENT), MapBuilder.of("registrationName", REACT_ON_PROGRESS_EVENT),
REACT_ON_LOAD_EVENT, REACT_ON_LOAD_EVENT,
MapBuilder.of("registrationName", REACT_ON_LOAD_EVENT), MapBuilder.of("registrationName", REACT_ON_LOAD_EVENT),
REACT_ON_ERROR_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 @Override
public void onProgress(long bytesRead, long expectedLength) { public void onProgress(String key, long bytesRead, long expectedLength) {
WritableMap event = new WritableNativeMap(); List<ImageViewWithUrl> viewsForKey = VIEWS_FOR_URLS.get(key);
double progress = ((float) bytesRead / (float) expectedLength) * 100; if (viewsForKey != null) {
event.putDouble("progress", progress); for (ImageViewWithUrl view: viewsForKey) {
ThemedReactContext context = (ThemedReactContext) imageView.getContext(); WritableMap event = new WritableNativeMap();
RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class); event.putInt("loaded", (int) bytesRead);
int viewId = imageView.getId(); event.putInt("total", (int) expectedLength);
eventEmitter.receiveEvent(viewId, REACT_ON_PROGRESS_EVENT, event); ThemedReactContext context = (ThemedReactContext) view.getContext();
RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class);
int viewId = view.getId();
eventEmitter.receiveEvent(viewId, REACT_ON_PROGRESS_EVENT, event);
}
}
} }
@Override @Override

View File

@ -56,12 +56,12 @@ public class OkHttpProgressGlideModule implements GlideModule {
}; };
} }
public static void forget(String url) { public static void forget(String key) {
DispatchingProgressListener.forget(url); DispatchingProgressListener.forget(key);
} }
public static void expect(String url, UIProgressListener listener) { public static void expect(String key, ProgressListener listener) {
DispatchingProgressListener.expect(url, listener); DispatchingProgressListener.expect(key, listener);
} }
private interface ResponseProgressListener { private interface ResponseProgressListener {
@ -69,7 +69,7 @@ public class OkHttpProgressGlideModule implements GlideModule {
} }
private static class DispatchingProgressListener implements ResponseProgressListener { private static class DispatchingProgressListener implements ResponseProgressListener {
private static final Map<String, UIProgressListener> LISTENERS = new HashMap<>(); private static final Map<String, ProgressListener> LISTENERS = new HashMap<>();
private static final Map<String, Long> PROGRESSES = new HashMap<>(); private static final Map<String, Long> PROGRESSES = new HashMap<>();
private final Handler handler; private final Handler handler;
@ -78,18 +78,18 @@ public class OkHttpProgressGlideModule implements GlideModule {
this.handler = new Handler(Looper.getMainLooper()); this.handler = new Handler(Looper.getMainLooper());
} }
static void forget(String url) { static void forget(String key) {
LISTENERS.remove(url); LISTENERS.remove(key);
PROGRESSES.remove(url); PROGRESSES.remove(key);
} }
static void expect(String url, UIProgressListener listener) { static void expect(String key, ProgressListener listener) {
LISTENERS.put(url, listener); LISTENERS.put(key, listener);
} }
@Override @Override
public void update(String key, final long bytesRead, final long contentLength) { public void update(final String key, final long bytesRead, final long contentLength) {
final UIProgressListener listener = LISTENERS.get(key); final ProgressListener listener = LISTENERS.get(key);
if (listener == null) { if (listener == null) {
return; return;
} }
@ -100,7 +100,7 @@ public class OkHttpProgressGlideModule implements GlideModule {
handler.post(new Runnable() { handler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
listener.onProgress(bytesRead, contentLength); listener.onProgress(key, bytesRead, contentLength);
} }
}); });
} }

View File

@ -111,7 +111,8 @@ class FastImageExample extends Component {
priority: FastImage.priority.high, priority: FastImage.priority.high,
}} }}
onLoadStart={() => console.log('onLoadStart')} 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')} onLoad={() => console.log('onLoad')}
onError={() => console.log('onError')} onError={() => console.log('onError')}
onLoadEnd={() => console.log('onLoadEnd')} onLoadEnd={() => console.log('onLoadEnd')}

View File

@ -61,9 +61,11 @@
progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) { progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
double progress = MIN(1, MAX(0, (double) receivedSize / (double) expectedSize)); double progress = MIN(1, MAX(0, (double) receivedSize / (double) expectedSize));
if (_onFastImageProgress) { if (_onFastImageProgress) {
_onFastImageProgress(@{ @"progress": @(progress) }); _onFastImageProgress(@{
@"loaded": @(receivedSize),
@"total": @(expectedSize)
});
} }
} completed:^(UIImage * _Nullable image, } completed:^(UIImage * _Nullable image,
NSError * _Nullable error, NSError * _Nullable error,
SDImageCacheType cacheType, SDImageCacheType cacheType,