Update unread channel navigation

This commit is contained in:
Ali 2021-07-30 17:17:30 +02:00
parent 15d1ba7193
commit 5b154aec67
11 changed files with 295 additions and 48 deletions

View File

@ -396,8 +396,8 @@ public enum TabBarItemContextActionType {
} }
navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: navigationLayout.defaultContentHeight, additionalTopHeight: statusBarHeight, additionalContentHeight: self.additionalNavigationBarHeight, additionalBackgroundHeight: additionalBackgroundHeight, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, appearsHidden: !self.displayNavigationBar, isLandscape: isLandscape, transition: transition) navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: navigationLayout.defaultContentHeight, additionalTopHeight: statusBarHeight, additionalContentHeight: self.additionalNavigationBarHeight, additionalBackgroundHeight: additionalBackgroundHeight, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, appearsHidden: !self.displayNavigationBar, isLandscape: isLandscape, transition: transition)
if !transition.isAnimated { if !transition.isAnimated {
navigationBar.layer.cancelAnimationsRecursive(key: "bounds") navigationBar.layer.removeAnimation(forKey: "bounds")
navigationBar.layer.cancelAnimationsRecursive(key: "position") navigationBar.layer.removeAnimation(forKey: "position")
} }
transition.updateFrame(node: navigationBar, frame: navigationBarFrame) transition.updateFrame(node: navigationBar, frame: navigationBarFrame)
navigationBar.setHidden(!self.displayNavigationBar, animated: transition.isAnimated) navigationBar.setHidden(!self.displayNavigationBar, animated: transition.isAnimated)

View File

