Skip to content

Commit fa0e6f8

Browse files
yungstersfacebook-github-bot
authored andcommitted
RN: Image Progress Event on Android
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
1 parent 74ab8f6 commit fa0e6f8

File tree

5 files changed

+179
-70
lines changed

5 files changed

+179
-70
lines changed

RNTester/js/examples/Image/ImageExample.js

+50-51
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type ImageSource = $ReadOnly<{|
3636
type NetworkImageCallbackExampleState = {|
3737
events: Array<string>,
3838
startLoadPrefetched: boolean,
39-
mountTime: Date,
39+
mountTime: number,
4040
|};
4141

4242
type NetworkImageCallbackExampleProps = $ReadOnly<{|
@@ -51,11 +51,11 @@ class NetworkImageCallbackExample extends React.Component<
5151
state = {
5252
events: [],
5353
startLoadPrefetched: false,
54-
mountTime: new Date(),
54+
mountTime: Date.now(),
5555
};
5656

5757
UNSAFE_componentWillMount() {
58-
this.setState({mountTime: new Date()});
58+
this.setState({mountTime: Date.now()});
5959
}
6060

6161
_loadEventFired = (event: string) => {
@@ -72,43 +72,50 @@ class NetworkImageCallbackExample extends React.Component<
7272
source={this.props.source}
7373
style={[styles.base, {overflow: 'visible'}]}
7474
onLoadStart={() =>
75-
this._loadEventFired(`✔ onLoadStart (+${new Date() - mountTime}ms)`)
75+
this._loadEventFired(`✔ onLoadStart (+${Date.now() - mountTime}ms)`)
7676
}
77+
onProgress={event => {
78+
const {loaded, total} = event.nativeEvent;
79+
const percent = Math.round((loaded / total) * 100);
80+
this._loadEventFired(
81+
`✔ onProgress ${percent}% (+${Date.now() - mountTime}ms)`,
82+
);
83+
}}
7784
onLoad={event => {
7885
if (event.nativeEvent.source) {
7986
const url = event.nativeEvent.source.uri;
8087
this._loadEventFired(
81-
`✔ onLoad (+${new Date() - mountTime}ms) for URL ${url}`,
88+
`✔ onLoad (+${Date.now() - mountTime}ms) for URL ${url}`,
8289
);
8390
} else {
84-
this._loadEventFired(`✔ onLoad (+${new Date() - mountTime}ms)`);
91+
this._loadEventFired(`✔ onLoad (+${Date.now() - mountTime}ms)`);
8592
}
8693
}}
8794
onLoadEnd={() => {
88-
this._loadEventFired(`✔ onLoadEnd (+${new Date() - mountTime}ms)`);
95+
this._loadEventFired(`✔ onLoadEnd (+${Date.now() - mountTime}ms)`);
8996
this.setState({startLoadPrefetched: true}, () => {
9097
prefetchTask.then(
9198
() => {
9299
this._loadEventFired(
93-
`✔ Prefetch OK (+${new Date() - mountTime}ms)`,
100+
`✔ Prefetch OK (+${Date.now() - mountTime}ms)`,
94101
);
95102
Image.queryCache([IMAGE_PREFETCH_URL]).then(map => {
96103
const result = map[IMAGE_PREFETCH_URL];
97104
if (result) {
98105
this._loadEventFired(
99-
`✔ queryCache "${result}" (+${new Date() -
106+
`✔ queryCache "${result}" (+${Date.now() -
100107
mountTime}ms)`,
101108
);
102109
} else {
103110
this._loadEventFired(
104-
`✘ queryCache (+${new Date() - mountTime}ms)`,
111+
`✘ queryCache (+${Date.now() - mountTime}ms)`,
105112
);
106113
}
107114
});
108115
},
109116
error => {
110117
this._loadEventFired(
111-
`✘ Prefetch failed (+${new Date() - mountTime}ms)`,
118+
`✘ Prefetch failed (+${Date.now() - mountTime}ms)`,
112119
);
113120
},
114121
);
@@ -121,26 +128,26 @@ class NetworkImageCallbackExample extends React.Component<
121128
style={[styles.base, {overflow: 'visible'}]}
122129
onLoadStart={() =>
123130
this._loadEventFired(
124-
`✔ (prefetched) onLoadStart (+${new Date() - mountTime}ms)`,
131+
`✔ (prefetched) onLoadStart (+${Date.now() - mountTime}ms)`,
125132
)
126133
}
127134
onLoad={event => {
128135
// Currently this image source feature is only available on iOS.
129136
if (event.nativeEvent.source) {
130137
const url = event.nativeEvent.source.uri;
131138
this._loadEventFired(
132-
`✔ (prefetched) onLoad (+${new Date() -
139+
`✔ (prefetched) onLoad (+${Date.now() -
133140
mountTime}ms) for URL ${url}`,
134141
);
135142
} else {
136143
this._loadEventFired(
137-
`✔ (prefetched) onLoad (+${new Date() - mountTime}ms)`,
144+
`✔ (prefetched) onLoad (+${Date.now() - mountTime}ms)`,
138145
);
139146
}
140147
}}
141148
onLoadEnd={() =>
142149
this._loadEventFired(
143-
`✔ (prefetched) onLoadEnd (+${new Date() - mountTime}ms)`,
150+
`✔ (prefetched) onLoadEnd (+${Date.now() - mountTime}ms)`,
144151
)
145152
}
146153
/>
@@ -152,9 +159,9 @@ class NetworkImageCallbackExample extends React.Component<
152159
}
153160

154161
type NetworkImageExampleState = {|
155-
error: boolean,
162+
error: ?string,
156163
loading: boolean,
157-
progress: number,
164+
progress: $ReadOnlyArray<number>,
158165
|};
159166

160167
type NetworkImageExampleProps = $ReadOnly<{|
@@ -166,38 +173,38 @@ class NetworkImageExample extends React.Component<
166173
NetworkImageExampleState,
167174
> {
168175
state = {
169-
error: false,
176+
error: null,
170177
loading: false,
171-
progress: 0,
178+
progress: [],
172179
};
173180

174181
render() {
175-
const loader = this.state.loading ? (
176-
<View style={styles.progress}>
177-
<Text>{this.state.progress}%</Text>
178-
<ActivityIndicator style={{marginLeft: 5}} />
179-
</View>
180-
) : null;
181-
return this.state.error ? (
182+
return this.state.error != null ? (
182183
<Text>{this.state.error}</Text>
183184
) : (
184-
<ImageBackground
185-
source={this.props.source}
186-
style={[styles.base, {overflow: 'visible'}]}
187-
onLoadStart={e => this.setState({loading: true})}
188-
onError={e =>
189-
this.setState({error: e.nativeEvent.error, loading: false})
190-
}
191-
onProgress={e =>
192-
this.setState({
193-
progress: Math.round(
194-
(100 * e.nativeEvent.loaded) / e.nativeEvent.total,
195-
),
196-
})
197-
}
198-
onLoad={() => this.setState({loading: false, error: false})}>
199-
{loader}
200-
</ImageBackground>
185+
<>
186+
<Image
187+
source={this.props.source}
188+
style={[styles.base, {overflow: 'visible'}]}
189+
onLoadStart={e => this.setState({loading: true})}
190+
onError={e =>
191+
this.setState({error: e.nativeEvent.error, loading: false})
192+
}
193+
onProgress={e => {
194+
const {loaded, total} = e.nativeEvent;
195+
this.setState(prevState => ({
196+
progress: [
197+
...prevState.progress,
198+
Math.round((100 * loaded) / total),
199+
],
200+
}));
201+
}}
202+
onLoad={() => this.setState({loading: false, error: null})}
203+
/>
204+
<Text>
205+
{this.state.progress.map(progress => `${progress}%`).join('\n')}
206+
</Text>
207+
</>
201208
);
202209
}
203210
}
@@ -346,12 +353,6 @@ const styles = StyleSheet.create({
346353
width: 38,
347354
height: 38,
348355
},
349-
progress: {
350-
flex: 1,
351-
alignItems: 'center',
352-
flexDirection: 'row',
353-
width: 100,
354-
},
355356
leftMargin: {
356357
marginLeft: 10,
357358
},
@@ -465,7 +466,6 @@ exports.examples = [
465466
/>
466467
);
467468
},
468-
platform: 'ios',
469469
},
470470
{
471471
title: 'Image Download Progress',
@@ -478,7 +478,6 @@ exports.examples = [
478478
/>
479479
);
480480
},
481-
platform: 'ios',
482481
},
483482
{
484483
title: 'defaultSource',

ReactAndroid/src/main/java/com/facebook/react/views/image/ImageLoadEvent.java

+25-6
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ public class ImageLoadEvent extends Event<ImageLoadEvent> {
2121
@Retention(RetentionPolicy.SOURCE)
2222
@interface ImageEventType {}
2323

24-
// Currently ON_PROGRESS is not implemented, these can be added
25-
// easily once support exists in fresco.
2624
public static final int ON_ERROR = 1;
2725
public static final int ON_LOAD = 2;
2826
public static final int ON_LOAD_END = 3;
@@ -34,26 +32,38 @@ public class ImageLoadEvent extends Event<ImageLoadEvent> {
3432
private final @Nullable String mSourceUri;
3533
private final int mWidth;
3634
private final int mHeight;
35+
private final int mLoaded;
36+
private final int mTotal;
3737

3838
public static final ImageLoadEvent createLoadStartEvent(int viewId) {
3939
return new ImageLoadEvent(viewId, ON_LOAD_START);
4040
}
4141

42+
/**
43+
* @param loaded Amount of the image that has been loaded. It should be number of bytes, but
44+
* Fresco does not currently provides that information.
45+
* @param total Amount that `loaded` will be when the image is fully loaded.
46+
*/
47+
public static final ImageLoadEvent createProgressEvent(
48+
int viewId, @Nullable String imageUri, int loaded, int total) {
49+
return new ImageLoadEvent(viewId, ON_PROGRESS, null, imageUri, 0, 0, loaded, total);
50+
}
51+
4252
public static final ImageLoadEvent createLoadEvent(
4353
int viewId, @Nullable String imageUri, int width, int height) {
44-
return new ImageLoadEvent(viewId, ON_LOAD, null, imageUri, width, height);
54+
return new ImageLoadEvent(viewId, ON_LOAD, null, imageUri, width, height, 0, 0);
4555
}
4656

4757
public static final ImageLoadEvent createErrorEvent(int viewId, Throwable throwable) {
48-
return new ImageLoadEvent(viewId, ON_ERROR, throwable.getMessage(), null, 0, 0);
58+
return new ImageLoadEvent(viewId, ON_ERROR, throwable.getMessage(), null, 0, 0, 0, 0);
4959
}
5060

5161
public static final ImageLoadEvent createLoadEndEvent(int viewId) {
5262
return new ImageLoadEvent(viewId, ON_LOAD_END);
5363
}
5464

5565
private ImageLoadEvent(int viewId, @ImageEventType int eventType) {
56-
this(viewId, eventType, null, null, 0, 0);
66+
this(viewId, eventType, null, null, 0, 0, 0, 0);
5767
}
5868

5969
private ImageLoadEvent(
@@ -62,13 +72,17 @@ private ImageLoadEvent(
6272
@Nullable String errorMessage,
6373
@Nullable String sourceUri,
6474
int width,
65-
int height) {
75+
int height,
76+
int loaded,
77+
int total) {
6678
super(viewId);
6779
mEventType = eventType;
6880
mErrorMessage = errorMessage;
6981
mSourceUri = sourceUri;
7082
mWidth = width;
7183
mHeight = height;
84+
mLoaded = loaded;
85+
mTotal = total;
7286
}
7387

7488
public static String eventNameForType(@ImageEventType int eventType) {
@@ -105,6 +119,11 @@ public void dispatch(RCTEventEmitter rctEventEmitter) {
105119
WritableMap eventData = null;
106120

107121
switch (mEventType) {
122+
case ON_PROGRESS:
123+
eventData = Arguments.createMap();
124+
eventData.putInt("loaded", mLoaded);
125+
eventData.putInt("total", mTotal);
126+
break;
108127
case ON_LOAD:
109128
eventData = Arguments.createMap();
110129
eventData.putMap("source", createEventDataSource());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.views.image;
9+
10+
import android.graphics.Canvas;
11+
import android.graphics.ColorFilter;
12+
import android.graphics.PixelFormat;
13+
import android.graphics.drawable.Animatable;
14+
import android.graphics.drawable.Drawable;
15+
import com.facebook.drawee.controller.ControllerListener;
16+
import com.facebook.drawee.drawable.ForwardingDrawable;
17+
import javax.annotation.Nullable;
18+
19+
public class ReactImageDownloadListener<INFO> extends ForwardingDrawable
20+
implements ControllerListener<INFO> {
21+
22+
private static final int MAX_LEVEL = 10000;
23+
24+
public ReactImageDownloadListener() {
25+
super(new EmptyDrawable());
26+
}
27+
28+
public void onProgressChange(int loaded, int total) {}
29+
30+
@Override
31+
protected boolean onLevelChange(int level) {
32+
onProgressChange(level, MAX_LEVEL);
33+
return super.onLevelChange(level);
34+
}
35+
36+
@Override
37+
public void onSubmit(String id, Object callerContext) {}
38+
39+
@Override
40+
public void onFinalImageSet(
41+
String id, @Nullable INFO imageInfo, @Nullable Animatable animatable) {}
42+
43+
@Override
44+
public void onIntermediateImageSet(String id, @Nullable INFO imageInfo) {}
45+
46+
@Override
47+
public void onIntermediateImageFailed(String id, Throwable throwable) {}
48+
49+
@Override
50+
public void onFailure(String id, Throwable throwable) {}
51+
52+
@Override
53+
public void onRelease(String id) {}
54+
55+
/** A {@link Drawable} that renders nothing. */
56+
private static final class EmptyDrawable extends Drawable {
57+
58+
@Override
59+
public void draw(Canvas canvas) {
60+
// Do nothing.
61+
}
62+
63+
@Override
64+
public void setAlpha(int alpha) {
65+
// Do nothing.
66+
}
67+
68+
@Override
69+
public void setColorFilter(ColorFilter colorFilter) {
70+
// Do nothing.
71+
}
72+
73+
@Override
74+
public int getOpacity() {
75+
return PixelFormat.OPAQUE;
76+
}
77+
}
78+
}

ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java

+6-4
Original file line numberDiff line numberDiff line change
@@ -242,13 +242,15 @@ public void setHeaders(ReactImageView view, ReadableMap headers) {
242242
public @Nullable Map getExportedCustomDirectEventTypeConstants() {
243243
return MapBuilder.of(
244244
ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD_START),
245-
MapBuilder.of("registrationName", "onLoadStart"),
245+
MapBuilder.of("registrationName", "onLoadStart"),
246+
ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_PROGRESS),
247+
MapBuilder.of("registrationName", "onProgress"),
246248
ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD),
247-
MapBuilder.of("registrationName", "onLoad"),
249+
MapBuilder.of("registrationName", "onLoad"),
248250
ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_ERROR),
249-
MapBuilder.of("registrationName", "onError"),
251+
MapBuilder.of("registrationName", "onError"),
250252
ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD_END),
251-
MapBuilder.of("registrationName", "onLoadEnd"));
253+
MapBuilder.of("registrationName", "onLoadEnd"));
252254
}
253255

254256
@Override

0 commit comments

Comments
 (0)