Skip to content

Commit

Permalink
Added border curve style prop ("Squircle" effect - iOS only) (#33783)
Browse files Browse the repository at this point in the history
Summary:
<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? -->
NOTE: PR is based on #32017 which went stale for quite a long time but can now safely be closed

![](https://preview.redd.it/nuvl4746ys471.png?width=960&crop=smart&auto=webp&s=084a517a645364ac246b70b7fa8e0f2470cc7af3)

Since iOS 13+, it is possible to change the corner curve property on iOS in order to smoothen border radius and make it more "rounded" (also called "squircle")
Here's an [article](https://medium.com/arthurofbabylon/a-smooth-corner-radius-in-ios-54b80aa2d372) explaining in details what it is.
This property is also built in figma, but currently there is no way to implement this directly with react-native despite it being available natively on iOS.

Many open source react-native libraries were created in order to simulate this behaviour:
[react-native-super-ellipse-mask](https://github.com/everdrone/react-native-super-ellipse-mask)
[react-native-squircle-view](https://github.com/everdrone/react-native-squircle-view)
[react-native-figma-squircle](https://github.com/tienphaw/react-native-figma-squircle)

But they rely on creating an SVG shape with the smoothed corners and masking the view behind. This makes it not very performant (flickering on mounting was a common side-effect)

This PR aims at implementing the property natively.

PR for the docs update: facebook/react-native-website#2785

## Changelog

<!-- Help reviewers and the release process by writing your own changelog entry. For an example, see:
https://github.com/facebook/react-native/wiki/Changelog
-->

[iOS] [Added] - Added `borderCurve` style prop for smooth border radius (squircle effect)

Pull Request resolved: #33783

Test Plan:
We used the RNTester app and added an example with `cornerCurve ` set to `'continuous'` (only on iOS).

As the difference is quite subtle, we also made some more tests to better illustrate the difference (these are not in the RN-tester app):

![IMG_0810](https://user-images.githubusercontent.com/19872411/133893536-26207c53-aade-4583-9eef-7a1739b6907b.PNG)

We overlapped two views with `position: absolute`, the one in the background has a red background and has `cornerRadius` set to `false`, and the one in the foreground is set to `true`. We can clearly see where the borders differs on the corners.

Reviewed By: sammy-SC

Differential Revision: D37883631

Pulled By: cipolleschi

fbshipit-source-id: 09f06de9628fa326323eba63875de30102c4a59e
  • Loading branch information
eric-edouard authored and facebook-github-bot committed Jul 21, 2022
1 parent 64528e5 commit 8993ffc
Show file tree
Hide file tree
Showing 17 changed files with 147 additions and 14 deletions.
1 change: 1 addition & 0 deletions BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ REACT_PUBLIC_HEADERS = {
"React/RCTAnimationType.h": RCTVIEWS_PATH + "RCTAnimationType.h",
"React/RCTAssert.h": RCTBASE_PATH + "RCTAssert.h",
"React/RCTAutoInsetsProtocol.h": RCTVIEWS_PATH + "RCTAutoInsetsProtocol.h",
"React/RCTBorderCurve.h": RCTVIEWS_PATH + "RCTBorderCurve.h",
"React/RCTBorderDrawing.h": RCTVIEWS_PATH + "RCTBorderDrawing.h",
"React/RCTBorderStyle.h": RCTVIEWS_PATH + "RCTBorderStyle.h",
"React/RCTBridge+Private.h": RCTBASE_PATH + "RCTBridge+Private.h",
Expand Down
1 change: 1 addition & 0 deletions Libraries/Components/View/ReactNativeStyleAttributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
borderBottomRightRadius: true,
borderBottomStartRadius: true,
borderColor: colorAttributes,
borderCurve: true,
borderEndColor: colorAttributes,
borderLeftColor: colorAttributes,
borderRadius: true,
Expand Down
1 change: 1 addition & 0 deletions Libraries/NativeComponent/BaseViewConfig.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ const validAttributesForNonEventProps = {
removeClippedSubviews: true,
borderRadius: true,
borderColor: {process: require('../StyleSheet/processColor')},
borderCurve: true,
borderWidth: true,
borderStyle: true,
hitSlop: {diff: require('../Utilities/differ/insetsDiffer')},
Expand Down
1 change: 1 addition & 0 deletions Libraries/StyleSheet/StyleSheetTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,7 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{
backfaceVisibility?: 'visible' | 'hidden',
backgroundColor?: ____ColorValue_Internal,
borderColor?: ____ColorValue_Internal,
borderCurve?: 'circular' | 'continuous',
borderBottomColor?: ____ColorValue_Internal,
borderEndColor?: ____ColorValue_Internal,
borderLeftColor?: ____ColorValue_Internal,
Expand Down
2 changes: 2 additions & 0 deletions React/Base/RCTConvert.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#import <UIKit/UIKit.h>

#import <React/RCTAnimationType.h>
#import <React/RCTBorderCurve.h>
#import <React/RCTBorderStyle.h>
#import <React/RCTDefines.h>
#import <React/RCTLog.h>
Expand Down Expand Up @@ -130,6 +131,7 @@ typedef BOOL css_backface_visibility_t;
+ (RCTPointerEvents)RCTPointerEvents:(id)json;
+ (RCTAnimationType)RCTAnimationType:(id)json;
+ (RCTBorderStyle)RCTBorderStyle:(id)json;
+ (RCTBorderCurve)RCTBorderCurve:(id)json;
+ (RCTTextDecorationLineType)RCTTextDecorationLineType:(id)json;

@end
Expand Down
9 changes: 9 additions & 0 deletions React/Base/RCTConvert.m
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,15 @@ + (NSLocale *)NSLocale:(id)json
RCTBorderStyleSolid,
integerValue)

RCT_ENUM_CONVERTER(
RCTBorderCurve,
(@{
@"circular" : @(RCTBorderCurveCircular),
@"continuous" : @(RCTBorderCurveContinuous),
}),
RCTBorderCurveCircular,
integerValue)

RCT_ENUM_CONVERTER(
RCTTextDecorationLineType,
(@{
Expand Down
15 changes: 15 additions & 0 deletions React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,18 @@ static void RCTReleaseRCTBorderColors(RCTBorderColors borderColors)
CGColorRelease(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 @@ -580,6 +592,9 @@ - (void)invalidateLayer
layer.borderColor = borderColor;
CGColorRelease(borderColor);
layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft;
if (@available(iOS 13.0, *)) {
layer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft);
}
layer.backgroundColor = _backgroundColor.CGColor;
} else {
if (!_borderLayer) {
Expand Down
13 changes: 13 additions & 0 deletions React/Views/RCTBorderCurve.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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 <Foundation/Foundation.h>

typedef NS_ENUM(NSInteger, RCTBorderCurve) {
RCTBorderCurveContinuous = 0,
RCTBorderCurveCircular,
};
6 changes: 6 additions & 0 deletions React/Views/RCTView.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#import <UIKit/UIKit.h>

#import <React/RCTBorderCurve.h>
#import <React/RCTBorderStyle.h>
#import <React/RCTComponent.h>
#import <React/RCTPointerEvents.h>
Expand Down Expand Up @@ -93,6 +94,11 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait;
@property (nonatomic, assign) CGFloat borderEndWidth;
@property (nonatomic, assign) CGFloat borderWidth;

/**
* Border curve.
*/
@property (nonatomic, assign) RCTBorderCurve borderCurve;

/**
* Border styles.
*/
Expand Down
20 changes: 18 additions & 2 deletions React/Views/RCTView.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#import <React/RCTMockDef.h>

#import "RCTAutoInsetsProtocol.h"
#import "RCTBorderCurve.h"
#import "RCTBorderDrawing.h"
#import "RCTI18nUtil.h"
#import "RCTLog.h"
Expand Down Expand Up @@ -126,6 +127,7 @@ - (instancetype)initWithFrame:(CGRect)frame
_borderBottomRightRadius = -1;
_borderBottomStartRadius = -1;
_borderBottomEndRadius = -1;
_borderCurve = RCTBorderCurveCircular;
_borderStyle = RCTBorderStyleSolid;
_hitTestEdgeInsets = UIEdgeInsetsZero;

Expand Down Expand Up @@ -945,6 +947,20 @@ -(void)setBorder##side##Radius : (CGFloat)radius \
setBorderRadius(TopEnd) setBorderRadius(BottomLeft) setBorderRadius(BottomRight)
setBorderRadius(BottomStart) setBorderRadius(BottomEnd)

#pragma mark - Border Curve

#define setBorderCurve(side) \
-(void)setBorder##side##Curve : (RCTBorderCurve)curve \
{ \
if (_border##side##Curve == curve) { \
return; \
} \
_border##side##Curve = curve; \
[self.layer setNeedsDisplay]; \
}

setBorderCurve()

#pragma mark - Border Style

#define setBorderStyle(side) \
Expand All @@ -957,6 +973,6 @@ -(void)setBorder##side##Style : (RCTBorderStyle)style \
[self.layer setNeedsDisplay]; \
}

setBorderStyle()
setBorderStyle()

@end
@end
14 changes: 14 additions & 0 deletions React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#import "RCTViewManager.h"

#import "RCTAssert.h"
#import "RCTBorderCurve.h"
#import "RCTBorderStyle.h"
#import "RCTBridge.h"
#import "RCTConvert+Transform.h"
Expand Down Expand Up @@ -264,6 +265,19 @@ - (RCTShadowView *)shadowView
view.removeClippedSubviews = json ? [RCTConvert BOOL:json] : defaultView.removeClippedSubviews;
}
}
RCT_CUSTOM_VIEW_PROPERTY(borderCurve, RCTBorderCurve, RCTView)
{
if (@available(iOS 13.0, *)) {
switch ([RCTConvert RCTBorderCurve:json]) {
case RCTBorderCurveContinuous:
view.layer.cornerCurve = kCACornerCurveContinuous;
break;
case RCTBorderCurveCircular:
view.layer.cornerCurve = kCACornerCurveCircular;
break;
}
}
}
RCT_CUSTOM_VIEW_PROPERTY(borderRadius, CGFloat, RCTView)
{
if ([view respondsToSelector:@selector(setBorderRadius:)]) {
Expand Down
10 changes: 10 additions & 0 deletions ReactCommon/react/renderer/components/view/ViewProps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ ViewProps::ViewProps(
"Color",
sourceProps.borderColors,
{})),
borderCurves(
Props::enablePropIteratorSetter ? sourceProps.borderCurves
: convertRawProp(
context,
rawProps,
"border",
"Curve",
sourceProps.borderCurves,
{})),
borderStyles(
Props::enablePropIteratorSetter ? sourceProps.borderStyles
: convertRawProp(
Expand Down Expand Up @@ -412,6 +421,7 @@ BorderMetrics ViewProps::resolveBorderMetrics(
/* .borderWidths = */ borderWidths.resolve(isRTL, 0),
/* .borderRadii = */
ensureNoOverlap(borderRadii.resolve(isRTL, 0), layoutMetrics.frame.size),
/* .borderCurves = */ borderCurves.resolve(isRTL, BorderCurve::Circular),
/* .borderStyles = */ borderStyles.resolve(isRTL, BorderStyle::Solid),
};
}
Expand Down
1 change: 1 addition & 0 deletions ReactCommon/react/renderer/components/view/ViewProps.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class ViewProps : public YogaStylableProps, public AccessibilityProps {
// Borders
CascadedBorderRadii borderRadii{};
CascadedBorderColors borderColors{};
CascadedBorderCurves borderCurves{};
CascadedBorderStyles borderStyles{};

// Shadow
Expand Down
18 changes: 18 additions & 0 deletions ReactCommon/react/renderer/components/view/conversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,24 @@ inline void fromRawValue(
react_native_assert(false);
}

inline void fromRawValue(
const PropsParserContext &context,
const RawValue &value,
BorderCurve &result) {
react_native_assert(value.hasType<std::string>());
auto stringValue = (std::string)value;
if (stringValue == "circular") {
result = BorderCurve::Circular;
return;
}
if (stringValue == "continuous") {
result = BorderCurve::Continuous;
return;
}
LOG(FATAL) << "Could not parse BorderCurve:" << stringValue;
react_native_assert(false);
}

inline void fromRawValue(
const PropsParserContext &context,
const RawValue &value,
Expand Down
7 changes: 7 additions & 0 deletions ReactCommon/react/renderer/components/view/primitives.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ inline static bool operator!=(ViewEvents const &lhs, ViewEvents const &rhs) {

enum class BackfaceVisibility { Auto, Visible, Hidden };

enum class BorderCurve { Circular, Continuous };

enum class BorderStyle { Solid, Dotted, Dashed };

template <typename T>
Expand Down Expand Up @@ -202,11 +204,13 @@ struct CascadedRectangleCorners {
};

using BorderWidths = RectangleEdges<Float>;
using BorderCurves = RectangleCorners<BorderCurve>;
using BorderStyles = RectangleEdges<BorderStyle>;
using BorderColors = RectangleEdges<SharedColor>;
using BorderRadii = RectangleCorners<Float>;

using CascadedBorderWidths = CascadedRectangleEdges<Float>;
using CascadedBorderCurves = CascadedRectangleCorners<BorderCurve>;
using CascadedBorderStyles = CascadedRectangleEdges<BorderStyle>;
using CascadedBorderColors = CascadedRectangleEdges<SharedColor>;
using CascadedBorderRadii = CascadedRectangleCorners<Float>;
Expand All @@ -215,18 +219,21 @@ struct BorderMetrics {
BorderColors borderColors{};
BorderWidths borderWidths{};
BorderRadii borderRadii{};
BorderCurves borderCurves{};
BorderStyles borderStyles{};

bool operator==(const BorderMetrics &rhs) const {
return std::tie(
this->borderColors,
this->borderWidths,
this->borderRadii,
this->borderCurves,
this->borderStyles) ==
std::tie(
rhs.borderColors,
rhs.borderWidths,
rhs.borderRadii,
rhs.borderCurves,
rhs.borderStyles);
}

Expand Down
12 changes: 6 additions & 6 deletions packages/rn-tester/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@ DEPENDENCIES:
- RCTRequired (from `../../Libraries/RCTRequired`)
- RCTTypeSafety (from `../../Libraries/TypeSafety`)
- React (from `../../`)
- React-bridging (from `../../ReactCommon/react/bridging`)
- React-bridging (from `../../ReactCommon`)
- React-callinvoker (from `../../ReactCommon/callinvoker`)
- React-Codegen (from `build/generated/ios`)
- React-Core (from `../../`)
Expand Down Expand Up @@ -826,7 +826,7 @@ EXTERNAL SOURCES:
React:
:path: "../../"
React-bridging:
:path: "../../ReactCommon/react/bridging"
:path: "../../ReactCommon"
React-callinvoker:
:path: "../../ReactCommon/callinvoker"
React-Codegen:
Expand Down Expand Up @@ -905,11 +905,11 @@ SPEC CHECKSUMS:
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
RCT-Folly: 9638863070ed4e7b2be5e91385745a0ad741e9c1
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCTRequired: 1c8808cf84569265784a6c33984bbb506ada8c6e
RCTTypeSafety: b6dcb5036a808864ee8cad66ca15f263c24661cc
React: 8d809d414723bb5763093ddec7658066a21ccabc
React-bridging: 3ba3efbd3a2d7d99aad5658b8e48b1c134c16ecf
React-bridging: cc10a051eff1f03306a1d7659593d8aac3242bc3
React-callinvoker: 5f16202ad4e45f0607b1fae0f6955a8f7c87eef1
React-Codegen: 5adf19af97eb37a7d441c040521191e446255086
React-Core: 0cfb25c65d4dcb856b1807fe44a1ebe5e7ec9749
Expand All @@ -936,12 +936,12 @@ SPEC CHECKSUMS:
React-RCTVibration: 0386f50996a153b3f39cecbe7d139763ac9a9fdf
React-rncore: 6daa27c74047a9e13ce3412b99660274a5780603
React-runtimeexecutor: 97dca9247f4d3cfe0733384b189c6930fbd402b7
ReactCommon: a34f02c7251e6725e744167b9381d5dd9d016591
ReactCommon: 6cef8ed13ee2a9d7d4cf9660dbe6dd2ea6ba7104
ScreenshotManager: 71d047abd38a77310985b87f8136b620c5c61e88
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
Yoga: 1b1a12ff3d86a10565ea7cbe057d42f5e5fb2a07
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

PODFILE CHECKSUM: e067e04e7697dc3cd3d3fbd7556f77c6eccf8075
PODFILE CHECKSUM: 54d9bd86f3c8151531bd4da1d3ba2e2e1f9a6ca9

COCOAPODS: 1.11.3
30 changes: 24 additions & 6 deletions packages/rn-tester/js/examples/View/ViewExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const {
Text,
TouchableWithoutFeedback,
View,
Platform,
} = require('react-native');

class ViewBorderStyleExample extends React.Component<
Expand Down Expand Up @@ -360,12 +361,29 @@ exports.examples = [
title: 'Border Radius',
render(): React.Node {
return (
<View style={{borderWidth: 0.5, borderRadius: 5, padding: 5}}>
<Text style={{fontSize: 11}}>
Too much use of `borderRadius` (especially large radii) on anything
which is scrolling may result in dropped frames. Use sparingly.
</Text>
</View>
<>
<View style={{borderWidth: 0.5, borderRadius: 5, padding: 5}}>
<Text style={{fontSize: 11}}>
Too much use of `borderRadius` (especially large radii) on
anything which is scrolling may result in dropped frames. Use
sparingly.
</Text>
</View>
{Platform.OS === 'ios' && (
<View
style={{
borderRadius: 20,
padding: 8,
marginTop: 12,
backgroundColor: '#527FE4',
borderCurve: 'continuous',
}}>
<Text style={{fontSize: 16, color: 'white'}}>
View with continuous border curve
</Text>
</View>
)}
</>
);
},
},
Expand Down

0 comments on commit 8993ffc

Please sign in to comment.