diff --git a/RNTester/js/examples/Image/ImageExample.js b/RNTester/js/examples/Image/ImageExample.js index 32b9c58db99..95a3e70e24d 100644 --- a/RNTester/js/examples/Image/ImageExample.js +++ b/RNTester/js/examples/Image/ImageExample.js @@ -36,7 +36,7 @@ type ImageSource = $ReadOnly<{| type NetworkImageCallbackExampleState = {| events: Array, startLoadPrefetched: boolean, - mountTime: Date, + mountTime: number, |}; type NetworkImageCallbackExampleProps = $ReadOnly<{| @@ -51,11 +51,11 @@ class NetworkImageCallbackExample extends React.Component< state = { events: [], startLoadPrefetched: false, - mountTime: new Date(), + mountTime: Date.now(), }; UNSAFE_componentWillMount() { - this.setState({mountTime: new Date()}); + this.setState({mountTime: Date.now()}); } _loadEventFired = (event: string) => { @@ -72,43 +72,50 @@ class NetworkImageCallbackExample extends React.Component< source={this.props.source} style={[styles.base, {overflow: 'visible'}]} onLoadStart={() => - this._loadEventFired(`✔ onLoadStart (+${new Date() - mountTime}ms)`) + this._loadEventFired(`✔ onLoadStart (+${Date.now() - mountTime}ms)`) } + onProgress={event => { + const {loaded, total} = event.nativeEvent; + const percent = Math.round((loaded / total) * 100); + this._loadEventFired( + `✔ onProgress ${percent}% (+${Date.now() - mountTime}ms)`, + ); + }} onLoad={event => { if (event.nativeEvent.source) { const url = event.nativeEvent.source.uri; this._loadEventFired( - `✔ onLoad (+${new Date() - mountTime}ms) for URL ${url}`, + `✔ onLoad (+${Date.now() - mountTime}ms) for URL ${url}`, ); } else { - this._loadEventFired(`✔ onLoad (+${new Date() - mountTime}ms)`); + this._loadEventFired(`✔ onLoad (+${Date.now() - mountTime}ms)`); } }} onLoadEnd={() => { - this._loadEventFired(`✔ onLoadEnd (+${new Date() - mountTime}ms)`); + this._loadEventFired(`✔ onLoadEnd (+${Date.now() - mountTime}ms)`); this.setState({startLoadPrefetched: true}, () => { prefetchTask.then( () => { this._loadEventFired( - `✔ Prefetch OK (+${new Date() - mountTime}ms)`, + `✔ Prefetch OK (+${Date.now() - mountTime}ms)`, ); Image.queryCache([IMAGE_PREFETCH_URL]).then(map => { const result = map[IMAGE_PREFETCH_URL]; if (result) { this._loadEventFired( - `✔ queryCache "${result}" (+${new Date() - + `✔ queryCache "${result}" (+${Date.now() - mountTime}ms)`, ); } else { this._loadEventFired( - `✘ queryCache (+${new Date() - mountTime}ms)`, + `✘ queryCache (+${Date.now() - mountTime}ms)`, ); } }); }, error => { this._loadEventFired( - `✘ Prefetch failed (+${new Date() - mountTime}ms)`, + `✘ Prefetch failed (+${Date.now() - mountTime}ms)`, ); }, ); @@ -121,7 +128,7 @@ class NetworkImageCallbackExample extends React.Component< style={[styles.base, {overflow: 'visible'}]} onLoadStart={() => this._loadEventFired( - `✔ (prefetched) onLoadStart (+${new Date() - mountTime}ms)`, + `✔ (prefetched) onLoadStart (+${Date.now() - mountTime}ms)`, ) } onLoad={event => { @@ -129,18 +136,18 @@ class NetworkImageCallbackExample extends React.Component< if (event.nativeEvent.source) { const url = event.nativeEvent.source.uri; this._loadEventFired( - `✔ (prefetched) onLoad (+${new Date() - + `✔ (prefetched) onLoad (+${Date.now() - mountTime}ms) for URL ${url}`, ); } else { this._loadEventFired( - `✔ (prefetched) onLoad (+${new Date() - mountTime}ms)`, + `✔ (prefetched) onLoad (+${Date.now() - mountTime}ms)`, ); } }} onLoadEnd={() => this._loadEventFired( - `✔ (prefetched) onLoadEnd (+${new Date() - mountTime}ms)`, + `✔ (prefetched) onLoadEnd (+${Date.now() - mountTime}ms)`, ) } /> @@ -152,9 +159,9 @@ class NetworkImageCallbackExample extends React.Component< } type NetworkImageExampleState = {| - error: boolean, + error: ?string, loading: boolean, - progress: number, + progress: $ReadOnlyArray, |}; type NetworkImageExampleProps = $ReadOnly<{| @@ -166,38 +173,38 @@ class NetworkImageExample extends React.Component< NetworkImageExampleState, > { state = { - error: false, + error: null, loading: false, - progress: 0, + progress: [], }; render() { - const loader = this.state.loading ? ( - - {this.state.progress}% - - - ) : null; - return this.state.error ? ( + return this.state.error != null ? ( {this.state.error} ) : ( - this.setState({loading: true})} - onError={e => - this.setState({error: e.nativeEvent.error, loading: false}) - } - onProgress={e => - this.setState({ - progress: Math.round( - (100 * e.nativeEvent.loaded) / e.nativeEvent.total, - ), - }) - } - onLoad={() => this.setState({loading: false, error: false})}> - {loader} - + <> + this.setState({loading: true})} + onError={e => + this.setState({error: e.nativeEvent.error, loading: false}) + } + onProgress={e => { + const {loaded, total} = e.nativeEvent; + this.setState(prevState => ({ + progress: [ + ...prevState.progress, + Math.round((100 * loaded) / total), + ], + })); + }} + onLoad={() => this.setState({loading: false, error: null})} + /> + + {this.state.progress.map(progress => `${progress}%`).join('\n')} + + ); } } @@ -346,12 +353,6 @@ const styles = StyleSheet.create({ width: 38, height: 38, }, - progress: { - flex: 1, - alignItems: 'center', - flexDirection: 'row', - width: 100, - }, leftMargin: { marginLeft: 10, }, @@ -465,7 +466,6 @@ exports.examples = [ /> ); }, - platform: 'ios', }, { title: 'Image Download Progress', @@ -478,7 +478,6 @@ exports.examples = [ /> ); }, - platform: 'ios', }, { title: 'defaultSource', diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageLoadEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageLoadEvent.java index 6e900a432aa..69778f028f5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageLoadEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageLoadEvent.java @@ -21,8 +21,6 @@ public class ImageLoadEvent extends Event { @Retention(RetentionPolicy.SOURCE) @interface ImageEventType {} - // Currently ON_PROGRESS is not implemented, these can be added - // easily once support exists in fresco. public static final int ON_ERROR = 1; public static final int ON_LOAD = 2; public static final int ON_LOAD_END = 3; @@ -34,18 +32,30 @@ public class ImageLoadEvent extends Event { private final @Nullable String mSourceUri; private final int mWidth; private final int mHeight; + private final int mLoaded; + private final int mTotal; public static final ImageLoadEvent createLoadStartEvent(int viewId) { return new ImageLoadEvent(viewId, ON_LOAD_START); } + /** + * @param loaded Amount of the image that has been loaded. It should be number of bytes, but + * Fresco does not currently provides that information. + * @param total Amount that `loaded` will be when the image is fully loaded. + */ + public static final ImageLoadEvent createProgressEvent( + int viewId, @Nullable String imageUri, int loaded, int total) { + return new ImageLoadEvent(viewId, ON_PROGRESS, null, imageUri, 0, 0, loaded, total); + } + public static final ImageLoadEvent createLoadEvent( int viewId, @Nullable String imageUri, int width, int height) { - return new ImageLoadEvent(viewId, ON_LOAD, null, imageUri, width, height); + return new ImageLoadEvent(viewId, ON_LOAD, null, imageUri, width, height, 0, 0); } public static final ImageLoadEvent createErrorEvent(int viewId, Throwable throwable) { - return new ImageLoadEvent(viewId, ON_ERROR, throwable.getMessage(), null, 0, 0); + return new ImageLoadEvent(viewId, ON_ERROR, throwable.getMessage(), null, 0, 0, 0, 0); } public static final ImageLoadEvent createLoadEndEvent(int viewId) { @@ -53,7 +63,7 @@ public class ImageLoadEvent extends Event { } private ImageLoadEvent(int viewId, @ImageEventType int eventType) { - this(viewId, eventType, null, null, 0, 0); + this(viewId, eventType, null, null, 0, 0, 0, 0); } private ImageLoadEvent( @@ -62,13 +72,17 @@ public class ImageLoadEvent extends Event { @Nullable String errorMessage, @Nullable String sourceUri, int width, - int height) { + int height, + int loaded, + int total) { super(viewId); mEventType = eventType; mErrorMessage = errorMessage; mSourceUri = sourceUri; mWidth = width; mHeight = height; + mLoaded = loaded; + mTotal = total; } public static String eventNameForType(@ImageEventType int eventType) { @@ -105,6 +119,11 @@ public class ImageLoadEvent extends Event { WritableMap eventData = null; switch (mEventType) { + case ON_PROGRESS: + eventData = Arguments.createMap(); + eventData.putInt("loaded", mLoaded); + eventData.putInt("total", mTotal); + break; case ON_LOAD: eventData = Arguments.createMap(); eventData.putMap("source", createEventDataSource()); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageDownloadListener.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageDownloadListener.java new file mode 100644 index 00000000000..336738e5d43 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageDownloadListener.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.image; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import com.facebook.drawee.controller.ControllerListener; +import com.facebook.drawee.drawable.ForwardingDrawable; +import javax.annotation.Nullable; + +public class ReactImageDownloadListener extends ForwardingDrawable + implements ControllerListener { + + private static final int MAX_LEVEL = 10000; + + public ReactImageDownloadListener() { + super(new EmptyDrawable()); + } + + public void onProgressChange(int loaded, int total) {} + + @Override + protected boolean onLevelChange(int level) { + onProgressChange(level, MAX_LEVEL); + return super.onLevelChange(level); + } + + @Override + public void onSubmit(String id, Object callerContext) {} + + @Override + public void onFinalImageSet( + String id, @Nullable INFO imageInfo, @Nullable Animatable animatable) {} + + @Override + public void onIntermediateImageSet(String id, @Nullable INFO imageInfo) {} + + @Override + public void onIntermediateImageFailed(String id, Throwable throwable) {} + + @Override + public void onFailure(String id, Throwable throwable) {} + + @Override + public void onRelease(String id) {} + + /** A {@link Drawable} that renders nothing. */ + private static final class EmptyDrawable extends Drawable { + + @Override + public void draw(Canvas canvas) { + // Do nothing. + } + + @Override + public void setAlpha(int alpha) { + // Do nothing. + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + // Do nothing. + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java index 7077ceb7ad9..397a8378d60 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java @@ -242,13 +242,15 @@ public class ReactImageManager extends SimpleViewManager { public @Nullable Map getExportedCustomDirectEventTypeConstants() { return MapBuilder.of( ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD_START), - MapBuilder.of("registrationName", "onLoadStart"), + MapBuilder.of("registrationName", "onLoadStart"), + ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_PROGRESS), + MapBuilder.of("registrationName", "onProgress"), ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD), - MapBuilder.of("registrationName", "onLoad"), + MapBuilder.of("registrationName", "onLoad"), ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_ERROR), - MapBuilder.of("registrationName", "onError"), + MapBuilder.of("registrationName", "onError"), ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD_END), - MapBuilder.of("registrationName", "onLoadEnd")); + MapBuilder.of("registrationName", "onLoadEnd")); } @Override diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java index d2dde40b443..ec49b8dd53e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java @@ -26,7 +26,6 @@ import androidx.annotation.Nullable; import com.facebook.common.references.CloseableReference; import com.facebook.common.util.UriUtil; import com.facebook.drawee.controller.AbstractDraweeControllerBuilder; -import com.facebook.drawee.controller.BaseControllerListener; import com.facebook.drawee.controller.ControllerListener; import com.facebook.drawee.controller.ForwardingControllerListener; import com.facebook.drawee.drawable.AutoRotateDrawable; @@ -199,7 +198,7 @@ public class ReactImageView extends GenericDraweeView { private final RoundedCornerPostprocessor mRoundedCornerPostprocessor; private final TilePostprocessor mTilePostprocessor; private @Nullable IterativeBoxBlurPostProcessor mIterativeBoxBlurPostProcessor; - private @Nullable ControllerListener mControllerListener; + private @Nullable ReactImageDownloadListener mDownloadListener; private @Nullable ControllerListener mControllerForTesting; private @Nullable GlobalImageLoadListener mGlobalImageLoadListener; private @Nullable Object mCallerContext; @@ -231,13 +230,21 @@ public class ReactImageView extends GenericDraweeView { public void setShouldNotifyLoadEvents(boolean shouldNotify) { if (!shouldNotify) { - mControllerListener = null; + mDownloadListener = null; } else { final EventDispatcher mEventDispatcher = UIManagerHelper.getEventDispatcherForReactTag((ReactContext) getContext(), getId()); - mControllerListener = - new BaseControllerListener() { + mDownloadListener = + new ReactImageDownloadListener() { + @Override + public void onProgressChange(int loaded, int total) { + // TODO: Somehow get image size and convert `loaded` and `total` to image bytes. + mEventDispatcher.dispatchEvent( + ImageLoadEvent.createProgressEvent( + getId(), mImageSource.getSource(), loaded, total)); + } + @Override public void onSubmit(String id, Object callerContext) { mEventDispatcher.dispatchEvent(ImageLoadEvent.createLoadStartEvent(getId())); @@ -538,15 +545,19 @@ public class ReactImageView extends GenericDraweeView { mDraweeControllerBuilder.setLowResImageRequest(cachedImageRequest); } - if (mControllerListener != null && mControllerForTesting != null) { + if (mDownloadListener != null && mControllerForTesting != null) { ForwardingControllerListener combinedListener = new ForwardingControllerListener(); - combinedListener.addListener(mControllerListener); + combinedListener.addListener(mDownloadListener); combinedListener.addListener(mControllerForTesting); mDraweeControllerBuilder.setControllerListener(combinedListener); } else if (mControllerForTesting != null) { mDraweeControllerBuilder.setControllerListener(mControllerForTesting); - } else if (mControllerListener != null) { - mDraweeControllerBuilder.setControllerListener(mControllerListener); + } else if (mDownloadListener != null) { + mDraweeControllerBuilder.setControllerListener(mDownloadListener); + } + + if (mDownloadListener != null) { + hierarchy.setProgressBarImage(mDownloadListener); } setController(mDraweeControllerBuilder.build());