mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
Initial avatar editor implementation
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import ViewControllerComponent
|
||||
import ComponentDisplayAdapters
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import MultilineTextComponent
|
||||
import EmojiStatusComponent
|
||||
import Postbox
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import StickerResources
|
||||
|
||||
final class AvatarPreviewComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
let context: AccountContext
|
||||
let background: AvatarBackground
|
||||
let file: TelegramMediaFile?
|
||||
let tapped: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
background: AvatarBackground,
|
||||
file: TelegramMediaFile?,
|
||||
tapped: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.background = background
|
||||
self.file = file
|
||||
self.tapped = tapped
|
||||
}
|
||||
|
||||
static func ==(lhs: AvatarPreviewComponent, rhs: AvatarPreviewComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.background != rhs.background {
|
||||
return false
|
||||
}
|
||||
if lhs.file != rhs.file {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView, UITextFieldDelegate {
|
||||
private let imageView: UIImageView
|
||||
|
||||
private let imageNode: TransformImageNode
|
||||
private var animationNode: AnimatedStickerNode?
|
||||
|
||||
private var component: AvatarPreviewComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private let stickerFetchedDisposable = MetaDisposable()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.imageView = UIImageView()
|
||||
self.imageView.isUserInteractionEnabled = false
|
||||
|
||||
self.imageNode = TransformImageNode()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.disablesInteractiveModalDismiss = true
|
||||
self.disablesInteractiveKeyboardGestureRecognizer = true
|
||||
|
||||
self.addSubview(self.imageView)
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped)))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stickerFetchedDisposable.dispose()
|
||||
}
|
||||
|
||||
@objc func tapped() {
|
||||
self.animationNode?.playOnce()
|
||||
self.component?.tapped()
|
||||
}
|
||||
|
||||
func update(component: AvatarPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
let previousBackground = self.component?.background
|
||||
|
||||
let hadFile = self.component?.file != nil
|
||||
var fileUpdated = false
|
||||
if self.component?.file?.fileId != component.file?.fileId {
|
||||
self.imageNode.isHidden = false
|
||||
fileUpdated = true
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = CGSize(width: availableSize.width * 0.66, height: availableSize.width * 0.66)
|
||||
|
||||
var dimensions: CGSize?
|
||||
if let file = component.file, fileUpdated, let fileDimensions = file.dimensions?.cgSize {
|
||||
dimensions = fileDimensions
|
||||
|
||||
if !self.imageNode.isHidden && hadFile, let snapshotView = self.imageNode.view.snapshotContentTree() {
|
||||
self.imageNode.view.superview?.addSubview(snapshotView)
|
||||
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
snapshotView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
if let animationNode = self.animationNode {
|
||||
self.animationNode = nil
|
||||
|
||||
animationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
animationNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { [weak animationNode] _ in
|
||||
animationNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" {
|
||||
self.imageNode.isHidden = false
|
||||
|
||||
if self.animationNode == nil {
|
||||
let animationNode = DefaultAnimatedStickerNodeImpl()
|
||||
animationNode.autoplay = false
|
||||
self.animationNode = animationNode
|
||||
animationNode.started = { [weak self] in
|
||||
self?.imageNode.isHidden = true
|
||||
}
|
||||
self.addSubnode(animationNode)
|
||||
}
|
||||
|
||||
self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: component.context.account.postbox, userLocation: .other, file: file, small: false, size: fileDimensions.aspectFitted(CGSize(width: 256.0, height: 256.0))))
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start())
|
||||
} else {
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.visibility = false
|
||||
self.animationNode = nil
|
||||
animationNode.removeFromSupernode()
|
||||
self.imageNode.isHidden = false
|
||||
}
|
||||
self.imageNode.setSignal(chatMessageSticker(account: component.context.account, userLocation: .other, file: file, small: false, synchronousLoad: false))
|
||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: false)).start())
|
||||
}
|
||||
|
||||
if fileUpdated && hadFile {
|
||||
self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.imageNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
animationNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let dimensions {
|
||||
let imageSize = dimensions.aspectFitted(size)
|
||||
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))()
|
||||
self.imageNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - imageSize.width) / 2.0), y: (availableSize.height - imageSize.height) / 2.0), size: imageSize)
|
||||
|
||||
if let animationNode = self.animationNode {
|
||||
animationNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - imageSize.width) / 2.0), y: (availableSize.height - imageSize.height) / 2.0), size: imageSize)
|
||||
animationNode.updateLayout(size: imageSize)
|
||||
}
|
||||
|
||||
if fileUpdated {
|
||||
self.updateVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
self.imageView.frame = CGRect(origin: .zero, size: availableSize)
|
||||
if previousBackground != component.background {
|
||||
if let _ = previousBackground, !transition.animation.isImmediate, let snapshotView = self.imageView.snapshotContentTree() {
|
||||
self.insertSubview(snapshotView, aboveSubview: self.imageView)
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
self.imageView.image = component.background.generateImage(size: availableSize)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
||||
private func updateVisibility() {
|
||||
guard let component = self.component, let file = component.file else {
|
||||
return
|
||||
}
|
||||
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||
let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
||||
let source = AnimatedStickerResourceSource(account: component.context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm")
|
||||
self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .count(2), mode: .direct(cachePathPrefix: nil))
|
||||
self.animationNode?.visibility = true
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user