Perf improvements for CVC rendering

Fixes some low-hanging expensive renders in CVC. Namely:
- There are a few places where shadow paths can be trivially calculated.
  Explicitly setting the shadow path saves an offscreen pass.
- Adopts CALayer corner masking where feasible (e.g. layers with
  consistent corner radii).
This commit is contained in:
Michelle Linington 2021-04-08 00:41:27 -07:00
parent a0d542657b
commit 01a4ee4cc7
10 changed files with 168 additions and 141 deletions

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
@ -120,6 +120,9 @@ class CallButton: UIButton {
currentConstraints.removeAll()
currentConstraints += circleView.autoSetDimensions(to: CGSize(square: currentIconSize))
circleView.layer.shadowPath = UIBezierPath(
ovalIn: CGRect(origin: .zero, size: .square(currentIconSize))
).cgPath
currentConstraints += iconView.autoPinEdgesToSuperviewEdges(with: currentIconInsets)
if let dropdownIconView = dropdownIconView {
currentConstraints.append(dropdownIconView.autoPinEdge(.leading, to: .trailing, of: iconView, withOffset: isSmall ? 0 : 2))

View File

@ -250,6 +250,9 @@ class ProfileSettingsViewController: OWSTableViewController2 {
cameraImageContainer.layer.shadowOpacity = 0.2
cameraImageContainer.layer.shadowRadius = 4
cameraImageContainer.layer.shadowOffset = CGSize(width: 0, height: 2)
cameraImageContainer.layer.shadowPath = UIBezierPath(
ovalIn: CGRect(origin: .zero, size: .square(32))
).cgPath
cell.contentView.addSubview(cameraImageContainer)
cameraImageContainer.autoPinTrailing(toEdgeOf: avatarImageView)
@ -260,6 +263,9 @@ class ProfileSettingsViewController: OWSTableViewController2 {
secondaryShadowView.layer.shadowOpacity = 0.12
secondaryShadowView.layer.shadowRadius = 16
secondaryShadowView.layer.shadowOffset = CGSize(width: 0, height: 4)
secondaryShadowView.layer.shadowPath = UIBezierPath(
ovalIn: CGRect(origin: .zero, size: .square(32))
).cgPath
cameraImageContainer.addSubview(secondaryShadowView)
secondaryShadowView.autoPinEdgesToSuperviewEdges()

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
@ -167,6 +167,42 @@ public class QuotedMessageView: UIView {
: conversationStyle.quotingSelfHighlightColor())
}
func createInnerBubbleView(sharpCorners: OWSDirectionalRectCorner) -> UIView {
let sharpCornerRadius: CGFloat = 4
let wideCornerRadius: CGFloat = 12
let innerBubbleView: UIView
if sharpCorners.isEmpty || sharpCorners.contains(.allCorners) {
innerBubbleView = UIView()
innerBubbleView.layer.maskedCorners = .all
innerBubbleView.layer.cornerRadius = sharpCorners.isEmpty ? wideCornerRadius : sharpCornerRadius
} else {
// Slow path. CA isn't optimized to handle corners of multiple radii
// Let's do it by hand with a CAShapeLayer
let maskLayer = CAShapeLayer()
innerBubbleView = OWSLayerView(frame: .zero) { (layerView: UIView) in
let layerFrame = layerView.bounds
let bubbleLeft: CGFloat = 0
let bubbleRight = layerFrame.width
let bubbleTop: CGFloat = 0
let bubbleBottom = layerFrame.height
let bezierPath = OWSBubbleView.roundedBezierRect(withBubbleTop: bubbleTop,
bubbleLeft: bubbleLeft,
bubbleBottom: bubbleBottom,
bubbleRight: bubbleRight,
sharpCornerRadius: sharpCornerRadius,
wideCornerRadius: wideCornerRadius,
sharpCorners: sharpCorners)
maskLayer.path = bezierPath.cgPath
}
innerBubbleView.layer.mask = maskLayer
}
innerBubbleView.backgroundColor = conversationStyle.quotedReplyBubbleColor
return innerBubbleView
}
func createContents(rootView: UIView,
quotedAuthorLabel: UILabel,
quotedTextLabel: UILabel,
@ -179,32 +215,7 @@ public class QuotedMessageView: UIView {
rootView.layoutMargins = .zero
rootView.clipsToBounds = true
let maskLayer = CAShapeLayer()
let innerBubbleView = OWSLayerView(frame: .zero) { (layerView: UIView) in
let layerFrame = layerView.bounds
let bubbleLeft: CGFloat = 0
let bubbleRight = layerFrame.width
let bubbleTop: CGFloat = 0
let bubbleBottom = layerFrame.height
let sharpCornerRadius: CGFloat = 4
let wideCornerRadius: CGFloat = 12
let bezierPath = OWSBubbleView.roundedBezierRect(withBubbleTop: bubbleTop,
bubbleLeft: bubbleLeft,
bubbleBottom: bubbleBottom,
bubbleRight: bubbleRight,
sharpCornerRadius: sharpCornerRadius,
wideCornerRadius: wideCornerRadius,
sharpCorners: sharpCorners)
maskLayer.path = bezierPath.cgPath
}
innerBubbleView.layer.mask = maskLayer
innerBubbleView.backgroundColor = conversationStyle.quotedReplyBubbleColor
let innerBubbleView = createInnerBubbleView(sharpCorners: sharpCorners)
rootView.addSubview(innerBubbleView)
innerBubbleView.autoPinLeadingToSuperviewMargin(withInset: bubbleHMargin)
innerBubbleView.autoPinTrailingToSuperviewMargin(withInset: bubbleHMargin)

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import "ConversationScrollButton.h"
@ -67,6 +67,7 @@ NS_ASSUME_NONNULL_BEGIN
iconView.userInteractionEnabled = NO;
const CGFloat circleSize = self.class.circleSize;
CGRect shadowRect = CGRectMake(0, 0, circleSize, circleSize);
UIView *shadowView = [[OWSCircleView alloc] initWithDiameter:circleSize];
self.shadowView = shadowView;
@ -75,6 +76,7 @@ NS_ASSUME_NONNULL_BEGIN
shadowView.layer.shadowRadius = 4;
shadowView.layer.shadowOpacity = 0.05f;
shadowView.layer.shadowColor = UIColor.blackColor.CGColor;
shadowView.layer.shadowPath = [UIBezierPath bezierPathWithOvalInRect:shadowRect].CGPath;
UIView *circleView = [[OWSCircleView alloc] initWithDiameter:circleSize];
self.circleView = circleView;
@ -83,6 +85,7 @@ NS_ASSUME_NONNULL_BEGIN
circleView.layer.shadowRadius = 12.f;
circleView.layer.shadowOpacity = 0.3f;
circleView.layer.shadowColor = UIColor.blackColor.CGColor;
circleView.layer.shadowPath = [UIBezierPath bezierPathWithOvalInRect:shadowRect].CGPath;
UIView *unreadBadge = [UIView new];
self.unreadBadge = unreadBadge;

View File

@ -339,13 +339,9 @@ public protocol LinkPreviewViewDraftDelegate {
// MARK: -
@objc
public class LinkPreviewImageView: UIImageView {
private let maskLayer = CAShapeLayer()
@objc
public enum Rounding: UInt {
case none
private class LinkPreviewImageView: UIImageView {
fileprivate enum Rounding: UInt {
case standard
case asymmetrical
case circular
}
@ -353,34 +349,38 @@ public class LinkPreviewImageView: UIImageView {
private let rounding: Rounding
fileprivate var isHero = false
@objc
public init(rounding: Rounding) {
self.rounding = rounding
// We only need to use a more complicated corner mask if we're
// drawing asymmetric corners. This is an exceptional case to match
// the input toolbar curve.
private let asymmetricCornerMask = CAShapeLayer()
init(rounding: Rounding) {
self.rounding = rounding
super.init(frame: .zero)
self.layer.mask = maskLayer
if rounding == .asymmetrical {
layer.mask = asymmetricCornerMask
}
}
public required init?(coder aDecoder: NSCoder) {
self.rounding = .none
required init?(coder aDecoder: NSCoder) {
self.rounding = .standard
super.init(coder: aDecoder)
}
public override var bounds: CGRect {
override var bounds: CGRect {
didSet {
updateMaskLayer()
}
}
public override var frame: CGRect {
override var frame: CGRect {
didSet {
updateMaskLayer()
}
}
public override var center: CGPoint {
override var center: CGPoint {
didSet {
updateMaskLayer()
}
@ -388,62 +388,62 @@ public class LinkPreviewImageView: UIImageView {
private func updateMaskLayer() {
let layerBounds = self.bounds
guard rounding != .circular else {
maskLayer.path = UIBezierPath(ovalIn: layerBounds).cgPath
return
}
// One of the corners has assymetrical rounding to match the input toolbar border.
// This is somewhat inconvenient.
let upperLeft = CGPoint(x: 0, y: 0)
let upperRight = CGPoint(x: layerBounds.size.width, y: 0)
let lowerRight = CGPoint(x: layerBounds.size.width, y: layerBounds.size.height)
let lowerLeft = CGPoint(x: 0, y: layerBounds.size.height)
let bigRounding: CGFloat = 14
let smallRounding: CGFloat = 6
let upperLeftRounding: CGFloat
let upperRightRounding: CGFloat
if rounding == .asymmetrical {
upperLeftRounding = CurrentAppContext().isRTL ? smallRounding : bigRounding
upperRightRounding = CurrentAppContext().isRTL ? bigRounding : smallRounding
} else {
upperLeftRounding = smallRounding
upperRightRounding = smallRounding
switch rounding {
case .standard:
layer.cornerRadius = smallRounding
layer.maskedCorners = isHero ? .top : .all
case .circular:
layer.cornerRadius = bounds.size.smallerAxis
layer.maskedCorners = .all
case .asymmetrical:
// This uses a more expensive layer mask to clip corners
// with different radii.
// This should only be used in the input toolbar so perf is
// less of a concern here.
owsAssertDebug(!isHero, "Link preview drafts never use hero images")
let upperLeft = CGPoint(x: 0, y: 0)
let upperRight = CGPoint(x: layerBounds.size.width, y: 0)
let lowerRight = CGPoint(x: layerBounds.size.width, y: layerBounds.size.height)
let lowerLeft = CGPoint(x: 0, y: layerBounds.size.height)
let upperLeftRounding: CGFloat = CurrentAppContext().isRTL ? smallRounding : bigRounding
let upperRightRounding: CGFloat = CurrentAppContext().isRTL ? bigRounding : smallRounding
let lowerRightRounding = smallRounding
let lowerLeftRounding = smallRounding
let path = UIBezierPath()
// It's sufficient to "draw" the rounded corners and not the edges that connect them.
path.addArc(withCenter: upperLeft.offsetBy(dx: +upperLeftRounding).offsetBy(dy: +upperLeftRounding),
radius: upperLeftRounding,
startAngle: CGFloat.pi * 1.0,
endAngle: CGFloat.pi * 1.5,
clockwise: true)
path.addArc(withCenter: upperRight.offsetBy(dx: -upperRightRounding).offsetBy(dy: +upperRightRounding),
radius: upperRightRounding,
startAngle: CGFloat.pi * 1.5,
endAngle: CGFloat.pi * 0.0,
clockwise: true)
path.addArc(withCenter: lowerRight.offsetBy(dx: -lowerRightRounding).offsetBy(dy: -lowerRightRounding),
radius: lowerRightRounding,
startAngle: CGFloat.pi * 0.0,
endAngle: CGFloat.pi * 0.5,
clockwise: true)
path.addArc(withCenter: lowerLeft.offsetBy(dx: +lowerLeftRounding).offsetBy(dy: -lowerLeftRounding),
radius: lowerLeftRounding,
startAngle: CGFloat.pi * 0.5,
endAngle: CGFloat.pi * 1.0,
clockwise: true)
asymmetricCornerMask.path = path.cgPath
}
let lowerRightRounding = isHero ? 0 : smallRounding
let lowerLeftRounding = isHero ? 0 : smallRounding
let path = UIBezierPath()
// It's sufficient to "draw" the rounded corners and not the edges that connect them.
path.addArc(withCenter: upperLeft.offsetBy(dx: +upperLeftRounding).offsetBy(dy: +upperLeftRounding),
radius: upperLeftRounding,
startAngle: CGFloat.pi * 1.0,
endAngle: CGFloat.pi * 1.5,
clockwise: true)
path.addArc(withCenter: upperRight.offsetBy(dx: -upperRightRounding).offsetBy(dy: +upperRightRounding),
radius: upperRightRounding,
startAngle: CGFloat.pi * 1.5,
endAngle: CGFloat.pi * 0.0,
clockwise: true)
path.addArc(withCenter: lowerRight.offsetBy(dx: -lowerRightRounding).offsetBy(dy: -lowerRightRounding),
radius: lowerRightRounding,
startAngle: CGFloat.pi * 0.0,
endAngle: CGFloat.pi * 0.5,
clockwise: true)
path.addArc(withCenter: lowerLeft.offsetBy(dx: +lowerLeftRounding).offsetBy(dy: -lowerLeftRounding),
radius: lowerLeftRounding,
startAngle: CGFloat.pi * 0.5,
endAngle: CGFloat.pi * 1.0,
clockwise: true)
maskLayer.path = path.cgPath
}
}
@ -472,6 +472,7 @@ public class LinkPreviewView: UIStackView {
public var hasAsymmetricalRounding: Bool = false {
didSet {
AssertIsOnMainThread()
owsAssertDebug(isDraft)
if hasAsymmetricalRounding != oldValue {
updateContents()
@ -1016,13 +1017,7 @@ public class LinkPreviewView: UIStackView {
owsFailDebug("Could not load image.")
return nil
}
let rounding: LinkPreviewImageView.Rounding = {
if let roundingParam = roundingParam {
return roundingParam
}
return self.hasAsymmetricalRounding ? .asymmetrical : .none
}()
let imageView = LinkPreviewImageView(rounding: rounding)
let imageView = LinkPreviewImageView(rounding: roundingParam ?? .standard)
imageView.image = image
imageView.isHero = Self.sentIsHero(state: state)
return imageView
@ -1041,7 +1036,7 @@ public class LinkPreviewView: UIStackView {
owsFailDebug("Could not load image.")
return nil
}
let rounding: LinkPreviewImageView.Rounding = hasAsymmetricalRounding ? .asymmetrical : .none
let rounding: LinkPreviewImageView.Rounding = hasAsymmetricalRounding ? .asymmetrical : .standard
let imageView = LinkPreviewImageView(rounding: rounding)
imageView.image = image
return imageView
@ -1268,29 +1263,6 @@ public class LinkPreviewView: UIStackView {
return CGSize(width: width, height: height)
}
@objc
public func addBorderViews(bubbleView: OWSBubbleView) {
if let heroImageView = self.heroImageView {
let borderView = OWSBubbleShapeView(draw: ())
borderView.strokeColor = Theme.primaryTextColor
borderView.strokeThickness = CGHairlineWidthFraction(1.8)
heroImageView.addSubview(borderView)
bubbleView.addPartnerView(borderView)
borderView.autoPinEdgesToSuperviewEdges()
}
if let sentBodyView = self.sentBodyView {
let borderView = OWSBubbleShapeView(draw: ())
let borderColor = (Theme.isDarkThemeEnabled ? UIColor.ows_gray60 : UIColor.ows_gray15)
borderView.strokeColor = borderColor
borderView.strokeThickness = CGHairlineWidthFraction(1.8)
sentBodyView.addSubview(borderView)
bubbleView.addPartnerView(borderView)
borderView.autoPinEdgesToSuperviewEdges()
} else {
owsFailDebug("Missing sentBodyView")
}
}
@objc func didTapCancel(sender: UIButton) {
self.draftDelegate?.linkPreviewDidCancel()
}

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import "OWSBubbleShapeView.h"
@ -255,9 +255,6 @@ typedef NS_ENUM(NSUInteger, OWSBubbleShapeViewMode) {
self.layer.mask = self.maskLayer;
break;
case OWSBubbleShapeViewMode_InnerShadow: {
self.maskLayer.path = bezierPath.CGPath;
self.layer.mask = self.maskLayer;
// Inner shadow.
// This should usually not be visible; it is used to distinguish
// profile pics from the background if they are similar.
@ -267,16 +264,21 @@ typedef NS_ENUM(NSUInteger, OWSBubbleShapeViewMode) {
UIBezierPath *shadowPath = [bezierPath copy];
// This can be any value large enough to cast a sufficiently large shadow.
CGFloat shadowInset = -(self.innerShadowRadius * 4.f);
[shadowPath
appendPath:[UIBezierPath bezierPathWithRect:CGRectInset(shadowBounds, shadowInset, shadowInset)]];
CGRect outerRect = CGRectInset(shadowBounds, shadowInset, shadowInset);
UIBezierPath *outerPath = [UIBezierPath bezierPathWithRect:outerRect];
// -[CALayer shadowPath] uses the non-zero winding rule
// Reverse the path to make the directions line up correctly.
[shadowPath appendPath:[outerPath bezierPathByReversingPath]];
// This can be any color since the fill should be clipped.
self.shapeLayer.fillColor = UIColor.blackColor.CGColor;
self.shapeLayer.path = shadowPath.CGPath;
self.shapeLayer.fillRule = kCAFillRuleEvenOdd;
self.shapeLayer.shadowColor = self.innerShadowColor.CGColor;
self.shapeLayer.shadowRadius = self.innerShadowRadius;
self.shapeLayer.shadowOpacity = self.innerShadowOpacity;
self.shapeLayer.shadowOffset = CGSizeZero;
self.shapeLayer.shadowPath = shadowPath.CGPath;
break;
}

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@ -12,7 +12,9 @@ typedef NS_OPTIONS(NSUInteger, OWSDirectionalRectCorner) {
OWSDirectionalRectCornerTopTrailing = 1 << 1,
OWSDirectionalRectCornerBottomLeading = 1 << 2,
OWSDirectionalRectCornerBottomTrailing = 1 << 3,
OWSDirectionalRectCornerAllCorners = ~0UL
OWSDirectionalRectCornerAllCorners = OWSDirectionalRectCornerTopLeading | OWSDirectionalRectCornerTopTrailing
| OWSDirectionalRectCornerBottomLeading | OWSDirectionalRectCornerBottomTrailing
};
@class OWSBubbleView;

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import "OWSBubbleView.h"
@ -73,8 +73,8 @@ const CGFloat kOWSMessageCellCornerRadius_Small = 4;
[self.layer addSublayer:self.gradientLayer];
self.gradientLayer.hidden = YES;
self.layer.masksToBounds = YES;
self.maskLayer = [CAShapeLayer new];
self.layer.mask = self.maskLayer;
_partnerViews = [NSMutableArray new];
@ -211,6 +211,24 @@ const CGFloat kOWSMessageCellCornerRadius_Small = 4;
self.maskLayer.path = bezierPath.CGPath;
BOOL noSharpCorners = (self.sharpCorners == 0);
BOOL allSharpCorners
= (self.sharpCorners & OWSDirectionalRectCornerAllCorners) == OWSDirectionalRectCornerAllCorners;
if (noSharpCorners || allSharpCorners) {
// Although we have a bubble bezier path, it's just a simple rounded rect
// It's more performant to use the CA corner mask property than a layer mask
CGFloat sharpRadius = kOWSMessageCellCornerRadius_Small;
CGFloat wideRadius = kOWSMessageCellCornerRadius_Large;
self.layer.cornerRadius = (self.sharpCorners == 0) ? wideRadius : sharpRadius;
self.layer.mask = nil;
} else {
self.layer.cornerRadius = 0;
self.layer.mask = self.maskLayer;
}
[CATransaction commit];
}

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
@ -117,6 +117,7 @@ open class TooltipView: UIView {
bezierPath.addLine(to: tailPoint)
shapeLayer.path = bezierPath.cgPath
shapeLayer.shadowPath = bezierPath.cgPath
shapeLayer.frame = view.bounds
}

View File

@ -621,6 +621,15 @@ public extension UIBezierPath {
}
}
public extension CACornerMask {
static let top: CACornerMask = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
static let bottom: CACornerMask = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
static let left: CACornerMask = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
static let right: CACornerMask = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
static let all: CACornerMask = top.union(bottom)
}
// MARK: -
@objc