Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Image resizeMode=repeat on Android #17404

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Libraries/Image/Image.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
22 changes: 10 additions & 12 deletions RNTester/js/ImageExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import javax.annotation.Nullable;

import android.graphics.Shader;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.drawee.drawable.ScalingUtils;

Expand All @@ -34,6 +35,10 @@ public static ScalingUtils.ScaleType toScaleType(@Nullable String resizeModeValu
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();
Expand All @@ -42,11 +47,38 @@ public static ScalingUtils.ScaleType toScaleType(@Nullable String resizeModeValu
"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.
*/
public static ScalingUtils.ScaleType defaultValue() {
return ScalingUtils.ScaleType.CENTER_CROP;
}

public static Shader.TileMode defaultTileMode() {
return Shader.TileMode.CLAMP;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ public void setBorderRadius(ReactImageView view, int index, float borderRadius)
@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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
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;
Expand All @@ -33,6 +34,7 @@
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;
Expand All @@ -49,6 +51,7 @@
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;
Expand Down Expand Up @@ -141,6 +144,40 @@ public void process(Bitmap output, Bitmap source) {
}
}

// 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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: do you need to clone the output?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This clones just the reference (a cheap operation), unless I'm missing something? So to gracefully handle any errors between allocating and returning the bitmap (e.g. I don't know if new Canvas() might throw, or if more code might be introduced in between), I treat output as a local reference which gets released within process(), and explicitly make a new reference to return (keeping the Bitmap alive) right before releasing it.

} finally {
CloseableReference.closeSafely(output);
}
}
}

private final List<ImageSource> mSources;

private @Nullable ImageSource mImageSource;
Expand All @@ -152,9 +189,11 @@ public void process(Bitmap output, Bitmap source) {
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;
Expand All @@ -180,6 +219,7 @@ public ReactImageView(
mScaleType = ImageResizeMode.defaultValue();
mDraweeControllerBuilder = draweeControllerBuilder;
mRoundedCornerPostprocessor = new RoundedCornerPostprocessor();
mTilePostprocessor = new TilePostprocessor();
mGlobalImageLoadListener = globalImageLoadListener;
mCallerContext = callerContext;
mSources = new LinkedList<>();
Expand Down Expand Up @@ -275,6 +315,11 @@ public void setScaleType(ScalingUtils.ScaleType scaleType) {
mIsDirty = true;
}

public void setTileMode(Shader.TileMode tileMode) {
mTileMode = tileMode;
mIsDirty = true;
}

public void setResizeMethod(ImageResizeMethod resizeMethod) {
mResizeMethod = resizeMethod;
mIsDirty = true;
Expand Down Expand Up @@ -362,6 +407,11 @@ public void maybeUpdateView() {
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);

Expand Down Expand Up @@ -396,13 +446,17 @@ public void maybeUpdateView() {
? 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;

Expand Down Expand Up @@ -468,7 +522,7 @@ public void setControllerListener(ControllerListener controllerListener) {
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();
}
}
Expand All @@ -485,6 +539,10 @@ private boolean hasMultipleSources() {
return mSources.size() > 1;
}

private boolean isTiled() {
return mTileMode != Shader.TileMode.CLAMP;
}

private void setSourceImage() {
mImageSource = null;
if (mSources.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
}
}