mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
260 lines
16 KiB
Swift
260 lines
16 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import PassKit
|
|
|
|
enum BotCheckoutActionButtonState: Equatable {
|
|
case loading
|
|
case active(String)
|
|
case inactive(String)
|
|
case applePay
|
|
|
|
static func ==(lhs: BotCheckoutActionButtonState, rhs: BotCheckoutActionButtonState) -> Bool {
|
|
switch lhs {
|
|
case .loading:
|
|
if case .loading = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .active(title):
|
|
if case .active(title) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .inactive(title):
|
|
if case .inactive(title) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .applePay:
|
|
if case .applePay = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private let titleFont = Font.semibold(17.0)
|
|
|
|
final class BotCheckoutActionButton: HighlightableButtonNode {
|
|
static var diameter: CGFloat = 48.0
|
|
|
|
private var inactiveFillColor: UIColor
|
|
private var activeFillColor: UIColor
|
|
private var foregroundColor: UIColor
|
|
|
|
private let progressBackgroundNode: ASImageNode
|
|
private let inactiveBackgroundNode: ASImageNode
|
|
private let activeBackgroundNode: ASImageNode
|
|
private var applePayButton: UIButton?
|
|
private let labelNode: TextNode
|
|
|
|
private var state: BotCheckoutActionButtonState?
|
|
private var validLayout: CGSize?
|
|
|
|
init(inactiveFillColor: UIColor, activeFillColor: UIColor, foregroundColor: UIColor) {
|
|
self.inactiveFillColor = inactiveFillColor
|
|
self.activeFillColor = activeFillColor
|
|
self.foregroundColor = foregroundColor
|
|
|
|
self.progressBackgroundNode = ASImageNode()
|
|
self.progressBackgroundNode.displaysAsynchronously = false
|
|
self.progressBackgroundNode.displayWithoutProcessing = true
|
|
self.progressBackgroundNode.isLayerBacked = true
|
|
self.progressBackgroundNode.image = generateImage(CGSize(width: BotCheckoutActionButton.diameter, height: BotCheckoutActionButton.diameter), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
let strokeWidth: CGFloat = 2.0
|
|
context.setFillColor(activeFillColor.cgColor)
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
|
|
|
context.setFillColor(inactiveFillColor.cgColor)
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: strokeWidth, y: strokeWidth), size: CGSize(width: size.width - strokeWidth * 2.0, height: size.height - strokeWidth * 2.0)))
|
|
let cutout: CGFloat = 10.0
|
|
context.fill(CGRect(origin: CGPoint(x: floor((size.width - cutout) / 2.0), y: 0.0), size: CGSize(width: cutout, height: cutout)))
|
|
})
|
|
|
|
self.inactiveBackgroundNode = ASImageNode()
|
|
self.inactiveBackgroundNode.displaysAsynchronously = false
|
|
self.inactiveBackgroundNode.displayWithoutProcessing = true
|
|
self.inactiveBackgroundNode.isLayerBacked = true
|
|
self.inactiveBackgroundNode.image = generateStretchableFilledCircleImage(diameter: BotCheckoutActionButton.diameter, color: self.foregroundColor, strokeColor: activeFillColor, strokeWidth: 2.0)
|
|
self.inactiveBackgroundNode.alpha = 0.0
|
|
|
|
self.activeBackgroundNode = ASImageNode()
|
|
self.activeBackgroundNode.displaysAsynchronously = false
|
|
self.activeBackgroundNode.displayWithoutProcessing = true
|
|
self.activeBackgroundNode.isLayerBacked = true
|
|
self.activeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: BotCheckoutActionButton.diameter, color: activeFillColor)
|
|
|
|
self.labelNode = TextNode()
|
|
self.labelNode.displaysAsynchronously = false
|
|
self.labelNode.isUserInteractionEnabled = false
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.progressBackgroundNode)
|
|
self.addSubnode(self.inactiveBackgroundNode)
|
|
self.addSubnode(self.activeBackgroundNode)
|
|
self.addSubnode(self.labelNode)
|
|
}
|
|
|
|
func setState(_ state: BotCheckoutActionButtonState) {
|
|
if self.state != state {
|
|
let previousState = self.state
|
|
self.state = state
|
|
|
|
if let validLayout = self.validLayout, let previousState = previousState {
|
|
switch state {
|
|
case .loading:
|
|
self.inactiveBackgroundNode.layer.animateFrame(from: self.inactiveBackgroundNode.frame, to: self.progressBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
|
if !self.inactiveBackgroundNode.alpha.isZero {
|
|
self.inactiveBackgroundNode.alpha = 0.0
|
|
self.inactiveBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
|
}
|
|
self.activeBackgroundNode.layer.animateFrame(from: self.activeBackgroundNode.frame, to: self.progressBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
|
self.activeBackgroundNode.alpha = 0.0
|
|
self.activeBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
|
self.labelNode.alpha = 0.0
|
|
self.labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
|
|
|
self.progressBackgroundNode.alpha = 1.0
|
|
self.progressBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
|
|
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
|
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
|
basicAnimation.duration = 0.8
|
|
basicAnimation.fromValue = NSNumber(value: Float(0.0))
|
|
basicAnimation.toValue = NSNumber(value: Float.pi * 2.0)
|
|
basicAnimation.repeatCount = Float.infinity
|
|
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
|
|
|
self.progressBackgroundNode.layer.add(basicAnimation, forKey: "progressRotation")
|
|
case let .active(title):
|
|
if case .active = previousState {
|
|
let makeLayout = TextNode.asyncLayout(self.labelNode)
|
|
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
|
|
let _ = labelApply()
|
|
} else {
|
|
self.inactiveBackgroundNode.layer.animateFrame(from: self.progressBackgroundNode.frame, to: self.activeBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
|
self.inactiveBackgroundNode.alpha = 1.0
|
|
self.progressBackgroundNode.alpha = 0.0
|
|
|
|
self.activeBackgroundNode.layer.animateFrame(from: self.progressBackgroundNode.frame, to: self.activeBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
|
self.activeBackgroundNode.alpha = 1.0
|
|
self.activeBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
|
|
let makeLayout = TextNode.asyncLayout(self.labelNode)
|
|
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
|
|
let _ = labelApply()
|
|
self.labelNode.alpha = 1.0
|
|
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
}
|
|
case let .inactive(title):
|
|
if case .inactive = previousState {
|
|
let makeLayout = TextNode.asyncLayout(self.labelNode)
|
|
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.activeFillColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
|
|
let _ = labelApply()
|
|
} else {
|
|
self.inactiveBackgroundNode.layer.animateFrame(from: self.inactiveBackgroundNode.frame, to: self.activeBackgroundNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
|
self.inactiveBackgroundNode.alpha = 1.0
|
|
self.progressBackgroundNode.alpha = 0.0
|
|
|
|
self.activeBackgroundNode.alpha = 0.0
|
|
|
|
let makeLayout = TextNode.asyncLayout(self.labelNode)
|
|
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: validLayout, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
self.labelNode.frame = CGRect(origin: CGPoint(x: floor((validLayout.width - labelLayout.size.width) / 2.0), y: floor((validLayout.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
|
|
let _ = labelApply()
|
|
self.labelNode.alpha = 1.0
|
|
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
}
|
|
case .applePay:
|
|
if case .applePay = previousState {
|
|
|
|
} else {
|
|
|
|
}
|
|
}
|
|
} else {
|
|
switch state {
|
|
case .loading:
|
|
self.labelNode.alpha = 0.0
|
|
self.progressBackgroundNode.alpha = 1.0
|
|
self.activeBackgroundNode.alpha = 0.0
|
|
|
|
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
|
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
|
basicAnimation.duration = 0.8
|
|
basicAnimation.fromValue = NSNumber(value: Float(0.0))
|
|
basicAnimation.toValue = NSNumber(value: Float.pi * 2.0)
|
|
basicAnimation.repeatCount = Float.infinity
|
|
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
|
|
|
self.progressBackgroundNode.layer.add(basicAnimation, forKey: "progressRotation")
|
|
case .active:
|
|
self.labelNode.alpha = 1.0
|
|
self.progressBackgroundNode.alpha = 0.0
|
|
self.inactiveBackgroundNode.alpha = 0.0
|
|
self.activeBackgroundNode.alpha = 1.0
|
|
case .inactive:
|
|
self.labelNode.alpha = 1.0
|
|
self.progressBackgroundNode.alpha = 0.0
|
|
self.inactiveBackgroundNode.alpha = 1.0
|
|
self.activeBackgroundNode.alpha = 0.0
|
|
case .applePay:
|
|
self.labelNode.alpha = 0.0
|
|
self.progressBackgroundNode.alpha = 0.0
|
|
self.inactiveBackgroundNode.alpha = 0.0
|
|
self.activeBackgroundNode.alpha = 0.0
|
|
if self.applePayButton == nil {
|
|
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
|
let applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black)
|
|
self.view.addSubview(applePayButton)
|
|
self.applePayButton = applePayButton
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
|
self.validLayout = size
|
|
|
|
transition.updateFrame(node: self.progressBackgroundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - BotCheckoutActionButton.diameter) / 2.0), y: 0.0), size: CGSize(width: BotCheckoutActionButton.diameter, height: BotCheckoutActionButton.diameter)))
|
|
transition.updateFrame(node: self.inactiveBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter)))
|
|
transition.updateFrame(node: self.activeBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter)))
|
|
if let applePayButton = self.applePayButton {
|
|
applePayButton.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: BotCheckoutActionButton.diameter))
|
|
}
|
|
|
|
var labelSize = self.labelNode.bounds.size
|
|
if let state = self.state {
|
|
switch state {
|
|
case let .active(title):
|
|
let makeLayout = TextNode.asyncLayout(self.labelNode)
|
|
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
let _ = labelApply()
|
|
labelSize = labelLayout.size
|
|
case let .inactive(title):
|
|
let makeLayout = TextNode.asyncLayout(self.labelNode)
|
|
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.activeFillColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
let _ = labelApply()
|
|
labelSize = labelLayout.size
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) / 2.0), y: floor((size.height - labelSize.height) / 2.0)), size: labelSize))
|
|
}
|
|
}
|