Skip to content

Commit

Permalink
Implement outline properties on iOS (#46444)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #46444

This diff adds:

`outline-width`: https://developer.mozilla.org/en-US/docs/Web/CSS/outline-width
`outline-color`: https://developer.mozilla.org/en-US/docs/Web/CSS/outline-color
`outline-style`: https://developer.mozilla.org/en-US/docs/Web/CSS/outline-style
`outline-offset`: https://developer.mozilla.org/en-US/docs/Web/CSS/outline-offset

Using `BackgroundStyleApplicator`

Changelog: [iOS] [Added] - Outline properties `outline-width`, `outline-color`, `outline-style` & `outline-offset`

Reviewed By: joevilches

Differential Revision: D62273339

fbshipit-source-id: 0ed775218e7d1dfb13bbf1760bc6ec331a388b64
  • Loading branch information
jorge-cab authored and facebook-github-bot committed Sep 19, 2024
1 parent 66c3074 commit 1288e38
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ @implementation RCTViewComponentView {
UIColor *_backgroundColor;
CALayer *_backgroundColorLayer;
__weak CALayer *_borderLayer;
CALayer *_outlineLayer;
CALayer *_boxShadowLayer;
CALayer *_filterLayer;
NSMutableArray<CAGradientLayer *> *_gradientLayers;
Expand Down Expand Up @@ -319,6 +320,14 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
needsInvalidateLayer = YES;
}

// `outline`
if (oldViewProps.outlineStyle != newViewProps.outlineStyle ||
oldViewProps.outlineColor != newViewProps.outlineColor ||
oldViewProps.outlineOffset != newViewProps.outlineOffset ||
oldViewProps.outlineWidth != newViewProps.outlineWidth) {
needsInvalidateLayer = YES;
}

// `nativeId`
if (oldViewProps.nativeId != newViewProps.nativeId) {
self.nativeId = RCTNSStringFromStringNilIfEmpty(newViewProps.nativeId);
Expand Down Expand Up @@ -651,6 +660,54 @@ static RCTCornerRadii RCTCornerRadiiFromBorderRadii(BorderRadii borderRadii)
.bottomRightVertical = (CGFloat)borderRadii.bottomRight.vertical};
}

static RCTCornerRadii RCTCreateOutlineCornerRadiiFromBorderRadii(const BorderRadii &borderRadii, CGFloat outlineWidth)
{
return RCTCornerRadii{
borderRadii.topLeft.horizontal != 0 ? borderRadii.topLeft.horizontal + outlineWidth : 0,
borderRadii.topLeft.vertical != 0 ? borderRadii.topLeft.vertical + outlineWidth : 0,
borderRadii.topRight.horizontal != 0 ? borderRadii.topRight.horizontal + outlineWidth : 0,
borderRadii.topRight.vertical != 0 ? borderRadii.topRight.vertical + outlineWidth : 0,
borderRadii.bottomLeft.horizontal != 0 ? borderRadii.bottomLeft.horizontal + outlineWidth : 0,
borderRadii.bottomLeft.vertical != 0 ? borderRadii.bottomLeft.vertical + outlineWidth : 0,
borderRadii.bottomRight.horizontal != 0 ? borderRadii.bottomRight.horizontal + outlineWidth : 0,
borderRadii.bottomRight.vertical != 0 ? borderRadii.bottomRight.vertical + outlineWidth : 0};
}

// To be used for CSS properties like `border` and `outline`.
static void RCTAddContourEffectToLayer(
CALayer *layer,
const RCTCornerRadii &cornerRadii,
const RCTBorderColors &contourColors,
const UIEdgeInsets &contourInsets,
const RCTBorderStyle &contourStyle)
{
UIImage *image = RCTGetBorderImage(
contourStyle, layer.bounds.size, cornerRadii, contourInsets, contourColors, [UIColor clearColor].CGColor, NO);

if (image == nil) {
layer.contents = nil;
} else {
CGSize imageSize = image.size;
UIEdgeInsets imageCapInsets = image.capInsets;
CGRect contentsCenter = CGRect{
CGPoint{imageCapInsets.left / imageSize.width, imageCapInsets.top / imageSize.height},
CGSize{(CGFloat)1.0 / imageSize.width, (CGFloat)1.0 / imageSize.height}};
layer.contents = (id)image.CGImage;
layer.contentsScale = image.scale;

BOOL isResizable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero);
if (isResizable) {
layer.contentsCenter = contentsCenter;
} else {
layer.contentsCenter = CGRect{CGPoint{0.0, 0.0}, CGSize{1.0, 1.0}};
}
}

// If mutations are applied inside of Animation block, it may cause layer to be animated.
// To stop that, imperatively remove all animations from layer.
[layer removeAllAnimations];
}

static RCTBorderColors RCTCreateRCTBorderColorsFromBorderColors(BorderColors borderColors)
{
return RCTBorderColors{
Expand Down Expand Up @@ -692,12 +749,25 @@ static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle)
}
}

