Add support for ImageLoadEvents to RCTImageView

Summary: This diff implements ImageLoadEvents (ON_LOAD, ON_LOAD_END and ON_LOAD_START) in RCTImageView. ON_ERROR and ON_PROGRESS are easy to implement, too, but these 2 are supposed to carry extra information (error message and progress) but ImageLoadEvent doesn't support a payload yet (I'll add them in a separate patch).

Reviewed By: ahmedre

Differential Revision: D2824772
This commit is contained in:
Denis Koroskin 2016-01-12 18:51:12 -08:00 committed by Ahmed El-Helw
parent 76abec8894
commit 59fe71caff
9 changed files with 115 additions and 7 deletions

View File

@ -14,4 +14,5 @@ import android.graphics.Bitmap;
/* package */ interface BitmapUpdateListener { /* package */ interface BitmapUpdateListener {
public void onSecondaryAttach(Bitmap bitmap); public void onSecondaryAttach(Bitmap bitmap);
public void onBitmapReady(Bitmap bitmap); public void onBitmapReady(Bitmap bitmap);
public void onImageLoadEvent(int imageLoadEvent);
} }

View File

@ -44,6 +44,11 @@ import com.facebook.imagepipeline.request.ImageRequest;
*/ */
public ScaleType getScaleType(); public ScaleType getScaleType();
/**
* React tag used for dispatching ImageLoadEvents, or 0 to ignore events.
*/
public void setReactTag(int reactTag);
public void setBorderWidth(float borderWidth); public void setBorderWidth(float borderWidth);
public float getBorderWidth(); public float getBorderWidth();

View File

