Added border curve style prop ("Squircle" effect - iOS only) (#33783)

Summary:
<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? -->
NOTE: PR is based on https://github.com/facebook/react-native/pull/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: https://github.com/facebook/react-native-website/pull/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: https://github.com/facebook/react-native/pull/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
This commit is contained in:
Eric Edouard 2022-07-21 04:11:30 -07:00 коммит произвёл Facebook GitHub Bot
Родитель 64528e5faa
Коммит 8993ffc82e
17 изменённых файлов: 147 добавлений и 14 удалений

1
BUCK
Просмотреть файл

@ -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",

Просмотреть файл

@ -98,6 +98,7 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
borderBottomRightRadius: true,
borderBottomStartRadius: true,
borderColor: colorAttributes,
borderCurve: true,
borderEndColor: colorAttributes,
borderLeftColor: colorAttributes,
borderRadius: true,

Просмотреть файл

@ -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')},

Просмотреть файл

@ -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,

Просмотреть файл

@ -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>
@ -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

Просмотреть файл

@ -345,6 +345,15 @@ RCT_ENUM_CONVERTER(
RCTBorderStyleSolid,
integerValue)
RCT_ENUM_CONVERTER(
RCTBorderCurve,
(@{
@"circular" : @(RCTBorderCurveCircular),
@"continuous" : @(RCTBorderCurveContinuous),
}),
RCTBorderCurveCircular,
integerValue)
RCT_ENUM_CONVERTER(
RCTTextDecorationLineType,
(@{

Просмотреть файл

@ -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) {
@ -580,6 +592,9 @@ static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle)
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) {

Просмотреть файл

@ -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,
};

Просмотреть файл

@ -7,6 +7,7 @@
#import <UIKit/UIKit.h>
#import <React/RCTBorderCurve.h>
#import <React/RCTBorderStyle.h>
#import <React/RCTComponent.h>
#import <React/RCTPointerEvents.h>
@ -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.
*/

Просмотреть файл

@ -10,6 +10,7 @@
#import <React/RCTMockDef.h>
#import "RCTAutoInsetsProtocol.h"
#import "RCTBorderCurve.h"
#import "RCTBorderDrawing.h"
#import "RCTI18nUtil.h"
#import "RCTLog.h"
@ -126,6 +127,7 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
_borderBottomRightRadius = -1;
_borderBottomStartRadius = -1;
_borderBottomEndRadius = -1;
_borderCurve = RCTBorderCurveCircular;
_borderStyle = RCTBorderStyleSolid;
_hitTestEdgeInsets = UIEdgeInsetsZero;
@ -945,6 +947,20 @@ setBorderColor() setBorderColor(Top) setBorderColor(Right) setBorderColor(Bottom
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) \
@ -957,6 +973,6 @@ setBorderColor() setBorderColor(Top) setBorderColor(Right) setBorderColor(Bottom
[self.layer setNeedsDisplay]; \
}
setBorderStyle()
setBorderStyle()
@end
@end

Просмотреть файл

@ -8,6 +8,7 @@
#import "RCTViewManager.h"
#import "RCTAssert.h"
#import "RCTBorderCurve.h"
#import "RCTBorderStyle.h"
#import "RCTBridge.h"
#import "RCTConvert+Transform.h"
@ -264,6 +265,19 @@ RCT_CUSTOM_VIEW_PROPERTY(removeClippedSubviews, BOOL, RCTView)
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:)]) {

Просмотреть файл

@ -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(
@ -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),
};
}

Просмотреть файл

@ -51,6 +51,7 @@ class ViewProps : public YogaStylableProps, public AccessibilityProps {
// Borders
CascadedBorderRadii borderRadii{};
CascadedBorderColors borderColors{};
CascadedBorderCurves borderCurves{};
CascadedBorderStyles borderStyles{};
// Shadow

Просмотреть файл

@ -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,

Просмотреть файл

@ -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>
@ -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>;
@ -215,6 +219,7 @@ struct BorderMetrics {
BorderColors borderColors{};
BorderWidths borderWidths{};
BorderRadii borderRadii{};
BorderCurves borderCurves{};
BorderStyles borderStyles{};
bool operator==(const BorderMetrics &rhs) const {
@ -222,11 +227,13 @@ struct BorderMetrics {
this->borderColors,
this->borderWidths,
this->borderRadii,
this->borderCurves,
this->borderStyles) ==
std::tie(
rhs.borderColors,
rhs.borderWidths,
rhs.borderRadii,
rhs.borderCurves,
rhs.borderStyles);
}

Просмотреть файл

@ -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 `../../`)
@ -826,7 +826,7 @@ EXTERNAL SOURCES:
React:
:path: "../../"
React-bridging:
:path: "../../ReactCommon/react/bridging"
:path: "../../ReactCommon"
React-callinvoker:
:path: "../../ReactCommon/callinvoker"
React-Codegen:
@ -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
@ -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

Просмотреть файл

@ -17,6 +17,7 @@ const {
Text,
TouchableWithoutFeedback,
View,
Platform,
} = require('react-native');
class ViewBorderStyleExample extends React.Component<
@ -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>
)}
</>
);
},
},