mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-05-21 18:20:41 +00:00
Glass
This commit is contained in:
@@ -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",
|
||||
|
||||
+8
-4
@@ -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)
|
||||
|
||||
|
||||
+1
-1
@@ -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",
|
||||
],
|
||||
)
|
||||
+206
@@ -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)
|
||||
}
|
||||
|
||||
+10
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user