diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js
index 0c374b19e..905c17f4c 100644
--- a/Examples/UIExplorer/ImageExample.js
+++ b/Examples/UIExplorer/ImageExample.js
@@ -156,6 +156,63 @@ var ImageSizeExample = React.createClass({
},
});
+var MultipleSourcesExample = React.createClass({
+ getInitialState: function() {
+ return {
+ width: 30,
+ height: 30,
+ };
+ },
+ render: function() {
+ return (
+
+
+
+ Decrease image size
+
+
+ Increase image size
+
+
+ Container image size: {this.state.width}x{this.state.height}
+
+
+
+
+ );
+ },
+ increaseImageSize: function() {
+ if (this.state.width >= 100) {
+ return;
+ }
+ this.setState({
+ width: this.state.width + 10,
+ height: this.state.height + 10,
+ });
+ },
+ decreaseImageSize: function() {
+ if (this.state.width <= 10) {
+ return;
+ }
+ this.setState({
+ width: this.state.width - 10,
+ height: this.state.height - 10,
+ });
+ },
+});
+
exports.displayName = (undefined: ?string);
exports.framework = 'React';
exports.title = '';
@@ -510,6 +567,16 @@ exports.examples = [
return ;
},
},
+ {
+ title: 'MultipleSourcesExample',
+ description:
+ 'The `source` prop allows passing in an array of uris, so that native to choose which image ' +
+ 'to diplay based on the size of the of the target image',
+ render: function() {
+ return ;
+ },
+ platform: 'android',
+ },
];
var fullImage = {uri: 'http://facebook.github.io/react/img/logo_og.png'};
@@ -567,4 +634,8 @@ var styles = StyleSheet.create({
height: 50,
resizeMode: 'contain',
},
+ touchableText: {
+ fontWeight: '500',
+ color: 'blue',
+ },
});
diff --git a/Libraries/Image/Image.android.js b/Libraries/Image/Image.android.js
index 91dee727a..dc267f8fa 100644
--- a/Libraries/Image/Image.android.js
+++ b/Libraries/Image/Image.android.js
@@ -71,6 +71,9 @@ var Image = React.createClass({
* `uri` is a string representing the resource identifier for the image, which
* could be an http address, a local file path, or a static image
* resource (which should be wrapped in the `require('./path/to/image.png')` function).
+ * This prop can also contain several remote `uri`, specified together with
+ * their width and height. The native side will then choose the best `uri` to display
+ * based on the measured size of the image container.
*/
source: PropTypes.oneOfType([
PropTypes.shape({
@@ -78,6 +81,13 @@ var Image = React.createClass({
}),
// Opaque type returned by require('./image.jpg')
PropTypes.number,
+ // Multiple sources
+ PropTypes.arrayOf(
+ PropTypes.shape({
+ uri: PropTypes.string,
+ width: PropTypes.number,
+ height: PropTypes.number,
+ }))
]),
/**
* similarly to `source`, this property represents the resource used to render
@@ -176,11 +186,11 @@ var Image = React.createClass({
},
render: function() {
- var source = resolveAssetSource(this.props.source);
- var loadingIndicatorSource = resolveAssetSource(this.props.loadingIndicatorSource);
+ const source = resolveAssetSource(this.props.source);
+ const loadingIndicatorSource = resolveAssetSource(this.props.loadingIndicatorSource);
- // As opposed to the ios version, here it render `null`
- // when no source or source.uri... so let's not break that.
+ // As opposed to the ios version, here we render `null` when there is no source, source.uri
+ // or source array.
if (source && source.uri === '') {
console.warn('source.uri should not be an empty string');
@@ -190,21 +200,29 @@ var Image = React.createClass({
console.warn('The component requires a `source` property rather than `src`.');
}
- if (source && source.uri) {
- var {width, height} = source;
- var style = flattenStyle([{width, height}, styles.base, this.props.style]);
- var {onLoadStart, onLoad, onLoadEnd} = this.props;
+ if (source && (source.uri || Array.isArray(source))) {
+ let style;
+ let sources;
+ if (source.uri) {
+ const {width, height} = source;
+ style = flattenStyle([{width, height}, styles.base, this.props.style]);
+ sources = [{uri: source.uri}];
+ } else {
+ style = flattenStyle([styles.base, this.props.style]);
+ sources = source;
+ }
- var nativeProps = merge(this.props, {
+ const {onLoadStart, onLoad, onLoadEnd} = this.props;
+ const nativeProps = merge(this.props, {
style,
shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd),
- src: source.uri,
+ src: sources,
loadingIndicatorSrc: loadingIndicatorSource ? loadingIndicatorSource.uri : null,
});
if (nativeProps.children) {
// TODO(6033040): Consider implementing this as a separate native component
- var imageProps = merge(nativeProps, {
+ const imageProps = merge(nativeProps, {
style: styles.absoluteImage,
children: undefined,
});
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 fbd98355c..518ea9e0e 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
@@ -19,13 +19,14 @@ import android.graphics.PorterDuff.Mode;
import com.facebook.csslayout.CSSConstants;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
+import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.common.MapBuilder;
-import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.PixelUtil;
-import com.facebook.react.uimanager.annotations.ReactPropGroup;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewProps;
+import com.facebook.react.uimanager.annotations.ReactProp;
+import com.facebook.react.uimanager.annotations.ReactPropGroup;
public class ReactImageManager extends SimpleViewManager {
@@ -75,10 +76,10 @@ public class ReactImageManager extends SimpleViewManager {
mResourceDrawableIdHelper);
}
- // In JS this is Image.props.source.uri
+ // In JS this is Image.props.source
@ReactProp(name = "src")
- public void setSource(ReactImageView view, @Nullable String source) {
- view.setSource(source);
+ public void setSource(ReactImageView view, @Nullable ReadableArray sources) {
+ view.setSource(sources);
}
// In JS this is Image.props.loadingIndicatorSource.uri
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 d7d093868..7d029f0e4 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
@@ -12,6 +12,8 @@ package com.facebook.react.views.image;
import javax.annotation.Nullable;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
import android.content.Context;
import android.graphics.Bitmap;
@@ -48,6 +50,8 @@ 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.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.SystemClock;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.UIManagerModule;
@@ -136,6 +140,7 @@ public class ReactImageView extends GenericDraweeView {
}
private final ResourceDrawableIdHelper mResourceDrawableIdHelper;
+ private final Map mSources;
private @Nullable Uri mUri;
private @Nullable Drawable mLoadingImageDrawable;
@@ -173,6 +178,7 @@ public class ReactImageView extends GenericDraweeView {
mRoundedCornerPostprocessor = new RoundedCornerPostprocessor();
mCallerContext = callerContext;
mResourceDrawableIdHelper = resourceDrawableIdHelper;
+ mSources = new HashMap<>();
}
public void setShouldNotifyLoadEvents(boolean shouldNotify) {
@@ -259,23 +265,19 @@ public class ReactImageView extends GenericDraweeView {
mIsDirty = true;
}
- public void setSource(@Nullable String source) {
- mUri = null;
- if (source != null) {
- try {
- mUri = Uri.parse(source);
- // Verify scheme is set, so that relative uri (used by static resources) are not handled.
- if (mUri.getScheme() == null) {
- mUri = null;
- }
- } catch (Exception e) {
- // ignore malformed uri, then attempt to extract resource ID.
- }
- if (mUri == null) {
- mUri = mResourceDrawableIdHelper.getResourceDrawableUri(getContext(), source);
- mIsLocalImage = true;
+ public void setSource(@Nullable ReadableArray sources) {
+ mSources.clear();
+ if (sources != null && sources.size() != 0) {
+ // Optimize for the case where we have just one uri, case in which we don't need the sizes
+ if (sources.size() == 1) {
+ mSources.put(sources.getMap(0).getString("uri"), 0.0);
} else {
- mIsLocalImage = false;
+ for (int idx = 0; idx < sources.size(); idx++) {
+ ReadableMap source = sources.getMap(idx);
+ mSources.put(
+ source.getString("uri"),
+ source.getDouble("width") * source.getDouble("height"));
+ }
}
}
mIsDirty = true;
@@ -312,6 +314,16 @@ public class ReactImageView extends GenericDraweeView {
return;
}
+ if (hasMultipleSources() && (getWidth() <= 0 || getHeight() <= 0)) {
+ // If we need to choose from multiple uris but the size is not yet set, wait for layout pass
+ return;
+ }
+
+ computeSourceUri();
+ if (mUri == null) {
+ return;
+ }
+
boolean doResize = shouldResize(mUri);
if (doResize && (getWidth() <= 0 || getHeight() <= 0)) {
// If need a resize and the size is not yet set, wait until the layout pass provides one
@@ -398,6 +410,7 @@ public class ReactImageView extends GenericDraweeView {
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w > 0 && h > 0) {
+ mIsDirty = mIsDirty || hasMultipleSources();
maybeUpdateView();
}
}
@@ -410,10 +423,65 @@ public class ReactImageView extends GenericDraweeView {
return false;
}
- private static boolean shouldResize(@Nullable Uri uri) {
+ private boolean hasMultipleSources() {
+ return mSources.size() > 1;
+ }
+
+ private void computeSourceUri() {
+ mUri = null;
+ if (mSources.isEmpty()) {
+ return;
+ }
+ if (hasMultipleSources()) {
+ setUriFromMultipleSources();
+ return;
+ }
+
+ final String singleSource = mSources.keySet().iterator().next();
+ setUriFromSingleSource(singleSource);
+ }
+
+ private void setUriFromSingleSource(String source) {
+ try {
+ mUri = Uri.parse(source);
+ // Verify scheme is set, so that relative uri (used by static resources) are not handled.
+ if (mUri.getScheme() == null) {
+ mUri = null;
+ }
+ } catch (Exception e) {
+ // ignore malformed uri, then attempt to extract resource ID.
+ }
+ if (mUri == null) {
+ mUri = mResourceDrawableIdHelper.getResourceDrawableUri(getContext(), source);
+ mIsLocalImage = true;
+ } else {
+ mIsLocalImage = false;
+ }
+ }
+
+ /**
+ * Chooses the uri with the size closest to the target image size. Must be called only after the
+ * layout pass when the sizes of the target image have been computed, and when there are at least
+ * two sources to choose from.
+ */
+ private void setUriFromMultipleSources() {
+ final double targetImageSize = getWidth() * getHeight();
+ double bestPrecision = Double.MAX_VALUE;
+ String bestUri = null;
+ for (Map.Entry source : mSources.entrySet()) {
+ final double precision = Math.abs(1.0 - (source.getValue()) / targetImageSize);
+ if (precision < bestPrecision) {
+ bestPrecision = precision;
+ bestUri = source.getKey();
+ }
+ }
+ setUriFromSingleSource(bestUri);
+ }
+
+ private static boolean shouldResize(Uri uri) {
// Resizing is inferior to scaling. See http://frescolib.org/docs/resizing-rotating.html#_
// We resize here only for images likely to be from the device's camera, where the app developer
// has no control over the original size
- return uri != null && (UriUtil.isLocalContentUri(uri) || UriUtil.isLocalFileUri(uri));
+ return UriUtil.isLocalContentUri(uri) || UriUtil.isLocalFileUri(uri);
}
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java
index b1d3c012a..21e6f2384 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java
@@ -20,6 +20,7 @@ import android.net.Uri;
import com.facebook.common.util.UriUtil;
import com.facebook.csslayout.CSSNode;
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
+import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.views.text.ReactTextInlineImageShadowNode;
import com.facebook.react.views.text.TextInlineImageSpan;
@@ -43,7 +44,9 @@ public class FrescoBasedReactTextInlineImageShadowNode extends ReactTextInlineIm
}
@ReactProp(name = "src")
- public void setSource(@Nullable String source) {
+ public void setSource(@Nullable ReadableArray sources) {
+ final String source =
+ (sources == null || sources.size() == 0) ? null : sources.getMap(0).getString("uri");
Uri uri = null;
if (source != null) {
try {
diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.java
index 186e4dca4..0e051ab79 100644
--- a/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.java
+++ b/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.java
@@ -14,6 +14,7 @@ import android.util.DisplayMetrics;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.react.bridge.CatalystInstance;
+import com.facebook.react.bridge.JavaOnlyArray;
import com.facebook.react.bridge.ReactTestHelper;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReactApplicationContext;
@@ -82,7 +83,9 @@ public class ReactImagePropertyTest {
public void testBorderColor() {
ReactImageManager viewManager = new ReactImageManager();
ReactImageView view = viewManager.createViewInstance(mThemeContext);
- viewManager.updateProperties(view, buildStyles("src", "http://mysite.com/mypic.jpg"));
+ viewManager.updateProperties(
+ view,
+ buildStyles("src", JavaOnlyArray.of(JavaOnlyMap.of("uri", "http://mysite.com/mypic.jpg"))));
viewManager.updateProperties(view, buildStyles("borderColor", Color.argb(0, 0, 255, 255)));
int borderColor = view.getHierarchy().getRoundingParams().getBorderColor();
@@ -110,7 +113,9 @@ public class ReactImagePropertyTest {
public void testRoundedCorners() {
ReactImageManager viewManager = new ReactImageManager();
ReactImageView view = viewManager.createViewInstance(mThemeContext);
- viewManager.updateProperties(view, buildStyles("src", "http://mysite.com/mypic.jpg"));
+ viewManager.updateProperties(
+ view,
+ buildStyles("src", JavaOnlyArray.of(JavaOnlyMap.of("uri", "http://mysite.com/mypic.jpg"))));
// We can't easily verify if rounded corner was honored or not, this tests simply verifies
// we're not crashing..