mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
339 lines
15 KiB
Swift
339 lines
15 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import ComponentFlow
|
|
import AppBundle
|
|
import BundleIconComponent
|
|
|
|
public final class NavigationSearchComponent: Component {
|
|
public struct Colors: Equatable {
|
|
public var background: UIColor
|
|
public var inactiveForeground: UIColor
|
|
public var foreground: UIColor
|
|
public var button: UIColor
|
|
|
|
public init(
|
|
background: UIColor,
|
|
inactiveForeground: UIColor,
|
|
foreground: UIColor,
|
|
button: UIColor
|
|
) {
|
|
self.background = background
|
|
self.inactiveForeground = inactiveForeground
|
|
self.foreground = foreground
|
|
self.button = button
|
|
}
|
|
}
|
|
|
|
public let colors: Colors
|
|
public let cancel: String
|
|
public let placeholder: String
|
|
public let isSearchActive: Bool
|
|
public let collapseFraction: CGFloat
|
|
public let activateSearch: () -> Void
|
|
public let deactivateSearch: () -> Void
|
|
public let updateQuery: (String) -> Void
|
|
|
|
public init(
|
|
colors: Colors,
|
|
cancel: String,
|
|
placeholder: String,
|
|
isSearchActive: Bool,
|
|
collapseFraction: CGFloat,
|
|
activateSearch: @escaping () -> Void,
|
|
deactivateSearch: @escaping () -> Void,
|
|
updateQuery: @escaping (String) -> Void
|
|
) {
|
|
self.colors = colors
|
|
self.cancel = cancel
|
|
self.placeholder = placeholder
|
|
self.isSearchActive = isSearchActive
|
|
self.collapseFraction = collapseFraction
|
|
self.activateSearch = activateSearch
|
|
self.deactivateSearch = deactivateSearch
|
|
self.updateQuery = updateQuery
|
|
}
|
|
|
|
public static func ==(lhs: NavigationSearchComponent, rhs: NavigationSearchComponent) -> Bool {
|
|
if lhs.colors != rhs.colors {
|
|
return false
|
|
}
|
|
if lhs.cancel != rhs.cancel {
|
|
return false
|
|
}
|
|
if lhs.placeholder != rhs.placeholder {
|
|
return false
|
|
}
|
|
if lhs.isSearchActive != rhs.isSearchActive {
|
|
return false
|
|
}
|
|
if lhs.collapseFraction != rhs.collapseFraction {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public final class View: UIView, UITextFieldDelegate {
|
|
private var component: NavigationSearchComponent?
|
|
private weak var state: EmptyComponentState?
|
|
|
|
private let backgroundView: UIView
|
|
private let searchIconView: UIImageView
|
|
private let placeholderText = ComponentView<Empty>()
|
|
|
|
private let clearButton: HighlightableButton
|
|
private let clearIconView: UIImageView
|
|
|
|
private var button: ComponentView<Empty>?
|
|
|
|
private var textField: UITextField?
|
|
|
|
override init(frame: CGRect) {
|
|
self.backgroundView = UIView()
|
|
|
|
self.searchIconView = UIImageView(image: UIImage(bundleImageName: "Components/Search Bar/Loupe")?.withRenderingMode(.alwaysTemplate))
|
|
|
|
self.clearButton = HighlightableButton()
|
|
self.clearIconView = UIImageView(image: UIImage(bundleImageName: "Components/Search Bar/Clear")?.withRenderingMode(.alwaysTemplate))
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubview(self.backgroundView)
|
|
self.addSubview(self.searchIconView)
|
|
|
|
self.addSubview(self.clearButton)
|
|
self.clearButton.addSubview(self.clearIconView)
|
|
self.clearButton.isHidden = true
|
|
|
|
self.backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.backgroundTapGesture(_:))))
|
|
|
|
self.clearButton.addTarget(self, action: #selector(self.clearPressed), for: .touchUpInside)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
}
|
|
|
|
@objc private func backgroundTapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
if self.textField == nil {
|
|
let textField = UITextField()
|
|
self.textField = textField
|
|
textField.delegate = self
|
|
textField.addTarget(self, action: #selector(self.textChanged), for: .editingChanged)
|
|
self.addSubview(textField)
|
|
textField.keyboardAppearance = .dark
|
|
textField.returnKeyType = .done
|
|
}
|
|
|
|
self.textField?.becomeFirstResponder()
|
|
}
|
|
}
|
|
|
|
public func textFieldDidBeginEditing(_ textField: UITextField) {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
if !component.isSearchActive {
|
|
component.activateSearch()
|
|
}
|
|
}
|
|
|
|
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
textField.resignFirstResponder()
|
|
return false
|
|
}
|
|
|
|
@objc private func textChanged() {
|
|
self.updateText(updateComponent: true)
|
|
}
|
|
|
|
@objc private func clearPressed() {
|
|
self.textField?.text = ""
|
|
self.updateText(updateComponent: true)
|
|
}
|
|
|
|
@objc private func updateText(updateComponent: Bool) {
|
|
let isEmpty = self.textField?.text?.isEmpty ?? true
|
|
self.placeholderText.view?.isHidden = !isEmpty
|
|
|
|
self.clearButton.isHidden = isEmpty
|
|
|
|
if updateComponent, let component = self.component {
|
|
component.updateQuery(self.textField?.text ?? "")
|
|
}
|
|
}
|
|
|
|
func update(component: NavigationSearchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
|
let previousComponent = self.component
|
|
|
|
self.component = component
|
|
self.state = state
|
|
|
|
let baseHeight: CGFloat = 52.0
|
|
let size = CGSize(width: availableSize.width, height: baseHeight)
|
|
|
|
let sideInset: CGFloat = 16.0
|
|
let fieldHeight: CGFloat = 36.0
|
|
let fieldSideInset: CGFloat = 8.0
|
|
let searchIconSpacing: CGFloat = 4.0
|
|
let buttonSpacing: CGFloat = 8.0
|
|
|
|
let contentAlphaDistance: CGFloat = 0.35
|
|
let contentAlpha: CGFloat = 1.0 - max(0.0, min(1.0, component.collapseFraction / contentAlphaDistance))
|
|
|
|
let rightInset: CGFloat
|
|
if component.isSearchActive {
|
|
var buttonTransition = transition
|
|
let button: ComponentView<Empty>
|
|
if let current = self.button {
|
|
button = current
|
|
} else {
|
|
buttonTransition = buttonTransition.withAnimation(.none)
|
|
button = ComponentView()
|
|
self.button = button
|
|
}
|
|
|
|
let buttonSize = button.update(
|
|
transition: buttonTransition,
|
|
component: AnyComponent(Button(
|
|
content: AnyComponent(Text(text: component.cancel, font: Font.regular(17.0), color: component.colors.button)),
|
|
action: { [weak self] in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
component.deactivateSearch()
|
|
}
|
|
).minSize(CGSize(width: 8.0, height: baseHeight))),
|
|
environment: {},
|
|
containerSize: CGSize(width: 200.0, height: 100.0)
|
|
)
|
|
let buttonFrame = CGRect(origin: CGPoint(x: size.width - sideInset - buttonSize.width, y: floor((size.height - buttonSize.height) * 0.5)), size: buttonSize)
|
|
if let buttonView = button.view {
|
|
var animateIn = false
|
|
if buttonView.superview == nil {
|
|
animateIn = true
|
|
self.addSubview(buttonView)
|
|
}
|
|
buttonTransition.setFrame(view: buttonView, frame: buttonFrame)
|
|
if animateIn {
|
|
transition.animatePosition(view: buttonView, from: CGPoint(x: size.width - buttonFrame.minX, y: 0.0), to: CGPoint(), additive: true)
|
|
}
|
|
}
|
|
|
|
rightInset = sideInset + buttonSize.width + buttonSpacing
|
|
} else {
|
|
if let button = self.button {
|
|
self.button = nil
|
|
|
|
if let buttonView = button.view {
|
|
transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: size.width, y: buttonView.frame.minY), size: buttonView.bounds.size))
|
|
}
|
|
}
|
|
|
|
rightInset = sideInset
|
|
}
|
|
|
|
let backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - fieldHeight) * 0.5)), size: CGSize(width: availableSize.width - sideInset - rightInset, height: fieldHeight * (1.0 - component.collapseFraction)))
|
|
|
|
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
|
transition.setCornerRadius(layer: self.backgroundView.layer, cornerRadius: min(10.0, backgroundFrame.height * 0.5))
|
|
if previousComponent?.colors.background != component.colors.background {
|
|
self.backgroundView.backgroundColor = component.colors.background
|
|
}
|
|
if previousComponent?.colors.inactiveForeground != component.colors.inactiveForeground {
|
|
self.searchIconView.tintColor = component.colors.inactiveForeground
|
|
self.clearIconView.tintColor = component.colors.inactiveForeground
|
|
}
|
|
|
|
let placeholderSize = self.placeholderText.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(Text(text: component.placeholder, font: Font.regular(17.0), color: component.colors.inactiveForeground)),
|
|
environment: {},
|
|
containerSize: CGSize(width: backgroundFrame.width - fieldSideInset * 2.0, height: backgroundFrame.height)
|
|
)
|
|
|
|
let searchIconSize = self.searchIconView.image?.size ?? CGSize(width: 20.0, height: 20.0)
|
|
//let searchPlaceholderCombinedWidth = searchIconSize.width + searchIconSpacing + placeholderSize.width
|
|
|
|
let placeholderTextFrame = CGRect(origin: CGPoint(x: component.isSearchActive ? (backgroundFrame.minX + fieldSideInset + searchIconSize.width + searchIconSpacing) : floor(backgroundFrame.midX - placeholderSize.width * 0.5), y: backgroundFrame.minY + (backgroundFrame.height - placeholderSize.height) * 0.5), size: placeholderSize)
|
|
var placeholderDeltaX: CGFloat = 0.0
|
|
if let placeholderTextView = self.placeholderText.view {
|
|
if placeholderTextView.superview == nil {
|
|
placeholderTextView.layer.anchorPoint = CGPoint()
|
|
placeholderTextView.isUserInteractionEnabled = false
|
|
self.insertSubview(placeholderTextView, aboveSubview: self.searchIconView)
|
|
} else {
|
|
placeholderDeltaX = placeholderTextFrame.minX - placeholderTextView.frame.minX
|
|
}
|
|
transition.setPosition(view: placeholderTextView, position: placeholderTextFrame.origin)
|
|
transition.setBounds(view: placeholderTextView, bounds: CGRect(origin: CGPoint(), size: placeholderTextFrame.size))
|
|
transition.setAlpha(view: placeholderTextView, alpha: contentAlpha)
|
|
}
|
|
|
|
let searchIconFrame = CGRect(origin: CGPoint(x: placeholderTextFrame.minX - searchIconSpacing - searchIconSize.width, y: backgroundFrame.minY + (backgroundFrame.height - searchIconSize.height) * 0.5), size: searchIconSize)
|
|
transition.setFrame(view: self.searchIconView, frame: searchIconFrame)
|
|
transition.setAlpha(view: self.searchIconView, alpha: contentAlpha)
|
|
|
|
if let image = self.clearIconView.image {
|
|
let clearSize = image.size
|
|
let clearFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - 6.0 - clearSize.width, y: backgroundFrame.minY + (backgroundFrame.height - clearSize.height) / 2.0), size: clearSize)
|
|
|
|
let clearButtonFrame = CGRect(origin: CGPoint(x: clearFrame.minX - 4.0, y: backgroundFrame.minY), size: CGSize(width: clearFrame.width + 8.0, height: backgroundFrame.height))
|
|
transition.setFrame(view: self.clearButton, frame: clearButtonFrame)
|
|
transition.setFrame(view: self.clearIconView, frame: clearFrame.offsetBy(dx: -clearButtonFrame.minX, dy: -clearButtonFrame.minY))
|
|
}
|
|
|
|
if let textField = self.textField {
|
|
var textFieldTransition = transition
|
|
var animateIn = false
|
|
if textField.bounds.isEmpty {
|
|
textFieldTransition = textFieldTransition.withAnimation(.none)
|
|
animateIn = true
|
|
}
|
|
|
|
if textField.textColor != component.colors.foreground {
|
|
textField.textColor = component.colors.foreground
|
|
textField.font = Font.regular(17.0)
|
|
}
|
|
|
|
let textLeftInset: CGFloat = fieldSideInset + searchIconSize.width + searchIconSpacing
|
|
let textRightInset: CGFloat = 8.0 + 30.0
|
|
textFieldTransition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: placeholderTextFrame.minX, y: backgroundFrame.minY - 1.0), size: CGSize(width: backgroundFrame.width - textLeftInset - textRightInset, height: backgroundFrame.height)))
|
|
|
|
if animateIn {
|
|
transition.animatePosition(view: textField, from: CGPoint(x: -placeholderDeltaX, y: 0.0), to: CGPoint(), additive: true)
|
|
}
|
|
}
|
|
|
|
if let textField = self.textField {
|
|
if !component.isSearchActive {
|
|
if !(textField.text?.isEmpty ?? true) {
|
|
textField.text = ""
|
|
self.updateText(updateComponent: false)
|
|
}
|
|
|
|
if textField.isFirstResponder {
|
|
DispatchQueue.main.async { [weak textField] in
|
|
textField?.resignFirstResponder()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|