mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
533 lines
19 KiB
Swift
533 lines
19 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import SwiftSignalKit
|
|
import Display
|
|
import AnimationCache
|
|
import Accelerate
|
|
|
|
public protocol MultiAnimationRenderer: AnyObject {
|
|
func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable
|
|
func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool
|
|
func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable
|
|
}
|
|
|
|
private var nextRenderTargetId: Int64 = 1
|
|
|
|
open class MultiAnimationRenderTarget: SimpleLayer {
|
|
public let id: Int64
|
|
|
|
fileprivate let deinitCallbacks = Bag<() -> Void>()
|
|
fileprivate let updateStateCallbacks = Bag<() -> Void>()
|
|
|
|
public final var shouldBeAnimating: Bool = false {
|
|
didSet {
|
|
if self.shouldBeAnimating != oldValue {
|
|
for f in self.updateStateCallbacks.copyItems() {
|
|
f()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public override init() {
|
|
assert(Thread.isMainThread)
|
|
|
|
self.id = nextRenderTargetId
|
|
nextRenderTargetId += 1
|
|
|
|
super.init()
|
|
}
|
|
|
|
public override init(layer: Any) {
|
|
guard let layer = layer as? MultiAnimationRenderTarget else {
|
|
preconditionFailure()
|
|
}
|
|
|
|
self.id = layer.id
|
|
|
|
super.init(layer: layer)
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
for f in self.deinitCallbacks.copyItems() {
|
|
f()
|
|
}
|
|
}
|
|
|
|
open func updateDisplayPlaceholder(displayPlaceholder: Bool) {
|
|
}
|
|
}
|
|
|
|
private final class FrameGroup {
|
|
let image: UIImage
|
|
let badgeImage: UIImage?
|
|
let size: CGSize
|
|
let timestamp: Double
|
|
|
|
init?(item: AnimationCacheItem, timestamp: Double) {
|
|
guard let firstFrame = item.getFrame(at: timestamp) else {
|
|
return nil
|
|
}
|
|
|
|
switch firstFrame.format {
|
|
case let .rgba(width, height, bytesPerRow):
|
|
let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow)
|
|
|
|
firstFrame.data.withUnsafeBytes { bytes -> Void in
|
|
memcpy(context.bytes, bytes.baseAddress!.advanced(by: firstFrame.range.lowerBound), height * bytesPerRow)
|
|
|
|
/*var sourceBuffer = vImage_Buffer()
|
|
sourceBuffer.width = UInt(width)
|
|
sourceBuffer.height = UInt(height)
|
|
sourceBuffer.data = UnsafeMutableRawPointer(mutating: bytes.baseAddress!.advanced(by: firstFrame.range.lowerBound))
|
|
sourceBuffer.rowBytes = bytesPerRow
|
|
|
|
var destinationBuffer = vImage_Buffer()
|
|
destinationBuffer.width = UInt(32)
|
|
destinationBuffer.height = UInt(32)
|
|
destinationBuffer.data = context.bytes
|
|
destinationBuffer.rowBytes = bytesPerRow
|
|
|
|
vImageBoxConvolve_ARGB8888(&sourceBuffer,
|
|
&destinationBuffer,
|
|
nil,
|
|
UInt(width - 32 - 16), UInt(height - 32 - 16),
|
|
UInt32(31),
|
|
UInt32(31),
|
|
nil,
|
|
vImage_Flags(kvImageEdgeExtend))*/
|
|
}
|
|
|
|
guard let image = context.generateImage() else {
|
|
return nil
|
|
}
|
|
|
|
self.image = image
|
|
self.size = CGSize(width: CGFloat(width), height: CGFloat(height))
|
|
self.timestamp = timestamp
|
|
self.badgeImage = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class LoadFrameGroupTask {
|
|
let task: () -> () -> Void
|
|
|
|
init(task: @escaping () -> () -> Void) {
|
|
self.task = task
|
|
}
|
|
}
|
|
|
|
private final class ItemAnimationContext {
|
|
static let queue = Queue(name: "ItemAnimationContext", qos: .default)
|
|
|
|
private let cache: AnimationCache
|
|
private let stateUpdated: () -> Void
|
|
|
|
private var disposable: Disposable?
|
|
private var displayLink: ConstantDisplayLinkAnimator?
|
|
private var timestamp: Double = 0.0
|
|
private var item: AnimationCacheItem?
|
|
|
|
private var currentFrameGroup: FrameGroup?
|
|
private var isLoadingFrameGroup: Bool = false
|
|
|
|
private(set) var isPlaying: Bool = false {
|
|
didSet {
|
|
if self.isPlaying != oldValue {
|
|
self.stateUpdated()
|
|
}
|
|
}
|
|
}
|
|
|
|
let targets = Bag<Weak<MultiAnimationRenderTarget>>()
|
|
|
|
init(cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) {
|
|
self.cache = cache
|
|
self.stateUpdated = stateUpdated
|
|
|
|
self.disposable = cache.get(sourceId: itemId, size: size, fetch: fetch).start(next: { [weak self] result in
|
|
Queue.mainQueue().async {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.item = result.item
|
|
strongSelf.updateIsPlaying()
|
|
|
|
if result.item == nil {
|
|
for target in strongSelf.targets.copyItems() {
|
|
if let target = target.value {
|
|
target.updateDisplayPlaceholder(displayPlaceholder: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.disposable?.dispose()
|
|
self.displayLink?.invalidate()
|
|
}
|
|
|
|
func updateAddedTarget(target: MultiAnimationRenderTarget) {
|
|
if let currentFrameGroup = self.currentFrameGroup {
|
|
target.updateDisplayPlaceholder(displayPlaceholder: false)
|
|
target.contents = currentFrameGroup.image.cgImage
|
|
}
|
|
|
|
self.updateIsPlaying()
|
|
}
|
|
|
|
func updateIsPlaying() {
|
|
var isPlaying = true
|
|
if self.item == nil {
|
|
isPlaying = false
|
|
}
|
|
|
|
var shouldBeAnimating = false
|
|
for target in self.targets.copyItems() {
|
|
if let target = target.value {
|
|
if target.shouldBeAnimating {
|
|
shouldBeAnimating = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !shouldBeAnimating {
|
|
isPlaying = false
|
|
}
|
|
|
|
self.isPlaying = isPlaying
|
|
}
|
|
|
|
func animationTick(advanceTimestamp: Double) -> LoadFrameGroupTask? {
|
|
return self.update(advanceTimestamp: advanceTimestamp)
|
|
}
|
|
|
|
private func update(advanceTimestamp: Double?) -> LoadFrameGroupTask? {
|
|
guard let item = self.item else {
|
|
return nil
|
|
}
|
|
|
|
let timestamp = self.timestamp
|
|
if let advanceTimestamp = advanceTimestamp {
|
|
self.timestamp += advanceTimestamp
|
|
}
|
|
|
|
if let currentFrameGroup = self.currentFrameGroup, currentFrameGroup.timestamp == self.timestamp {
|
|
} else if !self.isLoadingFrameGroup {
|
|
self.isLoadingFrameGroup = true
|
|
|
|
return LoadFrameGroupTask(task: { [weak self] in
|
|
let currentFrameGroup = FrameGroup(item: item, timestamp: timestamp)
|
|
|
|
return {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
strongSelf.isLoadingFrameGroup = false
|
|
|
|
if let currentFrameGroup = currentFrameGroup {
|
|
strongSelf.currentFrameGroup = currentFrameGroup
|
|
for target in strongSelf.targets.copyItems() {
|
|
if let target = target.value {
|
|
target.contents = currentFrameGroup.image.cgImage
|
|
target.updateDisplayPlaceholder(displayPlaceholder: false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
if let _ = self.currentFrameGroup {
|
|
for target in self.targets.copyItems() {
|
|
if let target = target.value {
|
|
target.updateDisplayPlaceholder(displayPlaceholder: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
|
private final class GroupContext {
|
|
private let firstFrameQueue: Queue
|
|
private let stateUpdated: () -> Void
|
|
|
|
private var itemContexts: [String: ItemAnimationContext] = [:]
|
|
|
|
private(set) var isPlaying: Bool = false {
|
|
didSet {
|
|
if self.isPlaying != oldValue {
|
|
self.stateUpdated()
|
|
}
|
|
}
|
|
}
|
|
|
|
init(firstFrameQueue: Queue, stateUpdated: @escaping () -> Void) {
|
|
self.firstFrameQueue = firstFrameQueue
|
|
self.stateUpdated = stateUpdated
|
|
}
|
|
|
|
func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable {
|
|
let itemContext: ItemAnimationContext
|
|
if let current = self.itemContexts[itemId] {
|
|
itemContext = current
|
|
} else {
|
|
itemContext = ItemAnimationContext(cache: cache, itemId: itemId, size: size, fetch: fetch, stateUpdated: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateIsPlaying()
|
|
})
|
|
self.itemContexts[itemId] = itemContext
|
|
}
|
|
|
|
let index = itemContext.targets.add(Weak(target))
|
|
itemContext.updateAddedTarget(target: target)
|
|
|
|
let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in
|
|
Queue.mainQueue().async {
|
|
guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else {
|
|
return
|
|
}
|
|
itemContext.targets.remove(index)
|
|
if itemContext.targets.isEmpty {
|
|
strongSelf.itemContexts.removeValue(forKey: itemId)
|
|
}
|
|
}
|
|
}
|
|
|
|
let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in
|
|
guard let itemContext = itemContext else {
|
|
return
|
|
}
|
|
itemContext.updateIsPlaying()
|
|
}
|
|
|
|
return ActionDisposable { [weak self, weak itemContext, weak target] in
|
|
guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else {
|
|
return
|
|
}
|
|
if let target = target {
|
|
target.deinitCallbacks.remove(deinitIndex)
|
|
target.updateStateCallbacks.remove(updateStateIndex)
|
|
}
|
|
itemContext.targets.remove(index)
|
|
if itemContext.targets.isEmpty {
|
|
strongSelf.itemContexts.removeValue(forKey: itemId)
|
|
}
|
|
}
|
|
}
|
|
|
|
func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool {
|
|
if let item = cache.getFirstFrameSynchronously(sourceId: itemId, size: size) {
|
|
guard let frameGroup = FrameGroup(item: item, timestamp: 0.0) else {
|
|
return false
|
|
}
|
|
|
|
target.contents = frameGroup.image.cgImage
|
|
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable {
|
|
return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, completion: { [weak target] item in
|
|
guard let item = item else {
|
|
Queue.mainQueue().async {
|
|
completion(false)
|
|
}
|
|
return
|
|
}
|
|
|
|
let frameGroup = FrameGroup(item: item, timestamp: 0.0)
|
|
|
|
Queue.mainQueue().async {
|
|
guard let target = target else {
|
|
completion(false)
|
|
return
|
|
}
|
|
if let frameGroup = frameGroup {
|
|
target.contents = frameGroup.image.cgImage
|
|
|
|
completion(true)
|
|
} else {
|
|
completion(false)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
private func updateIsPlaying() {
|
|
var isPlaying = false
|
|
for (_, itemContext) in self.itemContexts {
|
|
if itemContext.isPlaying {
|
|
isPlaying = true
|
|
break
|
|
}
|
|
}
|
|
|
|
self.isPlaying = isPlaying
|
|
}
|
|
|
|
func animationTick(advanceTimestamp: Double) -> [LoadFrameGroupTask] {
|
|
var tasks: [LoadFrameGroupTask] = []
|
|
for (_, itemContext) in self.itemContexts {
|
|
if itemContext.isPlaying {
|
|
if let task = itemContext.animationTick(advanceTimestamp: advanceTimestamp) {
|
|
tasks.append(task)
|
|
}
|
|
}
|
|
}
|
|
|
|
return tasks
|
|
}
|
|
}
|
|
|
|
public static let firstFrameQueue = Queue(name: "MultiAnimationRenderer-FirstFrame", qos: .userInteractive)
|
|
|
|
private var groupContexts: [String: GroupContext] = [:]
|
|
private var frameSkip: Int
|
|
private var displayLink: ConstantDisplayLinkAnimator?
|
|
|
|
private(set) var isPlaying: Bool = false {
|
|
didSet {
|
|
if self.isPlaying != oldValue {
|
|
if self.isPlaying {
|
|
if self.displayLink == nil {
|
|
self.displayLink = ConstantDisplayLinkAnimator { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.animationTick()
|
|
}
|
|
self.displayLink?.frameInterval = self.frameSkip
|
|
self.displayLink?.isPaused = false
|
|
}
|
|
} else {
|
|
if let displayLink = self.displayLink {
|
|
self.displayLink = nil
|
|
displayLink.invalidate()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public init() {
|
|
if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.activeProcessorCount > 2 {
|
|
self.frameSkip = 1
|
|
} else {
|
|
self.frameSkip = 2
|
|
}
|
|
}
|
|
|
|
public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable {
|
|
let groupContext: GroupContext
|
|
if let current = self.groupContexts[groupId] {
|
|
groupContext = current
|
|
} else {
|
|
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateIsPlaying()
|
|
})
|
|
self.groupContexts[groupId] = groupContext
|
|
}
|
|
|
|
let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch)
|
|
|
|
return ActionDisposable {
|
|
disposable.dispose()
|
|
}
|
|
}
|
|
|
|
public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool {
|
|
let groupContext: GroupContext
|
|
if let current = self.groupContexts[groupId] {
|
|
groupContext = current
|
|
} else {
|
|
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateIsPlaying()
|
|
})
|
|
self.groupContexts[groupId] = groupContext
|
|
}
|
|
|
|
return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size)
|
|
}
|
|
|
|
public func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable {
|
|
let groupContext: GroupContext
|
|
if let current = self.groupContexts[groupId] {
|
|
groupContext = current
|
|
} else {
|
|
groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateIsPlaying()
|
|
})
|
|
self.groupContexts[groupId] = groupContext
|
|
}
|
|
|
|
return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, completion: completion)
|
|
}
|
|
|
|
private func updateIsPlaying() {
|
|
var isPlaying = false
|
|
for (_, groupContext) in self.groupContexts {
|
|
if groupContext.isPlaying {
|
|
isPlaying = true
|
|
break
|
|
}
|
|
}
|
|
|
|
self.isPlaying = isPlaying
|
|
}
|
|
|
|
private func animationTick() {
|
|
let secondsPerFrame = Double(self.frameSkip) / 60.0
|
|
|
|
var tasks: [LoadFrameGroupTask] = []
|
|
for (_, groupContext) in self.groupContexts {
|
|
if groupContext.isPlaying {
|
|
tasks.append(contentsOf: groupContext.animationTick(advanceTimestamp: secondsPerFrame))
|
|
}
|
|
}
|
|
|
|
if !tasks.isEmpty {
|
|
ItemAnimationContext.queue.async {
|
|
var completions: [() -> Void] = []
|
|
for task in tasks {
|
|
let complete = task.task()
|
|
completions.append(complete)
|
|
}
|
|
|
|
if !completions.isEmpty {
|
|
Queue.mainQueue().async {
|
|
for completion in completions {
|
|
completion()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|