Support Image resizeMode=repeat on Android
Summary: `<Image resizeMode="repeat" />` for Android, matching the iOS implementation (#7968). (Non-goal: changing the component's API for finer-grained control / feature parity with CSS - this would be nice in the future) As requested in e.g. #14158. Given https://github.com/facebook/fresco/issues/1575, and lacking the context to follow the specific recommendations in https://github.com/facebook/fresco/issues/1575#issuecomment-267004303, I've opted for a minimal change within RN itself. It's likely that performance can be improved by offloading this work to Fresco in some clever way; but I'm assuming that the present naive approach is still an improvement over a userland implementation with `onLayout` and multiple `<Image>` instances. - Picking up on a TODO note in the existing code, I implemented `MultiPostprocessor` to allow arbitrary chaining of Fresco-compatible postprocessors inside `ReactImageView`. - Rather than extensively refactor `ImageResizeMode`, `ReactImageManager` and `ReactImageView`, I mostly preserved the existing API that maps `resizeMode` values to [`ScaleType`](http://frescolib.org/javadoc/reference/com/facebook/drawee/drawable/ScalingUtils.ScaleType.html) instances, and simply added a second mapping, to [`TileMode`](https://developer.android.com/reference/android/graphics/Shader.TileMode.html). - To match the iOS rendering exactly for oversized images, I found that scaling with a custom `ScaleType` was required - a kind of combination of `CENTER_INSIDE` and `FIT_START` which Fresco doesn't provide - so I implemented that as `ScaleTypeStartInside`. (This is, frankly, questionable as the default behaviour on iOS to begin with - but I am aiming for parity here) - `resizeMode="repeat"` is therefore unpacked by the view manager to the effect of: ```js view.setScaleType(ScaleTypeStartInside.INSTANCE); view.setTileMode(Shader.TileMode.REPEAT); ``` And the added postprocessing in the view (in case of a non-`CLAMP` tile mode) consists of waiting for layout, allocating a destination bitmap and painting the source bitmap with the requested tile mode and scale type. Note that as in https://github.com/facebook/react-native/pull/17398#issue-285235247, I have neither updated nor tested the "Flat" UI implementation - everything compiles but I've taken [this comment](https://github.com/facebook/react-native/issues/12770#issuecomment-294052694) to mean there's no point in trying to wade through it on my own right now; I'm happy to tackle it if given some pointers. Also, I'm happy to address any code style issues or other feedback; I'm new to this codebase and a very infrequent Android/Java coder. Tested by enabling the relevant case in RNTester on Android. | iOS | Android | |-|-| | <img src=https://user-images.githubusercontent.com/2246565/34461897-4e12008e-ee2f-11e7-8581-1dc0cc8f2779.png width=300>| <img src=https://user-images.githubusercontent.com/2246565/34461894-40b2c8ec-ee2f-11e7-8a8f-96704f3c8caa.png width=300> | Docs update: https://github.com/facebook/react-native-website/pull/106 [ANDROID] [FEATURE] [Image] - Implement resizeMode=repeat Closes https://github.com/facebook/react-native/pull/17404 Reviewed By: achen1 Differential Revision: D7070329 Pulled By: mdvacca fbshipit-source-id: 6a72fcbdcc7c7c2daf293dc1d8b6728f54ad0249
This commit is contained in:
parent
1dde989919
commit
0459e4ffaa
|
@ -119,7 +119,7 @@ var Image = createReactClass({
|
|||
*
|
||||
* See https://facebook.github.io/react-native/docs/image.html#resizemode
|
||||
*/
|
||||
resizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch', 'center']),
|
||||
resizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch', 'repeat', 'center']),
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
|
|
@ -558,18 +558,16 @@ exports.examples = [
|
|||
source={image}
|
||||
/>
|
||||
</View>
|
||||
{ Platform.OS === 'ios' ?
|
||||
<View style={styles.leftMargin}>
|
||||
<Text style={[styles.resizeModeText]}>
|
||||
Repeat
|
||||
</Text>
|
||||
<Image
|
||||
style={styles.resizeMode}
|
||||
resizeMode={Image.resizeMode.repeat}
|
||||
source={image}
|
||||
/>
|
||||
</View>
|
||||
: null }
|
||||
<View style={styles.leftMargin}>
|
||||
<Text style={[styles.resizeModeText]}>
|
||||
Repeat
|
||||
</Text>
|
||||
<Image
|
||||
style={styles.resizeMode}
|
||||
resizeMode={Image.resizeMode.repeat}
|
||||
source={image}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.leftMargin}>
|
||||
<Text style={[styles.resizeModeText]}>
|
||||
Center
|
||||
|
|
|
@ -9,6 +9,7 @@ package com.facebook.react.views.image;
|
|||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import android.graphics.Shader;
|
||||
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
|
||||
import com.facebook.drawee.drawable.ScalingUtils;
|
||||
|
||||
|
@ -34,6 +35,10 @@ public class ImageResizeMode {
|
|||
if ("center".equals(resizeModeValue)) {
|
||||
return ScalingUtils.ScaleType.CENTER_INSIDE;
|
||||
}
|
||||
if ("repeat".equals(resizeModeValue)) {
|
||||
// Handled via a combination of ScaleType and TileMode
|
||||
return ScaleTypeStartInside.INSTANCE;
|
||||
}
|
||||
if (resizeModeValue == null) {
|
||||
// Use the default. Never use null.
|
||||
return defaultValue();
|
||||
|
@ -42,6 +47,29 @@ public class ImageResizeMode {
|
|||
"Invalid resize mode: '" + resizeModeValue + "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts JS resize modes into {@code Shader.TileMode}.
|
||||
* See {@code ImageResizeMode.js}.
|
||||
*/
|
||||
public static Shader.TileMode toTileMode(@Nullable String resizeModeValue) {
|
||||
if ("contain".equals(resizeModeValue)
|
||||
|| "cover".equals(resizeModeValue)
|
||||
|| "stretch".equals(resizeModeValue)
|
||||
|| "center".equals(resizeModeValue)) {
|
||||
return Shader.TileMode.CLAMP;
|
||||
}
|
||||
if ("repeat".equals(resizeModeValue)) {
|
||||
// Handled via a combination of ScaleType and TileMode
|
||||
return Shader.TileMode.REPEAT;
|
||||
}
|
||||
if (resizeModeValue == null) {
|
||||
// Use the default. Never use null.
|
||||
return defaultTileMode();
|
||||
}
|
||||
throw new JSApplicationIllegalArgumentException(
|
||||
"Invalid resize mode: '" + resizeModeValue + "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the default as per web and iOS.
|
||||
* We want to be consistent across platforms.
|
||||
|
@ -49,4 +77,8 @@ public class ImageResizeMode {
|
|||
public static ScalingUtils.ScaleType defaultValue() {
|
||||
return ScalingUtils.ScaleType.CENTER_CROP;
|
||||
}
|
||||
|
||||
public static Shader.TileMode defaultTileMode() {
|
||||
return Shader.TileMode.CLAMP;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Copyright (c) 2017-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.graphics.Bitmap;
|
||||
|
||||
import com.facebook.cache.common.CacheKey;
|
||||
import com.facebook.cache.common.MultiCacheKey;
|
||||
import com.facebook.common.references.CloseableReference;
|
||||
import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory;
|
||||
import com.facebook.imagepipeline.request.Postprocessor;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class MultiPostprocessor implements Postprocessor {
|
||||
private final List<Postprocessor> mPostprocessors;
|
||||
|
||||
public static Postprocessor from(List<Postprocessor> postprocessors) {
|
||||
switch (postprocessors.size()) {
|
||||
case 0:
|
||||
return null;
|
||||
case 1:
|
||||
return postprocessors.get(0);
|
||||
default:
|
||||
return new MultiPostprocessor(postprocessors);
|
||||
}
|
||||
}
|
||||
|
||||
private MultiPostprocessor(List<Postprocessor> postprocessors) {
|
||||
mPostprocessors = new LinkedList<>(postprocessors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName () {
|
||||
StringBuilder name = new StringBuilder();
|
||||
for (Postprocessor p: mPostprocessors) {
|
||||
if (name.length() > 0) {
|
||||
name.append(",");
|
||||
}
|
||||
name.append(p.getName());
|
||||
}
|
||||
name.insert(0, "MultiPostProcessor (");
|
||||
name.append(")");
|
||||
return name.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheKey getPostprocessorCacheKey () {
|
||||
LinkedList<CacheKey> keys = new LinkedList<>();
|
||||
for (Postprocessor p: mPostprocessors) {
|
||||
keys.push(p.getPostprocessorCacheKey());
|
||||
}
|
||||
return new MultiCacheKey(keys);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CloseableReference<Bitmap> process(Bitmap sourceBitmap, PlatformBitmapFactory bitmapFactory) {
|
||||
CloseableReference<Bitmap> prevBitmap = null, nextBitmap = null;
|
||||
|
||||
try {
|
||||
for (Postprocessor p : mPostprocessors) {
|
||||
nextBitmap = p.process(prevBitmap != null ? prevBitmap.get() : sourceBitmap, bitmapFactory);
|
||||
CloseableReference.closeSafely(prevBitmap);
|
||||
prevBitmap = nextBitmap.clone();
|
||||
}
|
||||
return nextBitmap.clone();
|
||||
} finally {
|
||||
CloseableReference.closeSafely(nextBitmap);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -139,6 +139,7 @@ public class ReactImageManager extends SimpleViewManager<ReactImageView> {
|
|||
@ReactProp(name = ViewProps.RESIZE_MODE)
|
||||
public void setResizeMode(ReactImageView view, @Nullable String resizeMode) {
|
||||
view.setScaleType(ImageResizeMode.toScaleType(resizeMode));
|
||||
view.setTileMode(ImageResizeMode.toTileMode(resizeMode));
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.RESIZE_METHOD)
|
||||
|
|
|
@ -22,6 +22,7 @@ import android.graphics.drawable.Animatable;
|
|||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
import com.facebook.common.references.CloseableReference;
|
||||
import com.facebook.common.util.UriUtil;
|
||||
import com.facebook.drawee.controller.AbstractDraweeControllerBuilder;
|
||||
import com.facebook.drawee.controller.BaseControllerListener;
|
||||
|
@ -33,6 +34,7 @@ import com.facebook.drawee.generic.GenericDraweeHierarchy;
|
|||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
||||
import com.facebook.drawee.generic.RoundingParams;
|
||||
import com.facebook.drawee.view.GenericDraweeView;
|
||||
import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory;
|
||||
import com.facebook.imagepipeline.common.ResizeOptions;
|
||||
import com.facebook.imagepipeline.image.ImageInfo;
|
||||
import com.facebook.imagepipeline.postprocessors.IterativeBoxBlurPostProcessor;
|
||||
|
@ -49,6 +51,7 @@ import com.facebook.react.uimanager.FloatUtil;
|
|||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
import com.facebook.react.uimanager.events.EventDispatcher;
|
||||
import com.facebook.react.views.image.ImageResizeMode;
|
||||
import com.facebook.react.views.imagehelper.ImageSource;
|
||||
import com.facebook.react.views.imagehelper.MultiSourceHelper;
|
||||
import com.facebook.react.views.imagehelper.MultiSourceHelper.MultiSourceResult;
|
||||
|
@ -141,6 +144,40 @@ public class ReactImageView extends GenericDraweeView {
|
|||
}
|
||||
}
|
||||
|
||||
// Fresco lacks support for repeating images, see https://github.com/facebook/fresco/issues/1575
|
||||
// We implement it here as a postprocessing step.
|
||||
private static final Matrix sTileMatrix = new Matrix();
|
||||
|
||||
private class TilePostprocessor extends BasePostprocessor {
|
||||
@Override
|
||||
public CloseableReference<Bitmap> process(Bitmap source, PlatformBitmapFactory bitmapFactory) {
|
||||
final Rect destRect = new Rect(0, 0, getWidth(), getHeight());
|
||||
|
||||
mScaleType.getTransform(
|
||||
sTileMatrix,
|
||||
destRect,
|
||||
source.getWidth(),
|
||||
source.getHeight(),
|
||||
0.0f,
|
||||
0.0f);
|
||||
|
||||
Paint paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
Shader shader = new BitmapShader(source, mTileMode, mTileMode);
|
||||
shader.setLocalMatrix(sTileMatrix);
|
||||
paint.setShader(shader);
|
||||
|
||||
CloseableReference<Bitmap> output = bitmapFactory.createBitmap(getWidth(), getHeight());
|
||||
try {
|
||||
Canvas canvas = new Canvas(output.get());
|
||||
canvas.drawRect(destRect, paint);
|
||||
return output.clone();
|
||||
} finally {
|
||||
CloseableReference.closeSafely(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final List<ImageSource> mSources;
|
||||
|
||||
private @Nullable ImageSource mImageSource;
|
||||
|
@ -152,9 +189,11 @@ public class ReactImageView extends GenericDraweeView {
|
|||
private float mBorderRadius = YogaConstants.UNDEFINED;
|
||||
private @Nullable float[] mBorderCornerRadii;
|
||||
private ScalingUtils.ScaleType mScaleType;
|
||||
private Shader.TileMode mTileMode = ImageResizeMode.defaultTileMode();
|
||||
private boolean mIsDirty;
|
||||
private final AbstractDraweeControllerBuilder mDraweeControllerBuilder;
|
||||
private final RoundedCornerPostprocessor mRoundedCornerPostprocessor;
|
||||
private final TilePostprocessor mTilePostprocessor;
|
||||
private @Nullable IterativeBoxBlurPostProcessor mIterativeBoxBlurPostProcessor;
|
||||
private @Nullable ControllerListener mControllerListener;
|
||||
private @Nullable ControllerListener mControllerForTesting;
|
||||
|
@ -180,6 +219,7 @@ public class ReactImageView extends GenericDraweeView {
|
|||
mScaleType = ImageResizeMode.defaultValue();
|
||||
mDraweeControllerBuilder = draweeControllerBuilder;
|
||||
mRoundedCornerPostprocessor = new RoundedCornerPostprocessor();
|
||||
mTilePostprocessor = new TilePostprocessor();
|
||||
mGlobalImageLoadListener = globalImageLoadListener;
|
||||
mCallerContext = callerContext;
|
||||
mSources = new LinkedList<>();
|
||||
|
@ -275,6 +315,11 @@ public class ReactImageView extends GenericDraweeView {
|
|||
mIsDirty = true;
|
||||
}
|
||||
|
||||
public void setTileMode(Shader.TileMode tileMode) {
|
||||
mTileMode = tileMode;
|
||||
mIsDirty = true;
|
||||
}
|
||||
|
||||
public void setResizeMethod(ImageResizeMethod resizeMethod) {
|
||||
mResizeMethod = resizeMethod;
|
||||
mIsDirty = true;
|
||||
|
@ -362,6 +407,11 @@ public class ReactImageView extends GenericDraweeView {
|
|||
return;
|
||||
}
|
||||
|
||||
if (isTiled() && (getWidth() <= 0 || getHeight() <= 0)) {
|
||||
// If need to tile and the size is not yet set, wait until the layout pass provides one
|
||||
return;
|
||||
}
|
||||
|
||||
GenericDraweeHierarchy hierarchy = getHierarchy();
|
||||
hierarchy.setActualImageScaleType(mScaleType);
|
||||
|
||||
|
@ -396,13 +446,17 @@ public class ReactImageView extends GenericDraweeView {
|
|||
? mFadeDurationMs
|
||||
: mImageSource.isResource() ? 0 : REMOTE_IMAGE_FADE_DURATION_MS);
|
||||
|
||||
// TODO: t13601664 Support multiple PostProcessors
|
||||
Postprocessor postprocessor = null;
|
||||
List<Postprocessor> postprocessors = new LinkedList<>();
|
||||
if (usePostprocessorScaling) {
|
||||
postprocessor = mRoundedCornerPostprocessor;
|
||||
} else if (mIterativeBoxBlurPostProcessor != null) {
|
||||
postprocessor = mIterativeBoxBlurPostProcessor;
|
||||
postprocessors.add(mRoundedCornerPostprocessor);
|
||||
}
|
||||
if (mIterativeBoxBlurPostProcessor != null) {
|
||||
postprocessors.add(mIterativeBoxBlurPostProcessor);
|
||||
}
|
||||
if (isTiled()) {
|
||||
postprocessors.add(mTilePostprocessor);
|
||||
}
|
||||
Postprocessor postprocessor = MultiPostprocessor.from(postprocessors);
|
||||
|
||||
ResizeOptions resizeOptions = doResize ? new ResizeOptions(getWidth(), getHeight()) : null;
|
||||
|
||||
|
@ -468,7 +522,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();
|
||||
mIsDirty = mIsDirty || hasMultipleSources() || isTiled();
|
||||
maybeUpdateView();
|
||||
}
|
||||
}
|
||||
|
@ -485,6 +539,10 @@ public class ReactImageView extends GenericDraweeView {
|
|||
return mSources.size() > 1;
|
||||
}
|
||||
|
||||
private boolean isTiled() {
|
||||
return mTileMode != Shader.TileMode.CLAMP;
|
||||
}
|
||||
|
||||
private void setSourceImage() {
|
||||
mImageSource = null;
|
||||
if (mSources.isEmpty()) {
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Copyright (c) 2017-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.graphics.Matrix;
|
||||
import android.graphics.Rect;
|
||||
import com.facebook.drawee.drawable.ScalingUtils;
|
||||
|
||||
public class ScaleTypeStartInside extends ScalingUtils.AbstractScaleType {
|
||||
public static final ScalingUtils.ScaleType INSTANCE = new ScaleTypeStartInside();
|
||||
|
||||
@Override
|
||||
public void getTransformImpl(
|
||||
Matrix outTransform,
|
||||
Rect parentRect,
|
||||
int childWidth,
|
||||
int childHeight,
|
||||
float focusX,
|
||||
float focusY,
|
||||
float scaleX,
|
||||
float scaleY) {
|
||||
float scale = Math.min(Math.min(scaleX, scaleY), 1.0f);
|
||||
float dx = parentRect.left;
|
||||
float dy = parentRect.top;
|
||||
outTransform.setScale(scale, scale);
|
||||
outTransform.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "start_inside";
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue