diff --git a/submodules/TelegramCallsUI/Sources/Components/AnimatedCountView.swift b/submodules/TelegramCallsUI/Sources/Components/AnimatedCountView.swift new file mode 100644 index 0000000000..70b3a9cbbb --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/Components/AnimatedCountView.swift @@ -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.. 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 && characters[index].string == "," { + offset -= 4 + } + return offset + } else { + var offset = self.chars[0.. 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.. 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") + } +} diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 30bf2b9a48..d1ff3a84a8 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -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 { @@ -1880,7 +1884,10 @@ public final class Throttler { if lastValue == nil { queue.asyncAfter(deadline: .now() + duration) { [self] in accumulator.removeAll() - isThrottling = false + // TODO: quick fix, replace with timer + queue.asyncAfter(deadline: .now() + duration) { [self] in + isThrottling = false + } guard let lastValue = lastValue, diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index 2b1781d797..ec41f4b8dd 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -9,7 +9,6 @@ import Display import ShimmerEffect import TelegramCore - typealias MediaStreamVideoComponent = _MediaStreamVideoComponent final class _MediaStreamVideoComponent: Component { diff --git a/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift b/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift index 6820f71989..436bb7a054 100644 --- a/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift @@ -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..