mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
553 lines
22 KiB
Swift
553 lines
22 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import ComponentFlow
|
|
import AnimatedTextComponent
|
|
import ActivityIndicator
|
|
import BundleIconComponent
|
|
|
|
public final class ButtonBadgeComponent: Component {
|
|
let fillColor: UIColor
|
|
let style: ButtonTextContentComponent.BadgeStyle
|
|
let content: AnyComponent<Empty>
|
|
|
|
public init(
|
|
fillColor: UIColor,
|
|
style: ButtonTextContentComponent.BadgeStyle,
|
|
content: AnyComponent<Empty>
|
|
) {
|
|
self.fillColor = fillColor
|
|
self.style = style
|
|
self.content = content
|
|
}
|
|
|
|
public static func ==(lhs: ButtonBadgeComponent, rhs: ButtonBadgeComponent) -> Bool {
|
|
if lhs.fillColor != rhs.fillColor {
|
|
return false
|
|
}
|
|
if lhs.style != rhs.style {
|
|
return false
|
|
}
|
|
if lhs.content != rhs.content {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public final class View: UIView {
|
|
private let backgroundView: UIImageView
|
|
private let content = ComponentView<Empty>()
|
|
|
|
private var component: ButtonBadgeComponent?
|
|
|
|
override public init(frame: CGRect) {
|
|
self.backgroundView = UIImageView()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubview(self.backgroundView)
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public func update(component: ButtonBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
let height: CGFloat
|
|
switch component.style {
|
|
case .round:
|
|
height = 20.0
|
|
case .roundedRectangle:
|
|
height = 18.0
|
|
}
|
|
let contentInset: CGFloat = 10.0
|
|
|
|
let themeUpdated = self.component?.fillColor != component.fillColor
|
|
self.component = component
|
|
|
|
let contentSize = self.content.update(
|
|
transition: transition,
|
|
component: component.content,
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
let backgroundWidth: CGFloat = max(height, contentSize.width + contentInset)
|
|
let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundWidth, height: height))
|
|
|
|
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
|
|
|
if let contentView = self.content.view {
|
|
if contentView.superview == nil {
|
|
self.addSubview(contentView)
|
|
}
|
|
transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundFrame.width - contentSize.width) * 0.5), y: floorToScreenPixels((backgroundFrame.height - contentSize.height) * 0.5)), size: contentSize))
|
|
}
|
|
|
|
if themeUpdated || backgroundFrame.height != self.backgroundView.image?.size.height {
|
|
switch component.style {
|
|
case .round:
|
|
self.backgroundView.image = generateStretchableFilledCircleImage(diameter: backgroundFrame.height, color: component.fillColor)
|
|
case .roundedRectangle:
|
|
self.backgroundView.image = generateFilledRoundedRectImage(size: CGSize(width: height, height: height), cornerRadius: 4.0, color: component.fillColor)?.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0))
|
|
}
|
|
}
|
|
|
|
return backgroundFrame.size
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public 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)
|
|
}
|
|
}
|
|
|
|
public final class ButtonTextContentComponent: Component {
|
|
public enum BadgeStyle {
|
|
case round
|
|
case roundedRectangle
|
|
}
|
|
|
|
public let text: String
|
|
public let badge: Int
|
|
public let textColor: UIColor
|
|
public let badgeBackground: UIColor
|
|
public let badgeForeground: UIColor
|
|
public let badgeStyle: BadgeStyle
|
|
public let badgeIconName: String?
|
|
public let combinedAlignment: Bool
|
|
|
|
public init(
|
|
text: String,
|
|
badge: Int,
|
|
textColor: UIColor,
|
|
badgeBackground: UIColor,
|
|
badgeForeground: UIColor,
|
|
badgeStyle: BadgeStyle = .round,
|
|
badgeIconName: String? = nil,
|
|
combinedAlignment: Bool = false
|
|
) {
|
|
self.text = text
|
|
self.badge = badge
|
|
self.textColor = textColor
|
|
self.badgeBackground = badgeBackground
|
|
self.badgeForeground = badgeForeground
|
|
self.badgeStyle = badgeStyle
|
|
self.badgeIconName = badgeIconName
|
|
self.combinedAlignment = combinedAlignment
|
|
}
|
|
|
|
public static func ==(lhs: ButtonTextContentComponent, rhs: ButtonTextContentComponent) -> Bool {
|
|
if lhs.text != rhs.text {
|
|
return false
|
|
}
|
|
if lhs.badge != rhs.badge {
|
|
return false
|
|
}
|
|
if lhs.textColor != rhs.textColor {
|
|
return false
|
|
}
|
|
if lhs.badgeBackground != rhs.badgeBackground {
|
|
return false
|
|
}
|
|
if lhs.badgeForeground != rhs.badgeForeground {
|
|
return false
|
|
}
|
|
if lhs.badgeStyle != rhs.badgeStyle {
|
|
return false
|
|
}
|
|
if lhs.badgeIconName != rhs.badgeIconName {
|
|
return false
|
|
}
|
|
if lhs.combinedAlignment != rhs.combinedAlignment {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public final class View: UIView {
|
|
private var component: ButtonTextContentComponent?
|
|
private weak var componentState: EmptyComponentState?
|
|
|
|
private let content = ComponentView<Empty>()
|
|
private var badge: ComponentView<Empty>?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
|
|
func update(component: ButtonTextContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
let previousBadge = self.component?.badge
|
|
|
|
self.component = component
|
|
self.componentState = state
|
|
|
|
var badgeSpacing: CGFloat = 6.0
|
|
if component.badgeIconName != nil {
|
|
badgeSpacing += 4.0
|
|
}
|
|
|
|
let contentSize = self.content.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(Text(
|
|
text: component.text,
|
|
font: Font.semibold(17.0),
|
|
color: component.textColor
|
|
)),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
|
|
var badgeSize: CGSize?
|
|
if component.badge > 0 {
|
|
var badgeTransition = transition
|
|
let badge: ComponentView<Empty>
|
|
if let current = self.badge {
|
|
badge = current
|
|
} else {
|
|
badgeTransition = .immediate
|
|
badge = ComponentView()
|
|
self.badge = badge
|
|
}
|
|
|
|
var badgeContent: [AnyComponentWithIdentity<Empty>] = []
|
|
if let badgeIconName = component.badgeIconName {
|
|
badgeContent.append(AnyComponentWithIdentity(
|
|
id: "icon",
|
|
component: AnyComponent(BundleIconComponent(
|
|
name: badgeIconName,
|
|
tintColor: component.badgeForeground
|
|
)))
|
|
)
|
|
}
|
|
badgeContent.append(AnyComponentWithIdentity(
|
|
id: "text",
|
|
component: AnyComponent(AnimatedTextComponent(
|
|
font: Font.with(size: 15.0, design: .round, weight: .semibold, traits: .monospacedNumbers),
|
|
color: component.badgeForeground,
|
|
items: [
|
|
AnimatedTextComponent.Item(id: AnyHashable(0), content: .number(component.badge, minDigits: 0))
|
|
]
|
|
)))
|
|
)
|
|
|
|
badgeSize = badge.update(
|
|
transition: badgeTransition,
|
|
component: AnyComponent(ButtonBadgeComponent(
|
|
fillColor: component.badgeBackground,
|
|
style: component.badgeStyle,
|
|
content: AnyComponent(HStack(badgeContent, spacing: 2.0))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 100.0, height: 100.0)
|
|
)
|
|
}
|
|
|
|
var size = contentSize
|
|
var measurementSize = size
|
|
if let badgeSize {
|
|
if component.combinedAlignment {
|
|
measurementSize.width += badgeSpacing
|
|
measurementSize.width += badgeSize.width
|
|
}
|
|
size.height = max(size.height, badgeSize.height)
|
|
}
|
|
|
|
let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - measurementSize.width) * 0.5), y: floorToScreenPixels((size.height - measurementSize.height) * 0.5)), size: measurementSize)
|
|
|
|
if let contentView = self.content.view {
|
|
if contentView.superview == nil {
|
|
self.addSubview(contentView)
|
|
}
|
|
transition.setFrame(view: contentView, frame: CGRect(origin: contentFrame.origin, size: contentSize))
|
|
}
|
|
|
|
if let badgeSize, let badge = self.badge {
|
|
let badgeFrame = CGRect(origin: CGPoint(x: contentFrame.minX + contentSize.width + badgeSpacing, y: floorToScreenPixels((size.height - badgeSize.height) * 0.5) + 1.0), size: badgeSize)
|
|
|
|
if let badgeView = badge.view {
|
|
var animateIn = false
|
|
if badgeView.superview == nil {
|
|
animateIn = true
|
|
self.addSubview(badgeView)
|
|
}
|
|
|
|
if animateIn {
|
|
badgeView.frame = badgeFrame
|
|
} else {
|
|
transition.setFrame(view: badgeView, frame: badgeFrame)
|
|
|
|
if !transition.animation.isImmediate, let previousBadge, previousBadge != component.badge {
|
|
let middleScale: CGFloat = previousBadge < component.badge ? 1.1 : 0.9
|
|
let values: [NSNumber] = [1.0, middleScale as NSNumber, 1.0]
|
|
badgeView.layer.animateKeyframes(values: values, duration: 0.25, keyPath: "transform.scale")
|
|
}
|
|
}
|
|
|
|
if animateIn, !transition.animation.isImmediate {
|
|
badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
|
badgeView.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
|
}
|
|
}
|
|
} else {
|
|
if let badge = self.badge {
|
|
self.badge = nil
|
|
if let badgeView = badge.view {
|
|
if !transition.animation.isImmediate {
|
|
badgeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak badgeView] _ in
|
|
badgeView?.removeFromSuperview()
|
|
})
|
|
badgeView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.25, removeOnCompletion: false)
|
|
} else {
|
|
badgeView.removeFromSuperview()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public 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)
|
|
}
|
|
}
|
|
|
|
public final class ButtonComponent: Component {
|
|
public struct Background: Equatable {
|
|
public var color: UIColor
|
|
public var foreground: UIColor
|
|
public var pressedColor: UIColor
|
|
public var cornerRadius: CGFloat
|
|
|
|
public init(
|
|
color: UIColor,
|
|
foreground: UIColor,
|
|
pressedColor: UIColor,
|
|
cornerRadius: CGFloat = 10.0
|
|
) {
|
|
self.color = color
|
|
self.foreground = foreground
|
|
self.pressedColor = pressedColor
|
|
self.cornerRadius = cornerRadius
|
|
}
|
|
}
|
|
|
|
public let background: Background
|
|
public let content: AnyComponentWithIdentity<Empty>
|
|
public let isEnabled: Bool
|
|
public let tintWhenDisabled: Bool
|
|
public let allowActionWhenDisabled: Bool
|
|
public let displaysProgress: Bool
|
|
public let action: () -> Void
|
|
|
|
public init(
|
|
background: Background,
|
|
content: AnyComponentWithIdentity<Empty>,
|
|
isEnabled: Bool,
|
|
tintWhenDisabled: Bool = true,
|
|
allowActionWhenDisabled: Bool = false,
|
|
displaysProgress: Bool,
|
|
action: @escaping () -> Void
|
|
) {
|
|
self.background = background
|
|
self.content = content
|
|
self.isEnabled = isEnabled
|
|
self.tintWhenDisabled = tintWhenDisabled
|
|
self.allowActionWhenDisabled = allowActionWhenDisabled
|
|
self.displaysProgress = displaysProgress
|
|
self.action = action
|
|
}
|
|
|
|
public static func ==(lhs: ButtonComponent, rhs: ButtonComponent) -> Bool {
|
|
if lhs.background != rhs.background {
|
|
return false
|
|
}
|
|
if lhs.content != rhs.content {
|
|
return false
|
|
}
|
|
if lhs.isEnabled != rhs.isEnabled {
|
|
return false
|
|
}
|
|
if lhs.tintWhenDisabled != rhs.tintWhenDisabled {
|
|
return false
|
|
}
|
|
if lhs.allowActionWhenDisabled != rhs.allowActionWhenDisabled {
|
|
return false
|
|
}
|
|
if lhs.displaysProgress != rhs.displaysProgress {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private final class ContentItem {
|
|
let id: AnyHashable
|
|
let view = ComponentView<Empty>()
|
|
|
|
init(id: AnyHashable) {
|
|
self.id = id
|
|
}
|
|
}
|
|
|
|
public final class View: HighlightTrackingButton {
|
|
private var component: ButtonComponent?
|
|
private weak var componentState: EmptyComponentState?
|
|
|
|
private var contentItem: ContentItem?
|
|
|
|
private var activityIndicator: ActivityIndicator?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
|
|
|
self.highligthedChanged = { [weak self] highlighted in
|
|
if let self, let component = self.component, component.isEnabled {
|
|
if highlighted {
|
|
self.layer.removeAnimation(forKey: "opacity")
|
|
self.alpha = 0.7
|
|
} else {
|
|
self.alpha = 1.0
|
|
self.layer.animateAlpha(from: 7, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
preconditionFailure()
|
|
}
|
|
|
|
@objc private func pressed() {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
component.action()
|
|
}
|
|
|
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
|
|
func update(component: ButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
self.componentState = state
|
|
|
|
self.isEnabled = (component.isEnabled || component.allowActionWhenDisabled) && !component.displaysProgress
|
|
|
|
transition.setBackgroundColor(view: self, color: component.background.color)
|
|
transition.setCornerRadius(layer: self.layer, cornerRadius: component.background.cornerRadius)
|
|
|
|
var contentAlpha: CGFloat = 1.0
|
|
if component.displaysProgress {
|
|
contentAlpha = 0.0
|
|
} else if !component.isEnabled && component.tintWhenDisabled {
|
|
contentAlpha = 0.7
|
|
}
|
|
|
|
var previousContentItem: ContentItem?
|
|
let contentItem: ContentItem
|
|
var contentItemTransition = transition
|
|
if let current = self.contentItem, current.id == component.content.id {
|
|
contentItem = current
|
|
} else {
|
|
contentItemTransition = .immediate
|
|
previousContentItem = self.contentItem
|
|
contentItem = ContentItem(id: component.content.id)
|
|
self.contentItem = contentItem
|
|
}
|
|
|
|
let contentSize = contentItem.view.update(
|
|
transition: contentItemTransition,
|
|
component: component.content.component,
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
if let contentView = contentItem.view.view {
|
|
var animateIn = false
|
|
var contentTransition = transition
|
|
if contentView.superview == nil {
|
|
contentTransition = .immediate
|
|
animateIn = true
|
|
contentView.isUserInteractionEnabled = false
|
|
self.addSubview(contentView)
|
|
}
|
|
let contentFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - contentSize.height) * 0.5)), size: contentSize)
|
|
|
|
contentTransition.setFrame(view: contentView, frame: contentFrame)
|
|
contentTransition.setAlpha(view: contentView, alpha: contentAlpha)
|
|
|
|
if animateIn && previousContentItem != nil && !transition.animation.isImmediate {
|
|
contentView.layer.animateScale(from: 0.4, to: 1.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring)
|
|
contentView.layer.animateAlpha(from: 0.0, to: contentAlpha, duration: 0.1)
|
|
contentView.layer.animatePosition(from: CGPoint(x: 0.0, y: -availableSize.height * 0.15), to: CGPoint(), duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
}
|
|
}
|
|
|
|
if let previousContentItem, let previousContentView = previousContentItem.view.view {
|
|
if !transition.animation.isImmediate {
|
|
previousContentView.layer.animateScale(from: 1.0, to: 0.0, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
|
previousContentView.layer.animateAlpha(from: contentAlpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousContentView] _ in
|
|
previousContentView?.removeFromSuperview()
|
|
})
|
|
previousContentView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: availableSize.height * 0.35), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
|
|
} else {
|
|
previousContentView.removeFromSuperview()
|
|
}
|
|
}
|
|
|
|
if component.displaysProgress {
|
|
let activityIndicator: ActivityIndicator
|
|
var activityIndicatorTransition = transition
|
|
if let current = self.activityIndicator {
|
|
activityIndicator = current
|
|
} else {
|
|
activityIndicatorTransition = .immediate
|
|
activityIndicator = ActivityIndicator(type: .custom(component.background.foreground, 22.0, 2.0, true))
|
|
activityIndicator.view.alpha = 0.0
|
|
self.activityIndicator = activityIndicator
|
|
self.addSubview(activityIndicator.view)
|
|
}
|
|
let indicatorSize = CGSize(width: 22.0, height: 22.0)
|
|
transition.setAlpha(view: activityIndicator.view, alpha: 1.0)
|
|
activityIndicatorTransition.setFrame(view: activityIndicator.view, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - indicatorSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - indicatorSize.height) / 2.0)), size: indicatorSize))
|
|
} else {
|
|
if let activityIndicator = self.activityIndicator {
|
|
self.activityIndicator = nil
|
|
transition.setAlpha(view: activityIndicator.view, alpha: 0.0, completion: { [weak activityIndicator] _ in
|
|
activityIndicator?.view.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public 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)
|
|
}
|
|
}
|