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 @@ -1485,7 +1485,8 @@ - (void)invalidateLayer
CALayer *maskLayer =
[self createMaskLayer:self.bounds
cornerInsets:RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)];
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)
borderCurve:borderMetrics.borderCurves.topLeft];
self.currentContainerView.layer.mask = maskLayer;
}

Expand All @@ -1497,7 +1498,9 @@ - (void)invalidateLayer

// 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];
subview.layer.mask = [self createMaskLayer:subview.bounds
cornerInsets:cornerInsets
borderCurve:borderMetrics.borderCurves.topLeft];
}
}
} else if (
Expand All @@ -1506,7 +1509,8 @@ - (void)invalidateLayer
CALayer *maskLayer = [self createMaskLayer:RCTCGRectFromRect(_layoutMetrics.getPaddingFrame())
cornerInsets:RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths))];
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths))
borderCurve:borderMetrics.borderCurves.topLeft];
self.currentContainerView.layer.mask = maskLayer;
} else {
self.currentContainerView.layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
Expand All @@ -1526,16 +1530,59 @@ - (void)shapeLayerToMatchView:(CALayer *)layer borderMetrics:(BorderMetrics)bord
layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
layer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft);
} else {
CAShapeLayer *maskLayer = [self
CALayer *maskLayer = [self
createMaskLayer:self.bounds
cornerInsets:RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)];
cornerInsets:RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)
borderCurve:borderMetrics.borderCurves.topLeft];
layer.mask = maskLayer;
layer.cornerRadius = 0;
}
}