@ -585,22 +585,12 @@ public final class PostboxEncoder {
} }
public func encode<T: Encodable>(_ value: T, forKey key: String) { public func encode<T: Encodable>(_ value: T, forKey key: String) {
let innerEncoder = AdaptedPostboxEncoder() let typeHash: Int32 = murMurHashString32("\(type(of: value))")
guard let innerData = try? innerEncoder.encode(value) else { let innerEncoder = _AdaptedPostboxEncoder(typeHash: typeHash)
return try! value.encode(to: innerEncoder)
}
self.encodeKey(key) let (data, valueType) = innerEncoder.makeData()
var t: Int8 = ValueType.Object.rawValue self.encodeInnerObjectData(data, valueType: valueType, forKey: key)
self.buffer.write(&t, offset: 0, length: 1)
let string = "\(type(of: value))"
var typeHash: Int32 = murMurHashString32(string)
self.buffer.write(&typeHash, offset: 0, length: 4)
var length: Int32 = Int32(innerData.count)
self.buffer.write(&length, offset: 0, length: 4)
self.buffer.write(innerData)
} }
func encodeInnerObjectData(_ value: Data, valueType: ValueType, forKey key: String) { func encodeInnerObjectData(_ value: Data, valueType: ValueType, forKey key: String) {
@ -995,9 +985,10 @@ public final class PostboxDecoder {
var length: Int32 = 0 var length: Int32 = 0
memcpy(&length, self.buffer.memory + self.offset, 4) memcpy(&length, self.buffer.memory + self.offset, 4)
self.offset += 4
let innerData = ReadBuffer(memory: self.buffer.memory + (self.offset + 4), length: Int(length), freeWhenDone: false).makeData() let innerData = ReadBuffer(memory: self.buffer.memory + self.offset, length: Int(length), freeWhenDone: false).makeData()
self.offset += 4 + Int(length) self.offset += Int(length)
return (innerData, actualValueType) return (innerData, actualValueType)
} else { } else {

View File

@ -1,8 +1,11 @@
import Foundation import Foundation
import MurMurHash32
public class AdaptedPostboxEncoder { public class AdaptedPostboxEncoder {
func encode(_ value: Encodable) throws -> Data { func encode(_ value: Encodable) throws -> Data {
let encoder = _AdaptedPostboxEncoder() let typeHash: Int32 = murMurHashString32("\(type(of: value))")
let encoder = _AdaptedPostboxEncoder(typeHash: typeHash)
try value.encode(to: encoder) try value.encode(to: encoder)
return encoder.makeData().0 return encoder.makeData().0
} }
@ -12,9 +15,15 @@ final class _AdaptedPostboxEncoder {
var codingPath: [CodingKey] = [] var codingPath: [CodingKey] = []
var userInfo: [CodingUserInfoKey : Any] = [:] var userInfo: [CodingUserInfoKey : Any] = [:]
let typeHash: Int32
fileprivate var container: AdaptedPostboxEncodingContainer? fileprivate var container: AdaptedPostboxEncodingContainer?
init(typeHash: Int32) {
self.typeHash = typeHash
}
func makeData() -> (Data, ValueType) { func makeData() -> (Data, ValueType) {
return self.container!.makeData() return self.container!.makeData()
} }
@ -27,8 +36,8 @@ extension _AdaptedPostboxEncoder: Encoder {
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey { func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
assertCanCreateContainer() assertCanCreateContainer()
let container = KeyedContainer<Key>(codingPath: self.codingPath, userInfo: self.userInfo) let container = KeyedContainer<Key>(codingPath: self.codingPath, userInfo: self.userInfo, typeHash: self.typeHash)
self.container = container self.container = container
return KeyedEncodingContainer(container) return KeyedEncodingContainer(container)

View File

@ -1,15 +1,18 @@
import Foundation import Foundation
import MurMurHash32
extension _AdaptedPostboxEncoder { extension _AdaptedPostboxEncoder {
final class KeyedContainer<Key> where Key: CodingKey { final class KeyedContainer<Key> where Key: CodingKey {
var codingPath: [CodingKey] var codingPath: [CodingKey]
var userInfo: [CodingUserInfoKey: Any] var userInfo: [CodingUserInfoKey: Any]
let typeHash: Int32
let encoder: PostboxEncoder let encoder: PostboxEncoder
init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) { init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], typeHash: Int32) {
self.codingPath = codingPath self.codingPath = codingPath
self.userInfo = userInfo self.userInfo = userInfo
self.typeHash = typeHash
self.encoder = PostboxEncoder() self.encoder = PostboxEncoder()
} }
@ -17,7 +20,7 @@ extension _AdaptedPostboxEncoder {
func makeData() -> (Data, ValueType) { func makeData() -> (Data, ValueType) {
let buffer = WriteBuffer() let buffer = WriteBuffer()
var typeHash: Int32 = 0 var typeHash: Int32 = self.typeHash
buffer.write(&typeHash, offset: 0, length: 4) buffer.write(&typeHash, offset: 0, length: 4)
let data = self.encoder.makeData() let data = self.encoder.makeData()
@ -33,7 +36,8 @@ extension _AdaptedPostboxEncoder {
extension _AdaptedPostboxEncoder.KeyedContainer: KeyedEncodingContainerProtocol { extension _AdaptedPostboxEncoder.KeyedContainer: KeyedEncodingContainerProtocol {
func encode<T>(_ value: T, forKey key: Key) throws where T : Encodable { func encode<T>(_ value: T, forKey key: Key) throws where T : Encodable {
let innerEncoder = _AdaptedPostboxEncoder() let typeHash: Int32 = murMurHashString32("\(type(of: value))")
let innerEncoder = _AdaptedPostboxEncoder(typeHash: typeHash)
try! value.encode(to: innerEncoder) try! value.encode(to: innerEncoder)
let (data, valueType) = innerEncoder.makeData() let (data, valueType) = innerEncoder.makeData()

View File

@ -106,17 +106,14 @@ extension _AdaptedPostboxEncoder.UnkeyedContainer: UnkeyedEncodingContainer {
} }
func encode<T>(_ value: T) throws where T : Encodable { func encode<T>(_ value: T) throws where T : Encodable {
let innerEncoder = _AdaptedPostboxEncoder() let typeHash: Int32 = murMurHashString32("\(type(of: value))")
let innerEncoder = _AdaptedPostboxEncoder(typeHash: typeHash)
try! value.encode(to: innerEncoder) try! value.encode(to: innerEncoder)
let (data, _) = innerEncoder.makeData() let (data, _) = innerEncoder.makeData()
let buffer = WriteBuffer() let buffer = WriteBuffer()
var typeHash: Int32 = murMurHashString32("\(type(of: value))")
buffer.write(&typeHash, offset: 0, length: 4)
var length: Int32 = Int32(data.count)
buffer.write(&length, offset: 0, length: 4)
buffer.write(data) buffer.write(data)

View File

@ -52,4 +52,33 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
func onLayout() { func onLayout() {
} }
final class SnapshotState {
fileprivate let snapshotView: UIView
fileprivate init(snapshotView: UIView) {
self.snapshotView = snapshotView
}
}
func prepareSnapshotState() -> SnapshotState {
let snapshotView = self.avatarNode.view.snapshotView(afterScreenUpdates: false)!
return SnapshotState(
snapshotView: snapshotView
)
}
func animateFromSnapshot(_ snapshotState: SnapshotState) {
self.avatarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.avatarNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true)
snapshotState.snapshotView.frame = self.frame
self.containerNode.view.addSubview(snapshotState.snapshotView)
let snapshotView = snapshotState.snapshotView
snapshotState.snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
} }

View File

@ -7036,11 +7036,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
if let navigationController = strongSelf.effectiveNavigationController, let snapshotView = strongSelf.chatDisplayNode.view.snapshotView(afterScreenUpdates: false) { if let navigationController = strongSelf.effectiveNavigationController {
snapshotView.frame = strongSelf.view.bounds let snapshotState = strongSelf.chatDisplayNode.prepareSnapshotState(
strongSelf.view.addSubview(snapshotView) titleViewSnapshotState: strongSelf.chatTitleView?.prepareSnapshotState(),
avatarSnapshotState: (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.prepareSnapshotState()
)
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), animated: false, completion: { nextController in strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), animated: false, completion: { nextController in
(nextController as! ChatControllerImpl).animateFromPreviousController(snapshotView: snapshotView) (nextController as! ChatControllerImpl).animateFromPreviousController(snapshotState: snapshotState)
})) }))
} }
} }
@ -7048,10 +7050,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.displayNodeDidLoad() self.displayNodeDidLoad()
} }
private var storedAnimateFromSnapshotView: UIView? private var storedAnimateFromSnapshotState: ChatControllerNode.SnapshotState?
private func animateFromPreviousController(snapshotView: UIView) { private func animateFromPreviousController(snapshotState: ChatControllerNode.SnapshotState) {
self.storedAnimateFromSnapshotView = snapshotView self.storedAnimateFromSnapshotState = snapshotState
} }
override public func viewWillAppear(_ animated: Bool) { override public func viewWillAppear(_ animated: Bool) {
@ -7106,7 +7108,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.didAppear = true self.didAppear = true
self.chatDisplayNode.historyNode.preloadPages = true
self.chatDisplayNode.historyNode.experimentalSnapScrollToItem = false self.chatDisplayNode.historyNode.experimentalSnapScrollToItem = false
self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in
return a && b return a && b
@ -7455,15 +7456,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}) })
} }
if let snapshotView = self.storedAnimateFromSnapshotView { if let snapshotState = self.storedAnimateFromSnapshotState {
self.storedAnimateFromSnapshotView = nil self.storedAnimateFromSnapshotState = nil
snapshotView.frame = self.view.bounds.offsetBy(dx: 0.0, dy: -self.view.bounds.height) if let titleViewSnapshotState = snapshotState.titleViewSnapshotState {
self.view.insertSubview(snapshotView, at: 0) self.chatTitleView?.animateFromSnapshot(titleViewSnapshotState)
}
self.view.layer.animateBoundsOriginYAdditive(from: -self.view.bounds.height, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, completion: { [weak snapshotView] _ in if let avatarSnapshotState = snapshotState.avatarSnapshotState {
snapshotView?.removeFromSuperview() (self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.animateFromSnapshot(avatarSnapshotState)
}) }
self.chatDisplayNode.animateFromSnapshot(snapshotState)
} }
} }

View File

@ -2498,4 +2498,86 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
return false return false
} }
} }
final class SnapshotState {
fileprivate let historySnapshotState: ChatHistoryListNode.SnapshotState
let titleViewSnapshotState: ChatTitleView.SnapshotState?
let avatarSnapshotState: ChatAvatarNavigationNode.SnapshotState?
let navigationButtonsSnapshotState: ChatHistoryNavigationButtons.SnapshotState
let titleAccessoryPanelSnapshot: UIView?
let navigationBarHeight: CGFloat
fileprivate init(
historySnapshotState: ChatHistoryListNode.SnapshotState,
titleViewSnapshotState: ChatTitleView.SnapshotState?,
avatarSnapshotState: ChatAvatarNavigationNode.SnapshotState?,
navigationButtonsSnapshotState: ChatHistoryNavigationButtons.SnapshotState,
titleAccessoryPanelSnapshot: UIView?,
navigationBarHeight: CGFloat
) {
self.historySnapshotState = historySnapshotState
self.titleViewSnapshotState = titleViewSnapshotState
self.avatarSnapshotState = avatarSnapshotState
self.navigationButtonsSnapshotState = navigationButtonsSnapshotState
self.titleAccessoryPanelSnapshot = titleAccessoryPanelSnapshot
self.navigationBarHeight = navigationBarHeight
}
}
func prepareSnapshotState(
titleViewSnapshotState: ChatTitleView.SnapshotState?,
avatarSnapshotState: ChatAvatarNavigationNode.SnapshotState?
) -> SnapshotState {
var titleAccessoryPanelSnapshot: UIView?
if let titleAccessoryPanelNode = self.titleAccessoryPanelNode, let snapshot = titleAccessoryPanelNode.view.snapshotView(afterScreenUpdates: false) {
snapshot.frame = titleAccessoryPanelNode.frame
titleAccessoryPanelSnapshot = snapshot
}
return SnapshotState(
historySnapshotState: self.historyNode.prepareSnapshotState(),
titleViewSnapshotState: titleViewSnapshotState,
avatarSnapshotState: avatarSnapshotState,
navigationButtonsSnapshotState: self.navigateButtons.prepareSnapshotState(),
titleAccessoryPanelSnapshot: titleAccessoryPanelSnapshot,
navigationBarHeight: self.navigationBar?.backgroundNode.bounds.height ?? 0.0
)
}
func animateFromSnapshot(_ snapshotState: SnapshotState) {
self.historyNode.animateFromSnapshot(snapshotState.historySnapshotState)
self.navigateButtons.animateFromSnapshot(snapshotState.navigationButtonsSnapshotState)
if let titleAccessoryPanelSnapshot = snapshotState.titleAccessoryPanelSnapshot {
self.titleAccessoryPanelContainer.view.addSubview(titleAccessoryPanelSnapshot)
if let _ = self.titleAccessoryPanelNode {
titleAccessoryPanelSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak titleAccessoryPanelSnapshot] _ in
titleAccessoryPanelSnapshot?.removeFromSuperview()
})
titleAccessoryPanelSnapshot.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -10.0), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
} else {
titleAccessoryPanelSnapshot.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -titleAccessoryPanelSnapshot.bounds.height), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak titleAccessoryPanelSnapshot] _ in
titleAccessoryPanelSnapshot?.removeFromSuperview()
})
}
}
if let titleAccessoryPanelNode = self.titleAccessoryPanelNode {
if let _ = snapshotState.titleAccessoryPanelSnapshot {
titleAccessoryPanelNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true)
titleAccessoryPanelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: true)
} else {
titleAccessoryPanelNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -titleAccessoryPanelNode.bounds.height), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true)
}
}
if let navigationBar = self.navigationBar {
let currentFrame = navigationBar.backgroundNode.frame
var previousFrame = currentFrame
previousFrame.size.height = snapshotState.navigationBarHeight
if previousFrame != currentFrame {
navigationBar.backgroundNode.update(size: previousFrame.size, transition: .immediate)
navigationBar.backgroundNode.update(size: currentFrame.size, transition: .animated(duration: 0.5, curve: .spring))
}
}
}
} }

