From b6c343353765d8b13344841613250214fe8783db Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Wed, 12 Jun 2024 12:57:53 -0700 Subject: [PATCH] Hybrid image aliasing (#44803) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/44803 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 Reviewed By: javache Differential Revision: D58120352 fbshipit-source-id: e0ebf4bd899170134825a29f72a68621447106c0 --- .../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 | 21 ++++++++++++++++++- 7 files changed, 51 insertions(+), 9 deletions(-) diff --git a/packages/react-native/Libraries/Image/ImageProps.js b/packages/react-native/Libraries/Image/ImageProps.js index 7f3166d4de02d8..3b62af9fcdcfec 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-android + */ + 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 f270fe7529ead5..9ef6b99d40414a 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 7058755cfc3333..582ef18f982de2 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -6390,6 +6390,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 @@ -6423,6 +6424,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..d02b91663b769a 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,14 @@ public void setResizeMethod(ImageResizeMethod resizeMethod) { } } + public void setResizeMultiplier(float multiplier) { + boolean isNewMultiplier = Math.abs(mResizeMultiplier - multiplier) > 0.0001f; + if (isNewMultiplier) { + mResizeMultiplier = multiplier; + mIsDirty = true; + } + } + public void setSource(@Nullable ReadableArray sources) { List tmpSources = new LinkedList<>(); @@ -478,7 +487,7 @@ public void maybeUpdateView() { } Postprocessor postprocessor = MultiPostprocessor.from(postprocessors); - ResizeOptions resizeOptions = doResize ? new ResizeOptions(getWidth(), getHeight()) : null; + ResizeOptions resizeOptions = doResize ? getResizeOptions() : null; ImageRequestBuilder imageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(mImageSource.getUri()) @@ -601,6 +610,16 @@ private boolean shouldResize(ImageSource imageSource) { } } + @Nullable + private ResizeOptions getResizeOptions() { + 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.