static RCTBorderStyle RCTBorderStyleFromOutlineStyle(OutlineStyle outlineStyle)
{
switch (outlineStyle) {
case OutlineStyle::Solid:
return RCTBorderStyleSolid;
case OutlineStyle::Dotted:
return RCTBorderStyleDotted;
case OutlineStyle::Dashed:
return RCTBorderStyleDashed;
}
}

- (BOOL)styleWouldClipOverflowInk
{
const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics);
BOOL nonZeroBorderWidth = !(borderMetrics.borderWidths.isUniform() && borderMetrics.borderWidths.left == 0);
BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox();
return _props->getClipsContentToBounds() && (!_props->boxShadow.empty() || (clipToPaddingBox && nonZeroBorderWidth));
return _props->getClipsContentToBounds() &&
((!_props->boxShadow.empty() || (clipToPaddingBox && nonZeroBorderWidth)) || _props->outlineWidth != 0);
}

// This UIView is the UIView that holds all subviews. It is sometimes not self
Expand Down Expand Up @@ -861,40 +931,49 @@ - (void)invalidateLayer
layer.cornerRadius = 0;

RCTBorderColors borderColors = RCTCreateRCTBorderColorsFromBorderColors(borderMetrics.borderColors);
UIImage *image = RCTGetBorderImage(
RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left),
layer.bounds.size,

RCTAddContourEffectToLayer(
_borderLayer,
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths),
borderColors,
[UIColor clearColor].CGColor,
NO);
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths),
RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left));

RCTReleaseRCTBorderColors(borderColors);
}

if (image == nil) {
_borderLayer.contents = nil;
} else {
CGSize imageSize = image.size;
UIEdgeInsets imageCapInsets = image.capInsets;
CGRect contentsCenter = CGRect{
CGPoint{imageCapInsets.left / imageSize.width, imageCapInsets.top / imageSize.height},
CGSize{(CGFloat)1.0 / imageSize.width, (CGFloat)1.0 / imageSize.height}};

_borderLayer.contents = (id)image.CGImage;
_borderLayer.contentsScale = image.scale;

BOOL isResizable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero);
if (isResizable) {
_borderLayer.contentsCenter = contentsCenter;
} else {
_borderLayer.contentsCenter = CGRect{CGPoint{0.0, 0.0}, CGSize{1.0, 1.0}};
}
// outline
[_outlineLayer removeFromSuperlayer];
_outlineLayer = nil;
if (_props->outlineWidth != 0) {
if (!_outlineLayer) {
CALayer *outlineLayer = [CALayer new];
outlineLayer.magnificationFilter = kCAFilterNearest;
outlineLayer.zPosition = BACKGROUND_COLOR_ZPOSITION + 2;

[layer addSublayer:outlineLayer];
_outlineLayer = outlineLayer;
}
_outlineLayer.frame = CGRectInset(
layer.bounds, -_props->outlineOffset - _props->outlineWidth, -_props->outlineOffset - _props->outlineWidth);

if (borderMetrics.borderRadii.isUniform() && borderMetrics.borderRadii.topLeft.horizontal == 0) {
CGColorRef outlineColor = RCTCreateCGColorRefFromSharedColor(_props->outlineColor);
_outlineLayer.borderWidth = _props->outlineWidth;
_outlineLayer.borderColor = outlineColor;
CGColorRelease(outlineColor);
} else {
CGColorRef outlineColor = RCTCreateCGColorRefFromSharedColor(_props->outlineColor);

// If mutations are applied inside of Animation block, it may cause _borderLayer to be animated.
// To stop that, imperatively remove all animations from _borderLayer.
[_borderLayer removeAllAnimations];
RCTAddContourEffectToLayer(
_outlineLayer,
RCTCreateOutlineCornerRadiiFromBorderRadii(borderMetrics.borderRadii, _props->outlineWidth),
RCTBorderColors{outlineColor, outlineColor, outlineColor, outlineColor},
UIEdgeInsets{_props->outlineWidth, _props->outlineWidth, _props->outlineWidth, _props->outlineWidth},
RCTBorderStyleFromOutlineStyle(_props->outlineStyle));

CGColorRelease(outlineColor);
}
}

// filter
Expand Down
13 changes: 12 additions & 1 deletion packages/rn-tester/js/examples/View/ViewExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,18 @@ function OutlineExample(): React.Node {
},
]}
/>
<View
style={[
defaultStyleSize,
{
borderColor: 'green',
borderWidth: 12,
outlineWidth: 4,
outlineOffset: -8,
outlineColor: 'orange',
},
]}
/>
<View
style={[
defaultStyleSize,
Expand Down Expand Up @@ -1298,7 +1310,6 @@ export default ({
{
title: 'Outline',
name: 'outline',
platform: 'android',
render: OutlineExample,
},
],
Expand Down

0 comments on commit 1288e38

Please sign in to comment.