mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Various improvements
This commit is contained in:
parent
3ca012d610
commit
bb23c6f653
18 changed files with 839 additions and 165 deletions
|
|
@ -1813,7 +1813,7 @@ swift_library(
|
|||
ios_ui_test_suite(
|
||||
name = "iOSAppUITestSuite",
|
||||
bundle_id = "org.telegram.Telegram-iOS-uitests",
|
||||
minimum_os_version = "13.0",
|
||||
minimum_os_version = minimum_os_version,
|
||||
runners = [
|
||||
":iPhone-17__26.2",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -438,11 +438,31 @@ public final class ChatPresentationInterfaceState: Equatable {
|
|||
}
|
||||
|
||||
public struct PersistentPeerData: Codable, Equatable {
|
||||
public var topicListPanelLocation: Bool
|
||||
public enum TopicListPanelLocation: Int32 {
|
||||
case top = 0
|
||||
case side = 1
|
||||
case bottom = 2
|
||||
}
|
||||
|
||||
public init(topicListPanelLocation: Bool) {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case topicListPanelLocation
|
||||
}
|
||||
|
||||
public var topicListPanelLocation: TopicListPanelLocation
|
||||
|
||||
public init(topicListPanelLocation: TopicListPanelLocation) {
|
||||
self.topicListPanelLocation = topicListPanelLocation
|
||||
}
|
||||
|
||||
public init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.topicListPanelLocation = TopicListPanelLocation(rawValue: try container.decode(Int32.self, forKey: .topicListPanelLocation)) ?? .top
|
||||
}
|
||||
|
||||
public func encode(to encoder: any Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(Int32(self.topicListPanelLocation.rawValue), forKey: .topicListPanelLocation)
|
||||
}
|
||||
}
|
||||
|
||||
public final class RemovePaidMessageFeeData: Equatable {
|
||||
|
|
@ -660,7 +680,7 @@ public final class ChatPresentationInterfaceState: Equatable {
|
|||
self.alwaysShowGiftButton = false
|
||||
self.disallowedGifts = nil
|
||||
self.persistentData = PersistentPeerData(
|
||||
topicListPanelLocation: false
|
||||
topicListPanelLocation: .top
|
||||
)
|
||||
self.removePaidMessageFeeData = nil
|
||||
self.viewForumAsMessages = false
|
||||
|
|
|
|||
|
|
@ -911,6 +911,10 @@ public extension CALayer {
|
|||
static func displacementMap() -> NSObject? {
|
||||
return makeDisplacementMapFilter()
|
||||
}
|
||||
|
||||
static func colorMatrix() -> NSObject? {
|
||||
return makeColorMatrixFilter()
|
||||
}
|
||||
}
|
||||
|
||||
public extension CALayer {
|
||||
|
|
|
|||
|
|
@ -362,8 +362,11 @@ static CGRect viewFrame(UIView *view)
|
|||
|
||||
_deleteButton = [[TGModernButton alloc] initWithFrame:CGRectMake(0.0f, (self.bounds.size.height - 45.0f) / 2.0f, 45.0f, 45.0f)];
|
||||
[_deleteButton setImage:deleteImage forState:UIControlStateNormal];
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
_deleteButton.adjustsImageWhenDisabled = false;
|
||||
_deleteButton.adjustsImageWhenHighlighted = false;
|
||||
#pragma clang diagnostic pop
|
||||
[_deleteButton addTarget:self action:@selector(deleteButtonPressed) forControlEvents:UIControlEventTouchUpInside];
|
||||
if (!_forStory) {
|
||||
[self addSubview:_deleteButton];
|
||||
|
|
@ -379,7 +382,10 @@ static CGRect viewFrame(UIView *view)
|
|||
_sendButton.alpha = 0.0f;
|
||||
_sendButton.exclusiveTouch = true;
|
||||
[_sendButton setImage:_assets.sendImage forState:UIControlStateNormal];
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
_sendButton.adjustsImageWhenHighlighted = false;
|
||||
#pragma clang diagnostic pop
|
||||
[_sendButton addTarget:self action:@selector(sendButtonPressed) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
_longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(doneButtonLongPressed:)];
|
||||
|
|
|
|||
|
|
@ -34,14 +34,20 @@
|
|||
|
||||
_leftSegmentView = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 16, height)];
|
||||
_leftSegmentView.exclusiveTouch = true;
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
_leftSegmentView.adjustsImageWhenHighlighted = false;
|
||||
#pragma clang diagnostic pop
|
||||
[_leftSegmentView setBackgroundImage:TGComponentsImageNamed(@"VideoMessageLeftHandle") forState:UIControlStateNormal];
|
||||
_leftSegmentView.hitTestEdgeInsets = UIEdgeInsetsMake(-5, -25, -5, -10);
|
||||
[self addSubview:_leftSegmentView];
|
||||
|
||||
_rightSegmentView = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 16, height)];
|
||||
_rightSegmentView.exclusiveTouch = true;
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
_rightSegmentView.adjustsImageWhenHighlighted = false;
|
||||
#pragma clang diagnostic pop
|
||||
[_rightSegmentView setBackgroundImage:TGComponentsImageNamed(@"VideoMessageRightHandle") forState:UIControlStateNormal];
|
||||
_rightSegmentView.hitTestEdgeInsets = UIEdgeInsetsMake(-5, -10, -5, -25);
|
||||
[self addSubview:_rightSegmentView];
|
||||
|
|
|
|||
|
|
@ -287,6 +287,8 @@ static NSData *base64_decode(NSString *str) {
|
|||
|
||||
size_t outlen = block_size;
|
||||
OSStatus status = noErr;
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
status = SecKeyEncrypt(keyRef,
|
||||
kSecPaddingNone,
|
||||
srcbuf + idx,
|
||||
|
|
@ -294,6 +296,7 @@ static NSData *base64_decode(NSString *str) {
|
|||
outbuf,
|
||||
&outlen
|
||||
);
|
||||
#pragma clang diagnostic pop
|
||||
if (status != 0) {
|
||||
NSLog(@"SecKeyEncrypt fail. Error Code: %d", (int)status);
|
||||
ret = nil;
|
||||
|
|
@ -343,6 +346,8 @@ static NSData *base64_decode(NSString *str) {
|
|||
|
||||
size_t outlen = block_size;
|
||||
OSStatus status = noErr;
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
status = SecKeyDecrypt(keyRef,
|
||||
kSecPaddingNone,
|
||||
srcbuf + idx,
|
||||
|
|
@ -350,6 +355,7 @@ static NSData *base64_decode(NSString *str) {
|
|||
outbuf,
|
||||
&outlen
|
||||
);
|
||||
#pragma clang diagnostic pop
|
||||
if (status != 0) {
|
||||
NSLog(@"SecKeyEncrypt fail. Error Code: %d", (int)status);
|
||||
ret = nil;
|
||||
|
|
|
|||
|
|
@ -1722,7 +1722,7 @@ public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, Context
|
|||
}
|
||||
transition.setPosition(view: self.contentContainer, position: CGRect(origin: CGPoint(), size: size).center)
|
||||
transition.setBounds(view: self.contentContainer, bounds: CGRect(origin: CGPoint(), size: size))
|
||||
self.contentContainer.update(size: size, cornerRadius: min(30.0, size.height * 0.5), transition: transition)
|
||||
self.contentContainer.update(size: size, cornerRadius: min(30.0, size.height * 0.5), isDark: presentationData.theme.overallDarkAppearance, transition: transition)
|
||||
|
||||
//let backgroundContainerFrame = CGRect(origin: CGPoint(), size: size).insetBy(dx: -self.backgroundContainerInset, dy: -self.backgroundContainerInset)
|
||||
|
||||
|
|
|
|||
|
|
@ -818,7 +818,9 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
|
|||
if let transitionInfo = reference.transitionInfo() {
|
||||
if let referenceView = transitionInfo.referenceView as? ContextExtractableContainer {
|
||||
if #available(iOS 26.0, *) {
|
||||
contextExtractableContainer = (referenceView, convertFrame(transitionInfo.referenceView.bounds.inset(by: transitionInfo.insets), from: transitionInfo.referenceView, to: self.view))
|
||||
if transitionInfo.referenceView.bounds.width == transitionInfo.referenceView.bounds.height {
|
||||
contextExtractableContainer = (referenceView, convertFrame(transitionInfo.referenceView.bounds.inset(by: transitionInfo.insets), from: transitionInfo.referenceView, to: self.view))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -310,6 +310,8 @@ public class GlassBackgroundView: UIView {
|
|||
}
|
||||
|
||||
private let legacyView: LegacyGlassView?
|
||||
private let legacyHighlightContainerView: UIView?
|
||||
private var glassHighlightRecognizer: GlassHighlightGestureRecognizer?
|
||||
|
||||
private let nativeView: UIVisualEffectView?
|
||||
private let nativeViewClippingContext: ClippingShapeContext?
|
||||
|
|
@ -339,6 +341,7 @@ public class GlassBackgroundView: UIView {
|
|||
public override init(frame: CGRect) {
|
||||
if #available(iOS 26.0, *), !GlassBackgroundView.useCustomGlassImpl {
|
||||
self.legacyView = nil
|
||||
self.legacyHighlightContainerView = nil
|
||||
|
||||
let glassEffect = UIGlassEffect(style: .regular)
|
||||
glassEffect.isInteractive = false
|
||||
|
|
@ -355,6 +358,10 @@ public class GlassBackgroundView: UIView {
|
|||
self.shadowView = nil
|
||||
} else {
|
||||
self.legacyView = LegacyGlassView(frame: CGRect())
|
||||
let legacyHighlightContainerView = UIView()
|
||||
legacyHighlightContainerView.isUserInteractionEnabled = false
|
||||
legacyHighlightContainerView.clipsToBounds = true
|
||||
self.legacyHighlightContainerView = legacyHighlightContainerView
|
||||
self.nativeView = nil
|
||||
self.nativeViewClippingContext = nil
|
||||
self.nativeParamsView = nil
|
||||
|
|
@ -384,18 +391,29 @@ public class GlassBackgroundView: UIView {
|
|||
}
|
||||
if let legacyView = self.legacyView {
|
||||
self.addSubview(legacyView)
|
||||
let glassHighlightRecognizer = GlassHighlightGestureRecognizer(target: self, action: #selector(self.onHighlightGesture(_:)))
|
||||
glassHighlightRecognizer.highlightContainerView = self.legacyHighlightContainerView
|
||||
self.glassHighlightRecognizer = glassHighlightRecognizer
|
||||
self.addGestureRecognizer(glassHighlightRecognizer)
|
||||
glassHighlightRecognizer.isEnabled = false
|
||||
}
|
||||
if let foregroundView = self.foregroundView {
|
||||
self.addSubview(foregroundView)
|
||||
foregroundView.mask = self.maskContainerView
|
||||
}
|
||||
self.addSubview(self.contentContainer)
|
||||
if let legacyHighlightContainerView = self.legacyHighlightContainerView {
|
||||
self.addSubview(legacyHighlightContainerView)
|
||||
}
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func onHighlightGesture(_ recognizer: GlassHighlightGestureRecognizer) {
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if !self.isUserInteractionEnabled {
|
||||
return nil
|
||||
|
|
@ -421,6 +439,10 @@ public class GlassBackgroundView: UIView {
|
|||
public func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, tintColor: TintColor, isInteractive: Bool = false, isVisible: Bool = true, transition: ComponentTransition) {
|
||||
let shape: Shape = .roundedRect(cornerRadius: cornerRadius)
|
||||
|
||||
if let glassHighlightRecognizer = self.glassHighlightRecognizer {
|
||||
glassHighlightRecognizer.isEnabled = isInteractive
|
||||
}
|
||||
|
||||
if let nativeView = self.nativeView, let nativeViewClippingContext = self.nativeViewClippingContext, (nativeView.bounds.size != size || nativeViewClippingContext.shape != shape) {
|
||||
|
||||
nativeViewClippingContext.update(shape: shape, size: size, transition: transition)
|
||||
|
|
@ -455,6 +477,16 @@ public class GlassBackgroundView: UIView {
|
|||
}
|
||||
transition.setFrame(view: legacyView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.setAlpha(view: legacyView, alpha: isVisible ? 1.0 : 0.0)
|
||||
|
||||
transition.setPosition(view: self.contentView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
|
||||
transition.setBounds(view: self.contentView, bounds: CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
if let legacyHighlightContainerView = self.legacyHighlightContainerView {
|
||||
transition.setFrame(view: legacyHighlightContainerView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
switch shape {
|
||||
case let .roundedRect(cornerRadius):
|
||||
transition.setCornerRadius(layer: legacyHighlightContainerView.layer, cornerRadius: cornerRadius)
|
||||
}
|
||||
}
|
||||
|
||||
let shadowInset: CGFloat = 32.0
|
||||
|
|
@ -548,6 +580,9 @@ public class GlassBackgroundView: UIView {
|
|||
}
|
||||
}
|
||||
foregroundView.image = GlassBackgroundView.generateLegacyGlassImage(size: CGSize(width: outerCornerRadius * 2.0, height: outerCornerRadius * 2.0), inset: shadowInset, borderWidthFactor: borderWidthFactor, isDark: isDark, fillColor: fillColor)
|
||||
#if DEBUG
|
||||
//foregroundView.image = nil
|
||||
#endif
|
||||
transition.setAlpha(view: foregroundView, alpha: isVisible ? 1.0 : 0.0)
|
||||
} else {
|
||||
if let nativeParamsView = self.nativeParamsView, let nativeView = self.nativeView {
|
||||
|
|
@ -717,6 +752,31 @@ public final class GlassBackgroundContainerView: UIView {
|
|||
}
|
||||
for view in self.contentView.subviews.reversed() {
|
||||
if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled {
|
||||
|
||||
#if DEBUG
|
||||
func findMatrix(layer: CALayer) -> AnyObject? {
|
||||
for filter in layer.filters ?? [] {
|
||||
if "\(filter)".contains("vibrantColorMatrix") {
|
||||
return filter as AnyObject
|
||||
}
|
||||
}
|
||||
|
||||
for sublayer in layer.sublayers ?? [] {
|
||||
if let result = findMatrix(layer: sublayer) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*if let filter = findMatrix(layer: self.layer) as? NSObject {
|
||||
var matrix: [Float32] = .init(repeating: 0, count: 20)
|
||||
let matrixValues = filter.value(forKey: "inputColorMatrix") as! NSValue
|
||||
matrixValues.getValue(&matrix, size: 4 * 20)
|
||||
assert(true)
|
||||
}*/
|
||||
#endif
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,21 +138,40 @@ final class LegacyGlassView: UIView {
|
|||
}
|
||||
|
||||
if previousParams?.style != style {
|
||||
if let blurFilter = CALayer.blur() {
|
||||
if let blurFilter = CALayer.blur(), let colorMatrixFilter = CALayer.colorMatrix() {
|
||||
switch style {
|
||||
case .clear:
|
||||
blurFilter.setValue(6.0 as NSNumber, forKey: "inputRadius")
|
||||
if DeviceMetrics.performance.isGraphicallyCapable {
|
||||
blurFilter.setValue(2.0 as NSNumber, forKey: "inputRadius")
|
||||
} else {
|
||||
blurFilter.setValue(6.0 as NSNumber, forKey: "inputRadius")
|
||||
}
|
||||
case .normal:
|
||||
blurFilter.setValue(2.0 as NSNumber, forKey: "inputRadius")
|
||||
}
|
||||
backdropLayer.filters = [blurFilter]
|
||||
|
||||
var matrix: [Float32] = [
|
||||
2.6705, -1.1087999, -0.1117, 0.0, 0.049999997,
|
||||
-0.3295, 1.8914, -0.111899994, 0.0, 0.049999997,
|
||||
-0.3297, -1.1084, 2.8881, 0.0, 0.049999997,
|
||||
0.0, 0.0, 0.0, 1.0, 0.0
|
||||
]
|
||||
colorMatrixFilter.setValue(NSValue(bytes: &matrix, objCType: "{CAColorMatrix=ffffffffffffffffffff}"), forKey: "inputColorMatrix")
|
||||
colorMatrixFilter.setValue(true as NSNumber, forKey: "inputBackdropAware")
|
||||
|
||||
switch style {
|
||||
case .clear:
|
||||
backdropLayer.filters = [blurFilter]
|
||||
case .normal:
|
||||
backdropLayer.filters = [colorMatrixFilter, blurFilter]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius)
|
||||
transition.setFrame(layer: backdropLayer, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
if !"".isEmpty {
|
||||
if DeviceMetrics.performance.isGraphicallyCapable {
|
||||
let size = CGSize(width: max(1.0, size.width), height: max(1.0, size.height))
|
||||
let cornerRadius = min(min(size.width, size.height) * 0.5, cornerRadius)
|
||||
let displacementMagnitudePoints: CGFloat = 20.0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,352 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
|
||||
final class GlassHighlightGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate {
|
||||
var highlightContainerView: UIView?
|
||||
|
||||
private var touchEffect: TouchEffect?
|
||||
private var initialTouchLocation: CGPoint?
|
||||
weak var touchEffectView: UIView?
|
||||
var parameters = TouchEffect.Parameters() {
|
||||
didSet {
|
||||
self.touchEffect?.setParameters(self.parameters, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
override init(target: Any?, action: Selector?) {
|
||||
super.init(target: target, action: action)
|
||||
|
||||
self.delegate = self
|
||||
self.cancelsTouchesInView = false
|
||||
self.delaysTouchesBegan = false
|
||||
self.delaysTouchesEnded = false
|
||||
self.requiresExclusiveTouchType = false
|
||||
}
|
||||
|
||||
override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
if let touchEffect = self.touchEffect {
|
||||
touchEffect.setIsTracking(false)
|
||||
}
|
||||
|
||||
self.touchEffect = nil
|
||||
self.initialTouchLocation = nil
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
if let view = self.touchEffectView ?? self.view, let touch = touches.first {
|
||||
let touchLocation = touch.location(in: view)
|
||||
let touchEffect = TouchEffect(view: view, highlightContainerView: self.highlightContainerView)
|
||||
touchEffect.setParameters(self.parameters, animated: false)
|
||||
if let highlightContainerView = self.highlightContainerView {
|
||||
touchEffect.setTouchLocation(touch.location(in: highlightContainerView), animated: false)
|
||||
}
|
||||
touchEffect.setStretchVector(.zero, animated: false)
|
||||
self.touchEffect = touchEffect
|
||||
self.initialTouchLocation = touchLocation
|
||||
touchEffect.setIsTracking(true)
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
if let touchEffect = self.touchEffect {
|
||||
touchEffect.setIsTracking(false)
|
||||
}
|
||||
self.touchEffect = nil
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
if let touchEffect = self.touchEffect {
|
||||
touchEffect.setIsTracking(false)
|
||||
}
|
||||
self.touchEffect = nil
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
guard let touchEffect = self.touchEffect,
|
||||
let view = self.touchEffectView ?? self.view,
|
||||
let touch = touches.first,
|
||||
let initialTouchLocation = self.initialTouchLocation else {
|
||||
return
|
||||
}
|
||||
let touchLocation = touch.location(in: view)
|
||||
if let highlightContainerView = self.highlightContainerView {
|
||||
touchEffect.setTouchLocation(touch.location(in: highlightContainerView), animated: false)
|
||||
}
|
||||
touchEffect.setStretchVector(
|
||||
CGPoint(
|
||||
x: touchLocation.x - initialTouchLocation.x,
|
||||
y: touchLocation.y - initialTouchLocation.y
|
||||
),
|
||||
animated: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class TouchEffect {
|
||||
private struct State: Equatable {
|
||||
var isTracking: Bool
|
||||
var stretchVector: CGPoint
|
||||
var touchLocation: CGPoint?
|
||||
}
|
||||
|
||||
struct SpringParameters {
|
||||
var mass: CGFloat
|
||||
var stiffness: CGFloat
|
||||
var damping: CGFloat
|
||||
var initialVelocity: CGFloat
|
||||
}
|
||||
|
||||
struct Parameters {
|
||||
var liftOn = SpringParameters(
|
||||
mass: 1.36,
|
||||
stiffness: 568.0,
|
||||
damping: 39.7,
|
||||
initialVelocity: 0.0
|
||||
)
|
||||
var liftOff = SpringParameters(
|
||||
mass: 2.0,
|
||||
stiffness: 460.0,
|
||||
damping: 21.8,
|
||||
initialVelocity: 0.0
|
||||
)
|
||||
var pressedSizeIncrease: CGFloat = 20.0
|
||||
}
|
||||
|
||||
private weak var view: UIView?
|
||||
private weak var highlightContainerView: UIView?
|
||||
private let radialHighlightLayer: CAGradientLayer = {
|
||||
let layer = CAGradientLayer()
|
||||
layer.type = .radial
|
||||
|
||||
let baseGradientAlpha: CGFloat = 0.5
|
||||
let numSteps = 8
|
||||
let firstStep = 1
|
||||
let firstLocation = 0.5
|
||||
let colors = (0 ..< numSteps).map { i -> UIColor in
|
||||
if i < firstStep {
|
||||
return UIColor(white: 1.0, alpha: 1.0)
|
||||
} else {
|
||||
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
|
||||
let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step)
|
||||
return UIColor(white: 1.0, alpha: baseGradientAlpha * value)
|
||||
}
|
||||
}
|
||||
let locations = (0 ..< numSteps).map { i -> CGFloat in
|
||||
if i < firstStep {
|
||||
return 0.0
|
||||
} else {
|
||||
let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1)
|
||||
return (firstLocation + (1.0 - firstLocation) * step)
|
||||
}
|
||||
}
|
||||
|
||||
layer.colors = colors.map(\.cgColor)
|
||||
layer.locations = locations.map { $0 as NSNumber }
|
||||
layer.startPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
layer.endPoint = CGPoint(x: 1.0, y: 1.0)
|
||||
layer.opacity = 0.0
|
||||
layer.actions = [
|
||||
"position": NSNull(),
|
||||
"bounds": NSNull(),
|
||||
"opacity": NSNull()
|
||||
]
|
||||
return layer
|
||||
}()
|
||||
private var state = State(isTracking: false, stretchVector: .zero, touchLocation: nil)
|
||||
private var appliedState: State?
|
||||
|
||||
var parameters = Parameters()
|
||||
|
||||
init(view: UIView, highlightContainerView: UIView?) {
|
||||
self.view = view
|
||||
self.highlightContainerView = highlightContainerView
|
||||
|
||||
if let highlightContainerView {
|
||||
highlightContainerView.layer.addSublayer(self.radialHighlightLayer)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.radialHighlightLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
private func currentTransform(for state: State, view: UIView) -> CATransform3D {
|
||||
let referenceView = self.highlightContainerView ?? view
|
||||
let viewWidth = max(1.0, referenceView.bounds.width)
|
||||
let viewHeight = max(1.0, referenceView.bounds.height)
|
||||
let aspectRatio = viewWidth / viewHeight
|
||||
|
||||
let baseScaleX: CGFloat
|
||||
let baseScaleY: CGFloat
|
||||
if state.isTracking {
|
||||
if viewWidth < viewHeight {
|
||||
baseScaleY = 1.0 + self.parameters.pressedSizeIncrease / viewHeight
|
||||
baseScaleX = baseScaleY
|
||||
} else {
|
||||
baseScaleX = 1.0 + self.parameters.pressedSizeIncrease / viewWidth
|
||||
baseScaleY = baseScaleX
|
||||
}
|
||||
} else {
|
||||
baseScaleX = 1.0
|
||||
baseScaleY = 1.0
|
||||
}
|
||||
|
||||
guard state.isTracking else {
|
||||
return CATransform3DScale(CATransform3DIdentity, baseScaleX, baseScaleY, 1.0)
|
||||
}
|
||||
|
||||
let stretchVector = state.stretchVector
|
||||
let adjustedX = stretchVector.x / aspectRatio
|
||||
let length = sqrt(pow(adjustedX, 2) + pow(stretchVector.y, 2))
|
||||
|
||||
guard length != 0.0 else {
|
||||
return CATransform3DScale(CATransform3DIdentity, baseScaleX, baseScaleY, 1.0)
|
||||
}
|
||||
|
||||
let normal = CGPoint(
|
||||
x: adjustedX / length,
|
||||
y: stretchVector.y / length
|
||||
)
|
||||
let k: CGFloat = -1.0 / ((length / viewHeight) / (5.0 * aspectRatio) + 1.0) + 1.0
|
||||
let additionalMaxScale = (viewHeight + 16.0 / aspectRatio) / viewHeight - 1.0
|
||||
let t = additionalMaxScale * k * aspectRatio
|
||||
let maxOffset: CGFloat = 24.0
|
||||
|
||||
if abs(normal.x) > abs(normal.y) {
|
||||
let diff = abs(normal.x) - abs(normal.y)
|
||||
var transform = CATransform3DIdentity
|
||||
transform.m11 = baseScaleX * (1.0 + t * diff)
|
||||
transform.m22 = baseScaleY * (1.0 / (1.0 + t * diff))
|
||||
transform.m41 = normal.x * maxOffset * k
|
||||
transform.m42 = normal.y * maxOffset * k
|
||||
return transform
|
||||
} else {
|
||||
let diff = abs(normal.y) - abs(normal.x)
|
||||
var transform = CATransform3DIdentity
|
||||
transform.m11 = baseScaleX * (1.0 / (1.0 + t * diff))
|
||||
transform.m22 = baseScaleY * (1.0 + t * diff)
|
||||
transform.m41 = normal.x * maxOffset * k
|
||||
transform.m42 = normal.y * maxOffset * k
|
||||
return transform
|
||||
}
|
||||
}
|
||||
|
||||
private func currentSpringParameters(from previousState: State?, to state: State) -> SpringParameters {
|
||||
guard let previousState, previousState != state else {
|
||||
return state.isTracking ? self.parameters.liftOn : self.parameters.liftOff
|
||||
}
|
||||
if !previousState.isTracking && state.isTracking {
|
||||
return self.parameters.liftOn
|
||||
} else {
|
||||
return self.parameters.liftOff
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRadialHighlight(animated: Bool) {
|
||||
guard self.highlightContainerView != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
let baseAlpha: Float = 0.1
|
||||
let targetOpacity: Float = self.state.isTracking ? baseAlpha : 0.0
|
||||
let size = CGSize(width: 300.0, height: 300.0)
|
||||
if let touchLocation = self.state.touchLocation {
|
||||
self.radialHighlightLayer.bounds = CGRect(origin: CGPoint(), size: size)
|
||||
self.radialHighlightLayer.position = touchLocation
|
||||
}
|
||||
|
||||
if animated {
|
||||
let animation = CABasicAnimation(keyPath: "opacity")
|
||||
animation.fromValue = self.radialHighlightLayer.presentation()?.opacity ?? self.radialHighlightLayer.opacity
|
||||
self.radialHighlightLayer.opacity = targetOpacity
|
||||
animation.toValue = targetOpacity
|
||||
animation.duration = self.state.isTracking ? 0.12 : 0.22
|
||||
animation.timingFunction = CAMediaTimingFunction(name: self.state.isTracking ? .easeOut : .easeInEaseOut)
|
||||
self.radialHighlightLayer.add(animation, forKey: "opacity")
|
||||
} else {
|
||||
self.radialHighlightLayer.opacity = targetOpacity
|
||||
}
|
||||
}
|
||||
|
||||
func applyCurrentTransform(animated: Bool = true) {
|
||||
guard let view = self.view else {
|
||||
return
|
||||
}
|
||||
|
||||
let targetTransform = self.currentTransform(for: self.state, view: view)
|
||||
|
||||
if !animated {
|
||||
view.layer.removeAnimation(forKey: "sublayerTransform")
|
||||
view.layer.sublayerTransform = targetTransform
|
||||
self.updateRadialHighlight(animated: false)
|
||||
self.appliedState = self.state
|
||||
return
|
||||
}
|
||||
|
||||
let springParameters = self.currentSpringParameters(from: self.appliedState, to: self.state)
|
||||
let animation = CASpringAnimation(keyPath: "sublayerTransform")
|
||||
animation.fromValue = NSValue(caTransform3D: view.layer.presentation()?.sublayerTransform ?? view.layer.sublayerTransform)
|
||||
animation.toValue = NSValue(caTransform3D: targetTransform)
|
||||
animation.mass = springParameters.mass
|
||||
animation.stiffness = springParameters.stiffness
|
||||
animation.damping = springParameters.damping
|
||||
animation.initialVelocity = springParameters.initialVelocity
|
||||
animation.duration = animation.settlingDuration
|
||||
animation.fillMode = .both
|
||||
animation.isRemovedOnCompletion = false
|
||||
|
||||
view.layer.sublayerTransform = targetTransform
|
||||
view.layer.add(animation, forKey: "sublayerTransform")
|
||||
self.updateRadialHighlight(animated: true)
|
||||
self.appliedState = self.state
|
||||
}
|
||||
|
||||
func setParameters(_ parameters: Parameters, animated: Bool = false) {
|
||||
self.parameters = parameters
|
||||
self.applyCurrentTransform(animated: animated)
|
||||
}
|
||||
|
||||
func setIsTracking(_ value: Bool, animated: Bool = true) {
|
||||
let nextState = State(
|
||||
isTracking: value,
|
||||
stretchVector: value ? self.state.stretchVector : .zero,
|
||||
touchLocation: value ? self.state.touchLocation : self.state.touchLocation
|
||||
)
|
||||
guard self.state != nextState else {
|
||||
return
|
||||
}
|
||||
self.state = nextState
|
||||
self.applyCurrentTransform(animated: animated)
|
||||
}
|
||||
|
||||
func setTouchLocation(_ touchLocation: CGPoint, animated: Bool = false) {
|
||||
let nextState = State(isTracking: self.state.isTracking, stretchVector: self.state.stretchVector, touchLocation: touchLocation)
|
||||
guard self.state != nextState else {
|
||||
return
|
||||
}
|
||||
self.state = nextState
|
||||
self.applyCurrentTransform(animated: animated)
|
||||
}
|
||||
|
||||
func setStretchVector(_ stretchVector: CGPoint, animated: Bool = false) {
|
||||
let nextState = State(isTracking: self.state.isTracking, stretchVector: stretchVector, touchLocation: self.state.touchLocation)
|
||||
guard self.state != nextState else {
|
||||
return
|
||||
}
|
||||
self.state = nextState
|
||||
self.applyCurrentTransform(animated: animated)
|
||||
}
|
||||
}
|
||||
|
|
@ -251,13 +251,27 @@ public final class HeaderPanelContainerComponent: Component {
|
|||
self.backgroundContainer.update(size: CGSize(width: backgroundSize.width + 32.0 * 2.0, height: backgroundSize.height + 32.0 * 2.0), isDark: component.theme.overallDarkAppearance, transition: transition)
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: 32.0 + sideInset, y: 32.0), size: CGSize(width: size.width - sideInset * 2.0, height: backgroundSize.height))
|
||||
|
||||
var panelCount = 0
|
||||
if component.tabs != nil {
|
||||
panelCount += 1
|
||||
}
|
||||
panelCount += component.panels.count
|
||||
var cornerRadius: CGFloat = 0.0
|
||||
if panelCount == 1 {
|
||||
cornerRadius = backgroundFrame.height * 0.5
|
||||
} else {
|
||||
cornerRadius = 20.0
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 20.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: component.preferClearGlass ? .clear : .panel), isInteractive: true, transition: transition)
|
||||
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: cornerRadius, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: component.preferClearGlass ? .clear : .panel), isInteractive: true, transition: transition)
|
||||
|
||||
transition.setAlpha(view: self.backgroundContainer, alpha: (component.tabs != nil || !component.panels.isEmpty) ? 1.0 : 0.0)
|
||||
|
||||
transition.setFrame(view: self.contentContainer, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
self.contentContainer.layer.cornerRadius = 20.0
|
||||
|
||||
transition.setCornerRadius(layer: self.contentContainer.layer, cornerRadius: min(cornerRadius, backgroundFrame.height * 0.5))
|
||||
|
||||
return size
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import Display
|
|||
import ComponentFlow
|
||||
import Display
|
||||
import UIKitRuntimeUtils
|
||||
import GlassBackgroundComponent
|
||||
|
||||
@inline(__always)
|
||||
private func getMethod<T>(object: NSObject, selector: String) -> T? {
|
||||
|
|
@ -98,13 +99,11 @@ public protocol LensTransitionContainerEffectView: UIView {
|
|||
}
|
||||
|
||||
public protocol LensTransitionContainerProtocol: UIView {
|
||||
var effectView: LensTransitionContainerEffectView { get }
|
||||
var contentsEffectView: UIView { get }
|
||||
var contentsView: UIView { get }
|
||||
|
||||
func animateIn(fromRect: CGRect, toRect: CGRect, fromCornerRadius: CGFloat, toCornerRadius: CGFloat, isDark: Bool, sourceEffectView: LensTransitionContainerEffectView)
|
||||
func animateOut(fromRect: CGRect, toRect: CGRect, fromCornerRadius: CGFloat, toCornerRadius: CGFloat, isDark: Bool, sourceEffectView: LensTransitionContainerEffectView)
|
||||
func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition)
|
||||
func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, transition: ComponentTransition)
|
||||
}
|
||||
|
||||
@available(iOS 26.0, *)
|
||||
|
|
@ -228,7 +227,7 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
self.cancelAnimationsRecursively(layer: sdfElementLayer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private struct TransitionKeyframes {
|
||||
let bakedSizes: [CGSize]
|
||||
let bakedPositions: [CGPoint]
|
||||
|
|
@ -242,7 +241,7 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
private func makeForwardTransitionKeyframes(fromRect: CGRect, toRect: CGRect, toCornerRadius: CGFloat) -> TransitionKeyframes {
|
||||
let sourceMaxEdgeDistance: CGFloat = 20.0
|
||||
let sourceSuckDurationFraction: CGFloat = 0.9
|
||||
let sourceFinalInsideDistance: CGFloat = -8.0
|
||||
let sourceFinalFurthestInsideDistance: CGFloat = -8.0
|
||||
let sourceFinalInsideStartFraction: CGFloat = 0.65
|
||||
let sourceFullInsideInset: CGFloat = max(1.0, min(fromRect.width, fromRect.height) * 0.5)
|
||||
let sampleCount = 30
|
||||
|
|
@ -257,7 +256,6 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
let centerLineLength = hypot(centerLineDelta.x, centerLineDelta.y)
|
||||
let centerLineDirection: CGPoint = centerLineLength > 1e-6 ? CGPoint(x: centerLineDelta.x / centerLineLength, y: centerLineDelta.y / centerLineLength) : CGPoint(x: 1.0, y: 0.0)
|
||||
let centerLineMinDistance: CGFloat = -centerLineLength
|
||||
let centerLineMaxDistance: CGFloat = 0.0
|
||||
|
||||
func isPointInsideRoundedRect(point: CGPoint, rectCenter: CGPoint, rectSize: CGSize, cornerRadius: CGFloat) -> Bool {
|
||||
let halfWidth = rectSize.width * 0.5
|
||||
|
|
@ -386,6 +384,72 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
return CGPoint(x: rectCenter.x + nearestLocal.x, y: rectCenter.y + nearestLocal.y)
|
||||
}
|
||||
|
||||
func boundaryPointOnRoundedRectRay(rectCenter: CGPoint, rectSize: CGSize, cornerRadius: CGFloat, direction: CGPoint) -> CGPoint {
|
||||
let halfWidth = rectSize.width * 0.5
|
||||
let halfHeight = rectSize.height * 0.5
|
||||
if halfWidth <= 0.0 || halfHeight <= 0.0 {
|
||||
return rectCenter
|
||||
}
|
||||
|
||||
let radius = max(0.0, min(cornerRadius, min(halfWidth, halfHeight)))
|
||||
let innerX = max(0.0, halfWidth - radius)
|
||||
let innerY = max(0.0, halfHeight - radius)
|
||||
|
||||
let dirLength = hypot(direction.x, direction.y)
|
||||
let dir: CGPoint
|
||||
if dirLength > 1e-6 {
|
||||
dir = CGPoint(x: direction.x / dirLength, y: direction.y / dirLength)
|
||||
} else {
|
||||
dir = CGPoint(x: 1.0, y: 0.0)
|
||||
}
|
||||
let dx = abs(dir.x)
|
||||
let dy = abs(dir.y)
|
||||
|
||||
@inline(__always)
|
||||
func signedPoint(_ x: CGFloat, _ y: CGFloat, _ direction: CGPoint) -> CGPoint {
|
||||
let sx: CGFloat = direction.x >= 0.0 ? 1.0 : -1.0
|
||||
let sy: CGFloat = direction.y >= 0.0 ? 1.0 : -1.0
|
||||
return CGPoint(x: x * sx, y: y * sy)
|
||||
}
|
||||
|
||||
if dx > 1e-6 {
|
||||
let tVertical = halfWidth / dx
|
||||
let yAtVertical = tVertical * dy
|
||||
if yAtVertical <= innerY + 1e-6 {
|
||||
let local = signedPoint(halfWidth, yAtVertical, dir)
|
||||
return CGPoint(x: rectCenter.x + local.x, y: rectCenter.y + local.y)
|
||||
}
|
||||
}
|
||||
|
||||
if dy > 1e-6 {
|
||||
let tHorizontal = halfHeight / dy
|
||||
let xAtHorizontal = tHorizontal * dx
|
||||
if xAtHorizontal <= innerX + 1e-6 {
|
||||
let local = signedPoint(xAtHorizontal, halfHeight, dir)
|
||||
return CGPoint(x: rectCenter.x + local.x, y: rectCenter.y + local.y)
|
||||
}
|
||||
}
|
||||
|
||||
if radius <= 1e-6 {
|
||||
let tx = dx > 1e-6 ? (halfWidth / dx) : CGFloat.greatestFiniteMagnitude
|
||||
let ty = dy > 1e-6 ? (halfHeight / dy) : CGFloat.greatestFiniteMagnitude
|
||||
let t = min(tx, ty)
|
||||
let local = signedPoint(dx * t, dy * t, dir)
|
||||
return CGPoint(x: rectCenter.x + local.x, y: rectCenter.y + local.y)
|
||||
}
|
||||
|
||||
let a = dx * dx + dy * dy
|
||||
let b = -2.0 * (dx * innerX + dy * innerY)
|
||||
let c = innerX * innerX + innerY * innerY - radius * radius
|
||||
let discriminant = max(0.0, b * b - 4.0 * a * c)
|
||||
let sqrtDiscriminant = sqrt(discriminant)
|
||||
let t1 = (-b - sqrtDiscriminant) / (2.0 * a)
|
||||
let t2 = (-b + sqrtDiscriminant) / (2.0 * a)
|
||||
let tCorner = max(0.0, max(t1, t2))
|
||||
let cornerLocal = signedPoint(dx * tCorner, dy * tCorner, dir)
|
||||
return CGPoint(x: rectCenter.x + cornerLocal.x, y: rectCenter.y + cornerLocal.y)
|
||||
}
|
||||
|
||||
var bakedSizes: [CGSize] = []
|
||||
var bakedPositions: [CGPoint] = []
|
||||
var localPositions: [CGPoint] = []
|
||||
|
|
@ -444,14 +508,11 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
}
|
||||
|
||||
let lineDirection = centerLineDirection
|
||||
let nearestEdgePoint = nearestBoundaryPointOnRoundedRect(
|
||||
point: CGPoint(
|
||||
x: blobCenter.x + lineDirection.x * 10000.0,
|
||||
y: blobCenter.y + lineDirection.y * 10000.0
|
||||
),
|
||||
let nearestEdgePoint = boundaryPointOnRoundedRectRay(
|
||||
rectCenter: blobCenter,
|
||||
rectSize: scaledSize,
|
||||
cornerRadius: scaledCornerRadius
|
||||
cornerRadius: scaledCornerRadius,
|
||||
direction: lineDirection
|
||||
)
|
||||
|
||||
let normalizedSuckProgress = max(0.0, min(1.0, t / sourceSuckDurationFraction))
|
||||
|
|
@ -462,17 +523,20 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
let normalizedFinalInsideProgress = max(0.0, min(1.0, (t - sourceFinalInsideStartFraction) / max(0.001, 1.0 - sourceFinalInsideStartFraction)))
|
||||
let oneMinusFinalInsideProgress = 1.0 - normalizedFinalInsideProgress
|
||||
let finalInsideEase = 1.0 - oneMinusFinalInsideProgress * oneMinusFinalInsideProgress * oneMinusFinalInsideProgress
|
||||
let sourceInsetDistance = baseSourceInsetDistance + (sourceFinalInsideDistance - baseSourceInsetDistance) * finalInsideEase
|
||||
let sourceHalfWidth = fromRect.width * 0.5
|
||||
let sourceHalfHeight = fromRect.height * 0.5
|
||||
let sourceHalfExtentAlongRay = abs(lineDirection.x) * sourceHalfWidth + abs(lineDirection.y) * sourceHalfHeight
|
||||
let sourceFinalInsideDistance = sourceFinalFurthestInsideDistance - sourceHalfExtentAlongRay * 2.0
|
||||
let sourceInsetDistance = baseSourceInsetDistance + (sourceFinalInsideDistance - baseSourceInsetDistance) * finalInsideEase
|
||||
let sourceCenterDistance = sourceInsetDistance + sourceHalfExtentAlongRay
|
||||
let sourcePosition = CGPoint(
|
||||
x: nearestEdgePoint.x + lineDirection.x * sourceCenterDistance,
|
||||
y: nearestEdgePoint.y + lineDirection.y * sourceCenterDistance
|
||||
)
|
||||
let centerLineDistance = (sourcePosition.x - fromCenter.x) * centerLineDirection.x + (sourcePosition.y - fromCenter.y) * centerLineDirection.y
|
||||
let clampedCenterLineDistance = max(centerLineMinDistance, min(centerLineMaxDistance, centerLineDistance))
|
||||
let centerLineOutwardAllowance = max(0.0, centerLineDistance) * finalInsideEase
|
||||
let centerLineUpperBound = sourceInsetDistance < 0.0 ? centerLineOutwardAllowance : 0.0
|
||||
let clampedCenterLineDistance = max(centerLineMinDistance, min(centerLineUpperBound, centerLineDistance))
|
||||
var projectedSourcePosition = CGPoint(
|
||||
x: fromCenter.x + centerLineDirection.x * clampedCenterLineDistance,
|
||||
y: fromCenter.y + centerLineDirection.y * clampedCenterLineDistance
|
||||
|
|
@ -481,26 +545,19 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
x: projectedSourcePosition.x - centerLineDirection.x * sourceHalfExtentAlongRay,
|
||||
y: projectedSourcePosition.y - centerLineDirection.y * sourceHalfExtentAlongRay
|
||||
)
|
||||
let sourceNearestBoundaryPoint = nearestBoundaryPointOnRoundedRect(
|
||||
point: sourceNearestPoint,
|
||||
let sourceNearestBoundaryPoint = boundaryPointOnRoundedRectRay(
|
||||
rectCenter: blobCenter,
|
||||
rectSize: scaledSize,
|
||||
cornerRadius: scaledCornerRadius
|
||||
cornerRadius: scaledCornerRadius,
|
||||
direction: lineDirection
|
||||
)
|
||||
let sourceNearestDistance = hypot(
|
||||
sourceNearestPoint.x - sourceNearestBoundaryPoint.x,
|
||||
sourceNearestPoint.y - sourceNearestBoundaryPoint.y
|
||||
)
|
||||
let sourceNearestSignedDistance: CGFloat = isPointInsideRoundedRect(
|
||||
point: sourceNearestPoint,
|
||||
rectCenter: blobCenter,
|
||||
rectSize: scaledSize,
|
||||
cornerRadius: scaledCornerRadius
|
||||
) ? -sourceNearestDistance : sourceNearestDistance
|
||||
let sourceNearestSignedDistance: CGFloat =
|
||||
(sourceNearestPoint.x - sourceNearestBoundaryPoint.x) * lineDirection.x +
|
||||
(sourceNearestPoint.y - sourceNearestBoundaryPoint.y) * lineDirection.y
|
||||
let centerCorrection = sourceNearestSignedDistance - sourceInsetDistance
|
||||
if abs(centerCorrection) > 0.01 {
|
||||
let correctedCenterLineDistance = centerLineDistance - centerCorrection
|
||||
let clampedCorrectedCenterLineDistance = max(centerLineMinDistance, min(centerLineMaxDistance, correctedCenterLineDistance))
|
||||
let clampedCorrectedCenterLineDistance = max(centerLineMinDistance, min(centerLineUpperBound, correctedCenterLineDistance))
|
||||
projectedSourcePosition = CGPoint(
|
||||
x: fromCenter.x + centerLineDirection.x * clampedCorrectedCenterLineDistance,
|
||||
y: fromCenter.y + centerLineDirection.y * clampedCorrectedCenterLineDistance
|
||||
|
|
@ -551,7 +608,7 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
|
||||
let sourceMaxEdgeDistance: CGFloat = 20.0
|
||||
let sourceSuckDurationFraction: CGFloat = 0.9
|
||||
let sourceFinalInsideDistance: CGFloat = -8.0
|
||||
let sourceFinalFurthestInsideDistance: CGFloat = -8.0
|
||||
let sourceFinalInsideStartFraction: CGFloat = 0.65
|
||||
let sourceFullInsideInset: CGFloat = max(1.0, min(fromRect.width, fromRect.height) * 0.5)
|
||||
let sampleCount = 30
|
||||
|
|
@ -566,7 +623,6 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
let centerLineLength = hypot(centerLineDelta.x, centerLineDelta.y)
|
||||
let centerLineDirection: CGPoint = centerLineLength > 1e-6 ? CGPoint(x: centerLineDelta.x / centerLineLength, y: centerLineDelta.y / centerLineLength) : CGPoint(x: 1.0, y: 0.0)
|
||||
let centerLineMinDistance: CGFloat = -centerLineLength
|
||||
let centerLineMaxDistance: CGFloat = 0.0
|
||||
|
||||
func isPointInsideRoundedRect(point: CGPoint, rectCenter: CGPoint, rectSize: CGSize, cornerRadius: CGFloat) -> Bool {
|
||||
let halfWidth = rectSize.width * 0.5
|
||||
|
|
@ -698,6 +754,72 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
return CGPoint(x: rectCenter.x + nearestLocal.x, y: rectCenter.y + nearestLocal.y)
|
||||
}
|
||||
|
||||
func boundaryPointOnRoundedRectRay(rectCenter: CGPoint, rectSize: CGSize, cornerRadius: CGFloat, direction: CGPoint) -> CGPoint {
|
||||
let halfWidth = rectSize.width * 0.5
|
||||
let halfHeight = rectSize.height * 0.5
|
||||
if halfWidth <= 0.0 || halfHeight <= 0.0 {
|
||||
return rectCenter
|
||||
}
|
||||
|
||||
let radius = max(0.0, min(cornerRadius, min(halfWidth, halfHeight)))
|
||||
let innerX = max(0.0, halfWidth - radius)
|
||||
let innerY = max(0.0, halfHeight - radius)
|
||||
|
||||
let dirLength = hypot(direction.x, direction.y)
|
||||
let dir: CGPoint
|
||||
if dirLength > 1e-6 {
|
||||
dir = CGPoint(x: direction.x / dirLength, y: direction.y / dirLength)
|
||||
} else {
|
||||
dir = CGPoint(x: 1.0, y: 0.0)
|
||||
}
|
||||
let dx = abs(dir.x)
|
||||
let dy = abs(dir.y)
|
||||
|
||||
@inline(__always)
|
||||
func signedPoint(_ x: CGFloat, _ y: CGFloat, _ direction: CGPoint) -> CGPoint {
|
||||
let sx: CGFloat = direction.x >= 0.0 ? 1.0 : -1.0
|
||||
let sy: CGFloat = direction.y >= 0.0 ? 1.0 : -1.0
|
||||
return CGPoint(x: x * sx, y: y * sy)
|
||||
}
|
||||
|
||||
if dx > 1e-6 {
|
||||
let tVertical = halfWidth / dx
|
||||
let yAtVertical = tVertical * dy
|
||||
if yAtVertical <= innerY + 1e-6 {
|
||||
let local = signedPoint(halfWidth, yAtVertical, dir)
|
||||
return CGPoint(x: rectCenter.x + local.x, y: rectCenter.y + local.y)
|
||||
}
|
||||
}
|
||||
|
||||
if dy > 1e-6 {
|
||||
let tHorizontal = halfHeight / dy
|
||||
let xAtHorizontal = tHorizontal * dx
|
||||
if xAtHorizontal <= innerX + 1e-6 {
|
||||
let local = signedPoint(xAtHorizontal, halfHeight, dir)
|
||||
return CGPoint(x: rectCenter.x + local.x, y: rectCenter.y + local.y)
|
||||
}
|
||||
}
|
||||
|
||||
if radius <= 1e-6 {
|
||||
let tx = dx > 1e-6 ? (halfWidth / dx) : CGFloat.greatestFiniteMagnitude
|
||||
let ty = dy > 1e-6 ? (halfHeight / dy) : CGFloat.greatestFiniteMagnitude
|
||||
let t = min(tx, ty)
|
||||
let local = signedPoint(dx * t, dy * t, dir)
|
||||
return CGPoint(x: rectCenter.x + local.x, y: rectCenter.y + local.y)
|
||||
}
|
||||
|
||||
let a = dx * dx + dy * dy
|
||||
let b = -2.0 * (dx * innerX + dy * innerY)
|
||||
let c = innerX * innerX + innerY * innerY - radius * radius
|
||||
let discriminant = max(0.0, b * b - 4.0 * a * c)
|
||||
let sqrtDiscriminant = sqrt(discriminant)
|
||||
let t1 = (-b - sqrtDiscriminant) / (2.0 * a)
|
||||
let t2 = (-b + sqrtDiscriminant) / (2.0 * a)
|
||||
let tCorner = max(0.0, max(t1, t2))
|
||||
let cornerLocal = signedPoint(dx * tCorner, dy * tCorner, dir)
|
||||
return CGPoint(x: rectCenter.x + cornerLocal.x, y: rectCenter.y + cornerLocal.y)
|
||||
}
|
||||
|
||||
var bakedSizes: [CGSize] = []
|
||||
var bakedPositions: [CGPoint] = []
|
||||
var localPositions: [CGPoint] = []
|
||||
|
|
@ -757,14 +879,11 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
}
|
||||
|
||||
let lineDirection = centerLineDirection
|
||||
let nearestEdgePoint = nearestBoundaryPointOnRoundedRect(
|
||||
point: CGPoint(
|
||||
x: blobCenter.x + lineDirection.x * 10000.0,
|
||||
y: blobCenter.y + lineDirection.y * 10000.0
|
||||
),
|
||||
let nearestEdgePoint = boundaryPointOnRoundedRectRay(
|
||||
rectCenter: blobCenter,
|
||||
rectSize: scaledSize,
|
||||
cornerRadius: scaledCornerRadius
|
||||
cornerRadius: scaledCornerRadius,
|
||||
direction: lineDirection
|
||||
)
|
||||
|
||||
let normalizedSuckProgress = max(0.0, min(1.0, t / sourceSuckDurationFraction))
|
||||
|
|
@ -775,17 +894,20 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
let normalizedFinalInsideProgress = max(0.0, min(1.0, (t - sourceFinalInsideStartFraction) / max(0.001, 1.0 - sourceFinalInsideStartFraction)))
|
||||
let oneMinusFinalInsideProgress = 1.0 - normalizedFinalInsideProgress
|
||||
let finalInsideEase = 1.0 - oneMinusFinalInsideProgress * oneMinusFinalInsideProgress * oneMinusFinalInsideProgress
|
||||
let sourceInsetDistance = baseSourceInsetDistance + (sourceFinalInsideDistance - baseSourceInsetDistance) * finalInsideEase
|
||||
let sourceHalfWidth = fromRect.width * 0.5
|
||||
let sourceHalfHeight = fromRect.height * 0.5
|
||||
let sourceHalfExtentAlongRay = abs(lineDirection.x) * sourceHalfWidth + abs(lineDirection.y) * sourceHalfHeight
|
||||
let sourceFinalInsideDistance = sourceFinalFurthestInsideDistance - sourceHalfExtentAlongRay * 2.0
|
||||
let sourceInsetDistance = baseSourceInsetDistance + (sourceFinalInsideDistance - baseSourceInsetDistance) * finalInsideEase
|
||||
let sourceCenterDistance = sourceInsetDistance + sourceHalfExtentAlongRay
|
||||
let sourcePosition = CGPoint(
|
||||
x: nearestEdgePoint.x + lineDirection.x * sourceCenterDistance,
|
||||
y: nearestEdgePoint.y + lineDirection.y * sourceCenterDistance
|
||||
)
|
||||
let centerLineDistance = (sourcePosition.x - fromCenter.x) * centerLineDirection.x + (sourcePosition.y - fromCenter.y) * centerLineDirection.y
|
||||
let clampedCenterLineDistance = max(centerLineMinDistance, min(centerLineMaxDistance, centerLineDistance))
|
||||
let centerLineOutwardAllowance = max(0.0, centerLineDistance) * finalInsideEase
|
||||
let centerLineUpperBound = sourceInsetDistance < 0.0 ? centerLineOutwardAllowance : 0.0
|
||||
let clampedCenterLineDistance = max(centerLineMinDistance, min(centerLineUpperBound, centerLineDistance))
|
||||
var projectedSourcePosition = CGPoint(
|
||||
x: fromCenter.x + centerLineDirection.x * clampedCenterLineDistance,
|
||||
y: fromCenter.y + centerLineDirection.y * clampedCenterLineDistance
|
||||
|
|
@ -794,26 +916,19 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
x: projectedSourcePosition.x - centerLineDirection.x * sourceHalfExtentAlongRay,
|
||||
y: projectedSourcePosition.y - centerLineDirection.y * sourceHalfExtentAlongRay
|
||||
)
|
||||
let sourceNearestBoundaryPoint = nearestBoundaryPointOnRoundedRect(
|
||||
point: sourceNearestPoint,
|
||||
let sourceNearestBoundaryPoint = boundaryPointOnRoundedRectRay(
|
||||
rectCenter: blobCenter,
|
||||
rectSize: scaledSize,
|
||||
cornerRadius: scaledCornerRadius
|
||||
cornerRadius: scaledCornerRadius,
|
||||
direction: lineDirection
|
||||
)
|
||||
let sourceNearestDistance = hypot(
|
||||
sourceNearestPoint.x - sourceNearestBoundaryPoint.x,
|
||||
sourceNearestPoint.y - sourceNearestBoundaryPoint.y
|
||||
)
|
||||
let sourceNearestSignedDistance: CGFloat = isPointInsideRoundedRect(
|
||||
point: sourceNearestPoint,
|
||||
rectCenter: blobCenter,
|
||||
rectSize: scaledSize,
|
||||
cornerRadius: scaledCornerRadius
|
||||
) ? -sourceNearestDistance : sourceNearestDistance
|
||||
let sourceNearestSignedDistance: CGFloat =
|
||||
(sourceNearestPoint.x - sourceNearestBoundaryPoint.x) * lineDirection.x +
|
||||
(sourceNearestPoint.y - sourceNearestBoundaryPoint.y) * lineDirection.y
|
||||
let centerCorrection = sourceNearestSignedDistance - sourceInsetDistance
|
||||
if abs(centerCorrection) > 0.01 {
|
||||
let correctedCenterLineDistance = centerLineDistance - centerCorrection
|
||||
let clampedCorrectedCenterLineDistance = max(centerLineMinDistance, min(centerLineMaxDistance, correctedCenterLineDistance))
|
||||
let clampedCorrectedCenterLineDistance = max(centerLineMinDistance, min(centerLineUpperBound, correctedCenterLineDistance))
|
||||
projectedSourcePosition = CGPoint(
|
||||
x: fromCenter.x + centerLineDirection.x * clampedCorrectedCenterLineDistance,
|
||||
y: fromCenter.y + centerLineDirection.y * clampedCorrectedCenterLineDistance
|
||||
|
|
@ -996,20 +1111,13 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
self.setIsFilterActive(isFilterActive: true)
|
||||
sourceEffectView.setTransitionFraction(value: 0.0, duration: 0.0)
|
||||
sourceEffectView.setTransitionFraction(value: 1.0, duration: 0.3)
|
||||
//sourceEffectView.backgroundColor = .blue
|
||||
|
||||
let sourceMaxEdgeDistance: CGFloat = 20.0
|
||||
let sourceEaseOutDurationFraction: CGFloat = 0.35
|
||||
let sourceCenterPullStartFraction: CGFloat
|
||||
let sourceCenterPullDurationFraction: CGFloat
|
||||
|
||||
let centerDistance = sqrt(pow(fromRect.midX - toRect.midX, 2.0) + pow(fromRect.midY - toRect.midY, 2.0))
|
||||
if centerDistance >= 100.0 {
|
||||
sourceCenterPullStartFraction = 0.2
|
||||
sourceCenterPullDurationFraction = 0.3
|
||||
} else {
|
||||
sourceCenterPullStartFraction = 0.0
|
||||
sourceCenterPullDurationFraction = 0.0
|
||||
}
|
||||
let sourceSuckDurationFraction: CGFloat = 0.9
|
||||
let sourceFinalFurthestInsideDistance: CGFloat = -8.0
|
||||
let sourceFinalInsideStartFraction: CGFloat = 0.65
|
||||
let sourceFullInsideInset: CGFloat = max(1.0, min(fromRect.width, fromRect.height) * 0.5)
|
||||
|
||||
let sampleCount = 36
|
||||
let sampleEndIndex = CGFloat(sampleCount - 1)
|
||||
|
|
@ -1158,24 +1266,47 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
cornerRadius: radius,
|
||||
direction: centerLineDirection
|
||||
)
|
||||
let outwardDirection = normalized(
|
||||
CGPoint(
|
||||
x: sourceBoundaryPoint.x - centerLinePosition.x,
|
||||
y: sourceBoundaryPoint.y - centerLinePosition.y
|
||||
)
|
||||
)
|
||||
let normalizedSourceProgress = max(0.0, min(1.0, t / sourceEaseOutDurationFraction))
|
||||
let oneMinusSourceProgress = 1.0 - normalizedSourceProgress
|
||||
let sourceEaseOut = 1.0 - oneMinusSourceProgress * oneMinusSourceProgress * oneMinusSourceProgress
|
||||
let sourceEdgeOffset = lerp(-sourceMaxEdgeDistance, sourceMaxEdgeDistance, sourceEaseOut)
|
||||
let sourceEdgePosition = CGPoint(
|
||||
x: sourceBoundaryPoint.x + outwardDirection.x * sourceEdgeOffset,
|
||||
y: sourceBoundaryPoint.y + outwardDirection.y * sourceEdgeOffset
|
||||
)
|
||||
let normalizedCenterPullProgress = max(0.0, min(1.0, (t - sourceCenterPullStartFraction) / max(0.001, sourceCenterPullDurationFraction)))
|
||||
let oneMinusCenterPull = 1.0 - normalizedCenterPullProgress
|
||||
let lineDirection = centerLineDirection
|
||||
let sourceHalfWidth = fromRect.width * 0.5
|
||||
let sourceHalfHeight = fromRect.height * 0.5
|
||||
let sourceHalfExtentAlongRay = abs(lineDirection.x) * sourceHalfWidth + abs(lineDirection.y) * sourceHalfHeight
|
||||
let reverseT = 1.0 - t
|
||||
let normalizedSuckProgress = max(0.0, min(1.0, reverseT / sourceSuckDurationFraction))
|
||||
let oneMinusSuckProgress = 1.0 - normalizedSuckProgress
|
||||
let suckFraction = 1.0 - oneMinusSuckProgress * oneMinusSuckProgress * oneMinusSuckProgress
|
||||
let reverseAnimatedInsetDistance = sourceMaxEdgeDistance - suckFraction * (sourceMaxEdgeDistance + sourceFullInsideInset)
|
||||
let normalizedFinalInsideProgress = max(0.0, min(1.0, (reverseT - sourceFinalInsideStartFraction) / max(0.001, 1.0 - sourceFinalInsideStartFraction)))
|
||||
let oneMinusFinalInsideProgress = 1.0 - normalizedFinalInsideProgress
|
||||
let finalInsideEase = 1.0 - oneMinusFinalInsideProgress * oneMinusFinalInsideProgress * oneMinusFinalInsideProgress
|
||||
let sourceFinalInsideDistance = sourceFinalFurthestInsideDistance - sourceHalfExtentAlongRay * 2.0
|
||||
let reverseInsetDistance = reverseAnimatedInsetDistance + (sourceFinalInsideDistance - reverseAnimatedInsetDistance) * finalInsideEase
|
||||
let oneMinusCenterPull = 1.0 - t
|
||||
let centerPullEase = 1.0 - oneMinusCenterPull * oneMinusCenterPull * oneMinusCenterPull
|
||||
sourcePositions.append(lerpPoint(sourceEdgePosition, centerLinePosition, centerPullEase))
|
||||
let sourceBoundaryDistanceFromCenter =
|
||||
(sourceBoundaryPoint.x - centerLinePosition.x) * lineDirection.x +
|
||||
(sourceBoundaryPoint.y - centerLinePosition.y) * lineDirection.y
|
||||
let sourceCenterInsetDistance = -(sourceBoundaryDistanceFromCenter + sourceHalfExtentAlongRay)
|
||||
let sourceInsetDistance = lerp(reverseInsetDistance, sourceCenterInsetDistance, centerPullEase)
|
||||
let sourceCenterDistance = sourceInsetDistance + sourceHalfExtentAlongRay
|
||||
var projectedSourcePosition = CGPoint(
|
||||
x: sourceBoundaryPoint.x + lineDirection.x * sourceCenterDistance,
|
||||
y: sourceBoundaryPoint.y + lineDirection.y * sourceCenterDistance
|
||||
)
|
||||
let sourceNearestPoint = CGPoint(
|
||||
x: projectedSourcePosition.x - lineDirection.x * sourceHalfExtentAlongRay,
|
||||
y: projectedSourcePosition.y - lineDirection.y * sourceHalfExtentAlongRay
|
||||
)
|
||||
let sourceNearestSignedDistance =
|
||||
(sourceNearestPoint.x - sourceBoundaryPoint.x) * lineDirection.x +
|
||||
(sourceNearestPoint.y - sourceBoundaryPoint.y) * lineDirection.y
|
||||
if sourceNearestSignedDistance > sourceMaxEdgeDistance {
|
||||
let centerCorrection = sourceNearestSignedDistance - sourceMaxEdgeDistance
|
||||
projectedSourcePosition = CGPoint(
|
||||
x: projectedSourcePosition.x - lineDirection.x * centerCorrection,
|
||||
y: projectedSourcePosition.y - lineDirection.y * centerCorrection
|
||||
)
|
||||
}
|
||||
sourcePositions.append(projectedSourcePosition)
|
||||
}
|
||||
|
||||
if let finalContainerPosition = containerPositions.last {
|
||||
|
|
@ -1314,7 +1445,7 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
)
|
||||
}
|
||||
|
||||
public func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) {
|
||||
public func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, transition: ComponentTransition) {
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
let center = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
|
||||
|
||||
|
|
@ -1342,24 +1473,18 @@ final class LensTransitionContainerImpl: UIView, LensTransitionContainerProtocol
|
|||
}
|
||||
|
||||
private final class LensTransitionContainerFallbackImpl: UIView, LensTransitionContainerProtocol {
|
||||
let effectView: LensTransitionContainerEffectView
|
||||
private let containerView: UIView
|
||||
let contentsEffectView: UIView
|
||||
let contentsView: UIView
|
||||
private let backgroundView: GlassBackgroundView
|
||||
|
||||
init(effectView: LensTransitionContainerEffectView) {
|
||||
self.effectView = effectView
|
||||
self.containerView = UIView()
|
||||
self.contentsEffectView = UIView()
|
||||
self.contentsView = UIView()
|
||||
public var contentsView: UIView {
|
||||
return self.backgroundView.contentView
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = GlassBackgroundView()
|
||||
|
||||
super.init(frame: .zero)
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.containerView)
|
||||
self.containerView.addSubview(self.effectView)
|
||||
self.containerView.addSubview(self.contentsEffectView)
|
||||
self.contentsEffectView.addSubview(self.contentsView)
|
||||
self.contentsView.clipsToBounds = true
|
||||
self.addSubview(self.backgroundView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
|
@ -1367,59 +1492,42 @@ private final class LensTransitionContainerFallbackImpl: UIView, LensTransitionC
|
|||
}
|
||||
|
||||
func animateIn(fromRect: CGRect, toRect: CGRect, fromCornerRadius: CGFloat, toCornerRadius: CGFloat, isDark: Bool, sourceEffectView: LensTransitionContainerEffectView) {
|
||||
self.update(size: fromRect.size, cornerRadius: fromCornerRadius, transition: .immediate)
|
||||
UIView.animate(withDuration: 0.5, delay: 0.0, options: [.curveEaseInOut], animations: {
|
||||
self.containerView.center = CGPoint(x: toRect.midX, y: toRect.midY)
|
||||
self.update(size: toRect.size, cornerRadius: toCornerRadius, transition: .immediate)
|
||||
})
|
||||
}
|
||||
|
||||
func animateOut(fromRect: CGRect, toRect: CGRect, fromCornerRadius: CGFloat, toCornerRadius: CGFloat, isDark: Bool, sourceEffectView: LensTransitionContainerEffectView) {
|
||||
self.update(size: fromRect.size, cornerRadius: fromCornerRadius, transition: .immediate)
|
||||
UIView.animate(withDuration: 0.5, delay: 0.0, options: [.curveEaseInOut], animations: {
|
||||
self.containerView.center = CGPoint(x: toRect.midX, y: toRect.midY)
|
||||
self.update(size: toRect.size, cornerRadius: toCornerRadius, transition: .immediate)
|
||||
})
|
||||
}
|
||||
|
||||
func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) {
|
||||
transition.setBounds(view: self.containerView, bounds: CGRect(origin: .zero, size: size))
|
||||
transition.setPosition(view: self.containerView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
|
||||
|
||||
transition.setBounds(view: self.contentsEffectView, bounds: CGRect(origin: .zero, size: size))
|
||||
transition.setPosition(view: self.contentsEffectView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
|
||||
|
||||
transition.setBounds(view: self.contentsView, bounds: CGRect(origin: .zero, size: size))
|
||||
transition.setPosition(view: self.contentsView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
|
||||
transition.setCornerRadius(layer: self.contentsView.layer, cornerRadius: cornerRadius)
|
||||
|
||||
transition.setBounds(view: self.effectView, bounds: CGRect(origin: .zero, size: size))
|
||||
transition.setPosition(view: self.effectView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
|
||||
|
||||
self.effectView.updateCornerRadius(duration: 0.0, keyframes: [cornerRadius])
|
||||
func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, transition: ComponentTransition) {
|
||||
transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: .zero, size: size))
|
||||
transition.setPosition(view: self.backgroundView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5))
|
||||
self.backgroundView.update(size: size, cornerRadius: cornerRadius, isDark: isDark, tintColor: .init(kind: .panel), transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class LensTransitionContainer: UIView {
|
||||
private let impl: (UIView & LensTransitionContainerProtocol)
|
||||
|
||||
public var effectView: LensTransitionContainerEffectView {
|
||||
return self.impl.effectView
|
||||
}
|
||||
|
||||
public var contentsEffectView: UIView {
|
||||
return self.impl.contentsEffectView
|
||||
public var effectView: UIView? {
|
||||
if #available(iOS 26.0, *) {
|
||||
if let impl = self.impl as? LensTransitionContainerImpl {
|
||||
return impl.effectView
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var contentsView: UIView {
|
||||
return self.impl.contentsView
|
||||
return impl.contentsView
|
||||
}
|
||||
|
||||
public init(effectView: LensTransitionContainerEffectView) {
|
||||
if #available(iOS 26.0, *) {
|
||||
self.impl = LensTransitionContainerImpl(effectView: effectView)
|
||||
} else {
|
||||
self.impl = LensTransitionContainerFallbackImpl(effectView: effectView)
|
||||
self.impl = LensTransitionContainerFallbackImpl(frame: CGRect())
|
||||
}
|
||||
super.init(frame: .zero)
|
||||
|
||||
|
|
@ -1443,8 +1551,8 @@ public final class LensTransitionContainer: UIView {
|
|||
self.impl.animateOut(fromRect: fromRect, toRect: toRect, fromCornerRadius: fromCornerRadius, toCornerRadius: toCornerRadius, isDark: isDark, sourceEffectView: sourceEffectView)
|
||||
}
|
||||
|
||||
public func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) {
|
||||
self.impl.update(size: size, cornerRadius: cornerRadius, transition: transition)
|
||||
public func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, transition: ComponentTransition) {
|
||||
self.impl.update(size: size, cornerRadius: cornerRadius, isDark: isDark, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -662,7 +662,7 @@ extension ChatControllerImpl {
|
|||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum, self.presentationInterfaceState.persistentData.topicListPanelLocation == true, self.presentationInterfaceState.chatLocation.threadId != nil {
|
||||
if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum, self.presentationInterfaceState.persistentData.topicListPanelLocation == .side, self.presentationInterfaceState.chatLocation.threadId != nil {
|
||||
self.updateChatLocationThread(threadId: nil, animationDirection: .left)
|
||||
} else {
|
||||
if self.attemptNavigation({ [weak self] in
|
||||
|
|
@ -4540,7 +4540,14 @@ extension ChatControllerImpl {
|
|||
}
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { presentationInterfaceState in
|
||||
var persistentData = presentationInterfaceState.persistentData
|
||||
persistentData.topicListPanelLocation = !persistentData.topicListPanelLocation
|
||||
switch persistentData.topicListPanelLocation {
|
||||
case .top:
|
||||
persistentData.topicListPanelLocation = .side
|
||||
case .side:
|
||||
persistentData.topicListPanelLocation = .bottom
|
||||
case .bottom:
|
||||
persistentData.topicListPanelLocation = .top
|
||||
}
|
||||
return presentationInterfaceState.updatedPersistentData(persistentData)
|
||||
})
|
||||
}, updateDisplayHistoryFilterAsList: { [weak self] displayAsList in
|
||||
|
|
|
|||
|
|
@ -249,6 +249,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
private var floatingTopicsPanelContainer: ChatControllerTitlePanelNodeContainer
|
||||
private var floatingTopicsPanel: (view: ComponentView<ChatSidePanelEnvironment>, component: ChatFloatingTopicsPanel)?
|
||||
private var headerPanelsView: ComponentView<Empty>?
|
||||
private var footerPanelsView: ComponentView<Empty>?
|
||||
|
||||
private var topBackgroundEdgeEffectNode: WallpaperEdgeEffectNode?
|
||||
private var bottomBackgroundEdgeEffectNode: WallpaperEdgeEffectNode?
|
||||
|
|
@ -762,6 +763,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
self.usePlainInputSeparator = false
|
||||
self.plainInputSeparatorAlpha = nil
|
||||
}
|
||||
self.inputPanelBackgroundNode.isUserInteractionEnabled = false
|
||||
|
||||
self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme, preferClearGlass: self.chatPresentationInterfaceState.preferredGlassType == .clear, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat, backgroundNode: self.backgroundNode, isChatRotated: historyNodeRotated)
|
||||
self.navigateButtons.accessibilityElementsHidden = true
|
||||
|
|
@ -1347,13 +1349,19 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
self.containerLayoutAndNavigationBarHeight = (layout, navigationBarHeight)
|
||||
|
||||
var headerPanels: [HeaderPanelContainerComponent.Panel] = []
|
||||
var footerPanels: [HeaderPanelContainerComponent.Panel] = []
|
||||
|
||||
if let headerTopicsPanel = headerTopicsPanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, context: self.context, controllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction, force: false) {
|
||||
headerPanels.append(HeaderPanelContainerComponent.Panel(
|
||||
if let headerTopicsPanel = headerTopicsPanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, context: self.context, controllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction, force: false) {
|
||||
let panel = HeaderPanelContainerComponent.Panel(
|
||||
key: "topics",
|
||||
orderIndex: 0,
|
||||
component: headerTopicsPanel
|
||||
))
|
||||
)
|
||||
if self.chatPresentationInterfaceState.persistentData.topicListPanelLocation == .top {
|
||||
headerPanels.append(panel)
|
||||
} else {
|
||||
footerPanels.append(panel)
|
||||
}
|
||||
}
|
||||
if let mediaPlayback = self.controller?.globalControlPanelsContextState?.mediaPlayback {
|
||||
headerPanels.append(HeaderPanelContainerComponent.Panel(
|
||||
|
|
@ -2232,6 +2240,59 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
var containerInsets = insets
|
||||
if let dismissAsOverlayLayout = self.dismissAsOverlayLayout {
|
||||
if let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow {
|
||||
containerInsets = dismissAsOverlayLayout.insets(options: [])
|
||||
containerInsets.bottom = max(inputNodeHeightAndOverflow.0 + inputNodeHeightAndOverflow.1, insets.bottom)
|
||||
} else {
|
||||
containerInsets = dismissAsOverlayLayout.insets(options: [.input])
|
||||
}
|
||||
}
|
||||
|
||||
var footerPanelsSize: CGSize?
|
||||
if !footerPanels.isEmpty {
|
||||
let footerPanelsView: ComponentView<Empty>
|
||||
var footerPanelsTransition = ComponentTransition(transition)
|
||||
if let current = self.footerPanelsView {
|
||||
footerPanelsView = current
|
||||
} else {
|
||||
footerPanelsTransition = footerPanelsTransition.withAnimation(.none)
|
||||
footerPanelsView = ComponentView()
|
||||
self.footerPanelsView = footerPanelsView
|
||||
}
|
||||
|
||||
var footerPanelsWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right + 16.0
|
||||
if containerInsets.bottom <= 32.0 {
|
||||
footerPanelsWidth -= 36.0
|
||||
}
|
||||
|
||||
let footerPanelsSizeValue = footerPanelsView.update(
|
||||
transition: footerPanelsTransition,
|
||||
component: AnyComponent(HeaderPanelContainerComponent(
|
||||
theme: self.chatPresentationInterfaceState.theme,
|
||||
preferClearGlass: self.chatPresentationInterfaceState.preferredGlassType == .clear,
|
||||
tabs: nil,
|
||||
panels: footerPanels
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: footerPanelsWidth, height: layout.size.height)
|
||||
)
|
||||
footerPanelsSize = footerPanelsSizeValue
|
||||
floatingTopicsPanelInsets.bottom += footerPanelsSizeValue.height
|
||||
} else if let footerPanelsView = self.footerPanelsView {
|
||||
self.footerPanelsView = nil
|
||||
if let footerPanelsComponentView = footerPanelsView.view {
|
||||
transition.updateAlpha(layer: footerPanelsComponentView.layer, alpha: 0.0, completion: { [weak footerPanelsComponentView] _ in
|
||||
footerPanelsComponentView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let footerPanelsSize {
|
||||
inputPanelsHeight += 12.0 + footerPanelsSize.height
|
||||
}
|
||||
|
||||
if self.dismissedAsOverlay {
|
||||
inputPanelsHeight = 0.0
|
||||
}
|
||||
|
|
@ -2308,16 +2369,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
transition.updateFrame(node: scrollContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
}
|
||||
|
||||
var containerInsets = insets
|
||||
if let dismissAsOverlayLayout = self.dismissAsOverlayLayout {
|
||||
if let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow {
|
||||
containerInsets = dismissAsOverlayLayout.insets(options: [])
|
||||
containerInsets.bottom = max(inputNodeHeightAndOverflow.0 + inputNodeHeightAndOverflow.1, insets.bottom)
|
||||
} else {
|
||||
containerInsets = dismissAsOverlayLayout.insets(options: [.input])
|
||||
}
|
||||
}
|
||||
|
||||
let visibleAreaInset = UIEdgeInsets(top: containerInsets.top, left: 0.0, bottom: containerInsets.bottom + inputPanelsHeight + 8.0 + 8.0, right: 0.0)
|
||||
self.visibleAreaInset = visibleAreaInset
|
||||
|
||||
|
|
@ -2567,6 +2618,17 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
sidePanelTopInset += headerPanelsSize.height + 2.0
|
||||
}
|
||||
|
||||
if let footerPanelsComponentView = self.footerPanelsView?.view, let footerPanelsSize {
|
||||
let footerPanelsFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - footerPanelsSize.width) * 0.5), y: layout.size.height - (containerInsets.bottom + inputPanelsHeight + 8.0)), size: footerPanelsSize)
|
||||
var footerPanelsTransition = ComponentTransition(transition)
|
||||
if footerPanelsComponentView.superview == nil {
|
||||
footerPanelsTransition.animateAlpha(view: footerPanelsComponentView, from: 0.0, to: 1.0)
|
||||
footerPanelsTransition = footerPanelsTransition.withAnimation(.none)
|
||||
self.floatingTopicsPanelContainer.view.addSubview(footerPanelsComponentView)
|
||||
}
|
||||
footerPanelsTransition.setFrame(view: footerPanelsComponentView, frame: footerPanelsFrame)
|
||||
}
|
||||
|
||||
let floatingTopicsPanelContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 0.0, height: layout.size.height))
|
||||
transition.updateFrame(node: self.floatingTopicsPanelContainer, frame: floatingTopicsPanelContainerFrame)
|
||||
if let floatingTopicsPanel = self.floatingTopicsPanel {
|
||||
|
|
@ -5076,6 +5138,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
if let headerPanelsComponentView = self.headerPanelsView?.view as? HeaderPanelContainerComponent.View, let topicsPanelView = headerPanelsComponentView.panel(forKey: AnyHashable("topics")) as? ChatTopicsHeaderPanelComponent.View {
|
||||
leftIndex = topicsPanelView.topicIndex(threadId: fromLocation)
|
||||
rightIndex = topicsPanelView.topicIndex(threadId: toLocation)
|
||||
} else if let footerPanelsComponentView = self.footerPanelsView?.view as? HeaderPanelContainerComponent.View, let topicsPanelView = footerPanelsComponentView.panel(forKey: AnyHashable("topics")) as? ChatTopicsHeaderPanelComponent.View {
|
||||
leftIndex = topicsPanelView.topicIndex(threadId: fromLocation)
|
||||
rightIndex = topicsPanelView.topicIndex(threadId: toLocation)
|
||||
}
|
||||
}
|
||||
guard let leftIndex, let rightIndex else {
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ func headerTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterf
|
|||
}
|
||||
|
||||
if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect), chatPresentationInterfaceState.search == nil {
|
||||
let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation
|
||||
let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation == .side
|
||||
if !topicListDisplayModeOnTheSide {
|
||||
return AnyComponent(ChatTopicsHeaderPanelComponent(
|
||||
context: context,
|
||||
|
|
@ -282,7 +282,7 @@ func headerTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterf
|
|||
if !chatPresentationInterfaceState.viewForumAsMessages {
|
||||
return nil
|
||||
}
|
||||
let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation
|
||||
let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation == .side
|
||||
if !topicListDisplayModeOnTheSide {
|
||||
return AnyComponent(ChatTopicsHeaderPanelComponent(
|
||||
context: context,
|
||||
|
|
@ -314,7 +314,7 @@ func headerTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterf
|
|||
return nil
|
||||
}
|
||||
}
|
||||
let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation
|
||||
let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation == .side
|
||||
if !topicListDisplayModeOnTheSide {
|
||||
return AnyComponent(ChatTopicsHeaderPanelComponent(
|
||||
context: context,
|
||||
|
|
@ -367,7 +367,7 @@ func floatingTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInte
|
|||
}
|
||||
|
||||
if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = chatPresentationInterfaceState.renderedPeer?.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect), chatPresentationInterfaceState.search == nil {
|
||||
let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation
|
||||
let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation == .side
|
||||
if topicListDisplayModeOnTheSide {
|
||||
return ChatFloatingTopicsPanel(
|
||||
context: context,
|
||||
|
|
@ -399,7 +399,7 @@ func floatingTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInte
|
|||
if !chatPresentationInterfaceState.viewForumAsMessages {
|
||||
return nil
|
||||
}
|
||||
let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation
|
||||
let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation == .side
|
||||
if topicListDisplayModeOnTheSide {
|
||||
return ChatFloatingTopicsPanel(
|
||||
context: context,
|
||||
|
|
@ -433,7 +433,7 @@ func floatingTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInte
|
|||
return nil
|
||||
}
|
||||
}
|
||||
let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation
|
||||
let topicListDisplayModeOnTheSide = chatPresentationInterfaceState.persistentData.topicListPanelLocation == .side
|
||||
if topicListDisplayModeOnTheSide {
|
||||
return ChatFloatingTopicsPanel(
|
||||
context: context,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ NSObject * _Nullable makeLuminanceToAlphaFilter();
|
|||
NSObject * _Nullable makeColorInvertFilter();
|
||||
NSObject * _Nullable makeMonochromeFilter();
|
||||
NSObject * _Nullable makeDisplacementMapFilter();
|
||||
NSObject * _Nullable makeColorMatrixFilter();
|
||||
|
||||
void setLayerDisableScreenshots(CALayer * _Nonnull layer, bool disableScreenshots);
|
||||
bool getLayerDisableScreenshots(CALayer * _Nonnull layer);
|
||||
|
|
|
|||
|
|
@ -290,6 +290,10 @@ NSObject * _Nullable makeDisplacementMapFilter() {
|
|||
}
|
||||
}
|
||||
|
||||
NSObject * _Nullable makeColorMatrixFilter() {
|
||||
return [(id<GraphicsFilterProtocol>)NSClassFromString(@"CAFilter") filterWithName:@"colorMatrix"];
|
||||
}
|
||||
|
||||
static const void *layerDisableScreenshotsKey = &layerDisableScreenshotsKey;
|
||||
|
||||
void setLayerDisableScreenshots(CALayer * _Nonnull layer, bool disableScreenshots) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue