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

[ASCornerRounding] Introduce .cornerRoundingType: CALayer, Precomposited, or Clip Corners. #465

Merged
merged 9 commits into from
Sep 18, 2017
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;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think following code will be better performance.
It reduce accessing to array.

CALayer *layer = [[CALayer alloc] init];
layer.zPosition = 99999;
layer.delegate = self;
_clipCornerLayers[idx] = layer;

Copy link
Contributor

Choose a reason for hiding this comment

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

@appleguy
Thanks! I understand.
Sorry, I may be misunderstanding about retain and release.
Does returning by subscript of NSArray not create a retain?

_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) {
Copy link
Member

Choose a reason for hiding this comment

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

Not sure whether you're looking for && image == NULL or && *image == nil here.

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();
}
Copy link
Member

Choose a reason for hiding this comment

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

Should this be if (image)? And should we ensure that we only end the image context if we created one above, and not if we were passed one? The context create-readimage-destroy behavior here seems a little unclear, for instance in the caller we will actually re-generate the image and overwrite it if the caller passed us a context.

}
}

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