From 987b42977f1673ece57a002a7e090ec153606eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 31 Jul 2026 02:13:36 -0700 Subject: [PATCH] fix(android): props 2.0 image tintColor=transparent broken (#57668) Summary: When enabling the props 2.0 feature flags I noticed that for an `` the image is actually still showing, instead of becoming transparent. ### The underlying issue All color props are defined as `SharedColor`, where a `SharedColor` has a default value of `0`: https://github.com/facebook/react-native/blob/f3678f51d9873cb19602d7e36a4d8ed71562b9d0/packages/react-native/ReactCommon/react/renderer/graphics/platform/android/react/renderer/graphics/HostPlatformColor.h#L15-L18 https://github.com/facebook/react-native/blob/f3678f51d9873cb19602d7e36a4d8ed71562b9d0/packages/react-native/ReactCommon/react/renderer/graphics/Color.h#L30-L32 > [!NOTE] > This is a bit confusing to me. `0` is not really an "undefined" color, but its actually "transparent". What we are really saying this way is that all color props have a default value of "transparent". The naming makes me unsure whether this has been intentional. For other use cases, this seems to make sense. Ie. a user expects their text to have a background color of transparent/"nothing". However, we do not expect our image's tint color to have a default color of "transparent". This would hide all our images. With props 2.0 we use the `getDiffProp` function, and when we pass `tintColor: 'transparent'` it will be passed to native as `tintColor: 0`. When we then compare the passed prop's value vs the default value here, it will not include the `tintColor`: https://github.com/facebook/react-native/blob/f3678f51d9873cb19602d7e36a4d8ed71562b9d0/packages/react-native/ReactCommon/react/renderer/components/image/ImageProps.cpp#L249-L251 `tintColor` really is an optional color prop, and should be treated as such. The best fix I found was therefor making it really an `std::optional`. Let me know if you think otherwise! ## Changelog: [ANDROID] [CHANGED] - ImageProps make `tintColor` an `std::optional` to support color `transparent` (`0`) with props 2.0 X-link: https://github.com/facebook/react-native/pull/55535 Test Plan: - In the RNTester app change one of the Tint Color image examples to use tintColor of "transparent" - The image should be invisible now as all visible pixels turned transparent - Enable the props 2.0 feature flags - Run the same example, notice that the tintColor has not been applied Reviewed By: lenaic Differential Revision: D93140534 Pulled By: coado --- .../Image/RCTImageComponentView.mm | 12 +++++++++- .../renderer/components/image/ImageProps.cpp | 22 +++++++++++++++++-- .../renderer/components/image/ImageProps.h | 2 +- .../imagemanager/ImageRequestParams.h | 4 ++-- .../react/renderer/imagemanager/conversions.h | 13 +++++++++-- .../api-snapshots/ReactAndroidDebugCxx.api | 6 ++--- .../api-snapshots/ReactAndroidNewarchCxx.api | 6 ++--- .../api-snapshots/ReactAndroidReleaseCxx.api | 6 ++--- .../api-snapshots/ReactAppleDebugCxx.api | 2 +- .../api-snapshots/ReactAppleNewarchCxx.api | 2 +- .../api-snapshots/ReactAppleReleaseCxx.api | 2 +- .../api-snapshots/ReactCommonDebugCxx.api | 2 +- .../api-snapshots/ReactCommonNewarchCxx.api | 2 +- .../api-snapshots/ReactCommonReleaseCxx.api | 2 +- 14 files changed, 60 insertions(+), 23 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm index 99104f0ec8fc..eeee2ed1a725 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Image/RCTImageComponentView.mm @@ -11,9 +11,11 @@ #import #import #import +#import #import #import #import +#import #import #import @@ -63,7 +65,15 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & // `tintColor` if (oldImageProps.tintColor != newImageProps.tintColor) { - _imageView.tintColor = RCTUIColorFromSharedColor(newImageProps.tintColor); + if (ReactNativeFeatureFlags::enableImageTransparentTintColor()) { + if (newImageProps.tintColor.has_value()) { + _imageView.tintColor = RCTUIColorFromSharedColor(newImageProps.tintColor.value()); + } else { + _imageView.tintColor = nil; + } + } else { + _imageView.tintColor = RCTUIColorFromSharedColor(newImageProps.tintColor.value_or(SharedColor{})); + } } [super updateProps:props oldProps:oldProps]; diff --git a/packages/react-native/ReactCommon/react/renderer/components/image/ImageProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/image/ImageProps.cpp index 3645736ea94f..25570ca1ea51 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/image/ImageProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/image/ImageProps.cpp @@ -5,10 +5,12 @@ * LICENSE file in the root directory of this source tree. */ +#include #include #include #include #include +#include namespace facebook::react { @@ -199,8 +201,24 @@ folly::dynamic ImageProps::getDiffProps(const Props* prevProps) const { result["capInsets"] = convertEdgeInsets(capInsets); } - if (tintColor != oldProps->tintColor) { - result["tintColor"] = *tintColor; + if (ReactNativeFeatureFlags::enableImageTransparentTintColor()) { + // New: emit any defined tint (including transparent); emit null to clear on + // unset. + if (tintColor != oldProps->tintColor) { + if (tintColor.has_value()) { + result["tintColor"] = *tintColor.value(); + } else { + result["tintColor"] = folly::dynamic(nullptr); + } + } + } else { + // pre-`std::optional` behavior + SharedColor tintColorValue = tintColor.value_or(SharedColor{}); + SharedColor prevTintColorValue = + oldProps->tintColor.value_or(SharedColor{}); + if (tintColorValue != prevTintColorValue) { + result["tintColor"] = *tintColorValue; + } } if (internal_analyticTag != oldProps->internal_analyticTag) { diff --git a/packages/react-native/ReactCommon/react/renderer/components/image/ImageProps.h b/packages/react-native/ReactCommon/react/renderer/components/image/ImageProps.h index f38fdb179e8b..e53e1995d38c 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/image/ImageProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/image/ImageProps.h @@ -31,7 +31,7 @@ class ImageProps final : public ViewProps { ImageResizeMode resizeMode{ImageResizeMode::Stretch}; Float blurRadius{}; EdgeInsets capInsets{}; - SharedColor tintColor{}; + std::optional tintColor{}; std::string internal_analyticTag{}; std::string resizeMethod{"auto"}; Float resizeMultiplier{1.f}; diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/android/react/renderer/imagemanager/ImageRequestParams.h b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/android/react/renderer/imagemanager/ImageRequestParams.h index e960e01b5b5c..d28bf2b3a63f 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/android/react/renderer/imagemanager/ImageRequestParams.h +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/android/react/renderer/imagemanager/ImageRequestParams.h @@ -26,7 +26,7 @@ class ImageRequestParams { Float resizeMultiplier, bool shouldNotifyLoadEvents, SharedColor overlayColor, - SharedColor tintColor, + std::optional tintColor, Float fadeDuration, bool progressiveRenderingEnabled, ImageSource loadingIndicatorSource, @@ -55,7 +55,7 @@ class ImageRequestParams { Float resizeMultiplier{}; bool shouldNotifyLoadEvents{}; SharedColor overlayColor{}; - SharedColor tintColor{}; + std::optional tintColor{}; Float fadeDuration{}; bool progressiveRenderingEnabled{}; ImageSource loadingIndicatorSource{}; diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/android/react/renderer/imagemanager/conversions.h b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/android/react/renderer/imagemanager/conversions.h index bf4ca03661d7..f68374914a9d 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/android/react/renderer/imagemanager/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/platform/android/react/renderer/imagemanager/conversions.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -75,8 +76,16 @@ inline void serializeImageRequestParams(MapBufferBuilder &builder, const ImageRe if (isColorMeaningful(imageRequestParams.overlayColor)) { builder.putInt(IS_KEY_OVERLAY_COLOR, toAndroidRepr(imageRequestParams.overlayColor)); } - if (isColorMeaningful(imageRequestParams.tintColor)) { - builder.putInt(IS_KEY_TINT_COLOR, toAndroidRepr(imageRequestParams.tintColor)); + if (ReactNativeFeatureFlags::enableImageTransparentTintColor()) { + if (imageRequestParams.tintColor.has_value()) { + builder.putInt(IS_KEY_TINT_COLOR, toAndroidRepr(imageRequestParams.tintColor.value())); + } + } else { + // pre-`std::optional` behavior + SharedColor tintColor = imageRequestParams.tintColor.value_or(SharedColor{}); + if (isColorMeaningful(tintColor)) { + builder.putInt(IS_KEY_TINT_COLOR, toAndroidRepr(tintColor)); + } } builder.putInt(IS_KEY_FADE_DURATION, static_cast(imageRequestParams.fadeDuration)); builder.putBool(IS_KEY_PROGRESSIVE_RENDERING_ENABLED, imageRequestParams.progressiveRenderingEnabled); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 5caa29627d62..dff12a0398b3 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -2594,7 +2594,7 @@ class facebook::react::ImageProps : public facebook::react::HostPlatformViewProp public facebook::react::ImageSource loadingIndicatorSource; public facebook::react::ImageSources sources; public facebook::react::SharedColor overlayColor; - public facebook::react::SharedColor tintColor; + public std::optional tintColor; public std::string resizeMethod; public virtual facebook::react::ComponentName getDiffPropsImplementationTarget() const override; public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override; @@ -2616,7 +2616,7 @@ class facebook::react::ImageRequest { class facebook::react::ImageRequestParams { public ImageRequestParams() = default; - public ImageRequestParams(facebook::react::Float blurRadius, facebook::react::ImageSource defaultSource, facebook::react::ImageResizeMode resizeMode, std::string resizeMethod, facebook::react::Float resizeMultiplier, bool shouldNotifyLoadEvents, facebook::react::SharedColor overlayColor, facebook::react::SharedColor tintColor, facebook::react::Float fadeDuration, bool progressiveRenderingEnabled, facebook::react::ImageSource loadingIndicatorSource, std::string analyticTag, facebook::react::Size size); + public ImageRequestParams(facebook::react::Float blurRadius, facebook::react::ImageSource defaultSource, facebook::react::ImageResizeMode resizeMode, std::string resizeMethod, facebook::react::Float resizeMultiplier, bool shouldNotifyLoadEvents, facebook::react::SharedColor overlayColor, std::optional tintColor, facebook::react::Float fadeDuration, bool progressiveRenderingEnabled, facebook::react::ImageSource loadingIndicatorSource, std::string analyticTag, facebook::react::Size size); public bool operator==(const facebook::react::ImageRequestParams& rhs) const = default; public bool progressiveRenderingEnabled; public bool shouldNotifyLoadEvents; @@ -2627,8 +2627,8 @@ class facebook::react::ImageRequestParams { public facebook::react::ImageSource defaultSource; public facebook::react::ImageSource loadingIndicatorSource; public facebook::react::SharedColor overlayColor; - public facebook::react::SharedColor tintColor; public facebook::react::Size size; + public std::optional tintColor; public std::string analyticTag; public std::string resizeMethod; } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 957dedf6aba8..bae821b61ec2 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -2576,7 +2576,7 @@ class facebook::react::ImageProps : public facebook::react::HostPlatformViewProp public facebook::react::ImageSource loadingIndicatorSource; public facebook::react::ImageSources sources; public facebook::react::SharedColor overlayColor; - public facebook::react::SharedColor tintColor; + public std::optional tintColor; public std::string resizeMethod; public virtual facebook::react::ComponentName getDiffPropsImplementationTarget() const override; public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override; @@ -2598,7 +2598,7 @@ class facebook::react::ImageRequest { class facebook::react::ImageRequestParams { public ImageRequestParams() = default; - public ImageRequestParams(facebook::react::Float blurRadius, facebook::react::ImageSource defaultSource, facebook::react::ImageResizeMode resizeMode, std::string resizeMethod, facebook::react::Float resizeMultiplier, bool shouldNotifyLoadEvents, facebook::react::SharedColor overlayColor, facebook::react::SharedColor tintColor, facebook::react::Float fadeDuration, bool progressiveRenderingEnabled, facebook::react::ImageSource loadingIndicatorSource, std::string analyticTag, facebook::react::Size size); + public ImageRequestParams(facebook::react::Float blurRadius, facebook::react::ImageSource defaultSource, facebook::react::ImageResizeMode resizeMode, std::string resizeMethod, facebook::react::Float resizeMultiplier, bool shouldNotifyLoadEvents, facebook::react::SharedColor overlayColor, std::optional tintColor, facebook::react::Float fadeDuration, bool progressiveRenderingEnabled, facebook::react::ImageSource loadingIndicatorSource, std::string analyticTag, facebook::react::Size size); public bool operator==(const facebook::react::ImageRequestParams& rhs) const = default; public bool progressiveRenderingEnabled; public bool shouldNotifyLoadEvents; @@ -2609,8 +2609,8 @@ class facebook::react::ImageRequestParams { public facebook::react::ImageSource defaultSource; public facebook::react::ImageSource loadingIndicatorSource; public facebook::react::SharedColor overlayColor; - public facebook::react::SharedColor tintColor; public facebook::react::Size size; + public std::optional tintColor; public std::string analyticTag; public std::string resizeMethod; } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 016bc534b9f6..beb96517ca4e 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -2591,7 +2591,7 @@ class facebook::react::ImageProps : public facebook::react::HostPlatformViewProp public facebook::react::ImageSource loadingIndicatorSource; public facebook::react::ImageSources sources; public facebook::react::SharedColor overlayColor; - public facebook::react::SharedColor tintColor; + public std::optional tintColor; public std::string resizeMethod; public virtual facebook::react::ComponentName getDiffPropsImplementationTarget() const override; public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override; @@ -2613,7 +2613,7 @@ class facebook::react::ImageRequest { class facebook::react::ImageRequestParams { public ImageRequestParams() = default; - public ImageRequestParams(facebook::react::Float blurRadius, facebook::react::ImageSource defaultSource, facebook::react::ImageResizeMode resizeMode, std::string resizeMethod, facebook::react::Float resizeMultiplier, bool shouldNotifyLoadEvents, facebook::react::SharedColor overlayColor, facebook::react::SharedColor tintColor, facebook::react::Float fadeDuration, bool progressiveRenderingEnabled, facebook::react::ImageSource loadingIndicatorSource, std::string analyticTag, facebook::react::Size size); + public ImageRequestParams(facebook::react::Float blurRadius, facebook::react::ImageSource defaultSource, facebook::react::ImageResizeMode resizeMode, std::string resizeMethod, facebook::react::Float resizeMultiplier, bool shouldNotifyLoadEvents, facebook::react::SharedColor overlayColor, std::optional tintColor, facebook::react::Float fadeDuration, bool progressiveRenderingEnabled, facebook::react::ImageSource loadingIndicatorSource, std::string analyticTag, facebook::react::Size size); public bool operator==(const facebook::react::ImageRequestParams& rhs) const = default; public bool progressiveRenderingEnabled; public bool shouldNotifyLoadEvents; @@ -2624,8 +2624,8 @@ class facebook::react::ImageRequestParams { public facebook::react::ImageSource defaultSource; public facebook::react::ImageSource loadingIndicatorSource; public facebook::react::SharedColor overlayColor; - public facebook::react::SharedColor tintColor; public facebook::react::Size size; + public std::optional tintColor; public std::string analyticTag; public std::string resizeMethod; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 32fdfd48d335..c27595d7f77a 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -5008,7 +5008,7 @@ class facebook::react::ImageProps : public facebook::react::HostPlatformViewProp public facebook::react::ImageSource loadingIndicatorSource; public facebook::react::ImageSources sources; public facebook::react::SharedColor overlayColor; - public facebook::react::SharedColor tintColor; + public std::optional tintColor; public std::string resizeMethod; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index bee7a8777e7b..c8a738e9069d 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -4979,7 +4979,7 @@ class facebook::react::ImageProps : public facebook::react::HostPlatformViewProp public facebook::react::ImageSource loadingIndicatorSource; public facebook::react::ImageSources sources; public facebook::react::SharedColor overlayColor; - public facebook::react::SharedColor tintColor; + public std::optional tintColor; public std::string resizeMethod; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 01edab95afd9..3d09b6d29173 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -5005,7 +5005,7 @@ class facebook::react::ImageProps : public facebook::react::HostPlatformViewProp public facebook::react::ImageSource loadingIndicatorSource; public facebook::react::ImageSources sources; public facebook::react::SharedColor overlayColor; - public facebook::react::SharedColor tintColor; + public std::optional tintColor; public std::string resizeMethod; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index e33b73a68d4b..0c7d0a5b8a1c 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -1729,7 +1729,7 @@ class facebook::react::ImageProps : public facebook::react::HostPlatformViewProp public facebook::react::ImageSource loadingIndicatorSource; public facebook::react::ImageSources sources; public facebook::react::SharedColor overlayColor; - public facebook::react::SharedColor tintColor; + public std::optional tintColor; public std::string resizeMethod; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 7dcd0bb477e2..4b6b80e8c2b8 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -1712,7 +1712,7 @@ class facebook::react::ImageProps : public facebook::react::HostPlatformViewProp public facebook::react::ImageSource loadingIndicatorSource; public facebook::react::ImageSources sources; public facebook::react::SharedColor overlayColor; - public facebook::react::SharedColor tintColor; + public std::optional tintColor; public std::string resizeMethod; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index f6ba681cc2a7..a0e1c10862b6 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -1726,7 +1726,7 @@ class facebook::react::ImageProps : public facebook::react::HostPlatformViewProp public facebook::react::ImageSource loadingIndicatorSource; public facebook::react::ImageSources sources; public facebook::react::SharedColor overlayColor; - public facebook::react::SharedColor tintColor; + public std::optional tintColor; public std::string resizeMethod; public void setProp(const facebook::react::PropsParserContext& context, facebook::react::RawPropsPropNameHash hash, const char* propName, const facebook::react::RawValue& value); }