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:
parent
a0d542657b
commit
01a4ee4cc7
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@ -120,6 +120,9 @@ class CallButton: UIButton {
|
|||||||
currentConstraints.removeAll()
|
currentConstraints.removeAll()
|
||||||
|
|
||||||
currentConstraints += circleView.autoSetDimensions(to: CGSize(square: currentIconSize))
|
currentConstraints += circleView.autoSetDimensions(to: CGSize(square: currentIconSize))
|
||||||
|
circleView.layer.shadowPath = UIBezierPath(
|
||||||
|
ovalIn: CGRect(origin: .zero, size: .square(currentIconSize))
|
||||||
|
).cgPath
|
||||||
currentConstraints += iconView.autoPinEdgesToSuperviewEdges(with: currentIconInsets)
|
currentConstraints += iconView.autoPinEdgesToSuperviewEdges(with: currentIconInsets)
|
||||||
if let dropdownIconView = dropdownIconView {
|
if let dropdownIconView = dropdownIconView {
|
||||||
currentConstraints.append(dropdownIconView.autoPinEdge(.leading, to: .trailing, of: iconView, withOffset: isSmall ? 0 : 2))
|
currentConstraints.append(dropdownIconView.autoPinEdge(.leading, to: .trailing, of: iconView, withOffset: isSmall ? 0 : 2))
|
||||||
|
|||||||
@ -250,6 +250,9 @@ class ProfileSettingsViewController: OWSTableViewController2 {
|
|||||||
cameraImageContainer.layer.shadowOpacity = 0.2
|
cameraImageContainer.layer.shadowOpacity = 0.2
|
||||||
cameraImageContainer.layer.shadowRadius = 4
|
cameraImageContainer.layer.shadowRadius = 4
|
||||||
cameraImageContainer.layer.shadowOffset = CGSize(width: 0, height: 2)
|
cameraImageContainer.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||||
|
cameraImageContainer.layer.shadowPath = UIBezierPath(
|
||||||
|
ovalIn: CGRect(origin: .zero, size: .square(32))
|
||||||
|
).cgPath
|
||||||
|
|
||||||
cell.contentView.addSubview(cameraImageContainer)
|
cell.contentView.addSubview(cameraImageContainer)
|
||||||
cameraImageContainer.autoPinTrailing(toEdgeOf: avatarImageView)
|
cameraImageContainer.autoPinTrailing(toEdgeOf: avatarImageView)
|
||||||
@ -260,6 +263,9 @@ class ProfileSettingsViewController: OWSTableViewController2 {
|
|||||||
secondaryShadowView.layer.shadowOpacity = 0.12
|
secondaryShadowView.layer.shadowOpacity = 0.12
|
||||||
secondaryShadowView.layer.shadowRadius = 16
|
secondaryShadowView.layer.shadowRadius = 16
|
||||||
secondaryShadowView.layer.shadowOffset = CGSize(width: 0, height: 4)
|
secondaryShadowView.layer.shadowOffset = CGSize(width: 0, height: 4)
|
||||||
|
secondaryShadowView.layer.shadowPath = UIBezierPath(
|
||||||
|
ovalIn: CGRect(origin: .zero, size: .square(32))
|
||||||
|
).cgPath
|
||||||
|
|
||||||
cameraImageContainer.addSubview(secondaryShadowView)
|
cameraImageContainer.addSubview(secondaryShadowView)
|
||||||
secondaryShadowView.autoPinEdgesToSuperviewEdges()
|
secondaryShadowView.autoPinEdgesToSuperviewEdges()
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@ -167,6 +167,42 @@ public class QuotedMessageView: UIView {
|
|||||||
: conversationStyle.quotingSelfHighlightColor())
|
: 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,
|
func createContents(rootView: UIView,
|
||||||
quotedAuthorLabel: UILabel,
|
quotedAuthorLabel: UILabel,
|
||||||
quotedTextLabel: UILabel,
|
quotedTextLabel: UILabel,
|
||||||
@ -179,32 +215,7 @@ public class QuotedMessageView: UIView {
|
|||||||
rootView.layoutMargins = .zero
|
rootView.layoutMargins = .zero
|
||||||
rootView.clipsToBounds = true
|
rootView.clipsToBounds = true
|
||||||
|
|
||||||
let maskLayer = CAShapeLayer()
|
let innerBubbleView = createInnerBubbleView(sharpCorners: sharpCorners)
|
||||||
|
|
||||||
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
|
|
||||||
rootView.addSubview(innerBubbleView)
|
rootView.addSubview(innerBubbleView)
|
||||||
innerBubbleView.autoPinLeadingToSuperviewMargin(withInset: bubbleHMargin)
|
innerBubbleView.autoPinLeadingToSuperviewMargin(withInset: bubbleHMargin)
|
||||||
innerBubbleView.autoPinTrailingToSuperviewMargin(withInset: bubbleHMargin)
|
innerBubbleView.autoPinTrailingToSuperviewMargin(withInset: bubbleHMargin)
|
||||||
|
|||||||
@ -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"
|
#import "ConversationScrollButton.h"
|
||||||
@ -67,6 +67,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
iconView.userInteractionEnabled = NO;
|
iconView.userInteractionEnabled = NO;
|
||||||
|
|
||||||
const CGFloat circleSize = self.class.circleSize;
|
const CGFloat circleSize = self.class.circleSize;
|
||||||
|
CGRect shadowRect = CGRectMake(0, 0, circleSize, circleSize);
|
||||||
|
|
||||||
UIView *shadowView = [[OWSCircleView alloc] initWithDiameter:circleSize];
|
UIView *shadowView = [[OWSCircleView alloc] initWithDiameter:circleSize];
|
||||||
self.shadowView = shadowView;
|
self.shadowView = shadowView;
|
||||||
@ -75,6 +76,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
shadowView.layer.shadowRadius = 4;
|
shadowView.layer.shadowRadius = 4;
|
||||||
shadowView.layer.shadowOpacity = 0.05f;
|
shadowView.layer.shadowOpacity = 0.05f;
|
||||||
shadowView.layer.shadowColor = UIColor.blackColor.CGColor;
|
shadowView.layer.shadowColor = UIColor.blackColor.CGColor;
|
||||||
|
shadowView.layer.shadowPath = [UIBezierPath bezierPathWithOvalInRect:shadowRect].CGPath;
|
||||||
|
|
||||||
UIView *circleView = [[OWSCircleView alloc] initWithDiameter:circleSize];
|
UIView *circleView = [[OWSCircleView alloc] initWithDiameter:circleSize];
|
||||||
self.circleView = circleView;
|
self.circleView = circleView;
|
||||||
@ -83,6 +85,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||||||
circleView.layer.shadowRadius = 12.f;
|
circleView.layer.shadowRadius = 12.f;
|
||||||
circleView.layer.shadowOpacity = 0.3f;
|
circleView.layer.shadowOpacity = 0.3f;
|
||||||
circleView.layer.shadowColor = UIColor.blackColor.CGColor;
|
circleView.layer.shadowColor = UIColor.blackColor.CGColor;
|
||||||
|
circleView.layer.shadowPath = [UIBezierPath bezierPathWithOvalInRect:shadowRect].CGPath;
|
||||||
|
|
||||||
UIView *unreadBadge = [UIView new];
|
UIView *unreadBadge = [UIView new];
|
||||||
self.unreadBadge = unreadBadge;
|
self.unreadBadge = unreadBadge;
|
||||||
|
|||||||
@ -339,13 +339,9 @@ public protocol LinkPreviewViewDraftDelegate {
|
|||||||
|
|
||||||
// MARK: -
|
// MARK: -
|
||||||
|
|
||||||
@objc
|
private class LinkPreviewImageView: UIImageView {
|
||||||
public class LinkPreviewImageView: UIImageView {
|
fileprivate enum Rounding: UInt {
|
||||||
private let maskLayer = CAShapeLayer()
|
case standard
|
||||||
|
|
||||||
@objc
|
|
||||||
public enum Rounding: UInt {
|
|
||||||
case none
|
|
||||||
case asymmetrical
|
case asymmetrical
|
||||||
case circular
|
case circular
|
||||||
}
|
}
|
||||||
@ -353,34 +349,38 @@ public class LinkPreviewImageView: UIImageView {
|
|||||||
private let rounding: Rounding
|
private let rounding: Rounding
|
||||||
fileprivate var isHero = false
|
fileprivate var isHero = false
|
||||||
|
|
||||||
@objc
|
// We only need to use a more complicated corner mask if we're
|
||||||
public init(rounding: Rounding) {
|
// drawing asymmetric corners. This is an exceptional case to match
|
||||||
self.rounding = rounding
|
// the input toolbar curve.
|
||||||
|
private let asymmetricCornerMask = CAShapeLayer()
|
||||||
|
|
||||||
|
init(rounding: Rounding) {
|
||||||
|
self.rounding = rounding
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
self.layer.mask = maskLayer
|
if rounding == .asymmetrical {
|
||||||
|
layer.mask = asymmetricCornerMask
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
self.rounding = .none
|
self.rounding = .standard
|
||||||
|
|
||||||
super.init(coder: aDecoder)
|
super.init(coder: aDecoder)
|
||||||
}
|
}
|
||||||
|
|
||||||
public override var bounds: CGRect {
|
override var bounds: CGRect {
|
||||||
didSet {
|
didSet {
|
||||||
updateMaskLayer()
|
updateMaskLayer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override var frame: CGRect {
|
override var frame: CGRect {
|
||||||
didSet {
|
didSet {
|
||||||
updateMaskLayer()
|
updateMaskLayer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override var center: CGPoint {
|
override var center: CGPoint {
|
||||||
didSet {
|
didSet {
|
||||||
updateMaskLayer()
|
updateMaskLayer()
|
||||||
}
|
}
|
||||||
@ -388,62 +388,62 @@ public class LinkPreviewImageView: UIImageView {
|
|||||||
|
|
||||||
private func updateMaskLayer() {
|
private func updateMaskLayer() {
|
||||||
let layerBounds = self.bounds
|
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 bigRounding: CGFloat = 14
|
||||||
let smallRounding: CGFloat = 6
|
let smallRounding: CGFloat = 6
|
||||||
|
|
||||||
let upperLeftRounding: CGFloat
|
switch rounding {
|
||||||
let upperRightRounding: CGFloat
|
case .standard:
|
||||||
if rounding == .asymmetrical {
|
layer.cornerRadius = smallRounding
|
||||||
upperLeftRounding = CurrentAppContext().isRTL ? smallRounding : bigRounding
|
layer.maskedCorners = isHero ? .top : .all
|
||||||
upperRightRounding = CurrentAppContext().isRTL ? bigRounding : smallRounding
|
case .circular:
|
||||||
} else {
|
layer.cornerRadius = bounds.size.smallerAxis
|
||||||
upperLeftRounding = smallRounding
|
layer.maskedCorners = .all
|
||||||
upperRightRounding = smallRounding
|
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 {
|
public var hasAsymmetricalRounding: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
AssertIsOnMainThread()
|
AssertIsOnMainThread()
|
||||||
|
owsAssertDebug(isDraft)
|
||||||
|
|
||||||
if hasAsymmetricalRounding != oldValue {
|
if hasAsymmetricalRounding != oldValue {
|
||||||
updateContents()
|
updateContents()
|
||||||
@ -1016,13 +1017,7 @@ public class LinkPreviewView: UIStackView {
|
|||||||
owsFailDebug("Could not load image.")
|
owsFailDebug("Could not load image.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let rounding: LinkPreviewImageView.Rounding = {
|
let imageView = LinkPreviewImageView(rounding: roundingParam ?? .standard)
|
||||||
if let roundingParam = roundingParam {
|
|
||||||
return roundingParam
|
|
||||||
}
|
|
||||||
return self.hasAsymmetricalRounding ? .asymmetrical : .none
|
|
||||||
}()
|
|
||||||
let imageView = LinkPreviewImageView(rounding: rounding)
|
|
||||||
imageView.image = image
|
imageView.image = image
|
||||||
imageView.isHero = Self.sentIsHero(state: state)
|
imageView.isHero = Self.sentIsHero(state: state)
|
||||||
return imageView
|
return imageView
|
||||||
@ -1041,7 +1036,7 @@ public class LinkPreviewView: UIStackView {
|
|||||||
owsFailDebug("Could not load image.")
|
owsFailDebug("Could not load image.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let rounding: LinkPreviewImageView.Rounding = hasAsymmetricalRounding ? .asymmetrical : .none
|
let rounding: LinkPreviewImageView.Rounding = hasAsymmetricalRounding ? .asymmetrical : .standard
|
||||||
let imageView = LinkPreviewImageView(rounding: rounding)
|
let imageView = LinkPreviewImageView(rounding: rounding)
|
||||||
imageView.image = image
|
imageView.image = image
|
||||||
return imageView
|
return imageView
|
||||||
@ -1268,29 +1263,6 @@ public class LinkPreviewView: UIStackView {
|
|||||||
return CGSize(width: width, height: height)
|
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) {
|
@objc func didTapCancel(sender: UIButton) {
|
||||||
self.draftDelegate?.linkPreviewDidCancel()
|
self.draftDelegate?.linkPreviewDidCancel()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
#import "OWSBubbleShapeView.h"
|
||||||
@ -255,9 +255,6 @@ typedef NS_ENUM(NSUInteger, OWSBubbleShapeViewMode) {
|
|||||||
self.layer.mask = self.maskLayer;
|
self.layer.mask = self.maskLayer;
|
||||||
break;
|
break;
|
||||||
case OWSBubbleShapeViewMode_InnerShadow: {
|
case OWSBubbleShapeViewMode_InnerShadow: {
|
||||||
self.maskLayer.path = bezierPath.CGPath;
|
|
||||||
self.layer.mask = self.maskLayer;
|
|
||||||
|
|
||||||
// Inner shadow.
|
// Inner shadow.
|
||||||
// This should usually not be visible; it is used to distinguish
|
// This should usually not be visible; it is used to distinguish
|
||||||
// profile pics from the background if they are similar.
|
// profile pics from the background if they are similar.
|
||||||
@ -267,16 +264,21 @@ typedef NS_ENUM(NSUInteger, OWSBubbleShapeViewMode) {
|
|||||||
UIBezierPath *shadowPath = [bezierPath copy];
|
UIBezierPath *shadowPath = [bezierPath copy];
|
||||||
// This can be any value large enough to cast a sufficiently large shadow.
|
// This can be any value large enough to cast a sufficiently large shadow.
|
||||||
CGFloat shadowInset = -(self.innerShadowRadius * 4.f);
|
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.
|
// This can be any color since the fill should be clipped.
|
||||||
self.shapeLayer.fillColor = UIColor.blackColor.CGColor;
|
self.shapeLayer.fillColor = UIColor.blackColor.CGColor;
|
||||||
self.shapeLayer.path = shadowPath.CGPath;
|
self.shapeLayer.path = shadowPath.CGPath;
|
||||||
self.shapeLayer.fillRule = kCAFillRuleEvenOdd;
|
|
||||||
self.shapeLayer.shadowColor = self.innerShadowColor.CGColor;
|
self.shapeLayer.shadowColor = self.innerShadowColor.CGColor;
|
||||||
self.shapeLayer.shadowRadius = self.innerShadowRadius;
|
self.shapeLayer.shadowRadius = self.innerShadowRadius;
|
||||||
self.shapeLayer.shadowOpacity = self.innerShadowOpacity;
|
self.shapeLayer.shadowOpacity = self.innerShadowOpacity;
|
||||||
self.shapeLayer.shadowOffset = CGSizeZero;
|
self.shapeLayer.shadowOffset = CGSizeZero;
|
||||||
|
self.shapeLayer.shadowPath = shadowPath.CGPath;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
@ -12,7 +12,9 @@ typedef NS_OPTIONS(NSUInteger, OWSDirectionalRectCorner) {
|
|||||||
OWSDirectionalRectCornerTopTrailing = 1 << 1,
|
OWSDirectionalRectCornerTopTrailing = 1 << 1,
|
||||||
OWSDirectionalRectCornerBottomLeading = 1 << 2,
|
OWSDirectionalRectCornerBottomLeading = 1 << 2,
|
||||||
OWSDirectionalRectCornerBottomTrailing = 1 << 3,
|
OWSDirectionalRectCornerBottomTrailing = 1 << 3,
|
||||||
OWSDirectionalRectCornerAllCorners = ~0UL
|
|
||||||
|
OWSDirectionalRectCornerAllCorners = OWSDirectionalRectCornerTopLeading | OWSDirectionalRectCornerTopTrailing
|
||||||
|
| OWSDirectionalRectCornerBottomLeading | OWSDirectionalRectCornerBottomTrailing
|
||||||
};
|
};
|
||||||
|
|
||||||
@class OWSBubbleView;
|
@class OWSBubbleView;
|
||||||
|
|||||||
@ -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"
|
#import "OWSBubbleView.h"
|
||||||
@ -73,8 +73,8 @@ const CGFloat kOWSMessageCellCornerRadius_Small = 4;
|
|||||||
[self.layer addSublayer:self.gradientLayer];
|
[self.layer addSublayer:self.gradientLayer];
|
||||||
self.gradientLayer.hidden = YES;
|
self.gradientLayer.hidden = YES;
|
||||||
|
|
||||||
|
self.layer.masksToBounds = YES;
|
||||||
self.maskLayer = [CAShapeLayer new];
|
self.maskLayer = [CAShapeLayer new];
|
||||||
self.layer.mask = self.maskLayer;
|
|
||||||
|
|
||||||
_partnerViews = [NSMutableArray new];
|
_partnerViews = [NSMutableArray new];
|
||||||
|
|
||||||
@ -211,6 +211,24 @@ const CGFloat kOWSMessageCellCornerRadius_Small = 4;
|
|||||||
|
|
||||||
self.maskLayer.path = bezierPath.CGPath;
|
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];
|
[CATransaction commit];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@ -117,6 +117,7 @@ open class TooltipView: UIView {
|
|||||||
bezierPath.addLine(to: tailPoint)
|
bezierPath.addLine(to: tailPoint)
|
||||||
|
|
||||||
shapeLayer.path = bezierPath.cgPath
|
shapeLayer.path = bezierPath.cgPath
|
||||||
|
shapeLayer.shadowPath = bezierPath.cgPath
|
||||||
shapeLayer.frame = view.bounds
|
shapeLayer.frame = view.bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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: -
|
// MARK: -
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user