Skip to content

Commit

Permalink
[ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposi…
Browse files Browse the repository at this point in the history
…ted, or Clip Corners. (TextureGroup#465)

* [ASCornerRounding] Initial (untested) implementation of ASCornerRounding features.

* [ASCornerRounding] Working version of both clip corners and precomposited corners.

* [ASCornerRounding] Improve factoring and documentation of corner rounding features.

* [ASCornerRounding] Some final fixups.

* Add entry to changelog for .cornerRoundingType
  • Loading branch information
appleguy authored and bernieperez committed Apr 25, 2018
1 parent a026505 commit ccf87dc
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## master

* Add your own contributions to the next release on the line below this with your name.
- [ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. [Scott Goodson](https://github.com/appleguy) [#465](https://github.com/TextureGroup/Texture/pull/465)
- [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [Scott Goodson](https://github.com/appleguy)
- [ASCollectionNode] Add -isProcessingUpdates and -onDidFinishProcessingUpdates: APIs. [#522](https://github.com/TextureGroup/Texture/pull/522) [Scott Goodson](https://github.com/appleguy)
- [Accessibility] Add .isAccessibilityContainer property, allowing automatic aggregation of children's a11y labels. [#468][Scott Goodson](https://github.com/appleguy)
Expand Down
33 changes: 31 additions & 2 deletions Source/ASDisplayNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ typedef NS_OPTIONS(NSUInteger, ASInterfaceState)
ASInterfaceStateInHierarchy = ASInterfaceStateMeasureLayout | ASInterfaceStatePreload | ASInterfaceStateDisplay | ASInterfaceStateVisible,
};

typedef NS_ENUM(NSInteger, ASCornerRoundingType) {
ASCornerRoundingTypeDefaultSlowCALayer,
ASCornerRoundingTypePrecomposited,
ASCornerRoundingTypeClipping
};

/**
* Default drawing priority for display node
*/
Expand Down Expand Up @@ -375,7 +381,6 @@ extern NSInteger const ASDefaultDrawingPriority;

/** @name Drawing and Updating the View */


/**
* @abstract Whether this node's view performs asynchronous rendering.
*
Expand Down Expand Up @@ -631,6 +636,31 @@ extern NSInteger const ASDefaultDrawingPriority;
@property (nonatomic, assign) CGPoint position; // default=CGPointZero
@property (nonatomic, assign) CGFloat alpha; // default=1.0f

/* @abstract Sets the corner rounding method to use on the ASDisplayNode.
* There are three types of corner rounding provided by Texture: CALayer, Precomposited, and Clipping.
*
* - ASCornerRoundingTypeDefaultSlowCALayer: uses CALayer's inefficient .cornerRadius property. Use
* this type of corner in situations in which there is both movement through and movement underneath
* the corner (very rare). This uses only .cornerRadius.
*
* - ASCornerRoundingTypePrecomposited: corners are drawn using bezier paths to clip the content in a
* CGContext / UIGraphicsContext. This requires .backgroundColor and .cornerRadius to be set. Use opaque
* background colors when possible for optimal efficiency, but transparent colors are supported and much
* more efficient than CALayer. The only limitation of this approach is that it cannot clip children, and
* thus works best for ASImageNodes or containers showing a background around their children.
*
* - ASCornerRoundingTypeClipping: overlays 4 seperate opaque corners on top of the content that needs
* corner rounding. Requires .backgroundColor and .cornerRadius to be set. Use clip corners in situations
* in which is movement through the corner, with an opaque background (no movement underneath the corner).
* Clipped corners are ideal for animating / resizing views, and still outperform CALayer.
*
* For more information and examples, see http://texturegroup.org/docs/corner-rounding.html
*
* @default ASCornerRoundingTypeDefaultSlowCALayer
*/
@property (nonatomic, assign) ASCornerRoundingType cornerRoundingType; // default=Slow CALayer .cornerRadius (offscreen rendering)
@property (nonatomic, assign) CGFloat cornerRadius; // default=0.0

@property (nonatomic, assign) BOOL clipsToBounds; // default==NO
@property (nonatomic, getter=isHidden) BOOL hidden; // default==NO
@property (nonatomic, getter=isOpaque) BOOL opaque; // default==YES
Expand All @@ -643,7 +673,6 @@ extern NSInteger const ASDefaultDrawingPriority;

@property (nonatomic, assign) CGPoint anchorPoint; // default={0.5, 0.5}
@property (nonatomic, assign) CGFloat zPosition; // default=0.0
@property (nonatomic, assign) CGFloat cornerRadius; // default=0.0
@property (nonatomic, assign) CATransform3D transform; // default=CATransform3DIdentity
@property (nonatomic, assign) CATransform3D subnodeTransform; // default=CATransform3DIdentity

Expand Down
144 changes: 143 additions & 1 deletion Source/ASDisplayNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
// We have to forward declare the protocol as this place otherwise it will not compile compiling with an Base SDK < iOS 10
@protocol CALayerDelegate;

@interface ASDisplayNode () <UIGestureRecognizerDelegate, _ASDisplayLayerDelegate>
@interface ASDisplayNode () <UIGestureRecognizerDelegate, CALayerDelegate, _ASDisplayLayerDelegate>

/**
* See ASDisplayNodeInternal.h for ivars
Expand Down Expand Up @@ -912,6 +912,7 @@ - (void)__layout
if (loaded) {
ASPerformBlockOnMainThread(^{
[self layout];
[self _layoutClipCornersIfNeeded];
[self layoutDidFinish];
});
}
Expand Down Expand Up @@ -1465,6 +1466,147 @@ - (void)recursivelySetNeedsDisplayAtScale:(CGFloat)contentsScale
});
}

- (void)_layoutClipCornersIfNeeded
{
ASDisplayNodeAssertMainThread();
if (_clipCornerLayers[0] == nil) {
return;
}

CGSize boundsSize = self.bounds.size;
for (int idx = 0; idx < 4; idx++) {
BOOL isTop = (idx == 0 || idx == 1);
BOOL isRight = (idx == 1 || idx == 2);
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);
[_layer addSublayer:_clipCornerLayers[idx]];
}
}
}

- (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor:(UIColor *)backgroundColor
{
ASPerformBlockOnMainThread(^{
for (int idx = 0; idx < 4; idx++) {
// Layers are, in order: Top Left, Top Right, Bottom Right, Bottom Left.
// 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);

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

CGContextRef ctx = UIGraphicsGetCurrentContext();
if (isRight == YES) {
CGContextTranslateCTM(ctx, -radius + 1, 0);
}
if (isTop == YES) {
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];
clipCornerLayer.contents = (id)(UIGraphicsGetImageFromCurrentImageContext().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);

UIGraphicsEndImageContext();
}
[self _layoutClipCornersIfNeeded];
});
}

- (void)_setClipCornerLayersVisible:(BOOL)visible
{
ASPerformBlockOnMainThread(^{
ASDisplayNodeAssertMainThread();
if (visible) {
for (int idx = 0; idx < 4; idx++) {
if (_clipCornerLayers[idx] == nil) {
_clipCornerLayers[idx] = [[CALayer alloc] init];
_clipCornerLayers[idx].zPosition = 99999;
_clipCornerLayers[idx].delegate = self;
}
}
[self _updateClipCornerLayerContentsWithRadius:_cornerRadius backgroundColor:self.backgroundColor];
} else {
for (int idx = 0; idx < 4; idx++) {
[_clipCornerLayers[idx] removeFromSuperlayer];
_clipCornerLayers[idx] = nil;
}
}
});
}

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

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

ASPerformBlockOnMainThread(^{
ASDisplayNodeAssertMainThread();

if (oldRoundingType != newRoundingType || oldCornerRadius != newCornerRadius) {
if (oldRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) {
if (newRoundingType == ASCornerRoundingTypePrecomposited) {
self.layerCornerRadius = 0.0;
if (oldCornerRadius > 0.0) {
[self displayImmediately];
} else {
[self setNeedsDisplay]; // Async display is OK if we aren't replacing an existing .cornerRadius.
}
}
else if (newRoundingType == ASCornerRoundingTypeClipping) {
self.layerCornerRadius = 0.0;
[self _setClipCornerLayersVisible:YES];
} else if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) {
self.layerCornerRadius = newCornerRadius;
}
}
else if (oldRoundingType == ASCornerRoundingTypePrecomposited) {
if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) {
self.layerCornerRadius = newCornerRadius;
[self setNeedsDisplay];
}
else if (newRoundingType == ASCornerRoundingTypePrecomposited) {
// Corners are already precomposited, but the radius has changed.
// Default to async re-display. The user may force a synchronous display if desired.
[self setNeedsDisplay];
}
else if (newRoundingType == ASCornerRoundingTypeClipping) {
[self _setClipCornerLayersVisible:YES];
[self setNeedsDisplay];
}
}
else if (oldRoundingType == ASCornerRoundingTypeClipping) {
if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) {
self.layerCornerRadius = newCornerRadius;
[self _setClipCornerLayersVisible:NO];
}
else if (newRoundingType == ASCornerRoundingTypePrecomposited) {
[self _setClipCornerLayersVisible:NO];
[self displayImmediately];
}
else if (newRoundingType == ASCornerRoundingTypeClipping) {
// Clip corners already exist, but the radius has changed.
[self _updateClipCornerLayerContentsWithRadius:newCornerRadius backgroundColor:self.backgroundColor];
}
}
}
});
}

- (void)recursivelySetDisplaySuspended:(BOOL)flag
{
_recursivelySetDisplaySuspended(self, nil, flag);
Expand Down
112 changes: 94 additions & 18 deletions Source/Private/ASDisplayNode+AsyncDisplay.mm
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ - (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchro
isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock
rasterizing:(BOOL)rasterizing
{
ASDisplayNodeAssertMainThread();

asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil;
ASDisplayNodeFlags flags;

Expand All @@ -184,6 +186,9 @@ - (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchro

BOOL opaque = self.opaque;
CGRect bounds = self.bounds;
UIColor *backgroundColor = self.backgroundColor;
CGColorRef borderColor = self.borderColor;
CGFloat borderWidth = self.borderWidth;
CGFloat contentsScaleForDisplay = _contentsScaleForDisplay;

__instanceLock__.unlock();
Expand All @@ -208,7 +213,7 @@ - (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchro

// If [UIColor clearColor] or another semitransparent background color is used, include alpha channel when rasterizing.
// Unlike CALayer drawing, we include the backgroundColor as a base during rasterization.
opaque = opaque && CGColorGetAlpha(self.backgroundColor.CGColor) == 1.0f;
opaque = opaque && CGColorGetAlpha(backgroundColor.CGColor) == 1.0f;

displayBlock = ^id{
CHECK_CANCELLED_AND_RETURN_NIL();
Expand Down Expand Up @@ -237,32 +242,18 @@ - (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchro

CGContextRef currentContext = UIGraphicsGetCurrentContext();
UIImage *image = nil;

ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = nil;
ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = nil;
if (currentContext) {
__instanceLock__.lock();
willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext;
didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext;
__instanceLock__.unlock();
}



// For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or
// _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs.
if (willDisplayNodeContentWithRenderingContext != nil) {
willDisplayNodeContentWithRenderingContext(currentContext, drawParameters);
}
[self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters];

if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly.
image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock];
} else if (usesDrawRect) { // If we're using a draw method, this will operate on the currentContext.
[self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
}

if (didDisplayNodeContentWithRenderingContext != nil) {
didDisplayNodeContentWithRenderingContext(currentContext, drawParameters);
}
[self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor];

if (shouldCreateGraphicsContext) {
CHECK_CANCELLED_AND_RETURN_NIL( UIGraphicsEndImageContext(); );
Expand Down Expand Up @@ -292,6 +283,91 @@ - (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchro
return displayBlock;
}

- (void)__willDisplayNodeContentWithRenderingContext:(CGContextRef)context drawParameters:(id _Nullable)drawParameters
{
if (context) {
__instanceLock__.lock();
ASCornerRoundingType cornerRoundingType = _cornerRoundingType;
CGFloat cornerRadius = _cornerRadius;
ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext;
__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];
}

if (willDisplayNodeContentWithRenderingContext) {
willDisplayNodeContentWithRenderingContext(context, drawParameters);
}
}

}
- (void)__didDisplayNodeContentWithRenderingContext:(CGContextRef)context image:(UIImage **)image drawParameters:(id _Nullable)drawParameters backgroundColor:(UIColor *)backgroundColor borderWidth:(CGFloat)borderWidth borderColor:(CGColorRef)borderColor
{
if (context == NULL && *image == NULL) {
return;
}

__instanceLock__.lock();
ASCornerRoundingType cornerRoundingType = _cornerRoundingType;
CGFloat cornerRadius = _cornerRadius;
CGFloat contentsScale = _contentsScaleForDisplay;
ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext;
__instanceLock__.unlock();

if (context != NULL) {
if (didDisplayNodeContentWithRenderingContext) {
didDisplayNodeContentWithRenderingContext(context, drawParameters);
}
}

if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0f) {
CGRect bounds = CGRectZero;
if (context == NULL) {
bounds = self.threadSafeBounds;
bounds.size.width *= contentsScale;
bounds.size.height *= contentsScale;
CGFloat white = 0.0f, alpha = 0.0f;
[backgroundColor getWhite:&white alpha:&alpha];
UIGraphicsBeginImageContextWithOptions(bounds.size, (alpha == 1.0f), contentsScale);
[*image drawInRect:bounds];
} else {
bounds = CGContextGetClipBoundingBox(context);
}

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]];
roundedHole.usesEvenOddFillRule = YES;

UIBezierPath *roundedPath = nil;
if (borderWidth > 0.0f) { // Don't create roundedPath and stroke if borderWidth is 0.0
CGFloat strokeThickness = borderWidth * contentsScale;
CGFloat strokeInset = ((strokeThickness + 1.0f) / 2.0f) - 1.0f;
roundedPath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(bounds, strokeInset, strokeInset)
cornerRadius:_cornerRadius * contentsScale];
roundedPath.lineWidth = strokeThickness;
[[UIColor colorWithCGColor:borderColor] setStroke];
}

// Punch out the corners by copying the backgroundColor over them.
// This works for everything from clearColor to opaque colors.
[backgroundColor setFill];
[roundedHole fillWithBlendMode:kCGBlendModeCopy alpha:1.0f];

[roundedPath stroke]; // Won't do anything if borderWidth is 0 and roundedPath is nil.

if (*image) {
*image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
}
}

- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously
{
ASDisplayNodeAssertMainThread();
Expand Down
Loading

0 comments on commit ccf87dc

Please sign in to comment.