From 157a7a978dbecb08d952ade5ec3154c74f461a2f Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 13 Feb 2026 17:08:48 +0400 Subject: [PATCH] Glass --- .../Source/Base/Transition.swift | 19 ++ .../Display/Source/ViewController.swift | 7 + .../ChatItemGalleryFooterContentNode.swift | 4 +- .../GalleryUI/Sources/GalleryController.swift | 4 +- .../Sources/GalleryControllerNode.swift | 14 +- .../GalleryUI/Sources/GalleryFooterNode.swift | 2 +- .../GalleryUI/Sources/GalleryItemNode.swift | 2 +- .../GalleryUI/Sources/GalleryPagerNode.swift | 1 + .../Items/UniversalVideoGalleryItem.swift | 24 +- .../ZoomableContentGalleryItemNode.swift | 14 +- .../Sources/TabBarContollerNode.swift | 17 +- .../TabBarUI/Sources/TabBarController.swift | 15 ++ .../Components/ContextControllerImpl/BUILD | 1 + .../ContextControllerActionsStackNode.swift | 12 +- ...tControllerExtractedPresentationNode.swift | 2 +- .../Components/LensTransition/BUILD | 19 ++ .../Sources/LensTransitionContainer.swift | 206 ++++++++++++++++++ .../Sources/PeerInfoScreen.swift | 14 ++ .../Sources/TabBarComponent.swift | 65 ++++-- .../VideoPlaybackControlsComponent.swift | 10 + 20 files changed, 419 insertions(+), 33 deletions(-) create mode 100644 submodules/TelegramUI/Components/LensTransition/BUILD create mode 100644 submodules/TelegramUI/Components/LensTransition/Sources/LensTransitionContainer.swift diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 93886bc1a0..57b5f847e0 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -1493,6 +1493,25 @@ public struct ComponentTransition { }) } } + + public func animateScalarFloat(layer: CALayer, keyPath: String, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + completion?(true) + case let .curve(duration, curve): + layer.animate( + from: fromValue as NSNumber, + to: toValue as NSNumber, + keyPath: keyPath, + duration: duration, + delay: delay, + curve: curve, + removeOnCompletion: removeOnCompletion, + additive: false, + completion: completion + ) + } + } } private func generateParabolicMotionKeyframes( diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index a1ca25471c..f36d56686d 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -738,6 +738,13 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { open func tabBarItemContextAction(sourceView: ContextExtractedContentContainingView, gesture: ContextGesture) { } + open func tabBarItemHasDoubleTapAction() -> Bool { + return false + } + + open func tabBarItemPerformDoubleTapAction() { + } + open func tabBarDisabledAction() { } diff --git a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index 187ef43d59..69d9bf2752 100644 --- a/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift @@ -1506,7 +1506,9 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll transition.animatePositionAdditive(layer: scrubberView.layer, offset: CGPoint(x: 0.0, y: self.bounds.height - fromHeight)) } scrubberView.alpha = 1.0 - scrubberView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + if transition.isAnimated { + scrubberView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } } transition.animatePositionAdditive(node: self.scrollWrapperNode, offset: CGPoint(x: 0.0, y: self.bounds.height - fromHeight)) self.scrollNode.alpha = 0.0 diff --git a/submodules/GalleryUI/Sources/GalleryController.swift b/submodules/GalleryUI/Sources/GalleryController.swift index 98becb4ca7..9536622267 100644 --- a/submodules/GalleryUI/Sources/GalleryController.swift +++ b/submodules/GalleryUI/Sources/GalleryController.swift @@ -1475,7 +1475,7 @@ public class GalleryController: ViewController, StandalonePresentableController, self.prefersOnScreenNavigationHidden = !visible self.galleryNode.pager.forEachItemNode { itemNode in - itemNode.controlsVisibilityUpdated(isVisible: visible) + itemNode.controlsVisibilityUpdated(isVisible: visible, animated: true) } } @@ -1767,7 +1767,7 @@ public class GalleryController: ViewController, StandalonePresentableController, } if !self.isPresentedInPreviewingContext() { - self.galleryNode.setControlsHidden(self.landscape, animated: false) + //self.galleryNode.setControlsHidden(self.landscape, animated: false) if let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments { if presentationArguments.animated { self.galleryNode.animateIn(animateContent: !nodeAnimatesItself && !self.useSimpleAnimation, useSimpleAnimation: self.useSimpleAnimation) diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 4b19c53e4f..f0f6bbf31d 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -45,7 +45,8 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture private var isDismissed = false - public var areControlsHidden = false + private var didAnimateIn: Bool = false + public var areControlsHidden = true public var controlsVisibilityChanged: ((Bool) -> Void)? public var animateAlpha = true @@ -418,13 +419,14 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture } open func animateIn(animateContent: Bool, useSimpleAnimation: Bool) { + self.didAnimateIn = true + let duration: Double = animateContent ? 0.2 : 0.3 let backgroundColor = self.backgroundNode.backgroundColor ?? .black self.statusBar?.alpha = 0.0 self.navigationBar?.alpha = 0.0 - self.footerNode.alpha = 0.0 self.headerEdgeEffectView.alpha = 0.0 self.titleView?.alpha = 0.0 self.currentThumbnailContainerNode?.alpha = 0.0 @@ -440,13 +442,15 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture }) if !self.areControlsHidden { - self.footerNode.alpha = 1.0 self.footerNode.animateIn(transition: .animated(duration: 0.15, curve: .linear)) ComponentTransition.easeInOut(duration: 0.15).animateView { self.headerEdgeEffectView.alpha = 0.5 self.titleView?.alpha = 1.0 } + } else { + self.footerNode.animateIn(transition: .immediate) + self.footerNode.setVisibilityAlpha(0.0, animated: false) } if animateContent { @@ -627,6 +631,10 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture } open func updatePresentationState(_ f: (GalleryControllerPresentationState) -> GalleryControllerPresentationState, transition: ContainedViewLayoutTransition) { + var transition = transition + if !self.didAnimateIn { + transition = .immediate + } self.presentationState = f(self.presentationState) if let (navigationBarHeight, layout) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) diff --git a/submodules/GalleryUI/Sources/GalleryFooterNode.swift b/submodules/GalleryUI/Sources/GalleryFooterNode.swift index 950314e0fb..a2571990d0 100644 --- a/submodules/GalleryUI/Sources/GalleryFooterNode.swift +++ b/submodules/GalleryUI/Sources/GalleryFooterNode.swift @@ -35,7 +35,7 @@ public final class GalleryFooterNode: ASDisplayNode { self.visibilityAlpha = alpha let transition: ComponentTransition = animated ? .easeInOut(duration: 0.2) : .immediate transition.setAlpha(view: self.edgeEffectView, alpha: alpha * self.defaultEdgeEffectAlpha) - self.currentFooterContentNode?.setVisibilityAlpha(alpha, animated: true) + self.currentFooterContentNode?.setVisibilityAlpha(alpha, animated: animated) self.currentOverlayContentNode?.setVisibilityAlpha(alpha) } diff --git a/submodules/GalleryUI/Sources/GalleryItemNode.swift b/submodules/GalleryUI/Sources/GalleryItemNode.swift index cf7ec9510e..a0936b7286 100644 --- a/submodules/GalleryUI/Sources/GalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/GalleryItemNode.swift @@ -94,7 +94,7 @@ open class GalleryItemNode: ASDisplayNode { open func visibilityUpdated(isVisible: Bool) { } - open func controlsVisibilityUpdated(isVisible: Bool) { + open func controlsVisibilityUpdated(isVisible: Bool, animated: Bool) { } open func adjustForPreviewing() { diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index 49a57c5206..457e6a429b 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -652,6 +652,7 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest let node = self.makeNodeForItem(at: self.centralItemIndex ?? 0, synchronous: synchronous) node.frame = CGRect(origin: CGPoint(), size: self.scrollView.bounds.size) if let containerLayout = self.containerLayout { + node.controlsVisibilityUpdated(isVisible: self.controlsVisibility(), animated: false) node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) } self.addVisibleItemNode(node) diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 9faf3d495b..de7027f440 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -1034,7 +1034,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let nextTimestamp = timestamp + delta if nextTimestamp > duration { strongVideoNode.seek(0.0) - strongVideoNode.pause() + strongVideoNode.play() } else { strongVideoNode.seek(min(duration, timestamp + delta)) } @@ -1159,6 +1159,24 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return false } + override func contentDoubleTapAction(location: CGPoint) -> Bool { + if case let .message(message, _) = self.item?.contentInfo, let _ = message.adAttribute { + self.item?.performAction(.ad(message.id)) + return true + } + + if case let .playback(_, seekable) = self.footerContentNode.content, seekable { + if location.x >= self.bounds.width * 0.3 { + self.footerContentNode.seekForward?(15.0) + } else { + self.footerContentNode.seekBackward?(15.0) + } + return true + } + + return false + } + override func screenFrameUpdated(_ frame: CGRect) { let center = frame.midX - self.frame.width / 2.0 self.subnodeTransform = CATransform3DMakeTranslation(-center * 0.16, 0.0, 0.0) @@ -1960,7 +1978,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - override func controlsVisibilityUpdated(isVisible: Bool) { + override func controlsVisibilityUpdated(isVisible: Bool, animated: Bool) { self.areControlsVisible = isVisible self.controlsVisiblePromise.set(isVisible) @@ -1968,7 +1986,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.videoNode?.notifyPlaybackControlsHidden(!isVisible) if let validLayout = self.validLayout { - self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .animated(duration: 0.2, curve: .easeInOut)) + self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) } } diff --git a/submodules/GalleryUI/Sources/ZoomableContentGalleryItemNode.swift b/submodules/GalleryUI/Sources/ZoomableContentGalleryItemNode.swift index f07d828144..b5c2b8241b 100644 --- a/submodules/GalleryUI/Sources/ZoomableContentGalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/ZoomableContentGalleryItemNode.swift @@ -63,19 +63,27 @@ open class ZoomableContentGalleryItemNode: GalleryItemNode, ASScrollViewDelegate return false } + open func contentDoubleTapAction(location: CGPoint) -> Bool { + return false + } + @objc open func contentTap(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { if recognizer.state == .ended { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { let pointInNode = self.scrollNode.view.convert(location, to: self.view) if pointInNode.x < 44.0 || pointInNode.x > self.frame.width - 44.0 { } else { - if self.contentTapAction() { - return - } switch gesture { case .tap: + if self.contentTapAction() { + return + } self.toggleControlsVisibility() case .doubleTap: + if self.contentDoubleTapAction(location: pointInNode) { + return + } + if let contentView = self.zoomableContent?.1.view, self.scrollNode.view.zoomScale.isLessThanOrEqualTo(self.scrollNode.view.minimumZoomScale) { let pointInView = self.scrollNode.view.convert(location, to: contentView) diff --git a/submodules/TabBarUI/Sources/TabBarContollerNode.swift b/submodules/TabBarUI/Sources/TabBarContollerNode.swift index c4de51c38f..a1df8ff032 100644 --- a/submodules/TabBarUI/Sources/TabBarContollerNode.swift +++ b/submodules/TabBarUI/Sources/TabBarContollerNode.swift @@ -51,6 +51,8 @@ final class TabBarControllerNode: ASDisplayNode { private var theme: PresentationTheme private var strings: PresentationStrings private let itemSelected: (Int, Bool, [ASDisplayNode]) -> Void + private let itemHasDoubleTapAction: (Int) -> Bool + private let itemDoubleTapped: (Int) -> Void private let contextAction: (Int, ContextExtractedContentContainingView, ContextGesture) -> Void private let tabBarView = ComponentView() @@ -103,10 +105,12 @@ final class TabBarControllerNode: ASDisplayNode { return nil } - init(theme: PresentationTheme, strings: PresentationStrings, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingView, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void, activateSearch: @escaping () -> Void, deactivateSearch: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, itemHasDoubleTapAction: @escaping (Int) -> Bool, itemDoubleTapped: @escaping (Int) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingView, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void, activateSearch: @escaping () -> Void, deactivateSearch: @escaping () -> Void) { self.theme = theme self.strings = strings self.itemSelected = itemSelected + self.itemHasDoubleTapAction = itemHasDoubleTapAction + self.itemDoubleTapped = itemDoubleTapped self.contextAction = contextAction self.disabledOverlayNode = ASDisplayNode() self.disabledOverlayNode.backgroundColor = theme.rootController.tabBar.backgroundColor.withAlphaComponent(0.5) @@ -230,6 +234,9 @@ final class TabBarControllerNode: ASDisplayNode { strings: self.strings, items: self.tabBarItems.map { item in let itemId = AnyHashable(ObjectIdentifier(item.item)) + + let index = self.tabBarItems.firstIndex(where: { AnyHashable(ObjectIdentifier($0.item)) == itemId }) ?? 0 + return TabBarComponent.Item( item: item.item, action: { [weak self] isLongTap in @@ -240,6 +247,14 @@ final class TabBarControllerNode: ASDisplayNode { self.itemSelected(index, isLongTap, []) } }, + doubleTapAction: self.itemHasDoubleTapAction(index) ? { [weak self] in + guard let self else { + return + } + if let index = self.tabBarItems.firstIndex(where: { AnyHashable(ObjectIdentifier($0.item)) == itemId }) { + self.itemDoubleTapped(index) + } + } : nil, contextAction: { [weak self] gesture, sourceView in guard let self else { return diff --git a/submodules/TabBarUI/Sources/TabBarController.swift b/submodules/TabBarUI/Sources/TabBarController.swift index 111fe208d1..a71c2b09de 100644 --- a/submodules/TabBarUI/Sources/TabBarController.swift +++ b/submodules/TabBarUI/Sources/TabBarController.swift @@ -219,6 +219,21 @@ open class TabBarControllerImpl: ViewController, TabBarController { } })) } + }, itemHasDoubleTapAction: { [weak self] index in + guard let self else { + return false + } + if index >= 0 && index < self.tabBarControllerNode.tabBarItems.count { + return self.controllers[index].tabBarItemHasDoubleTapAction() + } + return false + }, itemDoubleTapped: { [weak self] index in + guard let self else { + return + } + if index >= 0 && index < self.tabBarControllerNode.tabBarItems.count { + self.controllers[index].tabBarItemPerformDoubleTapAction() + } }, contextAction: { [weak self] index, view, gesture in guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/BUILD b/submodules/TelegramUI/Components/ContextControllerImpl/BUILD index e88fcdb1ed..0f4c45d696 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/BUILD +++ b/submodules/TelegramUI/Components/ContextControllerImpl/BUILD @@ -34,6 +34,7 @@ swift_library( "//submodules/TextSelectionNode", "//submodules/UIKitRuntimeUtils", "//submodules/UndoUI", + "//submodules/TelegramUI/Components/LensTransition", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift index 1a1e7bb9e4..19c64c874e 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerActionsStackNode.swift @@ -19,6 +19,7 @@ import GlassBackgroundComponent import LottieComponent import TextNodeWithEntities import ContextUI +import LensTransition public protocol ContextControllerActionsListItemNode: ASDisplayNode { func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void) @@ -1411,7 +1412,7 @@ public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, Context let backgroundContainerInset: CGFloat let backgroundView: GlassBackgroundView var sourceExtractableContainer: ContextExtractableContainer? - let contentContainer: UIView + let contentContainer: LensTransitionContainer var requestUpdate: ((ContainedViewLayoutTransition) -> Void)? var requestPop: (() -> Void)? @@ -1430,7 +1431,7 @@ public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, Context self.backgroundView = GlassBackgroundView() self.backgroundContainer.contentView.addSubview(self.backgroundView) - self.contentContainer = UIView() + self.contentContainer = LensTransitionContainer() self.contentContainer.clipsToBounds = true self.backgroundView.contentView.addSubview(self.contentContainer) @@ -1512,11 +1513,13 @@ public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, Context } self.contentContainer.frame = CGRect(origin: CGPoint(), size: sourceSize) + self.contentContainer.update(size: sourceSize, cornerRadius: min(sourceSize.width, sourceSize.height) * 0.5, state: .animatedOut, transition: .immediate) self.contentContainer.layer.cornerRadius = normalCornerRadius extractableContainer.extractableContentView.frame = CGRect(origin: CGPoint(x: (currentSize.width - sourceSize.width) * 0.5, y: (currentSize.height - sourceSize.height) * 0.5), size: sourceSize).offsetBy(dx: self.backgroundContainerInset, dy: self.backgroundContainerInset) transition.setFrame(view: extractableContainer.extractableContentView, frame: CGRect(origin: CGPoint(x: self.backgroundContainerInset, y: self.backgroundContainerInset), size: currentSize)) transition.setFrame(view: self.contentContainer, frame: CGRect(origin: CGPoint(), size: currentSize)) + self.contentContainer.update(size: currentSize, cornerRadius: 30.0, state: .animatedIn, transition: transition) transition.setCornerRadius(layer: self.contentContainer.layer, cornerRadius: 30.0) self.contentContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) @@ -1540,6 +1543,8 @@ public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, Context transition.setFrame(view: extractableContainer.extractableContentView, frame: CGRect(origin: CGPoint(x: self.backgroundContainerInset, y: self.backgroundContainerInset), size: normalSize).offsetBy(dx: (currentSize.width - normalSize.width) * 0.5, dy: (currentSize.height - normalSize.height) * 0.5)) transition.setFrame(view: self.contentContainer, frame: CGRect(origin: CGPoint(), size: normalSize)) + self.contentContainer.update(size: normalSize, cornerRadius: normalCornerRadius, state: .animatedOut, transition: transition) + transition.setCornerRadius(layer: self.contentContainer.layer, cornerRadius: normalCornerRadius) transition.setAlpha(view: self.contentContainer, alpha: 0.0) @@ -1561,6 +1566,7 @@ public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, Context let transition = ComponentTransition(transition) transition.setFrame(view: self.contentContainer, frame: CGRect(origin: CGPoint(), size: size)) + transition.setCornerRadius(layer: self.contentContainer.layer, cornerRadius: min(30.0, size.height * 0.5)) let backgroundContainerFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -self.backgroundContainerInset, dy: -self.backgroundContainerInset) @@ -1569,8 +1575,6 @@ public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, Context transition.setFrame(view: self.backgroundContainer, frame: backgroundContainerFrame) } - transition.setCornerRadius(layer: self.contentContainer.layer, cornerRadius: min(30.0, size.height * 0.5)) - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: self.backgroundContainerInset, y: self.backgroundContainerInset), size: size)) self.backgroundView.update(size: size, cornerRadius: min(30.0, size.height * 0.5), isDark: presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition) diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift index d7b3b69bab..ff2b6e7a2e 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift @@ -1352,7 +1352,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if let contextExtractableContainer { let positionTransition = ComponentTransition(animation: .curve(duration: 0.4, curve: .bounce(stiffness: 900.0, damping: 95.0))) - let transition = ComponentTransition(animation: .curve(duration: 0.5, curve: .bounce(stiffness: 900.0, damping: 95.0))) + let transition = ComponentTransition(animation: .curve(duration: 0.9, curve: .bounce(stiffness: 900.0, damping: 95.0))) positionTransition.animatePosition(layer: self.actionsContainerNode.layer, from: CGPoint( x: contextExtractableContainer.sourceRect.midX - self.actionsContainerNode.frame.midX, diff --git a/submodules/TelegramUI/Components/LensTransition/BUILD b/submodules/TelegramUI/Components/LensTransition/BUILD new file mode 100644 index 0000000000..ade2df5c4e --- /dev/null +++ b/submodules/TelegramUI/Components/LensTransition/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "LensTransition", + module_name = "LensTransition", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/LensTransition/Sources/LensTransitionContainer.swift b/submodules/TelegramUI/Components/LensTransition/Sources/LensTransitionContainer.swift new file mode 100644 index 0000000000..d60f1c4b79 --- /dev/null +++ b/submodules/TelegramUI/Components/LensTransition/Sources/LensTransitionContainer.swift @@ -0,0 +1,206 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +@inline(__always) +private func getMethod(object: NSObject, selector: String) -> T? { + guard let method = object.method(for: NSSelectorFromString(selector)) else { + return nil + } + return unsafeBitCast(method, to: T.self) +} + +private var cachedClasses: [String: NSObject] = [:] +private func getAndCacheClass(name: String) -> NSObject? { + if let value = cachedClasses[name] { + return value + } else { + if let value = NSClassFromString(name as String) as AnyObject as? NSObject { + cachedClasses[name] = value + return value + } else { + return nil + } + } +} + +private var cachedAllocMethods: [String: (@convention(c) (AnyObject, Selector) -> NSObject?, Selector)] = [:] +private func invokeAllocMethod(className: String) -> NSObject? { + guard let classObject = getAndCacheClass(name: className) else { + return nil + } + if let cachedMethod = cachedAllocMethods[className] { + return cachedMethod.0(classObject, cachedMethod.1) + } else { + let method: (@convention(c) (AnyObject, Selector) -> NSObject?)? = getMethod(object: classObject, selector: "alloc") + if let method { + let selector = NSSelectorFromString("alloc") + cachedAllocMethods[className] = (method, selector) + return method(classObject, selector) + } else { + return nil + } + } +} + +private var cachedInitMethods: [String: (@convention(c) (AnyObject, Selector) -> NSObject?, Selector)] = [:] +private func invokeInitMethod(className: String, object: NSObject) -> NSObject? { + if let cachedInitMethod = cachedInitMethods[className] { + return cachedInitMethod.0(object, cachedInitMethod.1) + } else { + let method: (@convention(c) (AnyObject, Selector) -> NSObject?)? = getMethod(object: object, selector: "init") + if let method { + let selector = NSSelectorFromString("init") + cachedInitMethods[className] = (method, selector) + return method(object, selector) + } else { + return nil + } + } +} + +private func createObject(className: String) -> NSObject? { + if let object = invokeAllocMethod(className: className) { + return invokeInitMethod(className: className, object: object) + } else { + return nil + } +} + +private func setFilterName(object: NSObject, name: String) { + object.perform(NSSelectorFromString("setName:"), with: name) +} + +private final class EmptyLayerDelegate: NSObject, CALayerDelegate { + func action(for layer: CALayer, forKey event: String) -> CAAction? { + return nullAction + } +} + +public final class LensTransitionContainer: UIView { + public enum State { + case animatedOut + case animatedIn + } + + private let emptyLayerDelegate = EmptyLayerDelegate() + + private let sdfElementLayer: CALayer? + private let sdfLayer: CALayer? + private let displacementEffect: NSObject? + + private(set) var state: State = .animatedOut + + override public init(frame: CGRect) { + self.sdfElementLayer = createObject(className: ("CAS" as NSString).appending("DFElementLayer") as String) as? CALayer + self.sdfLayer = createObject(className: ("CAS" as NSString).appending("DFLayer")) as? CALayer + self.displacementEffect = createObject(className: ("CAS" as NSString).appending("DFGlassDisplacementEffect")) + + super.init(frame: frame) + + let curvature: CGFloat = 1.0 + + if let displacementEffect = self.displacementEffect { + displacementEffect.setValue(curvature, forKey: "curvature") + displacementEffect.setValue(0.0 as NSNumber, forKey: "angle") + } + + if let sdfLayer = self.sdfLayer, let displacementEffect = self.displacementEffect { + sdfLayer.name = "sdfLayer" + sdfLayer.setValue(3.0, forKey: "scale") + sdfLayer.setValue(displacementEffect, forKey: "effect") + sdfLayer.delegate = self.emptyLayerDelegate + } + + if let sdfLayer = self.sdfLayer, let sdfElementLayer = self.sdfElementLayer { + sdfElementLayer.setValue(0.0 as NSNumber, forKey: "gradientOvalization") + sdfElementLayer.isOpaque = true + sdfElementLayer.allowsEdgeAntialiasing = true + let sdfLayerDelegate = unsafeBitCast(sdfLayer, to: CALayerDelegate.self) + sdfElementLayer.delegate = sdfLayerDelegate + sdfElementLayer.setValue(UIScreenScale, forKey: "scale") + sdfLayer.addSublayer(sdfElementLayer) + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setIsFilterActive(isFilterActive: Bool) { + if isFilterActive { + if self.layer.filters == nil { + if let sdfLayer = self.sdfLayer { + self.layer.insertSublayer(sdfLayer, at: 0) + } + if let displacementFilter = CALayer.displacementMap(), let blurFilter = CALayer.blur() { + setFilterName(object: blurFilter, name: "gaussianBlur") + blurFilter.setValue(true, forKey: "inputNormalizeEdgesTransparent") + + setFilterName(object: displacementFilter, name: "displacementMap") + displacementFilter.setValue("sdfLayer", forKey: "inputSourceSublayerName") + + self.layer.filters = [ + blurFilter, + displacementFilter + ] + } + } + } else if self.layer.filters != nil { + self.layer.filters = nil + if let sdfLayer = self.sdfLayer { + sdfLayer.removeFromSuperlayer() + } + } + } + + public func update(size: CGSize, cornerRadius: CGFloat, state: State, transition: ComponentTransition) { + let minHeight: CGFloat = 60.0 + let fullHeight: CGFloat = 30.0 + let fullAmount: CGFloat = -40.0 + let fullBlur: CGFloat = 2.0 + + let previousState = self.state + self.state = state + if let sdfLayer = self.sdfLayer, let sdfElementLayer = self.sdfElementLayer, sdfLayer.bounds.size != size || previousState != state { + let previousSize = sdfLayer.bounds.size + + self.setIsFilterActive(isFilterActive: true) + + transition.setFrame(layer: sdfLayer, frame: CGRect(origin: CGPoint(), size: size), completion: { [weak self] _ in + guard let self else { + return + } + self.setIsFilterActive(isFilterActive: false) + }) + transition.setCornerRadius(layer: sdfLayer, cornerRadius: cornerRadius) + transition.setFrame(layer: sdfElementLayer, frame: CGRect(origin: CGPoint(), size: size)) + + transition.setCornerRadius(layer: sdfElementLayer, cornerRadius: cornerRadius) + transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius) + + let height: CGFloat = state == .animatedIn ? minHeight : fullHeight + let amount: CGFloat = state == .animatedIn ? 0.0 : fullAmount + let blur: CGFloat = state == .animatedIn ? 0.0 : fullBlur + self.layer.setValue(height as NSNumber, forKeyPath: "sublayers.sdfLayer.effect.height") + self.layer.setValue(amount as NSNumber, forKeyPath: "filters.displacementMap.inputAmount") + self.layer.setValue(blur as NSNumber, forKeyPath: "filters.gaussianBlur.inputRadius") + + if !transition.animation.isImmediate && previousSize != .zero { + if previousState != state { + let previousHeight: CGFloat = previousState == .animatedIn ? minHeight : fullHeight + let previousAmount: CGFloat = previousState == .animatedIn ? 0.0 : fullAmount + let previousBlur: CGFloat = previousState == .animatedIn ? 0.0 : fullBlur + + let glassTransition: ComponentTransition = transition + + glassTransition.animateScalarFloat(layer: self.layer, keyPath: "sublayers.sdfLayer.effect.height", from: previousHeight, to: height, delay: 0.0) + glassTransition.animateScalarFloat(layer: self.layer, keyPath: "filters.displacementMap.inputAmount", from: previousAmount, to: amount, delay: 0.0) + + transition.animateScalarFloat(layer: self.layer, keyPath: "filters.gaussianBlur.inputRadius", from: previousBlur, to: blur) + } + } + } + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index fdb533285d..597f2f5350 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -6974,6 +6974,20 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.controllerNode.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition) } + override public func tabBarItemHasDoubleTapAction() -> Bool { + return true + } + + override public func tabBarItemPerformDoubleTapAction() { + guard let (maybePrimary, other) = self.accountsAndPeersValue, let _ = maybePrimary else { + return + } + for account in other { + self.controllerNode.switchToAccount(id: account.0.account.id) + break + } + } + override public func tabBarItemContextAction(sourceView: ContextExtractedContentContainingView, gesture: ContextGesture) { guard let (maybePrimary, other) = self.accountsAndPeersValue, let primary = maybePrimary else { return diff --git a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift index 2e62055074..cfce67e3be 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift +++ b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift @@ -250,15 +250,17 @@ public final class TabBarComponent: Component { public final class Item: Equatable { public let item: UITabBarItem public let action: (Bool) -> Void + public let doubleTapAction: (() -> Void)? public let contextAction: ((ContextGesture, ContextExtractedContentContainingView) -> Void)? fileprivate var id: AnyHashable { return AnyHashable(ObjectIdentifier(self.item)) } - public init(item: UITabBarItem, action: @escaping (Bool) -> Void, contextAction: ((ContextGesture, ContextExtractedContentContainingView) -> Void)?) { + public init(item: UITabBarItem, action: @escaping (Bool) -> Void, doubleTapAction: (() -> Void)?, contextAction: ((ContextGesture, ContextExtractedContentContainingView) -> Void)?) { self.item = item self.action = action + self.doubleTapAction = doubleTapAction self.contextAction = contextAction } @@ -269,6 +271,9 @@ public final class TabBarComponent: Component { if lhs.item !== rhs.item { return false } + if (lhs.doubleTapAction == nil) != (rhs.doubleTapAction == nil) { + return false + } if (lhs.contextAction == nil) != (rhs.contextAction == nil) { return false } @@ -340,7 +345,7 @@ public final class TabBarComponent: Component { return true } - public final class View: UIView, UITabBarDelegate, UIGestureRecognizerDelegate { + public final class View: UIView, UIGestureRecognizerDelegate { private let backgroundContainer: GlassBackgroundContainerView private let liquidLensView: LiquidLensView private let contextGestureContainerView: ContextControllerSourceView @@ -359,6 +364,7 @@ public final class TabBarComponent: Component { private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemWidth: CGFloat, itemId: AnyHashable)? private var overrideSelectedItemId: AnyHashable? + private var pendingDoubleTapItem: (id: AnyHashable, previouslySelectedId: AnyHashable?, timer: Foundation.Timer)? public var currentSearchNode: ASDisplayNode? { return self.searchView?.searchBarNode @@ -450,15 +456,8 @@ public final class TabBarComponent: Component { fatalError("init(coder:) has not been implemented") } - public func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { - guard let component = self.component else { - return - } - if let index = tabBar.items?.firstIndex(where: { $0 === item }) { - if index < component.items.count { - component.items[index].action(false) - } - } + deinit { + self.pendingDoubleTapItem?.timer.invalidate() } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { @@ -473,6 +472,11 @@ public final class TabBarComponent: Component { case .began: if let search = component.search, search.isActive { } else if let itemId = self.item(at: recognizer.location(in: self)), let itemView = self.itemViews[itemId]?.view { + if let pendingDoubleTapItemValue = self.pendingDoubleTapItem, pendingDoubleTapItemValue.id != itemId { + self.pendingDoubleTapItem = nil + pendingDoubleTapItemValue.timer.invalidate() + } + let startX = itemView.frame.minX - 4.0 self.selectionGestureState = (startX, startX, itemView.bounds.width, itemId) self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) @@ -496,8 +500,43 @@ public final class TabBarComponent: Component { guard let item = component.items.first(where: { $0.id == selectionGestureState.itemId }) else { return } - self.overrideSelectedItemId = selectionGestureState.itemId - item.action(false) + + var handledDoubleTap = false + if let pendingDoubleTapItemValue = self.pendingDoubleTapItem { + self.pendingDoubleTapItem = nil + pendingDoubleTapItemValue.timer.invalidate() + + if pendingDoubleTapItemValue.id == selectionGestureState.itemId { + handledDoubleTap = true + item.doubleTapAction?() + } + } + + if !handledDoubleTap { + if item.doubleTapAction != nil { + let timer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.18, repeats: false, block: { [weak self] timer in + guard let self else { + return + } + if let pendingDoubleTapItemValue = self.pendingDoubleTapItem, pendingDoubleTapItemValue.timer === timer { + self.pendingDoubleTapItem = nil + + self.overrideSelectedItemId = pendingDoubleTapItemValue.id + if let item = component.items.first(where: { $0.id == pendingDoubleTapItemValue.id }) { + item.action(false) + } + + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } + }) + self.overrideSelectedItemId = selectionGestureState.itemId + item.action(false) + self.pendingDoubleTapItem = (selectionGestureState.itemId, self.overrideSelectedItemId, timer) + } else { + self.overrideSelectedItemId = selectionGestureState.itemId + item.action(false) + } + } } self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) } diff --git a/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift b/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift index 3a9e2c9ec5..4e92cd3b51 100644 --- a/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift +++ b/submodules/TelegramUI/Components/VideoPlaybackControlsComponent/Sources/VideoPlaybackControlsComponent.swift @@ -134,6 +134,16 @@ public final class VideoPlaybackControlsComponent: Component { } } + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let result = super.hitTest(point, with: event) else { + return nil + } + if result === self { + return nil + } + return result + } + func update(component: VideoPlaybackControlsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let isVisibleChanged = self.component?.isVisible != component.isVisible