This commit is contained in:
Isaac
2026-02-13 17:08:48 +04:00
parent 2a317d87d2
commit 157a7a978d
20 changed files with 419 additions and 33 deletions
@@ -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(
@@ -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() {
}
@@ -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
@@ -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)
@@ -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)
@@ -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)
}
@@ -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() {
@@ -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)
@@ -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)
}
}
@@ -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)
@@ -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<Empty>()
@@ -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
@@ -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
@@ -34,6 +34,7 @@ swift_library(
"//submodules/TextSelectionNode",
"//submodules/UIKitRuntimeUtils",
"//submodules/UndoUI",
"//submodules/TelegramUI/Components/LensTransition",
],
visibility = [
"//visibility:public",
@@ -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)
@@ -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,
@@ -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",
],
)
@@ -0,0 +1,206 @@
import Foundation
import UIKit
import Display
import ComponentFlow
@inline(__always)
private func getMethod<T>(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)
}
}
}
}
}
@@ -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
@@ -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)
}
@@ -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<Empty>, transition: ComponentTransition) -> CGSize {
let isVisibleChanged = self.component?.isVisible != component.isVisible