mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-03 14:17:19 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
264 lines
12 KiB
Swift
264 lines
12 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AppBundle
|
|
import Display
|
|
import ComponentFlow
|
|
import GlassBackgroundComponent
|
|
import TelegramPresentationData
|
|
|
|
final class WalletSendAnimatedRateButton: UIControl {
|
|
private let contentView = UIView()
|
|
private let backgroundView = WalletSendCommentBackgroundView(frame: .zero)
|
|
private let glassHighlightRecognizer = GlassHighlightGestureRecognizer(target: nil, action: nil)
|
|
private let title = ComponentView<Empty>()
|
|
private var titleSize = CGSize.zero
|
|
private let gramIcon = UIImageView()
|
|
private var arrows: [UIImageView] = []
|
|
private var displayLink: SharedDisplayLinkDriver.Link?
|
|
private var arrowTiming: WalletSendAmountMotionTiming?
|
|
private var arrowFrom: CGFloat = 0.0
|
|
private var arrowTo: CGFloat = 0.0
|
|
private var arrowPhase: CGFloat = 0.0
|
|
private var arrowPaceFrom: CGFloat = 0.0
|
|
private var arrowPace: CGFloat = 0.0
|
|
private var geometryTiming: WalletSendAmountMotionTiming?
|
|
private var previousMode: WalletSendInputMode?
|
|
private var previousText = ""
|
|
private var widthFrom: CGFloat = 22.0
|
|
private var widthTo: CGFloat = 22.0
|
|
private var currentWidth: CGFloat = 22.0
|
|
private var gramFrom: CGFloat = 0.0
|
|
private var gramTo: CGFloat = 0.0
|
|
private var currentGram: CGFloat = 0.0
|
|
private var visible = false
|
|
private var theme: PresentationTheme?
|
|
var action: (() -> Void)?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
self.isExclusiveTouch = true
|
|
self.isAccessibilityElement = true
|
|
self.accessibilityTraits = .button
|
|
self.addGestureRecognizer(self.glassHighlightRecognizer)
|
|
self.glassHighlightRecognizer.isEnabled = false
|
|
self.contentView.isUserInteractionEnabled = false
|
|
self.contentView.clipsToBounds = true
|
|
self.contentView.layer.cornerRadius = 13.0
|
|
self.addSubview(self.contentView)
|
|
self.contentView.addSubview(self.backgroundView)
|
|
self.gramIcon.image = UIImage(bundleImageName: "Wallet/TopGram")
|
|
self.gramIcon.contentMode = .scaleAspectFit
|
|
self.contentView.addSubview(self.gramIcon)
|
|
for side in 0 ..< 2 {
|
|
for _ in 0 ..< 5 {
|
|
let view = UIImageView(image: UIImage(bundleImageName: "Wallet/Swap")?.withRenderingMode(.alwaysTemplate))
|
|
view.contentMode = .scaleAspectFit
|
|
let mask = CAShapeLayer()
|
|
mask.path = CGPath(rect: CGRect(x: CGFloat(side) * 9.0, y: 0.0, width: 9.0, height: 18.0), transform: nil)
|
|
view.layer.mask = mask
|
|
self.contentView.addSubview(view)
|
|
self.arrows.append(view)
|
|
}
|
|
}
|
|
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(self.applicationWillResignActive), name: UIApplication.willResignActiveNotification, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(self.stopAnimations), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(self.reduceMotionStatusChanged), name: UIAccessibility.reduceMotionStatusDidChangeNotification, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(self.resumePresentation), name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
deinit {
|
|
self.displayLink?.invalidate()
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
@objc private func applicationWillResignActive() {
|
|
self.updateTouchEffect(isActive: false)
|
|
}
|
|
|
|
@objc private func stopAnimations() {
|
|
self.updateTouchEffect(isActive: false)
|
|
self.finishMotion()
|
|
}
|
|
|
|
@objc private func resumePresentation() {
|
|
self.updateTouchEffect()
|
|
self.setNeedsLayout()
|
|
}
|
|
|
|
@objc private func reduceMotionStatusChanged() {
|
|
self.updateTouchEffect()
|
|
self.finishMotion()
|
|
}
|
|
|
|
@objc private func pressed() { self.action?() }
|
|
|
|
override var isEnabled: Bool {
|
|
didSet {
|
|
self.updateTouchEffect()
|
|
}
|
|
}
|
|
|
|
private func updateTouchEffect(isActive: Bool = true) {
|
|
let isEnabled = isActive && UIApplication.shared.applicationState == .active && self.window != nil && self.visible && self.isEnabled && !UIAccessibility.isReduceMotionEnabled
|
|
self.glassHighlightRecognizer.isEnabled = isEnabled
|
|
if !isEnabled {
|
|
self.layer.removeAnimation(forKey: "sublayerTransform")
|
|
self.layer.sublayerTransform = CATransform3DIdentity
|
|
}
|
|
}
|
|
|
|
override func didMoveToWindow() {
|
|
super.didMoveToWindow()
|
|
self.updateTouchEffect()
|
|
if self.window == nil { self.finishMotion() }
|
|
}
|
|
|
|
func update(
|
|
text: String, displaysGramIcon: Bool, mode: WalletSendInputMode,
|
|
dateTimeFormat: PresentationDateTimeFormat, theme: PresentationTheme,
|
|
isVisible: Bool, isEnabled: Bool, timing sharedTiming: WalletSendAmountMotionTiming?,
|
|
maxWidth: CGFloat
|
|
) -> CGSize {
|
|
let wasVisible = self.visible
|
|
self.visible = isVisible
|
|
self.isEnabled = isEnabled
|
|
self.theme = theme
|
|
self.accessibilityLabel = displaysGramIcon ? "GRAM " + text : text
|
|
for arrow in self.arrows { arrow.tintColor = theme.list.itemSecondaryTextColor }
|
|
let now = CACurrentMediaTime()
|
|
let switched = self.previousMode != nil && self.previousMode != mode
|
|
let titleView = self.title.view as? WalletSendAnimatedTextComponent.View
|
|
let previousProgress = self.geometryTiming?.layoutProgress(at: now) ?? 1.0
|
|
let previousWidth = self.widthFrom + (self.widthTo - self.widthFrom) * previousProgress + (titleView?.widthAdjustment(at: now) ?? 0.0)
|
|
let sharedStart = sharedTiming.flatMap { now - $0.start < $0.duration ? $0.start : nil }
|
|
let timing: WalletSendAmountMotionTiming?
|
|
if self.previousMode != nil && wasVisible && isVisible && titleView?.canAnimate == true {
|
|
timing = WalletSendAmountMotionTiming(spin: switched, up: !(sharedTiming?.up ?? true), start: sharedStart ?? now)
|
|
} else {
|
|
timing = nil
|
|
}
|
|
let titleSize = self.title.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(WalletSendAnimatedTextComponent(
|
|
text: text,
|
|
font: WalletSendAmountFonts.rate,
|
|
color: theme.list.itemSecondaryTextColor,
|
|
dateTimeFormat: dateTimeFormat,
|
|
isVisible: isVisible,
|
|
timing: timing
|
|
)),
|
|
environment: {}, containerSize: CGSize(width: max(1.0, maxWidth - 56.0), height: 26.0)
|
|
)
|
|
self.titleSize = titleSize
|
|
let textWidth = titleSize.width
|
|
let gramWidth: CGFloat = displaysGramIcon ? 19.0 : 0.0
|
|
let width = min(maxWidth, max(22.0, 16.0 + gramWidth + textWidth + 3.0 + 18.0))
|
|
if let titleView = self.title.view {
|
|
if titleView.superview == nil {
|
|
titleView.isUserInteractionEnabled = false
|
|
titleView.accessibilityElementsHidden = true
|
|
self.contentView.insertSubview(titleView, aboveSubview: self.backgroundView)
|
|
}
|
|
}
|
|
if text != self.previousText || self.widthTo != width || self.gramTo != (displaysGramIcon ? 1.0 : 0.0) {
|
|
self.widthFrom = previousWidth
|
|
self.widthTo = width
|
|
self.gramFrom = self.gramFrom + (self.gramTo - self.gramFrom) * previousProgress
|
|
self.gramTo = displaysGramIcon ? 1.0 : 0.0
|
|
self.geometryTiming = timing
|
|
if switched {
|
|
self.arrowTiming = timing
|
|
self.arrowFrom = self.arrowPhase
|
|
self.arrowTo = floor(self.arrowPhase) + 1.0
|
|
self.arrowPaceFrom = self.arrowPace
|
|
}
|
|
}
|
|
self.previousMode = mode
|
|
self.previousText = text
|
|
if !isVisible { self.finishMotion() }
|
|
self.renderFrame()
|
|
if self.isAnimating && self.displayLink == nil {
|
|
self.displayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in
|
|
self?.renderFrame()
|
|
})
|
|
}
|
|
return CGSize(width: width, height: 26.0)
|
|
}
|
|
|
|
private var isAnimating: Bool {
|
|
let now = CACurrentMediaTime()
|
|
return self.visible && ((self.title.view as? WalletSendAnimatedTextComponent.View)?.isAnimating == true || (self.geometryTiming?.progress(at: now) ?? 1.0) < 1.0 || (self.arrowTiming?.progress(at: now) ?? 1.0) < 1.0)
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
self.renderFrame()
|
|
}
|
|
|
|
private func renderFrame() {
|
|
let now = CACurrentMediaTime()
|
|
if !self.isAnimating {
|
|
self.geometryTiming = nil
|
|
self.arrowTiming = nil
|
|
}
|
|
let p = self.geometryTiming?.layoutProgress(at: now) ?? 1.0
|
|
self.currentWidth = self.widthFrom + (self.widthTo - self.widthFrom) * p + ((self.title.view as? WalletSendAnimatedTextComponent.View)?.widthAdjustment(at: now) ?? 0.0)
|
|
self.currentGram = self.gramFrom + (self.gramTo - self.gramFrom) * p
|
|
self.contentView.bounds = CGRect(x: 0.0, y: 0.0, width: self.currentWidth, height: 26.0)
|
|
self.contentView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
|
|
self.backgroundView.frame = self.contentView.bounds
|
|
if let theme = self.theme {
|
|
self.backgroundView.update(
|
|
size: self.contentView.bounds.size,
|
|
maxCornerRadius: 13.0,
|
|
minCornerRadius: 13.0,
|
|
theme: theme,
|
|
hasTail: false
|
|
)
|
|
}
|
|
self.title.view?.frame = CGRect(
|
|
x: 8.0 + 19.0 * self.currentGram,
|
|
y: floorToScreenPixels((26.0 - self.titleSize.height) / 2.0),
|
|
width: self.titleSize.width,
|
|
height: self.titleSize.height
|
|
)
|
|
self.gramIcon.frame = CGRect(x: 8.0, y: 5.0, width: 16.0, height: 16.0)
|
|
self.gramIcon.alpha = self.currentGram
|
|
let raw = self.arrowTiming?.progress(at: now) ?? 1.0
|
|
self.arrowPhase = self.arrowFrom + (self.arrowTo - self.arrowFrom) * WalletSendAmountMotionTiming.ease(raw)
|
|
let phase = self.arrowPhase.truncatingRemainder(dividingBy: 1.0)
|
|
let speed = pow(1.0 - raw, 1.2)
|
|
self.arrowPace = self.arrowPaceFrom + (speed - self.arrowPaceFrom) * WalletSendAmountMotionTiming.ease(min(1.0, raw / 0.15))
|
|
let pace: CGFloat = self.arrowTiming?.reduced == true ? 0.0 : self.arrowPace
|
|
let loop = phase < 0.5 ? phase * 2.0 : phase * 2.0 - 2.0
|
|
let smear = 8.0 * pace
|
|
let multiple = smear > 1.0
|
|
let weights: [CGFloat] = [0.45, 0.8625, 1.0, 0.8625, 0.45]
|
|
for (i, arrow) in self.arrows.enumerated() {
|
|
let sample = i % 5
|
|
let direction: CGFloat = i < 5 ? -1.0 : 1.0
|
|
let k: CGFloat = multiple ? CGFloat(sample) / 4.0 - 0.5 : 0.0
|
|
arrow.transform = .identity
|
|
arrow.frame = CGRect(x: self.currentWidth - 26.0, y: 4.0, width: 18.0, height: 18.0)
|
|
let travel = self.arrowTiming?.reduced == true ? 0.0 : direction * (22.0 * loop + k * smear)
|
|
arrow.transform = CGAffineTransform(translationX: 0.0, y: travel).scaledBy(x: 1.0, y: 1.0 + 0.55 * pace)
|
|
arrow.alpha = multiple ? weights[sample] / 3.625 : (sample == 0 ? 1.0 : 0.0)
|
|
}
|
|
if !self.isAnimating {
|
|
self.displayLink?.invalidate()
|
|
self.displayLink = nil
|
|
}
|
|
}
|
|
|
|
private func finishMotion() {
|
|
(self.title.view as? WalletSendAnimatedTextComponent.View)?.finishMotion()
|
|
self.geometryTiming = nil
|
|
self.arrowTiming = nil
|
|
self.displayLink?.invalidate()
|
|
self.displayLink = nil
|
|
self.renderFrame()
|
|
}
|
|
}
|