import Foundation import UIKit import Display import ComponentFlow import PagerComponent import TelegramPresentationData import TelegramCore import Postbox import AnimationCache import MultiAnimationRenderer import AccountContext import AsyncDisplayKit import ComponentDisplayAdapters import LottieAnimationComponent import EmojiStatusComponent import LottieComponent import AudioToolbox import SwiftSignalKit import GZip import RLottieBinding import AppBundle import Lottie private final class LottieDirectContent: LottieComponent.Content { let path: String init(path: String) { self.path = path } override var frameRange: Range { return 0.0 ..< 1.0 } override func isEqual(to other: LottieComponent.Content) -> Bool { guard let other = other as? LottieDirectContent else { return false } if self.path != other.path { return false } return true } override func load(_ f: @escaping (LottieComponent.ContentData) -> Void) -> Disposable { if let data = try? Data(contentsOf: URL(fileURLWithPath: self.path)) { let result = TGGUnzipData(data, 2 * 1024 * 1024) ?? data f(.animation(data: result, cacheKey: nil)) } return EmptyDisposable } } private protocol EmojiSearchStatusAnimationState { var content: EmojiSearchStatusComponent.ContentState { get } var image: UIImage? { get } var isCompleted: Bool { get } func advanceIfNeeded() func updateImage() } final class EmojiSearchStatusComponent: Component { enum Content: Equatable { case search case progress case results } let theme: PresentationTheme let forceNeedsVibrancy: Bool let strings: PresentationStrings let useOpaqueTheme: Bool let content: Content init( theme: PresentationTheme, forceNeedsVibrancy: Bool, strings: PresentationStrings, useOpaqueTheme: Bool, content: Content ) { self.theme = theme self.forceNeedsVibrancy = forceNeedsVibrancy self.strings = strings self.useOpaqueTheme = useOpaqueTheme self.content = content } static func ==(lhs: EmojiSearchStatusComponent, rhs: EmojiSearchStatusComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.forceNeedsVibrancy != rhs.forceNeedsVibrancy { return false } if lhs.strings !== rhs.strings { return false } if lhs.useOpaqueTheme != rhs.useOpaqueTheme { return false } if lhs.content != rhs.content { return false } return true } fileprivate enum ContentState { case search case searchToProgress case progress case results init(content: Content) { switch content { case .search: self = .search case .progress: self = .progress case .results: self = .results } } var content: Content { switch self { case .search: return .search case .searchToProgress, .progress: return .progress case .results: return .results } } var automaticNextState: ContentState? { switch self { case .searchToProgress: return .progress default: return nil } } } private final class LottieAnimationState: EmojiSearchStatusAnimationState { let content: ContentState private let animationInstance: LottieInstance private var currentFrameStartTime: Double? private var currentFrame: Int = 0 private let frameRange: ClosedRange? private(set) var image: UIImage? private(set) var previousAnimationState: EmojiSearchStatusAnimationState? private(set) var isCompleted: Bool = false var displaySize: CGSize { didSet { if self.displaySize != oldValue { self.image = nil } } } init?(content: ContentState, data: Data, displaySize: CGSize, frameRange: ClosedRange?, previousAnimationState: EmojiSearchStatusAnimationState?) { guard let animationInstance = LottieInstance(data: data, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else { return nil } self.content = content self.animationInstance = animationInstance self.displaySize = displaySize self.frameRange = frameRange self.previousAnimationState = previousAnimationState if let frameRange { self.currentFrame = frameRange.lowerBound } } func advanceIfNeeded() { if let previousAnimationState = self.previousAnimationState { previousAnimationState.advanceIfNeeded() if previousAnimationState.isCompleted { self.previousAnimationState = nil } if previousAnimationState.image == nil { self.image = nil } } if self.isCompleted { return } if let frameRange = self.frameRange { if frameRange.lowerBound == frameRange.upperBound { self.isCompleted = true return } } let timestamp = CACurrentMediaTime() guard let currentFrameStartTime = self.currentFrameStartTime else { currentFrameStartTime = timestamp return } let secondsPerFrame: Double if animationInstance.frameRate == 0 { secondsPerFrame = 1.0 / 60.0 } else { secondsPerFrame = 1.0 / Double(animationInstance.frameRate) } if currentFrameStartTime + secondsPerFrame * 0.9 <= timestamp { self.currentFrame += 1 let maxFrame: Int if let frameRange = self.frameRange { maxFrame = frameRange.upperBound } else { maxFrame = Int(animationInstance.frameCount) - 1 } if self.currentFrame >= maxFrame { self.currentFrame = maxFrame self.isCompleted = true } else { self.currentFrameStartTime = timestamp self.image = nil } } } func updateImage() { guard let frameContext = DrawingContext(size: self.displaySize, scale: 1.0, opaque: false, clear: true) else { return } self.animationInstance.renderFrame(with: Int32(self.currentFrame % Int(self.animationInstance.frameCount)), into: frameContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(self.displaySize.width), height: Int32(self.displaySize.height), bytesPerRow: Int32(frameContext.bytesPerRow)) if let previousAnimationState = self.previousAnimationState as? ProgressAnimationState { guard let context = DrawingContext(size: self.displaySize, scale: 1.0, opaque: false, clear: true) else { return } if previousAnimationState.image == nil { previousAnimationState.updateImage() } if let frameImage = frameContext.generateImage()?.cgImage, let cgImage = previousAnimationState.image?.cgImage { context.withFlippedContext { c in c.draw(cgImage, in: CGRect(origin: CGPoint(), size: context.size)) c.translateBy(x: self.displaySize.width * 0.5, y: self.displaySize.height * 0.5) c.rotate(by: previousAnimationState.currentRotationAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2.0)) c.translateBy(x: -self.displaySize.width * 0.5, y: -self.displaySize.height * 0.5) c.draw(frameImage, in: CGRect(origin: CGPoint(), size: context.size)) } } self.image = context.generateImage()?.withRenderingMode(.alwaysTemplate) } else { self.image = frameContext.generateImage()?.withRenderingMode(.alwaysTemplate) } } } private final class ProgressAnimationState: EmojiSearchStatusAnimationState { let content: ContentState private var currentFrameStartTime: Double? private var currentOffset: CGFloat private(set) var currentRotationAngle: CGFloat private var lastStageStartOffset: CGFloat? private var lastStageRotationAngle: CGFloat? private(set) var image: UIImage? var shouldComplete: Bool = false { didSet { if self.shouldComplete != oldValue && self.shouldComplete { self.lastStageStartOffset = self.currentOffset self.currentRotationAngle = self.currentRotationAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2.0) self.lastStageRotationAngle = self.currentRotationAngle } } } private(set) var isCompleted: Bool = false var displaySize: CGSize { didSet { if self.displaySize != oldValue { self.image = nil } } } init(content: ContentState, displaySize: CGSize) { self.content = content self.displaySize = displaySize self.currentOffset = 0.0 self.currentRotationAngle = 0.0 } func advanceIfNeeded() { if self.isCompleted { return } let timestamp = CACurrentMediaTime() guard let currentFrameStartTime = self.currentFrameStartTime else { currentFrameStartTime = timestamp return } let secondsPerFrame: Double = 1.0 / 60.0 let offsetVelocity: CGFloat = CGFloat.pi * 3.0 let maxOffset: CGFloat = CGFloat.pi * 2.0 - CGFloat.pi * 1.0 / 1.4 let rotationVelocity: CGFloat = CGFloat.pi * 3.0 * 1.0 if currentFrameStartTime + secondsPerFrame * 0.9 <= timestamp { if let lastStageStartOffset = self.lastStageStartOffset { let lastStageRemainingOffset: CGFloat = CGFloat.pi * 2.0 - lastStageStartOffset let lastStageRemainingVelocity: CGFloat = lastStageRemainingOffset / 9.0 * 60.0 self.currentOffset = min(CGFloat.pi * 2.0, self.currentOffset + lastStageRemainingVelocity * secondsPerFrame) } else if self.shouldComplete { self.currentOffset = min(CGFloat.pi * 2.0, self.currentOffset + offsetVelocity * secondsPerFrame) if self.currentOffset == CGFloat.pi * 2.0 { self.isCompleted = true } } else { self.currentOffset = min(maxOffset, self.currentOffset + offsetVelocity * secondsPerFrame) } if let lastStageRotationAngle = self.lastStageRotationAngle { let _ = lastStageRotationAngle /*let lastStageRemainingAngle: CGFloat = CGFloat.pi * 2.0 + lastStageRotationAngle let lastStageRemainingAngleVelocity: CGFloat = lastStageRemainingAngle / 12.0 * 60.0 self.currentRotationAngle = max(-CGFloat.pi * 2.0, self.currentRotationAngle - lastStageRemainingAngleVelocity * secondsPerFrame)*/ self.currentRotationAngle = max(-CGFloat.pi * 2.0, self.currentRotationAngle - rotationVelocity * secondsPerFrame) } else { self.currentRotationAngle -= rotationVelocity * secondsPerFrame } if self.lastStageStartOffset != nil && self.lastStageRotationAngle != nil { if self.currentOffset == CGFloat.pi * 2.0 && self.currentRotationAngle == -CGFloat.pi * 2.0 { self.isCompleted = true } } self.currentFrameStartTime = timestamp self.image = nil } } func updateImage() { guard let context = DrawingContext(size: self.displaySize, scale: 1.0, opaque: false, clear: true) else { return } context.withFlippedContext { c in c.setStrokeColor(UIColor.white.cgColor) c.setLineCap(.round) let lineWidth: CGFloat = 1.33 * UIScreenScale let fullDiameter = 20.0 * UIScreenScale c.setLineWidth(lineWidth) let startAngle: CGFloat = 0.0 let endAngle: CGFloat = startAngle + (CGFloat.pi * 2.0 - self.currentOffset.truncatingRemainder(dividingBy: CGFloat.pi * 2.0)) c.translateBy(x: self.displaySize.width * 0.5, y: self.displaySize.height * 0.5) c.rotate(by: self.currentRotationAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2.0)) c.translateBy(x: -self.displaySize.width * 0.5, y: -self.displaySize.height * 0.5) if self.currentOffset != CGFloat.pi * 2.0 { c.addArc(center: CGPoint(x: self.displaySize.width * 0.5, y: self.displaySize.height * 0.5), radius: fullDiameter * 0.5 - lineWidth, startAngle: startAngle, endAngle: endAngle, clockwise: false) c.strokePath() } } self.image = context.generateImage()?.withRenderingMode(.alwaysTemplate) } } final class View: UIView { private var component: EmojiSearchStatusComponent? private var disappearingAnimationStates: [(UIImageView, UIImageView, EmojiSearchStatusAnimationState)] = [] private var currentAnimationState: EmojiSearchStatusAnimationState? private var pendingContent: Content? private var displaySize: CGSize? private var displayLink: SharedDisplayLinkDriver.Link? public let contentView: UIImageView public let tintContainerView: UIView public let tintContentView: UIImageView override init(frame: CGRect) { self.contentView = UIImageView() self.tintContainerView = UIView() self.tintContentView = UIImageView() super.init(frame: frame) self.addSubview(self.contentView) self.tintContainerView.isUserInteractionEnabled = false self.tintContainerView.addSubview(self.tintContentView) //self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { } } func update(component: EmojiSearchStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.component = component let displaySize = CGSize(width: availableSize.width * UIScreenScale, height: availableSize.height * UIScreenScale) self.displaySize = displaySize let overlayColor: UIColor if component.theme.overallDarkAppearance && component.forceNeedsVibrancy { overlayColor = component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor.withMultipliedAlpha(0.3) } else { overlayColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor } let baseColor: UIColor = .black if self.contentView.tintColor != overlayColor { self.contentView.tintColor = overlayColor } if self.tintContentView.tintColor != baseColor { self.tintContentView.tintColor = baseColor } let currentTargetContent = self.pendingContent ?? self.currentAnimationState?.content.content if component.content != currentTargetContent { var canSwitchNow = false if let currentAnimationState = self.currentAnimationState { if currentAnimationState.isCompleted { canSwitchNow = true } else if let _ = currentAnimationState as? ProgressAnimationState { canSwitchNow = true } } else { canSwitchNow = true } if canSwitchNow { /*if let currentAnimationState = self.currentAnimationState, case .search = currentAnimationState.content, case .progress = component.content { self.switchToContent(content: .searchToProgress) } else {*/ self.switchToContent(content: ContentState(content: component.content)) //} } else { self.pendingContent = component.content } } self.updateAnimation() transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: availableSize)) transition.setFrame(view: self.tintContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) return availableSize } private func switchToContent(content: ContentState) { guard let displaySize = self.displaySize else { return } enum FrameRangeValue { case index(Int) case marker(String) case end } var name: String? var isJson = false var frameRange: (FrameRangeValue, FrameRangeValue)? var manualTransition = false var previousAnimationState: EmojiSearchStatusAnimationState? previousAnimationState = nil let manualPreviousState = self.currentAnimationState if let currentAnimationState = self.currentAnimationState { switch currentAnimationState.content { case .search: switch content { case .search: name = "emoji_search_to_arrow" frameRange = (.index(0), .index(0)) case .searchToProgress: name = "emoji_search_to_progress" isJson = true //frameRange = (.index(0), .marker("{\r\"name\":\"Search to Progress\"\r}")) frameRange = (.index(0), .index(7)) case .progress: manualTransition = true break case .results: name = "emoji_search_to_arrow" } case .searchToProgress: switch content { case .search: manualTransition = true name = "emoji_search_to_arrow" frameRange = (.index(0), .index(0)) case .searchToProgress: break case .progress: break case .results: manualTransition = true name = "emoji_arrow_to_search" frameRange = (.index(0), .index(0)) } case .progress: switch content { case .search: manualTransition = true name = "emoji_search_to_arrow" frameRange = (.index(0), .index(0)) case .searchToProgress: break case .progress: break case .results: manualTransition = true name = "emoji_arrow_to_search" frameRange = (.index(0), .index(0)) } /*switch content { case .search: manualTransition = true name = "emoji_search_to_arrow" frameRange = (.index(0), .index(0)) case .searchToProgress: name = "emoji_search_to_progress" isJson = true case .progress: break case .results: name = "emoji_search_to_progress" isJson = true //frameRange = (.marker("{\n\"name\":\"Progress to Arrow\"\n}"), .end) frameRange = (.index(87), .end) previousAnimationState = currentAnimationState (currentAnimationState as? ProgressAnimationState)?.shouldComplete = true /*name = "emoji_arrow_to_search" frameRange = (.index(0), .index(0))*/ }*/ case .results: switch content { case .search: name = "emoji_arrow_to_search" case .searchToProgress: name = "emoji_search_to_progress" isJson = true case .progress: manualTransition = true case .results: name = "emoji_arrow_to_search" frameRange = (.index(0), .index(0)) } } } else { switch content { case .search: name = "emoji_search_to_arrow" frameRange = (.index(0), .index(0)) case .searchToProgress: name = "emoji_search_to_progress" isJson = true case .progress: break case .results: name = "emoji_arrow_to_search" frameRange = (.index(0), .index(0)) } } if manualTransition, let manualPreviousState { let tempImageView = UIImageView() tempImageView.image = self.contentView.image tempImageView.frame = self.contentView.frame tempImageView.tintColor = self.contentView.tintColor self.contentView.superview?.insertSubview(tempImageView, aboveSubview: self.contentView) let tempTintImageView = UIImageView() tempTintImageView.image = self.tintContentView.image tempTintImageView.frame = self.tintContentView.frame tempTintImageView.tintColor = self.tintContentView.tintColor self.tintContentView.superview?.insertSubview(tempTintImageView, aboveSubview: self.tintContentView) self.disappearingAnimationStates.append((tempImageView, tempTintImageView, manualPreviousState)) let minScale: CGFloat = 0.6 tempImageView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak self, weak tempImageView] _ in if let self, let tempImageView { tempImageView.removeFromSuperview() self.disappearingAnimationStates.removeAll(where: { $0.0 === tempImageView }) } }) tempImageView.layer.animateScale(from: 1.0, to: minScale, duration: 0.18, removeOnCompletion: false) tempTintImageView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak self, weak tempTintImageView] _ in if let self, let tempTintImageView { tempImageView.removeFromSuperview() self.disappearingAnimationStates.removeAll(where: { $0.1 === tempTintImageView }) } }) tempTintImageView.layer.animateScale(from: 1.0, to: minScale, duration: 0.18, removeOnCompletion: false) self.contentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) self.contentView.layer.animateScale(from: minScale, to: 1.0, duration: 0.18) self.tintContentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) self.tintContentView.layer.animateScale(from: minScale, to: 1.0, duration: 0.18) } if case .progress = content { self.currentAnimationState = ProgressAnimationState(content: content, displaySize: displaySize) } else if let name, let data = getAppBundle().path(forResource: name, ofType: isJson ? "json" : "tgs").flatMap({ return try? Data(contentsOf: URL(fileURLWithPath: $0)) }).flatMap({ data -> Data in if isJson { return data } return TGGUnzipData(data, 2 * 1024 * 1024) ?? data }) { var resolvedFrameRange: ClosedRange? if let frameRange { var hasMarkers = false if case .marker = frameRange.0 { hasMarkers = true } if case .marker = frameRange.1 { hasMarkers = true } if case .end = frameRange.0 { hasMarkers = true } if case .end = frameRange.1 { hasMarkers = true } var resolvedLowerBound: Int = 0 var resolvedUpperBound: Int = 0 if case let .index(index) = frameRange.0 { resolvedLowerBound = index } if case let .index(index) = frameRange.1 { resolvedUpperBound = index } if hasMarkers, let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let animation = try? Animation(dictionary: json) { let numFrames = animation.endFrame - animation.startFrame if case let .marker(markerName) = frameRange.0 { if let value = animation.progressTime(forMarker: markerName) { resolvedLowerBound = Int(value * numFrames) } } if case .end = frameRange.0 { resolvedLowerBound = Int(numFrames) - 1 } if case let .marker(markerName) = frameRange.1 { if let value = animation.progressTime(forMarker: markerName) { resolvedUpperBound = Int(round(value * numFrames)) } } if case .end = frameRange.1 { resolvedUpperBound = Int(numFrames) - 1 } } resolvedFrameRange = resolvedLowerBound ... max(resolvedLowerBound, resolvedUpperBound) } self.currentAnimationState = LottieAnimationState(content: content, data: data, displaySize: displaySize, frameRange: resolvedFrameRange, previousAnimationState: previousAnimationState) } else { self.currentAnimationState = nil } } private func updateAnimation() { var needsAnimation = false for (tempView, tempTintView, animationState) in self.disappearingAnimationStates { animationState.advanceIfNeeded() if animationState.image == nil { animationState.updateImage() } tempView.image = animationState.image tempTintView.image = animationState.image needsAnimation = true } while true { if let currentAnimationState = self.currentAnimationState { if self.pendingContent != nil, let currentAnimationState = currentAnimationState as? ProgressAnimationState { currentAnimationState.shouldComplete = true } currentAnimationState.advanceIfNeeded() if currentAnimationState.image == nil { currentAnimationState.updateImage() } if let previousAnimationState = (currentAnimationState as? LottieAnimationState)?.previousAnimationState, !previousAnimationState.isCompleted { needsAnimation = true } if currentAnimationState.isCompleted { if self.pendingContent == nil, let automaticNextState = currentAnimationState.content.automaticNextState { self.switchToContent(content: automaticNextState) } else if let pendingContent = self.pendingContent { self.pendingContent = nil self.switchToContent(content: ContentState(content: pendingContent)) } else { break } } else { needsAnimation = true break } } else { break } } if let currentAnimationState = self.currentAnimationState { if currentAnimationState.image == nil { currentAnimationState.updateImage() } if let image = currentAnimationState.image { self.contentView.image = image self.tintContentView.image = image } } if needsAnimation { if self.displayLink == nil { var counter = 0 self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in counter += 1 if counter % 1 == 0 { self?.updateAnimation() } } } } else { if let displayLink = self.displayLink { self.displayLink = nil displayLink.invalidate() } } } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }