Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ - (instancetype)initWithFrame:(CGRect)frame
_props = defaultProps;

_imageView = [RCTUIImageViewAnimated new];
_imageView.clipsToBounds = YES;
_imageView.contentMode = RCTContentModeFromImageResizeMode(defaultProps->resizeMode);
_imageView.layer.minificationFilter = kCAFilterTrilinear;
_imageView.layer.magnificationFilter = kCAFilterTrilinear;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#import <CoreGraphics/CoreGraphics.h>
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
#import <optional>
#import <ranges>

#import <RCTSwiftUIWrapper/RCTSwiftUIContainerViewWrapper.h>
Expand All @@ -19,6 +20,7 @@
#import <React/RCTBorderDrawing.h>
#import <React/RCTBoxShadow.h>
#import <React/RCTConversions.h>
#import <React/RCTLayerCornerConfiguration.h>
#import <React/RCTLinearGradient.h>
#import <React/RCTLocalizedString.h>
#import <React/RCTRadialGradient.h>
Expand Down Expand Up @@ -855,18 +857,6 @@ static RCTBorderColors RCTCreateRCTBorderColorsFromBorderColors(BorderColors bor
.right = RCTUIColorFromSharedColor(borderColors.right)};
}

static CALayerCornerCurve CornerCurveFromBorderCurve(BorderCurve borderCurve)
{
// The constants are available only starting from iOS 13
// CALayerCornerCurve is a typealias on NSString *
switch (borderCurve) {
case BorderCurve::Continuous:
return @"continuous"; // kCACornerCurveContinuous;
case BorderCurve::Circular:
return @"circular"; // kCACornerCurveCircular;
}
}

