From eb4c21c54540d2c1c0b63a6b0665a77fea810e6c Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Thu, 26 Jul 2018 09:44:10 -0700 Subject: [PATCH] Optimize drawing code + add examples how to round corners (#996) * Use CoreGraphics for drawing and cropping of node content * Smaller fixes --- AsyncDisplayKit.xcodeproj/project.pbxproj | 16 ++-- CHANGELOG.md | 2 +- Source/ASDisplayNode.mm | 55 ++++++------ Source/ASImageNode.mm | 38 +++++++-- Source/Base/ASBaseDefines.h | 2 +- Source/Details/CoreGraphics+ASConvenience.h | 9 +- Source/Details/CoreGraphics+ASConvenience.m | 19 ----- Source/Details/CoreGraphics+ASConvenience.mm | 64 ++++++++++++++ Source/Layout/ASDimension.h | 10 +-- Source/Layout/ASDimension.mm | 2 +- Source/Private/ASDisplayNode+AsyncDisplay.mm | 78 +++++++++++++----- ...Convenience.m => UIImage+ASConvenience.mm} | 48 +++++++---- Source/tvOS/ASControlNode+tvOS.m | 8 +- Source/tvOS/ASImageNode+tvOS.m | 4 +- .../testRoundedCornerBlock@2x.png | Bin 0 -> 11609 bytes examples/ASDKgram/Sample/PhotoCellNode.m | 4 +- 16 files changed, 250 insertions(+), 109 deletions(-) delete mode 100644 Source/Details/CoreGraphics+ASConvenience.m create mode 100644 Source/Details/CoreGraphics+ASConvenience.mm rename Source/{UIImage+ASConvenience.m => UIImage+ASConvenience.mm} (86%) create mode 100644 Tests/ReferenceImages_iOS_10/ASImageNodeSnapshotTests/testRoundedCornerBlock@2x.png diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 3c6668706..d3e26159d 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -109,7 +109,7 @@ 509E68631B3AEDB4009B9150 /* ASCollectionViewLayoutController.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E1B1B373A2C007741D0 /* ASCollectionViewLayoutController.h */; }; 509E68641B3AEDB7009B9150 /* ASCollectionViewLayoutController.m in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E1C1B373A2C007741D0 /* ASCollectionViewLayoutController.m */; }; 509E68651B3AEDC5009B9150 /* CoreGraphics+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E1F1B376416007741D0 /* CoreGraphics+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 509E68661B3AEDD7009B9150 /* CoreGraphics+ASConvenience.m in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E201B376416007741D0 /* CoreGraphics+ASConvenience.m */; }; + 509E68661B3AEDD7009B9150 /* CoreGraphics+ASConvenience.mm in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E201B376416007741D0 /* CoreGraphics+ASConvenience.mm */; }; 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.mm in Sources */ = {isa = PBXBuildFile; fileRef = DBC452DA1C5BF64600B16017 /* NSArray+Diffing.mm */; }; 636EA1A51C7FF4EF00EE152F /* ASDefaultPlayButton.m in Sources */ = {isa = PBXBuildFile; fileRef = AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.m */; }; 680346941CE4052A0009FEB4 /* ASNavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 68FC85DC1CE29AB700EDD713 /* ASNavigationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -176,7 +176,7 @@ 7AB338671C55B3460055FDE8 /* ASRelativeLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A06A7391C35F08800FE8DAA /* ASRelativeLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7AB338691C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7AB338681C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm */; }; 8021EC1D1D2B00B100799119 /* UIImage+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 8021EC1A1D2B00B100799119 /* UIImage+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 8021EC1F1D2B00B100799119 /* UIImage+ASConvenience.m in Sources */ = {isa = PBXBuildFile; fileRef = 8021EC1B1D2B00B100799119 /* UIImage+ASConvenience.m */; }; + 8021EC1F1D2B00B100799119 /* UIImage+ASConvenience.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8021EC1B1D2B00B100799119 /* UIImage+ASConvenience.mm */; }; 81E95C141D62639600336598 /* ASTextNodeSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 81E95C131D62639600336598 /* ASTextNodeSnapshotTests.m */; }; 83A7D95B1D44547700BF333E /* ASWeakMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 83A7D9591D44542100BF333E /* ASWeakMap.m */; }; 83A7D95C1D44548100BF333E /* ASWeakMap.h in Headers */ = {isa = PBXBuildFile; fileRef = 83A7D9581D44542100BF333E /* ASWeakMap.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -621,7 +621,7 @@ 205F0E1B1B373A2C007741D0 /* ASCollectionViewLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionViewLayoutController.h; sourceTree = ""; }; 205F0E1C1B373A2C007741D0 /* ASCollectionViewLayoutController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionViewLayoutController.m; sourceTree = ""; }; 205F0E1F1B376416007741D0 /* CoreGraphics+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CoreGraphics+ASConvenience.h"; sourceTree = ""; }; - 205F0E201B376416007741D0 /* CoreGraphics+ASConvenience.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CoreGraphics+ASConvenience.m"; sourceTree = ""; }; + 205F0E201B376416007741D0 /* CoreGraphics+ASConvenience.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "CoreGraphics+ASConvenience.mm"; sourceTree = ""; }; 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBasicImageDownloaderTests.m; sourceTree = ""; }; 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASCollectionViewFlowLayoutInspectorTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 254C6B511BF8FE6D003EC431 /* ASTextKitTruncationTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextKitTruncationTests.mm; sourceTree = ""; }; @@ -734,7 +734,7 @@ 7A06A7391C35F08800FE8DAA /* ASRelativeLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRelativeLayoutSpec.h; sourceTree = ""; }; 7AB338681C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRelativeLayoutSpecSnapshotTests.mm; sourceTree = ""; }; 8021EC1A1D2B00B100799119 /* UIImage+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+ASConvenience.h"; sourceTree = ""; }; - 8021EC1B1D2B00B100799119 /* UIImage+ASConvenience.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+ASConvenience.m"; sourceTree = ""; }; + 8021EC1B1D2B00B100799119 /* UIImage+ASConvenience.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "UIImage+ASConvenience.mm"; sourceTree = ""; }; 81E95C131D62639600336598 /* ASTextNodeSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTextNodeSnapshotTests.m; sourceTree = ""; }; 81EE384D1C8E94F000456208 /* ASRunLoopQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASRunLoopQueue.h; path = ../ASRunLoopQueue.h; sourceTree = ""; }; 81EE384E1C8E94F000456208 /* ASRunLoopQueue.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASRunLoopQueue.mm; path = ../ASRunLoopQueue.mm; sourceTree = ""; }; @@ -1234,7 +1234,7 @@ 68FC85E81CE29C7D00EDD713 /* ASVisibilityProtocols.m */, 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */, 8021EC1A1D2B00B100799119 /* UIImage+ASConvenience.h */, - 8021EC1B1D2B00B100799119 /* UIImage+ASConvenience.m */, + 8021EC1B1D2B00B100799119 /* UIImage+ASConvenience.mm */, CC55A70B1E529FA200594372 /* UIResponder+AsyncDisplayKit.h */, CC55A70C1E529FA200594372 /* UIResponder+AsyncDisplayKit.m */, ); @@ -1422,7 +1422,7 @@ CC3B20871C3F7A5400798563 /* ASWeakSet.h */, CC3B20881C3F7A5400798563 /* ASWeakSet.m */, 205F0E1F1B376416007741D0 /* CoreGraphics+ASConvenience.h */, - 205F0E201B376416007741D0 /* CoreGraphics+ASConvenience.m */, + 205F0E201B376416007741D0 /* CoreGraphics+ASConvenience.mm */, DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */, DBC452DA1C5BF64600B16017 /* NSArray+Diffing.mm */, CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */, @@ -2405,7 +2405,7 @@ CCA282C51E9EAE630037E8B7 /* ASLayerBackingTipProvider.m in Sources */, 509E68641B3AEDB7009B9150 /* ASCollectionViewLayoutController.m in Sources */, B35061F91B010EFD0018CF92 /* ASControlNode.mm in Sources */, - 8021EC1F1D2B00B100799119 /* UIImage+ASConvenience.m in Sources */, + 8021EC1F1D2B00B100799119 /* UIImage+ASConvenience.mm in Sources */, CCAA0B80206ADBF30057B336 /* ASRecursiveUnfairLock.m in Sources */, CCBDDD0620C62A2D00CBA922 /* ASMainThreadDeallocation.mm in Sources */, B35062181B010EFD0018CF92 /* ASDataController.mm in Sources */, @@ -2504,7 +2504,7 @@ 6959433F1D70815300B0EE1F /* ASDisplayNodeLayout.mm in Sources */, 68355B3E1CB57A60001D4E68 /* ASPINRemoteImageDownloader.m in Sources */, CC034A141E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.m in Sources */, - 509E68661B3AEDD7009B9150 /* CoreGraphics+ASConvenience.m in Sources */, + 509E68661B3AEDD7009B9150 /* CoreGraphics+ASConvenience.mm in Sources */, 254C6B871BF94F8A003EC431 /* ASTextKitEntityAttribute.m in Sources */, 34566CB31BC1213700715E6B /* ASPhotosFrameworkImageRequest.m in Sources */, 254C6B831BF94F8A003EC431 /* ASTextKitCoreTextAdditions.m in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f87a289..5be5334b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ - Reduced binary size by disabling exception support (which we don't use.) [Adlai Holler](https://github.com/Adlai-Holler) - Create and set delegate for clip corner layers within ASDisplayNode [Michael Schneider](https://github.com/maicki) [#1029](https://github.com/TextureGroup/Texture/pull/1029) - Improve locking situation in ASVideoPlayerNode [Michael Schneider](https://github.com/maicki) [#1042](https://github.com/TextureGroup/Texture/pull/1042) - +- Optimize drawing code + add examples how to round corners. [Michael Schneider](https://github.com/maicki) ## 2.7 - Fix pager node for interface coalescing. [Max Wang](https://github.com/wsdwsd0829) [#877](https://github.com/TextureGroup/Texture/pull/877) diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 347d41c42..0fa7a9eb6 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -53,6 +53,7 @@ #import #import #import +#import // Conditionally time these scopes to our debug ivars (only exist in debug/profile builds) #if TIME_DISPLAYNODE_OPS @@ -1508,7 +1509,7 @@ - (void)_pendingNodeDidDisplay:(ASDisplayNode *)node __instanceLock__.lock(); if (_placeholderLayer.superlayer && !placeholderShouldPersist) { void (^cleanupBlock)() = ^{ - [_placeholderLayer removeFromSuperlayer]; + [self->_placeholderLayer removeFromSuperlayer]; }; if (_placeholderFadeDuration > 0.0 && ASInterfaceStateIncludesVisible(self.interfaceState)) { @@ -1666,24 +1667,29 @@ - (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor CGSize size = CGSizeMake(radius + 1, radius + 1); ASGraphicsBeginImageContextWithOptions(size, NO, self.contentsScaleForDisplay); - CGContextRef ctx = UIGraphicsGetCurrentContext(); + CGContextRef context = UIGraphicsGetCurrentContext(); if (isRight == YES) { - CGContextTranslateCTM(ctx, -radius + 1, 0); + CGContextTranslateCTM(context, -radius + 1, 0); } if (isTop == YES) { - CGContextTranslateCTM(ctx, 0, -radius + 1); + CGContextTranslateCTM(context, 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]; - + + CGMutablePathRef roundedPath = CGPathCreateMutable(); + CGPathRef addedPath = ASCGRoundedPathCreate(CGRectMake(0, 0, radius * 2, radius * 2), radius); + CGPathAddPath(roundedPath, NULL, addedPath); + CGPathAddRect(roundedPath, NULL, CGRectMake(-1, -1, radius * 2 + 1, radius * 2 + 1)); + CGContextAddPath(context, roundedPath); + CGContextSetFillColorWithColor(context, backgroundColor.CGColor); + CGContextEOFillPath(context); + // No lock needed, as _clipCornerLayers is only modified on the main thread. - 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); + _clipCornerLayers[idx].contents = (id)(ASGraphicsGetImageAndEndCurrentContext().CGImage); + _clipCornerLayers[idx].bounds = CGRectMake(0.0, 0.0, size.width, size.height); + _clipCornerLayers[idx].anchorPoint = CGPointMake(isRight ? 1.0 : 0.0, isTop ? 1.0 : 0.0); + + CGPathRelease(addedPath); + CGPathRelease(roundedPath); } [self _layoutClipCornersIfNeeded]; }); @@ -1868,7 +1874,10 @@ - (void)didDisplayAsyncLayer:(_ASDisplayLayer *)layer [self displayDidFinish]; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" - (void)displayWillStart {} +#pragma clang diagnostic pop - (void)displayWillStartAsynchronously:(BOOL)asynchronously { ASDisplayNodeAssertMainThread(); @@ -2979,11 +2988,11 @@ - (void)didExitHierarchy if (ASInterfaceStateIncludesVisible(self.pendingInterfaceState)) { void(^exitVisibleInterfaceState)(void) = ^{ // This block intentionally retains self. - __instanceLock__.lock(); - unsigned isStillInHierarchy = _flags.isInHierarchy; - BOOL isVisible = ASInterfaceStateIncludesVisible(_pendingInterfaceState); - ASInterfaceState newState = (_pendingInterfaceState & ~ASInterfaceStateVisible); - __instanceLock__.unlock(); + self->__instanceLock__.lock(); + unsigned isStillInHierarchy = self->_flags.isInHierarchy; + BOOL isVisible = ASInterfaceStateIncludesVisible(self->_pendingInterfaceState); + ASInterfaceState newState = (self->_pendingInterfaceState & ~ASInterfaceStateVisible); + self->__instanceLock__.unlock(); if (!isStillInHierarchy && isVisible) { #if ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR if (![self supportsRangeManagedInterfaceState]) { @@ -3142,8 +3151,8 @@ - (void)applyPendingInterfaceState:(ASInterfaceState)newPendingState [self setDisplaySuspended:YES]; //schedule clear contents on next runloop dispatch_async(dispatch_get_main_queue(), ^{ - ASDN::MutexLocker l(__instanceLock__); - if (ASInterfaceStateIncludesDisplay(_interfaceState) == NO) { + ASDN::MutexLocker l(self->__instanceLock__); + if (ASInterfaceStateIncludesDisplay(self->_interfaceState) == NO) { [self clearContents]; } }); @@ -3160,8 +3169,8 @@ - (void)applyPendingInterfaceState:(ASInterfaceState)newPendingState [[self asyncLayer] cancelAsyncDisplay]; //schedule clear contents on next runloop dispatch_async(dispatch_get_main_queue(), ^{ - ASDN::MutexLocker l(__instanceLock__); - if (ASInterfaceStateIncludesDisplay(_interfaceState) == NO) { + ASDN::MutexLocker l(self->__instanceLock__); + if (ASInterfaceStateIncludesDisplay(self->_interfaceState) == NO) { [self clearContents]; } }); diff --git a/Source/ASImageNode.mm b/Source/ASImageNode.mm index d060d8993..374099136 100644 --- a/Source/ASImageNode.mm +++ b/Source/ASImageNode.mm @@ -737,19 +737,43 @@ asimagenode_modification_block_t ASImageNodeRoundBorderModificationBlock(CGFloat { return ^(UIImage *originalImage) { ASGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale); - UIBezierPath *roundOutline = [UIBezierPath bezierPathWithOvalInRect:(CGRect){CGPointZero, originalImage.size}]; + + CGContextRef context = UIGraphicsGetCurrentContext(); + CGRect rect = (CGRect){CGPointZero, originalImage.size}; + CGMutablePathRef path = CGPathCreateMutable(); + + CGPathAddEllipseInRect(path, NULL, rect); + CGContextAddPath(context, path); + // Make the image round - [roundOutline addClip]; + CGContextClip(context); + + // Although drawAtPoint:blendMode: would consider the CTM already, we are using CGContext* functions for drawing + // the image instead calling drawAtPoint:blendMode. This will save use 50% of retain calls for the image + CGContextSetBlendMode(context, kCGBlendModeCopy); + CGContextTranslateCTM(context, 0, CGRectGetMaxY(rect) + CGRectGetMinY(rect)); + CGContextScaleCTM(context, originalImage.scale, -originalImage.scale); + CGContextSetAlpha(context, 1.0); + CGContextDrawImage(context, rect, originalImage.CGImage); - // Draw the original image - [originalImage drawAtPoint:CGPointZero blendMode:kCGBlendModeCopy alpha:1]; + CGPathRelease(path); // Draw a border on top. if (borderWidth > 0.0) { - [borderColor setStroke]; - [roundOutline setLineWidth:borderWidth]; - [roundOutline stroke]; + // Begin a new path for the border + CGContextBeginPath(context); + + CGFloat strokeThickness = borderWidth; + CGFloat strokeInset = floor((strokeThickness + 1.0f) / 2.0f) - 1.0f; + CGPathRef path = CGPathCreateWithEllipseInRect(CGRectInset(rect, strokeInset, strokeInset), NULL); + CGContextAddPath(context, path); + + CGContextSetStrokeColorWithColor(context, borderColor.CGColor); + CGContextSetLineWidth(context, borderWidth); + CGContextStrokePath(context); + + CGPathRelease(path); } return ASGraphicsGetImageAndEndCurrentContext(); diff --git a/Source/Base/ASBaseDefines.h b/Source/Base/ASBaseDefines.h index 2fabc0dde..b3543ced6 100755 --- a/Source/Base/ASBaseDefines.h +++ b/Source/Base/ASBaseDefines.h @@ -130,7 +130,7 @@ #endif #endif -#define ASOVERLOADABLE __attribute__((overloadable)) +#define AS_OVERLOADABLE __attribute__((overloadable)) #if __has_attribute(noescape) diff --git a/Source/Details/CoreGraphics+ASConvenience.h b/Source/Details/CoreGraphics+ASConvenience.h index 9eeef3192..6229d4ad2 100644 --- a/Source/Details/CoreGraphics+ASConvenience.h +++ b/Source/Details/CoreGraphics+ASConvenience.h @@ -17,8 +17,8 @@ #import -#import #import +#import #import @@ -56,4 +56,11 @@ ASDISPLAYNODE_INLINE BOOL CGSizeEqualToSizeWithIn(CGSize size1, CGSize size2, CG return fabs(size1.width - size2.width) < delta && fabs(size1.height - size2.height) < delta; }; +AS_OVERLOADABLE AS_WARN_UNUSED_RESULT AS_EXTERN CGPathRef ASCGRoundedPathCreate(CGRect rect, UIRectCorner corners, CGSize cornerRadii); + +AS_OVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT CGPathRef ASCGRoundedPathCreate(CGRect rect, CGFloat cornerRadius) { + return ASCGRoundedPathCreate(rect, UIRectCornerAllCorners, CGSizeMake(cornerRadius, cornerRadius)); +} + + NS_ASSUME_NONNULL_END diff --git a/Source/Details/CoreGraphics+ASConvenience.m b/Source/Details/CoreGraphics+ASConvenience.m deleted file mode 100644 index 92169ffe4..000000000 --- a/Source/Details/CoreGraphics+ASConvenience.m +++ /dev/null @@ -1,19 +0,0 @@ -// -// CoreGraphics+ASConvenience.m -// Texture -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional -// grant of patent rights can be found in the PATENTS file in the same directory. -// -// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, -// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// - -#import - diff --git a/Source/Details/CoreGraphics+ASConvenience.mm b/Source/Details/CoreGraphics+ASConvenience.mm new file mode 100644 index 000000000..2a3b6b84e --- /dev/null +++ b/Source/Details/CoreGraphics+ASConvenience.mm @@ -0,0 +1,64 @@ +// +// CoreGraphics+ASConvenience.m +// Texture +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +AS_OVERLOADABLE CGPathRef ASCGRoundedPathCreate(CGRect rect, UIRectCorner corners, CGSize cornerRadii) { + CGMutablePathRef path = CGPathCreateMutable(); + + const CGPoint topLeft = rect.origin; + const CGPoint topRight = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect)); + const CGPoint bottomRight = CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(rect)); + const CGPoint bottomLeft = CGPointMake(CGRectGetMinX(rect), CGRectGetMaxY(rect)); + + if (corners & UIRectCornerTopLeft) { + CGPathMoveToPoint(path, NULL, topLeft.x+cornerRadii.width, topLeft.y); + } else { + CGPathMoveToPoint(path, NULL, topLeft.x, topLeft.y); + } + + if (corners & UIRectCornerTopRight) { + CGPathAddLineToPoint(path, NULL, topRight.x-cornerRadii.width, topRight.y); + CGPathAddCurveToPoint(path, NULL, topRight.x, topRight.y, topRight.x, topRight.y+cornerRadii.height, topRight.x, topRight.y+cornerRadii.height); + } else { + CGPathAddLineToPoint(path, NULL, topRight.x, topRight.y); + } + + if (corners & UIRectCornerBottomRight) { + CGPathAddLineToPoint(path, NULL, bottomRight.x, bottomRight.y-cornerRadii.height); + CGPathAddCurveToPoint(path, NULL, bottomRight.x, bottomRight.y, bottomRight.x-cornerRadii.width, bottomRight.y, bottomRight.x-cornerRadii.width, bottomRight.y); + } else { + CGPathAddLineToPoint(path, NULL, bottomRight.x, bottomRight.y); + } + + if (corners & UIRectCornerBottomLeft) { + CGPathAddLineToPoint(path, NULL, bottomLeft.x+cornerRadii.width, bottomLeft.y); + CGPathAddCurveToPoint(path, NULL, bottomLeft.x, bottomLeft.y, bottomLeft.x, bottomLeft.y-cornerRadii.height, bottomLeft.x, bottomLeft.y-cornerRadii.height); + } else { + CGPathAddLineToPoint(path, NULL, bottomLeft.x, bottomLeft.y); + } + + if (corners & UIRectCornerTopLeft) { + CGPathAddLineToPoint(path, NULL, topLeft.x, topLeft.y+cornerRadii.height); + CGPathAddCurveToPoint(path, NULL, topLeft.x, topLeft.y, topLeft.x+cornerRadii.width, topLeft.y, topLeft.x+cornerRadii.width, topLeft.y); + } else { + CGPathAddLineToPoint(path, NULL, topLeft.x, topLeft.y); + } + + CGPathCloseSubpath(path); + return path; +} diff --git a/Source/Layout/ASDimension.h b/Source/Layout/ASDimension.h index 462406eed..ffe6138b5 100644 --- a/Source/Layout/ASDimension.h +++ b/Source/Layout/ASDimension.h @@ -94,7 +94,7 @@ AS_EXTERN ASDimension const ASDimensionAuto; /** * Returns a dimension with the specified type and value. */ -ASOVERLOADABLE ASDISPLAYNODE_INLINE ASDimension ASDimensionMake(ASDimensionUnit unit, CGFloat value) +AS_OVERLOADABLE ASDISPLAYNODE_INLINE ASDimension ASDimensionMake(ASDimensionUnit unit, CGFloat value) { if (unit == ASDimensionUnitAuto ) { ASDisplayNodeCAssert(value == 0, @"ASDimension auto value must be 0."); @@ -112,7 +112,7 @@ ASOVERLOADABLE ASDISPLAYNODE_INLINE ASDimension ASDimensionMake(ASDimensionUnit /** * Returns a dimension with the specified points value. */ -ASOVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASDimension ASDimensionMake(CGFloat points) +AS_OVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASDimension ASDimensionMake(CGFloat points) { return ASDimensionMake(ASDimensionUnitPoints, points); } @@ -122,7 +122,7 @@ ASOVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASDimension ASDimensio * Examples: ASDimensionMake(@"50%") = ASDimensionMake(ASDimensionUnitFraction, 0.5) * ASDimensionMake(@"0.5pt") = ASDimensionMake(ASDimensionUnitPoints, 0.5) */ -ASOVERLOADABLE AS_WARN_UNUSED_RESULT AS_EXTERN ASDimension ASDimensionMake(NSString *dimension); +AS_OVERLOADABLE AS_WARN_UNUSED_RESULT AS_EXTERN ASDimension ASDimensionMake(NSString *dimension); /** * Returns a dimension with the specified points value. @@ -244,7 +244,7 @@ ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASSizeRangeHasSignificantArea(AS /** * Creates an ASSizeRange with provided min and max size. */ -ASOVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASSizeRange ASSizeRangeMake(CGSize min, CGSize max) +AS_OVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASSizeRange ASSizeRangeMake(CGSize min, CGSize max) { ASDisplayNodeCAssertPositiveReal(@"Range min width", min.width); ASDisplayNodeCAssertPositiveReal(@"Range min height", min.height); @@ -263,7 +263,7 @@ ASOVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASSizeRange ASSizeRang /** * Creates an ASSizeRange with provided size as both min and max. */ -ASOVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASSizeRange ASSizeRangeMake(CGSize exactSize) +AS_OVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASSizeRange ASSizeRangeMake(CGSize exactSize) { return ASSizeRangeMake(exactSize, exactSize); } diff --git a/Source/Layout/ASDimension.mm b/Source/Layout/ASDimension.mm index 4fd0e1800..a27226dda 100644 --- a/Source/Layout/ASDimension.mm +++ b/Source/Layout/ASDimension.mm @@ -25,7 +25,7 @@ ASDimension const ASDimensionAuto = {ASDimensionUnitAuto, 0}; -ASOVERLOADABLE ASDimension ASDimensionMake(NSString *dimension) +AS_OVERLOADABLE ASDimension ASDimensionMake(NSString *dimension) { if (dimension.length > 0) { diff --git a/Source/Private/ASDisplayNode+AsyncDisplay.mm b/Source/Private/ASDisplayNode+AsyncDisplay.mm index 2cf772aec..16cf9b39e 100644 --- a/Source/Private/ASDisplayNode+AsyncDisplay.mm +++ b/Source/Private/ASDisplayNode+AsyncDisplay.mm @@ -25,6 +25,7 @@ #import #import #import +#import @interface ASDisplayNode () <_ASDisplayLayerDelegate> @@ -111,10 +112,13 @@ - (void)_recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:(asdisplaynode CGContextTranslateCTM(context, frame.origin.x, frame.origin.y); - //support cornerRadius + // Support cornerRadius if (rasterizingFromAscendent && clipsToBounds) { if (cornerRadius) { - [[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius] addClip]; + CGPathRef cornerRadiusPath = ASCGRoundedPathCreate(bounds, cornerRadius); + CGContextAddPath(context, cornerRadiusPath); + CGContextClip(context); + CGPathRelease(cornerRadiusPath); } else { CGContextClipToRect(context, bounds); } @@ -127,13 +131,18 @@ - (void)_recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:(asdisplaynode CGContextFillRect(context, bounds); } - // If there is a display block, call it to get the image, then copy the image into the current context (which is the rasterized container's backing store). + // If there is a display block, call it to get the image, then copy the image into the current context (which + // is the rasterized container's backing store). if (displayBlock) { UIImage *image = (UIImage *)displayBlock(); if (image) { BOOL opaque = ASImageAlphaInfoIsOpaque(CGImageGetAlphaInfo(image.CGImage)); CGBlendMode blendMode = opaque ? kCGBlendModeCopy : kCGBlendModeNormal; - [image drawInRect:bounds blendMode:blendMode alpha:1]; + CGContextSetBlendMode(context, blendMode); + CGContextTranslateCTM(context, 0, CGRectGetMaxY(bounds) + CGRectGetMinY(bounds)); + CGContextScaleCTM(context, 1, -1); + CGContextSetAlpha(context, 1.0); + CGContextDrawImage(context, bounds, image.CGImage); } } }; @@ -295,7 +304,10 @@ - (void)__willDisplayNodeContentWithRenderingContext:(CGContextRef)context drawP 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]; + CGPathRef cornerRadiusPath = ASCGRoundedPathCreate(boundingBox, cornerRadius); + CGContextAddPath(context, cornerRadiusPath); + CGContextClip(context); + CGPathRelease(cornerRadiusPath); } if (willDisplayNodeContentWithRenderingContext) { @@ -332,34 +344,58 @@ - (void)__didDisplayNodeContentWithRenderingContext:(CGContextRef)context image: CGFloat white = 0.0f, alpha = 0.0f; [backgroundColor getWhite:&white alpha:&alpha]; ASGraphicsBeginImageContextWithOptions(bounds.size, (alpha == 1.0f), contentsScale); + context = UIGraphicsGetCurrentContext(); [*image drawInRect:bounds]; } else { bounds = CGContextGetClipBoundingBox(context); } ASDisplayNodeAssert(UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self); + + CGContextSaveGState(context); - 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]; - } + CGMutablePathRef roundedHole = CGPathCreateMutable(); + CGPathAddRect(roundedHole, NULL, bounds); + + CGPathRef additionalPath = ASCGRoundedPathCreate(bounds, cornerRadius * contentsScale); + CGPathAddPath(roundedHole, NULL, additionalPath); + + CGContextAddPath(context, roundedHole); // 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]; + CGContextSetFillColorWithColor(context, backgroundColor.CGColor); + + CGContextSetAlpha(context, 1.0); + CGContextSetBlendMode(context, kCGBlendModeCopy); + CGContextEOFillPath(context); + + CGPathRelease(additionalPath); + CGPathRelease(roundedHole); - [roundedPath stroke]; // Won't do anything if borderWidth is 0 and roundedPath is nil. + CGContextRestoreGState(context); + // Drawing borders with ASCornerRoundingTypePrecomposited set has some problems at the moment. If the borderWidth is + // set, besides we are drawing the border with the given corner radius, the CALayer also picks up the borderWidth + // value and draws the border without the cornerRadius. + if (borderWidth > 0.0f) { // Don't create roundedPath and stroke if borderWidth is 0.0 + CGContextSaveGState(context); + + CGFloat strokeThickness = borderWidth * contentsScale; + CGFloat strokeInset = ((strokeThickness + 1.0f) / 2.0f) - 1.0f; + CGPathRef roundedPath = ASCGRoundedPathCreate(CGRectInset(bounds, strokeInset, strokeInset), _cornerRadius * contentsScale); + CGContextAddPath(context, roundedPath); + + CGContextSetLineWidth(context, strokeThickness); + CGContextSetStrokeColorWithColor(context, borderColor); + + CGContextStrokePath(context); + + CGPathRelease(roundedPath); + + CGContextRestoreGState(context); + } + if (*image) { *image = ASGraphicsGetImageAndEndCurrentContext(); } diff --git a/Source/UIImage+ASConvenience.m b/Source/UIImage+ASConvenience.mm similarity index 86% rename from Source/UIImage+ASConvenience.m rename to Source/UIImage+ASConvenience.mm index c1d0751e3..84e87b0fc 100644 --- a/Source/UIImage+ASConvenience.m +++ b/Source/UIImage+ASConvenience.mm @@ -19,6 +19,7 @@ #import #import #import +#import #pragma mark - ASDKFastImageNamed @@ -114,9 +115,9 @@ + (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius // UIBezierPath objects are fairly small and these are equally sized. 20 should be plenty for many different parameters. __pathCache.countLimit = 20; }); - + // Treat clear background color as no background color - if ([cornerColor isEqual:[UIColor clearColor]]) { + if (CGColorGetAlpha(cornerColor.CGColor) == 0) { cornerColor = nil; } @@ -140,33 +141,46 @@ + (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius // We should probably check if the background color has any alpha component but that // might be expensive due to needing to check mulitple color spaces. ASGraphicsBeginImageContextWithOptions(bounds.size, cornerColor != nil, scale); - + + CGContextRef context = UIGraphicsGetCurrentContext(); + + // Draw Corners BOOL contextIsClean = YES; if (cornerColor) { contextIsClean = NO; - [cornerColor setFill]; + + CGContextSetFillColorWithColor(context, cornerColor.CGColor); // Copy "blend" mode is extra fast because it disregards any value currently in the buffer and overrides directly. - UIRectFillUsingBlendMode(bounds, kCGBlendModeCopy); + CGContextSetBlendMode(context, kCGBlendModeCopy); + CGContextFillRect(context, bounds); } - + + // Draw fill BOOL canUseCopy = contextIsClean || (CGColorGetAlpha(fillColor.CGColor) == 1); - [fillColor setFill]; - [path fillWithBlendMode:(canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal) alpha:1]; - + CGContextSetFillColorWithColor(context, fillColor.CGColor); + CGContextSetBlendMode(context, canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal); + CGContextSetAlpha(context, 1.0); + CGContextAddPath(context, path.CGPath); + CGContextFillPath(context); + + // Add a border if (borderColor) { - [borderColor setStroke]; - // Inset border fully inside filled path (not halfway on each side of path) CGRect strokeRect = CGRectInset(bounds, borderWidth / 2.0, borderWidth / 2.0); - + // It is rarer to have a stroke path, and our cache key only handles rounded rects for the exact-stretchable // size calculated by cornerRadius, so we won't bother caching this path. Profiling validates this decision. - UIBezierPath *strokePath = [UIBezierPath bezierPathWithRoundedRect:strokeRect - byRoundingCorners:roundedCorners - cornerRadii:cornerRadii]; - [strokePath setLineWidth:borderWidth]; + CGPathRef strokePath = ASCGRoundedPathCreate(strokeRect, roundedCorners, cornerRadii); + + CGContextSetStrokeColorWithColor(context, borderColor.CGColor); + CGContextSetLineWidth(context, borderWidth); + CGContextSetAlpha(context, 1.0); BOOL canUseCopy = (CGColorGetAlpha(borderColor.CGColor) == 1); - [strokePath strokeWithBlendMode:(canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal) alpha:1]; + CGContextSetBlendMode(context, (canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal)); + CGContextAddPath(context, strokePath); + CGContextStrokePath(context); + + CGPathRelease(strokePath); } UIImage *result = ASGraphicsGetImageAndEndCurrentContext(); diff --git a/Source/tvOS/ASControlNode+tvOS.m b/Source/tvOS/ASControlNode+tvOS.m index 77e541862..c18b67eec 100644 --- a/Source/tvOS/ASControlNode+tvOS.m +++ b/Source/tvOS/ASControlNode+tvOS.m @@ -83,7 +83,9 @@ - (void)applyDefaultShadowProperties:(CALayer *)layer layer.shadowColor = [UIColor blackColor].CGColor; layer.shadowRadius = 12.0; layer.shadowOpacity = 0.45; - layer.shadowPath = [UIBezierPath bezierPathWithRect:self.layer.bounds].CGPath; + CGPathRef shadowPath = CGPathCreateWithRect(self.layer.bounds, NULL); + layer.shadowPath = shadowPath; + CGPathRelease(shadowPath); } - (void)setDefaultFocusAppearance @@ -93,7 +95,9 @@ - (void)setDefaultFocusAppearance layer.shadowColor = [UIColor blackColor].CGColor; layer.shadowRadius = 0; layer.shadowOpacity = 0; - layer.shadowPath = [UIBezierPath bezierPathWithRect:self.layer.bounds].CGPath; + CGPathRef shadowPath = CGPathCreateWithRect(self.layer.bounds, NULL); + layer.shadowPath = shadowPath; + CGPathRelease(shadowPath); self.view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1); } @end diff --git a/Source/tvOS/ASImageNode+tvOS.m b/Source/tvOS/ASImageNode+tvOS.m index 9482fdc8c..8f8486b94 100644 --- a/Source/tvOS/ASImageNode+tvOS.m +++ b/Source/tvOS/ASImageNode+tvOS.m @@ -169,7 +169,9 @@ - (void)setFocusedState layer.shadowColor = [UIColor blackColor].CGColor; layer.shadowRadius = 12.0; layer.shadowOpacity = 0.45; - layer.shadowPath = [UIBezierPath bezierPathWithRect:self.layer.bounds].CGPath; + CGPathRef shadowPath = CGPathCreateWithRect(self.layer.bounds, NULL); + layer.shadowPath = shadowPath; + CGPathRelease(shadowPath); view.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1.25, 1.25); } diff --git a/Tests/ReferenceImages_iOS_10/ASImageNodeSnapshotTests/testRoundedCornerBlock@2x.png b/Tests/ReferenceImages_iOS_10/ASImageNodeSnapshotTests/testRoundedCornerBlock@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c4686232a23cb980c70b44339427ddfa81412562 GIT binary patch literal 11609 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