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
This commit is contained in:
Tim Yung
2020-06-16 15:01:05 -07:00
committed by Facebook GitHub Bot
parent 74ab8f6e5a
commit fa0e6f8051
5 changed files with 179 additions and 70 deletions
@@ -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;
@@ -34,18 +32,30 @@ 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) {
@@ -53,7 +63,7 @@ public class ImageLoadEvent extends Event<ImageLoadEvent> {
}
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<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) {
@@ -105,6 +119,11 @@ public class ImageLoadEvent extends Event<ImageLoadEvent> {
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());
@@ -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;
}
}
}
@@ -242,13 +242,15 @@ public class ReactImageManager extends SimpleViewManager<ReactImageView> {
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
@@ -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<ImageInfo>() {
mDownloadListener =
new ReactImageDownloadListener<ImageInfo>() {
@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());