static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle)
{
switch (borderStyle) {
Expand Down Expand Up @@ -1041,10 +1031,15 @@ - (void)invalidateLayer
[self setHoverStyle:hoverStyle];
}
#endif

const std::optional<RCTLayerCornerConfiguration> layerCornerConfiguration =
RCTGetLayerCornerConfiguration(borderMetrics);
const bool layerCornersAreRepresentable = layerCornerConfiguration.has_value();

const bool useCoreAnimationBorderRendering =
borderMetrics.borderColors.isUniform() && borderMetrics.borderWidths.isUniform() &&
borderMetrics.borderStyles.isUniform() && borderMetrics.borderStyles.left == BorderStyle::Solid &&
areBorderRadiiCircular(borderMetrics.borderRadii) &&
layerCornersAreRepresentable &&
(
// iOS draws borders in front of the content whereas CSS draws them behind
// the content. For this reason, only use iOS border drawing when clipping
Expand Down Expand Up @@ -1085,8 +1080,7 @@ - (void)invalidateLayer
layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left;
UIColor *borderColor = RCTUIColorFromSharedColor(borderMetrics.borderColors.left);
layer.borderColor = borderColor.CGColor;
layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft.horizontal;
layer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft);
RCTApplyLayerCornerConfiguration(layer, *layerCornerConfiguration);
} else {
if (!_borderLayer) {
CALayer *borderLayer = [CALayer new];
Expand Down Expand Up @@ -1126,7 +1120,7 @@ - (void)invalidateLayer
_outlineLayer.frame = CGRectInset(
layer.bounds, -_props->outlineOffset - _props->outlineWidth, -_props->outlineOffset - _props->outlineWidth);

if (areBorderRadiiCircular(borderMetrics.borderRadii) && borderMetrics.borderRadii.topLeft.horizontal == 0) {
if (layerCornersAreRepresentable && layerCornerConfiguration->cornerRadius == 0) {
UIColor *outlineColor = RCTUIColorFromSharedColor(_props->outlineColor);
_outlineLayer.borderWidth = _props->outlineWidth;
_outlineLayer.borderColor = outlineColor.CGColor;
Expand Down Expand Up @@ -1298,41 +1292,49 @@ - (void)invalidateLayer
}

// clipping
self.currentContainerView.layer.mask = nil;
if (self.currentContainerView.clipsToBounds) {
UIView *currentContainerView = self.currentContainerView;
currentContainerView.layer.mask = nil;
if (currentContainerView.clipsToBounds) {
BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox();
if (!clipToPaddingBox) {
if (areBorderRadiiCircular(borderMetrics.borderRadii)) {
self.currentContainerView.layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
if (layerCornersAreRepresentable) {
RCTApplyLayerCornerConfiguration(currentContainerView.layer, *layerCornerConfiguration);
} else {
CALayer *maskLayer =
[self createMaskLayer:self.bounds
cornerInsets:RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)];
self.currentContainerView.layer.mask = maskLayer;
currentContainerView.layer.cornerRadius = 0;
currentContainerView.layer.mask = maskLayer;
}

for (UIView *subview in self.currentContainerView.subviews) {
if ([subview isKindOfClass:[UIImageView class]]) {
RCTCornerInsets cornerInsets = RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths));

// If the subview is an image view, we have to apply the mask directly to the image view's layer,
// otherwise the image might overflow with the border radius.
subview.layer.mask = [self createMaskLayer:subview.bounds cornerInsets:cornerInsets];
if (!layerCornersAreRepresentable &&
(borderMetrics.borderColors.left || borderMetrics.borderColors.right || borderMetrics.borderColors.top ||
borderMetrics.borderColors.bottom)) {
for (UIView *subview in currentContainerView.subviews) {
if ([subview isKindOfClass:[UIImageView class]]) {
RCTCornerInsets cornerInsets = RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths));

// If the subview is an image view, we have to apply the mask directly to the image view's layer,
// otherwise the image might overflow with the border radius.
// Applying a mask is rendering wise expensive so we only apply it when needed, which is only
// for none uniform border radii (that are actually visible by color).
subview.layer.mask = [self createMaskLayer:subview.bounds cornerInsets:cornerInsets];
}
}
}
} else if (
!borderMetrics.borderWidths.isUniform() || borderMetrics.borderWidths.left != 0 ||
!areBorderRadiiCircular(borderMetrics.borderRadii)) {
!layerCornersAreRepresentable) {
CALayer *maskLayer = [self createMaskLayer:RCTCGRectFromRect(_layoutMetrics.getPaddingFrame())
cornerInsets:RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths))];
self.currentContainerView.layer.mask = maskLayer;
currentContainerView.layer.mask = maskLayer;
} else {
self.currentContainerView.layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
RCTApplyLayerCornerConfiguration(currentContainerView.layer, *layerCornerConfiguration);
}
}
}
Expand All @@ -1344,10 +1346,9 @@ - (void)shapeLayerToMatchView:(CALayer *)layer borderMetrics:(BorderMetrics)bord
// Bounds is needed here to account for scaling transforms properly and ensure
// we do not scale twice
layer.frame = CGRectMake(0, 0, self.layer.bounds.size.width, self.layer.bounds.size.height);
if (areBorderRadiiCircular(borderMetrics.borderRadii)) {
if (const auto cornerConfiguration = RCTGetLayerCornerConfiguration(borderMetrics)) {
layer.mask = nil;
layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
layer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft);
RCTApplyLayerCornerConfiguration(layer, *cornerConfiguration);
} else {
CAShapeLayer *maskLayer = [self
createMaskLayer:self.bounds
Expand Down Expand Up @@ -1714,6 +1715,7 @@ - (void)transferVisualPropertiesFromView:(UIView *)sourceView toView:(UIView *)d
// corner
destinationView.layer.cornerRadius = sourceView.layer.cornerRadius;
sourceView.layer.cornerRadius = 0;
destinationView.layer.maskedCorners = sourceView.layer.maskedCorners;
destinationView.layer.cornerCurve = sourceView.layer.cornerCurve;

// custom layers
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import <optional>

#import <QuartzCore/QuartzCore.h>
#import <react/renderer/components/view/primitives.h>

/*
* Describes how a view's border radii can be rendered through CoreAnimation's
* `cornerRadius` / `maskedCorners` fast path instead of a `CAShapeLayer` mask.
*/
struct RCTLayerCornerConfiguration {
CGFloat cornerRadius{0};
CACornerMask maskedCorners{0};
facebook::react::BorderCurve cornerCurve{facebook::react::BorderCurve::Circular};
bool hasRoundedCorner{false};
};

/*
* Returns a corner configuration when `borderMetrics` can be represented with a
* single `cornerRadius` + `maskedCorners`, i.e. every rounded corner shares the
* same circular radius and curve. Returns `std::nullopt` when the radii require
* a mask layer instead (an elliptical corner, or differing radii/curves between
* rounded corners).
*/
std::optional<RCTLayerCornerConfiguration> RCTGetLayerCornerConfiguration(
const facebook::react::BorderMetrics &borderMetrics);

/*
* Applies a corner configuration to a layer's `cornerRadius`, `maskedCorners`
* and `cornerCurve`.
*/
void RCTApplyLayerCornerConfiguration(CALayer *layer, const RCTLayerCornerConfiguration &cornerConfiguration);
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import "RCTLayerCornerConfiguration.h"

using namespace facebook::react;

static CALayerCornerCurve CornerCurveFromBorderCurve(BorderCurve borderCurve)
{
switch (borderCurve) {
case BorderCurve::Continuous:
return kCACornerCurveContinuous;
case BorderCurve::Circular:
return kCACornerCurveCircular;
}
}

static bool RCTUpdateLayerCornerConfiguration(
const CornerRadii &cornerRadii,
BorderCurve cornerCurve,
CACornerMask cornerMask,
RCTLayerCornerConfiguration &cornerConfiguration)
{
if (cornerRadii.horizontal != cornerRadii.vertical) {
return false;
}

if (cornerRadii.horizontal == 0) {
return true;
}

CGFloat cornerRadius = (CGFloat)cornerRadii.horizontal;
if (cornerConfiguration.hasRoundedCorner) {
if (cornerConfiguration.cornerRadius != cornerRadius) {
return false;
}

if (cornerConfiguration.cornerCurve != cornerCurve) {
return false;
}
} else {
cornerConfiguration.cornerRadius = cornerRadius;
cornerConfiguration.cornerCurve = cornerCurve;
cornerConfiguration.hasRoundedCorner = true;
}

cornerConfiguration.maskedCorners |= cornerMask;
return true;
}

std::optional<RCTLayerCornerConfiguration> RCTGetLayerCornerConfiguration(const BorderMetrics &borderMetrics)
{
RCTLayerCornerConfiguration cornerConfiguration;

const struct {
const CornerRadii &radii;
BorderCurve curve;
CACornerMask mask;
} corners[] = {
{borderMetrics.borderRadii.topLeft, borderMetrics.borderCurves.topLeft, kCALayerMinXMinYCorner},
{borderMetrics.borderRadii.topRight, borderMetrics.borderCurves.topRight, kCALayerMaxXMinYCorner},
{borderMetrics.borderRadii.bottomLeft, borderMetrics.borderCurves.bottomLeft, kCALayerMinXMaxYCorner},
{borderMetrics.borderRadii.bottomRight, borderMetrics.borderCurves.bottomRight, kCALayerMaxXMaxYCorner},
};

for (const auto &corner : corners) {
bool isRepresentable =
RCTUpdateLayerCornerConfiguration(corner.radii, corner.curve, corner.mask, cornerConfiguration);
if (!isRepresentable) {
return std::nullopt;
}
}

if (!cornerConfiguration.hasRoundedCorner) {
cornerConfiguration.maskedCorners =
kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner | kCALayerMinXMaxYCorner | kCALayerMaxXMaxYCorner;
}

return cornerConfiguration;
}

void RCTApplyLayerCornerConfiguration(CALayer *layer, const RCTLayerCornerConfiguration &cornerConfiguration)
{
layer.cornerRadius = cornerConfiguration.cornerRadius;
layer.maskedCorners = cornerConfiguration.maskedCorners;
layer.cornerCurve = CornerCurveFromBorderCurve(cornerConfiguration.cornerCurve);
}
11 changes: 11 additions & 0 deletions packages/rn-tester/js/examples/Image/ImageExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,13 @@ const styles = StyleSheet.create({
flex: {
flex: 1,
},
imageWithUniformButNoneCircularBorderRadius: {
width: 200,
height: 100,
borderRadius: '50%',
borderWidth: 4,
borderColor: 'blue',
},
imageWithBorderRadius: {
borderRadius: 5,
},
Expand Down Expand Up @@ -1420,6 +1427,10 @@ exports.examples = [
render: function (): React.Node {
return (
<View style={styles.horizontal} testID="border-radius-example">
<Image
style={styles.imageWithUniformButNoneCircularBorderRadius}
source={fullImage}
/>
<Image
style={[styles.base, styles.imageWithBorderRadius]}
source={fullImage}
Expand Down
7 changes: 7 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -3189,6 +3189,13 @@ struct RCTFontProperties {
public UIFontWeight weight;
}

struct RCTLayerCornerConfiguration {
public CACornerMask maskedCorners;
public CGFloat cornerRadius;
public bool hasRoundedCorner;
public facebook::react::BorderCurve cornerCurve;
}

struct RCTLayoutContext {
public CGPoint absolutePosition;
public __unsafe_unretained NSHashTable<NSString*>* _Nonnull other;
Expand Down
7 changes: 7 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -3177,6 +3177,13 @@ struct RCTFontProperties {
public UIFontWeight weight;
}

struct RCTLayerCornerConfiguration {
public CACornerMask maskedCorners;
public CGFloat cornerRadius;
public bool hasRoundedCorner;
public facebook::react::BorderCurve cornerCurve;
}

struct RCTLayoutContext {
public CGPoint absolutePosition;
public __unsafe_unretained NSHashTable<NSString*>* _Nonnull other;
Expand Down
7 changes: 7 additions & 0 deletions scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api
Original file line number Diff line number Diff line change
Expand Up @@ -3189,6 +3189,13 @@ struct RCTFontProperties {
public UIFontWeight weight;
}

struct RCTLayerCornerConfiguration {
public CACornerMask maskedCorners;
public CGFloat cornerRadius;
public bool hasRoundedCorner;
public facebook::react::BorderCurve cornerCurve;
}

struct RCTLayoutContext {
public CGPoint absolutePosition;
public __unsafe_unretained NSHashTable<NSString*>* _Nonnull other;
Expand Down
Loading