From d8cfd98070cbccc5e8a49446d76bdc2cb0c6939f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateo=20Guzm=C3=A1n?= Date: Tue, 5 Nov 2024 11:36:27 -0800 Subject: [PATCH] feat(image): support for `resizeMode` and `objectFit` value of `'none'` (#47110) Summary: As part of https://github.com/facebook/react-native/issues/34425, `objectFit` value of `'none'` needs to be supported for the Image component. In order to support this, a new value must also be added to support the equivalent in `resizeMode`. With this new value, the image will not be resized at all and keeping it in the initial position within a container (see in the screenshots). In this PR the support is added for both Fabric and Paper. ## Changelog: [GENERAL] [ADDED] - image `resizeMode` and `objectFit` support for `'none'`. Pull Request resolved: https://github.com/facebook/react-native/pull/47110 Test Plan: Using the `rn-tester`, there is a new image example for both `resizeMode` and `objectFit`. See below the results for both Android and iOS:
Fabric screenshots **Android:** | Resize Mode | Object Fit | | --------- | ---------- | | ![Screenshot_1729232899](https://github.com/user-attachments/assets/ea765afc-9f85-4ac3-96ab-229b3f1def20) | ![Screenshot_1729232912](https://github.com/user-attachments/assets/75033e76-5faa-438d-81b1-4bf8436f9ef2) | **iOS:** | Resize Mode | Object Fit | | --------- | ---------- | | ![Simulator Screenshot - iPhone 16 Pro Max - 2024-10-18 at 08 16 37](https://github.com/user-attachments/assets/ade02ba9-4792-4760-aada-6ea56b591801) | ![Simulator Screenshot - iPhone 16 Pro Max - 2024-10-18 at 08 16 55](https://github.com/user-attachments/assets/abf68db9-841a-4ee5-b5db-64fe84a69089) |
Paper screenshots **Android:** | Resize Mode | Object Fit | | --------- | ---------- | | ![Screenshot_1729286528](https://github.com/user-attachments/assets/88e89191-d70a-4013-8380-2ecefd9532b4) | ![Screenshot_1729286542](https://github.com/user-attachments/assets/43d84ae0-2ed3-47ad-a725-ac6aea0b3245) | **iOS:** | Resize Mode | Object Fit | | --------- | ---------- | | ![Simulator Screenshot - iPhone 16 Pro Max - 2024-10-18 at 22 21 22](https://github.com/user-attachments/assets/e14a81de-3a69-4e73-8c85-ec08ac30b04f) | ![Simulator Screenshot - iPhone 16 Pro Max - 2024-10-18 at 22 21 16](https://github.com/user-attachments/assets/595f9f5e-96a6-4f6b-9614-f6c236837ba8) |
Reviewed By: fabriziocucci Differential Revision: D65420002 Pulled By: javache fbshipit-source-id: df3bc8fc931b88cde5fe51d89685bf327e30ed9f --- .../react-native/Libraries/Image/Image.d.ts | 2 ++ .../Libraries/Image/ImageProps.js | 3 ++- .../Libraries/Image/ImageResizeMode.d.ts | 9 ++++++- .../Libraries/Image/ImageResizeMode.js | 5 +++- .../Libraries/Image/ImageUtils.js | 9 ++++--- .../Libraries/Image/RCTImageUtils.mm | 2 ++ .../Libraries/Image/RCTResizeMode.h | 5 +++- .../Libraries/Image/RCTResizeMode.mm | 1 + .../Libraries/StyleSheet/StyleSheetTypes.d.ts | 2 +- .../Libraries/StyleSheet/StyleSheetTypes.js | 9 ++++--- .../__snapshots__/public-api-test.js.snap | 18 ++++++------- .../react/views/image/ImageResizeMode.kt | 5 +++- .../react/views/image/ImageResizeModeTest.kt | 2 ++ .../renderer/components/image/conversions.h | 4 +++ .../RCTImagePrimitivesConversions.h | 4 +++ .../react/renderer/imagemanager/primitives.h | 1 + .../js/examples/Image/ImageExample.js | 26 +++++++++++++++++++ 17 files changed, 85 insertions(+), 22 deletions(-) diff --git a/packages/react-native/Libraries/Image/Image.d.ts b/packages/react-native/Libraries/Image/Image.d.ts index 95e42447033cbf..d50679a548bfb2 100644 --- a/packages/react-native/Libraries/Image/Image.d.ts +++ b/packages/react-native/Libraries/Image/Image.d.ts @@ -200,6 +200,8 @@ export interface ImagePropsBase * 'center': Scale the image down so that it is completely visible, * if bigger than the area of the view. * The image will not be scaled up. + * + * 'none': Do not resize the image. The image will be displayed at its intrinsic size. */ resizeMode?: ImageResizeMode | undefined; diff --git a/packages/react-native/Libraries/Image/ImageProps.js b/packages/react-native/Libraries/Image/ImageProps.js index c0c39075872fdf..80c05862e4330a 100644 --- a/packages/react-native/Libraries/Image/ImageProps.js +++ b/packages/react-native/Libraries/Image/ImageProps.js @@ -19,6 +19,7 @@ import type { } from '../StyleSheet/StyleSheet'; import type {LayoutEvent, SyntheticEvent} from '../Types/CoreEventTypes'; import typeof Image from './Image'; +import type {ImageResizeMode} from './ImageResizeMode'; import type {ImageSource} from './ImageSource'; import type {ElementRef, Node, RefSetter} from 'react'; @@ -234,7 +235,7 @@ export type ImageProps = $ReadOnly<{| * * See https://reactnative.dev/docs/image#resizemode */ - resizeMode?: ?('cover' | 'contain' | 'stretch' | 'repeat' | 'center'), + resizeMode?: ?ImageResizeMode, /** * A unique identifier for this element to be used in UI Automation diff --git a/packages/react-native/Libraries/Image/ImageResizeMode.d.ts b/packages/react-native/Libraries/Image/ImageResizeMode.d.ts index 84174e3da9623c..5df952b678de26 100644 --- a/packages/react-native/Libraries/Image/ImageResizeMode.d.ts +++ b/packages/react-native/Libraries/Image/ImageResizeMode.d.ts @@ -12,7 +12,8 @@ export type ImageResizeMode = | 'contain' | 'stretch' | 'repeat' - | 'center'; + | 'center' + | 'none'; /** * @see ImageResizeMode.js @@ -46,4 +47,10 @@ export interface ImageResizeModeStatic { * image will keep it's size and aspect ratio. */ repeat: ImageResizeMode; + + /** + * none - The image will be displayed at its intrinsic size, which means the + * image will not be scaled up or down. + */ + none: ImageResizeMode; } diff --git a/packages/react-native/Libraries/Image/ImageResizeMode.js b/packages/react-native/Libraries/Image/ImageResizeMode.js index b63627b793bd43..50001ce552c82f 100644 --- a/packages/react-native/Libraries/Image/ImageResizeMode.js +++ b/packages/react-native/Libraries/Image/ImageResizeMode.js @@ -33,4 +33,7 @@ export type ImageResizeMode = // Resize by stretching it to fill the entire frame of the view without // clipping. This may change the aspect ratio of the image, distorting it. - | 'stretch'; + | 'stretch' + + // The image will not be resized at all. + | 'none'; diff --git a/packages/react-native/Libraries/Image/ImageUtils.js b/packages/react-native/Libraries/Image/ImageUtils.js index 732b5733bc6e81..c0e00bb534a84d 100644 --- a/packages/react-native/Libraries/Image/ImageUtils.js +++ b/packages/react-native/Libraries/Image/ImageUtils.js @@ -8,15 +8,18 @@ * @format */ -type ResizeMode = 'cover' | 'contain' | 'stretch' | 'repeat' | 'center'; +import type {ImageResizeMode} from './ImageResizeMode'; -const objectFitMap: {[string]: ResizeMode} = { +const objectFitMap: {[string]: ImageResizeMode} = { contain: 'contain', cover: 'cover', fill: 'stretch', 'scale-down': 'contain', + none: 'none', }; -export function convertObjectFitToResizeMode(objectFit: ?string): ?ResizeMode { +export function convertObjectFitToResizeMode( + objectFit: ?string, +): ?ImageResizeMode { return objectFit != null ? objectFitMap[objectFit] : undefined; } diff --git a/packages/react-native/Libraries/Image/RCTImageUtils.mm b/packages/react-native/Libraries/Image/RCTImageUtils.mm index 38fbd6bad434af..b7f85f83fc33a6 100644 --- a/packages/react-native/Libraries/Image/RCTImageUtils.mm +++ b/packages/react-native/Libraries/Image/RCTImageUtils.mm @@ -85,6 +85,7 @@ CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize, CGFloat destScale, RCTR switch (resizeMode) { case RCTResizeModeStretch: case RCTResizeModeRepeat: + case RCTResizeModeNone: return (CGRect){CGPointZero, RCTCeilSize(destSize, destScale)}; @@ -249,6 +250,7 @@ BOOL RCTUpscalingRequired( case RCTResizeModeRepeat: case RCTResizeModeCenter: + case RCTResizeModeNone: return NO; } diff --git a/packages/react-native/Libraries/Image/RCTResizeMode.h b/packages/react-native/Libraries/Image/RCTResizeMode.h index 148e3dd2fe964d..e65f113a0dc14f 100644 --- a/packages/react-native/Libraries/Image/RCTResizeMode.h +++ b/packages/react-native/Libraries/Image/RCTResizeMode.h @@ -13,6 +13,7 @@ typedef NS_ENUM(NSInteger, RCTResizeMode) { RCTResizeModeStretch = UIViewContentModeScaleToFill, RCTResizeModeCenter = UIViewContentModeCenter, RCTResizeModeRepeat = -1, // Use negative values to avoid conflicts with iOS enum values. + RCTResizeModeNone = UIViewContentModeTopLeft, }; static inline RCTResizeMode RCTResizeModeFromUIViewContentMode(UIViewContentMode mode) @@ -30,12 +31,14 @@ static inline RCTResizeMode RCTResizeModeFromUIViewContentMode(UIViewContentMode case UIViewContentModeCenter: return RCTResizeModeCenter; break; + case UIViewContentModeTopLeft: + return RCTResizeModeNone; + break; case UIViewContentModeRedraw: case UIViewContentModeTop: case UIViewContentModeBottom: case UIViewContentModeLeft: case UIViewContentModeRight: - case UIViewContentModeTopLeft: case UIViewContentModeTopRight: case UIViewContentModeBottomLeft: case UIViewContentModeBottomRight: diff --git a/packages/react-native/Libraries/Image/RCTResizeMode.mm b/packages/react-native/Libraries/Image/RCTResizeMode.mm index c35c32e3fd16fc..88ea96eb036e4c 100644 --- a/packages/react-native/Libraries/Image/RCTResizeMode.mm +++ b/packages/react-native/Libraries/Image/RCTResizeMode.mm @@ -17,6 +17,7 @@ @implementation RCTConvert (RCTResizeMode) @"stretch" : @(RCTResizeModeStretch), @"center" : @(RCTResizeModeCenter), @"repeat" : @(RCTResizeModeRepeat), + @"none" : @(RCTResizeModeNone), }), RCTResizeModeStretch, integerValue) diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts index a4aa69d027579e..05a8b6410f454e 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts @@ -568,6 +568,6 @@ export interface ImageStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle { overlayColor?: ColorValue | undefined; tintColor?: ColorValue | undefined; opacity?: AnimatableNumericValue | undefined; - objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | undefined; + objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | 'none' | undefined; cursor?: CursorValue | undefined; } diff --git a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js index ebc1dfa98b09c7..0d75a94204048c 100644 --- a/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js +++ b/packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js @@ -11,6 +11,7 @@ 'use strict'; import type AnimatedNode from '../Animated/nodes/AnimatedNode'; +import type {ImageResizeMode} from './../Image/ImageResizeMode'; import type { ____DangerouslyImpreciseStyle_InternalOverrides, ____ImageStyle_InternalOverrides, @@ -941,8 +942,8 @@ export type ____TextStyle_Internal = $ReadOnly<{ export type ____ImageStyle_InternalCore = $ReadOnly<{ ...$Exact<____ViewStyle_Internal>, - resizeMode?: 'contain' | 'cover' | 'stretch' | 'center' | 'repeat', - objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down', + resizeMode?: ImageResizeMode, + objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | 'none', tintColor?: ____ColorValue_Internal, overlayColor?: string, }>; @@ -954,8 +955,8 @@ export type ____ImageStyle_Internal = $ReadOnly<{ export type ____DangerouslyImpreciseStyle_InternalCore = $ReadOnly<{ ...$Exact<____TextStyle_Internal>, - resizeMode?: 'contain' | 'cover' | 'stretch' | 'center' | 'repeat', - objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down', + resizeMode?: ImageResizeMode, + objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | 'none', tintColor?: ____ColorValue_Internal, overlayColor?: string, }>; 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 ceca9bbce45dd6..ea0a2fe12eff77 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 @@ -4869,7 +4869,7 @@ export type ImageProps = $ReadOnly<{| | \\"strict-origin-when-cross-origin\\" | \\"unsafe-url\\" ), - resizeMode?: ?(\\"cover\\" | \\"contain\\" | \\"stretch\\" | \\"repeat\\" | \\"center\\"), + resizeMode?: ?ImageResizeMode, testID?: ?string, tintColor?: ColorValue, src?: ?string, @@ -4892,7 +4892,8 @@ exports[`public API should not change unintentionally Libraries/Image/ImageResiz | \\"contain\\" | \\"cover\\" | \\"repeat\\" - | \\"stretch\\"; + | \\"stretch\\" + | \\"none\\"; " `; @@ -4989,10 +4990,9 @@ export type { ImageProps } from \\"./ImageProps\\"; `; exports[`public API should not change unintentionally Libraries/Image/ImageUtils.js 1`] = ` -"type ResizeMode = \\"cover\\" | \\"contain\\" | \\"stretch\\" | \\"repeat\\" | \\"center\\"; -declare export function convertObjectFitToResizeMode( +"declare export function convertObjectFitToResizeMode( objectFit: ?string -): ?ResizeMode; +): ?ImageResizeMode; " `; @@ -8261,8 +8261,8 @@ export type ____TextStyle_Internal = $ReadOnly<{ }>; export type ____ImageStyle_InternalCore = $ReadOnly<{ ...$Exact<____ViewStyle_Internal>, - resizeMode?: \\"contain\\" | \\"cover\\" | \\"stretch\\" | \\"center\\" | \\"repeat\\", - objectFit?: \\"cover\\" | \\"contain\\" | \\"fill\\" | \\"scale-down\\", + resizeMode?: ImageResizeMode, + objectFit?: \\"cover\\" | \\"contain\\" | \\"fill\\" | \\"scale-down\\" | \\"none\\", tintColor?: ____ColorValue_Internal, overlayColor?: string, }>; @@ -8272,8 +8272,8 @@ export type ____ImageStyle_Internal = $ReadOnly<{ }>; export type ____DangerouslyImpreciseStyle_InternalCore = $ReadOnly<{ ...$Exact<____TextStyle_Internal>, - resizeMode?: \\"contain\\" | \\"cover\\" | \\"stretch\\" | \\"center\\" | \\"repeat\\", - objectFit?: \\"cover\\" | \\"contain\\" | \\"fill\\" | \\"scale-down\\", + resizeMode?: ImageResizeMode, + objectFit?: \\"cover\\" | \\"contain\\" | \\"fill\\" | \\"scale-down\\" | \\"none\\", tintColor?: ____ColorValue_Internal, overlayColor?: string, }>; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.kt index 76da29958b1169..61098ad9c38771 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ImageResizeMode.kt @@ -19,6 +19,7 @@ public object ImageResizeMode { private const val RESIZE_MODE_STRETCH = "stretch" private const val RESIZE_MODE_CENTER = "center" private const val RESIZE_MODE_REPEAT = "repeat" + private const val RESIZE_MODE_NONE = "none" /** Converts JS resize modes into `ScalingUtils.ScaleType`. See `ImageResizeMode.js`. */ @JvmStatic @@ -30,6 +31,7 @@ public object ImageResizeMode { RESIZE_MODE_CENTER -> return ScalingUtils.ScaleType.CENTER_INSIDE // Handled via a combination of ScaleType and TileMode RESIZE_MODE_REPEAT -> return ScaleTypeStartInside.INSTANCE + RESIZE_MODE_NONE -> return ScaleTypeStartInside.INSTANCE } if (resizeModeValue != null) { @@ -45,7 +47,8 @@ public object ImageResizeMode { if (RESIZE_MODE_CONTAIN == resizeModeValue || RESIZE_MODE_COVER == resizeModeValue || RESIZE_MODE_STRETCH == resizeModeValue || - RESIZE_MODE_CENTER == resizeModeValue) { + RESIZE_MODE_CENTER == resizeModeValue || + RESIZE_MODE_NONE == resizeModeValue) { return TileMode.CLAMP } if (RESIZE_MODE_REPEAT == resizeModeValue) { diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/image/ImageResizeModeTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/image/ImageResizeModeTest.kt index 1c6f98d76ec7bf..c3c589986af95b 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/image/ImageResizeModeTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/image/ImageResizeModeTest.kt @@ -28,6 +28,8 @@ class ImageResizeModeTest { .isEqualTo(ScalingUtils.ScaleType.FIT_XY) Assertions.assertThat(ImageResizeMode.toScaleType("center")) .isEqualTo(ScalingUtils.ScaleType.CENTER_INSIDE) + Assertions.assertThat(ImageResizeMode.toScaleType("none")) + .isEqualTo(ScaleTypeStartInside.INSTANCE) // No resizeMode set Assertions.assertThat(ImageResizeMode.defaultValue()) diff --git a/packages/react-native/ReactCommon/react/renderer/components/image/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/image/conversions.h index 0a76824f588b46..2803206c792d5c 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/image/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/image/conversions.h @@ -148,6 +148,8 @@ inline void fromRawValue( result = ImageResizeMode::Center; } else if (stringValue == "repeat") { result = ImageResizeMode::Repeat; + } else if (stringValue == "none") { + result = ImageResizeMode::None; } else { LOG(ERROR) << "Unsupported ImageResizeMode value: " << stringValue; react_native_expect(false); @@ -168,6 +170,8 @@ inline std::string toString(const ImageResizeMode& value) { return "center"; case ImageResizeMode::Repeat: return "repeat"; + case ImageResizeMode::None: + return "none"; } } diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImagePrimitivesConversions.h b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImagePrimitivesConversions.h index 88487f94d48c68..50194b7a7548d7 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImagePrimitivesConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/ios/react/renderer/imagemanager/RCTImagePrimitivesConversions.h @@ -26,6 +26,8 @@ inline static UIViewContentMode RCTContentModeFromImageResizeMode(facebook::reac // Repeat resize mode is handled by the UIImage. Use scale to fill // so the repeated image fills the UIImageView. return UIViewContentModeScaleToFill; + case facebook::react::ImageResizeMode::None: + return UIViewContentModeTopLeft; } } @@ -42,6 +44,8 @@ inline std::string toString(const facebook::react::ImageResizeMode &value) return "center"; case facebook::react::ImageResizeMode::Repeat: return "repeat"; + case facebook::react::ImageResizeMode::None: + return "none"; } } diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h b/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h index 18aba9e10b53be..e8d4be21db453e 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h @@ -47,6 +47,7 @@ enum class ImageResizeMode { Stretch, Center, Repeat, + None, }; class ImageErrorInfo { diff --git a/packages/rn-tester/js/examples/Image/ImageExample.js b/packages/rn-tester/js/examples/Image/ImageExample.js index 81e42ad8adb72e..b3dddd83e875d8 100644 --- a/packages/rn-tester/js/examples/Image/ImageExample.js +++ b/packages/rn-tester/js/examples/Image/ImageExample.js @@ -825,6 +825,9 @@ const styles = StyleSheet.create({ objectFitScaleDown: { objectFit: 'scale-down', }, + objectFitNone: { + objectFit: 'none', + }, imageInBundle: { borderColor: 'yellow', borderWidth: 4, @@ -1466,6 +1469,17 @@ exports.examples = [ /> + + + + None + + + + ); })} @@ -1537,6 +1551,18 @@ exports.examples = [ /> + + + + None + + + + ); })}