Swiftgram/submodules/WebUI/Sources/FullscreenControlsComponent.swift
Ilya Laktyushin 0512739b8c Various fixes
2024-11-24 16:38:03 +04:00

366 lines
17 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import BundleIconComponent
import MultilineTextComponent
import MoreButtonNode
import AccountContext
import TelegramPresentationData
import LottieAnimationComponent
final class FullscreenControlsComponent: Component {
let context: AccountContext
let strings: PresentationStrings
let title: String
let isVerified: Bool
let insets: UIEdgeInsets
let statusBarStyle: StatusBarStyle
var hasBack: Bool
let backPressed: () -> Void
let minimizePressed: () -> Void
let morePressed: (ASDisplayNode, ContextGesture?) -> Void
init(
context: AccountContext,
strings: PresentationStrings,
title: String,
isVerified: Bool,
insets: UIEdgeInsets,
statusBarStyle: StatusBarStyle,
hasBack: Bool,
backPressed: @escaping () -> Void,
minimizePressed: @escaping () -> Void,
morePressed: @escaping (ASDisplayNode, ContextGesture?) -> Void
) {
self.context = context
self.strings = strings
self.title = title
self.isVerified = isVerified
self.insets = insets
self.statusBarStyle = statusBarStyle
self.hasBack = hasBack
self.backPressed = backPressed
self.minimizePressed = minimizePressed
self.morePressed = morePressed
}
static func ==(lhs: FullscreenControlsComponent, rhs: FullscreenControlsComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.isVerified != rhs.isVerified {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.statusBarStyle != rhs.statusBarStyle {
return false
}
if lhs.hasBack != rhs.hasBack {
return false
}
return true
}
final class View: UIView {
private let leftBackgroundView: BlurredBackgroundView
private let rightBackgroundView: BlurredBackgroundView
private let closeIcon = ComponentView<Empty>()
private let leftButton = HighlightTrackingButton()
private let titleClippingView = UIView()
private let title = ComponentView<Empty>()
private let credibility = ComponentView<Empty>()
private let buttonTitle = ComponentView<Empty>()
private let minimizeButton = ComponentView<Empty>()
private let moreNode = MoreButtonNode(theme: defaultPresentationTheme, size: CGSize(width: 36.0, height: 36.0), encircled: false)
private var displayTitle = true
private var timer: Timer?
private var component: FullscreenControlsComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.leftBackgroundView = BlurredBackgroundView(color: nil)
self.rightBackgroundView = BlurredBackgroundView(color: nil)
super.init(frame: frame)
self.titleClippingView.clipsToBounds = true
self.titleClippingView.isUserInteractionEnabled = false
self.leftBackgroundView.clipsToBounds = true
self.addSubview(self.leftBackgroundView)
self.addSubview(self.leftButton)
self.addSubview(self.titleClippingView)
self.rightBackgroundView.clipsToBounds = true
self.addSubview(self.rightBackgroundView)
self.addSubview(self.moreNode.view)
self.moreNode.updateColor(.white, transition: .immediate)
self.leftButton.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
if let view = self.closeIcon.view {
view.layer.removeAnimation(forKey: "opacity")
view.alpha = 0.6
}
if let view = self.buttonTitle.view {
view.layer.removeAnimation(forKey: "opacity")
view.alpha = 0.6
}
} else {
if let view = self.closeIcon.view {
view.alpha = 1.0
view.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
}
if let view = self.buttonTitle.view {
view.alpha = 1.0
view.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
}
}
}
self.leftButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.timer?.invalidate()
}
@objc private func closePressed() {
guard let component = self.component else {
return
}
component.backPressed()
}
@objc private func timerEvent() {
self.timer?.invalidate()
self.timer = nil
self.displayTitle = false
self.state?.updated(transition: .spring(duration: 0.3))
}
func update(component: FullscreenControlsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let isFirstTime = self.component == nil
let previousComponent = self.component
self.component = component
self.state = state
let sideInset: CGFloat = 16.0
let leftBackgroundSize = CGSize(width: 30.0, height: 30.0)
let rightBackgroundSize = CGSize(width: 72.0, height: 30.0)
let backgroundColor: UIColor = component.statusBarStyle == .Black ? UIColor(white: 0.7, alpha: 0.35) : UIColor(white: 0.45, alpha: 0.25)
let textColor: UIColor = component.statusBarStyle == .Black ? UIColor(rgb: 0x808080) : .white
self.leftBackgroundView.updateColor(color: backgroundColor, transition: transition.containedViewLayoutTransition)
self.rightBackgroundView.updateColor(color: backgroundColor, transition: transition.containedViewLayoutTransition)
let rightBackgroundFrame = CGRect(origin: CGPoint(x: availableSize.width - component.insets.right - sideInset - rightBackgroundSize.width, y: 0.0), size: rightBackgroundSize)
self.rightBackgroundView.update(size: rightBackgroundSize, cornerRadius: rightBackgroundFrame.height / 2.0, transition: transition.containedViewLayoutTransition)
transition.setFrame(view: self.rightBackgroundView, frame: rightBackgroundFrame)
var isAnimatingTextTransition = false
self.moreNode.updateColor(textColor, transition: .immediate)
var additionalLeftWidth: CGFloat = 0.0
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.title, font: Font.with(size: 13.0, design: .round, weight: .semibold), textColor: textColor)))),
environment: {},
containerSize: availableSize
)
let titleFrame = CGRect(origin: CGPoint(x: self.displayTitle ? 3.0 : -titleSize.width - 15.0, y: floorToScreenPixels((leftBackgroundSize.height - titleSize.height) / 2.0)), size: titleSize)
if let view = self.title.view {
if view.superview == nil {
self.titleClippingView.addSubview(view)
}
if !view.alpha.isZero && !self.displayTitle {
isAnimatingTextTransition = true
}
transition.setFrame(view: view, frame: titleFrame)
transition.setAlpha(view: view, alpha: self.displayTitle ? 1.0 : 0.0)
}
let buttonTitleUpdated = (previousComponent?.hasBack ?? false) != component.hasBack
let animationMultiplier = !component.hasBack ? -1.0 : 1.0
if buttonTitleUpdated && !self.displayTitle {
isAnimatingTextTransition = true
if let view = self.buttonTitle.view, let snapshotView = view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = view.frame
self.titleClippingView.addSubview(snapshotView)
snapshotView.layer.animatePosition(from: .zero, to: CGPoint(x: -(snapshotView.frame.width * 1.5) * animationMultiplier, y: 0.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
snapshotView.removeFromSuperview()
})
}
}
let buttonTitleSize = self.buttonTitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: component.hasBack ? component.strings.Common_Back : component.strings.Common_Close, font: Font.with(size: 13.0, design: .round, weight: .semibold), textColor: textColor)))),
environment: {},
containerSize: availableSize
)
if self.displayTitle {
additionalLeftWidth += titleSize.width + 10.0
} else {
additionalLeftWidth += buttonTitleSize.width + 10.0
}
let buttonTitleFrame = CGRect(origin: CGPoint(x: self.displayTitle ? leftBackgroundSize.width + additionalLeftWidth + 3.0 : 3.0, y: floorToScreenPixels((leftBackgroundSize.height - buttonTitleSize.height) / 2.0)), size: buttonTitleSize)
if let view = self.buttonTitle.view {
if view.superview == nil {
self.titleClippingView.addSubview(view)
}
transition.setFrame(view: view, frame: buttonTitleFrame)
if buttonTitleUpdated {
view.layer.animatePosition(from: CGPoint(x: (view.frame.width * 1.5) * animationMultiplier, y: 0.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
if component.isVerified {
let credibilitySize = self.credibility.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(name: "Instant View/Verified", tintColor: textColor)),
environment: {},
containerSize: availableSize
)
if let view = self.credibility.view {
if view.superview == nil {
view.alpha = 0.6
self.titleClippingView.addSubview(view)
}
let credibilityFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 2.0, y: floorToScreenPixels((leftBackgroundSize.height - credibilitySize.height) / 2.0)), size: credibilitySize)
transition.setFrame(view: view, frame: credibilityFrame)
}
if self.displayTitle {
additionalLeftWidth += credibilitySize.width + 2.0
}
}
var leftBackgroundTransition = transition
if buttonTitleUpdated {
leftBackgroundTransition = .spring(duration: 0.3)
}
let leftBackgroundFrame = CGRect(origin: CGPoint(x: sideInset + component.insets.left, y: 0.0), size: CGSize(width: leftBackgroundSize.width + additionalLeftWidth, height: leftBackgroundSize.height))
self.leftBackgroundView.update(size: leftBackgroundFrame.size, cornerRadius: leftBackgroundSize.height / 2.0, transition: leftBackgroundTransition.containedViewLayoutTransition)
leftBackgroundTransition.setFrame(view: self.leftBackgroundView, frame: leftBackgroundFrame)
self.leftButton.frame = leftBackgroundFrame
if isAnimatingTextTransition, self.titleClippingView.mask == nil {
if let maskImage = generateGradientImage(size: CGSize(width: 42.0, height: 10.0), colors: [UIColor.clear, UIColor.black, UIColor.black, UIColor.clear], locations: [0.0, 0.1, 0.9, 1.0], direction: .horizontal) {
let maskView = UIImageView(image: maskImage.stretchableImage(withLeftCapWidth: 4, topCapHeight: 0))
self.titleClippingView.mask = maskView
maskView.frame = CGRect(origin: .zero, size: CGSize(width: self.titleClippingView.bounds.width, height: self.titleClippingView.bounds.height))
}
}
transition.setFrame(view: self.titleClippingView, frame: CGRect(origin: CGPoint(x: sideInset + component.insets.left + leftBackgroundSize.height - 3.0, y: 0.0), size: CGSize(width: leftBackgroundFrame.width - leftBackgroundSize.height, height: leftBackgroundSize.height)))
if let maskView = self.titleClippingView.mask {
leftBackgroundTransition.setFrame(view: maskView, frame: CGRect(origin: .zero, size: CGSize(width: self.titleClippingView.bounds.width, height: self.titleClippingView.bounds.height)), completion: { _ in
self.titleClippingView.mask = nil
})
}
let backButtonSize = self.closeIcon.update(
transition: .immediate,
component: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "web_backToCancel",
mode: .animating(loop: false),
range: component.hasBack ? (0.5, 1.0) : (0.0, 0.5)
),
colors: ["__allcolors__": textColor],
size: CGSize(width: 30.0, height: 30.0)
)
),
environment: {},
containerSize: CGSize(width: 30.0, height: 30.0)
)
if let view = self.closeIcon.view {
if view.superview == nil {
view.isUserInteractionEnabled = false
self.addSubview(view)
}
let buttonFrame = CGRect(origin: CGPoint(x: leftBackgroundFrame.minX, y: 0.0), size: backButtonSize)
transition.setFrame(view: view, frame: buttonFrame)
}
let minimizeButtonSize = self.minimizeButton.update(
transition: .immediate,
component: AnyComponent(Button(
content: AnyComponent(
BundleIconComponent(name: "Instant View/MinimizeArrow", tintColor: textColor)
),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.minimizePressed()
}
).minSize(CGSize(width: 30.0, height: 30.0))),
environment: {},
containerSize: CGSize(width: 30.0, height: 30.0)
)
if let view = self.minimizeButton.view {
if view.superview == nil {
self.addSubview(view)
}
let buttonFrame = CGRect(origin: CGPoint(x: rightBackgroundFrame.minX + 2.0, y: 0.0), size: minimizeButtonSize)
transition.setFrame(view: view, frame: buttonFrame)
}
transition.setFrame(view: self.moreNode.view, frame: CGRect(origin: CGPoint(x: rightBackgroundFrame.maxX - 42.0, y: -4.0), size: CGSize(width: 36.0, height: 36.0)))
self.moreNode.action = { [weak self] node, gesture in
guard let self, let component = self.component else {
return
}
component.morePressed(node, gesture)
}
if isFirstTime {
let timer = Timer(timeInterval: 2.5, target: self, selector: #selector(self.timerEvent), userInfo: nil, repeats: false)
self.timer = timer
RunLoop.main.add(timer, forMode: .common)
}
return CGSize(width: availableSize.width, height: leftBackgroundSize.height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}