Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for clipping only specific corners, add unit tests #1415

Merged
merged 3 commits into from
Mar 29, 2019
Merged
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
8 changes: 8 additions & 0 deletions Source/ASDisplayNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,14 @@ AS_EXTERN NSInteger const ASDefaultDrawingPriority;
*/
@property CGFloat cornerRadius; // default=0.0

/** @abstract Which corners to mask when rounding corners.
*
* @note This option cannot be changed when using iOS < 11
* and using ASCornerRoundingTypeDefaultSlowCALayer. Use a different corner rounding type to implement not-all-corners
* rounding in prior versions of iOS.
*/
@property CACornerMask maskedCorners; // default=all corners.

@property BOOL clipsToBounds; // default==NO
@property (getter=isHidden) BOOL hidden; // default==NO
@property (getter=isOpaque) BOOL opaque; // default==YES
Expand Down
91 changes: 52 additions & 39 deletions Source/ASDisplayNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ - (void)_initializeInstance

_contentsScaleForDisplay = ASScreenScale();
_drawingPriority = ASDefaultTransactionPriority;
_maskedCorners = kASCACornerAllCorners;

_primitiveTraitCollection = ASPrimitiveTraitCollectionMakeDefault();

Expand Down Expand Up @@ -1526,17 +1527,17 @@ - (void)recursivelySetNeedsDisplayAtScale:(CGFloat)contentsScale
- (void)_layoutClipCornersIfNeeded
{
ASDisplayNodeAssertMainThread();
if (_clipCornerLayers[0] == nil) {
if (_clipCornerLayers[0] == nil && _clipCornerLayers[1] == nil && _clipCornerLayers[2] == nil &&
_clipCornerLayers[3] == nil) {
return;
}

CGSize boundsSize = self.bounds.size;
for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) {
BOOL isTop = (idx == 0 || idx == 1);
BOOL isRight = (idx == 1 || idx == 2);
BOOL isRight = (idx == 1 || idx == 3);
if (_clipCornerLayers[idx]) {
// Note the Core Animation coordinates are reversed for y; 0 is at the bottom.
_clipCornerLayers[idx].position = CGPointMake(isRight ? boundsSize.width : 0.0, isTop ? boundsSize.height : 0.0);
_clipCornerLayers[idx].position = CGPointMake(isRight ? boundsSize.width : 0.0, isTop ? 0.0 : boundsSize.height);
[_layer addSublayer:_clipCornerLayers[idx]];
}
}
Expand All @@ -1546,78 +1547,87 @@ - (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor
{
ASPerformBlockOnMainThread(^{
for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) {
// Layers are, in order: Top Left, Top Right, Bottom Right, Bottom Left.
// Skip corners that aren't clipped (we have already set up & torn down layers based on maskedCorners.)
if (_clipCornerLayers[idx] == nil) {
continue;
}

// Layers are, in order: Top Left, Top Right, Bottom Left, Bottom Right, which mirrors CACornerMask.
// anchorPoint is Bottom Left at 0,0 and Top Right at 1,1.
BOOL isTop = (idx == 0 || idx == 1);
BOOL isRight = (idx == 1 || idx == 2);
BOOL isRight = (idx == 1 || idx == 3);
Copy link
Member Author

@Adlai-Holler Adlai-Holler Mar 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason some of these bits flipped is (1) I changed our NUM_CLIP_CORNERS array order to match the CACornerMask order and (2) the original comments about CA being inverted in this context are incorrect.


CGSize size = CGSizeMake(radius + 1, radius + 1);
ASGraphicsBeginImageContextWithOptions(size, NO, self.contentsScaleForDisplay);

CGContextRef ctx = UIGraphicsGetCurrentContext();
if (isRight == YES) {
CGContextTranslateCTM(ctx, -radius + 1, 0);
}
if (isTop == YES) {
if (isTop == NO) {
CGContextTranslateCTM(ctx, 0, -radius + 1);
}

UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, radius * 2, radius * 2) cornerRadius:radius];
[roundedRect setUsesEvenOddFillRule:YES];
[roundedRect appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(-1, -1, radius * 2 + 1, radius * 2 + 1)]];
[backgroundColor setFill];
[roundedRect fill];

// No lock needed, as _clipCornerLayers is only modified on the main thread.
CALayer *clipCornerLayer = _clipCornerLayers[idx];
unowned CALayer *clipCornerLayer = _clipCornerLayers[idx];
clipCornerLayer.contents = (id)(ASGraphicsGetImageAndEndCurrentContext().CGImage);
clipCornerLayer.bounds = CGRectMake(0.0, 0.0, size.width, size.height);
clipCornerLayer.anchorPoint = CGPointMake(isRight ? 1.0 : 0.0, isTop ? 1.0 : 0.0);
clipCornerLayer.anchorPoint = CGPointMake(isRight ? 1.0 : 0.0, isTop ? 0.0 : 1.0);
}
[self _layoutClipCornersIfNeeded];
});
}

