diff --git a/Libraries/Image/Image.android.js b/Libraries/Image/Image.android.js
index bdb29ab8fe8965..4e218487c9b505 100644
--- a/Libraries/Image/Image.android.js
+++ b/Libraries/Image/Image.android.js
@@ -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: {
diff --git a/RNTester/js/ImageExample.js b/RNTester/js/ImageExample.js
index d30a24ca7c93b2..37f36b697de32a 100644
--- a/RNTester/js/ImageExample.js
+++ b/RNTester/js/ImageExample.js
@@ -558,18 +558,16 @@ exports.examples = [
source={image}
/>
- { Platform.OS === 'ios' ?
-
-
- Repeat
-
-
-
- : null }
+
+
+ Repeat
+
+
+
Center
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.java
index 3cff8b9667a292..3f98058feedd0c 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.java
@@ -9,6 +9,7 @@
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 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();
@@ -42,6 +47,29 @@ 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.
@@ -49,4 +77,8 @@ public static ScalingUtils.ScaleType toScaleType(@Nullable String resizeModeValu
public static ScalingUtils.ScaleType defaultValue() {
return ScalingUtils.ScaleType.CENTER_CROP;
}
+
+ public static Shader.TileMode defaultTileMode() {
+ return Shader.TileMode.CLAMP;
+ }
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/MultiPostprocessor.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/MultiPostprocessor.java
new file mode 100644
index 00000000000000..d6cf6a3ca8d6c4
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/MultiPostprocessor.java
@@ -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 mPostprocessors;
+
+ public static Postprocessor from(List postprocessors) {
+ switch (postprocessors.size()) {
+ case 0:
+ return null;
+ case 1:
+ return postprocessors.get(0);
+ default:
+ return new MultiPostprocessor(postprocessors);
+ }
+ }
+
+ private MultiPostprocessor(List 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 keys = new LinkedList<>();
+ for (Postprocessor p: mPostprocessors) {
+ keys.push(p.getPostprocessorCacheKey());
+ }
+ return new MultiCacheKey(keys);
+ }
+
+ @Override
+ public CloseableReference process(Bitmap sourceBitmap, PlatformBitmapFactory bitmapFactory) {
+ CloseableReference 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);
+ }
+ }
+}
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 21014c5c7b0fd5..938524d3012156 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
@@ -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)
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 3fa2e66eb82e6d..9396cac1c5c1f2 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
@@ -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;
@@ -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;
@@ -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;
@@ -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 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 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 mSources;
private @Nullable ImageSource mImageSource;
@@ -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;
@@ -180,6 +219,7 @@ public ReactImageView(
mScaleType = ImageResizeMode.defaultValue();
mDraweeControllerBuilder = draweeControllerBuilder;
mRoundedCornerPostprocessor = new RoundedCornerPostprocessor();
+ mTilePostprocessor = new TilePostprocessor();
mGlobalImageLoadListener = globalImageLoadListener;
mCallerContext = callerContext;
mSources = new LinkedList<>();
@@ -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;
@@ -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);
@@ -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 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 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();
}
}
@@ -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()) {
diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/image/ScaleTypeStartInside.java b/ReactAndroid/src/main/java/com/facebook/react/views/image/ScaleTypeStartInside.java
new file mode 100644
index 00000000000000..e2b902b2c5248f
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/views/image/ScaleTypeStartInside.java
@@ -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";
+ }
+}