Improve peer profile button animations

This commit is contained in:
Ilya Laktyushin 2022-01-28 19:15:08 +03:00
parent 47fb248cc7
commit 387b83c9aa
5 changed files with 181 additions and 20 deletions

View File

@ -38,13 +38,15 @@ public final class ManagedAnimationState {
guard let path = item.source.path else {
return nil
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
guard var data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
return nil
}
guard let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) else {
return nil
if path.hasSuffix(".json") {
} else if let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) {
data = unpackedData
}
guard let instance = LottieInstance(data: unpackedData, fitzModifier: .none, cacheKey: item.source.cacheKey) else {
guard let instance = LottieInstance(data: data, fitzModifier: .none, cacheKey: item.source.cacheKey) else {
return nil
}
resolvedInstance = instance
@ -92,7 +94,10 @@ public enum ManagedAnimationSource: Equatable {
var path: String? {
switch self {
case let .local(name):
return getAppBundle().path(forResource: name, ofType: "tgs")
if let tgsPath = getAppBundle().path(forResource: name, ofType: "tgs") {
return tgsPath
}
return getAppBundle().path(forResource: name, ofType: "json")
case let .resource(account, resource):
return account.postbox.mediaBox.completedResourcePath(resource._asResource())
}

View File

@ -0,0 +1 @@
{"v":"5.8.1","fr":60,"ip":0,"op":46,"w":300,"h":300,"nm":"more","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"more","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2,"l":2},"a":{"a":0,"k":[150,150,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-55.228,0],[0,55.229],[55.229,0],[0,-55.228]],"o":[[55.229,0],[0,-55.228],[-55.228,0],[0,55.229]],"v":[[23.5,123.5],[123.5,23.5],[23.5,-76.5],[-76.5,23.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[126.5,126.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-8.284,0],[0,8.284],[8.284,0],[0,-8.284]],"o":[[8.284,0],[0,-8.284],[-8.284,0],[0,8.284]],"v":[[50,15],[65,0],[50,-15],[35,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[200,150],"to":[0,-5],"ti":[0,-2]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":20,"s":[200,120],"to":[0,2],"ti":[0,-5]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":30,"s":[200,162],"to":[0,5],"ti":[0,2]},{"t":40,"s":[200,150]}],"ix":2},"a":{"a":0,"k":[50,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.5,0.5],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]},"t":15,"s":[100,100]},{"i":{"x":[0.5,0.5],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]},"t":25,"s":[125,125]},{"i":{"x":[0.5,0.5],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]},"t":35,"s":[95,95]},{"t":45,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"3","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-8.284,0],[0,8.284],[8.284,0],[0,-8.284]],"o":[[8.284,0],[0,-8.284],[-8.284,0],[0,8.284]],"v":[[0.5,15],[15.5,0],[0.5,-15],[-14.5,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":5,"s":[150,150],"to":[0,-5],"ti":[0,-2]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":15,"s":[150,120],"to":[0,2],"ti":[0,-5]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":25,"s":[150,162],"to":[0,5],"ti":[0,2]},{"t":35,"s":[150,150]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.5,0.5],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]},"t":10,"s":[100,100]},{"i":{"x":[0.5,0.5],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]},"t":20,"s":[125,125]},{"i":{"x":[0.5,0.5],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]},"t":30,"s":[95,95]},{"t":40,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"2","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-8.284,0],[0,8.284],[8.284,0],[0,-8.284]],"o":[[8.284,0],[0,-8.284],[-8.284,0],[0,8.284]],"v":[[-49,15],[-34,0],[-49,-15],[-64,0]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[100,150],"to":[0,-5],"ti":[0,-2]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[100,120],"to":[0,2],"ti":[0,-5]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":20,"s":[100,162],"to":[0,5],"ti":[0,2]},{"t":30,"s":[100,150]}],"ix":2},"a":{"a":0,"k":[-50,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.5,0.5],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]},"t":5,"s":[100,100]},{"i":{"x":[0.5,0.5],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]},"t":15,"s":[125,125]},{"i":{"x":[0.5,0.5],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]},"t":25,"s":[95,95]},{"t":35,"s":[100,100]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"1","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":-35,"op":47,"st":-38,"bm":0}],"markers":[]}

File diff suppressed because one or more lines are too long

View File