View File

@ -585,6 +585,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
nextClientId += 1 nextClientId += 1
super.init() super.init()
self.clipsToBounds = false
self.accessibilityPageScrolledString = { [weak self] row, count in self.accessibilityPageScrolledString = { [weak self] row, count in
if let strongSelf = self { if let strongSelf = self {
@ -625,7 +627,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
} }
} }
self.preloadPages = false self.preloadPages = true
switch self.mode { switch self.mode {
case .bubbles: case .bubbles:
self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0) self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
@ -2407,4 +2409,76 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
} }
var animationCorrelationMessageFound: ((ChatMessageItemView, Int64?) -> Void)? var animationCorrelationMessageFound: ((ChatMessageItemView, Int64?) -> Void)?
final class SnapshotState {
fileprivate let snapshotTopInset: CGFloat
fileprivate let snapshotBottomInset: CGFloat
fileprivate let snapshotView: UIView
fileprivate init(
snapshotTopInset: CGFloat,
snapshotBottomInset: CGFloat,
snapshotView: UIView
) {
self.snapshotTopInset = snapshotTopInset
self.snapshotBottomInset = snapshotBottomInset
self.snapshotView = snapshotView
}
}
func prepareSnapshotState() -> SnapshotState {
var snapshotTopInset: CGFloat = 0.0
var snapshotBottomInset: CGFloat = 0.0
self.forEachItemNode { itemNode in
let topOverflow = itemNode.frame.maxY - self.bounds.height
snapshotTopInset = max(snapshotTopInset, topOverflow)
if itemNode.frame.minY < 0.0 {
snapshotBottomInset = max(snapshotBottomInset, -itemNode.frame.minY)
}
}
let snapshotView = self.view.snapshotView(afterScreenUpdates: false)!
let currentSnapshotView = self.view.snapshotView(afterScreenUpdates: false)!
currentSnapshotView.frame = self.view.bounds
if let sublayers = self.layer.sublayers {
for sublayer in sublayers {
sublayer.isHidden = true
}
}
self.view.addSubview(currentSnapshotView)
return SnapshotState(
snapshotTopInset: snapshotTopInset,
snapshotBottomInset: snapshotBottomInset,
snapshotView: snapshotView
)
}
func animateFromSnapshot(_ snapshotState: SnapshotState) {
var snapshotTopInset: CGFloat = 0.0
var snapshotBottomInset: CGFloat = 0.0
self.forEachItemNode { itemNode in
let topOverflow = itemNode.frame.maxY - self.bounds.height
snapshotTopInset = max(snapshotTopInset, topOverflow)
if itemNode.frame.minY < 0.0 {
snapshotBottomInset = max(snapshotBottomInset, -itemNode.frame.minY)
}
}
let snapshotParentView = UIView()
snapshotParentView.addSubview(snapshotState.snapshotView)
snapshotParentView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
snapshotParentView.frame = self.view.frame
snapshotState.snapshotView.frame = snapshotParentView.bounds
self.view.superview?.insertSubview(snapshotParentView, belowSubview: self.view)
snapshotParentView.layer.animatePosition(from: CGPoint(x: 0.0, y: 0.0), to: CGPoint(x: 0.0, y: -self.view.bounds.height - snapshotState.snapshotBottomInset - snapshotTopInset), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak snapshotParentView] _ in
snapshotParentView?.removeFromSuperview()
})
self.view.layer.animatePosition(from: CGPoint(x: 0.0, y: self.view.bounds.height + snapshotTopInset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true)
}
} }

