mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
745 lines
33 KiB
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
|
|
}
|
|
}
|