mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
366 lines
17 KiB
Swift
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)
|
|
}
|
|
}
|