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"; + } +}