Story animation transitions

This commit is contained in:
Ali 2023-06-09 11:11:57 +04:00
parent 69e49f9196
commit 3a2f75ab82
17 changed files with 205 additions and 46 deletions

View File

@ -2277,7 +2277,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
fileprivate func openStoryCamera() {
var cameraTransitionIn: StoryCameraTransitionIn?
if let componentView = self.chatListHeaderView() {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
cameraTransitionIn = StoryCameraTransitionIn(
sourceView: transitionView,
sourceRect: transitionView.bounds,
@ -2292,7 +2292,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return nil
}
if let componentView = self.chatListHeaderView() {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
return StoryCameraTransitionOut(
destinationView: transitionView,
destinationRect: transitionView.bounds,
@ -2340,7 +2340,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
var transitionIn: StoryContainerScreen.TransitionIn?
if let peer, let componentView = self.chatListHeaderView() {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) {
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: transitionView,
sourceRect: transitionView.bounds,
@ -2359,10 +2359,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
if let componentView = self.chatListHeaderView() {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
if let (transitionView, transitionContentView) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
return StoryContainerScreen.TransitionOut(
destinationView: transitionView,
transitionView: nil,
transitionView: transitionContentView,
destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height * 0.5,
destinationIsAvatar: true,
@ -2374,6 +2374,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return nil
}
)
if let componentView = self.chatListHeaderView() {
componentView.storyPeerListView()?.setPreviewedItem(signal: storyContainerScreen.focusedItem)
}
self.push(storyContainerScreen)
})
}
@ -2446,7 +2449,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
public func transitionViewForOwnStoryItem() -> UIView? {
if let componentView = self.chatListHeaderView() {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
return transitionView
}
}
@ -2455,7 +2458,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
public func animateStoryUploadRipple() {
if let componentView = self.chatListHeaderView() {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
let localRect = transitionView.convert(transitionView.bounds, to: self.view)
self.animateRipple(centerLocation: localRect.center)
}
@ -4778,7 +4781,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return nil
}
if finished, let componentView = self.chatListHeaderView() {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
return StoryCameraTransitionOut(
destinationView: transitionView,
destinationRect: transitionView.bounds,

View File

@ -41,6 +41,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stories/StoryContentComponent",
"//submodules/TelegramUI/Components/Stories/StoryPeerListComponent",
"//submodules/TelegramUI/Components/ChatListTitleView",
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
"//submodules/ComponentFlow",
],
visibility = [

View File

@ -529,7 +529,7 @@ public class ContactsController: ViewController {
var transitionIn: StoryContainerScreen.TransitionIn?
if let peer, let componentView = self.chatListHeaderView() {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) {
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peer.id) {
transitionIn = StoryContainerScreen.TransitionIn(
sourceView: transitionView,
sourceRect: transitionView.bounds,
@ -548,10 +548,10 @@ public class ContactsController: ViewController {
}
if let componentView = self.chatListHeaderView() {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
if let (transitionView, transitionContentView) = componentView.storyPeerListView()?.transitionViewForItem(peerId: peerId) {
return StoryContainerScreen.TransitionOut(
destinationView: transitionView,
transitionView: nil,
transitionView: transitionContentView,
destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height * 0.5,
destinationIsAvatar: true,

View File

@ -372,6 +372,7 @@ swift_library(
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen",
"//submodules/TelegramUI/Components/MoreHeaderButton",
] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View File

@ -23,6 +23,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stories/StoryPeerListComponent",
"//submodules/Components/ComponentDisplayAdapters",
"//submodules/SearchUI",
"//submodules/TelegramUI/Components/MoreHeaderButton",
],
visibility = [
"//visibility:public",

View File

@ -8,6 +8,7 @@ import ChatListTitleView
import AppBundle
import StoryPeerListComponent
import TelegramCore
import MoreHeaderButton
public final class HeaderNetworkStatusComponent: Component {
public enum Content: Equatable {

View File

@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MoreHeaderButton",
module_name = "MoreHeaderButton",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AsyncDisplayKit",
"//submodules/AnimationUI",
],
visibility = [
"//visibility:public",
],
)

View File

@ -25,6 +25,7 @@ swift_library(
"//submodules/ContextUI",
"//submodules/UndoUI",
"//submodules/TelegramUI/Components/BottomButtonPanelComponent",
"//submodules/TelegramUI/Components/MoreHeaderButton",
],
visibility = [
"//visibility:public",

View File

@ -13,6 +13,7 @@ import ContextUI
import ChatTitleView
import BottomButtonPanelComponent
import UndoUI
import MoreHeaderButton
final class PeerInfoStoryGridScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment

View File

@ -696,6 +696,42 @@ public final class StoryItemSetContainerComponent: Component {
if let rightInfoView = self.rightInfoItem?.view.view {
if transitionOut.destinationIsAvatar {
let transitionView = transitionOut.transitionView
let transitionViewImpl = transitionView?.makeView()
if let transitionViewImpl {
self.insertSubview(transitionViewImpl, aboveSubview: self.contentContainerView)
let rightInfoSourceFrame = rightInfoView.convert(rightInfoView.bounds, to: self)
let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: sourceLocalFrame.center, to: rightInfoSourceFrame.center, elevation: 0.0, duration: 0.3, curve: .spring, reverse: true)
transitionViewImpl.frame = rightInfoSourceFrame
transitionViewImpl.alpha = 0.0
transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState(
sourceSize: rightInfoSourceFrame.size,
destinationSize: sourceLocalFrame.size,
progress: 0.0
), .immediate)
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
transitionViewImpl.alpha = 1.0
transitionViewImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
rightInfoView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame)
transitionViewImpl.layer.position = positionKeyframes[positionKeyframes.count - 1]
transitionViewImpl.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: 0.3, keyPath: "position", removeOnCompletion: false, additive: false)
transitionViewImpl.layer.animateBounds(from: CGRect(origin: CGPoint(), size: rightInfoSourceFrame.size), to: CGRect(origin: CGPoint(), size: sourceLocalFrame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
transitionView?.updateView(transitionViewImpl, StoryContainerScreen.TransitionState(
sourceSize: rightInfoSourceFrame.size,
destinationSize: sourceLocalFrame.size,
progress: 1.0
), transition)
}
let positionKeyframes: [CGPoint] = generateParabollicMotionKeyframes(from: innerSourceLocalFrame.center, to: rightInfoView.layer.position, elevation: 0.0, duration: 0.3, curve: .spring, reverse: true)
rightInfoView.layer.position = positionKeyframes[positionKeyframes.count - 1]
rightInfoView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: 0.3, keyPath: "position", removeOnCompletion: false, additive: false)
@ -715,6 +751,7 @@ public final class StoryItemSetContainerComponent: Component {
removeOnCompletion: false
)
if !transitionOut.destinationIsAvatar {
let transitionView = transitionOut.transitionView
let transitionViewImpl = transitionView?.makeView()
if let transitionViewImpl {
@ -727,9 +764,7 @@ public final class StoryItemSetContainerComponent: Component {
destinationSize: sourceLocalFrame.size,
progress: 0.0
), .immediate)
}
if let transitionViewImpl {
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
transitionViewImpl.alpha = 1.0
@ -743,6 +778,7 @@ public final class StoryItemSetContainerComponent: Component {
progress: 1.0
), transition)
}
}
if let component = self.component, let visibleItemView = self.visibleItems[component.slice.item.id]?.view.view {
let innerScale = innerSourceLocalFrame.width / visibleItemView.bounds.width

View File

@ -14,10 +14,10 @@ swift_library(
"//submodules/ComponentFlow",
"//submodules/AppBundle",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/ChatListHeaderComponent",
"//submodules/AnimatedAvatarSetNode",
"//submodules/AccountContext",
"//submodules/TelegramCore",
"//submodules/TelegramUI/Components/MoreHeaderButton",
],
visibility = [
"//visibility:public",

View File

@ -4,10 +4,10 @@ import Display
import ComponentFlow
import AppBundle
import BundleIconComponent
import ChatListHeaderComponent
import AnimatedAvatarSetNode
import AccountContext
import TelegramCore
import MoreHeaderButton
public final class StoryFooterPanelComponent: Component {
public let context: AccountContext

View File

@ -20,6 +20,7 @@ swift_library(
"//submodules/TelegramPresentationData",
"//submodules/AvatarNode",
"//submodules/ContextUI",
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
],
visibility = [
"//visibility:public",

View File

@ -6,8 +6,10 @@ import AppBundle
import BundleIconComponent
import AccountContext
import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import StoryContainerScreen
public final class StoryPeerListComponent: Component {
public final class ExternalState {
@ -142,6 +144,9 @@ public final class StoryPeerListComponent: Component {
private var requestedLoadMoreToken: String?
private let loadMoreDisposable = MetaDisposable()
private var previewedItemDisposable: Disposable?
private var previewedItemId: EnginePeer.Id?
public override init(frame: CGRect) {
self.collapsedButton = HighlightableButton()
@ -187,6 +192,7 @@ public final class StoryPeerListComponent: Component {
deinit {
self.loadMoreDisposable.dispose()
self.previewedItemDisposable?.dispose()
}
@objc private func collapsedButtonPressed() {
@ -196,12 +202,41 @@ public final class StoryPeerListComponent: Component {
component.peerAction(nil)
}
public func transitionViewForItem(peerId: EnginePeer.Id) -> UIView? {
public func setPreviewedItem(signal: Signal<StoryId?, NoError>) {
self.previewedItemDisposable?.dispose()
self.previewedItemDisposable = (signal |> map(\.?.peerId) |> distinctUntilChanged |> deliverOnMainQueue).start(next: { [weak self] itemId in
guard let self else {
return
}
self.previewedItemId = itemId
for (peerId, visibleItem) in self.visibleItems {
if let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View {
itemView.updateIsPreviewing(isPreviewing: peerId == itemId)
}
}
})
}
public func transitionViewForItem(peerId: EnginePeer.Id) -> (UIView, StoryContainerScreen.TransitionView)? {
if self.collapsedButton.isUserInteractionEnabled {
return nil
}
if let visibleItem = self.visibleItems[peerId], let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View {
return itemView.transitionView()
if !self.scrollView.bounds.intersects(itemView.frame) {
return nil
}
return itemView.transitionView().flatMap { transitionView in
return (transitionView, StoryContainerScreen.TransitionView(
makeView: { [weak itemView] in
return StoryPeerListItemComponent.TransitionView(itemView: itemView)
},
updateView: { view, state, transition in
(view as? StoryPeerListItemComponent.TransitionView)?.update(state: state, transition: transition)
}
))
}
}
return nil
}
@ -394,6 +429,8 @@ public final class StoryPeerListComponent: Component {
itemTransition.setFrame(view: itemView.backgroundContainer, frame: itemFrame)
itemTransition.setAlpha(view: itemView.backgroundContainer, alpha: itemAlpha)
itemTransition.setScale(view: itemView.backgroundContainer, scale: itemScale)
itemView.updateIsPreviewing(isPreviewing: self.previewedItemId == itemSet.peer.id)
}
}

View File

@ -11,6 +11,7 @@ import TelegramPresentationData
import AvatarNode
import ContextUI
import AsyncDisplayKit
import StoryContainerScreen
private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, radius: CGFloat) -> (point1Angle: CGFloat, point2Angle: CGFloat)? {
let distanceVector = CGPoint(x: otherCenter.x - center.x, y: otherCenter.y - center.y)
@ -144,6 +145,47 @@ private final class StoryProgressLayer: SimpleShapeLayer {
private var sharedAvatarBackgroundImage: UIImage?
public final class StoryPeerListItemComponent: Component {
public final class TransitionView: UIView {
private weak var itemView: StoryPeerListItemComponent.View?
private var snapshotView: UIView?
private var portalView: PortalView?
init(itemView: StoryPeerListItemComponent.View?) {
self.itemView = itemView
super.init(frame: CGRect())
if let itemView {
if let portalView = PortalView(matchPosition: false) {
itemView.avatarContent.addPortal(view: portalView)
self.portalView = portalView
self.addSubview(portalView.view)
}
/*if let snapshotView = itemView.avatarContent.snapshotView(afterScreenUpdates: false) {
self.addSubview(snapshotView)
self.snapshotView = snapshotView
}*/
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(state: StoryContainerScreen.TransitionState, transition: Transition) {
let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress)
if let snapshotView = self.snapshotView {
transition.setPosition(view: snapshotView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: snapshotView, scale: size.width / state.destinationSize.width)
}
if let portalView = self.portalView {
transition.setPosition(view: portalView.view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
transition.setScale(view: portalView.view, scale: size.width / state.destinationSize.width)
}
}
}
public let context: AccountContext
public let theme: PresentationTheme
public let strings: PresentationStrings
@ -240,6 +282,7 @@ public final class StoryPeerListItemComponent: Component {
private let button: HighlightTrackingButton
fileprivate let avatarContent: PortalSourceView
private let avatarContainer: UIView
private let avatarBackgroundContainer: UIView
private let avatarBackgroundView: UIImageView
@ -266,6 +309,9 @@ public final class StoryPeerListItemComponent: Component {
self.extractedBackgroundView = UIImageView()
self.extractedBackgroundView.alpha = 0.0
self.avatarContent = PortalSourceView()
self.avatarContent.isUserInteractionEnabled = false
self.avatarContainer = UIView()
self.avatarContainer.isUserInteractionEnabled = false
@ -294,9 +340,10 @@ public final class StoryPeerListItemComponent: Component {
self.avatarBackgroundContainer.addSubview(self.avatarBackgroundView)
self.extractedContainerNode.contentNode.view.addSubview(self.button)
self.button.addSubview(self.avatarContainer)
self.avatarContent.addSubview(self.avatarContainer)
self.button.addSubview(self.avatarContent)
self.button.layer.addSublayer(self.indicatorColorLayer)
self.avatarContent.layer.addSublayer(self.indicatorColorLayer)
self.indicatorMaskLayer.addSublayer(self.indicatorShapeLayer)
self.indicatorColorLayer.mask = self.indicatorMaskLayer
@ -366,6 +413,10 @@ public final class StoryPeerListItemComponent: Component {
return self.avatarNode?.view
}
func updateIsPreviewing(isPreviewing: Bool) {
self.avatarContent.alpha = isPreviewing ? 0.0 : 1.0
}
func update(component: StoryPeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let size = availableSize
@ -439,7 +490,11 @@ public final class StoryPeerListItemComponent: Component {
peer: component.peer
)
avatarNode.updateSize(size: avatarSize)
transition.setPosition(view: self.avatarContainer, position: avatarFrame.center)
transition.setPosition(view: self.avatarContent, position: CGPoint(x: avatarFrame.midX, y: avatarFrame.midY))
transition.setBounds(view: self.avatarContent, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
transition.setPosition(view: self.avatarContainer, position: CGPoint(x: avatarFrame.width * 0.5, y: avatarFrame.height * 0.5))
transition.setBounds(view: self.avatarContainer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
transition.setPosition(view: self.avatarBackgroundContainer, position: avatarFrame.center)
@ -517,7 +572,7 @@ public final class StoryPeerListItemComponent: Component {
self.indicatorColorLayer.colors = colors
}
transition.setPosition(layer: self.indicatorColorLayer, position: indicatorFrame.center)
transition.setPosition(layer: self.indicatorColorLayer, position: indicatorFrame.offsetBy(dx: -avatarFrame.minX, dy: -avatarFrame.minY).center)
transition.setBounds(layer: self.indicatorColorLayer, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size))
transition.setPosition(layer: self.indicatorShapeLayer, position: CGPoint(x: indicatorFrame.width * 0.5, y: indicatorFrame.height * 0.5))
transition.setBounds(layer: self.indicatorShapeLayer, bounds: CGRect(origin: CGPoint(), size: indicatorFrame.size))

View File

@ -96,6 +96,7 @@ import LegacyCamera
import LegacyInstantVideoController
import StoryContainerScreen
import StoryContentComponent
import MoreHeaderButton
#if DEBUG
import os.signpost