Skip to content

Commit

Permalink
Add support for clipping only specific corners, add unit tests (Textu…
Browse files Browse the repository at this point in the history
…reGroup#1415)

* Add support for clipping only specific corners, add unit tests

* Remove some cleanup to make the diff smaller

* Fix
  • Loading branch information
Adlai-Holler authored Mar 29, 2019
1 parent 039b6f0 commit fe1cb1c
Show file tree
Hide file tree
Showing 18 changed files with 174 additions and 45 deletions.
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);

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;
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 @@ -216,6 +218,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 @@ -336,8 +339,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 @@ -397,6 +402,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

0 comments on commit fe1cb1c

Please sign in to comment.