From 01cb9231a16b56b1b0829bdce2d5e261cceac858 Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Wed, 5 Jun 2024 13:31:08 -0700 Subject: [PATCH] Hybrid image aliasing (#44803) Summary: This change introduces a new prop to the Android `Image` component: `resizeMultiplier`. This prop can be used when the `resizeMethod` is set to `resize`, and it directly modifies the resultant bitmap generated in memory from Fresco to be larger (or smaller) depending on the multiplier. A default of 1.0 means the bitmap size is designed to fit the destination dimensions. A multiplier greater than 1.0 will set the `ResizeOptions` provided to Fresco to be larger that the destination dimensions, and the resulting bitmap will be scaled from the hardware size. This new prop is most useful in cases where the destination dimensions are quite small and the source image is significantly larger. The `resize` resize method performs downsampling and significant image quality is lost between the source and destination image sizes, often resulting in a blurry image. By using a multiplier, the decoded image is slightly larger than the target size but smaller than the source image (if the source image is large enough). It's important to note that Fresco still chooses the closest power of 2 and will not scale the image larger than its source dimensions. If the multiplier yields `ResizeOptions` greater than the source dimensions, no downsampling occurs. Here's an example: If you have a source image with dimensions 200x200 and destination dimensions of 24x24, a `resizeMultiplier` of `2.0` will tell Fresco to downsample the image to 48x48. Fresco picks the closest power of 2 (so, 50x50) and decodes the image into a bitmap of that size. Without the multiplier, the closest power of 2 would be 25x25, which is half the quality. ## Changelog [Android][Added] - Adds a new `Image` prop `resizeMultiplier` to help increase quality of small images on low DPI devices Differential Revision: D58120352 --- .../Libraries/Image/ImageProps.js | 21 +++++++++++++----- .../Image/ImageViewNativeComponent.js | 3 ++- .../__snapshots__/public-api-test.js.snap | 3 ++- .../ReactAndroid/api/ReactAndroid.api | 2 ++ .../react/views/image/ImageResizeMethod.kt | 2 +- .../react/views/image/ReactImageManager.java | 8 +++++++ .../react/views/image/ReactImageView.java | 22 ++++++++++++++++++- 7 files changed, 52 insertions(+), 9 deletions(-) diff --git a/packages/react-native/Libraries/Image/ImageProps.js b/packages/react-native/Libraries/Image/ImageProps.js index 7f3166d4de02d8..d1566517faba65 100644 --- a/packages/react-native/Libraries/Image/ImageProps.js +++ b/packages/react-native/Libraries/Image/ImageProps.js @@ -59,6 +59,22 @@ type AndroidImageProps = $ReadOnly<{| loadingIndicatorSource?: ?(number | $ReadOnly<{|uri: string|}>), progressiveRenderingEnabled?: ?boolean, fadeDuration?: ?number, + + /** + * The mechanism that should be used to resize the image when the image's + * dimensions differ from the image view's dimensions. Defaults to `'auto'`. + * See https://reactnative.dev/docs/image#resizemethod + */ + resizeMethod?: ?('auto' | 'resize' | 'scale'), + + /** + * When the `resizeMethod` is set to `resize`, the destination dimensions are + * multiplied by this value. The `scale` method is used to perform the + * remainder of the resize. + * This is used to produce higher quality images when resizing to small dimensions. + * Defaults to 1.0. + */ + resizeMultiplier?: ?number, |}>; export type ImageProps = {| @@ -183,11 +199,6 @@ export type ImageProps = {| */ onLoadStart?: ?() => void, - /** - * See https://reactnative.dev/docs/image#resizemethod - */ - resizeMethod?: ?('auto' | 'resize' | 'scale'), - /** * The image source (either a remote URL or a local file resource). * diff --git a/packages/react-native/Libraries/Image/ImageViewNativeComponent.js b/packages/react-native/Libraries/Image/ImageViewNativeComponent.js index 01459dddc053d1..0dc98c6407273d 100644 --- a/packages/react-native/Libraries/Image/ImageViewNativeComponent.js +++ b/packages/react-native/Libraries/Image/ImageViewNativeComponent.js @@ -82,13 +82,14 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = validAttributes: { blurRadius: true, internal_analyticTag: true, + resizeMethod: true, resizeMode: true, + resizeMultiplier: true, tintColor: { process: require('../StyleSheet/processColor').default, }, borderBottomLeftRadius: true, borderTopLeftRadius: true, - resizeMethod: true, src: true, // NOTE: New Architecture expects this to be called `source`, // regardless of the platform, therefore propagate it as well. diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index d6222d194daa63..86a09eb493a7c4 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -4445,6 +4445,8 @@ type AndroidImageProps = $ReadOnly<{| loadingIndicatorSource?: ?(number | $ReadOnly<{| uri: string |}>), progressiveRenderingEnabled?: ?boolean, fadeDuration?: ?number, + resizeMethod?: ?(\\"auto\\" | \\"resize\\" | \\"scale\\"), + resizeMultiplier?: ?number, |}>; export type ImageProps = {| ...$Diff>, @@ -4472,7 +4474,6 @@ export type ImageProps = {| onLoad?: ?(event: ImageLoadEvent) => void, onLoadEnd?: ?() => void, onLoadStart?: ?() => void, - resizeMethod?: ?(\\"auto\\" | \\"resize\\" | \\"scale\\"), source?: ?ImageSource, style?: ?ImageStyleProp, referrerPolicy?: ?( diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 5a6275d610c873..afcd92ed01a512 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -6379,6 +6379,7 @@ public class com/facebook/react/views/image/ReactImageManager : com/facebook/rea public fun setProgressiveRenderingEnabled (Lcom/facebook/react/views/image/ReactImageView;Z)V public fun setResizeMethod (Lcom/facebook/react/views/image/ReactImageView;Ljava/lang/String;)V public fun setResizeMode (Lcom/facebook/react/views/image/ReactImageView;Ljava/lang/String;)V + public fun setResizeMultiplier (Lcom/facebook/react/views/image/ReactImageView;F)V public fun setSource (Lcom/facebook/react/views/image/ReactImageView;Lcom/facebook/react/bridge/ReadableArray;)V public fun setSrc (Lcom/facebook/react/views/image/ReactImageView;Lcom/facebook/react/bridge/ReadableArray;)V public fun setTintColor (Lcom/facebook/react/views/image/ReactImageView;Ljava/lang/Integer;)V @@ -6412,6 +6413,7 @@ public class com/facebook/react/views/image/ReactImageView : com/facebook/drawee public fun setOverlayColor (I)V public fun setProgressiveRenderingEnabled (Z)V public fun setResizeMethod (Lcom/facebook/react/views/image/ImageResizeMethod;)V + public fun setResizeMultiplier (F)V public fun setScaleType (Lcom/facebook/drawee/drawable/ScalingUtils$ScaleType;)V public fun setShouldNotifyLoadEvents (Z)V public fun setSource (Lcom/facebook/react/bridge/ReadableArray;)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMethod.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMethod.kt index 3e5f252fac37cc..4cd3e8425a615d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMethod.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMethod.kt @@ -10,5 +10,5 @@ package com.facebook.react.views.image public enum class ImageResizeMethod { AUTO, RESIZE, - SCALE + SCALE, } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java index 772c4de78d7749..ec5f5cecc7c743 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java @@ -223,6 +223,14 @@ public void setResizeMethod(ReactImageView view, @Nullable String resizeMethod) } } + @ReactProp(name = "resizeMultiplier") + public void setResizeMultiplier(ReactImageView view, float resizeMultiplier) { + if (resizeMultiplier < 0.01f) { + FLog.w(ReactConstants.TAG, "Invalid resize multiplier: '" + resizeMultiplier + "'"); + } + view.setResizeMultiplier(resizeMultiplier); + } + @ReactProp(name = "tintColor", customType = "Color") public void setTintColor(ReactImageView view, @Nullable Integer tintColor) { if (tintColor == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java index 92001e55fb1a65..93030d2549c4ed 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java @@ -134,6 +134,7 @@ public CloseableReference process(Bitmap source, PlatformBitmapFactory b private int mFadeDurationMs = -1; private boolean mProgressiveRenderingEnabled; private ReadableMap mHeaders; + private float mResizeMultiplier = 1.0f; // We can't specify rounding in XML, so have to do so here private static GenericDraweeHierarchy buildHierarchy(Context context) { @@ -307,6 +308,13 @@ public void setResizeMethod(ImageResizeMethod resizeMethod) { } } + public void setResizeMultiplier(float multiplier) { + if (mResizeMultiplier != multiplier) { + mResizeMultiplier = multiplier; + mIsDirty = true; + } + } + public void setSource(@Nullable ReadableArray sources) { List tmpSources = new LinkedList<>(); @@ -478,7 +486,7 @@ public void maybeUpdateView() { } Postprocessor postprocessor = MultiPostprocessor.from(postprocessors); - ResizeOptions resizeOptions = doResize ? new ResizeOptions(getWidth(), getHeight()) : null; + ResizeOptions resizeOptions = getResizeOptions(mImageSource); ImageRequestBuilder imageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(mImageSource.getUri()) @@ -601,6 +609,18 @@ private boolean shouldResize(ImageSource imageSource) { } } + private ResizeOptions getResizeOptions(ImageSource imageSource) { + if (!shouldResize(imageSource)) { + return null; + } + int width = Math.round((float) getWidth() * mResizeMultiplier); + int height = Math.round((float) getHeight() * mResizeMultiplier); + if (width <= 0 || height <= 0) { + return null; + } + return new ResizeOptions(width, height); + } + private void warnImageSource(String uri) { // TODO(T189014077): This code-path produces an infinite loop of js calls with logbox. // This is an issue with Fabric view preallocation, react, and LogBox. Fix.