View File

@ -163,4 +163,34 @@ final class ChatHistoryNavigationButtons: ASDisplayNode {
} }
} }
} }
final class SnapshotState {
fileprivate let downButtonSnapshotView: UIView?
fileprivate init(
downButtonSnapshotView: UIView?
) {
self.downButtonSnapshotView = downButtonSnapshotView
}
}
func prepareSnapshotState() -> SnapshotState {
var downButtonSnapshotView: UIView?
if !self.downButton.isHidden {
downButtonSnapshotView = self.downButton.view.snapshotView(afterScreenUpdates: false)!
}
return SnapshotState(
downButtonSnapshotView: downButtonSnapshotView
)
}
func animateFromSnapshot(_ snapshotState: SnapshotState) {
if self.downButton.isHidden != (snapshotState.downButtonSnapshotView == nil) {
if self.downButton.isHidden {
} else {
self.downButton.layer.animateAlpha(from: 0.0, to: self.downButton.alpha, duration: 0.3)
self.downButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true)
}
}
}
} }

View File

@ -736,4 +736,33 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
} }
return super.hitTest(point, with: event) return super.hitTest(point, with: event)
} }
final class SnapshotState {
fileprivate let snapshotView: UIView
fileprivate init(snapshotView: UIView) {
self.snapshotView = snapshotView
}
}
func prepareSnapshotState() -> SnapshotState {
let snapshotView = self.snapshotView(afterScreenUpdates: false)!
return SnapshotState(
snapshotView: snapshotView
)
}
func animateFromSnapshot(_ snapshotState: SnapshotState) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.layer.animatePosition(from: CGPoint(x: 0.0, y: 20.0), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true)
snapshotState.snapshotView.frame = self.frame
self.superview?.insertSubview(snapshotState.snapshotView, belowSubview: self)
let snapshotView = snapshotState.snapshotView
snapshotState.snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -20.0), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
}
} }