- (CAShapeLayer *)createMaskLayer:(CGRect)bounds cornerInsets:(RCTCornerInsets)cornerInsets
{
- (CALayer *)createMaskLayer:(CGRect)bounds
cornerInsets:(RCTCornerInsets)cornerInsets
borderCurve:(BorderCurve)borderCurve
{
if (borderCurve == BorderCurve::Continuous) {
const struct {
CGSize size;
CACornerMask mask;
} corners[] = {
{cornerInsets.topLeft, kCALayerMinXMinYCorner},
{cornerInsets.topRight, kCALayerMaxXMinYCorner},
{cornerInsets.bottomLeft, kCALayerMinXMaxYCorner},
{cornerInsets.bottomRight, kCALayerMaxXMaxYCorner}};
CGFloat radius = 0;
CACornerMask maskedCorners = 0;
bool useCoreAnimationMask = true;
for (const auto &corner : corners) {
if (CGSizeEqualToSize(corner.size, CGSizeZero)) {
continue;
}
if (corner.size.width != corner.size.height || (radius != 0 && corner.size.width != radius)) {
useCoreAnimationMask = false;
break;
}
radius = corner.size.width;
maskedCorners |= corner.mask;
}

if (useCoreAnimationMask && radius > 0 && radius <= MIN(bounds.size.width, bounds.size.height) / 2) {
// Core Animation supports a shared radius on any subset of corners. A
// rounded-rect CGPath would replace the requested continuous curve with
// circular arcs, including when only the top or bottom corners are round.
// Larger asymmetric radii need the path renderer: a native layer distorts
// the square corners when its radius exceeds half the shorter dimension.
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = bounds;
maskLayer.backgroundColor = UIColor.blackColor.CGColor;
maskLayer.cornerRadius = radius;
maskLayer.cornerCurve = CornerCurveFromBorderCurve(borderCurve);
maskLayer.maskedCorners = maskedCorners;
return maskLayer;
}
}

CGPathRef path = RCTPathCreateWithRoundedRect(bounds, cornerInsets, nil, NO);
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = path;
Expand Down
176 changes: 176 additions & 0 deletions packages/rn-tester/js/examples/View/ViewBorderCurveExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* 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.
*
* @flow strict-local
* @format
*/

import type {ViewStyle} from 'react-native';

import RNTesterText from '../../components/RNTesterText';
import * as React from 'react';
import {Image, StyleSheet, View} from 'react-native';

type Example = {
label: string,
corners: Pick<
ViewStyle,
| 'borderRadius'
| 'borderTopLeftRadius'
| 'borderTopRightRadius'
| 'borderBottomLeftRadius'
| 'borderBottomRightRadius',
>,
clip: boolean,
size?: ViewStyle,
image?: boolean,
asymmetricBorder?: boolean,
};

export default function ViewBorderCurveExample({
boundaryCases = false,
continuous = true,
}: {
boundaryCases?: boolean,
continuous?: boolean,
}): React.Node {
const [loadedImages, setLoadedImages] = React.useState<Set<string>>(
() => new Set(),
);
const curve: 'continuous' | 'circular' = continuous
? 'continuous'
: 'circular';
const examples: ReadonlyArray<Example> = boundaryCases
? [
{label: 'One corner', corners: styles.single, clip: true},
{label: 'Diagonal corners', corners: styles.diagonal, clip: true},
{label: 'Unequal radii', corners: styles.unequal, clip: true},
{label: 'Large radius', corners: styles.large, clip: true},
{
label: 'Narrow box',
corners: styles.single,
clip: true,
size: styles.narrow,
},
{
label: 'Image with border',
corners: styles.imageCorners,
clip: true,
image: true,
},
{
label: 'Unequal image insets',
corners: styles.imageCorners,
clip: true,
image: true,
asymmetricBorder: true,
},
]
: [
{label: 'All corners: background', corners: styles.all, clip: false},
{
label: 'Bottom corners: background',
corners: styles.bottom,
clip: false,
},
{
label: 'Bottom corners: clipped child',
corners: styles.bottom,
clip: true,
},
{label: 'Top corners: clipped child', corners: styles.top, clip: true},
];

return (
<View testID="view-border-curve-example" style={styles.container}>
<RNTesterText style={styles.label}>Curve: {curve}</RNTesterText>
<RNTesterText style={styles.label}>
Red marks the circular reference underneath.
</RNTesterText>
<View style={boundaryCases ? styles.grid : styles.list}>
{examples.map(
({label, corners, clip, size, image, asymmetricBorder}) => {
const foregroundStyle = [
styles.foreground,
corners,
{borderCurve: curve},
clip && styles.clipped,
];
return (
<View key={label} style={boundaryCases && styles.cell}>
<RNTesterText style={styles.label}>{label}</RNTesterText>
<View
style={[
styles.reference,
boundaryCases && styles.compact,
size,
corners,
]}>
{image === true ? (
<Image
source={require('../../assets/trees.jpg')}
onLoad={() =>
setLoadedImages(loaded => new Set(loaded).add(label))
}
style={[
foregroundStyle,
styles.image,
asymmetricBorder === true && styles.asymmetricBorder,
]}
/>
) : (
<View style={[foregroundStyle, !clip && styles.background]}>
{clip && (
<View
style={[StyleSheet.absoluteFill, styles.background]}
/>
)}
</View>
)}
</View>
</View>
);
},
)}
</View>
{boundaryCases && loadedImages.size === 2 && (
<RNTesterText testID="border-curve-image-loaded" style={styles.label}>
Images loaded
</RNTesterText>
)}
</View>
);
}

const styles = StyleSheet.create({
container: {padding: 12, gap: 8, backgroundColor: 'white'},
label: {color: 'black', fontSize: 12},
list: {gap: 8},
grid: {flexDirection: 'row', flexWrap: 'wrap', gap: 12},
cell: {width: 150},
reference: {width: 240, height: 110, backgroundColor: 'red'},
compact: {width: 150, height: 76},
narrow: {width: 36},
foreground: {...StyleSheet.absoluteFill},
all: {borderRadius: 36},
bottom: {borderBottomLeftRadius: 36, borderBottomRightRadius: 36},
top: {borderTopLeftRadius: 36, borderTopRightRadius: 36},
single: {borderBottomLeftRadius: 24},
diagonal: {borderTopLeftRadius: 24, borderBottomRightRadius: 24},
unequal: {borderBottomLeftRadius: 12, borderBottomRightRadius: 24},
large: {borderBottomLeftRadius: 60, borderBottomRightRadius: 60},
imageCorners: {borderRadius: 24},
clipped: {overflow: 'hidden'},
background: {backgroundColor: '#527FE4'},
image: {
width: '100%',
height: '100%',
tintColor: '#527FE4',
borderWidth: 4,
borderColor: '#222',
},
asymmetricBorder: {borderLeftWidth: 10},
});
29 changes: 29 additions & 0 deletions packages/rn-tester/js/examples/View/ViewExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import type {RNTesterModule} from '../../types/RNTesterTypes';

import RNTesterText from '../../components/RNTesterText';
import ViewBorderCurveExample from './ViewBorderCurveExample';
import * as React from 'react';
import {useState} from 'react';
import {
Expand Down Expand Up @@ -927,6 +928,34 @@ export default {
);
},
},
{
title: 'Border Curve (iOS)',
name: 'border-curve',
render(): React.Node {
return <ViewBorderCurveExample />;
},
},
{
title: 'Border Curve Circular Control (iOS)',
name: 'border-curve-circular',
render(): React.Node {
return <ViewBorderCurveExample continuous={false} />;
},
},
{
title: 'Border Curve Boundaries (iOS)',
name: 'border-curve-boundaries',
render(): React.Node {
return <ViewBorderCurveExample boundaryCases />;
},
},
{
title: 'Border Curve Boundaries Circular Control (iOS)',
name: 'border-curve-boundaries-circular',
render(): React.Node {
return <ViewBorderCurveExample boundaryCases continuous={false} />;
},
},
{
title: 'Border Style',
name: 'border-style',
Expand Down
26 changes: 26 additions & 0 deletions packages/rn-tester/visual-tests/flows/view-border-curve.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
appId: ${APP_ID}
---
- runFlow:
when:
platform: iOS
commands:
- runFlow:
file: ../helpers/capture-border-curve.yml
env:
EXAMPLE: border-curve
WAIT_FOR_IMAGES: false
- runFlow:
file: ../helpers/capture-border-curve.yml
env:
EXAMPLE: border-curve-circular
WAIT_FOR_IMAGES: false
- runFlow:
file: ../helpers/capture-border-curve.yml
env:
EXAMPLE: border-curve-boundaries
WAIT_FOR_IMAGES: true
- runFlow:
file: ../helpers/capture-border-curve.yml
env:
EXAMPLE: border-curve-boundaries-circular
WAIT_FOR_IMAGES: true
34 changes: 34 additions & 0 deletions packages/rn-tester/visual-tests/helpers/capture-border-curve.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
appId: ${APP_ID}
---
- launchApp:
stopApp: true
- runFlow:
when:
visible: 'Open'
commands:
- tapOn: 'Open'
- extendedWaitUntil:
visible: 'Components'
timeout: 30000
- openLink: rntester://example/ViewExample/${EXAMPLE}
- tapOn:
text: 'Open'
optional: true
- extendedWaitUntil:
visible:
id: 'view-border-curve-example'
timeout: 15000
- runFlow:
when:
true: ${WAIT_FOR_IMAGES}
commands:
- extendedWaitUntil:
visible:
id: 'border-curve-image-loaded'
timeout: 15000
- waitForAnimationToEnd:
timeout: 5000
- takeScreenshot:
path: ${VISUAL_OUTPUT_DIR}/view/${EXAMPLE}
cropOn:
id: 'view-border-curve-example'
Loading