Swiftgram/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift
2020-12-06 16:31:28 +00:00

263 lines
10 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import AvatarNode
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import AccountContext
import AudioBlob
public final class AnimatedAvatarSetContext {
public final class Content {
fileprivate final class Item {
fileprivate struct Key: Hashable {
var peerId: PeerId
}
fileprivate let peer: Peer
fileprivate init(peer: Peer) {
self.peer = peer
}
}
fileprivate var items: [(Item.Key, Item)]
fileprivate init(items: [(Item.Key, Item)]) {
self.items = items
}
}
private final class ItemState {
let peer: Peer
init(peer: Peer) {
self.peer = peer
}
}
private var peers: [Peer] = []
private var itemStates: [PeerId: ItemState] = [:]
public init() {
}
public func update(peers: [Peer], animated: Bool) -> Content {
var items: [(Content.Item.Key, Content.Item)] = []
for peer in peers {
items.append((Content.Item.Key(peerId: peer.id), Content.Item(peer: peer)))
}
return Content(items: items)
}
}
private let avatarFont = avatarPlaceholderFont(size: 12.0)
private final class ContentNode: ASDisplayNode {
private var audioLevelView: VoiceBlobView?
private var audioLevelBlobOverlay: UIImageView?
private let unclippedNode: ASImageNode
private let clippedNode: ASImageNode
private var disposable: Disposable?
init(context: AccountContext, peer: Peer, synchronousLoad: Bool) {
self.unclippedNode = ASImageNode()
self.clippedNode = ASImageNode()
super.init()
self.addSubnode(self.unclippedNode)
self.addSubnode(self.clippedNode)
if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: representation, displayDimensions: CGSize(width: 30.0, height: 30.0), synchronousLoad: synchronousLoad) {
let image = generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.lightGray.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
})!
self.updateImage(image: image)
let disposable = (signal
|> deliverOnMainQueue).start(next: { [weak self] imageVersions in
guard let strongSelf = self else {
return
}
let image = imageVersions?.0
if let image = image {
strongSelf.updateImage(image: image)
}
})
self.disposable = disposable
} else {
let image = generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
drawPeerAvatarLetters(context: context, size: size, font: avatarFont, letters: peer.displayLetters, peerId: peer.id)
})!
self.updateImage(image: image)
}
}
private func updateImage(image: UIImage) {
self.unclippedNode.image = image
self.clippedNode.image = generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: -1.5, dy: -1.5).offsetBy(dx: -20.0, dy: 0.0))
})
}
deinit {
self.disposable?.dispose()
}
func updateLayout(size: CGSize, isClipped: Bool, animated: Bool) {
self.unclippedNode.frame = CGRect(origin: CGPoint(), size: size)
self.clippedNode.frame = CGRect(origin: CGPoint(), size: size)
if animated && self.unclippedNode.alpha.isZero != self.clippedNode.alpha.isZero {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
transition.updateAlpha(node: self.unclippedNode, alpha: isClipped ? 0.0 : 1.0)
transition.updateAlpha(node: self.clippedNode, alpha: isClipped ? 1.0 : 0.0)
} else {
self.unclippedNode.alpha = isClipped ? 0.0 : 1.0
self.clippedNode.alpha = isClipped ? 1.0 : 0.0
}
}
func updateAudioLevel(color: UIColor, backgroundColor: UIColor, value: Float) {
if self.audioLevelView == nil, value > 0.0 {
let blobFrame = self.unclippedNode.bounds.insetBy(dx: -8.0, dy: -8.0)
let audioLevelView = VoiceBlobView(
frame: blobFrame,
maxLevel: 0.3,
smallBlobRange: (0, 0),
mediumBlobRange: (0.7, 0.8),
bigBlobRange: (0.8, 0.9)
)
let maskRect = CGRect(origin: .zero, size: blobFrame.size)
let playbackMaskLayer = CAShapeLayer()
playbackMaskLayer.frame = maskRect
playbackMaskLayer.fillRule = .evenOdd
let maskPath = UIBezierPath()
maskPath.append(UIBezierPath(roundedRect: maskRect.insetBy(dx: 12, dy: 12), cornerRadius: 22))
maskPath.append(UIBezierPath(rect: maskRect))
playbackMaskLayer.path = maskPath.cgPath
audioLevelView.layer.mask = playbackMaskLayer
audioLevelView.setColor(color)
self.audioLevelView = audioLevelView
self.view.insertSubview(audioLevelView, at: 0)
}
let level = min(1.0, max(0.0, CGFloat(value)))
if let audioLevelView = self.audioLevelView {
audioLevelView.updateLevel(CGFloat(value) * 2.0)
let avatarScale: CGFloat
if value > 0.0 {
audioLevelView.startAnimating()
avatarScale = 1.03 + level * 0.07
} else {
audioLevelView.stopAnimating(duration: 0.5)
avatarScale = 1.0
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
transition.updateSublayerTransformScale(node: self, scale: CGPoint(x: avatarScale, y: avatarScale), beginWithCurrentState: true)
}
}
}
public final class AnimatedAvatarSetNode: ASDisplayNode {
private var contentNodes: [AnimatedAvatarSetContext.Content.Item.Key: ContentNode] = [:]
override public init() {
super.init()
}
public func update(context: AccountContext, content: AnimatedAvatarSetContext.Content, itemSize: CGSize = CGSize(width: 30.0, height: 30.0), animated: Bool, synchronousLoad: Bool) -> CGSize {
var contentWidth: CGFloat = 0.0
let contentHeight: CGFloat = itemSize.height
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
var validKeys: [AnimatedAvatarSetContext.Content.Item.Key] = []
var index = 0
for i in 0 ..< content.items.count {
let (key, item) = content.items[i]
validKeys.append(key)
let itemFrame = CGRect(origin: CGPoint(x: contentWidth, y: 0.0), size: itemSize)
let itemNode: ContentNode
if let current = self.contentNodes[key] {
itemNode = current
itemNode.updateLayout(size: itemSize, isClipped: index != 0, animated: animated)
transition.updateFrame(node: itemNode, frame: itemFrame)
} else {
itemNode = ContentNode(context: context, peer: item.peer, synchronousLoad: synchronousLoad)
self.addSubnode(itemNode)
self.contentNodes[key] = itemNode
itemNode.updateLayout(size: itemSize, isClipped: index != 0, animated: false)
itemNode.frame = itemFrame
if animated {
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
}
}
itemNode.zPosition = CGFloat(100 - i)
contentWidth += itemSize.width - 10.0
index += 1
}
var removeKeys: [AnimatedAvatarSetContext.Content.Item.Key] = []
for key in self.contentNodes.keys {
if !validKeys.contains(key) {
removeKeys.append(key)
}
}
for key in removeKeys {
guard let itemNode = self.contentNodes.removeValue(forKey: key) else {
continue
}
itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemNode] _ in
itemNode?.removeFromSupernode()
})
itemNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
}
return CGSize(width: contentWidth, height: contentHeight)
}
public func updateAudioLevels(color: UIColor, backgroundColor: UIColor, levels: [PeerId: Float]) {
//print("updateAudioLevels visible: \(self.contentNodes.keys.map(\.peerId.id)) data: \(levels)")
for (key, itemNode) in self.contentNodes {
if let value = levels[key.peerId] {
itemNode.updateAudioLevel(color: color, backgroundColor: backgroundColor, value: value)
} else {
itemNode.updateAudioLevel(color: color, backgroundColor: backgroundColor, value: 0.0)
}
}
}
}