Support multi sources for images

Summary:
This adds support for specifying multiple sources for an image component, so that native can choose the best one based on the flexbox-computed size of the image.
The API is as follows: the image component receives in the `source` prop an array of objects of the type `{uri, width, height}`. On the native side, the native component will wait for the layout pass to receive the width and height of the image, and then parse the array to find the best fitting one. For now, this does not support local resources, but it will be added soon.
To see how this works and play with it, there's an example called `MultipleSourcesExample` under `ImageExample` In UIExplorer.

Reviewed By: foghina

Differential Revision: D3364550

fbshipit-source-id: 66c5aeb2794f2ffeff8da39a9c0b95155fb2d41f
This commit is contained in:
Andrei Coman 2016-06-13 14:04:19 -07:00 committed by Facebook Github Bot 0
parent 9443bc5c3f
commit 617a38d984
6 changed files with 203 additions and 37 deletions

View File

@ -156,6 +156,63 @@ var ImageSizeExample = React.createClass({
},
});
var MultipleSourcesExample = React.createClass({
getInitialState: function() {
return {
width: 30,
height: 30,
};
},
render: function() {
return (
<View style={styles.container}>
<View style={{flexDirection: 'row', justifyContent: 'space-between'}}>
<Text
style={styles.touchableText}
onPress={this.decreaseImageSize} >
Decrease image size
</Text>
<Text
style={styles.touchableText}
onPress={this.increaseImageSize} >
Increase image size
</Text>
</View>
<Text>Container image size: {this.state.width}x{this.state.height} </Text>
<View
style={[styles.imageContainer, {height: this.state.height, width: this.state.width}]} >
<Image
style={{flex: 1}}
source={[
{uri: 'http://facebook.github.io/react/img/logo_small.png', width: 38, height: 38},
{uri: 'http://facebook.github.io/react/img/logo_small_2x.png', width: 76, height: 76},
{uri: 'http://facebook.github.io/react/img/logo_og.png', width: 400, height: 400}
]}
/>
</View>
</View>
);
},
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 = '<Image>';
@ -510,6 +567,16 @@ exports.examples = [
return <ImageSizeExample source={fullImage} />;
},
},
{
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 <MultipleSourcesExample />;
},
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',
},
});

View File

@ -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 <Image> 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,
});

View File

@ -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<ReactImageView> {
@ -75,10 +76,10 @@ public class ReactImageManager extends SimpleViewManager<ReactImageView> {
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

View File

@ -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<String, Double> 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<String, Double> 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);
}
}

View File

@ -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 {

View File

@ -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..