mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1530 lines
72 KiB
Swift
1530 lines
72 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SwiftSignalKit
|
|
import HierarchyTrackingLayer
|
|
import ShimmerEffect
|
|
import ManagedAnimationNode
|
|
|
|
private func generateIndefiniteActivityIndicatorImage(color: UIColor, diameter: CGFloat = 22.0, lineWidth: CGFloat = 2.0) -> UIImage? {
|
|
return generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
context.setStrokeColor(color.cgColor)
|
|
context.setLineWidth(lineWidth)
|
|
context.setLineCap(.round)
|
|
let cutoutAngle: CGFloat = CGFloat.pi * 60.0 / 180.0
|
|
context.addArc(center: CGPoint(x: size.width / 2.0, y: size.height / 2.0), radius: size.width / 2.0 - lineWidth / 2.0, startAngle: 0.0, endAngle: CGFloat.pi * 2.0 - cutoutAngle, clockwise: false)
|
|
context.strokePath()
|
|
})
|
|
}
|
|
|
|
public final class SolidRoundedButtonTheme: Equatable {
|
|
public let backgroundColor: UIColor
|
|
public let backgroundColors: [UIColor]
|
|
public let foregroundColor: UIColor
|
|
public let disabledBackgroundColor: UIColor?
|
|
public let disabledForegroundColor: UIColor?
|
|
|
|
public init(backgroundColor: UIColor, backgroundColors: [UIColor] = [], foregroundColor: UIColor, disabledBackgroundColor: UIColor? = nil, disabledForegroundColor: UIColor? = nil) {
|
|
self.backgroundColor = backgroundColor
|
|
self.backgroundColors = backgroundColors
|
|
self.foregroundColor = foregroundColor
|
|
self.disabledBackgroundColor = disabledBackgroundColor
|
|
self.disabledForegroundColor = disabledForegroundColor
|
|
}
|
|
|
|
public static func ==(lhs: SolidRoundedButtonTheme, rhs: SolidRoundedButtonTheme) -> Bool {
|
|
if lhs.backgroundColor != rhs.backgroundColor {
|
|
return false
|
|
}
|
|
if lhs.backgroundColors != rhs.backgroundColors {
|
|
return false
|
|
}
|
|
if lhs.foregroundColor != rhs.foregroundColor {
|
|
return false
|
|
}
|
|
if lhs.disabledBackgroundColor != rhs.disabledBackgroundColor {
|
|
return false
|
|
}
|
|
if lhs.disabledForegroundColor != rhs.disabledForegroundColor {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
public enum SolidRoundedButtonFont {
|
|
case bold
|
|
case regular
|
|
}
|
|
|
|
public enum SolidRoundedButtonIconPosition {
|
|
case left
|
|
case right
|
|
}
|
|
|
|
public enum SolidRoundedButtonProgressType {
|
|
case fullSize
|
|
case embedded
|
|
}
|
|
|
|
private final class BadgeNode: ASDisplayNode {
|
|
private var fillColor: UIColor
|
|
private var strokeColor: UIColor
|
|
private var textColor: UIColor
|
|
|
|
private let textNode: ImmediateTextNode
|
|
private let backgroundNode: ASImageNode
|
|
|
|
private let font: UIFont = Font.with(size: 15.0, design: .round, weight: .bold)
|
|
|
|
var text: String = "" {
|
|
didSet {
|
|
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
|
|
self.invalidateCalculatedLayout()
|
|
}
|
|
}
|
|
|
|
init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) {
|
|
self.fillColor = fillColor
|
|
self.strokeColor = strokeColor
|
|
self.textColor = textColor
|
|
|
|
self.textNode = ImmediateTextNode()
|
|
self.textNode.isUserInteractionEnabled = false
|
|
self.textNode.displaysAsynchronously = false
|
|
|
|
self.backgroundNode = ASImageNode()
|
|
self.backgroundNode.isLayerBacked = true
|
|
self.backgroundNode.displayWithoutProcessing = true
|
|
self.backgroundNode.displaysAsynchronously = false
|
|
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: nil, strokeWidth: 1.0)
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.backgroundNode)
|
|
self.addSubnode(self.textNode)
|
|
|
|
self.isUserInteractionEnabled = false
|
|
}
|
|
|
|
func updateTheme(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) {
|
|
self.fillColor = fillColor
|
|
self.strokeColor = strokeColor
|
|
self.textColor = textColor
|
|
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0)
|
|
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
|
|
}
|
|
|
|
func animateBump(incremented: Bool) {
|
|
if incremented {
|
|
let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
|
|
firstTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.2)
|
|
firstTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.2, completion: { finished in
|
|
if finished {
|
|
let secondTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
|
|
secondTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.0)
|
|
secondTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.0)
|
|
}
|
|
})
|
|
} else {
|
|
let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
|
|
firstTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 0.8)
|
|
firstTransition.updateTransformScale(layer: self.textNode.layer, scale: 0.8, completion: { finished in
|
|
if finished {
|
|
let secondTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut)
|
|
secondTransition.updateTransformScale(layer: self.backgroundNode.layer, scale: 1.0)
|
|
secondTransition.updateTransformScale(layer: self.textNode.layer, scale: 1.0)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func animateOut() {
|
|
let timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue
|
|
self.backgroundNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil)
|
|
self.textNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil)
|
|
}
|
|
|
|
func update(_ constrainedSize: CGSize) -> CGSize {
|
|
let badgeSize = self.textNode.updateLayout(constrainedSize)
|
|
let backgroundSize = CGSize(width: max(18.0, badgeSize.width + 8.0), height: 18.0)
|
|
let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize)
|
|
self.backgroundNode.frame = backgroundFrame
|
|
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: floorToScreenPixels((backgroundFrame.size.height - badgeSize.height) / 2.0) - UIScreenPixel), size: badgeSize)
|
|
|
|
return backgroundSize
|
|
}
|
|
}
|
|
|
|
public final class SolidRoundedButtonNode: ASDisplayNode {
|
|
private var theme: SolidRoundedButtonTheme
|
|
private var fontSize: CGFloat
|
|
private let gloss: Bool
|
|
|
|
public let buttonBackgroundNode: ASImageNode
|
|
private var buttonBackgroundAnimationView: UIImageView?
|
|
|
|
private var shimmerView: ShimmerEffectForegroundView?
|
|
private var borderView: UIView?
|
|
private var borderMaskView: UIView?
|
|
private var borderShimmerView: ShimmerEffectForegroundView?
|
|
|
|
private let buttonNode: HighlightTrackingButtonNode
|
|
public let titleNode: ImmediateTextNode
|
|
private let subtitleNode: ImmediateTextNode
|
|
private let iconNode: ASImageNode
|
|
private var animationNode: SimpleAnimationNode?
|
|
private var progressNode: ASImageNode?
|
|
private var badgeNode: BadgeNode?
|
|
|
|
private let buttonHeight: CGFloat
|
|
public let buttonCornerRadius: CGFloat
|
|
|
|
public var pressed: (() -> Void)?
|
|
public var validLayout: CGFloat?
|
|
|
|
public var isEnabled: Bool = true {
|
|
didSet {
|
|
if self.isEnabled != oldValue {
|
|
self.updateColors(animated: true)
|
|
}
|
|
self.updateAccessibilityLabels()
|
|
}
|
|
}
|
|
|
|
public var title: String? {
|
|
didSet {
|
|
self.updateAccessibilityLabels()
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
public var font: SolidRoundedButtonFont {
|
|
didSet {
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
public var subtitle: String? {
|
|
didSet {
|
|
self.updateAccessibilityLabels()
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, previousSubtitle: oldValue, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
public var badge: String? {
|
|
didSet {
|
|
self.updateAccessibilityLabels()
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
public var icon: UIImage? {
|
|
didSet {
|
|
self.iconNode.image = generateTintedImage(image: self.icon, color: self.theme.foregroundColor)
|
|
}
|
|
}
|
|
|
|
private func updateAccessibilityLabels() {
|
|
self.accessibilityLabel = (self.title ?? "") + " " + (self.subtitle ?? "")
|
|
if !self.isEnabled {
|
|
self.accessibilityTraits = [.button, .notEnabled]
|
|
} else {
|
|
self.accessibilityTraits = [.button]
|
|
}
|
|
}
|
|
|
|
private var animationTimer: SwiftSignalKit.Timer?
|
|
public var animation: String? {
|
|
didSet {
|
|
if let animation = self.animation {
|
|
if animation != oldValue {
|
|
self.animationNode?.removeFromSupernode()
|
|
self.animationNode = nil
|
|
|
|
let animationNode = SimpleAnimationNode(animationName: animation, size: CGSize(width: 30.0, height: 30.0))
|
|
animationNode.customColor = self.theme.foregroundColor
|
|
animationNode.isUserInteractionEnabled = false
|
|
self.addSubnode(animationNode)
|
|
self.animationNode = animationNode
|
|
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
|
|
if self.gloss {
|
|
self.animationTimer?.invalidate()
|
|
|
|
Queue.mainQueue().after(1.25) {
|
|
self.animationNode?.play()
|
|
|
|
let timer = SwiftSignalKit.Timer(timeout: 4.0, repeat: true, completion: { [weak self] in
|
|
self?.animationNode?.play()
|
|
}, queue: Queue.mainQueue())
|
|
self.animationTimer = timer
|
|
timer.start()
|
|
}
|
|
} else {
|
|
self.animationNode?.play()
|
|
let timer = SwiftSignalKit.Timer(timeout: 4.0, repeat: true, completion: { [weak self] in
|
|
self?.animationNode?.play()
|
|
}, queue: Queue.mainQueue())
|
|
self.animationTimer = timer
|
|
timer.start()
|
|
}
|
|
}
|
|
} else if let animationNode = self.animationNode {
|
|
animationNode.removeFromSupernode()
|
|
self.animationNode = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
public var iconSpacing: CGFloat = 8.0 {
|
|
didSet {
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
public var animationSize: CGSize = CGSize(width: 30.0, height: 30.0) {
|
|
didSet {
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
public var iconPosition: SolidRoundedButtonIconPosition = .left {
|
|
didSet {
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
public var progressType: SolidRoundedButtonProgressType = .fullSize
|
|
|
|
public var highlightEnabled = true {
|
|
didSet {
|
|
if !self.highlightEnabled {
|
|
self.buttonBackgroundNode.alpha = 1.0
|
|
self.titleNode.alpha = 1.0
|
|
self.subtitleNode.alpha = 1.0
|
|
self.iconNode.alpha = 1.0
|
|
self.animationNode?.alpha = 1.0
|
|
self.buttonBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
|
self.titleNode.layer.removeAnimation(forKey: "opacity")
|
|
self.subtitleNode.layer.removeAnimation(forKey: "opacity")
|
|
self.iconNode.layer.removeAnimation(forKey: "opacity")
|
|
self.animationNode?.layer.removeAnimation(forKey: "opacity")
|
|
}
|
|
}
|
|
}
|
|
|
|
public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) {
|
|
self.theme = theme
|
|
self.font = font
|
|
self.fontSize = fontSize
|
|
self.buttonHeight = height
|
|
self.buttonCornerRadius = cornerRadius
|
|
self.title = title
|
|
self.gloss = gloss
|
|
|
|
self.buttonBackgroundNode = ASImageNode()
|
|
self.buttonBackgroundNode.displaysAsynchronously = false
|
|
self.buttonBackgroundNode.clipsToBounds = true
|
|
|
|
self.buttonBackgroundNode.backgroundColor = theme.backgroundColor
|
|
if theme.backgroundColors.count > 1 {
|
|
self.buttonBackgroundNode.backgroundColor = nil
|
|
|
|
var locations: [CGFloat] = []
|
|
let delta = 1.0 / CGFloat(theme.backgroundColors.count - 1)
|
|
for i in 0 ..< theme.backgroundColors.count {
|
|
locations.append(delta * CGFloat(i))
|
|
}
|
|
self.buttonBackgroundNode.image = generateGradientImage(size: CGSize(width: 200.0, height: height), colors: theme.backgroundColors, locations: locations, direction: .horizontal)
|
|
|
|
let buttonBackgroundAnimationView = UIImageView()
|
|
buttonBackgroundAnimationView.image = generateGradientImage(size: CGSize(width: 200.0, height: height), colors: theme.backgroundColors, locations: locations, direction: .horizontal)
|
|
self.buttonBackgroundAnimationView = buttonBackgroundAnimationView
|
|
}
|
|
|
|
self.buttonBackgroundNode.cornerRadius = cornerRadius
|
|
|
|
self.buttonNode = HighlightTrackingButtonNode()
|
|
|
|
self.titleNode = ImmediateTextNode()
|
|
self.titleNode.isUserInteractionEnabled = false
|
|
self.titleNode.displaysAsynchronously = false
|
|
|
|
self.subtitleNode = ImmediateTextNode()
|
|
self.subtitleNode.isUserInteractionEnabled = false
|
|
self.subtitleNode.displaysAsynchronously = false
|
|
|
|
self.iconNode = ASImageNode()
|
|
self.iconNode.displaysAsynchronously = false
|
|
self.iconNode.image = generateTintedImage(image: icon, color: self.theme.foregroundColor)
|
|
|
|
super.init()
|
|
|
|
self.isAccessibilityElement = true
|
|
|
|
self.addSubnode(self.buttonBackgroundNode)
|
|
self.addSubnode(self.buttonNode)
|
|
self.addSubnode(self.titleNode)
|
|
self.addSubnode(self.subtitleNode)
|
|
self.addSubnode(self.iconNode)
|
|
|
|
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
|
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self, strongSelf.isEnabled && strongSelf.highlightEnabled {
|
|
if highlighted {
|
|
strongSelf.buttonBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.buttonBackgroundNode.alpha = 0.55
|
|
strongSelf.titleNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.titleNode.alpha = 0.55
|
|
strongSelf.subtitleNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.subtitleNode.alpha = 0.55
|
|
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.iconNode.alpha = 0.55
|
|
strongSelf.animationNode?.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.animationNode?.alpha = 0.55
|
|
} else {
|
|
if strongSelf.buttonBackgroundNode.alpha > 0.0 {
|
|
strongSelf.buttonBackgroundNode.alpha = 1.0
|
|
strongSelf.buttonBackgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
|
strongSelf.titleNode.alpha = 1.0
|
|
strongSelf.titleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
|
strongSelf.subtitleNode.alpha = 1.0
|
|
strongSelf.subtitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
|
strongSelf.iconNode.alpha = 1.0
|
|
strongSelf.iconNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
|
strongSelf.animationNode?.alpha = 1.0
|
|
strongSelf.animationNode?.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.updateAccessibilityLabels()
|
|
}
|
|
|
|
public override func didLoad() {
|
|
super.didLoad()
|
|
|
|
if #available(iOS 13.0, *) {
|
|
self.buttonBackgroundNode.layer.cornerCurve = .continuous
|
|
}
|
|
|
|
if let buttonBackgroundAnimationView = self.buttonBackgroundAnimationView {
|
|
self.buttonBackgroundNode.view.addSubview(buttonBackgroundAnimationView)
|
|
}
|
|
|
|
self.setupGloss()
|
|
}
|
|
|
|
private func setupGloss() {
|
|
if self.gloss {
|
|
if self.shimmerView == nil {
|
|
let shimmerView = ShimmerEffectForegroundView()
|
|
self.shimmerView = shimmerView
|
|
|
|
if #available(iOS 13.0, *) {
|
|
shimmerView.layer.cornerCurve = .continuous
|
|
shimmerView.layer.cornerRadius = self.buttonCornerRadius
|
|
}
|
|
|
|
let borderView = UIView()
|
|
borderView.isUserInteractionEnabled = false
|
|
self.borderView = borderView
|
|
|
|
let borderMaskView = UIView()
|
|
borderMaskView.layer.borderWidth = 1.0 + UIScreenPixel
|
|
borderMaskView.layer.borderColor = UIColor.white.cgColor
|
|
borderMaskView.layer.cornerRadius = self.buttonCornerRadius
|
|
borderView.mask = borderMaskView
|
|
self.borderMaskView = borderMaskView
|
|
|
|
let borderShimmerView = ShimmerEffectForegroundView()
|
|
self.borderShimmerView = borderShimmerView
|
|
borderView.addSubview(borderShimmerView)
|
|
|
|
self.view.insertSubview(shimmerView, belowSubview: self.buttonNode.view)
|
|
self.view.insertSubview(borderView, belowSubview: self.buttonNode.view)
|
|
|
|
self.updateShimmerParameters()
|
|
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
}
|
|
} else if self.shimmerView != nil {
|
|
self.shimmerView?.removeFromSuperview()
|
|
self.borderView?.removeFromSuperview()
|
|
self.borderMaskView?.removeFromSuperview()
|
|
self.borderShimmerView?.removeFromSuperview()
|
|
|
|
self.shimmerView = nil
|
|
self.borderView = nil
|
|
self.borderMaskView = nil
|
|
self.borderShimmerView = nil
|
|
}
|
|
}
|
|
|
|
public func animateTitle(to title: String) {
|
|
Queue.mainQueue().justDispatch {
|
|
if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) {
|
|
snapshotView.frame = self.bounds
|
|
|
|
self.view.addSubview(snapshotView)
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
|
snapshotView?.removeFromSuperview()
|
|
})
|
|
}
|
|
|
|
self.title = title
|
|
self.buttonBackgroundNode.backgroundColor = self.theme.disabledBackgroundColor
|
|
self.isEnabled = false
|
|
}
|
|
}
|
|
|
|
private func setupGradientAnimations() {
|
|
guard let buttonBackgroundAnimationView = self.buttonBackgroundAnimationView else {
|
|
return
|
|
}
|
|
|
|
if let _ = buttonBackgroundAnimationView.layer.animation(forKey: "movement") {
|
|
} else {
|
|
let offset = (buttonBackgroundAnimationView.frame.width - self.frame.width) / 2.0
|
|
let previousValue = buttonBackgroundAnimationView.center.x
|
|
var newValue: CGFloat = offset
|
|
if offset - previousValue < buttonBackgroundAnimationView.frame.width * 0.25 {
|
|
newValue -= buttonBackgroundAnimationView.frame.width * 0.35
|
|
}
|
|
buttonBackgroundAnimationView.center = CGPoint(x: newValue, y: buttonBackgroundAnimationView.bounds.size.height / 2.0)
|
|
|
|
CATransaction.begin()
|
|
|
|
let animation = CABasicAnimation(keyPath: "position.x")
|
|
animation.duration = 4.5
|
|
animation.fromValue = previousValue
|
|
animation.toValue = newValue
|
|
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
|
|
CATransaction.setCompletionBlock { [weak self] in
|
|
// if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy {
|
|
self?.setupGradientAnimations()
|
|
// }
|
|
}
|
|
|
|
buttonBackgroundAnimationView.layer.add(animation, forKey: "movement")
|
|
CATransaction.commit()
|
|
}
|
|
}
|
|
|
|
func updateShimmerParameters() {
|
|
guard let shimmerView = self.shimmerView, let borderShimmerView = self.borderShimmerView else {
|
|
return
|
|
}
|
|
|
|
let color = self.theme.foregroundColor
|
|
let alpha: CGFloat
|
|
let borderAlpha: CGFloat
|
|
let compositingFilter: String?
|
|
if color.lightness > 0.5 {
|
|
alpha = 0.5
|
|
borderAlpha = 0.75
|
|
compositingFilter = "overlayBlendMode"
|
|
} else {
|
|
alpha = 0.2
|
|
borderAlpha = 0.3
|
|
compositingFilter = nil
|
|
}
|
|
|
|
shimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(alpha), gradientSize: 70.0, globalTimeOffset: false, duration: 4.0, horizontal: true)
|
|
borderShimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(borderAlpha), gradientSize: 70.0, globalTimeOffset: false, duration: 4.0, horizontal: true)
|
|
|
|
shimmerView.layer.compositingFilter = compositingFilter
|
|
borderShimmerView.layer.compositingFilter = compositingFilter
|
|
}
|
|
|
|
public func updateTheme(_ theme: SolidRoundedButtonTheme, animated: Bool = false) {
|
|
guard theme !== self.theme else {
|
|
return
|
|
}
|
|
let previousTheme = self.theme
|
|
self.theme = theme
|
|
|
|
let animateTransition = previousTheme.backgroundColors.count != theme.backgroundColors.count
|
|
|
|
if animated && animateTransition {
|
|
if let snapshotView = self.buttonBackgroundNode.view.snapshotView(afterScreenUpdates: false) {
|
|
self.view.insertSubview(snapshotView, aboveSubview: self.buttonBackgroundNode.view)
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
|
snapshotView?.removeFromSuperview()
|
|
})
|
|
}
|
|
|
|
if let snapshotView = self.titleNode.view.snapshotView(afterScreenUpdates: false) {
|
|
snapshotView.frame = self.titleNode.frame
|
|
|
|
self.view.insertSubview(snapshotView, aboveSubview: self.titleNode.view)
|
|
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 15.0), duration: 0.2, removeOnCompletion: false, additive: true)
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
|
snapshotView?.removeFromSuperview()
|
|
})
|
|
self.titleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -15.0), to: CGPoint(), duration: 0.2, additive: true)
|
|
self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
if let snapshotView = self.iconNode.view.snapshotView(afterScreenUpdates: false) {
|
|
snapshotView.frame = self.iconNode.frame
|
|
|
|
self.view.insertSubview(snapshotView, aboveSubview: self.iconNode.view)
|
|
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 15.0), duration: 0.2, removeOnCompletion: false, additive: true)
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
|
snapshotView?.removeFromSuperview()
|
|
})
|
|
self.iconNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -15.0), to: CGPoint(), duration: 0.2, additive: true)
|
|
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
if let animationNode = self.animationNode, let snapshotView = animationNode.view.snapshotView(afterScreenUpdates: false) {
|
|
snapshotView.frame = animationNode.frame
|
|
|
|
self.view.insertSubview(snapshotView, aboveSubview: animationNode.view)
|
|
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 15.0), duration: 0.2, removeOnCompletion: false, additive: true)
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
|
snapshotView?.removeFromSuperview()
|
|
})
|
|
animationNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -15.0), to: CGPoint(), duration: 0.2, additive: true)
|
|
animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
|
|
self.iconNode.image = generateTintedImage(image: self.iconNode.image, color: theme.foregroundColor)
|
|
self.animationNode?.customColor = theme.foregroundColor
|
|
|
|
self.updateColors(animated: false)
|
|
|
|
self.updateShimmerParameters()
|
|
}
|
|
|
|
func updateColors(animated: Bool) {
|
|
if animated {
|
|
if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) {
|
|
self.view.addSubview(snapshotView)
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
|
snapshotView?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
if !self.isEnabled, let disabledBackgroundColor = self.theme.disabledBackgroundColor, let disabledForegroundColor = self.theme.disabledForegroundColor {
|
|
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: disabledForegroundColor)
|
|
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: disabledForegroundColor)
|
|
|
|
self.buttonBackgroundNode.backgroundColor = disabledBackgroundColor
|
|
} else {
|
|
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: self.theme.foregroundColor)
|
|
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: self.theme.foregroundColor)
|
|
|
|
self.buttonBackgroundNode.backgroundColor = theme.backgroundColor
|
|
if theme.backgroundColors.count > 1 {
|
|
self.buttonBackgroundNode.backgroundColor = nil
|
|
|
|
var locations: [CGFloat] = []
|
|
let delta = 1.0 / CGFloat(theme.backgroundColors.count - 1)
|
|
for i in 0 ..< theme.backgroundColors.count {
|
|
locations.append(delta * CGFloat(i))
|
|
}
|
|
self.buttonBackgroundNode.image = generateGradientImage(size: CGSize(width: 200.0, height: self.buttonHeight), colors: theme.backgroundColors, locations: locations, direction: .horizontal)
|
|
|
|
if self.buttonBackgroundAnimationView == nil {
|
|
let buttonBackgroundAnimationView = UIImageView()
|
|
self.buttonBackgroundAnimationView = buttonBackgroundAnimationView
|
|
self.buttonBackgroundNode.view.addSubview(buttonBackgroundAnimationView)
|
|
}
|
|
|
|
self.buttonBackgroundAnimationView?.image = self.buttonBackgroundNode.image
|
|
} else {
|
|
self.buttonBackgroundNode.image = nil
|
|
self.buttonBackgroundAnimationView?.image = nil
|
|
}
|
|
}
|
|
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
public func sizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
|
let titleSize = self.titleNode.updateLayout(constrainedSize)
|
|
return CGSize(width: titleSize.width + 20.0, height: self.buttonHeight)
|
|
}
|
|
|
|
public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
|
return self.updateLayout(width: width, previousSubtitle: self.subtitle, transition: transition)
|
|
}
|
|
|
|
private func updateLayout(width: CGFloat, previousSubtitle: String?, transition: ContainedViewLayoutTransition) -> CGFloat {
|
|
self.validLayout = width
|
|
|
|
let buttonSize = CGSize(width: width, height: self.buttonHeight)
|
|
let buttonFrame = CGRect(origin: CGPoint(), size: buttonSize)
|
|
transition.updateFrame(node: self.buttonBackgroundNode, frame: buttonFrame)
|
|
|
|
if let shimmerView = self.shimmerView, let borderView = self.borderView, let borderMaskView = self.borderMaskView, let borderShimmerView = self.borderShimmerView {
|
|
transition.updateFrame(view: shimmerView, frame: buttonFrame)
|
|
transition.updateFrame(view: borderView, frame: buttonFrame)
|
|
transition.updateFrame(view: borderMaskView, frame: buttonFrame)
|
|
transition.updateFrame(view: borderShimmerView, frame: buttonFrame)
|
|
|
|
shimmerView.updateAbsoluteRect(CGRect(origin: CGPoint(x: width * 4.0, y: 0.0), size: buttonSize), within: CGSize(width: width * 9.0, height: buttonHeight))
|
|
borderShimmerView.updateAbsoluteRect(CGRect(origin: CGPoint(x: width * 4.0, y: 0.0), size: buttonSize), within: CGSize(width: width * 9.0, height: buttonHeight))
|
|
}
|
|
|
|
if let buttonBackgroundAnimationView = self.buttonBackgroundAnimationView {
|
|
buttonBackgroundAnimationView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width * 2.4, height: buttonSize.height))
|
|
if buttonBackgroundAnimationView.layer.animation(forKey: "movement") == nil {
|
|
buttonBackgroundAnimationView.center = CGPoint(x: buttonSize.width * 2.4 / 2.0 - buttonBackgroundAnimationView.frame.width * 0.35, y: buttonSize.height / 2.0)
|
|
}
|
|
self.setupGradientAnimations()
|
|
}
|
|
|
|
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
|
|
|
|
if self.title != self.titleNode.attributedText?.string {
|
|
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: self.isEnabled ? self.theme.foregroundColor : (self.theme.disabledForegroundColor ?? self.theme.foregroundColor))
|
|
}
|
|
|
|
let iconSize: CGSize
|
|
if let _ = self.animationNode {
|
|
iconSize = self.animationSize
|
|
} else {
|
|
iconSize = self.iconNode.image?.size ?? CGSize()
|
|
}
|
|
let titleSize = self.titleNode.updateLayout(buttonSize)
|
|
|
|
let spacingOffset: CGFloat = 9.0
|
|
let verticalInset: CGFloat = self.subtitle == nil ? floor((buttonFrame.height - titleSize.height) / 2.0) : floor((buttonFrame.height - titleSize.height) / 2.0) - spacingOffset
|
|
|
|
let iconSpacing: CGFloat = self.iconSpacing
|
|
|
|
var contentWidth: CGFloat = titleSize.width
|
|
if !iconSize.width.isZero {
|
|
contentWidth += iconSize.width + iconSpacing
|
|
}
|
|
var nextContentOrigin = floor((buttonFrame.width - contentWidth) / 2.0)
|
|
|
|
let iconFrame: CGRect
|
|
let titleFrame: CGRect
|
|
switch self.iconPosition {
|
|
case .left:
|
|
iconFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize)
|
|
if !iconSize.width.isZero {
|
|
nextContentOrigin += iconSize.width + iconSpacing
|
|
}
|
|
titleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + verticalInset), size: titleSize)
|
|
case .right:
|
|
titleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + verticalInset), size: titleSize)
|
|
if !iconSize.width.isZero {
|
|
nextContentOrigin += titleFrame.width + iconSpacing
|
|
}
|
|
iconFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize)
|
|
}
|
|
|
|
transition.updateFrame(node: self.iconNode, frame: iconFrame)
|
|
if let animationNode = self.animationNode {
|
|
transition.updateFrame(node: animationNode, frame: iconFrame)
|
|
}
|
|
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
|
|
|
if let badge = self.badge {
|
|
let badgeNode: BadgeNode
|
|
if let current = self.badgeNode {
|
|
badgeNode = current
|
|
} else {
|
|
badgeNode = BadgeNode(fillColor: self.theme.foregroundColor, strokeColor: .clear, textColor: self.theme.backgroundColor)
|
|
self.badgeNode = badgeNode
|
|
self.addSubnode(badgeNode)
|
|
}
|
|
badgeNode.text = badge
|
|
let badgeSize = badgeNode.update(CGSize(width: 100.0, height: 100.0))
|
|
transition.updateFrame(node: badgeNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floor((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize))
|
|
} else if let badgeNode = self.badgeNode {
|
|
self.badgeNode = nil
|
|
badgeNode.removeFromSupernode()
|
|
}
|
|
|
|
if self.subtitle != self.subtitleNode.attributedText?.string {
|
|
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: self.theme.foregroundColor)
|
|
}
|
|
|
|
let subtitleSize = self.subtitleNode.updateLayout(buttonSize)
|
|
let subtitleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - subtitleSize.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - titleSize.height) / 2.0) + spacingOffset + 2.0), size: subtitleSize)
|
|
transition.updateFrame(node: self.subtitleNode, frame: subtitleFrame)
|
|
|
|
if previousSubtitle == nil && self.subtitle != nil {
|
|
self.titleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: spacingOffset / 2.0), to: CGPoint(), duration: 0.3, additive: true)
|
|
self.subtitleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -spacingOffset / 2.0), to: CGPoint(), duration: 0.3, additive: true)
|
|
self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
}
|
|
|
|
return buttonSize.height
|
|
}
|
|
|
|
@objc private func buttonPressed() {
|
|
if self.isEnabled {
|
|
self.pressed?()
|
|
}
|
|
}
|
|
|
|
public func transitionToProgress() {
|
|
guard self.progressNode == nil else {
|
|
return
|
|
}
|
|
|
|
self.isUserInteractionEnabled = false
|
|
|
|
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
|
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
|
rotationAnimation.duration = 1.0
|
|
rotationAnimation.fromValue = NSNumber(value: Float(0.0))
|
|
rotationAnimation.toValue = NSNumber(value: Float.pi * 2.0)
|
|
rotationAnimation.repeatCount = Float.infinity
|
|
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
|
rotationAnimation.beginTime = 1.0
|
|
|
|
let buttonOffset = self.buttonBackgroundNode.frame.minX
|
|
let buttonWidth = self.buttonBackgroundNode.frame.width
|
|
|
|
let progressNode = ASImageNode()
|
|
|
|
switch self.progressType {
|
|
case .fullSize:
|
|
let progressFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(buttonOffset + (buttonWidth - self.buttonHeight) / 2.0), y: 0.0), size: CGSize(width: self.buttonHeight, height: self.buttonHeight))
|
|
progressNode.frame = progressFrame
|
|
progressNode.image = generateIndefiniteActivityIndicatorImage(color: self.buttonBackgroundNode.backgroundColor ?? .clear, diameter: self.buttonHeight, lineWidth: 2.0 + UIScreenPixel)
|
|
|
|
self.buttonBackgroundNode.layer.cornerRadius = self.buttonHeight / 2.0
|
|
self.buttonBackgroundNode.layer.animate(from: self.buttonCornerRadius as NSNumber, to: self.buttonHeight / 2.0 as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
|
self.buttonBackgroundNode.layer.animateFrame(from: self.buttonBackgroundNode.frame, to: progressFrame, duration: 0.2)
|
|
|
|
self.buttonBackgroundNode.alpha = 0.0
|
|
self.buttonBackgroundNode.layer.animateAlpha(from: 0.55, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
|
|
self.insertSubnode(progressNode, at: 0)
|
|
case .embedded:
|
|
let diameter: CGFloat = self.buttonHeight - 22.0
|
|
let progressFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(buttonOffset + (buttonWidth - diameter) / 2.0), y: floorToScreenPixels((self.buttonHeight - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))
|
|
progressNode.frame = progressFrame
|
|
progressNode.image = generateIndefiniteActivityIndicatorImage(color: self.theme.foregroundColor, diameter: diameter, lineWidth: 3.0)
|
|
|
|
self.addSubnode(progressNode)
|
|
}
|
|
|
|
progressNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
progressNode.layer.add(rotationAnimation, forKey: "progressRotation")
|
|
self.progressNode = progressNode
|
|
|
|
self.titleNode.alpha = 0.0
|
|
self.titleNode.layer.animateAlpha(from: 0.55, to: 0.0, duration: 0.2)
|
|
|
|
self.subtitleNode.alpha = 0.0
|
|
self.subtitleNode.layer.animateAlpha(from: 0.55, to: 0.0, duration: 0.2)
|
|
|
|
self.badgeNode?.alpha = 0.0
|
|
self.badgeNode?.layer.animateAlpha(from: 0.55, to: 0.0, duration: 0.2)
|
|
|
|
self.shimmerView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
self.borderShimmerView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
}
|
|
|
|
public func transitionFromProgress() {
|
|
guard let progressNode = self.progressNode else {
|
|
return
|
|
}
|
|
self.progressNode = nil
|
|
|
|
switch self.progressType {
|
|
case .fullSize:
|
|
self.buttonBackgroundNode.layer.cornerRadius = self.buttonCornerRadius
|
|
self.buttonBackgroundNode.layer.animate(from: self.buttonHeight / 2.0 as NSNumber, to: self.buttonCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
|
self.buttonBackgroundNode.layer.animateFrame(from: progressNode.frame, to: self.bounds, duration: 0.2)
|
|
|
|
self.buttonBackgroundNode.alpha = 1.0
|
|
self.buttonBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false)
|
|
case .embedded:
|
|
break
|
|
}
|
|
|
|
progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak progressNode, weak self] _ in
|
|
progressNode?.removeFromSupernode()
|
|
self?.isUserInteractionEnabled = true
|
|
})
|
|
|
|
self.titleNode.alpha = 1.0
|
|
self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
|
|
self.subtitleNode.alpha = 1.0
|
|
self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
|
|
self.badgeNode?.alpha = 1.0
|
|
self.badgeNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
|
|
self.shimmerView?.layer.removeAllAnimations()
|
|
self.shimmerView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
self.borderShimmerView?.layer.removeAllAnimations()
|
|
self.borderShimmerView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
|
|
public final class SolidRoundedButtonView: UIView {
|
|
private var theme: SolidRoundedButtonTheme
|
|
private var font: SolidRoundedButtonFont
|
|
private var fontSize: CGFloat
|
|
|
|
private let buttonBackgroundNode: UIImageView
|
|
private var buttonBackgroundAnimationView: UIImageView?
|
|
|
|
private var shimmerView: ShimmerEffectForegroundView?
|
|
private var borderView: UIView?
|
|
private var borderMaskView: UIView?
|
|
private var borderShimmerView: ShimmerEffectForegroundView?
|
|
|
|
private let buttonNode: HighlightTrackingButton
|
|
private let titleNode: ImmediateTextView
|
|
private let subtitleNode: ImmediateTextView
|
|
private let iconNode: UIImageView
|
|
private var animationNode: SimpleAnimationNode?
|
|
private var progressNode: UIImageView?
|
|
private var badgeNode: BadgeNode?
|
|
|
|
private let buttonHeight: CGFloat
|
|
private let buttonCornerRadius: CGFloat
|
|
|
|
public var pressed: (() -> Void)?
|
|
public var validLayout: CGFloat?
|
|
|
|
public var title: String? {
|
|
didSet {
|
|
self.updateAccessibilityLabels()
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
public var label: String? {
|
|
didSet {
|
|
self.updateAccessibilityLabels()
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
public var subtitle: String? {
|
|
didSet {
|
|
self.updateAccessibilityLabels()
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, previousSubtitle: oldValue, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
public var badge: String? {
|
|
didSet {
|
|
self.updateAccessibilityLabels()
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateAccessibilityLabels() {
|
|
self.accessibilityLabel = (self.title ?? "") + " " + (self.subtitle ?? "")
|
|
self.accessibilityValue = self.label
|
|
if !self.isEnabled {
|
|
self.accessibilityTraits = [.button, .notEnabled]
|
|
} else {
|
|
self.accessibilityTraits = [.button]
|
|
}
|
|
}
|
|
|
|
public var icon: UIImage? {
|
|
didSet {
|
|
self.iconNode.image = generateTintedImage(image: self.icon, color: self.theme.foregroundColor)
|
|
}
|
|
}
|
|
|
|
public var isEnabled: Bool = true {
|
|
didSet {
|
|
if self.isEnabled != oldValue {
|
|
self.titleNode.alpha = self.isEnabled ? 1.0 : 0.6
|
|
}
|
|
self.updateAccessibilityLabels()
|
|
}
|
|
}
|
|
|
|
private var animationTimer: SwiftSignalKit.Timer?
|
|
public var animation: String? {
|
|
didSet {
|
|
if let animation = self.animation {
|
|
if animation != oldValue {
|
|
self.animationNode?.view.removeFromSuperview()
|
|
self.animationNode = nil
|
|
|
|
let animationNode = SimpleAnimationNode(animationName: animation, size: CGSize(width: 30.0, height: 30.0))
|
|
animationNode.customColor = self.theme.foregroundColor
|
|
animationNode.isUserInteractionEnabled = false
|
|
self.addSubview(animationNode.view)
|
|
self.animationNode = animationNode
|
|
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
|
|
if self.gloss {
|
|
self.animationTimer?.invalidate()
|
|
|
|
Queue.mainQueue().after(1.25) {
|
|
self.animationNode?.play()
|
|
|
|
let timer = SwiftSignalKit.Timer(timeout: 4.0, repeat: true, completion: { [weak self] in
|
|
self?.animationNode?.play()
|
|
}, queue: Queue.mainQueue())
|
|
self.animationTimer = timer
|
|
timer.start()
|
|
}
|
|
} else {
|
|
self.animationNode?.play()
|
|
let timer = SwiftSignalKit.Timer(timeout: 4.0, repeat: true, completion: { [weak self] in
|
|
self?.animationNode?.play()
|
|
}, queue: Queue.mainQueue())
|
|
self.animationTimer = timer
|
|
timer.start()
|
|
}
|
|
}
|
|
} else if let animationNode = self.animationNode {
|
|
animationNode.view.removeFromSuperview()
|
|
self.animationNode = nil
|
|
|
|
self.animationTimer?.invalidate()
|
|
self.animationTimer = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
public var iconSpacing: CGFloat = 8.0 {
|
|
didSet {
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
public var iconPosition: SolidRoundedButtonIconPosition = .left {
|
|
didSet {
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
|
|
public var gloss: Bool {
|
|
didSet {
|
|
if self.gloss != oldValue {
|
|
self.setupGloss()
|
|
}
|
|
}
|
|
}
|
|
|
|
public var progressType: SolidRoundedButtonProgressType = .fullSize
|
|
|
|
public init(title: String? = nil, label: String? = nil, badge: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) {
|
|
self.theme = theme
|
|
self.font = font
|
|
self.fontSize = fontSize
|
|
self.buttonHeight = height
|
|
self.buttonCornerRadius = cornerRadius
|
|
self.title = title
|
|
self.label = label
|
|
self.badge = badge
|
|
self.gloss = gloss
|
|
|
|
self.buttonBackgroundNode = UIImageView()
|
|
self.buttonBackgroundNode.clipsToBounds = true
|
|
self.buttonBackgroundNode.layer.cornerRadius = cornerRadius
|
|
if #available(iOS 13.0, *) {
|
|
self.buttonBackgroundNode.layer.cornerCurve = .continuous
|
|
}
|
|
|
|
self.buttonBackgroundNode.backgroundColor = theme.backgroundColor
|
|
if theme.backgroundColors.count > 1 {
|
|
var locations: [CGFloat] = []
|
|
let delta = 1.0 / CGFloat(theme.backgroundColors.count - 1)
|
|
for i in 0 ..< theme.backgroundColors.count {
|
|
locations.append(delta * CGFloat(i))
|
|
}
|
|
self.buttonBackgroundNode.image = generateGradientImage(size: CGSize(width: 200.0, height: height), colors: theme.backgroundColors, locations: locations, direction: .horizontal)
|
|
|
|
let buttonBackgroundAnimationView = UIImageView()
|
|
buttonBackgroundAnimationView.image = self.buttonBackgroundNode.image
|
|
self.buttonBackgroundNode.addSubview(buttonBackgroundAnimationView)
|
|
self.buttonBackgroundAnimationView = buttonBackgroundAnimationView
|
|
}
|
|
|
|
self.buttonNode = HighlightTrackingButton()
|
|
self.buttonNode.isMultipleTouchEnabled = false
|
|
self.buttonNode.isExclusiveTouch = true
|
|
|
|
self.titleNode = ImmediateTextView()
|
|
self.titleNode.isUserInteractionEnabled = false
|
|
|
|
self.subtitleNode = ImmediateTextView()
|
|
self.subtitleNode.isUserInteractionEnabled = false
|
|
|
|
self.iconNode = UIImageView()
|
|
self.iconNode.image = generateTintedImage(image: icon, color: self.theme.foregroundColor)
|
|
|
|
super.init(frame: CGRect())
|
|
|
|
self.isAccessibilityElement = true
|
|
|
|
self.addSubview(self.buttonBackgroundNode)
|
|
self.addSubview(self.buttonNode)
|
|
self.addSubview(self.titleNode)
|
|
self.addSubview(self.subtitleNode)
|
|
self.addSubview(self.iconNode)
|
|
|
|
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
|
|
|
|
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self {
|
|
if highlighted {
|
|
strongSelf.buttonBackgroundNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.buttonBackgroundNode.alpha = 0.55
|
|
strongSelf.titleNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.titleNode.alpha = 0.55
|
|
strongSelf.subtitleNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.subtitleNode.alpha = 0.55
|
|
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.iconNode.alpha = 0.55
|
|
strongSelf.animationNode?.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.animationNode?.alpha = 0.55
|
|
} else {
|
|
if strongSelf.buttonBackgroundNode.alpha > 0.0 {
|
|
strongSelf.buttonBackgroundNode.alpha = 1.0
|
|
strongSelf.buttonBackgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
|
strongSelf.titleNode.alpha = 1.0
|
|
strongSelf.titleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
|
strongSelf.subtitleNode.alpha = 1.0
|
|
strongSelf.subtitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
|
strongSelf.iconNode.alpha = 1.0
|
|
strongSelf.iconNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
|
strongSelf.animationNode?.alpha = 1.0
|
|
strongSelf.animationNode?.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if #available(iOS 13.0, *) {
|
|
self.buttonBackgroundNode.layer.cornerCurve = .continuous
|
|
}
|
|
|
|
self.updateAccessibilityLabels()
|
|
}
|
|
|
|
required public init(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.animationTimer?.invalidate()
|
|
}
|
|
|
|
private func setupGloss() {
|
|
if self.gloss {
|
|
if self.shimmerView == nil {
|
|
let shimmerView = ShimmerEffectForegroundView()
|
|
self.shimmerView = shimmerView
|
|
|
|
if #available(iOS 13.0, *) {
|
|
shimmerView.layer.cornerCurve = .continuous
|
|
shimmerView.layer.cornerRadius = self.buttonCornerRadius
|
|
}
|
|
|
|
let borderView = UIView()
|
|
borderView.isUserInteractionEnabled = false
|
|
self.borderView = borderView
|
|
|
|
let borderMaskView = UIView()
|
|
borderMaskView.layer.borderWidth = 1.0 + UIScreenPixel
|
|
borderMaskView.layer.borderColor = UIColor.white.cgColor
|
|
borderMaskView.layer.cornerRadius = self.buttonCornerRadius
|
|
borderView.mask = borderMaskView
|
|
self.borderMaskView = borderMaskView
|
|
|
|
let borderShimmerView = ShimmerEffectForegroundView()
|
|
self.borderShimmerView = borderShimmerView
|
|
borderView.addSubview(borderShimmerView)
|
|
|
|
self.insertSubview(shimmerView, belowSubview: self.buttonNode)
|
|
self.insertSubview(borderView, belowSubview: self.buttonNode)
|
|
|
|
self.updateShimmerParameters()
|
|
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
}
|
|
} else if self.shimmerView != nil {
|
|
self.shimmerView?.removeFromSuperview()
|
|
self.borderView?.removeFromSuperview()
|
|
self.borderMaskView?.removeFromSuperview()
|
|
self.borderShimmerView?.removeFromSuperview()
|
|
|
|
self.shimmerView = nil
|
|
self.borderView = nil
|
|
self.borderMaskView = nil
|
|
self.borderShimmerView = nil
|
|
}
|
|
}
|
|
|
|
private func setupGradientAnimations() {
|
|
guard let buttonBackgroundAnimationView = self.buttonBackgroundAnimationView else {
|
|
return
|
|
}
|
|
|
|
if let _ = buttonBackgroundAnimationView.layer.animation(forKey: "movement") {
|
|
} else {
|
|
let offset = (buttonBackgroundAnimationView.frame.width - self.frame.width) / 2.0
|
|
let previousValue = buttonBackgroundAnimationView.center.x
|
|
var newValue: CGFloat = offset
|
|
if offset - previousValue < buttonBackgroundAnimationView.frame.width * 0.25 {
|
|
newValue -= buttonBackgroundAnimationView.frame.width * 0.35
|
|
}
|
|
buttonBackgroundAnimationView.center = CGPoint(x: newValue, y: buttonBackgroundAnimationView.bounds.size.height / 2.0)
|
|
|
|
CATransaction.begin()
|
|
|
|
let animation = CABasicAnimation(keyPath: "position.x")
|
|
animation.duration = 4.5
|
|
animation.fromValue = previousValue
|
|
animation.toValue = newValue
|
|
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
|
|
CATransaction.setCompletionBlock { [weak self] in
|
|
// if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy {
|
|
self?.setupGradientAnimations()
|
|
// }
|
|
}
|
|
|
|
buttonBackgroundAnimationView.layer.add(animation, forKey: "movement")
|
|
CATransaction.commit()
|
|
}
|
|
}
|
|
|
|
public func transitionToProgress() {
|
|
guard self.progressNode == nil else {
|
|
return
|
|
}
|
|
|
|
self.isUserInteractionEnabled = false
|
|
|
|
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
|
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
|
rotationAnimation.duration = 1.0
|
|
rotationAnimation.fromValue = NSNumber(value: Float(0.0))
|
|
rotationAnimation.toValue = NSNumber(value: Float.pi * 2.0)
|
|
rotationAnimation.repeatCount = Float.infinity
|
|
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
|
rotationAnimation.beginTime = 1.0
|
|
|
|
let buttonOffset = self.buttonBackgroundNode.frame.minX
|
|
let buttonWidth = self.buttonBackgroundNode.frame.width
|
|
|
|
let progressNode = UIImageView()
|
|
|
|
switch self.progressType {
|
|
case .fullSize:
|
|
let progressFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(buttonOffset + (buttonWidth - self.buttonHeight) / 2.0), y: 0.0), size: CGSize(width: self.buttonHeight, height: self.buttonHeight))
|
|
progressNode.frame = progressFrame
|
|
progressNode.image = generateIndefiniteActivityIndicatorImage(color: self.buttonBackgroundNode.backgroundColor ?? .clear, diameter: self.buttonHeight, lineWidth: 2.0 + UIScreenPixel)
|
|
|
|
self.buttonBackgroundNode.layer.cornerRadius = self.buttonHeight / 2.0
|
|
self.buttonBackgroundNode.layer.animate(from: self.buttonCornerRadius as NSNumber, to: self.buttonHeight / 2.0 as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
|
self.buttonBackgroundNode.layer.animateFrame(from: self.buttonBackgroundNode.frame, to: progressFrame, duration: 0.2)
|
|
|
|
self.buttonBackgroundNode.alpha = 0.0
|
|
self.buttonBackgroundNode.layer.animateAlpha(from: 0.55, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
|
|
self.insertSubview(progressNode, at: 0)
|
|
case .embedded:
|
|
let diameter: CGFloat = self.buttonHeight - 22.0
|
|
let progressFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(buttonOffset + (buttonWidth - diameter) / 2.0), y: floorToScreenPixels((self.buttonHeight - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))
|
|
progressNode.frame = progressFrame
|
|
progressNode.image = generateIndefiniteActivityIndicatorImage(color: self.theme.foregroundColor, diameter: diameter, lineWidth: 3.0)
|
|
|
|
self.addSubview(progressNode)
|
|
}
|
|
|
|
progressNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
progressNode.layer.add(rotationAnimation, forKey: "progressRotation")
|
|
self.progressNode = progressNode
|
|
|
|
self.titleNode.alpha = 0.0
|
|
self.titleNode.layer.animateAlpha(from: 0.55, to: 0.0, duration: 0.2)
|
|
|
|
self.subtitleNode.alpha = 0.0
|
|
self.subtitleNode.layer.animateAlpha(from: 0.55, to: 0.0, duration: 0.2)
|
|
|
|
self.badgeNode?.alpha = 0.0
|
|
self.badgeNode?.layer.animateAlpha(from: 0.55, to: 0.0, duration: 0.2)
|
|
|
|
self.shimmerView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
self.borderShimmerView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
}
|
|
|
|
public func transitionFromProgress() {
|
|
guard let progressNode = self.progressNode else {
|
|
return
|
|
}
|
|
self.progressNode = nil
|
|
|
|
switch self.progressType {
|
|
case .fullSize:
|
|
self.buttonBackgroundNode.layer.cornerRadius = self.buttonCornerRadius
|
|
self.buttonBackgroundNode.layer.animate(from: self.buttonHeight / 2.0 as NSNumber, to: self.buttonCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
|
self.buttonBackgroundNode.layer.animateFrame(from: progressNode.frame, to: self.bounds, duration: 0.2)
|
|
|
|
self.buttonBackgroundNode.alpha = 1.0
|
|
self.buttonBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false)
|
|
case .embedded:
|
|
break
|
|
}
|
|
|
|
progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak progressNode, weak self] _ in
|
|
progressNode?.removeFromSuperview()
|
|
self?.isUserInteractionEnabled = true
|
|
})
|
|
|
|
self.titleNode.alpha = 1.0
|
|
self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
|
|
self.subtitleNode.alpha = 1.0
|
|
self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
|
|
self.badgeNode?.alpha = 1.0
|
|
self.badgeNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
|
|
self.shimmerView?.layer.removeAllAnimations()
|
|
self.shimmerView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
self.borderShimmerView?.layer.removeAllAnimations()
|
|
self.borderShimmerView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
func updateShimmerParameters() {
|
|
guard let shimmerView = self.shimmerView, let borderShimmerView = self.borderShimmerView else {
|
|
return
|
|
}
|
|
|
|
let color = self.theme.foregroundColor
|
|
let alpha: CGFloat
|
|
let borderAlpha: CGFloat
|
|
let compositingFilter: String?
|
|
if color.lightness > 0.5 {
|
|
alpha = 0.5
|
|
borderAlpha = 0.75
|
|
compositingFilter = "overlayBlendMode"
|
|
} else {
|
|
alpha = 0.2
|
|
borderAlpha = 0.3
|
|
compositingFilter = nil
|
|
}
|
|
|
|
let globalTimeOffset = self.icon == nil && self.animation == nil
|
|
|
|
shimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(alpha), gradientSize: 70.0, globalTimeOffset: globalTimeOffset, duration: 4.0, horizontal: true)
|
|
borderShimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(borderAlpha), gradientSize: 70.0, globalTimeOffset: globalTimeOffset, duration: 4.0, horizontal: true)
|
|
|
|
shimmerView.layer.compositingFilter = compositingFilter
|
|
borderShimmerView.layer.compositingFilter = compositingFilter
|
|
}
|
|
|
|
public func updateTheme(_ theme: SolidRoundedButtonTheme) {
|
|
guard theme !== self.theme else {
|
|
return
|
|
}
|
|
self.theme = theme
|
|
|
|
self.buttonBackgroundNode.backgroundColor = theme.backgroundColor
|
|
if theme.backgroundColors.count > 1 {
|
|
var locations: [CGFloat] = []
|
|
let delta = 1.0 / CGFloat(theme.backgroundColors.count - 1)
|
|
for i in 0 ..< theme.backgroundColors.count {
|
|
locations.append(delta * CGFloat(i))
|
|
}
|
|
self.buttonBackgroundNode.image = generateGradientImage(size: CGSize(width: 200.0, height: self.buttonHeight), colors: theme.backgroundColors, locations: locations, direction: .horizontal)
|
|
self.buttonBackgroundAnimationView?.image = self.buttonBackgroundNode.image
|
|
} else {
|
|
self.buttonBackgroundNode.image = nil
|
|
self.buttonBackgroundAnimationView?.image = nil
|
|
}
|
|
|
|
let titleText = NSMutableAttributedString()
|
|
titleText.append(NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor))
|
|
if let label = self.label {
|
|
titleText.append(NSAttributedString(string: " " + label, font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor.withAlphaComponent(0.6)))
|
|
}
|
|
|
|
self.titleNode.attributedText = titleText
|
|
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor)
|
|
|
|
self.iconNode.image = generateTintedImage(image: self.iconNode.image, color: theme.foregroundColor)
|
|
|
|
if let width = self.validLayout {
|
|
_ = self.updateLayout(width: width, transition: .immediate)
|
|
}
|
|
|
|
self.updateShimmerParameters()
|
|
}
|
|
|
|
public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
|
return self.updateLayout(width: width, previousSubtitle: self.subtitle, transition: transition)
|
|
}
|
|
|
|
private func updateLayout(width: CGFloat, previousSubtitle: String?, transition: ContainedViewLayoutTransition) -> CGFloat {
|
|
self.validLayout = width
|
|
|
|
self.setupGloss()
|
|
|
|
let buttonSize = CGSize(width: width, height: self.buttonHeight)
|
|
let buttonFrame = CGRect(origin: CGPoint(), size: buttonSize)
|
|
transition.updateFrame(view: self.buttonBackgroundNode, frame: buttonFrame)
|
|
|
|
if let shimmerView = self.shimmerView, let borderView = self.borderView, let borderMaskView = self.borderMaskView, let borderShimmerView = self.borderShimmerView {
|
|
transition.updateFrame(view: shimmerView, frame: buttonFrame)
|
|
transition.updateFrame(view: borderView, frame: buttonFrame)
|
|
transition.updateFrame(view: borderMaskView, frame: buttonFrame)
|
|
transition.updateFrame(view: borderShimmerView, frame: buttonFrame)
|
|
|
|
shimmerView.updateAbsoluteRect(CGRect(origin: CGPoint(x: width * 4.0, y: 0.0), size: buttonSize), within: CGSize(width: width * 9.0, height: buttonHeight))
|
|
borderShimmerView.updateAbsoluteRect(CGRect(origin: CGPoint(x: width * 4.0, y: 0.0), size: buttonSize), within: CGSize(width: width * 9.0, height: buttonHeight))
|
|
}
|
|
|
|
if let buttonBackgroundAnimationView = self.buttonBackgroundAnimationView {
|
|
buttonBackgroundAnimationView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width * 2.4, height: buttonSize.height))
|
|
if buttonBackgroundAnimationView.layer.animation(forKey: "movement") == nil {
|
|
buttonBackgroundAnimationView.center = CGPoint(x: buttonSize.width * 2.4 / 2.0 - buttonBackgroundAnimationView.frame.width * 0.35, y: buttonSize.height / 2.0)
|
|
}
|
|
self.setupGradientAnimations()
|
|
}
|
|
|
|
transition.updateFrame(view: self.buttonNode, frame: buttonFrame)
|
|
|
|
let titleText = NSMutableAttributedString()
|
|
titleText.append(NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor))
|
|
if let label = self.label {
|
|
titleText.append(NSAttributedString(string: " " + label, font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor.withAlphaComponent(0.6)))
|
|
}
|
|
|
|
self.titleNode.attributedText = titleText
|
|
|
|
let iconSize: CGSize
|
|
if let _ = self.animationNode {
|
|
iconSize = CGSize(width: 30.0, height: 30.0)
|
|
} else {
|
|
iconSize = self.iconNode.image?.size ?? CGSize()
|
|
}
|
|
let titleSize = self.titleNode.updateLayout(buttonSize)
|
|
|
|
let spacingOffset: CGFloat = 9.0
|
|
let verticalInset: CGFloat = self.subtitle == nil ? floor((buttonFrame.height - titleSize.height) / 2.0) : floor((buttonFrame.height - titleSize.height) / 2.0) - spacingOffset
|
|
let iconSpacing: CGFloat = self.iconSpacing
|
|
|
|
var contentWidth: CGFloat = titleSize.width
|
|
if !iconSize.width.isZero {
|
|
contentWidth += iconSize.width + iconSpacing
|
|
}
|
|
var nextContentOrigin = floor((buttonFrame.width - contentWidth) / 2.0)
|
|
|
|
let iconFrame: CGRect
|
|
let titleFrame: CGRect
|
|
switch self.iconPosition {
|
|
case .left:
|
|
iconFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize)
|
|
if !iconSize.width.isZero {
|
|
nextContentOrigin += iconSize.width + iconSpacing
|
|
}
|
|
titleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + verticalInset), size: titleSize)
|
|
case .right:
|
|
titleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + verticalInset), size: titleSize)
|
|
if !iconSize.width.isZero {
|
|
nextContentOrigin += titleFrame.width + iconSpacing
|
|
}
|
|
iconFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize)
|
|
}
|
|
|
|
transition.updateFrame(view: self.iconNode, frame: iconFrame)
|
|
if let animationNode = self.animationNode {
|
|
transition.updateFrame(view: animationNode.view, frame: iconFrame)
|
|
}
|
|
transition.updateFrame(view: self.titleNode, frame: titleFrame)
|
|
|
|
if let badge = self.badge {
|
|
let badgeNode: BadgeNode
|
|
if let current = self.badgeNode {
|
|
badgeNode = current
|
|
} else {
|
|
badgeNode = BadgeNode(fillColor: self.theme.foregroundColor, strokeColor: .clear, textColor: self.theme.backgroundColor)
|
|
badgeNode.alpha = self.titleNode.alpha == 0.0 ? 0.0 : 1.0
|
|
self.badgeNode = badgeNode
|
|
self.addSubnode(badgeNode)
|
|
}
|
|
badgeNode.text = badge
|
|
let badgeSize = badgeNode.update(CGSize(width: 100.0, height: 100.0))
|
|
transition.updateFrame(node: badgeNode, frame: CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: titleFrame.minY + floor((titleFrame.height - badgeSize.height) * 0.5)), size: badgeSize))
|
|
} else if let badgeNode = self.badgeNode {
|
|
self.badgeNode = nil
|
|
badgeNode.removeFromSupernode()
|
|
}
|
|
|
|
if self.subtitle != self.subtitleNode.attributedText?.string {
|
|
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: self.theme.foregroundColor)
|
|
}
|
|
|
|
let subtitleSize = self.subtitleNode.updateLayout(buttonSize)
|
|
let subtitleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - subtitleSize.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - titleSize.height) / 2.0) + spacingOffset + 2.0), size: subtitleSize)
|
|
transition.updateFrame(view: self.subtitleNode, frame: subtitleFrame)
|
|
|
|
if previousSubtitle == nil && self.subtitle != nil {
|
|
self.titleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: spacingOffset / 2.0), to: CGPoint(), duration: 0.3, additive: true)
|
|
self.subtitleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -spacingOffset / 2.0), to: CGPoint(), duration: 0.3, additive: true)
|
|
self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
}
|
|
|
|
return buttonSize.height
|
|
}
|
|
|
|
@objc private func buttonPressed() {
|
|
self.pressed?()
|
|
}
|
|
}
|