- (void)_setClipCornerLayersVisible:(BOOL)visible
- (void)_setClipCornerLayersVisible:(CACornerMask)visibleCornerLayers
{
ASPerformBlockOnMainThread(^{
ASDisplayNodeAssertMainThread();
if (visible) {
for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) {
if (_clipCornerLayers[idx] == nil) {
static ASDisplayNodeCornerLayerDelegate *clipCornerLayers;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
clipCornerLayers = [[ASDisplayNodeCornerLayerDelegate alloc] init];
});
_clipCornerLayers[idx] = [[CALayer alloc] init];
_clipCornerLayers[idx].zPosition = 99999;
_clipCornerLayers[idx].delegate = clipCornerLayers;
}
}
[self _updateClipCornerLayerContentsWithRadius:_cornerRadius backgroundColor:self.backgroundColor];
} else {
for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) {
for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) {
BOOL visible = (0 != (visibleCornerLayers & (1 << idx)));
if (visible == (_clipCornerLayers[idx] != nil)) {
continue;
} else if (visible) {
static ASDisplayNodeCornerLayerDelegate *clipCornerLayers;
Copy link
Contributor

@maicki maicki Mar 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should rename that variable at some point: clipCornerLayersDelegate or sharedClipCornerLayersDelegate

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
clipCornerLayers = [[ASDisplayNodeCornerLayerDelegate alloc] init];
});
_clipCornerLayers[idx] = [[CALayer alloc] init];
_clipCornerLayers[idx].zPosition = 99999;
_clipCornerLayers[idx].delegate = clipCornerLayers;
} else {
[_clipCornerLayers[idx] removeFromSuperlayer];
_clipCornerLayers[idx] = nil;
}
}
[self _updateClipCornerLayerContentsWithRadius:_cornerRadius backgroundColor:self.backgroundColor];
});
}

- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType cornerRadius:(CGFloat)newCornerRadius
- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType
cornerRadius:(CGFloat)newCornerRadius
maskedCorners:(CACornerMask)newMaskedCorners
{
__instanceLock__.lock();
CGFloat oldCornerRadius = _cornerRadius;
ASCornerRoundingType oldRoundingType = _cornerRoundingType;
CACornerMask oldMaskedCorners = _maskedCorners;

_cornerRadius = newCornerRadius;
_cornerRoundingType = newRoundingType;
_maskedCorners = newMaskedCorners;
__instanceLock__.unlock();

ASPerformBlockOnMainThread(^{
ASDisplayNodeAssertMainThread();

if (oldRoundingType != newRoundingType || oldCornerRadius != newCornerRadius) {
if (oldRoundingType != newRoundingType || oldCornerRadius != newCornerRadius || oldMaskedCorners != newMaskedCorners) {
if (oldRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) {
if (newRoundingType == ASCornerRoundingTypePrecomposited) {
self.layerCornerRadius = 0.0;
Expand All @@ -1629,14 +1639,16 @@ - (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType corne
}
else if (newRoundingType == ASCornerRoundingTypeClipping) {
self.layerCornerRadius = 0.0;
[self _setClipCornerLayersVisible:YES];
[self _setClipCornerLayersVisible:newMaskedCorners];
} else if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) {
self.layerCornerRadius = newCornerRadius;
self.layerMaskedCorners = newMaskedCorners;
}
}
else if (oldRoundingType == ASCornerRoundingTypePrecomposited) {
if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) {
self.layerCornerRadius = newCornerRadius;
self.layerMaskedCorners = newMaskedCorners;
[self setNeedsDisplay];
}
else if (newRoundingType == ASCornerRoundingTypePrecomposited) {
Expand All @@ -1645,22 +1657,23 @@ - (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType corne
[self setNeedsDisplay];
}
else if (newRoundingType == ASCornerRoundingTypeClipping) {
[self _setClipCornerLayersVisible:YES];
[self _setClipCornerLayersVisible:newMaskedCorners];
[self setNeedsDisplay];
}
}
else if (oldRoundingType == ASCornerRoundingTypeClipping) {
if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) {
self.layerCornerRadius = newCornerRadius;
[self _setClipCornerLayersVisible:NO];
[self _setClipCornerLayersVisible:kNilOptions];
}
else if (newRoundingType == ASCornerRoundingTypePrecomposited) {
[self _setClipCornerLayersVisible:NO];
[self _setClipCornerLayersVisible:kNilOptions];
[self displayImmediately];
}
else if (newRoundingType == ASCornerRoundingTypeClipping) {
// Clip corners already exist, but the radius has changed.
[self _updateClipCornerLayerContentsWithRadius:newCornerRadius backgroundColor:self.backgroundColor];
// Clip corners already exist, but the radius and/or maskedCorners have changed.
// This method will add & remove them, and subsequently redraw them.
[self _setClipCornerLayersVisible:newMaskedCorners];
}
}
}
Expand Down
1 change: 1 addition & 0 deletions Source/Details/UIView+ASConvenience.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) CGFloat zPosition;
@property (nonatomic) CGPoint anchorPoint;
@property (nonatomic) CGFloat cornerRadius;
@property (nonatomic) CACornerMask maskedCorners API_AVAILABLE(ios(11), tvos(11));
@property (nullable, nonatomic) id contents;
@property (nonatomic, copy) NSString *contentsGravity;
@property (nonatomic) CGRect contentsRect;
Expand Down
10 changes: 8 additions & 2 deletions Source/Private/ASDisplayNode+AsyncDisplay.mm
Original file line number Diff line number Diff line change
Expand Up @@ -287,13 +287,15 @@ - (void)__willDisplayNodeContentWithRenderingContext:(CGContextRef)context drawP
ASCornerRoundingType cornerRoundingType = _cornerRoundingType;
CGFloat cornerRadius = _cornerRadius;
ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext;
CACornerMask maskedCorners = _maskedCorners;
__instanceLock__.unlock();

if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0) {
ASDisplayNodeAssert(context == UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self);
// TODO: This clip path should be removed if we are rasterizing.
CGRect boundingBox = CGContextGetClipBoundingBox(context);
[[UIBezierPath bezierPathWithRoundedRect:boundingBox cornerRadius:cornerRadius] addClip];
CGSize radii = CGSizeMake(cornerRadius, cornerRadius);
[[UIBezierPath bezierPathWithRoundedRect:boundingBox byRoundingCorners:maskedCorners cornerRadii:radii] addClip];
}

if (willDisplayNodeContentWithRenderingContext) {
Expand All @@ -313,6 +315,7 @@ - (void)__didDisplayNodeContentWithRenderingContext:(CGContextRef)context image:
CGFloat cornerRadius = _cornerRadius;
CGFloat contentsScale = _contentsScaleForDisplay;
ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext;
CACornerMask maskedCorners = _maskedCorners;
__instanceLock__.unlock();

if (context != NULL) {
Expand All @@ -338,7 +341,10 @@ - (void)__didDisplayNodeContentWithRenderingContext:(CGContextRef)context image:
ASDisplayNodeAssert(UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self);

UIBezierPath *roundedHole = [UIBezierPath bezierPathWithRect:bounds];
[roundedHole appendPath:[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius * contentsScale]];
CGSize radii = CGSizeMake(cornerRadius * contentsScale, cornerRadius * contentsScale);
[roundedHole appendPath:[UIBezierPath bezierPathWithRoundedRect:bounds
byRoundingCorners:maskedCorners
cornerRadii:radii]];
roundedHole.usesEvenOddFillRule = YES;

UIBezierPath *roundedPath = nil;
Expand Down
40 changes: 38 additions & 2 deletions Source/Private/ASDisplayNode+UIViewBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ - (CGFloat)cornerRadius

- (void)setCornerRadius:(CGFloat)newCornerRadius
{
[self updateCornerRoundingWithType:self.cornerRoundingType cornerRadius:newCornerRadius];
[self updateCornerRoundingWithType:self.cornerRoundingType
cornerRadius:newCornerRadius
maskedCorners:self.maskedCorners];
}

- (ASCornerRoundingType)cornerRoundingType
Expand All @@ -192,7 +194,20 @@ - (ASCornerRoundingType)cornerRoundingType

- (void)setCornerRoundingType:(ASCornerRoundingType)newRoundingType
{
[self updateCornerRoundingWithType:newRoundingType cornerRadius:self.cornerRadius];
[self updateCornerRoundingWithType:newRoundingType cornerRadius:self.cornerRadius maskedCorners:self.maskedCorners];
}

- (CACornerMask)maskedCorners
{
AS::MutexLocker l(__instanceLock__);
return _maskedCorners;
}

- (void)setMaskedCorners:(CACornerMask)newMaskedCorners
{
[self updateCornerRoundingWithType:self.cornerRoundingType
cornerRadius:self.cornerRadius
maskedCorners:newMaskedCorners];
}

- (NSString *)contentsGravity
Expand Down Expand Up @@ -983,6 +998,27 @@ - (void)setLayerCornerRadius:(CGFloat)newLayerCornerRadius
_setToLayer(cornerRadius, newLayerCornerRadius);
}

- (CACornerMask)layerMaskedCorners
{
_bridge_prologue_read;
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
return _getFromLayer(maskedCorners);
} else {
return kASCACornerAllCorners;
}
}

- (void)setLayerMaskedCorners:(CACornerMask)newLayerMaskedCorners
{
_bridge_prologue_write;
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
_setToLayer(maskedCorners, newLayerMaskedCorners);
} else {
ASDisplayNodeAssert(newLayerMaskedCorners == kASCACornerAllCorners,
@"Cannot change maskedCorners property in iOS < 11 while using DefaultSlowCALayer rounding.");
}
}

