From b28f10497ee8dbe93a68f226e30f6569d96bd902 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 19 Mar 2019 20:50:57 -0700 Subject: [PATCH 1/3] Add support for clipping only specific corners, add unit tests --- Source/ASDisplayNode.h | 8 ++ Source/ASDisplayNode.mm | 115 +++++++++++------- Source/Details/UIView+ASConvenience.h | 1 + Source/Private/ASDisplayNode+AsyncDisplay.mm | 10 +- Source/Private/ASDisplayNode+UIViewBridge.mm | 40 +++++- Source/Private/ASDisplayNodeInternal.h | 12 +- Source/Private/_ASPendingState.mm | 14 +++ Tests/ASDisplayNodeSnapshotTests.mm | 42 +++++++ .../testClippingCornerRounding@2x.png | Bin 0 -> 1851 bytes .../testClippingCornerRounding_15@2x.png | Bin 0 -> 2553 bytes .../testClippingCornerRounding_1@2x.png | Bin 0 -> 1851 bytes .../testClippingCornerRounding_3@2x.png | Bin 0 -> 2121 bytes .../testClippingCornerRounding_7@2x.png | Bin 0 -> 2397 bytes .../testPrecompositedCornerRounding@2x.png | Bin 0 -> 2145 bytes .../testPrecompositedCornerRounding_15@2x.png | Bin 0 -> 3119 bytes .../testPrecompositedCornerRounding_1@2x.png | Bin 0 -> 2145 bytes .../testPrecompositedCornerRounding_3@2x.png | Bin 0 -> 2409 bytes .../testPrecompositedCornerRounding_7@2x.png | Bin 0 -> 2825 bytes 18 files changed, 194 insertions(+), 48 deletions(-) create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding_15@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding_1@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding_3@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding_7@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_15@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_1@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_3@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_7@2x.png diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index ce54e0a51..161146a4d 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -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 diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index fc541178f..568de3a10 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -324,6 +324,7 @@ - (void)_initializeInstance _contentsScaleForDisplay = ASScreenScale(); _drawingPriority = ASDefaultTransactionPriority; + _maskedCorners = kASCACornerAllCorners; _primitiveTraitCollection = ASPrimitiveTraitCollectionMakeDefault(); @@ -1526,17 +1527,20 @@ - (void)recursivelySetNeedsDisplayAtScale:(CGFloat)contentsScale - (void)_layoutClipCornersIfNeeded { ASDisplayNodeAssertMainThread(); - if (_clipCornerLayers[0] == nil) { + if (!_layer) { return; } - + 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]]; } } @@ -1545,75 +1549,99 @@ - (void)_layoutClipCornersIfNeeded - (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor:(UIColor *)backgroundColor { ASPerformBlockOnMainThread(^{ + /// Cache the last path that was used. + static UIBezierPath *lastRoundedRect; + UIBezierPath *roundedRect; + if (lastRoundedRect.bounds.size.width == radius * 2) { + roundedRect = lastRoundedRect; + } else { + 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)]]; + lastRoundedRect = roundedRect; + } + 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 isTop = (idx == 0 || idx == 1); + BOOL isRight = (idx == 1 || idx == 3); + 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) { + 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; + } 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(); - + + // If we were previously not rounded, and we're still not rounded e.g. changing the rounding type during init, + // stop here. + BOOL wasNotRounded = (oldCornerRadius == 0 || oldMaskedCorners == 0); + BOOL isNotRounded = (newCornerRadius == 0 || newMaskedCorners == 0); + if (wasNotRounded && isNotRounded) { + return; + } + ASPerformBlockOnMainThread(^{ ASDisplayNodeAssertMainThread(); @@ -1629,14 +1657,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) { @@ -1645,22 +1675,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]; } } } diff --git a/Source/Details/UIView+ASConvenience.h b/Source/Details/UIView+ASConvenience.h index 8c4b2f55c..19fd57695 100644 --- a/Source/Details/UIView+ASConvenience.h +++ b/Source/Details/UIView+ASConvenience.h @@ -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; diff --git a/Source/Private/ASDisplayNode+AsyncDisplay.mm b/Source/Private/ASDisplayNode+AsyncDisplay.mm index 31662bd70..68bf4107a 100644 --- a/Source/Private/ASDisplayNode+AsyncDisplay.mm +++ b/Source/Private/ASDisplayNode+AsyncDisplay.mm @@ -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) { @@ -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) { @@ -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; diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index 4f7e93140..45828d7bc 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -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 @@ -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 @@ -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__); diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 55cef7579..27203b1a5 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -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 @@ -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; @@ -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; @@ -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 diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index 5dca0ce5c..422ee6239 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -87,6 +87,7 @@ int setPreservesSuperviewLayoutMargins:1; int setInsetsLayoutMarginsFromSafeArea:1; int setActions:1; + int setMaskedCorners : 1; } ASPendingStateFlags; @@ -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; @@ -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; @@ -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); diff --git a/Tests/ASDisplayNodeSnapshotTests.mm b/Tests/ASDisplayNodeSnapshotTests.mm index 489727e3b..e3f0a8195 100644 --- a/Tests/ASDisplayNodeSnapshotTests.mm +++ b/Tests/ASDisplayNodeSnapshotTests.mm @@ -33,4 +33,46 @@ - (void)testBasicHierarchySnapshotTesting ASSnapshotVerifyNode(node, nil); } +NS_INLINE UIImage *BlueImageMake(CGRect bounds) +{ + UIGraphicsBeginImageContextWithOptions(bounds.size, YES, 0); + [[UIColor blueColor] setFill]; + UIRectFill(bounds); + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + +- (void)testPrecompositedCornerRounding +{ + for (CACornerMask c = 1; c <= 0xf; c |= (c << 1)) { + auto node = [[ASImageNode alloc] init]; + auto bounds = CGRectMake(0, 0, 100, 100); + node.image = BlueImageMake(bounds); + node.frame = bounds; + node.cornerRoundingType = ASCornerRoundingTypePrecomposited; + node.backgroundColor = UIColor.greenColor; + node.maskedCorners = c; + node.cornerRadius = 15; + ASSnapshotVerifyNode(node, ([NSString stringWithFormat:@"%d", (int)c])); + } +} + +- (void)testClippingCornerRounding +{ + for (CACornerMask c = 1; c <= 0xf; c |= (c << 1)) { + auto node = [[ASImageNode alloc] init]; + auto bounds = CGRectMake(0, 0, 100, 100); + node.image = BlueImageMake(bounds); + node.frame = bounds; + node.cornerRoundingType = ASCornerRoundingTypeClipping; + node.backgroundColor = UIColor.greenColor; + node.maskedCorners = c; + node.cornerRadius = 15; + // A layout pass is required, because that's where we lay out the clip layers. + [node.layer layoutIfNeeded]; + ASSnapshotVerifyNode(node, ([NSString stringWithFormat:@"%d", (int)c])); + } +} + @end diff --git a/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding@2x.png b/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..aa72ff691a5134e6091a069c7c26b43efdb7742e GIT binary patch literal 1851 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 Dv8;>H literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding_7@2x.png b/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding_7@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..45d842d099ee65661b6464d2a63456c8563cdb83 GIT binary patch literal 2397 zcmeHH`#aMM7~gDcX=W}VePxP>S?G^C6dc<93i#P zMa{iV?w3>Q=!h9YDwi2Y#GHAa^TYWYdU~Jd^M2m<{XFk4pC3M{uFeh;;)>!B2t|8vAEF#2i zg$SYX9}8#r%+#xWLJeL|vLkxLL%)nUrTKa)bnp1 z&*sYM#lje6GFBpNR5(Kgrp`DN+njfsLCSCHSL%g1C0%!AFrg2iy^kWCGd2494qV?b~Laa;a-FhXUd8@Z3wieJD zaW*_fowDK-OxL>1#jnpfL2z5Vl&&xfBhRNZzR$eSEW)V^4W*o-X^t`2yt;HT_XsB6 zE2a#<$@ldbc7hf?lFI$OLD=34c`2&}!wK%>{E8uc)+%>msh`IisVc>$>iEB)$38A@ z>3zDI+`1i+>9zBS5{WCGi;p>&_i$!0w5#&q@=Q25wz>4uDb4!ygL&&`KInij^Wl=6 z))vCOC$m<6f!RB3pxXA2nbn8a_b#n2uMs$cmqGp`L7NFK)BXfMa)7~0F2jH1qtx!YKU)4T(fBU|hI~ zlHzNCk3afD{+?_Z#&1{&Gi+z4$e{k5Fuas@!)sy8EY0I5*_6rTYm8L7MgajO8jy`O zfgY~HDiENDJB|uFQD;Te6s1qp)sd3Tj#Or+pG_M*;bWG?QFf|WWPM=qcM|eE{o=0I z<61*$^fV~rG)4i5LaJYO|8*bVGQ$?b2*m1Oi!qsZJ`T&i^_IDe{hvHw^F#g~ZK2G9 zU(22T4tF>Gybw;EG9yO0qHdxva6k?CITYnmkF?2ZZ%O&-8d-&^8r7Vo7S4RG35~BA$*EJ-6zkMuHM0^U z(8Y_WFUXaTD9?m?PMo0Blrd-aeq}? zlx24teK?OUK@3YbZ+dn;*s4ivimjczjf#>AIli*Y5g<0qBG`JaNIf-P?Xq7vW&CrB z0(|3q?EYIE@kph7+-9g0{I>SWJdU(UJR@~)Xv>&!k`$I3&lW8-G$?8K3M1Z8dKZCt;Hl*v;NwncAC-l8;z1gBvPK-K9k|eOndh5>GX!1$y$kuT1xtW9s zQS0rCY${dfm1{Ks7T#!LR-OhwHRTBQyF>P~i_IOKMmM}3OBztE&mQ5hzUrm+lm;e9( literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding@2x.png b/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0b4a6b425fb624d9988018c2ad4d02ef2e30a3a3 GIT binary patch literal 2145 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>;VkfoEM{O3 zQw3p0&mff#KtY*I7yl3-%>=|CkOIUSP#VZ)`TKEiFHoN4q^FBxNCo4Yx%acr7D^nS zKhtpbq$f_xVjd)^iU{tWrKDS4r1*26kH~*aFTw}qrA3kR;Jr%I}RKo02 zYn!#i{Z^NJT_v+)?IpVfn|`*Q`em-Adss``Zne(eWtS|LT{2h~WWO-TJhHN5)dz*< zJvHnp*A>g4GmX8Tr| z>_1JvAg)>L|iVPDtE?qu*lCfi_gU+j3 zp{6^xS8w{^9@w6ycjW9*2T{ki=!&ge48@EQUw)qvSn_Adp9^Of@t>{XnA6=T+4QCS zgPOvJr%n2YYL{^u-CDBguV>4`nS!0gh6cV9Rv$0AJLNY+g$Ua+mY+LUe7eaiCZ1Y& zW@h_UO-H#m&usf`8kx)1%vsLz@?B!#MzQ1jKD}A3aw1~!scFwy=BXDH^-V4IoG^WM z$Sn7Wm^DW?uDAKE#d+zr>y64oYR@Yyr?XgA^&UMMEn|Ip)8R=xL8 zZONiPlAlfA3$i#SEYM*jszs zB558+lY&8lfrEvcg`jl*%vyh27DoXI0|^BgWtm$yw)ZMO>r~)iLN?-x^QaN9;2glP z@MQZ3EZhG7KfZM3G6n`_PEeut_NpP{0Rx^R2R^9ZRjLsDz#D6{YSRC`-)rr7EH=mp zbTA)mEMS<^uz6kAck6CtfyM#_83y$S5na~a8~<%}U^#@4J*_kgTe~DWM4fkPVYX literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_15@2x.png b/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_15@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ba850b21eb92e06d780f6cc7ca4988a7e99fdac8 GIT binary patch literal 3119 zcmeH}iBnVA5{GXRAP^ymFae$>m@q0R49K8{h{)lh4#eF+4`s0fH4 zD1w5hED8z;VPBPXfUpK7FhN1qfNaX132O1)fAH#6y;Iev@BN+ob)D{Ws_Wjcv@n%{ zZG!;-AVV}Wu@d=)4U?1*&Dm#j9*P`@Ze?l&Yce57w(J z?+bAHLYPi$-}C9k!mE4twVhiCe=&OIY^Rg*onJbB5*n}2v(n&&qP|vce~-Iw;P@6ci{bNNiOWZ*52ANC_;JPAgvw$)k1pzhJKL9Aim{#a zac$~_I+=q#{qy=#9o;7e=(h7zy}6$|7jI|Ad04tAt(2@cn`ksO=DM&Kc4oP6*Hy#480+!}=-|c=16*)w)vkr$UA&Ij8BJt!o@)Guv6{0%h(Kdwg!|73ph#C)?xUqpJ}~N{#+;e;CZ0w8+h&b=OdRs-TJ~Wq2d!xW1{fGh$$y4ZS=_L|$ab!8d{<_B6gdks+W#=Xm(=>z zEn9b?W_UQdi7CfSdx=I21-(=dWQR`LRTB-uPFn0s%O}Jxjk*QM_?zL%NP^Hzkg06B5Vji4Ka+NX}V=6LM&y9X&nI59J?5Q72 z4noMGGZLDzdvwJmm{rU|$d1ac;u8^I2vkCc<6{uwaOOv6<R9`3+ABsexCDDKd zz=FkugquZbfpAzIqz;e-Sh)G5>I-4U29lC+F*u+SY8>8BY4oJ--X@}Qpp^3{Bpino z6N7@Gp>kEJMniTuD+lvUBsTHFXj^3s5(y52FdWcTrLj zRL!gc@x^Ev$R+FRU5av>$P*`p<++#p#IA<&4fp^JtV6!Os%GMLOKKCb%HOiKWUUA` zGWebwdaubkO8G$Dzmd#$;p?kv(wn)#QOZJC%r=KjB&loCC}bUVZ8Nw3iE`LMb07d9 z6zWCo7BzqIwvFa4NbY}57Pa<-#3drGC;SilvtdpdmOIBIVK>hD{*HWXc5FPv9v_e< zWNzDg@oI`+x-zUg^61IiA51NYhQBGGtTJ2~4H`k<8ZZ+Q1*!7j$DKiqdmMsql$fsrprP<&Ws7vxmIIrD zh3n~5rS$x7ci|hs+4I5cX2D}F#TpTuk-s#z6M!ZK1AYsFnI>W!^U4x zhoIB-`9n(@>PlqkMhC@?R3%35)zB>jRG_SFl(Ne5-)Wzz2FQ(ly>d2wJKUwrAL3iq zM#NqwOIJK_9m}j_8e(ZFV^u{V>dE&{X)S&H7pYSdoG!(p2KlWkkSdQ@dlYGRbcAUF zMyea%{b^@gLO5rhm3t9EOQiMIu?kl1-dX{@D^|el!>})MP@+Z_5qaZ;dScxV&%Lmc*?;2!4Egfb|9y_BKVxyZpWzZfXZ@ zuS+MXdt8fMPeH0EAsqR!Pw$7WuI$|LRrb0cb2N!An6;E2Z)d00tQP>|Vs&e0EmR>b z?bEFVGw~GRUU?+ADu@ej2zE>%EcdtLqq1M^pM`VWTzGXf!`9}t))|ArFRG+z^mUN< z(*2S5AcE#s-Fk?)9>0T3(yo9~WTN{9d<=7%mv{~G3srK>y`>iW0(2D2z>IAQ_z!!uInJm8B2Z;pjNWp6a;J)P5$7d$U%N47q z(=RvaLbUA-##JVHmxwIf7g<$Cg5TU8(GN>)6pN8U`Ml_Pq+gj_izhF?J_biPNsm_t zT*Rc8E<5<_mW>;f`$-@cm+$HO%7Jr&gDBFi>4;&gw+KBK4U9(b(RY~+XC(#4&6u6Y z3gL81#UDBE?ApHP5DXL)1kQ-UsSH0uW118@VkdUI$k!kBlT9A6Hg%YBTAzv8b&Te% z_1?O*5V0B?;hDERO--|7mBmwZ7@I#&+EM0AR1tQMo$`vp85z5jpnB4UdGjBl1s@Z< zc1~9K{xVhuGZx4nkDd_ZK%X!Mm(M&pFwqhN594N{y(>;7|8#cH#^c2rsUm2g8bfw-uy~_!A0SiK#w$NQ&OfmwH*)*;!z8e93%U%e*lY+ BX|DhP literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_1@2x.png b/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_1@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0b4a6b425fb624d9988018c2ad4d02ef2e30a3a3 GIT binary patch literal 2145 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>;VkfoEM{O3 zQw3p0&mff#KtY*I7yl3-%>=|CkOIUSP#VZ)`TKEiFHoN4q^FBxNCo4Yx%acr7D^nS zKhtpbq$f_xVjd)^iU{tWrKDS4r1*26kH~*aFTw}qrA3kR;Jr%I}RKo02 zYn!#i{Z^NJT_v+)?IpVfn|`*Q`em-Adss``Zne(eWtS|LT{2h~WWO-TJhHN5)dz*< zJvHnp*A>g4GmX8Tr| z>_1JvAg)>L|iVPDtE?qu*lCfi_gU+j3 zp{6^xS8w{^9@w6ycjW9*2T{ki=!&ge48@EQUw)qvSn_Adp9^Of@t>{XnA6=T+4QCS zgPOvJr%n2YYL{^u-CDBguV>4`nS!0gh6cV9Rv$0AJLNY+g$Ua+mY+LUe7eaiCZ1Y& zW@h_UO-H#m&usf`8kx)1%vsLz@?B!#MzQ1jKD}A3aw1~!scFwy=BXDH^-V4IoG^WM z$Sn7Wm^DW?uDAKE#d+zr>y64oYR@Yyr?XgA^&UMMEn|Ip)8R=xL8 zZONiPlAlfA3$i#SEYM*jszs zB558+lY&8lfrEvcg`jl*%vyh27DoXI0|^BgWtm$yw)ZMO>r~)iLN?-x^QaN9;2glP z@MQZ3EZhG7KfZM3G6n`_PEeut_NpP{0Rx^R2R^9ZRjLsDz#D6{YSRC`-)rr7EH=mp zbTA)mEMS<^uz6kAck6CtfyM#_83y$S5na~a8~<%}U^#@4J*_kgTe~DWM4fkPVYX literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_3@2x.png b/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_3@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4282e29d394f52c6649c124e3175dc81bbdd54a9 GIT binary patch literal 2409 zcmeHHi&N5B6#fAwiio5`IR%9F(3Uo}heE7LNUMxS<>FegX}guL4VI+Ud=Y3ytC>B_ z%CObc%u3C)5FY_)Dq2dZk)ef~nU>@$QVVf$*Rj8$na<32zk9zk-^@8@?wr52(Y-8T zi(mi%SkkD|uzUs}n%}ZvqlgZp zZewVpPVx7*6$Y(uW1MnyvQNsh9!-QN z=T$4Rkppy?eR*T6A+cJq__Ng$KbNHmb)?hYO7<%2#1l=~R@Q4Oy-&+pedFDwHR_n) zkHn$O$1D48>TX@QxHjYw@geW&7;{Ke7E-dmZ`$)SH$y!#$rhfk)J8wnYTdeB4{(_h z6|2)u{Gg!=Yf!L0(!{a!sz-!(uDUDg-e&gM;)*R5KOebJHR3V>&m3ug_F^PWo$kYK zYpLpvuh-u5=;|}Py{m~87kET5OJA}&pzKR`D_BAAA~i(uAJ{)XPw~X~vxeB%h99tR zu2#kCD%35i?x*ai*tZs9AusaMq)KD8st3Jf?_t$EF;OaUKiJOFvBgoUDhzWlGOXzt zG}sp$5q(>ov!DI$1(*4OqmH^i0$;IEyNg-HUk^SN>JZh*p(4SbF8mgl|RjkU=%lXGQ)CtrvlM6qjsp= zLnEdG6aL$KZ+2l=i#s42cfNNW^15D+Q^<#6+5#kap;z0F`r4uDo~sKffKZMj;e@Y8 zy_us>UEo-s`0d#IMEJ3nS$o_N{rVIURuP@PkNko9$Yu<+fJnrNfBIf8C|X>ICYsAj z;aM66t@m*Ir%2wXxG5)qjDexB(IZN!q*2E!3TaSuRx>vvM`4t8mt&%QTE@eRNV@y4 zU#B$*Yy`O_Xsh11l7-qL{$P-tMo!pHcg`$*s-Q|dsx2k2^y|b9Y3a}cL7nP$L*RQn zq?^^~HYDBUiUZ%D7z*shSdC1Gr{Y86wWeoqGzFsn)AIc}VQy@6Tm~1rJVjorvdL{p z?T{`d$6QLaz;hp(`7Dvf`|4(AUm4 z6+I=*GZn}LcOgrp?UJo2UI63glE+IhDWLdFM514Gw}R$0#%S9rrl0uv&q+Z+TGg zooM@6J@|IV1G`zhE#i)))2!~=d*EKDq|TbSN7)Q2;{5i!y(Yk6T%Lajdi_8;LhvfY zzW)sX>S@IrV!Vk;C-<(_hsh8Olut%-a2&u`0%J1YW7<4}k3^!)&;T6}!cHGa@=kRD z+2IIC0w4nD;gLBW(=bo6nHjpN#To4LEcReB$Z1SdcQ5Sh7Vo84#$ve0G4oQ3vi^@le>znYnWU}A z*Vlis6m}*V^mzDOZwQqAH0<=Ey>k{(`yl0r0d;Jyx36LxLC|6d08CH2pIl*F{t)w- z<*w~;0m+=R-8OeN?D2&aByhA yL9-*7vtbx=(PC%i(0KkLz!aOd*W` literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_7@2x.png b/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_7@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..46ae9327a1e21a8493869f453fb153ff8bbd83a1 GIT binary patch literal 2825 zcmeHHdo+l2Ns<~TkEHQdsWI-jx8>e{@1J+AyVu&^{r!H{`s}s${(RSt*++L! zRa&P60DvluN^zFQ<`qXP%Fm+Tif_mR%-`983>3G436PRl{i zo3fW7k8ruK*za=R^drjeiVAE=%2xnT=%i70FoI#9vIgT09@L3;x9nzI-n5x;{v-MPZr}1dv!h`E+zk0P=zleA_$DGP6^}6pdx@`a5KXqVckrjQdT*B>?NVW}H z`S>%-`h5Eh1kak@;AJhopJ{?ndPOU7!fGLBVE8%fYO!#S@NbdVD`vKQRY{-edG&TC zaUj`|)!kMx7~UwUvU@Ws`}jm0D!6MG#4LH|+lyjlJQ4c3PXvms1!wSo|GpF2@PpuP${ z=BGe!(JPdv6P&|+IB?5@HSyM;Iqz#bx4!%9<@{v7eJAnX%Vp0g=AxF8lY#}qf;AOx z{>+e9zJDG(w`phNOhhJUKV7uPI>bhjiVyRR8mQ8KlNv_b)L3D5XE}nyC>HfIy>lZj zyMdLn23olRGm7J15AGi>e52>9+l%!j6`LqQ|dQacsW?NVkM?a;kTT0ix4C{LRV zMAt$#Q1r+wl{t%AaGM(ye4JB1t8(F8*3oU)57M|yd z$g%CqsUn+&x{;AY5nq#^dmmIA4!f@{DdK%{uceWq_R}rWOJ256%Iu!1mc5s56>LmQ zM&0E%^gV2Hn>0lX`ZjNysy<`|!Rx+Gxee;6&3qLsg?oid6t6(E7VL*_Mjp4kx3R#m zRDZltTy068W^P;jhmb3IgTfL&(_c4U5%a)~lm~nMU>(jpLPsH3%GLfhHn|B@ zE!+{L-4e)wP}pWz){(w)rBl6keRTnQBKzh5U+;7|JSWAV+@?%5u}klyTQuVjyOWE_ zU3|8b*dgJ)ovhqmQcvj{KNWv#lFq8n4M5?g&m=vSdTR6VEJtq)Wt^`QAwBeAI@)2o z+ezc5p{y`9P4M0|QPE2q1x0=h{|Q3Bx~FV!92|jCBz6Rn;tE|%y%2X*Q5!5evB=)p z*=C-~3O?Y^2(XU^{f!H?JXHziPn~VVQ1hq1xt06RwX(w0?mQ*hhLD zsl47Qc8zyEALI1M-SaV*@B{aJ*!#@j2j0zjth)IJZZ+&v)nC@2VSTjYIjo3%{B=*I z%f>IYcLGoaKFL`AdoRL=F#tByZYm3*T6b)=ebfFT7NLjYk#KB?4aiGiEZ(tO#-#Fa zI1mj2bRb$ODe^ouehbV1BI1ZZCO|(Gc*|~C$)1EpW06>3BhUVPb2a&Pi5w}@y0F^Y|HL@l zQN;)VSYEbG*dSm2$o!S%F3Eg%y7ib&O?GM87RMOfH0K;j8lj6Ngnka%O}lMoCquA= zLw3`-#!1PXyGG_hSAys#V`tIug=EhcK4X4wt864bq1j%IthO6_H7Z@IvcJr6Z}
    lRePW3jb>*kw1~`xn0ttwwbd$`c zelsK)B8rIsh{9$N+VUPurjyV}6o}%zuPMn>8uPPK=Ht*e< znqXogyD-0wo7qZ+kWq*PM2(;Lf$^U{N)yGHbEN+)q4fVH$c6atr%=w=g}Z>nUdQ6l zoa>z54p??^-=m`-v>e&0ifB5g>FqpQxcG&s9HFtI@i=J$U^@|=dZmo`29I&=6`d{g zPEMc_FdMsW54+9{a9Wu^z$rumCOSVoyC6yUDVVyVUmoH+Yc;1YSE{k1`gLrkodT$d zJz!~faJ^sISzILgJt>l$IULg&nJ$vRb(81Q}lmtym1mB3*1~UT>7cnL%d9<7o5LHxx$O zjTr33u|!+LJd9zb_uiywW1CwIDjw;iSZ$_>Hod@1$~W9%6caWTxt6LlGKsG$-wAad zPun3+nP|$uz7^L~$T1os6$C9Q+0i8EbAeANsG!llHVBg1L%m;&-f8C_w$174e7-Bj zwBxWoep#JrnLM;1Wq!$Ud`!v;Y0KaqPjl>iL56zwb!FSPnuJSCt|jJ!<;4$2@ Date: Wed, 20 Mar 2019 10:02:34 -0700 Subject: [PATCH 2/3] Remove some cleanup to make the diff smaller --- Source/ASDisplayNode.mm | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 568de3a10..4a0656da7 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -1527,9 +1527,6 @@ - (void)recursivelySetNeedsDisplayAtScale:(CGFloat)contentsScale - (void)_layoutClipCornersIfNeeded { ASDisplayNodeAssertMainThread(); - if (!_layer) { - return; - } if (_clipCornerLayers[0] == nil && _clipCornerLayers[1] == nil && _clipCornerLayers[2] == nil && _clipCornerLayers[3] == nil) { return; @@ -1549,18 +1546,6 @@ - (void)_layoutClipCornersIfNeeded - (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor:(UIColor *)backgroundColor { ASPerformBlockOnMainThread(^{ - /// Cache the last path that was used. - static UIBezierPath *lastRoundedRect; - UIBezierPath *roundedRect; - if (lastRoundedRect.bounds.size.width == radius * 2) { - roundedRect = lastRoundedRect; - } else { - 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)]]; - lastRoundedRect = roundedRect; - } - for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { // Skip corners that aren't clipped (we have already set up & torn down layers based on maskedCorners.) if (_clipCornerLayers[idx] == nil) { @@ -1569,7 +1554,7 @@ - (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor // 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 isTop = (idx == 0 || idx == 1); BOOL isRight = (idx == 1 || idx == 3); CGSize size = CGSizeMake(radius + 1, radius + 1); @@ -1583,6 +1568,9 @@ - (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor 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]; @@ -1634,14 +1622,6 @@ - (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType _maskedCorners = newMaskedCorners; __instanceLock__.unlock(); - // If we were previously not rounded, and we're still not rounded e.g. changing the rounding type during init, - // stop here. - BOOL wasNotRounded = (oldCornerRadius == 0 || oldMaskedCorners == 0); - BOOL isNotRounded = (newCornerRadius == 0 || newMaskedCorners == 0); - if (wasNotRounded && isNotRounded) { - return; - } - ASPerformBlockOnMainThread(^{ ASDisplayNodeAssertMainThread(); From 97c670f67195b353d6b5035445f61c952b899d01 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 20 Mar 2019 15:08:46 -0700 Subject: [PATCH 3/3] Fix --- Source/ASDisplayNode.mm | 6 ++++-- Tests/ASDisplayNodeSnapshotTests.mm | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 4a0656da7..04656012f 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -1590,7 +1590,9 @@ - (void)_setClipCornerLayersVisible:(CACornerMask)visibleCornerLayers ASDisplayNodeAssertMainThread(); for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { BOOL visible = (0 != (visibleCornerLayers & (1 << idx))); - if (visible && _clipCornerLayers[idx] == nil) { + if (visible == (_clipCornerLayers[idx] != nil)) { + continue; + } else if (visible) { static ASDisplayNodeCornerLayerDelegate *clipCornerLayers; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -1625,7 +1627,7 @@ - (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType ASPerformBlockOnMainThread(^{ ASDisplayNodeAssertMainThread(); - if (oldRoundingType != newRoundingType || oldCornerRadius != newCornerRadius) { + if (oldRoundingType != newRoundingType || oldCornerRadius != newCornerRadius || oldMaskedCorners != newMaskedCorners) { if (oldRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { if (newRoundingType == ASCornerRoundingTypePrecomposited) { self.layerCornerRadius = 0.0; diff --git a/Tests/ASDisplayNodeSnapshotTests.mm b/Tests/ASDisplayNodeSnapshotTests.mm index e3f0a8195..ae1bce744 100644 --- a/Tests/ASDisplayNodeSnapshotTests.mm +++ b/Tests/ASDisplayNodeSnapshotTests.mm @@ -8,6 +8,7 @@ #import "ASSnapshotTestCase.h" #import +#import @interface ASDisplayNodeSnapshotTests : ASSnapshotTestCase @@ -45,7 +46,7 @@ - (void)testBasicHierarchySnapshotTesting - (void)testPrecompositedCornerRounding { - for (CACornerMask c = 1; c <= 0xf; c |= (c << 1)) { + for (CACornerMask c = 1; c <= kASCACornerAllCorners; c |= (c << 1)) { auto node = [[ASImageNode alloc] init]; auto bounds = CGRectMake(0, 0, 100, 100); node.image = BlueImageMake(bounds); @@ -60,7 +61,7 @@ - (void)testPrecompositedCornerRounding - (void)testClippingCornerRounding { - for (CACornerMask c = 1; c <= 0xf; c |= (c << 1)) { + for (CACornerMask c = 1; c <= kASCACornerAllCorners; c |= (c << 1)) { auto node = [[ASImageNode alloc] init]; auto bounds = CGRectMake(0, 0, 100, 100); node.image = BlueImageMake(bounds);