diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js
index 4cab0c1a1..68b16b2d1 100644
--- a/Examples/UIExplorer/ImageExample.js
+++ b/Examples/UIExplorer/ImageExample.js
@@ -28,6 +28,44 @@ var base64Icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAQAAACS
var ImageCapInsetsExample = require('./ImageCapInsetsExample');
+var NetworkImageCallbackExample = React.createClass({
+ getInitialState: function() {
+ return {
+ events: [],
+ };
+ },
+
+ componentWillMount() {
+ this.setState({mountTime: new Date()});
+ },
+
+ render: function() {
+ var { mountTime } = this.state;
+
+ return (
+
+ this._loadEventFired(`✔ onLoadStart (+${new Date() - mountTime}ms)`)}
+ onLoad={() => this._loadEventFired(`✔ onLoad (+${new Date() - mountTime}ms)`)}
+ onLoadEnd={() => this._loadEventFired(`✔ onLoadEnd (+${new Date() - mountTime}ms)`)}
+ />
+
+
+ {this.state.events.join('\n')}
+
+
+ );
+ },
+
+ _loadEventFired(event) {
+ this.setState((state) => {
+ return state.events = [...state.events, event];
+ });
+ }
+});
+
var NetworkImageExample = React.createClass({
watchID: (null: ?number),
@@ -92,6 +130,14 @@ exports.examples = [
);
},
},
+ {
+ title: 'Image Loading Events',
+ render: function() {
+ return (
+
+ );
+ },
+ },
{
title: 'Error Handler',
render: function() {
diff --git a/Libraries/Image/Image.android.js b/Libraries/Image/Image.android.js
index 66308f682..9d9558c96 100644
--- a/Libraries/Image/Image.android.js
+++ b/Libraries/Image/Image.android.js
@@ -56,6 +56,7 @@ var ImageViewAttributes = merge(ReactNativeViewAttributes.UIView, {
resizeMode: true,
progressiveRenderingEnabled: true,
fadeDuration: true,
+ shouldNotifyLoadEvents: true,
});
var Image = React.createClass({
@@ -75,7 +76,18 @@ var Image = React.createClass({
]).isRequired,
progressiveRenderingEnabled: PropTypes.bool,
fadeDuration: PropTypes.number,
- style: StyleSheetPropType(ImageStylePropTypes),
+ /**
+ * Invoked on load start
+ */
+ onLoadStart: PropTypes.func,
+ /**
+ * Invoked when load completes successfully
+ */
+ onLoad: PropTypes.func,
+ /**
+ * Invoked when load either succeeds or fails
+ */
+ onLoadEnd: PropTypes.func,
/**
* Used to locate this view in end-to-end tests.
*/
@@ -137,9 +149,11 @@ var Image = React.createClass({
if (source && source.uri) {
var {width, height} = source;
var style = flattenStyle([{width, height}, styles.base, this.props.style]);
+ var {onLoadStart, onLoad, onLoadEnd} = this.props;
var nativeProps = merge(this.props, {
style,
+ shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd),
src: source.uri,
});
@@ -186,6 +200,7 @@ var cfg = {
defaultImageSrc: true,
imageTag: true,
progressHandlerRegistered: true,
+ shouldNotifyLoadEvents: true,
},
};
var RKImage = requireNativeComponent('RCTImageView', Image, cfg);
diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js
index 4da2d53e5..6943bf909 100644
--- a/Libraries/Image/Image.ios.js
+++ b/Libraries/Image/Image.ios.js
@@ -113,7 +113,6 @@ var Image = React.createClass({
onLayout: PropTypes.func,
/**
* Invoked on load start
- * @platform ios
*/
onLoadStart: PropTypes.func,
/**
@@ -128,12 +127,10 @@ var Image = React.createClass({
onError: PropTypes.func,
/**
* Invoked when load completes successfully
- * @platform ios
*/
onLoad: PropTypes.func,
/**
* Invoked when load either succeeds or fails
- * @platform ios
*/
onLoadEnd: PropTypes.func,
},
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
new file mode 100644
index 000000000..fc183507a
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageLoadEvent.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+package com.facebook.react.views.image;
+
+import android.support.annotation.IntDef;
+
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+public class ImageLoadEvent extends Event {
+ @IntDef({ON_ERROR, ON_LOAD, ON_LOAD_END, ON_LOAD_START, ON_PROGRESS})
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ImageEventType {}
+
+ // Currently ON_ERROR and ON_PROGRESS are 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;
+ public static final int ON_LOAD_START = 4;
+ public static final int ON_PROGRESS = 5;
+
+ private final int mEventType;
+
+ public ImageLoadEvent(int viewId, long timestampMs, @ImageEventType int eventType) {
+ super(viewId, timestampMs);
+ mEventType = eventType;
+ }
+
+ public static String eventNameForType(@ImageEventType int eventType) {
+ switch(eventType) {
+ case ON_ERROR:
+ return "topError";
+ case ON_LOAD:
+ return "topLoad";
+ case ON_LOAD_END:
+ return "topLoadEnd";
+ case ON_LOAD_START:
+ return "topLoadStart";
+ case ON_PROGRESS:
+ return "topProgress";
+ default:
+ throw new IllegalStateException("Invalid image event: " + Integer.toString(eventType));
+ }
+ }
+
+ @Override
+ public String getEventName() {
+ return ImageLoadEvent.eventNameForType(mEventType);
+ }
+
+ @Override
+ public short getCoalescingKey() {
+ // Intentionally casting mEventType because it is guaranteed to be small
+ // enough to fit into short.
+ return (short) mEventType;
+ }
+
+ @Override
+ public void dispatch(RCTEventEmitter rctEventEmitter) {
+ rctEventEmitter.receiveEvent(getViewTag(), getEventName(), null);
+ }
+}
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 0158ff072..19ff94205 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
@@ -11,10 +11,13 @@ package com.facebook.react.views.image;
import javax.annotation.Nullable;
+import java.util.Map;
+
import android.graphics.Color;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
+import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ReactProp;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
@@ -102,6 +105,23 @@ public class ReactImageManager extends SimpleViewManager {
view.setFadeDuration(durationMs);
}
+ @ReactProp(name = "shouldNotifyLoadEvents")
+ public void setLoadHandlersRegistered(ReactImageView view, boolean shouldNotifyLoadEvents) {
+ view.setShouldNotifyLoadEvents(shouldNotifyLoadEvents);
+ }
+
+ @Override
+ public @Nullable Map getExportedCustomDirectEventTypeConstants() {
+ return MapBuilder.of(
+ ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD_START),
+ MapBuilder.of("registrationName", "onLoadStart"),
+ ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD),
+ MapBuilder.of("registrationName", "onLoad"),
+ ImageLoadEvent.eventNameForType(ImageLoadEvent.ON_LOAD_END),
+ MapBuilder.of("registrationName", "onLoadEnd")
+ );
+ }
+
@Override
protected void onAfterUpdateTransaction(ReactImageView view) {
super.onAfterUpdateTransaction(view);
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 d741356fe..c889c8183 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
@@ -20,11 +20,15 @@ import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
+import android.graphics.drawable.Animatable;
import android.net.Uri;
+import android.os.SystemClock;
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.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchy;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
@@ -32,11 +36,15 @@ import com.facebook.drawee.generic.RoundingParams;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.GenericDraweeView;
import com.facebook.imagepipeline.common.ResizeOptions;
+import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.BasePostprocessor;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import com.facebook.imagepipeline.request.Postprocessor;
+import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.PixelUtil;
+import com.facebook.react.uimanager.UIManagerModule;
+import com.facebook.react.uimanager.events.EventDispatcher;
/**
* Wrapper class around Fresco's GenericDraweeView, enabling persisting props across multiple view
@@ -104,8 +112,9 @@ public class ReactImageView extends GenericDraweeView {
private boolean mIsLocalImage;
private final AbstractDraweeControllerBuilder mDraweeControllerBuilder;
private final RoundedCornerPostprocessor mRoundedCornerPostprocessor;
- private final @Nullable Object mCallerContext;
private @Nullable ControllerListener mControllerListener;
+ private @Nullable ControllerListener mControllerForTesting;
+ private final @Nullable Object mCallerContext;
private int mFadeDurationMs = -1;
private boolean mProgressiveRenderingEnabled;
@@ -127,6 +136,48 @@ public class ReactImageView extends GenericDraweeView {
mCallerContext = callerContext;
}
+ public void setShouldNotifyLoadEvents(boolean shouldNotify) {
+ if (!shouldNotify) {
+ mControllerListener = null;
+ } else {
+ final EventDispatcher mEventDispatcher = ((ReactContext) getContext()).
+ getNativeModule(UIManagerModule.class).getEventDispatcher();
+
+ mControllerListener = new BaseControllerListener() {
+ @Override
+ public void onSubmit(String id, Object callerContext) {
+ mEventDispatcher.dispatchEvent(
+ new ImageLoadEvent(getId(), SystemClock.uptimeMillis(), ImageLoadEvent.ON_LOAD_START)
+ );
+ }
+
+ @Override
+ public void onFinalImageSet(
+ String id,
+ @Nullable final ImageInfo imageInfo,
+ @Nullable Animatable animatable) {
+ if (imageInfo != null) {
+ mEventDispatcher.dispatchEvent(
+ new ImageLoadEvent(getId(), SystemClock.uptimeMillis(), ImageLoadEvent.ON_LOAD_END)
+ );
+ mEventDispatcher.dispatchEvent(
+ new ImageLoadEvent(getId(), SystemClock.uptimeMillis(), ImageLoadEvent.ON_LOAD)
+ );
+ }
+ }
+
+ @Override
+ public void onFailure(String id, Throwable throwable) {
+ mEventDispatcher.dispatchEvent(
+ new ImageLoadEvent(getId(), SystemClock.uptimeMillis(), ImageLoadEvent.ON_LOAD_END)
+ );
+ }
+ };
+ }
+
+ mIsDirty = true;
+ }
+
public void setBorderColor(int borderColor) {
mBorderColor = borderColor;
mIsDirty = true;
@@ -217,21 +268,33 @@ public class ReactImageView extends GenericDraweeView {
.setProgressiveRenderingEnabled(mProgressiveRenderingEnabled)
.build();
- DraweeController draweeController = mDraweeControllerBuilder
- .reset()
+ // This builder is reused
+ mDraweeControllerBuilder.reset();
+
+ mDraweeControllerBuilder
.setAutoPlayAnimations(true)
.setCallerContext(mCallerContext)
.setOldController(getController())
- .setImageRequest(imageRequest)
- .setControllerListener(mControllerListener)
- .build();
- setController(draweeController);
+ .setImageRequest(imageRequest);
+
+ if (mControllerListener != null && mControllerForTesting != null) {
+ ForwardingControllerListener combinedListener = new ForwardingControllerListener();
+ combinedListener.addListener(mControllerListener);
+ combinedListener.addListener(mControllerForTesting);
+ mDraweeControllerBuilder.setControllerListener(combinedListener);
+ } else if (mControllerForTesting != null) {
+ mDraweeControllerBuilder.setControllerListener(mControllerForTesting);
+ } else if (mControllerListener != null) {
+ mDraweeControllerBuilder.setControllerListener(mControllerListener);
+ }
+
+ setController(mDraweeControllerBuilder.build());
mIsDirty = false;
}
// VisibleForTesting
public void setControllerListener(ControllerListener controllerListener) {
- mControllerListener = controllerListener;
+ mControllerForTesting = controllerListener;
mIsDirty = true;
maybeUpdateView();
}