Skip to content

Commit

Permalink
feat(image): support for resizeMode and objectFit value of `'none…
Browse files Browse the repository at this point in the history
…'` (#47110)

Summary:
As part of #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: #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:

<details>
<summary>Fabric screenshots</summary>

**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) |

</details>

<details>
<summary>Paper screenshots</summary>

**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) |

</details>

Reviewed By: fabriziocucci

Differential Revision: D65420002

Pulled By: javache

fbshipit-source-id: df3bc8fc931b88cde5fe51d89685bf327e30ed9f
  • Loading branch information
mateoguzmana authored and facebook-github-bot committed Nov 5, 2024
1 parent d7d5de9 commit d8cfd98
Show file tree
Hide file tree
Showing 17 changed files with 85 additions and 22 deletions.
2 changes: 2 additions & 0 deletions packages/react-native/Libraries/Image/Image.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 2 additions & 1 deletion packages/react-native/Libraries/Image/ImageProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion packages/react-native/Libraries/Image/ImageResizeMode.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export type ImageResizeMode =
| 'contain'
| 'stretch'
| 'repeat'
| 'center';
| 'center'
| 'none';

/**
* @see ImageResizeMode.js
Expand Down Expand Up @@ -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;
}
5 changes: 4 additions & 1 deletion packages/react-native/Libraries/Image/ImageResizeMode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
9 changes: 6 additions & 3 deletions packages/react-native/Libraries/Image/ImageUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 2 additions & 0 deletions packages/react-native/Libraries/Image/RCTImageUtils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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)};

Expand Down Expand Up @@ -249,6 +250,7 @@ BOOL RCTUpscalingRequired(

case RCTResizeModeRepeat:
case RCTResizeModeCenter:
case RCTResizeModeNone:

return NO;
}
Expand Down
5 changes: 4 additions & 1 deletion packages/react-native/Libraries/Image/RCTResizeMode.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/Libraries/Image/RCTResizeMode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ @implementation RCTConvert (RCTResizeMode)
@"stretch" : @(RCTResizeModeStretch),
@"center" : @(RCTResizeModeCenter),
@"repeat" : @(RCTResizeModeRepeat),
@"none" : @(RCTResizeModeNone),
}),
RCTResizeModeStretch,
integerValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
9 changes: 5 additions & 4 deletions packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
}>;
Expand All @@ -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,
}>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -4892,7 +4892,8 @@ exports[`public API should not change unintentionally Libraries/Image/ImageResiz
| \\"contain\\"
| \\"cover\\"
| \\"repeat\\"
| \\"stretch\\";
| \\"stretch\\"
| \\"none\\";
"
`;

Expand Down Expand Up @@ -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;
"
`;

Expand Down Expand Up @@ -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,
}>;
Expand All @@ -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,
}>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -168,6 +170,8 @@ inline std::string toString(const ImageResizeMode& value) {
return "center";
case ImageResizeMode::Repeat:
return "repeat";
case ImageResizeMode::None:
return "none";
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand All @@ -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";
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ enum class ImageResizeMode {
Stretch,
Center,
Repeat,
None,
};

class ImageErrorInfo {
Expand Down
26 changes: 26 additions & 0 deletions packages/rn-tester/js/examples/Image/ImageExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,9 @@ const styles = StyleSheet.create({
objectFitScaleDown: {
objectFit: 'scale-down',
},
objectFitNone: {
objectFit: 'none',
},
imageInBundle: {
borderColor: 'yellow',
borderWidth: 4,
Expand Down Expand Up @@ -1466,6 +1469,17 @@ exports.examples = [
/>
</View>
</View>
<View style={styles.horizontal}>
<View>
<RNTesterText style={styles.resizeModeText}>
None
</RNTesterText>
<Image
style={[styles.resizeMode, styles.objectFitNone]}
source={image}
/>
</View>
</View>
</View>
);
})}
Expand Down Expand Up @@ -1537,6 +1551,18 @@ exports.examples = [
/>
</View>
</View>
<View style={styles.horizontal}>
<View>
<RNTesterText style={styles.resizeModeText}>
None
</RNTesterText>
<Image
style={styles.resizeMode}
resizeMode="none"
source={image}
/>
</View>
</View>
</View>
);
})}
Expand Down

0 comments on commit d8cfd98

Please sign in to comment.