mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 06:35:51 +00:00
Fixing counter separator and labels text color
This commit is contained in:
@@ -0,0 +1,465 @@
|
||||
/*import Foundation
|
||||
import UIKit
|
||||
|
||||
import Display
|
||||
|
||||
private let purple = UIColor(rgb: 0x3252ef)
|
||||
private let pink = UIColor(rgb: 0xe4436c)
|
||||
|
||||
private let latePurple = UIColor(rgb: 0x974aa9)
|
||||
private let latePink = UIColor(rgb: 0xf0436c)
|
||||
|
||||
public final class AnimatedCountView: UIView {
|
||||
let countLabel = AnimatedCountLabel()
|
||||
// let titleLabel = UILabel()
|
||||
let subtitleLabel = UILabel()
|
||||
|
||||
private let foregroundView = UIView()
|
||||
private let foregroundGradientLayer = CAGradientLayer()
|
||||
private let maskingView = UIView()
|
||||
|
||||
override init(frame: CGRect = .zero) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.foregroundGradientLayer.type = .radial
|
||||
self.foregroundGradientLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor]
|
||||
self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0]
|
||||
self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
|
||||
self.foregroundView.mask = self.maskingView
|
||||
self.foregroundView.layer.addSublayer(self.foregroundGradientLayer)
|
||||
|
||||
self.addSubview(self.foregroundView)
|
||||
// self.addSubview(self.titleLabel)
|
||||
self.addSubview(self.subtitleLabel)
|
||||
|
||||
self.maskingView.addSubview(countLabel)
|
||||
countLabel.clipsToBounds = false
|
||||
subtitleLabel.textAlignment = .center
|
||||
// self.backgroundColor = UIColor.white.withAlphaComponent(0.1)
|
||||
}
|
||||
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.foregroundView.frame = CGRect(origin: CGPoint.zero, size: bounds.size)// .insetBy(dx: -40, dy: -40)
|
||||
self.foregroundGradientLayer.frame = CGRect(origin: .zero, size: bounds.size).insetBy(dx: -60, dy: -60)
|
||||
self.maskingView.frame = CGRect(origin: .zero, size: bounds.size)
|
||||
countLabel.frame = CGRect(origin: .zero, size: bounds.size)
|
||||
subtitleLabel.frame = .init(x: bounds.midX - subtitleLabel.intrinsicContentSize.width / 2 - 10, y: subtitleLabel.text == "No viewers" ? bounds.midY - 10 : bounds.height - 6, width: subtitleLabel.intrinsicContentSize.width + 20, height: 20)
|
||||
}
|
||||
|
||||
func update(countString: String, subtitle: String) {
|
||||
self.setupGradientAnimations()
|
||||
|
||||
let text: String = countString// presentationStringsFormattedNumber(Int32(count), ",")
|
||||
|
||||
// self.titleNode.attributedText = NSAttributedString(string: "", font: Font.with(size: 23.0, design: .round, weight: .semibold, traits: []), textColor: .white)
|
||||
// let titleSize = self.titleNode.updateLayout(size)
|
||||
// self.titleNode.frame = CGRect(x: floor((size.width - titleSize.width) / 2.0), y: 48.0, width: titleSize.width, height: titleSize.height)
|
||||
if CGFloat(text.count * 40) < bounds.width - 32 {
|
||||
self.countLabel.attributedText = NSAttributedString(string: text, font: Font.with(size: 60.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)
|
||||
} else {
|
||||
self.countLabel.attributedText = NSAttributedString(string: text, font: Font.with(size: 54.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)
|
||||
}
|
||||
// var timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height))
|
||||
// if timerSize.width > size.width - 32.0 {
|
||||
// self.timerNode.attributedText = NSAttributedString(string: text, font: Font.with(size: 60.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)
|
||||
// timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height))
|
||||
// }
|
||||
|
||||
// self.timerNode.frame = CGRect(x: floor((size.width - timerSize.width) / 2.0), y: 78.0, width: timerSize.width, height: timerSize.height)
|
||||
|
||||
self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, font: Font.with(size: 16.0, design: .round, weight: .semibold, traits: []), textColor: .white)
|
||||
self.subtitleLabel.isHidden = subtitle.isEmpty
|
||||
// let subtitleSize = self.subtitleNode.updateLayout(size)
|
||||
// self.subtitleNode.frame = CGRect(x: floor((size.width - subtitleSize.width) / 2.0), y: 164.0, width: subtitleSize.width, height: subtitleSize.height)
|
||||
|
||||
// self.foregroundView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
// self.setNeedsLayout()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setupGradientAnimations() {
|
||||
if let _ = self.foregroundGradientLayer.animation(forKey: "movement") {
|
||||
} else {
|
||||
let previousValue = self.foregroundGradientLayer.startPoint
|
||||
let newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedCharLayer: CATextLayer {
|
||||
var text: String? {
|
||||
get {
|
||||
self.string as? String ?? (self.string as? NSAttributedString)?.string
|
||||
}
|
||||
set {
|
||||
self.string = newValue
|
||||
}
|
||||
}
|
||||
var attributedText: NSAttributedString? {
|
||||
get {
|
||||
self.string as? NSAttributedString //?? (self.string as? String).map { NSAttributed.init
|
||||
}
|
||||
set {
|
||||
self.string = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var layer: CALayer { self }
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.contentsScale = UIScreen.main.scale
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
self.contentsScale = UIScreen.main.scale
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedCountLabel: UILabel {
|
||||
override var text: String? {
|
||||
get {
|
||||
chars.reduce("") { $0 + ($1.text ?? "") }
|
||||
}
|
||||
set {
|
||||
update(with: newValue ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
override var attributedText: NSAttributedString? {
|
||||
get {
|
||||
let string = NSMutableAttributedString()
|
||||
for char in chars {
|
||||
string.append(char.attributedText ?? NSAttributedString())
|
||||
}
|
||||
return string
|
||||
}
|
||||
set {
|
||||
udpateAttributed(with: newValue ?? NSAttributedString())
|
||||
}
|
||||
}
|
||||
|
||||
private var chars = [AnimatedCharLayer]()
|
||||
private let containerView = UIView()
|
||||
|
||||
override init(frame: CGRect = .zero) {
|
||||
super.init(frame: frame)
|
||||
containerView.clipsToBounds = false
|
||||
addSubview(containerView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
var itemWidth: CGFloat { 36 }
|
||||
var commaWidth: CGFloat { 8 }
|
||||
var interItemSpacing: CGFloat { 0 }
|
||||
|
||||
private func offsetForChar(at index: Int, within characters: [NSAttributedString]? = nil) -> CGFloat {
|
||||
if let characters {
|
||||
return characters[0..<index].reduce(0) {
|
||||
if $1.string == "," {
|
||||
return $0 + commaWidth + interItemSpacing
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}
|
||||
} else {
|
||||
return self.chars[0..<index].reduce(0) {
|
||||
if $1.attributedText?.string == "," {
|
||||
return $0 + commaWidth + interItemSpacing
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
let countWidth = offsetForChar(at: chars.count) /*chars.reduce(0) {
|
||||
if $1.attributedText?.string == "," {
|
||||
return $0 + commaWidth + interItemSpacing
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}*/ - interItemSpacing
|
||||
|
||||
containerView.frame = .init(x: bounds.midX - countWidth / 2, y: 0, width: countWidth, height: bounds.height)
|
||||
chars.enumerated().forEach { (index, char) in
|
||||
let offset = offsetForChar(at: index)
|
||||
char.frame.origin.x = offset
|
||||
// char.frame.origin.x = CGFloat(chars.count - 1 - index) * (40 + interItemSpacing)
|
||||
char.frame.origin.y = 0
|
||||
}
|
||||
}
|
||||
/// Unused
|
||||
func update(with newString: String) {
|
||||
/*let itemWidth: CGFloat = 40
|
||||
let initialDuration: TimeInterval = 0.3
|
||||
let newChars = Array(newString).map { String($0) }
|
||||
let currentChars = chars.map { $0.text ?? "X" }
|
||||
|
||||
// let currentWidth = itemWidth * CGFloat(currentChars.count)
|
||||
let newWidth = itemWidth * CGFloat(newChars.count)
|
||||
|
||||
let interItemDelay: TimeInterval = 0.15
|
||||
var changeIndex = 0
|
||||
|
||||
var newLayers = [AnimatedCharLayer]()
|
||||
|
||||
for index in 0..<min(newChars.count, currentChars.count) {
|
||||
let newCharIndex = newChars.count - 1 - index
|
||||
let currCharIndex = currentChars.count - 1 - index
|
||||
|
||||
if true || newChars[newCharIndex] != currentChars[currCharIndex] {
|
||||
animateOut(for: chars[currCharIndex].layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
|
||||
let newLayer = AnimatedCharLayer()
|
||||
newLayer.text = newChars[newCharIndex]
|
||||
newLayer.frame = .init(x: newWidth - CGFloat(index + 1) * itemWidth, y: 100, width: itemWidth, height: 36)
|
||||
containerView.layer.addSublayer(newLayer)
|
||||
animateIn(for: newLayer.layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
newLayers.append(newLayer)
|
||||
changeIndex += 1
|
||||
} else {
|
||||
newLayers.append(chars[currCharIndex])
|
||||
}
|
||||
}
|
||||
|
||||
for index in min(newChars.count, currentChars.count)..<currentChars.count {
|
||||
let currCharIndex = currentChars.count - 1 - index
|
||||
// remove unused
|
||||
animateOut(for: chars[currCharIndex].layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
changeIndex += 1
|
||||
}
|
||||
|
||||
for index in min(newChars.count, currentChars.count)..<newChars.count {
|
||||
let newCharIndex = newChars.count - 1 - index
|
||||
|
||||
let newLayer = AnimatedCharLayer()
|
||||
newLayer.text = newChars[newCharIndex]
|
||||
newLayer.frame = .init(x: newWidth - CGFloat(index + 1) * itemWidth, y: 100, width: itemWidth, height: 36)
|
||||
containerView.layer.addSublayer(newLayer)
|
||||
animateIn(for: newLayer.layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
newLayers.append(newLayer)
|
||||
changeIndex += 1
|
||||
}
|
||||
chars = newLayers*/
|
||||
}
|
||||
|
||||
func udpateAttributed(with newString: NSAttributedString) {
|
||||
let interItemSpacing: CGFloat = 0
|
||||
|
||||
let separatedStrings = Array(newString.string).map { String($0) }
|
||||
var range = NSRange(location: 0, length: 0)
|
||||
var newChars = [NSAttributedString]()
|
||||
for string in separatedStrings {
|
||||
range.length = string.count
|
||||
let attributedString = newString.attributedSubstring(from: range)
|
||||
newChars.append(attributedString)
|
||||
range.location += range.length
|
||||
}
|
||||
|
||||
let currentChars = chars.map { $0.attributedText ?? .init() }
|
||||
|
||||
let maxAnimationDuration: TimeInterval = 0.5
|
||||
var numberOfChanges = abs(newChars.count - currentChars.count)
|
||||
for index in 0..<min(newChars.count, currentChars.count) {
|
||||
let newCharIndex = newChars.count - 1 - index
|
||||
let currCharIndex = currentChars.count - 1 - index
|
||||
if newChars[newCharIndex] != currentChars[currCharIndex] {
|
||||
numberOfChanges += 1
|
||||
}
|
||||
}
|
||||
|
||||
let initialDuration: TimeInterval = min(0.25, maxAnimationDuration / Double(numberOfChanges)) /// 0.25
|
||||
|
||||
// let currentWidth = itemWidth * CGFloat(currentChars.count)
|
||||
// let newWidth = itemWidth * CGFloat(newChars.count)
|
||||
|
||||
let interItemDelay: TimeInterval = 0.08
|
||||
var changeIndex = 0
|
||||
|
||||
var newLayers = [AnimatedCharLayer]()
|
||||
|
||||
for index in 0..<min(newChars.count, currentChars.count) {
|
||||
let newCharIndex = newChars.count - 1 - index
|
||||
let currCharIndex = currentChars.count - 1 - index
|
||||
|
||||
if true || newChars[newCharIndex] != currentChars[currCharIndex] {
|
||||
let initialDuration = newChars[newCharIndex] != currentChars[currCharIndex] ? initialDuration : 0
|
||||
|
||||
if newChars[newCharIndex] != currentChars[currCharIndex] {
|
||||
animateOut(for: chars[currCharIndex].layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
} else {
|
||||
chars[currCharIndex].layer.removeFromSuperlayer()
|
||||
}
|
||||
let newLayer = AnimatedCharLayer()
|
||||
newLayer.attributedText = newChars[newCharIndex]
|
||||
let offset = offsetForChar(at: newCharIndex, within: newChars)/* newChars[0..<newCharIndex].reduce(0) {
|
||||
if $1.string == "," {
|
||||
return $0 + commaWidth + interItemSpacing
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}*/
|
||||
newLayer.frame = .init(x: offset/*CGFloat(newCharIndex) * (40 + interItemSpacing)*/, y: 0, width: itemWidth, height: itemWidth * 1.8)
|
||||
// newLayer.frame = .init(x: CGFloat(chars.count - 1 - index) * (40 + interItemSpacing), y: 0, width: itemWidth, height: itemWidth * 1.8)
|
||||
containerView.layer.addSublayer(newLayer)
|
||||
if newChars[newCharIndex] != currentChars[currCharIndex] {
|
||||
newLayer.layer.opacity = 0
|
||||
animateIn(for: newLayer.layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
changeIndex += 1
|
||||
}
|
||||
newLayers.append(newLayer)
|
||||
} else {
|
||||
newLayers.append(chars[currCharIndex])
|
||||
}
|
||||
}
|
||||
|
||||
for index in min(newChars.count, currentChars.count)..<currentChars.count {
|
||||
let currCharIndex = currentChars.count - 1 - index
|
||||
// remove unused
|
||||
animateOut(for: chars[currCharIndex].layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
changeIndex += 1
|
||||
}
|
||||
|
||||
for index in min(newChars.count, currentChars.count)..<newChars.count {
|
||||
let newCharIndex = newChars.count - 1 - index
|
||||
|
||||
let newLayer = AnimatedCharLayer()
|
||||
newLayer.attributedText = newChars[newCharIndex]
|
||||
|
||||
let offset = offsetForChar(at: newCharIndex, within: newChars)/*newChars[0..<newCharIndex].reduce(0) {
|
||||
if $1.string == "," {
|
||||
return $0 + commaWidth + interItemSpacing
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}*/
|
||||
newLayer.frame = .init(x: offset/*CGFloat(newCharIndex) * (40 + interItemSpacing)*/, y: 0, width: itemWidth, height: itemWidth * 1.8)
|
||||
containerView.layer.addSublayer(newLayer)
|
||||
animateIn(for: newLayer.layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
newLayers.append(newLayer)
|
||||
changeIndex += 1
|
||||
}
|
||||
let prevCount = chars.count
|
||||
chars = newLayers.reversed()
|
||||
|
||||
let countWidth = offsetForChar(at: newChars.count, within: newChars) - interItemSpacing/*newChars.reduce(-interItemSpacing) {
|
||||
if $1.string == "," {
|
||||
return $0 + commaWidth + interItemSpacing
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}*/
|
||||
if didBegin && prevCount != chars.count {
|
||||
UIView.animate(withDuration: Double(changeIndex) * initialDuration/*, delay: initialDuration * Double(changeIndex)*/) { [self] in
|
||||
containerView.frame = .init(x: self.bounds.midX - countWidth / 2, y: 0, width: countWidth, height: self.bounds.height)
|
||||
// containerView.backgroundColor = .red.withAlphaComponent(0.3)
|
||||
}
|
||||
} else {
|
||||
containerView.frame = .init(x: self.bounds.midX - countWidth / 2, y: 0, width: countWidth, height: self.bounds.height)
|
||||
didBegin = true
|
||||
}
|
||||
// self.backgroundColor = .green.withAlphaComponent(0.2)
|
||||
self.clipsToBounds = false
|
||||
}
|
||||
var didBegin = false
|
||||
func animateOut(for layer: CALayer, duration: CFTimeInterval, beginTime: CFTimeInterval) {
|
||||
// let animation = CAKeyframeAnimation()
|
||||
// animation.keyPath = "opacity"
|
||||
// animation.values = [layer.presentation()?.value(forKey: "opacity") ?? 1, 0.0]
|
||||
// animation.keyTimes = [0, 1]
|
||||
// animation.duration = duration
|
||||
// animation.beginTime = CACurrentMediaTime() + beginTime
|
||||
//// animation.isAdditive = true
|
||||
// animation.isRemovedOnCompletion = false
|
||||
// animation.fillMode = .backwards
|
||||
// layer.opacity = 0
|
||||
// layer.add(animation, forKey: "opacity")
|
||||
//
|
||||
//
|
||||
let opacityInAnimation = CABasicAnimation(keyPath: "opacity")
|
||||
opacityInAnimation.fromValue = 1
|
||||
opacityInAnimation.toValue = 0
|
||||
opacityInAnimation.duration = duration
|
||||
opacityInAnimation.beginTime = CACurrentMediaTime() + beginTime
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + duration + beginTime) {
|
||||
layer.removeFromSuperlayer()
|
||||
}
|
||||
let scaleOutAnimation = CABasicAnimation(keyPath: "transform.scale")
|
||||
scaleOutAnimation.fromValue = 1 // layer.presentation()?.value(forKey: "transform.scale") ?? 1
|
||||
scaleOutAnimation.toValue = 0.1
|
||||
scaleOutAnimation.duration = duration
|
||||
scaleOutAnimation.beginTime = CACurrentMediaTime() + beginTime
|
||||
layer.add(scaleOutAnimation, forKey: "scaleout")
|
||||
|
||||
let translate = CABasicAnimation(keyPath: "transform.translation")
|
||||
translate.fromValue = CGPoint.zero
|
||||
translate.toValue = CGPoint(x: 0, y: -layer.bounds.height * 0.3)// -layer.bounds.height + 3.0)
|
||||
translate.duration = duration
|
||||
translate.beginTime = CACurrentMediaTime() + beginTime
|
||||
layer.add(translate, forKey: "translate")
|
||||
}
|
||||
|
||||
func animateIn(for newLayer: CALayer, duration: CFTimeInterval, beginTime: CFTimeInterval) {
|
||||
newLayer.opacity = 0
|
||||
// newLayer.backgroundColor = UIColor.red.cgColor
|
||||
|
||||
let opacityInAnimation = CABasicAnimation(keyPath: "opacity")
|
||||
opacityInAnimation.fromValue = 0
|
||||
opacityInAnimation.toValue = 1
|
||||
opacityInAnimation.duration = duration
|
||||
opacityInAnimation.beginTime = CACurrentMediaTime() + beginTime
|
||||
// opacityInAnimation.isAdditive = true
|
||||
opacityInAnimation.fillMode = .backwards
|
||||
newLayer.opacity = 1
|
||||
newLayer.add(opacityInAnimation, forKey: "opacity")
|
||||
// newLayer.opacity = 1
|
||||
|
||||
let scaleOutAnimation = CABasicAnimation(keyPath: "transform.scale")
|
||||
scaleOutAnimation.fromValue = 0
|
||||
scaleOutAnimation.toValue = 1
|
||||
scaleOutAnimation.duration = duration
|
||||
scaleOutAnimation.beginTime = CACurrentMediaTime() + beginTime
|
||||
// scaleOutAnimation.isAdditive = true
|
||||
newLayer.add(scaleOutAnimation, forKey: "scalein")
|
||||
|
||||
let animation = CAKeyframeAnimation()
|
||||
animation.keyPath = "position.y"
|
||||
animation.values = [18, -6, 0]
|
||||
animation.keyTimes = [0, 0.64, 1]
|
||||
animation.timingFunction = CAMediaTimingFunction.init(name: .easeInEaseOut)
|
||||
animation.duration = duration / 0.64
|
||||
animation.beginTime = CACurrentMediaTime() + beginTime
|
||||
animation.isAdditive = true
|
||||
newLayer.add(animation, forKey: "pos")
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,451 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
private let purple = UIColor(rgb: 0x3252ef)
|
||||
private let pink = UIColor(rgb: 0xe4436c)
|
||||
|
||||
private let latePurple = UIColor(rgb: 0x974aa9)
|
||||
private let latePink = UIColor(rgb: 0xf0436c)
|
||||
|
||||
public final class AnimatedCountView: UIView {
|
||||
let countLabel = AnimatedCountLabel()
|
||||
// let titleLabel = UILabel()
|
||||
let subtitleLabel = UILabel()
|
||||
|
||||
private let foregroundView = UIView()
|
||||
private let foregroundGradientLayer = CAGradientLayer()
|
||||
private let maskingView = UIView()
|
||||
|
||||
override init(frame: CGRect = .zero) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.foregroundGradientLayer.type = .radial
|
||||
self.foregroundGradientLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor]
|
||||
self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0]
|
||||
self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
|
||||
self.foregroundView.mask = self.maskingView
|
||||
self.foregroundView.layer.addSublayer(self.foregroundGradientLayer)
|
||||
|
||||
self.addSubview(self.foregroundView)
|
||||
// self.addSubview(self.titleLabel)
|
||||
self.addSubview(self.subtitleLabel)
|
||||
|
||||
self.maskingView.addSubview(countLabel)
|
||||
countLabel.clipsToBounds = false
|
||||
subtitleLabel.textAlignment = .center
|
||||
self.clipsToBounds = false
|
||||
// self.backgroundColor = UIColor.white.withAlphaComponent(0.1)
|
||||
}
|
||||
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.foregroundView.frame = CGRect(origin: CGPoint.zero, size: bounds.size)// .insetBy(dx: -40, dy: -40)
|
||||
self.foregroundGradientLayer.frame = CGRect(origin: .zero, size: bounds.size).insetBy(dx: -60, dy: -60)
|
||||
self.maskingView.frame = CGRect(origin: .zero, size: bounds.size)
|
||||
countLabel.frame = CGRect(origin: .zero, size: bounds.size)
|
||||
subtitleLabel.frame = .init(x: bounds.midX - subtitleLabel.intrinsicContentSize.width / 2 - 10, y: subtitleLabel.text == "No viewers" ? bounds.midY - 10 : bounds.height - 6, width: subtitleLabel.intrinsicContentSize.width + 20, height: 20)
|
||||
}
|
||||
|
||||
func update(countString: String, subtitle: String) {
|
||||
self.setupGradientAnimations()
|
||||
|
||||
let text: String = countString// presentationStringsFormattedNumber(Int32(count), ",")
|
||||
|
||||
// self.titleNode.attributedText = NSAttributedString(string: "", font: Font.with(size: 23.0, design: .round, weight: .semibold, traits: []), textColor: .white)
|
||||
// let titleSize = self.titleNode.updateLayout(size)
|
||||
// self.titleNode.frame = CGRect(x: floor((size.width - titleSize.width) / 2.0), y: 48.0, width: titleSize.width, height: titleSize.height)
|
||||
// if CGFloat(text.count * 40) < bounds.width - 32 {
|
||||
// self.countLabel.attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 60, weight: .semibold)])
|
||||
// } else {
|
||||
// self.countLabel.attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 54, weight: .semibold)])
|
||||
// }
|
||||
self.countLabel.attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 60, weight: .semibold)])
|
||||
// self.countLabel.attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 60, weight: .semibold)])
|
||||
// var timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height))
|
||||
// if timerSize.width > size.width - 32.0 {
|
||||
// self.timerNode.attributedText = NSAttributedString(string: text, font: Font.with(size: 60.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)
|
||||
// timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height))
|
||||
// }
|
||||
|
||||
// self.timerNode.frame = CGRect(x: floor((size.width - timerSize.width) / 2.0), y: 78.0, width: timerSize.width, height: timerSize.height)
|
||||
|
||||
self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, attributes: [.font: UIFont.systemFont(ofSize: 16, weight: .semibold)])
|
||||
self.subtitleLabel.isHidden = subtitle.isEmpty
|
||||
// let subtitleSize = self.subtitleNode.updateLayout(size)
|
||||
// self.subtitleNode.frame = CGRect(x: floor((size.width - subtitleSize.width) / 2.0), y: 164.0, width: subtitleSize.width, height: subtitleSize.height)
|
||||
|
||||
// self.foregroundView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
// self.setNeedsLayout()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setupGradientAnimations() {
|
||||
if let _ = self.foregroundGradientLayer.animation(forKey: "movement") {
|
||||
} else {
|
||||
let previousValue = self.foregroundGradientLayer.startPoint
|
||||
let newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedCharLayer: CATextLayer {
|
||||
var text: String? {
|
||||
get {
|
||||
self.string as? String ?? (self.string as? NSAttributedString)?.string
|
||||
}
|
||||
set {
|
||||
self.string = newValue
|
||||
}
|
||||
}
|
||||
var attributedText: NSAttributedString? {
|
||||
get {
|
||||
self.string as? NSAttributedString //?? (self.string as? String).map { NSAttributed.init
|
||||
}
|
||||
set {
|
||||
self.string = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var layer: CALayer { self }
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
self.contentsScale = UIScreen.main.scale
|
||||
self.masksToBounds = false
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
self.contentsScale = UIScreen.main.scale
|
||||
self.masksToBounds = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedCountLabel: UILabel {
|
||||
override var text: String? {
|
||||
get {
|
||||
chars.reduce("") { $0 + ($1.text ?? "") }
|
||||
}
|
||||
set {
|
||||
// update(with: newValue ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
override var attributedText: NSAttributedString? {
|
||||
get {
|
||||
let string = NSMutableAttributedString()
|
||||
for char in chars {
|
||||
string.append(char.attributedText ?? NSAttributedString())
|
||||
}
|
||||
return string
|
||||
}
|
||||
set {
|
||||
udpateAttributed(with: newValue ?? NSAttributedString())
|
||||
}
|
||||
}
|
||||
|
||||
private var chars = [AnimatedCharLayer]()
|
||||
private let containerView = UIView()
|
||||
|
||||
override init(frame: CGRect = .zero) {
|
||||
super.init(frame: frame)
|
||||
containerView.clipsToBounds = false
|
||||
addSubview(containerView)
|
||||
self.clipsToBounds = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
var itemWidth: CGFloat { 36 }
|
||||
var commaWidthForSpacing: CGFloat { 8 }
|
||||
var commaFrameWidth: CGFloat { 36 }
|
||||
var interItemSpacing: CGFloat { 0 }
|
||||
var didBegin = false
|
||||
|
||||
private func offsetForChar(at index: Int, within characters: [NSAttributedString]? = nil) -> CGFloat {
|
||||
if let characters {
|
||||
var offset = characters[0..<index].reduce(0) {
|
||||
if $1.string == "," {
|
||||
return $0 + commaWidthForSpacing + interItemSpacing
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}
|
||||
if characters.count > index && characters[index].string == "," {
|
||||
offset -= 4
|
||||
}
|
||||
return offset
|
||||
} else {
|
||||
var offset = self.chars[0..<index].reduce(0) {
|
||||
if $1.attributedText?.string == "," {
|
||||
return $0 + commaWidthForSpacing + interItemSpacing
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}
|
||||
if self.chars.count > index && self.chars[index].attributedText?.string == "," {
|
||||
offset -= 4
|
||||
}
|
||||
return offset
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
let countWidth = offsetForChar(at: chars.count) /*chars.reduce(0) {
|
||||
if $1.attributedText?.string == "," {
|
||||
return $0 + commaWidth + interItemSpacing
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}*/ - interItemSpacing
|
||||
|
||||
containerView.frame = .init(x: bounds.midX - countWidth / 2, y: 0, width: countWidth, height: bounds.height)
|
||||
chars.enumerated().forEach { (index, char) in
|
||||
let offset = offsetForChar(at: index)
|
||||
// char.frame.size.width = char.attributedText?.string == "," ? commaFrameWidth : itemWidth
|
||||
char.frame.origin.x = offset
|
||||
// char.frame.origin.x = CGFloat(chars.count - 1 - index) * (40 + interItemSpacing)
|
||||
char.frame.origin.y = 0
|
||||
}
|
||||
}
|
||||
|
||||
func udpateAttributed(with newString: NSAttributedString) {
|
||||
let interItemSpacing: CGFloat = 0
|
||||
|
||||
let separatedStrings = Array(newString.string).map { String($0) }
|
||||
var range = NSRange(location: 0, length: 0)
|
||||
var newChars = [NSAttributedString]()
|
||||
for string in separatedStrings {
|
||||
range.length = string.count
|
||||
let attributedString = newString.attributedSubstring(from: range)
|
||||
newChars.append(attributedString)
|
||||
range.location += range.length
|
||||
}
|
||||
|
||||
let currentChars = chars.map { $0.attributedText ?? .init() }
|
||||
|
||||
let maxAnimationDuration: TimeInterval = 0.5
|
||||
var numberOfChanges = abs(newChars.count - currentChars.count)
|
||||
for index in 0..<min(newChars.count, currentChars.count) {
|
||||
let newCharIndex = newChars.count - 1 - index
|
||||
let currCharIndex = currentChars.count - 1 - index
|
||||
if newChars[newCharIndex] != currentChars[currCharIndex] {
|
||||
numberOfChanges += 1
|
||||
}
|
||||
}
|
||||
|
||||
let initialDuration: TimeInterval = min(maxAnimationDuration / 2, maxAnimationDuration / Double(numberOfChanges)) /// 0.25
|
||||
|
||||
// let currentWidth = itemWidth * CGFloat(currentChars.count)
|
||||
// let newWidth = itemWidth * CGFloat(newChars.count)
|
||||
|
||||
let interItemDelay: TimeInterval = 0.08
|
||||
var changeIndex = 0
|
||||
|
||||
var newLayers = [AnimatedCharLayer]()
|
||||
|
||||
for index in 0..<min(newChars.count, currentChars.count) {
|
||||
let newCharIndex = newChars.count - 1 - index
|
||||
let currCharIndex = currentChars.count - 1 - index
|
||||
|
||||
if true || newChars[newCharIndex] != currentChars[currCharIndex] {
|
||||
// if newChars[newCharIndex].string != "," {
|
||||
// continue
|
||||
// }
|
||||
|
||||
let initialDuration = newChars[newCharIndex] != currentChars[currCharIndex] ? initialDuration : 0
|
||||
|
||||
if newChars[newCharIndex] != currentChars[currCharIndex] {
|
||||
animateOut(for: chars[currCharIndex].layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
} else {
|
||||
chars[currCharIndex].layer.removeFromSuperlayer()
|
||||
}
|
||||
let newLayer = AnimatedCharLayer()
|
||||
newLayer.attributedText = newChars[newCharIndex]
|
||||
let offset = offsetForChar(at: newCharIndex, within: newChars)/* newChars[0..<newCharIndex].reduce(0) {
|
||||
if $1.string == "," {
|
||||
return $0 + commaWidth + interItemSpacing
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}*/
|
||||
newLayer.frame = .init(
|
||||
x: offset/*CGFloat(newCharIndex) * (40 + interItemSpacing)*/,
|
||||
y: 0,
|
||||
width: newChars[newCharIndex].string == "," ? commaFrameWidth : itemWidth,
|
||||
height: itemWidth * 1.8 + (newChars[newCharIndex].string == "," ? 4 : 0)
|
||||
)
|
||||
// newLayer.frame = .init(x: CGFloat(chars.count - 1 - index) * (40 + interItemSpacing), y: 0, width: itemWidth, height: itemWidth * 1.8)
|
||||
containerView.layer.addSublayer(newLayer)
|
||||
if newChars[newCharIndex] != currentChars[currCharIndex] {
|
||||
newLayer.layer.opacity = 0
|
||||
animateIn(for: newLayer.layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
changeIndex += 1
|
||||
}
|
||||
newLayers.append(newLayer)
|
||||
// if newChars[newCharIndex].string == "," {
|
||||
// newLayer.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.6).cgColor
|
||||
// } else {
|
||||
// newLayer.backgroundColor = UIColor.green.withAlphaComponent(0.6).cgColor
|
||||
// }
|
||||
} else {
|
||||
newLayers.append(chars[currCharIndex])
|
||||
}
|
||||
}
|
||||
|
||||
for index in min(newChars.count, currentChars.count)..<currentChars.count {
|
||||
let currCharIndex = currentChars.count - 1 - index
|
||||
// remove unused
|
||||
animateOut(for: chars[currCharIndex].layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
changeIndex += 1
|
||||
}
|
||||
|
||||
for index in min(newChars.count, currentChars.count)..<newChars.count {
|
||||
|
||||
let newCharIndex = newChars.count - 1 - index
|
||||
// if newChars[newCharIndex].string != "," {
|
||||
// continue
|
||||
// }
|
||||
let newLayer = AnimatedCharLayer()
|
||||
newLayer.attributedText = newChars[newCharIndex]
|
||||
|
||||
let offset = offsetForChar(at: newCharIndex, within: newChars)/*newChars[0..<newCharIndex].reduce(0) {
|
||||
if $1.string == "," {
|
||||
return $0 + commaWidth + interItemSpacing
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}*/
|
||||
newLayer.frame = .init(x: offset/*CGFloat(newCharIndex) * (40 + interItemSpacing)*/, y: 0, width: newChars[newCharIndex].string == "," ? commaFrameWidth : itemWidth, height: itemWidth * 1.8 + (newChars[newCharIndex].string == "," ? 4 : 0))
|
||||
containerView.layer.addSublayer(newLayer)
|
||||
animateIn(for: newLayer.layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
newLayers.append(newLayer)
|
||||
changeIndex += 1
|
||||
}
|
||||
let prevCount = chars.count
|
||||
chars = newLayers.reversed()
|
||||
|
||||
let countWidth = offsetForChar(at: newChars.count, within: newChars) - interItemSpacing/*newChars.reduce(-interItemSpacing) {
|
||||
if $1.string == "," {
|
||||
return $0 + commaWidth + interItemSpacing
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}*/
|
||||
if didBegin && prevCount != chars.count {
|
||||
UIView.animate(withDuration: Double(changeIndex) * initialDuration/*, delay: initialDuration * Double(changeIndex)*/) { [self] in
|
||||
containerView.frame = .init(x: self.bounds.midX - countWidth / 2, y: 0, width: countWidth, height: self.bounds.height)
|
||||
if countWidth > self.bounds.width {
|
||||
let scale = countWidth / self.bounds.width
|
||||
self.transform = .init(scaleX: scale, y: scale)
|
||||
} else {
|
||||
self.transform = .identity
|
||||
}
|
||||
// containerView.backgroundColor = .red.withAlphaComponent(0.3)
|
||||
}
|
||||
} else {
|
||||
containerView.frame = .init(x: self.bounds.midX - countWidth / 2, y: 0, width: countWidth, height: self.bounds.height)
|
||||
didBegin = true
|
||||
}
|
||||
// self.backgroundColor = .green.withAlphaComponent(0.2)
|
||||
self.clipsToBounds = false
|
||||
}
|
||||
func animateOut(for layer: CALayer, duration: CFTimeInterval, beginTime: CFTimeInterval) {
|
||||
// let animation = CAKeyframeAnimation()
|
||||
// animation.keyPath = "opacity"
|
||||
// animation.values = [layer.presentation()?.value(forKey: "opacity") ?? 1, 0.0]
|
||||
// animation.keyTimes = [0, 1]
|
||||
// animation.duration = duration
|
||||
// animation.beginTime = CACurrentMediaTime() + beginTime
|
||||
//// animation.isAdditive = true
|
||||
// animation.isRemovedOnCompletion = false
|
||||
// animation.fillMode = .backwards
|
||||
// layer.opacity = 0
|
||||
// layer.add(animation, forKey: "opacity")
|
||||
//
|
||||
//
|
||||
let opacityInAnimation = CABasicAnimation(keyPath: "opacity")
|
||||
opacityInAnimation.fromValue = 1
|
||||
opacityInAnimation.toValue = 0
|
||||
opacityInAnimation.duration = duration
|
||||
opacityInAnimation.beginTime = CACurrentMediaTime() + beginTime
|
||||
layer.add(opacityInAnimation, forKey: "opacity")
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: duration + beginTime, repeats: false) { timer in
|
||||
DispatchQueue.main.async { // After(deadline: .now() + duration + beginTime) {
|
||||
layer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
|
||||
let scaleOutAnimation = CABasicAnimation(keyPath: "transform.scale")
|
||||
scaleOutAnimation.fromValue = 1 // layer.presentation()?.value(forKey: "transform.scale") ?? 1
|
||||
scaleOutAnimation.toValue = 0.1
|
||||
scaleOutAnimation.duration = duration
|
||||
scaleOutAnimation.beginTime = CACurrentMediaTime() + beginTime
|
||||
layer.add(scaleOutAnimation, forKey: "scaleout")
|
||||
|
||||
let translate = CABasicAnimation(keyPath: "transform.translation")
|
||||
translate.fromValue = CGPoint.zero
|
||||
translate.toValue = CGPoint(x: 0, y: -layer.bounds.height * 0.3)// -layer.bounds.height + 3.0)
|
||||
translate.duration = duration
|
||||
translate.beginTime = CACurrentMediaTime() + beginTime
|
||||
layer.add(translate, forKey: "translate")
|
||||
}
|
||||
|
||||
func animateIn(for newLayer: CALayer, duration: CFTimeInterval, beginTime: CFTimeInterval) {
|
||||
newLayer.opacity = 0
|
||||
// newLayer.backgroundColor = UIColor.red.cgColor
|
||||
|
||||
let opacityInAnimation = CABasicAnimation(keyPath: "opacity")
|
||||
opacityInAnimation.fromValue = 0
|
||||
opacityInAnimation.toValue = 1
|
||||
opacityInAnimation.duration = duration
|
||||
opacityInAnimation.beginTime = CACurrentMediaTime() + beginTime
|
||||
// opacityInAnimation.isAdditive = true
|
||||
opacityInAnimation.fillMode = .backwards
|
||||
newLayer.opacity = 1
|
||||
newLayer.add(opacityInAnimation, forKey: "opacity")
|
||||
// newLayer.opacity = 1
|
||||
|
||||
let scaleOutAnimation = CABasicAnimation(keyPath: "transform.scale")
|
||||
scaleOutAnimation.fromValue = 0
|
||||
scaleOutAnimation.toValue = 1
|
||||
scaleOutAnimation.duration = duration
|
||||
scaleOutAnimation.beginTime = CACurrentMediaTime() + beginTime
|
||||
// scaleOutAnimation.isAdditive = true
|
||||
newLayer.add(scaleOutAnimation, forKey: "scalein")
|
||||
|
||||
let animation = CAKeyframeAnimation()
|
||||
animation.keyPath = "position.y"
|
||||
animation.values = [18, -6, 0]
|
||||
animation.keyTimes = [0, 0.64, 1]
|
||||
animation.timingFunction = CAMediaTimingFunction.init(name: .easeInEaseOut)
|
||||
animation.duration = duration / 0.64
|
||||
animation.beginTime = CACurrentMediaTime() + beginTime
|
||||
animation.isAdditive = true
|
||||
newLayer.add(animation, forKey: "pos")
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ final class StreamTitleComponent: Component {
|
||||
label.text = "LIVE"
|
||||
label.font = .systemFont(ofSize: 12, weight: .semibold)
|
||||
label.textAlignment = .center
|
||||
label.textColor = .white
|
||||
layer.addSublayer(stalledAnimatedGradient)
|
||||
self.clipsToBounds = true
|
||||
if #available(iOS 13.0, *) {
|
||||
@@ -81,14 +82,14 @@ final class StreamTitleComponent: Component {
|
||||
if !wasLive {
|
||||
// TODO: animate
|
||||
wasLive = true
|
||||
let frame = self.frame
|
||||
// let frame = self.frame
|
||||
UIView.animate(withDuration: 0.15, animations: {
|
||||
self.toggle(isLive: true)
|
||||
self.transform = .init(scaleX: 1.5, y: 1.5)
|
||||
}, completion: { _ in
|
||||
UIView.animate(withDuration: 0.15) {
|
||||
self.transform = .identity
|
||||
self.frame = frame
|
||||
// self.frame = frame
|
||||
}
|
||||
})
|
||||
return
|
||||
@@ -663,6 +664,7 @@ final class RoundGradientButtonComponent: Component {
|
||||
titleLabel.textAlignment = .center
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
titleLabel.font = .systemFont(ofSize: 13)
|
||||
titleLabel.textColor = .white
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -806,9 +808,10 @@ public final class _MediaStreamComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
var updated = false
|
||||
// TODO: remove debug
|
||||
// TODO: remove debug timer
|
||||
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
|
||||
strongSelf.infoThrottler.publish(members.totalCount/*Int.random(in: 0..<10000000)*/) { [weak strongSelf] latestCount in
|
||||
strongSelf.infoThrottler.publish(/*members.totalCount*/ Int.random(in: 0..<10000000)) { [weak strongSelf] latestCount in
|
||||
print(members.totalCount)
|
||||
guard let strongSelf = strongSelf else { return }
|
||||
var updated = false
|
||||
let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: latestCount)
|
||||
@@ -1846,6 +1849,7 @@ public final class _MediaStreamComponentController: ViewControllerComponentConta
|
||||
}
|
||||
}
|
||||
|
||||
public typealias MediaStreamComponent = _MediaStreamComponent
|
||||
public typealias MediaStreamComponentController = _MediaStreamComponentController
|
||||
|
||||
public final class Throttler<T: Hashable> {
|
||||
@@ -1880,7 +1884,10 @@ public final class Throttler<T: Hashable> {
|
||||
if lastValue == nil {
|
||||
queue.asyncAfter(deadline: .now() + duration) { [self] in
|
||||
accumulator.removeAll()
|
||||
// TODO: quick fix, replace with timer
|
||||
queue.asyncAfter(deadline: .now() + duration) { [self] in
|
||||
isThrottling = false
|
||||
}
|
||||
|
||||
guard
|
||||
let lastValue = lastValue,
|
||||
|
||||
@@ -9,7 +9,6 @@ import Display
|
||||
import ShimmerEffect
|
||||
|
||||
import TelegramCore
|
||||
|
||||
typealias MediaStreamVideoComponent = _MediaStreamVideoComponent
|
||||
|
||||
final class _MediaStreamVideoComponent: Component {
|
||||
|
||||
@@ -304,437 +304,3 @@ final class ParticipantsComponent: Component {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public final class AnimatedCountView: UIView {
|
||||
let countLabel = AnimatedCountLabel()
|
||||
// let titleLabel = UILabel()
|
||||
let subtitleLabel = UILabel()
|
||||
|
||||
private let foregroundView = UIView()
|
||||
private let foregroundGradientLayer = CAGradientLayer()
|
||||
private let maskingView = UIView()
|
||||
|
||||
override init(frame: CGRect = .zero) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.foregroundGradientLayer.type = .radial
|
||||
self.foregroundGradientLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor]
|
||||
self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0]
|
||||
self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
|
||||
self.foregroundView.mask = self.maskingView
|
||||
self.foregroundView.layer.addSublayer(self.foregroundGradientLayer)
|
||||
|
||||
self.addSubview(self.foregroundView)
|
||||
// self.addSubview(self.titleLabel)
|
||||
self.addSubview(self.subtitleLabel)
|
||||
|
||||
self.maskingView.addSubview(countLabel)
|
||||
|
||||
subtitleLabel.textAlignment = .center
|
||||
// self.backgroundColor = UIColor.white.withAlphaComponent(0.1)
|
||||
}
|
||||
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.foregroundView.frame = CGRect(origin: CGPoint.zero, size: bounds.size)// .insetBy(dx: -40, dy: -40)
|
||||
self.foregroundGradientLayer.frame = CGRect(origin: .zero, size: bounds.size).insetBy(dx: -60, dy: -60)
|
||||
self.maskingView.frame = CGRect(origin: .zero, size: bounds.size)
|
||||
countLabel.frame = CGRect(origin: .zero, size: bounds.size)
|
||||
subtitleLabel.frame = .init(x: bounds.midX - subtitleLabel.intrinsicContentSize.width / 2 - 10, y: subtitleLabel.text == "No viewers" ? bounds.midY - 10 : bounds.height - 6, width: subtitleLabel.intrinsicContentSize.width + 20, height: 20)
|
||||
}
|
||||
|
||||
func update(countString: String, subtitle: String) {
|
||||
self.setupGradientAnimations()
|
||||
|
||||
let text: String = countString// presentationStringsFormattedNumber(Int32(count), ",")
|
||||
|
||||
// self.titleNode.attributedText = NSAttributedString(string: "", font: Font.with(size: 23.0, design: .round, weight: .semibold, traits: []), textColor: .white)
|
||||
// let titleSize = self.titleNode.updateLayout(size)
|
||||
// self.titleNode.frame = CGRect(x: floor((size.width - titleSize.width) / 2.0), y: 48.0, width: titleSize.width, height: titleSize.height)
|
||||
if CGFloat(text.count * 40) < bounds.width - 32 {
|
||||
self.countLabel.attributedText = NSAttributedString(string: text, font: Font.with(size: 60.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)
|
||||
} else {
|
||||
self.countLabel.attributedText = NSAttributedString(string: text, font: Font.with(size: 54.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)
|
||||
}
|
||||
// var timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height))
|
||||
// if timerSize.width > size.width - 32.0 {
|
||||
// self.timerNode.attributedText = NSAttributedString(string: text, font: Font.with(size: 60.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)
|
||||
// timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height))
|
||||
// }
|
||||
|
||||
// self.timerNode.frame = CGRect(x: floor((size.width - timerSize.width) / 2.0), y: 78.0, width: timerSize.width, height: timerSize.height)
|
||||
|
||||
self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, font: Font.with(size: 16.0, design: .round, weight: .semibold, traits: []), textColor: .white)
|
||||
self.subtitleLabel.isHidden = subtitle.isEmpty
|
||||
// let subtitleSize = self.subtitleNode.updateLayout(size)
|
||||
// self.subtitleNode.frame = CGRect(x: floor((size.width - subtitleSize.width) / 2.0), y: 164.0, width: subtitleSize.width, height: subtitleSize.height)
|
||||
|
||||
// self.foregroundView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
// self.setNeedsLayout()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setupGradientAnimations() {
|
||||
if let _ = self.foregroundGradientLayer.animation(forKey: "movement") {
|
||||
} else {
|
||||
let previousValue = self.foregroundGradientLayer.startPoint
|
||||
let newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedCharLayer: CATextLayer {
|
||||
var text: String? {
|
||||
get {
|
||||
self.string as? String ?? (self.string as? NSAttributedString)?.string
|
||||
}
|
||||
set {
|
||||
self.string = newValue
|
||||
}
|
||||
}
|
||||
var attributedText: NSAttributedString? {
|
||||
get {
|
||||
self.string as? NSAttributedString //?? (self.string as? String).map { NSAttributed.init
|
||||
}
|
||||
set {
|
||||
self.string = newValue
|
||||
}
|
||||
}
|
||||
|
||||
var layer: CALayer { self }
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.contentsScale = UIScreen.main.scale
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
self.contentsScale = UIScreen.main.scale
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedCountLabel: UILabel {
|
||||
override var text: String? {
|
||||
get {
|
||||
chars.reduce("") { $0 + ($1.text ?? "") }
|
||||
}
|
||||
set {
|
||||
update(with: newValue ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
override var attributedText: NSAttributedString? {
|
||||
get {
|
||||
let string = NSMutableAttributedString()
|
||||
for char in chars {
|
||||
string.append(char.attributedText ?? NSAttributedString())
|
||||
}
|
||||
return string
|
||||
}
|
||||
set {
|
||||
udpateAttributed(with: newValue ?? NSAttributedString())
|
||||
}
|
||||
}
|
||||
|
||||
private var chars = [AnimatedCharLayer]()
|
||||
private let containerView = UIView()
|
||||
|
||||
override init(frame: CGRect = .zero) {
|
||||
super.init(frame: frame)
|
||||
|
||||
addSubview(containerView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
var itemWidth: CGFloat { 36 }
|
||||
var commaWidth: CGFloat { 8 }
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
let interItemSpacing: CGFloat = 0
|
||||
let countWidth = chars.reduce(0) {
|
||||
if $1.attributedText?.string == "," {
|
||||
return $0 + commaWidth
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
} - interItemSpacing
|
||||
|
||||
containerView.frame = .init(x: bounds.midX - countWidth / 2, y: 0, width: countWidth, height: bounds.height)
|
||||
chars.enumerated().forEach { (index, char) in
|
||||
let offset = chars[0..<index].reduce(0) {
|
||||
if $1.attributedText?.string == "," {
|
||||
return $0 + commaWidth
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}
|
||||
char.frame.origin.x = offset
|
||||
// char.frame.origin.x = CGFloat(chars.count - 1 - index) * (40 + interItemSpacing)
|
||||
char.frame.origin.y = 0
|
||||
}
|
||||
}
|
||||
/// Unused
|
||||
func update(with newString: String) {
|
||||
/*let itemWidth: CGFloat = 40
|
||||
let initialDuration: TimeInterval = 0.3
|
||||
let newChars = Array(newString).map { String($0) }
|
||||
let currentChars = chars.map { $0.text ?? "X" }
|
||||
|
||||
// let currentWidth = itemWidth * CGFloat(currentChars.count)
|
||||
let newWidth = itemWidth * CGFloat(newChars.count)
|
||||
|
||||
let interItemDelay: TimeInterval = 0.15
|
||||
var changeIndex = 0
|
||||
|
||||
var newLayers = [AnimatedCharLayer]()
|
||||
|
||||
for index in 0..<min(newChars.count, currentChars.count) {
|
||||
let newCharIndex = newChars.count - 1 - index
|
||||
let currCharIndex = currentChars.count - 1 - index
|
||||
|
||||
if true || newChars[newCharIndex] != currentChars[currCharIndex] {
|
||||
animateOut(for: chars[currCharIndex].layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
|
||||
let newLayer = AnimatedCharLayer()
|
||||
newLayer.text = newChars[newCharIndex]
|
||||
newLayer.frame = .init(x: newWidth - CGFloat(index + 1) * itemWidth, y: 100, width: itemWidth, height: 36)
|
||||
containerView.layer.addSublayer(newLayer)
|
||||
animateIn(for: newLayer.layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
newLayers.append(newLayer)
|
||||
changeIndex += 1
|
||||
} else {
|
||||
newLayers.append(chars[currCharIndex])
|
||||
}
|
||||
}
|
||||
|
||||
for index in min(newChars.count, currentChars.count)..<currentChars.count {
|
||||
let currCharIndex = currentChars.count - 1 - index
|
||||
// remove unused
|
||||
animateOut(for: chars[currCharIndex].layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
changeIndex += 1
|
||||
}
|
||||
|
||||
for index in min(newChars.count, currentChars.count)..<newChars.count {
|
||||
let newCharIndex = newChars.count - 1 - index
|
||||
|
||||
let newLayer = AnimatedCharLayer()
|
||||
newLayer.text = newChars[newCharIndex]
|
||||
newLayer.frame = .init(x: newWidth - CGFloat(index + 1) * itemWidth, y: 100, width: itemWidth, height: 36)
|
||||
containerView.layer.addSublayer(newLayer)
|
||||
animateIn(for: newLayer.layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
newLayers.append(newLayer)
|
||||
changeIndex += 1
|
||||
}
|
||||
chars = newLayers*/
|
||||
}
|
||||
|
||||
func udpateAttributed(with newString: NSAttributedString) {
|
||||
let interItemSpacing: CGFloat = 0
|
||||
|
||||
let separatedStrings = Array(newString.string).map { String($0) }
|
||||
var range = NSRange(location: 0, length: 0)
|
||||
var newChars = [NSAttributedString]()
|
||||
for string in separatedStrings {
|
||||
range.length = string.count
|
||||
let attributedString = newString.attributedSubstring(from: range)
|
||||
newChars.append(attributedString)
|
||||
range.location += range.length
|
||||
}
|
||||
|
||||
let currentChars = chars.map { $0.attributedText ?? .init() }
|
||||
|
||||
let maxAnimationDuration: TimeInterval = 0.5
|
||||
var numberOfChanges = abs(newChars.count - currentChars.count)
|
||||
for index in 0..<min(newChars.count, currentChars.count) {
|
||||
let newCharIndex = newChars.count - 1 - index
|
||||
let currCharIndex = currentChars.count - 1 - index
|
||||
if newChars[newCharIndex] != currentChars[currCharIndex] {
|
||||
numberOfChanges += 1
|
||||
}
|
||||
}
|
||||
|
||||
let initialDuration: TimeInterval = min(0.25, maxAnimationDuration / Double(numberOfChanges)) /// 0.25
|
||||
|
||||
// let currentWidth = itemWidth * CGFloat(currentChars.count)
|
||||
// let newWidth = itemWidth * CGFloat(newChars.count)
|
||||
|
||||
let interItemDelay: TimeInterval = 0.08
|
||||
var changeIndex = 0
|
||||
|
||||
var newLayers = [AnimatedCharLayer]()
|
||||
|
||||
for index in 0..<min(newChars.count, currentChars.count) {
|
||||
let newCharIndex = newChars.count - 1 - index
|
||||
let currCharIndex = currentChars.count - 1 - index
|
||||
|
||||
if true || newChars[newCharIndex] != currentChars[currCharIndex] {
|
||||
let initialDuration = newChars[newCharIndex] != currentChars[currCharIndex] ? initialDuration : 0
|
||||
|
||||
if newChars[newCharIndex] != currentChars[currCharIndex] {
|
||||
animateOut(for: chars[currCharIndex].layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
} else {
|
||||
chars[currCharIndex].layer.removeFromSuperlayer()
|
||||
}
|
||||
let newLayer = AnimatedCharLayer()
|
||||
newLayer.attributedText = newChars[newCharIndex]
|
||||
let offset = newChars[0..<newCharIndex].reduce(0) {
|
||||
if $1.string == "," {
|
||||
return $0 + commaWidth
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}
|
||||
newLayer.frame = .init(x: offset/*CGFloat(newCharIndex) * (40 + interItemSpacing)*/, y: 0, width: itemWidth, height: itemWidth * 1.8)
|
||||
// newLayer.frame = .init(x: CGFloat(chars.count - 1 - index) * (40 + interItemSpacing), y: 0, width: itemWidth, height: itemWidth * 1.8)
|
||||
containerView.layer.addSublayer(newLayer)
|
||||
if newChars[newCharIndex] != currentChars[currCharIndex] {
|
||||
newLayer.layer.opacity = 0
|
||||
animateIn(for: newLayer.layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
}
|
||||
newLayers.append(newLayer)
|
||||
changeIndex += 1
|
||||
} else {
|
||||
newLayers.append(chars[currCharIndex])
|
||||
}
|
||||
}
|
||||
|
||||
for index in min(newChars.count, currentChars.count)..<currentChars.count {
|
||||
let currCharIndex = currentChars.count - 1 - index
|
||||
// remove unused
|
||||
animateOut(for: chars[currCharIndex].layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
changeIndex += 1
|
||||
}
|
||||
|
||||
for index in min(newChars.count, currentChars.count)..<newChars.count {
|
||||
let newCharIndex = newChars.count - 1 - index
|
||||
|
||||
let newLayer = AnimatedCharLayer()
|
||||
newLayer.attributedText = newChars[newCharIndex]
|
||||
|
||||
let offset = newChars[0..<newCharIndex].reduce(0) {
|
||||
if $1.string == "," {
|
||||
return $0 + commaWidth
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}
|
||||
newLayer.frame = .init(x: offset/*CGFloat(newCharIndex) * (40 + interItemSpacing)*/, y: 0, width: itemWidth, height: itemWidth * 1.8)
|
||||
containerView.layer.addSublayer(newLayer)
|
||||
animateIn(for: newLayer.layer, duration: initialDuration, beginTime: TimeInterval(changeIndex) * interItemDelay)
|
||||
newLayers.append(newLayer)
|
||||
changeIndex += 1
|
||||
}
|
||||
let prevCount = chars.count
|
||||
chars = newLayers.reversed()
|
||||
|
||||
let countWidth = newChars.reduce(-interItemSpacing) {
|
||||
if $1.string == "," {
|
||||
return $0 + commaWidth
|
||||
}
|
||||
return $0 + itemWidth + interItemSpacing
|
||||
}
|
||||
if didBegin && prevCount != chars.count {
|
||||
UIView.animate(withDuration: Double(changeIndex) * initialDuration/*, delay: initialDuration * Double(changeIndex)*/) { [self] in
|
||||
containerView.frame = .init(x: self.bounds.midX - countWidth / 2, y: 0, width: countWidth, height: self.bounds.height)
|
||||
// containerView.backgroundColor = .red.withAlphaComponent(0.3)
|
||||
}
|
||||
} else {
|
||||
containerView.frame = .init(x: self.bounds.midX - countWidth / 2, y: 0, width: countWidth, height: self.bounds.height)
|
||||
didBegin = true
|
||||
}
|
||||
// self.backgroundColor = .green.withAlphaComponent(0.2)
|
||||
self.clipsToBounds = false
|
||||
}
|
||||
var didBegin = false
|
||||
func animateOut(for layer: CALayer, duration: CFTimeInterval, beginTime: CFTimeInterval) {
|
||||
let animation = CAKeyframeAnimation()
|
||||
animation.keyPath = "opacity"
|
||||
animation.values = [layer.presentation()?.value(forKey: "opacity") ?? 1, 0.0]
|
||||
animation.keyTimes = [0, 1]
|
||||
animation.duration = duration
|
||||
animation.beginTime = CACurrentMediaTime() + beginTime
|
||||
// animation.isAdditive = true
|
||||
animation.isRemovedOnCompletion = false
|
||||
animation.fillMode = .backwards
|
||||
layer.opacity = 0
|
||||
layer.add(animation, forKey: "opacity")
|
||||
//
|
||||
//
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + duration + beginTime) {
|
||||
layer.removeFromSuperlayer()
|
||||
}
|
||||
let scaleOutAnimation = CABasicAnimation(keyPath: "transform.scale")
|
||||
scaleOutAnimation.fromValue = layer.presentation()?.value(forKey: "transform.scale") ?? 1
|
||||
scaleOutAnimation.toValue = 0.1
|
||||
scaleOutAnimation.duration = duration
|
||||
scaleOutAnimation.beginTime = CACurrentMediaTime() + beginTime
|
||||
layer.add(scaleOutAnimation, forKey: "scaleout")
|
||||
|
||||
let translate = CABasicAnimation(keyPath: "transform.translation")
|
||||
translate.fromValue = CGPoint.zero
|
||||
translate.toValue = CGPoint(x: 0, y: -layer.bounds.height * 0.3)// -layer.bounds.height + 3.0)
|
||||
translate.duration = duration
|
||||
translate.beginTime = CACurrentMediaTime() + beginTime
|
||||
layer.add(translate, forKey: "translate")
|
||||
}
|
||||
|
||||
func animateIn(for newLayer: CALayer, duration: CFTimeInterval, beginTime: CFTimeInterval) {
|
||||
newLayer.opacity = 0
|
||||
// newLayer.backgroundColor = UIColor.red.cgColor
|
||||
|
||||
let opacityInAnimation = CABasicAnimation(keyPath: "opacity")
|
||||
opacityInAnimation.fromValue = 0
|
||||
opacityInAnimation.toValue = 1
|
||||
opacityInAnimation.duration = duration
|
||||
opacityInAnimation.beginTime = CACurrentMediaTime() + beginTime
|
||||
// opacityInAnimation.isAdditive = true
|
||||
opacityInAnimation.fillMode = .backwards
|
||||
newLayer.opacity = 1
|
||||
newLayer.add(opacityInAnimation, forKey: "opacity")
|
||||
// newLayer.opacity = 1
|
||||
|
||||
let scaleOutAnimation = CABasicAnimation(keyPath: "transform.scale")
|
||||
scaleOutAnimation.fromValue = 0
|
||||
scaleOutAnimation.toValue = 1
|
||||
scaleOutAnimation.duration = duration
|
||||
scaleOutAnimation.beginTime = CACurrentMediaTime() + beginTime
|
||||
// scaleOutAnimation.isAdditive = true
|
||||
newLayer.add(scaleOutAnimation, forKey: "scalein")
|
||||
|
||||
let animation = CAKeyframeAnimation()
|
||||
animation.keyPath = "position.y"
|
||||
animation.values = [18, -6, 0]
|
||||
animation.keyTimes = [0, 0.64, 1]
|
||||
animation.timingFunction = CAMediaTimingFunction.init(name: .easeInEaseOut)
|
||||
animation.duration = duration / 0.64
|
||||
animation.beginTime = CACurrentMediaTime() + beginTime
|
||||
animation.isAdditive = true
|
||||
newLayer.add(animation, forKey: "pos")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user