@ -21,6 +21,7 @@ import TelegramUIPreferences
import PeerInfoAvatarListNode
import AnimationUI
import ContextUI
import ManagedAnimationNode
enum PeerInfoHeaderButtonKey: Hashable {
case message
@ -920,13 +921,99 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
}
}
private enum MoreIconNodeState: Equatable {
case more
case search
case moreToSearch(Float)
}
private final class MoreIconNode: ManagedAnimationNode {
private let duration: Double = 0.21
private var iconState: MoreIconNodeState = .more
init() {
super.init(size: CGSize(width: 30.0, height: 30.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_moretosearch"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.0))
}
func play() {
if case .more = self.iconState {
self.trackTo(item: ManagedAnimationItem(source: .local("anim_moredots"), frames: .range(startFrame: 0, endFrame: 46), duration: 0.76))
}
}
func enqueueState(_ state: MoreIconNodeState, animated: Bool) {
guard self.iconState != state else {
return
}
let previousState = self.iconState
self.iconState = state
let source = ManagedAnimationSource.local("anim_moretosearch")
let totalLength: Int = 90
if animated {
switch previousState {
case .more:
switch state {
case .more:
break
case .search:
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: totalLength), duration: self.duration))
case let .moreToSearch(progress):
let frame = Int(progress * Float(totalLength))
let duration = self.duration * Double(progress)
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: frame), duration: duration))
}
case .search:
switch state {
case .more:
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: 0), duration: self.duration))
case .search:
break
case let .moreToSearch(progress):
let frame = Int(progress * Float(totalLength))
let duration = self.duration * Double((1.0 - progress))
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: frame), duration: duration))
}
case let .moreToSearch(currentProgress):
let currentFrame = Int(currentProgress * Float(totalLength))
switch state {
case .more:
let duration = self.duration * Double(currentProgress)
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: 0), duration: duration))
case .search:
let duration = self.duration * (1.0 - Double(currentProgress))
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: totalLength), duration: duration))
case let .moreToSearch(progress):
let frame = Int(progress * Float(totalLength))
let duration = self.duration * Double(abs(currentProgress - progress))
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: currentFrame, endFrame: frame), duration: duration))
}
}
} else {
switch state {
case .more:
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: 0, endFrame: 0), duration: 0.0))
case .search:
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: totalLength, endFrame: totalLength), duration: 0.0))
case let .moreToSearch(progress):
let frame = Int(progress * Float(totalLength))
self.trackTo(item: ManagedAnimationItem(source: source, frames: .range(startFrame: frame, endFrame: frame), duration: 0.0))
}
}
}
}
final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
let containerNode: ContextControllerSourceNode
let contextSourceNode: ContextReferenceContentNode
private let regularTextNode: ImmediateTextNode
private let whiteTextNode: ImmediateTextNode
private let iconNode: ASImageNode
private var animationNode: AnimationNode?
private var animationNode: MoreIconNode?
private var key: PeerInfoHeaderNavigationButtonKey?
private var theme: PresentationTheme?
@ -982,11 +1069,13 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
}
@objc private func pressed() {
self.animationNode?.play()
self.action?(self.contextSourceNode, nil)
}
func update(key: PeerInfoHeaderNavigationButtonKey, presentationData: PresentationData, height: CGFloat) -> CGSize {
let textSize: CGSize
let isFirstTime = self.key == nil
if self.key != key || self.theme !== presentationData.theme {
self.key = key
self.theme = presentationData.theme
@ -995,6 +1084,8 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
var icon: UIImage?
var isBold = false
var isGestureEnabled = false
var isAnimation = false
var animationState: MoreIconNodeState = .more
switch key {
case .edit:
text = presentationData.strings.Common_Edit
@ -1005,18 +1096,24 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
text = presentationData.strings.Common_Select
case .search:
text = ""
icon = PresentationResourcesRootController.navigationCompactSearchIcon(presentationData.theme)
icon = nil// PresentationResourcesRootController.navigationCompactSearchIcon(presentationData.theme)
isAnimation = true
animationState = .search
case .editPhoto:
text = presentationData.strings.Settings_EditPhoto
case .editVideo:
text = presentationData.strings.Settings_EditVideo
case .more:
text = ""
icon = PresentationResourcesRootController.navigationMoreCircledIcon(presentationData.theme)
icon = nil// PresentationResourcesRootController.navigationMoreCircledIcon(presentationData.theme)
isGestureEnabled = true
isAnimation = true
animationState = .more
case .qrCode:
text = ""
icon = PresentationResourcesRootController.navigationQrCodeIcon(presentationData.theme)
case .moreToSearch:
text = ""
}
self.accessibilityLabel = text
self.containerNode.isGestureEnabled = isGestureEnabled
@ -1027,6 +1124,26 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
self.whiteTextNode.attributedText = NSAttributedString(string: text, font: font, textColor: .white)
self.iconNode.image = icon
if isAnimation {
self.iconNode.isHidden = true
let animationNode: MoreIconNode
if let current = self.animationNode {
animationNode = current
} else {
animationNode = MoreIconNode()
self.animationNode = animationNode
self.contextSourceNode.addSubnode(animationNode)
}
animationNode.customColor = presentationData.theme.rootController.navigationBar.accentTextColor
animationNode.enqueueState(animationState, animated: !isFirstTime)
} else {
self.iconNode.isHidden = false
if let current = self.animationNode {
self.animationNode = nil
current.removeFromSupernode()
}
}
textSize = self.regularTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
let _ = self.whiteTextNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
} else {
@ -1039,7 +1156,16 @@ final class PeerInfoHeaderNavigationButton: HighlightableButtonNode {
self.regularTextNode.frame = textFrame
self.whiteTextNode.frame = textFrame
if let image = self.iconNode.image {
if let animationNode = self.animationNode {
let animationSize = CGSize(width: 30.0, height: 30.0)
animationNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - animationSize.height) / 2.0)), size: animationSize)
let size = CGSize(width: animationSize.width + inset * 2.0, height: height)
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: size)
return size
} else if let image = self.iconNode.image {
self.iconNode.frame = CGRect(origin: CGPoint(x: inset, y: floor((height - image.size.height) / 2.0)), size: image.size)
let size = CGSize(width: image.size.width + inset * 2.0, height: height)
@ -1066,6 +1192,7 @@ enum PeerInfoHeaderNavigationButtonKey {
case editVideo
case more
case qrCode
case moreToSearch
}
struct PeerInfoHeaderNavigationButtonSpec: Equatable {
@ -1187,20 +1314,26 @@ final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode {
for spec in rightButtons.reversed() {
let buttonNode: PeerInfoHeaderNavigationButton
var wasAdded = false
if let current = self.rightButtonNodes[spec.key] {
var key = spec.key
if key == .more || key == .search {
key = .moreToSearch
}
if let current = self.rightButtonNodes[key] {
buttonNode = current
} else {
wasAdded = true
buttonNode = PeerInfoHeaderNavigationButton()
self.rightButtonNodes[spec.key] = buttonNode
self.rightButtonNodes[key] = buttonNode
self.addSubnode(buttonNode)
buttonNode.isWhite = self.isWhite
buttonNode.action = { [weak self] _, gesture in
guard let strongSelf = self, let buttonNode = strongSelf.rightButtonNodes[spec.key] else {
return
}
strongSelf.performAction?(spec.key, buttonNode.contextSourceNode, gesture)
}
buttonNode.action = { [weak self] _, gesture in
guard let strongSelf = self, let buttonNode = strongSelf.rightButtonNodes[key] else {
return
}
strongSelf.performAction?(spec.key, buttonNode.contextSourceNode, gesture)
}
let buttonSize = buttonNode.update(key: spec.key, presentationData: presentationData, height: size.height)
var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin
@ -1213,6 +1346,10 @@ final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode {
}
let alphaFactor: CGFloat = spec.isForExpandedView ? expandFraction : (1.0 - expandFraction)
if wasAdded {
if key == .moreToSearch {
buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
}
buttonNode.frame = buttonFrame
buttonNode.alpha = 0.0
transition.updateAlpha(node: buttonNode, alpha: alphaFactor * alphaFactor)
@ -1223,20 +1360,37 @@ final class PeerInfoHeaderNavigationButtonContainerNode: ASDisplayNode {
}
var removeKeys: [PeerInfoHeaderNavigationButtonKey] = []
for (key, _) in self.rightButtonNodes {
if !rightButtons.contains(where: { $0.key == key }) {
if key == .moreToSearch {
if !rightButtons.contains(where: { $0.key == .more || $0.key == .search }) {
removeKeys.append(key)
}
} else if !rightButtons.contains(where: { $0.key == key }) {
removeKeys.append(key)
}
}
for key in removeKeys {
if let buttonNode = self.rightButtonNodes.removeValue(forKey: key) {
buttonNode.removeFromSupernode()
if key == .moreToSearch {
buttonNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak buttonNode] _ in
buttonNode?.removeFromSupernode()
})
buttonNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false)
} else {
buttonNode.removeFromSupernode()
}
}
}
} else {
var nextRegularButtonOrigin = size.width - 16.0
var nextExpandedButtonOrigin = size.width - 16.0
for spec in rightButtons.reversed() {
if let buttonNode = self.rightButtonNodes[spec.key] {
var key = spec.key
if key == .more || key == .search {
key = .moreToSearch
}
if let buttonNode = self.rightButtonNodes[key] {
let buttonSize = buttonNode.bounds.size
var nextButtonOrigin = spec.isForExpandedView ? nextExpandedButtonOrigin : nextRegularButtonOrigin
let buttonFrame = CGRect(origin: CGPoint(x: nextButtonOrigin - buttonSize.width, y: expandOffset + (spec.isForExpandedView ? maximumExpandOffset : 0.0)), size: buttonSize)

View File

@ -2835,7 +2835,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}
case .qrCode:
strongSelf.openQrCode()
case .editPhoto, .editVideo:
case .editPhoto, .editVideo, .moreToSearch:
break
}
}