Add onLoadX support on Android

Summary: ~~This is a WIP, just finished the first bit and wanted to get some feedback to see if this approach seems appropriate, as I haven't done a lot of Android development.~~

Looks ready for review now.
Closes https://github.com/facebook/react-native/pull/3791

Reviewed By: svcscm

Differential Revision: D2672262

Pulled By: mkonicek

fb-gh-sync-id: 1e8f1cc6658fb719a68f7da455f30a7c9b1db730
This commit is contained in:
Brent Vatne 2015-11-25 17:06:59 -08:00 committed by facebook-github-bot-5
parent b65f1f2234
commit ae09a10c95
6 changed files with 226 additions and 12 deletions

View File

@ -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 (
<View>
<Image
source={this.props.source}
style={[styles.base, {overflow: 'visible'}]}
onLoadStart={() => this._loadEventFired(`✔ onLoadStart (+${new Date() - mountTime}ms)`)}
onLoad={() => this._loadEventFired(`✔ onLoad (+${new Date() - mountTime}ms)`)}
onLoadEnd={() => this._loadEventFired(`✔ onLoadEnd (+${new Date() - mountTime}ms)`)}
/>
<Text style={{marginTop: 20}}>
{this.state.events.join('\n')}
</Text>
</View>
);
},
_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 (
<NetworkImageCallbackExample source={{uri: 'http://facebook.github.io/origami/public/images/blog-hero.jpg?r=1'}}/>
);
},
},
{
title: 'Error Handler',
render: function() {

View File

@ -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);

View File

@ -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,
},

View File

@ -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<ImageLoadEvent> {
@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);
}
}

View File

@ -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<ReactImageView> {
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);

View File

@ -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<ImageInfo>() {
@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();
}