Skip to content

Commit

Permalink
RN: Image Progress Event on Android
Browse files Browse the repository at this point in the history
Summary:
Adds support for the `onProgress` event on `Image`, for Android.

Since Fresco does not provide a progress listener on `ControllerListener`, this uses a forwarding progress indicator `Drawable` to pass along values from `onLevelChange`.

Caveat: The ratio between `loaded` and `total` can be used, but `total` is currently always 10000. It seems that Fresco does not currently expose the content length from the network response headers.

Changelog:
[Android][Added] - Adds support for the `onProgress` event on `Image`

Reviewed By: mdvacca

Differential Revision: D22029915

fbshipit-source-id: 66174b55ed01e1a059c080e2b14415e7d268bc5c
  • Loading branch information
yungsters authored and facebook-github-bot committed Jun 16, 2020
1 parent 74ab8f6 commit fa0e6f8
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 70 deletions.
101 changes: 50 additions & 51 deletions RNTester/js/examples/Image/ImageExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type ImageSource = $ReadOnly<{|
type NetworkImageCallbackExampleState = {|
events: Array<string>,
startLoadPrefetched: boolean,
mountTime: Date,
mountTime: number,
|};

type NetworkImageCallbackExampleProps = $ReadOnly<{|
Expand All @@ -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) => {
Expand All @@ -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)`,
);
},
);
Expand All @@ -121,26 +128,26 @@ 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 => {
// Currently this image source feature is only available on iOS.
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)`,
)
}
/>
Expand All @@ -152,9 +159,9 @@ class NetworkImageCallbackExample extends React.Component<
}

type NetworkImageExampleState = {|
error: boolean,
error: ?string,
loading: boolean,
progress: number,
progress: $ReadOnlyArray<number>,
|};

type NetworkImageExampleProps = $ReadOnly<{|
Expand All @@ -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 ? (
<View style={styles.progress}>
<Text>{this.state.progress}%</Text>
<ActivityIndicator style={{marginLeft: 5}} />
</View>
) : null;
return this.state.error ? (
return this.state.error != null ? (
<Text>{this.state.error}</Text>
) : (
<ImageBackground
source={this.props.source}
style={[styles.base, {overflow: 'visible'}]}
onLoadStart={e => 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}
</ImageBackground>
<>
<Image
source={this.props.source}
style={[styles.base, {overflow: 'visible'}]}
onLoadStart={e => 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})}
/>
<Text>
{this.state.progress.map(progress => `${progress}%`).join('\n')}
</Text>
</>
);
}
}
Expand Down Expand Up @@ -346,12 +353,6 @@ const styles = StyleSheet.create({
width: 38,
height: 38,
},
progress: {
flex: 1,
alignItems: 'center',
flexDirection: 'row',
width: 100,
},
leftMargin: {
marginLeft: 10,
},
Expand Down Expand Up @@ -465,7 +466,6 @@ exports.examples = [
/>
);
},
platform: 'ios',
},
{
title: 'Image Download Progress',
Expand All @@ -478,7 +478,6 @@ exports.examples = [
/>
);
},
platform: 'ios',
},
{
title: 'defaultSource',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ public class ImageLoadEvent extends Event<ImageLoadEvent> {
@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;
Expand All @@ -34,26 +32,38 @@ public class ImageLoadEvent extends Event<ImageLoadEvent> {
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) {
return new ImageLoadEvent(viewId, ON_LOAD_END);
}

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(
Expand All @@ -62,13 +72,17 @@ private ImageLoadEvent(
@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) {
Expand Down Expand Up @@ -105,6 +119,11 @@ public void dispatch(RCTEventEmitter rctEventEmitter) {
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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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<INFO> extends ForwardingDrawable
implements ControllerListener<INFO> {

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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,15 @@ public void setHeaders(ReactImageView view, ReadableMap headers) {
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
Expand Down
Loading

0 comments on commit fa0e6f8

Please sign in to comment.