- (BOOL)_locked_insetsLayoutMarginsFromSafeArea
{
ASAssertLocked(__instanceLock__);
Expand Down
12 changes: 10 additions & 2 deletions Source/Private/ASDisplayNodeInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest
#define VISIBILITY_NOTIFICATIONS_DISABLED_BITS 4

#define TIME_DISPLAYNODE_OPS 0 // If you're using this information frequently, try: (DEBUG || PROFILE)
static constexpr CACornerMask kASCACornerAllCorners =
kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner | kCALayerMinXMaxYCorner | kCALayerMaxXMaxYCorner;

#define NUM_CLIP_CORNER_LAYERS 4

Expand Down Expand Up @@ -215,6 +217,7 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest
CGFloat _cornerRadius;
ASCornerRoundingType _cornerRoundingType;
CALayer *_clipCornerLayers[NUM_CLIP_CORNER_LAYERS];
CACornerMask _maskedCorners;

ASDisplayNodeContextModifier _willDisplayNodeContentWithRenderingContext;
ASDisplayNodeContextModifier _didDisplayNodeContentWithRenderingContext;
Expand Down Expand Up @@ -335,8 +338,10 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest
/// Display the node's view/layer immediately on the current thread, bypassing the background thread rendering. Will be deprecated.
- (void)displayImmediately;

/// Refreshes any precomposited or drawn clip corners, setting up state as required to transition radius or rounding type.
- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType cornerRadius:(CGFloat)newCornerRadius;
/// Refreshes any precomposited or drawn clip corners, setting up state as required to transition corner config.
- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType
cornerRadius:(CGFloat)newCornerRadius
maskedCorners:(CACornerMask)newMaskedCorners;

/// Alternative initialiser for backing with a custom view class. Supports asynchronous display with _ASDisplayView subclasses.
- (instancetype)initWithViewClass:(Class)viewClass;
Expand Down Expand Up @@ -396,6 +401,9 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest

@property (nonatomic) CGFloat layerCornerRadius;

/// NOTE: Changing this to non-default under iOS < 11 will make an assertion (for the end user to see.)
@property (nonatomic) CACornerMask layerMaskedCorners;

- (BOOL)_locked_insetsLayoutMarginsFromSafeArea;

@end
Expand Down
14 changes: 14 additions & 0 deletions Source/Private/_ASPendingState.mm
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
int setPreservesSuperviewLayoutMargins:1;
int setInsetsLayoutMarginsFromSafeArea:1;
int setActions:1;
int setMaskedCorners : 1;
} ASPendingStateFlags;


Expand Down Expand Up @@ -215,6 +216,7 @@ ASDISPLAYNODE_INLINE void ASPendingStateApplyMetricsToLayer(_ASPendingState *sta
@synthesize preservesSuperviewLayoutMargins=preservesSuperviewLayoutMargins;
@synthesize insetsLayoutMarginsFromSafeArea=insetsLayoutMarginsFromSafeArea;
@synthesize actions=actions;
@synthesize maskedCorners = maskedCorners;

static CGColorRef blackColorRef = NULL;
static UIColor *defaultTintColor = nil;
Expand Down Expand Up @@ -416,6 +418,12 @@ - (void)setCornerRadius:(CGFloat)newCornerRadius
_flags.setCornerRadius = YES;
}

- (void)setMaskedCorners:(CACornerMask)newMaskedCorners
{
maskedCorners = newMaskedCorners;
_flags.setMaskedCorners = YES;
}

- (void)setContentMode:(UIViewContentMode)newContentMode
{
contentMode = newContentMode;
Expand Down Expand Up @@ -890,6 +898,12 @@ - (void)applyToLayer:(CALayer *)layer
if (flags.setCornerRadius)
layer.cornerRadius = cornerRadius;

if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
if (flags.setMaskedCorners) {
layer.maskedCorners = maskedCorners;
}
}

if (flags.setContentMode)
layer.contentsGravity = ASDisplayNodeCAContentsGravityFromUIContentMode(contentMode);

Expand Down
Loading