@ -14,19 +14,23 @@ import javax.annotation.Nullable;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter; import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Animatable;
import com.facebook.drawee.controller.ControllerListener;
import com.facebook.drawee.drawable.ScalingUtils.ScaleType; import com.facebook.drawee.drawable.ScalingUtils.ScaleType;
import com.facebook.drawee.generic.GenericDraweeHierarchy; import com.facebook.drawee.generic.GenericDraweeHierarchy;
import com.facebook.drawee.generic.RoundingParams; import com.facebook.drawee.generic.RoundingParams;
import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.infer.annotation.Assertions; import com.facebook.infer.annotation.Assertions;
import com.facebook.react.views.image.ImageLoadEvent;
import com.facebook.react.views.image.ImageResizeMode; import com.facebook.react.views.image.ImageResizeMode;
/** /**
* DrawImageWithDrawee is DrawCommand that can draw a local or remote image. * DrawImageWithDrawee is DrawCommand that can draw a local or remote image.
* It uses DraweeRequestHelper internally to fetch and cache the images. * It uses DraweeRequestHelper internally to fetch and cache the images.
*/ */
/* package */ final class DrawImageWithDrawee extends AbstractDrawCommand implements DrawImage { /* package */ final class DrawImageWithDrawee extends AbstractDrawCommand
implements DrawImage, ControllerListener {
private @Nullable DraweeRequestHelper mRequestHelper; private @Nullable DraweeRequestHelper mRequestHelper;
private @Nullable PorterDuffColorFilter mColorFilter; private @Nullable PorterDuffColorFilter mColorFilter;
@ -34,6 +38,8 @@ import com.facebook.react.views.image.ImageResizeMode;
private float mBorderWidth; private float mBorderWidth;
private float mBorderRadius; private float mBorderRadius;
private int mBorderColor; private int mBorderColor;
private int mReactTag;
private @Nullable FlatViewGroup.InvalidateCallback mCallback;
@Override @Override
public boolean hasImageRequest() { public boolean hasImageRequest() {
@ -45,7 +51,7 @@ import com.facebook.react.views.image.ImageResizeMode;
if (imageRequest == null) { if (imageRequest == null) {
mRequestHelper = null; mRequestHelper = null;
} else { } else {
mRequestHelper = new DraweeRequestHelper(imageRequest); mRequestHelper = new DraweeRequestHelper(imageRequest, this);
} }
} }
@ -98,6 +104,11 @@ import com.facebook.react.views.image.ImageResizeMode;
return mBorderColor; return mBorderColor;
} }
@Override
public void setReactTag(int reactTag) {
mReactTag = reactTag;
}
@Override @Override
public void onDraw(Canvas canvas) { public void onDraw(Canvas canvas) {
Assertions.assumeNotNull(mRequestHelper).getDrawable().draw(canvas); Assertions.assumeNotNull(mRequestHelper).getDrawable().draw(canvas);
@ -105,6 +116,8 @@ import com.facebook.react.views.image.ImageResizeMode;
@Override @Override
public void onAttached(FlatViewGroup.InvalidateCallback callback) { public void onAttached(FlatViewGroup.InvalidateCallback callback) {
mCallback = callback;
GenericDraweeHierarchy hierarchy = Assertions.assumeNotNull(mRequestHelper).getHierarchy(); GenericDraweeHierarchy hierarchy = Assertions.assumeNotNull(mRequestHelper).getHierarchy();
RoundingParams roundingParams = hierarchy.getRoundingParams(); RoundingParams roundingParams = hierarchy.getRoundingParams();
@ -140,6 +153,43 @@ import com.facebook.react.views.image.ImageResizeMode;
Assertions.assumeNotNull(mRequestHelper).detach(); Assertions.assumeNotNull(mRequestHelper).detach();
} }
@Override
public void onSubmit(String id, Object callerContext) {
if (mCallback != null && mReactTag != 0) {
mCallback.dispatchImageLoadEvent(mReactTag, ImageLoadEvent.ON_LOAD_START);
}
}
@Override
public void onFinalImageSet(
String id,
@Nullable Object imageInfo,
@Nullable Animatable animatable) {
if (mCallback != null && mReactTag != 0) {
mCallback.dispatchImageLoadEvent(mReactTag, ImageLoadEvent.ON_LOAD_END);
mCallback.dispatchImageLoadEvent(mReactTag, ImageLoadEvent.ON_LOAD);
}
}
@Override
public void onIntermediateImageSet(String id, @Nullable Object imageInfo) {
}
@Override
public void onIntermediateImageFailed(String id, Throwable throwable) {
}
@Override
public void onFailure(String id, Throwable throwable) {
if (mCallback != null && mReactTag != 0) {
mCallback.dispatchImageLoadEvent(mReactTag, ImageLoadEvent.ON_LOAD_END);
}
}
@Override
public void onRelease(String id) {
}
private boolean shouldDisplayBorder() { private boolean shouldDisplayBorder() {
return mBorderColor != 0 || mBorderRadius >= 0.5f; return mBorderColor != 0 || mBorderRadius >= 0.5f;
} }

View File

@ -44,6 +44,7 @@ import com.facebook.react.views.image.ImageResizeMode;
private @Nullable Path mPathForRoundedBitmap; private @Nullable Path mPathForRoundedBitmap;
private @Nullable BitmapShader mBitmapShader; private @Nullable BitmapShader mBitmapShader;
private boolean mForceClip; private boolean mForceClip;
private int mReactTag;
@Override @Override
public boolean hasImageRequest() { public boolean hasImageRequest() {
@ -80,6 +81,11 @@ import com.facebook.react.views.image.ImageResizeMode;
return mScaleType; return mScaleType;
} }
@Override
public void setReactTag(int reactTag) {
mReactTag = reactTag;
}
@Override @Override
protected void onDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
Bitmap bitmap = Assertions.assumeNotNull(mRequestHelper).getBitmap(); Bitmap bitmap = Assertions.assumeNotNull(mRequestHelper).getBitmap();
@ -148,6 +154,13 @@ import com.facebook.react.views.image.ImageResizeMode;
updateBounds(bitmap); updateBounds(bitmap);
} }
@Override
public void onImageLoadEvent(int imageLoadEvent) {
if (mReactTag != 0 && mCallback != null) {
mCallback.dispatchImageLoadEvent(mReactTag, imageLoadEvent);
}
}
/* package */ void updateBounds(Bitmap bitmap) { /* package */ void updateBounds(Bitmap bitmap) {
Assertions.assumeNotNull(mCallback).invalidate(); Assertions.assumeNotNull(mCallback).invalidate();

View File

@ -14,6 +14,7 @@ import android.content.res.Resources;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder; import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
import com.facebook.drawee.controller.ControllerListener;
import com.facebook.drawee.generic.GenericDraweeHierarchy; import com.facebook.drawee.generic.GenericDraweeHierarchy;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.interfaces.DraweeController; import com.facebook.drawee.interfaces.DraweeController;
@ -37,10 +38,11 @@ import com.facebook.infer.annotation.Assertions;
private final DraweeController mDraweeController; private final DraweeController mDraweeController;
private int mAttachCounter; private int mAttachCounter;
/* package */ DraweeRequestHelper(ImageRequest imageRequest) { /* package */ DraweeRequestHelper(ImageRequest imageRequest, ControllerListener listener) {
DraweeController controller = sControllerBuilder DraweeController controller = sControllerBuilder
.setImageRequest(imageRequest) .setImageRequest(imageRequest)
.setCallerContext(RCTImageView.getCallerContext()) .setCallerContext(RCTImageView.getCallerContext())
.setControllerListener(listener)
.build(); .build();
controller.setHierarchy(sHierarchyBuilder.build()); controller.setHierarchy(sHierarchyBuilder.build());

View File

@ -18,17 +18,21 @@ import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewParent; import android.view.ViewParent;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.touch.CatalystInterceptingViewGroup; import com.facebook.react.touch.CatalystInterceptingViewGroup;
import com.facebook.react.touch.OnInterceptTouchEventListener; import com.facebook.react.touch.OnInterceptTouchEventListener;
import com.facebook.react.uimanager.PointerEvents; import com.facebook.react.uimanager.PointerEvents;
import com.facebook.react.uimanager.ReactCompoundView; import com.facebook.react.uimanager.ReactCompoundView;
import com.facebook.react.uimanager.ReactPointerEventsView; import com.facebook.react.uimanager.ReactPointerEventsView;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.views.image.ImageLoadEvent;
/** /**
* A view that FlatShadowNode hierarchy maps to. Performs drawing by iterating over * A view that FlatShadowNode hierarchy maps to. Performs drawing by iterating over
@ -54,6 +58,18 @@ import com.facebook.react.uimanager.ReactPointerEventsView;
view.invalidate(); view.invalidate();
} }
} }
public void dispatchImageLoadEvent(int reactTag, int imageLoadEvent) {
FlatViewGroup view = get();
if (view == null) {
return;
}
ReactContext reactContext = ((ReactContext) view.getContext());
UIManagerModule uiManagerModule = reactContext.getNativeModule(UIManagerModule.class);
uiManagerModule.getEventDispatcher().dispatchEvent(
new ImageLoadEvent(reactTag, SystemClock.uptimeMillis(), imageLoadEvent));
}
} }
private static final ArrayList<FlatViewGroup> LAYOUT_REQUESTS = new ArrayList<>(); private static final ArrayList<FlatViewGroup> LAYOUT_REQUESTS = new ArrayList<>();

View File

@ -99,6 +99,11 @@ import com.facebook.infer.annotation.Assertions;
Assertions.assumeNotNull(mCallback).invalidate(); Assertions.assumeNotNull(mCallback).invalidate();
} }
@Override
public void onImageLoadEvent(int imageLoadEvent) {
// ignore
}
@Override @Override
public void onAttached(FlatViewGroup.InvalidateCallback callback) { public void onAttached(FlatViewGroup.InvalidateCallback callback) {
mCallback = callback; mCallback = callback;

View File

@ -23,6 +23,7 @@ import com.facebook.imagepipeline.image.CloseableBitmap;
import com.facebook.imagepipeline.image.CloseableImage; import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.infer.annotation.Assertions; import com.facebook.infer.annotation.Assertions;
import com.facebook.react.views.image.ImageLoadEvent;
/** /**
* Helper class for DrawImage that helps manage fetch requests through ImagePipeline. * Helper class for DrawImage that helps manage fetch requests through ImagePipeline.
@ -54,11 +55,13 @@ import com.facebook.infer.annotation.Assertions;
// this is a secondary attach, ignore it, only updating Bitmap boundaries if needed. // this is a secondary attach, ignore it, only updating Bitmap boundaries if needed.
Bitmap bitmap = getBitmap(); Bitmap bitmap = getBitmap();
if (bitmap != null) { if (bitmap != null) {
mBitmapUpdateListener.onSecondaryAttach(bitmap); listener.onSecondaryAttach(bitmap);
} }
return; return;
} }
listener.onImageLoadEvent(ImageLoadEvent.ON_LOAD_START);
Assertions.assertCondition(mDataSource == null); Assertions.assertCondition(mDataSource == null);
Assertions.assertCondition(mImageRef == null); Assertions.assertCondition(mImageRef == null);
@ -150,7 +153,10 @@ import com.facebook.infer.annotation.Assertions;
return; return;
} }
Assertions.assumeNotNull(mBitmapUpdateListener).onBitmapReady(bitmap); BitmapUpdateListener listener = Assertions.assumeNotNull(mBitmapUpdateListener);
listener.onBitmapReady(bitmap);
listener.onImageLoadEvent(ImageLoadEvent.ON_LOAD_END);
listener.onImageLoadEvent(ImageLoadEvent.ON_LOAD);
} finally { } finally {
dataSource.close(); dataSource.close();
} }
@ -158,8 +164,8 @@ import com.facebook.infer.annotation.Assertions;
@Override @Override
public void onFailure(DataSource<CloseableReference<CloseableImage>> dataSource) { public void onFailure(DataSource<CloseableReference<CloseableImage>> dataSource) {
if (mDataSource != dataSource) { if (mDataSource == dataSource) {
// Should always be the case, but let's be safe. Assertions.assumeNotNull(mBitmapUpdateListener).onImageLoadEvent(ImageLoadEvent.ON_LOAD_END);
mDataSource = null; mDataSource = null;
} }
@ -168,6 +174,11 @@ import com.facebook.infer.annotation.Assertions;
@Override @Override
public void onCancellation(DataSource<CloseableReference<CloseableImage>> dataSource) { public void onCancellation(DataSource<CloseableReference<CloseableImage>> dataSource) {
if (mDataSource == dataSource) {
mDataSource = null;
}
dataSource.close();
} }
@Override @Override

View File

@ -80,6 +80,11 @@ import com.facebook.react.views.image.ImageResizeMode;
} }
} }
@ReactProp(name = "shouldNotifyLoadEvents")
public void setShouldNotifyLoadEvents(boolean shouldNotifyLoadEvents) {
getMutableDrawImage().setReactTag(shouldNotifyLoadEvents ? getReactTag() : 0);
}
@ReactProp(name = "src") @ReactProp(name = "src")
public void setSource(@Nullable String source) { public void setSource(@Nullable String source) {
getMutableDrawImage().setImageRequest( getMutableDrawImage().setImageRequest(