Swiftgram/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Sources/VoiceChatActionButtonBackgroundNode.swift

745 lines
33 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
private let progressLineWidth: CGFloat = 3.0 + UIScreenPixel
private let buttonSize = CGSize(width: 112.0, height: 112.0)
private let radius = buttonSize.width / 2.0
private let areaSize = CGSize(width: 300.0, height: 300.0)
private let blobSize = CGSize(width: 190.0, height: 190.0)
private let secondaryGreyColor = UIColor(rgb: 0x1c1c1e)
private let whiteColor = UIColor(rgb: 0xffffff)
private let greyColor = UIColor(rgb: 0x2c2c2e)
private let blue = UIColor(rgb: 0x007fff)
private let lightBlue = UIColor(rgb: 0x00affe)
private let green = UIColor(rgb: 0x33c659)
private let activeBlue = UIColor(rgb: 0x00a0b9)
private let purple = UIColor(rgb: 0x3252ef)
private let pink = UIColor(rgb: 0xef436c)
final class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
enum State: Equatable {
case connecting
case disabled
case button
case blob(Bool)
}
private var state: State
private var hasState = false
private var transition: State?
var audioLevel: CGFloat = 0.0 {
didSet {
self.maskBlobView.updateLevel(self.audioLevel, immediately: false)
}
}
var updatedActive: ((Bool) -> Void)?
var updatedColors: ((UIColor?, UIColor?) -> Void)?
private let backgroundCircleLayer = CAShapeLayer()
private let foregroundCircleLayer = CAShapeLayer()
private let growingForegroundCircleLayer = CAShapeLayer()
private let foregroundView = UIView()
private let foregroundGradientLayer = CAGradientLayer()
private let maskView = UIView()
private let maskGradientLayer = CAGradientLayer()
private let maskBlobView: VoiceBlobView
private let maskCircleLayer = CAShapeLayer()
let maskProgressLayer = CAShapeLayer()
private let maskMediumBlobLayer = CAShapeLayer()
private let maskBigBlobLayer = CAShapeLayer()
private let hierarchyTrackingNode: HierarchyTrackingNode
private var isCurrentlyInHierarchy = false
var ignoreHierarchyChanges = false
override init() {
self.state = .connecting
self.maskBlobView = VoiceBlobView(frame: CGRect(origin: CGPoint(x: (areaSize.width - blobSize.width) / 2.0, y: (areaSize.height - blobSize.height) / 2.0), size: blobSize), maxLevel: 1.5, mediumBlobRange: (0.69, 0.87), bigBlobRange: (0.71, 1.0))
self.maskBlobView.setColor(whiteColor)
self.maskBlobView.isHidden = true
var updateInHierarchy: ((Bool) -> Void)?
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
updateInHierarchy?(value)
})
super.init()
self.addSubnode(self.hierarchyTrackingNode)
let circlePath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: buttonSize)).cgPath
self.backgroundCircleLayer.fillColor = greyColor.cgColor
self.backgroundCircleLayer.path = circlePath
let smallerCirclePath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width - progressLineWidth, height: buttonSize.height - progressLineWidth))).cgPath
self.foregroundCircleLayer.fillColor = greyColor.cgColor
self.foregroundCircleLayer.path = smallerCirclePath
self.foregroundCircleLayer.transform = CATransform3DMakeScale(0.0, 0.0, 1)
self.foregroundCircleLayer.isHidden = true
self.growingForegroundCircleLayer.fillColor = greyColor.cgColor
self.growingForegroundCircleLayer.path = smallerCirclePath
self.growingForegroundCircleLayer.transform = CATransform3DMakeScale(1.0, 1.0, 1)
self.growingForegroundCircleLayer.isHidden = true
self.foregroundGradientLayer.type = .radial
self.foregroundGradientLayer.colors = [lightBlue.cgColor, blue.cgColor, blue.cgColor]
self.foregroundGradientLayer.locations = [0.0, 0.55, 1.0]
self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
self.maskView.backgroundColor = .clear
self.maskGradientLayer.type = .radial
self.maskGradientLayer.colors = [UIColor(rgb: 0xffffff, alpha: 0.4).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor]
self.maskGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
self.maskGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
self.maskGradientLayer.transform = CATransform3DMakeScale(0.3, 0.3, 1.0)
self.maskGradientLayer.isHidden = true
let path = CGMutablePath()
path.addArc(center: CGPoint(x: (buttonSize.width + 6.0) / 2.0, y: (buttonSize.height + 6.0) / 2.0), radius: radius, startAngle: 0.0, endAngle: CGFloat.pi * 2.0, clockwise: true)
self.maskProgressLayer.strokeColor = whiteColor.cgColor
self.maskProgressLayer.fillColor = UIColor.clear.cgColor
self.maskProgressLayer.lineWidth = progressLineWidth
self.maskProgressLayer.lineCap = .round
self.maskProgressLayer.path = path
let circleFrame = CGRect(origin: CGPoint(x: (areaSize.width - buttonSize.width) / 2.0, y: (areaSize.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0)
let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath
self.maskCircleLayer.path = largerCirclePath
self.maskCircleLayer.fillColor = whiteColor.cgColor
self.maskCircleLayer.isHidden = true
updateInHierarchy = { [weak self] value in
if let strongSelf = self, !strongSelf.ignoreHierarchyChanges {
strongSelf.isCurrentlyInHierarchy = value
strongSelf.updateAnimations()
}
}
}
override func didLoad() {
super.didLoad()
self.layer.addSublayer(self.backgroundCircleLayer)
self.view.addSubview(self.foregroundView)
self.layer.addSublayer(self.foregroundCircleLayer)
self.layer.addSublayer(self.growingForegroundCircleLayer)
self.foregroundView.mask = self.maskView
self.foregroundView.layer.addSublayer(self.foregroundGradientLayer)
self.maskView.layer.addSublayer(self.maskGradientLayer)
self.maskView.layer.addSublayer(self.maskProgressLayer)
self.maskView.addSubview(self.maskBlobView)
self.maskView.layer.addSublayer(self.maskCircleLayer)
self.maskBlobView.scaleUpdated = { [weak self] scale in
if let strongSelf = self {
strongSelf.updateGlowScale(strongSelf.isActive ? scale : nil)
}
}
}
private func setupGradientAnimations() {
if let _ = self.foregroundGradientLayer.animation(forKey: "movement") {
} else {
let previousValue = self.foregroundGradientLayer.startPoint
let newValue: CGPoint
if self.maskBlobView.presentationAudioLevel > 0.22 {
newValue = CGPoint(x: CGFloat.random(in: 0.9 ..< 1.0), y: CGFloat.random(in: 0.15 ..< 0.35))
} else if self.maskBlobView.presentationAudioLevel > 0.01 {
newValue = CGPoint(x: CGFloat.random(in: 0.57 ..< 0.85), y: CGFloat.random(in: 0.15 ..< 0.45))
} else {
newValue = CGPoint(x: CGFloat.random(in: 0.6 ..< 0.75), y: CGFloat.random(in: 0.25 ..< 0.45))
}
self.foregroundGradientLayer.startPoint = newValue
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "startPoint")
animation.duration = Double.random(in: 0.8 ..< 1.4)
animation.fromValue = previousValue
animation.toValue = newValue
CATransaction.setCompletionBlock { [weak self] in
if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy {
self?.setupGradientAnimations()
}
}
self.foregroundGradientLayer.add(animation, forKey: "movement")
CATransaction.commit()
}
}
private func setupProgressAnimations() {
if let _ = self.maskProgressLayer.animation(forKey: "progressRotation") {
} else {
self.maskProgressLayer.isHidden = false
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 1.0
animation.fromValue = NSNumber(value: Float(0.0))
animation.toValue = NSNumber(value: Float.pi * 2.0)
animation.repeatCount = Float.infinity
animation.beginTime = 0.0
self.maskProgressLayer.add(animation, forKey: "progressRotation")
let shrinkAnimation = CABasicAnimation(keyPath: "strokeEnd")
shrinkAnimation.fromValue = 1.0
shrinkAnimation.toValue = 0.0
shrinkAnimation.duration = 1.0
shrinkAnimation.beginTime = 0.0
let growthAnimation = CABasicAnimation(keyPath: "strokeEnd")
growthAnimation.fromValue = 0.0
growthAnimation.toValue = 1.0
growthAnimation.duration = 1.0
growthAnimation.beginTime = 1.0
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat.pi * 2
rotateAnimation.isAdditive = true
rotateAnimation.duration = 1.0
rotateAnimation.beginTime = 1.0
let groupAnimation = CAAnimationGroup()
groupAnimation.repeatCount = Float.infinity
groupAnimation.animations = [shrinkAnimation, growthAnimation, rotateAnimation]
groupAnimation.duration = 2.0
self.maskProgressLayer.add(groupAnimation, forKey: "progressGrowth")
}
}
var glowHidden: Bool = false {
didSet {
if self.glowHidden != oldValue {
let initialAlpha = CGFloat(self.maskProgressLayer.opacity)
let targetAlpha: CGFloat = self.glowHidden ? 0.0 : 1.0
self.maskGradientLayer.opacity = Float(targetAlpha)
self.maskGradientLayer.animateAlpha(from: initialAlpha, to: targetAlpha, duration: 0.2)
}
}
}
var disableGlowAnimations = false
func updateGlowScale(_ scale: CGFloat?) {
if self.disableGlowAnimations {
return
}
if let scale = scale {
self.maskGradientLayer.transform = CATransform3DMakeScale(0.89 + 0.11 * scale, 0.89 + 0.11 * scale, 1.0)
} else {
let initialScale: CGFloat = ((self.maskGradientLayer.value(forKeyPath: "presentationLayer.transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (((self.maskGradientLayer.value(forKeyPath: "transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (0.89))
let targetScale: CGFloat = self.isActive ? 0.89 : 0.85
if abs(targetScale - initialScale) > 0.03 {
self.maskGradientLayer.transform = CATransform3DMakeScale(targetScale, targetScale, 1.0)
self.maskGradientLayer.animateScale(from: initialScale, to: targetScale, duration: 0.3)
}
}
}
enum Gradient {
case speaking
case active
case connecting
case muted
}
func updateGlowAndGradientAnimations(type: Gradient, previousType: Gradient? = nil, animated: Bool = true) {
let effectivePreviousTyoe = previousType ?? .active
let scale: CGFloat
if case .speaking = effectivePreviousTyoe {
scale = 0.95
} else {
scale = 0.8
}
let initialScale: CGFloat = ((self.maskGradientLayer.value(forKeyPath: "presentationLayer.transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (((self.maskGradientLayer.value(forKeyPath: "transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? scale)
let initialColors = self.foregroundGradientLayer.colors
let outerColor: UIColor?
let activeColor: UIColor?
let targetColors: [CGColor]
let targetScale: CGFloat
switch type {
case .speaking:
targetColors = [activeBlue.cgColor, green.cgColor, green.cgColor]
targetScale = 0.89
outerColor = UIColor(rgb: 0x134b22)
activeColor = green
case .active:
targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor]
targetScale = 0.85
outerColor = UIColor(rgb: 0x002e5d)
activeColor = blue
case .connecting:
targetColors = [lightBlue.cgColor, blue.cgColor, blue.cgColor]
targetScale = 0.3
outerColor = nil
activeColor = blue
case .muted:
targetColors = [pink.cgColor, purple.cgColor, purple.cgColor]
targetScale = 0.85
outerColor = UIColor(rgb: 0x24306b)
activeColor = purple
}
self.updatedColors?(outerColor, activeColor)
self.maskGradientLayer.transform = CATransform3DMakeScale(targetScale, targetScale, 1.0)
if let _ = previousType {
self.maskGradientLayer.animateScale(from: initialScale, to: targetScale, duration: 0.3)
} else if animated {
self.maskGradientLayer.animateSpring(from: initialScale as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", duration: 0.45)
}
self.foregroundGradientLayer.colors = targetColors
if animated {
self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
}
}
private func playMuteAnimation() {
if self.animationsEnabled {
self.maskBlobView.startAnimating()
}
self.maskBlobView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
if strongSelf.state != .connecting {
return
}
strongSelf.maskBlobView.isHidden = true
strongSelf.maskBlobView.stopAnimating()
strongSelf.maskBlobView.layer.removeAllAnimations()
})
}
var animatingDisappearance = false
private func playDeactivationAnimation() {
if self.animatingDisappearance {
return
}
self.animatingDisappearance = true
CATransaction.begin()
CATransaction.setDisableActions(true)
self.growingForegroundCircleLayer.isHidden = false
CATransaction.commit()
self.disableGlowAnimations = true
self.maskGradientLayer.removeAllAnimations()
self.updateGlowAndGradientAnimations(type: .connecting, previousType: nil)
if self.animationsEnabled {
self.maskBlobView.startAnimating()
}
self.maskBlobView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
if strongSelf.state != .connecting {
return
}
strongSelf.maskBlobView.isHidden = true
strongSelf.maskBlobView.stopAnimating()
strongSelf.maskBlobView.layer.removeAllAnimations()
})
CATransaction.begin()
let growthAnimation = CABasicAnimation(keyPath: "transform.scale")
growthAnimation.fromValue = 0.0
growthAnimation.toValue = 1.0
growthAnimation.duration = 0.15
growthAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
growthAnimation.isRemovedOnCompletion = false
growthAnimation.fillMode = .forwards
CATransaction.setCompletionBlock {
self.animatingDisappearance = false
self.growingForegroundCircleLayer.isHidden = true
self.disableGlowAnimations = false
if self.state != .connecting {
return
}
CATransaction.begin()
CATransaction.setDisableActions(true)
self.maskGradientLayer.isHidden = true
self.maskCircleLayer.isHidden = true
self.growingForegroundCircleLayer.removeAllAnimations()
CATransaction.commit()
}
self.growingForegroundCircleLayer.add(growthAnimation, forKey: "insideGrowth")
CATransaction.commit()
}
private func playActivationAnimation(active: Bool) {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.maskCircleLayer.isHidden = false
self.maskProgressLayer.isHidden = true
self.maskGradientLayer.isHidden = false
CATransaction.commit()
self.maskGradientLayer.removeAllAnimations()
self.updateGlowAndGradientAnimations(type: active ? .speaking : .active, previousType: nil)
self.maskBlobView.isHidden = false
if self.animationsEnabled {
self.maskBlobView.startAnimating()
}
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
}
private func playConnectionAnimation(type: Gradient, completion: @escaping () -> Void) {
CATransaction.begin()
let initialRotation: CGFloat = CGFloat((self.maskProgressLayer.value(forKeyPath: "presentationLayer.transform.rotation.z") as? NSNumber)?.floatValue ?? 0.0)
let initialStrokeEnd: CGFloat = CGFloat((self.maskProgressLayer.value(forKeyPath: "presentationLayer.strokeEnd") as? NSNumber)?.floatValue ?? 1.0)
self.maskProgressLayer.removeAnimation(forKey: "progressGrowth")
self.maskProgressLayer.removeAnimation(forKey: "progressRotation")
let duration: Double = (1.0 - Double(initialStrokeEnd)) * 0.3
let growthAnimation = CABasicAnimation(keyPath: "strokeEnd")
growthAnimation.fromValue = initialStrokeEnd
growthAnimation.toValue = 1.0
growthAnimation.duration = duration
growthAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotateAnimation.fromValue = initialRotation
rotateAnimation.toValue = initialRotation + CGFloat.pi * 2
rotateAnimation.isAdditive = true
rotateAnimation.duration = duration
rotateAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
let groupAnimation = CAAnimationGroup()
groupAnimation.animations = [growthAnimation, rotateAnimation]
groupAnimation.duration = duration
CATransaction.setCompletionBlock {
var active = true
if case .connecting = self.state {
active = false
}
if active {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.foregroundCircleLayer.isHidden = false
self.foregroundCircleLayer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0)
self.maskCircleLayer.isHidden = false
self.maskProgressLayer.isHidden = true
self.maskGradientLayer.isHidden = false
CATransaction.commit()
completion()
self.updateGlowAndGradientAnimations(type: type, previousType: nil)
if case .connecting = self.state {
} else {
self.maskBlobView.isHidden = false
if self.animationsEnabled {
self.maskBlobView.startAnimating()
}
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
}
self.updatedActive?(true)
CATransaction.begin()
let shrinkAnimation = CABasicAnimation(keyPath: "transform.scale")
shrinkAnimation.fromValue = 1.0
shrinkAnimation.toValue = 0.00001
shrinkAnimation.duration = 0.15
shrinkAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
shrinkAnimation.isRemovedOnCompletion = false
shrinkAnimation.fillMode = .forwards
CATransaction.setCompletionBlock {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.foregroundCircleLayer.isHidden = true
self.foregroundCircleLayer.transform = CATransform3DMakeScale(0.0, 0.0, 1.0)
self.foregroundCircleLayer.removeAllAnimations()
CATransaction.commit()
}
self.foregroundCircleLayer.add(shrinkAnimation, forKey: "insideShrink")
CATransaction.commit()
}
}
self.maskProgressLayer.add(groupAnimation, forKey: "progressCompletion")
CATransaction.commit()
}
private var maskIsCircle = true
private func setupButtonAnimation() {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.backgroundCircleLayer.isHidden = true
self.foregroundCircleLayer.isHidden = true
self.maskCircleLayer.isHidden = false
self.maskProgressLayer.isHidden = true
self.maskGradientLayer.isHidden = true
let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: floor((self.bounds.height - VoiceChatActionButton.buttonHeight) / 2.0), width: self.bounds.width, height: VoiceChatActionButton.buttonHeight), cornerRadius: 10.0).cgPath
self.maskCircleLayer.path = path
self.maskIsCircle = false
CATransaction.commit()
self.updateGlowAndGradientAnimations(type: .muted, previousType: nil)
self.updatedActive?(true)
}
private func playScheduledAnimation() {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.maskGradientLayer.isHidden = false
CATransaction.commit()
let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0)
let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath
let previousPath = self.maskCircleLayer.path
self.maskCircleLayer.path = largerCirclePath
self.maskIsCircle = true
self.maskCircleLayer.animateSpring(from: previousPath as AnyObject, to: largerCirclePath as AnyObject, keyPath: "path", duration: 0.6, initialVelocity: 0.0, damping: 100.0)
self.maskBlobView.isHidden = false
if self.animationsEnabled {
self.maskBlobView.startAnimating()
}
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.6, damping: 100.0)
self.disableGlowAnimations = true
self.maskGradientLayer.removeAllAnimations()
self.maskGradientLayer.animateSpring(from: 0.3 as NSNumber, to: 0.85 as NSNumber, keyPath: "transform.scale", duration: 0.45, completion: { [weak self] _ in
self?.disableGlowAnimations = false
})
}
var animationsEnabled: Bool = true {
didSet {
self.updateAnimations()
}
}
var isActive = false
func updateAnimations() {
if !self.isCurrentlyInHierarchy {
self.foregroundGradientLayer.removeAllAnimations()
self.growingForegroundCircleLayer.removeAllAnimations()
self.maskGradientLayer.removeAllAnimations()
self.maskProgressLayer.removeAllAnimations()
self.maskBlobView.stopAnimating()
return
}
if !self.animationsEnabled {
self.foregroundGradientLayer.removeAllAnimations()
self.maskBlobView.stopAnimating()
} else {
self.setupGradientAnimations()
}
switch self.state {
case .connecting:
self.updatedActive?(false)
if let transition = self.transition {
self.updateGlowScale(nil)
if case .blob = transition {
self.playDeactivationAnimation()
} else if case .disabled = transition {
self.playDeactivationAnimation()
}
self.transition = nil
}
self.setupProgressAnimations()
self.isActive = false
case let .blob(newActive):
if let transition = self.transition {
let type: Gradient = newActive ? .speaking : .active
if transition == .connecting {
self.playConnectionAnimation(type: type) { [weak self] in
self?.isActive = newActive
}
} else if transition == .disabled {
self.playActivationAnimation(active: newActive)
self.transition = nil
self.isActive = newActive
self.updatedActive?(true)
} else if case let .blob(previousActive) = transition {
self.updateGlowAndGradientAnimations(type: type, previousType: previousActive ? .speaking : .active)
self.transition = nil
self.isActive = newActive
}
self.transition = nil
} else {
if self.animationsEnabled {
self.maskBlobView.startAnimating()
}
}
case .disabled:
self.updatedActive?(true)
self.isActive = false
if let transition = self.transition {
if case .button = transition {
self.playScheduledAnimation()
} else if case .connecting = transition {
self.playConnectionAnimation(type: .muted) { [weak self] in
self?.isActive = false
}
} else if case let .blob(previousActive) = transition {
self.updateGlowAndGradientAnimations(type: .muted, previousType: previousActive ? .speaking : .active)
self.playMuteAnimation()
}
self.transition = nil
} else {
if self.maskBlobView.isHidden {
self.updateGlowAndGradientAnimations(type: .muted, previousType: nil, animated: false)
self.maskCircleLayer.isHidden = false
self.maskProgressLayer.isHidden = true
self.maskGradientLayer.isHidden = false
self.maskBlobView.isHidden = false
if self.animationsEnabled {
self.maskBlobView.startAnimating()
}
self.maskBlobView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45)
}
}
case .button:
self.updatedActive?(true)
self.isActive = false
self.setupButtonAnimation()
}
}
var isDark: Bool = false {
didSet {
if self.isDark != oldValue {
self.updateColors()
}
}
}
var isSnap: Bool = false {
didSet {
if self.isSnap != oldValue {
self.updateColors()
}
}
}
var connectingColor: UIColor = UIColor(rgb: 0xb6b6bb) {
didSet {
if self.connectingColor.rgb != oldValue.rgb {
self.updateColors()
}
}
}
func updateColors() {
let previousColor: CGColor = self.backgroundCircleLayer.fillColor ?? greyColor.cgColor
let targetColor: CGColor
if self.isSnap {
targetColor = self.connectingColor.cgColor
} else if self.isDark {
targetColor = secondaryGreyColor.cgColor
} else {
targetColor = greyColor.cgColor
}
self.backgroundCircleLayer.fillColor = targetColor
self.foregroundCircleLayer.fillColor = targetColor
self.growingForegroundCircleLayer.fillColor = targetColor
self.backgroundCircleLayer.animate(from: previousColor, to: targetColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
self.foregroundCircleLayer.animate(from: previousColor, to: targetColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
self.growingForegroundCircleLayer.animate(from: previousColor, to: targetColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
}
func update(state: State, animated: Bool) {
var animated = animated
var hadState = true
if !self.hasState {
hadState = false
self.hasState = true
animated = false
}
if state != self.state || !hadState {
if animated {
self.transition = self.state
}
self.state = state
}
self.updateAnimations()
}
var previousSize: CGSize?
override func layout() {
super.layout()
let sizeUpdated = self.previousSize != self.bounds.size
self.previousSize = self.bounds.size
let bounds = CGRect(x: (self.bounds.width - areaSize.width) / 2.0, y: (self.bounds.height - areaSize.height) / 2.0, width: areaSize.width, height: areaSize.height)
let center = bounds.center
self.maskBlobView.frame = CGRect(origin: CGPoint(x: bounds.minX + (bounds.width - blobSize.width) / 2.0, y: bounds.minY + (bounds.height - blobSize.height) / 2.0), size: blobSize)
let circleFrame = CGRect(origin: CGPoint(x: bounds.minX + (bounds.width - buttonSize.width) / 2.0, y: bounds.minY + (bounds.height - buttonSize.height) / 2.0), size: buttonSize)
self.backgroundCircleLayer.frame = circleFrame
self.foregroundCircleLayer.position = center
self.foregroundCircleLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: circleFrame.width - progressLineWidth, height: circleFrame.height - progressLineWidth))
self.growingForegroundCircleLayer.position = center
self.growingForegroundCircleLayer.bounds = self.foregroundCircleLayer.bounds
self.maskCircleLayer.frame = self.bounds
if sizeUpdated && self.maskIsCircle {
CATransaction.begin()
CATransaction.setDisableActions(true)
let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize).insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0)
let largerCirclePath = UIBezierPath(roundedRect: CGRect(x: circleFrame.minX, y: circleFrame.minY, width: circleFrame.width, height: circleFrame.height), cornerRadius: circleFrame.width / 2.0).cgPath
self.maskCircleLayer.path = largerCirclePath
CATransaction.commit()
}
self.maskProgressLayer.frame = circleFrame.insetBy(dx: -3.0, dy: -3.0)
self.foregroundView.frame = self.bounds
self.foregroundGradientLayer.frame = self.bounds
self.maskGradientLayer.position = center
self.maskGradientLayer.bounds = bounds
self.maskView.frame = self.bounds
}
}