mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
753 lines
35 KiB
Swift
753 lines
35 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import ComponentFlow
|
|
import PagerComponent
|
|
import TelegramPresentationData
|
|
import TelegramCore
|
|
import Postbox
|
|
import AnimationCache
|
|
import MultiAnimationRenderer
|
|
import AccountContext
|
|
import AsyncDisplayKit
|
|
import ComponentDisplayAdapters
|
|
import LottieAnimationComponent
|
|
import EmojiStatusComponent
|
|
import LottieComponent
|
|
import LottieComponentEmojiContent
|
|
import AudioToolbox
|
|
|
|
private final class RoundMaskView: UIImageView {
|
|
private var currentDiameter: CGFloat?
|
|
|
|
func update(diameter: CGFloat) {
|
|
if self.currentDiameter != diameter {
|
|
self.currentDiameter = diameter
|
|
|
|
let shadowWidth: CGFloat = 6.0
|
|
self.image = generateImage(CGSize(width: shadowWidth * 2.0 + diameter, height: diameter), rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
|
|
|
let shadowColor = UIColor.black
|
|
|
|
let stepCount = 10
|
|
var colors: [CGColor] = []
|
|
var locations: [CGFloat] = []
|
|
|
|
for i in 0 ... stepCount {
|
|
let t = CGFloat(i) / CGFloat(stepCount)
|
|
colors.append(shadowColor.withAlphaComponent(t * t).cgColor)
|
|
locations.append(t)
|
|
}
|
|
|
|
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colors as CFArray, locations: &locations)!
|
|
|
|
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
|
let gradientWidth = shadowWidth
|
|
context.drawRadialGradient(gradient, startCenter: center, startRadius: size.width / 2.0, endCenter: center, endRadius: size.width / 2.0 - gradientWidth, options: [])
|
|
|
|
context.setFillColor(shadowColor.cgColor)
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowWidth, y: 0.0), size: CGSize(width: size.height, height: size.height)).insetBy(dx: -0.5, dy: -0.5))
|
|
})?.stretchableImage(withLeftCapWidth: Int(shadowWidth * 0.5 + diameter * 0.5), topCapHeight: Int(diameter * 0.5))
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class HoldGestureRecognizer: UITapGestureRecognizer {
|
|
private var currentHighlightPoint: CGPoint?
|
|
var updateHighlight: ((CGPoint?) -> Void)?
|
|
|
|
override var state: UIGestureRecognizer.State {
|
|
didSet {
|
|
print("set state \(self.state)")
|
|
}
|
|
}
|
|
|
|
override func reset() {
|
|
super.reset()
|
|
|
|
if let _ = self.currentHighlightPoint {
|
|
self.currentHighlightPoint = nil
|
|
self.updateHighlight?(nil)
|
|
}
|
|
}
|
|
|
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesBegan(touches, with: event)
|
|
|
|
let point = touches.first?.location(in: self.view)
|
|
if self.currentHighlightPoint == nil {
|
|
self.currentHighlightPoint = point
|
|
self.updateHighlight?(point)
|
|
}
|
|
}
|
|
|
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesEnded(touches, with: event)
|
|
}
|
|
|
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
super.touchesCancelled(touches, with: event)
|
|
}
|
|
}
|
|
|
|
final class EmojiSearchSearchBarComponent: Component {
|
|
enum TextInputState: Equatable {
|
|
case inactive
|
|
case active(hasText: Bool)
|
|
}
|
|
|
|
let context: AccountContext
|
|
let theme: PresentationTheme
|
|
let forceNeedsVibrancy: Bool
|
|
let strings: PresentationStrings
|
|
let useOpaqueTheme: Bool
|
|
let textInputState: TextInputState
|
|
let categories: EmojiSearchCategories?
|
|
let searchTermUpdated: (EmojiSearchCategories.Group?) -> Void
|
|
let activateTextInput: () -> Void
|
|
|
|
init(
|
|
context: AccountContext,
|
|
theme: PresentationTheme,
|
|
forceNeedsVibrancy: Bool,
|
|
strings: PresentationStrings,
|
|
useOpaqueTheme: Bool,
|
|
textInputState: TextInputState,
|
|
categories: EmojiSearchCategories?,
|
|
searchTermUpdated: @escaping (EmojiSearchCategories.Group?) -> Void,
|
|
activateTextInput: @escaping () -> Void
|
|
) {
|
|
self.context = context
|
|
self.theme = theme
|
|
self.forceNeedsVibrancy = forceNeedsVibrancy
|
|
self.strings = strings
|
|
self.useOpaqueTheme = useOpaqueTheme
|
|
self.textInputState = textInputState
|
|
self.categories = categories
|
|
self.searchTermUpdated = searchTermUpdated
|
|
self.activateTextInput = activateTextInput
|
|
}
|
|
|
|
static func ==(lhs: EmojiSearchSearchBarComponent, rhs: EmojiSearchSearchBarComponent) -> Bool {
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.forceNeedsVibrancy != rhs.forceNeedsVibrancy {
|
|
return false
|
|
}
|
|
if lhs.strings !== rhs.strings {
|
|
return false
|
|
}
|
|
if lhs.useOpaqueTheme != rhs.useOpaqueTheme {
|
|
return false
|
|
}
|
|
if lhs.textInputState != rhs.textInputState {
|
|
return false
|
|
}
|
|
if lhs.categories != rhs.categories {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private struct ItemLayout {
|
|
let containerSize: CGSize
|
|
let itemCount: Int
|
|
let itemSize: CGSize
|
|
let itemSpacing: CGFloat
|
|
let contentSize: CGSize
|
|
let leftInset: CGFloat
|
|
let rightInset: CGFloat
|
|
let itemStartX: CGFloat
|
|
|
|
let textSpacing: CGFloat
|
|
let textFrame: CGRect
|
|
|
|
init(containerSize: CGSize, textSize: CGSize, itemCount: Int) {
|
|
self.containerSize = containerSize
|
|
self.itemCount = itemCount
|
|
self.itemSpacing = 11.0
|
|
self.leftInset = 8.0
|
|
self.rightInset = 8.0
|
|
self.itemSize = CGSize(width: 24.0, height: 24.0)
|
|
self.textSpacing = 11.0
|
|
|
|
self.textFrame = CGRect(origin: CGPoint(x: self.leftInset, y: floor((containerSize.height - textSize.height) * 0.5)), size: textSize)
|
|
|
|
let itemsWidth: CGFloat = self.itemSize.width * CGFloat(self.itemCount) + self.itemSpacing * CGFloat(max(0, self.itemCount - 1))
|
|
|
|
var itemStartX = self.textFrame.maxX + self.textSpacing
|
|
if itemStartX + itemsWidth + self.rightInset < containerSize.width {
|
|
itemStartX = containerSize.width - self.rightInset - itemsWidth
|
|
}
|
|
|
|
self.itemStartX = itemStartX
|
|
|
|
self.contentSize = CGSize(width: self.itemStartX + itemsWidth + self.rightInset, height: containerSize.height)
|
|
}
|
|
|
|
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
|
let baseItemX: CGFloat = self.itemStartX
|
|
let offsetRect = rect.offsetBy(dx: -baseItemX, dy: 0.0)
|
|
var minVisibleIndex = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize.width + self.itemSpacing)))
|
|
minVisibleIndex = max(0, minVisibleIndex)
|
|
var maxVisibleIndex = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize.height + self.itemSpacing)))
|
|
maxVisibleIndex = min(maxVisibleIndex, self.itemCount - 1)
|
|
|
|
if minVisibleIndex <= maxVisibleIndex {
|
|
return minVisibleIndex ..< (maxVisibleIndex + 1)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func frame(at index: Int) -> CGRect {
|
|
return CGRect(origin: CGPoint(x: self.itemStartX + CGFloat(index) * (self.itemSize.width + self.itemSpacing), y: floor((self.containerSize.height - self.itemSize.height) * 0.5)), size: self.itemSize)
|
|
}
|
|
}
|
|
|
|
private final class ContentScrollView: UIScrollView, PagerExpandableScrollView {
|
|
override static var layerClass: AnyClass {
|
|
return EmojiPagerContentComponent.View.ContentScrollLayer.self
|
|
}
|
|
|
|
private let mirrorView: UIView
|
|
|
|
init(mirrorView: UIView) {
|
|
self.mirrorView = mirrorView
|
|
|
|
super.init(frame: CGRect())
|
|
|
|
(self.layer as? EmojiPagerContentComponent.View.ContentScrollLayer)?.mirrorLayer = mirrorView.layer
|
|
self.canCancelContentTouches = true
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func touchesShouldCancel(in view: UIView) -> Bool {
|
|
return true
|
|
}
|
|
}
|
|
|
|
private final class ItemView {
|
|
let view = ComponentView<Empty>()
|
|
let tintView = UIImageView()
|
|
|
|
init() {
|
|
}
|
|
}
|
|
|
|
final class View: UIView, UIScrollViewDelegate {
|
|
let tintContainerView: UIView
|
|
private let scrollView: ContentScrollView
|
|
private let tintScrollView: UIView
|
|
|
|
private let textView = ComponentView<Empty>()
|
|
private let textContainerView: UIView
|
|
|
|
private let tintTextView = ComponentView<Empty>()
|
|
private let tintTextContainerView: UIView
|
|
|
|
private var visibleItemViews: [AnyHashable: ItemView] = [:]
|
|
private let selectedItemBackground: SimpleLayer
|
|
private let selectedItemTintBackground: SimpleLayer
|
|
|
|
private var component: EmojiSearchSearchBarComponent?
|
|
private weak var componentState: EmptyComponentState?
|
|
|
|
private var itemLayout: ItemLayout?
|
|
private var ignoreScrolling: Bool = false
|
|
|
|
private let roundMaskView: RoundMaskView
|
|
private let tintRoundMaskView: RoundMaskView
|
|
|
|
private var highlightedItem: AnyHashable?
|
|
private var selectedItem: AnyHashable?
|
|
|
|
private var disableInteraction: Bool = false
|
|
|
|
private lazy var hapticFeedback: HapticFeedback = {
|
|
return HapticFeedback()
|
|
}()
|
|
|
|
override init(frame: CGRect) {
|
|
self.tintContainerView = UIView()
|
|
|
|
self.tintScrollView = UIView()
|
|
self.tintScrollView.clipsToBounds = true
|
|
self.scrollView = ContentScrollView(mirrorView: self.tintScrollView)
|
|
|
|
self.textContainerView = UIView()
|
|
self.textContainerView.isUserInteractionEnabled = false
|
|
self.tintTextContainerView = UIView()
|
|
self.tintTextContainerView.isUserInteractionEnabled = false
|
|
|
|
self.roundMaskView = RoundMaskView()
|
|
self.tintRoundMaskView = RoundMaskView()
|
|
|
|
self.selectedItemBackground = SimpleLayer()
|
|
self.selectedItemTintBackground = SimpleLayer()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.scrollView.delaysContentTouches = false
|
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
|
}
|
|
if #available(iOS 13.0, *) {
|
|
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
|
}
|
|
self.scrollView.showsVerticalScrollIndicator = true
|
|
self.scrollView.showsHorizontalScrollIndicator = false
|
|
self.scrollView.delegate = self
|
|
self.scrollView.clipsToBounds = true
|
|
self.scrollView.scrollsToTop = false
|
|
|
|
self.addSubview(self.scrollView)
|
|
self.addSubview(self.textContainerView)
|
|
|
|
self.tintContainerView.addSubview(self.tintScrollView)
|
|
self.tintContainerView.addSubview(self.tintTextContainerView)
|
|
|
|
self.mask = self.roundMaskView
|
|
self.tintContainerView.mask = self.tintRoundMaskView
|
|
|
|
self.scrollView.layer.addSublayer(self.selectedItemBackground)
|
|
self.tintScrollView.layer.addSublayer(self.selectedItemTintBackground)
|
|
|
|
let tapRecognizer = HoldGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
|
tapRecognizer.updateHighlight = { [weak self] point in
|
|
guard let self else {
|
|
return
|
|
}
|
|
var highlightedItem: AnyHashable?
|
|
|
|
if let point = point {
|
|
let location = self.convert(point, to: self.scrollView)
|
|
for (id, itemView) in self.visibleItemViews {
|
|
if let itemComponentView = itemView.view.view, itemComponentView.frame.contains(location) {
|
|
highlightedItem = id
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if self.highlightedItem != highlightedItem {
|
|
self.highlightedItem = highlightedItem
|
|
self.componentState?.updated(transition: .easeInOut(duration: 0.2))
|
|
}
|
|
}
|
|
self.addGestureRecognizer(tapRecognizer)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
guard let component = self.component, let itemLayout = self.itemLayout else {
|
|
return
|
|
}
|
|
let location = recognizer.location(in: self.scrollView)
|
|
if (component.categories?.groups ?? []).isEmpty || location.x <= itemLayout.itemStartX - itemLayout.textSpacing {
|
|
component.activateTextInput()
|
|
} else {
|
|
for (id, itemView) in self.visibleItemViews {
|
|
if let itemComponentView = itemView.view.view, itemComponentView.frame.contains(location), let itemId = id.base as? Int64 {
|
|
if self.selectedItem == AnyHashable(id) {
|
|
self.selectedItem = nil
|
|
} else {
|
|
self.selectedItem = AnyHashable(id)
|
|
AudioServicesPlaySystemSound(0x450)
|
|
self.hapticFeedback.tap()
|
|
}
|
|
self.componentState?.updated(transition: .easeInOut(duration: 0.2))
|
|
|
|
if let _ = self.selectedItem, let categories = component.categories, let group = categories.groups.first(where: { $0.id == itemId }) {
|
|
component.searchTermUpdated(group)
|
|
|
|
if let itemComponentView = itemView.view.view {
|
|
var offset = self.scrollView.contentOffset.x
|
|
let maxDistance: CGFloat = 44.0
|
|
if itemComponentView.frame.maxX - offset > self.scrollView.bounds.width - maxDistance {
|
|
offset = itemComponentView.frame.maxX - (self.scrollView.bounds.width - maxDistance)
|
|
}
|
|
if itemComponentView.frame.minX - offset < maxDistance {
|
|
offset = itemComponentView.frame.minX - maxDistance
|
|
}
|
|
offset = max(0.0, min(offset, self.scrollView.contentSize.width - self.scrollView.bounds.width))
|
|
if offset != self.scrollView.contentOffset.x {
|
|
self.scrollView.setContentOffset(CGPoint(x: offset, y: 0.0), animated: true)
|
|
}
|
|
}
|
|
} else {
|
|
let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))
|
|
transition.setBoundsOrigin(view: self.scrollView, origin: CGPoint())
|
|
self.updateScrolling(transition: transition, fromScrolling: false)
|
|
//self.scrollView.setContentOffset(CGPoint(), animated: true)
|
|
|
|
component.searchTermUpdated(nil)
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func clearSelection(dispatchEvent: Bool) {
|
|
if self.selectedItem != nil {
|
|
self.selectedItem = nil
|
|
|
|
let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))
|
|
transition.setBoundsOrigin(view: self.scrollView, origin: CGPoint())
|
|
self.updateScrolling(transition: transition, fromScrolling: false)
|
|
|
|
self.componentState?.updated(transition: transition)
|
|
|
|
if dispatchEvent {
|
|
self.component?.searchTermUpdated(nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
if !self.ignoreScrolling {
|
|
self.updateScrolling(transition: .immediate, fromScrolling: true)
|
|
}
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if self.disableInteraction {
|
|
for (_, itemView) in self.visibleItemViews {
|
|
if let itemComponentView = itemView.view.view {
|
|
if itemComponentView.bounds.contains(self.convert(point, to: itemComponentView)) {
|
|
return self
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
|
|
func leftTextPosition() -> CGFloat {
|
|
guard let itemLayout = self.itemLayout else {
|
|
return 0.0
|
|
}
|
|
|
|
let visibleBounds = self.scrollView.bounds
|
|
return (itemLayout.itemStartX - itemLayout.textSpacing) + visibleBounds.minX
|
|
}
|
|
|
|
private func updateScrolling(transition: ComponentTransition, fromScrolling: Bool) {
|
|
guard let component = self.component, let itemLayout = self.itemLayout else {
|
|
return
|
|
}
|
|
|
|
let itemAlpha: CGFloat
|
|
switch component.textInputState {
|
|
case let .active(hasText):
|
|
if hasText {
|
|
itemAlpha = 0.0
|
|
} else {
|
|
itemAlpha = 1.0
|
|
}
|
|
case .inactive:
|
|
itemAlpha = 1.0
|
|
}
|
|
|
|
var validItemIds = Set<AnyHashable>()
|
|
let visibleBounds = self.scrollView.bounds
|
|
|
|
var animateAppearingItems = false
|
|
if fromScrolling {
|
|
animateAppearingItems = true
|
|
}
|
|
|
|
let items = component.categories?.groups ?? []
|
|
|
|
for i in 0 ..< items.count {
|
|
let itemFrame = itemLayout.frame(at: i)
|
|
if visibleBounds.intersects(itemFrame) {
|
|
let item = items[i]
|
|
validItemIds.insert(AnyHashable(item.id))
|
|
|
|
var animateItem = false
|
|
var itemTransition = transition
|
|
let itemView: ItemView
|
|
if let current = self.visibleItemViews[AnyHashable(item.id)] {
|
|
itemView = current
|
|
} else {
|
|
animateItem = animateAppearingItems
|
|
itemTransition = .immediate
|
|
itemView = ItemView()
|
|
self.visibleItemViews[AnyHashable(item.id)] = itemView
|
|
}
|
|
|
|
let color: UIColor
|
|
if component.theme.overallDarkAppearance && component.forceNeedsVibrancy {
|
|
let tempColor = self.selectedItem == AnyHashable(item.id) ? component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlaySelectedColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
|
|
color = tempColor.withMultipliedAlpha(0.3)
|
|
} else if component.useOpaqueTheme {
|
|
color = self.selectedItem == AnyHashable(item.id) ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlaySelectedColor : component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor
|
|
} else {
|
|
color = self.selectedItem == AnyHashable(item.id) ? component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlaySelectedColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
|
|
}
|
|
|
|
let _ = itemView.view.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(LottieComponent(
|
|
content: LottieComponent.EmojiContent(
|
|
context: component.context,
|
|
fileId: item.id
|
|
),
|
|
color: color
|
|
)),
|
|
environment: {},
|
|
containerSize: itemLayout.itemSize
|
|
)
|
|
|
|
itemView.tintView.tintColor = .black
|
|
|
|
if let view = itemView.view.view as? LottieComponent.View {
|
|
if view.superview == nil {
|
|
self.scrollView.addSubview(view)
|
|
|
|
view.output = itemView.tintView
|
|
self.tintScrollView.addSubview(itemView.tintView)
|
|
}
|
|
|
|
itemTransition.setPosition(view: view, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY))
|
|
itemTransition.setBounds(view: view, bounds: CGRect(origin: CGPoint(), size: CGSize(width: itemLayout.itemSize.width, height: itemLayout.itemSize.height)))
|
|
|
|
var scaleFactor = itemFrame.width / itemLayout.itemSize.width
|
|
if self.highlightedItem == AnyHashable(item.id) {
|
|
scaleFactor *= 0.8
|
|
}
|
|
|
|
itemTransition.setScale(view: view, scale: scaleFactor)
|
|
|
|
itemTransition.setPosition(view: itemView.tintView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY))
|
|
itemTransition.setBounds(view: itemView.tintView, bounds: CGRect(origin: CGPoint(), size: CGSize(width: itemLayout.itemSize.width, height: itemLayout.itemSize.height)))
|
|
itemTransition.setScale(view: itemView.tintView, scale: scaleFactor)
|
|
|
|
itemTransition.setAlpha(view: view, alpha: itemAlpha)
|
|
itemTransition.setAlpha(view: itemView.tintView, alpha: itemAlpha)
|
|
|
|
let isHidden = !visibleBounds.intersects(itemFrame)
|
|
if isHidden != view.isHidden {
|
|
view.isHidden = isHidden
|
|
itemView.tintView.isHidden = true
|
|
|
|
if !isHidden {
|
|
view.playOnce()
|
|
}
|
|
} else if animateItem {
|
|
if fromScrolling {
|
|
view.playOnce(delay: 0.08)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var removedItemIds: [AnyHashable] = []
|
|
for (id, itemView) in self.visibleItemViews {
|
|
if !validItemIds.contains(id) {
|
|
removedItemIds.append(id)
|
|
|
|
if let itemComponentView = itemView.view.view {
|
|
transition.attachAnimation(view: itemComponentView, id: "remove", completion: { [weak itemComponentView] _ in
|
|
itemComponentView?.removeFromSuperview()
|
|
})
|
|
}
|
|
let tintView = itemView.tintView
|
|
transition.attachAnimation(view: tintView, id: "remove", completion: { [weak tintView] _ in
|
|
tintView?.removeFromSuperview()
|
|
})
|
|
//itemView.view.view?.removeFromSuperview()
|
|
//itemView.tintView.removeFromSuperview()
|
|
}
|
|
}
|
|
for id in removedItemIds {
|
|
self.visibleItemViews.removeValue(forKey: id)
|
|
}
|
|
|
|
let selectedColor: UIColor
|
|
if component.theme.overallDarkAppearance && component.forceNeedsVibrancy {
|
|
let tempColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayHighlightColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayHighlightColor
|
|
selectedColor = tempColor.withMultipliedAlpha(0.3)
|
|
} else {
|
|
selectedColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayHighlightColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayHighlightColor
|
|
}
|
|
|
|
if let selectedItem = self.selectedItem, let index = items.firstIndex(where: { AnyHashable($0.id) == selectedItem }) {
|
|
let selectedItemCenter = itemLayout.frame(at: index).center
|
|
let selectionSize = CGSize(width: 28.0, height: 28.0)
|
|
self.selectedItemBackground.backgroundColor = selectedColor.cgColor
|
|
self.selectedItemTintBackground.backgroundColor = UIColor(white: 0.0, alpha: 0.15).cgColor
|
|
self.selectedItemBackground.cornerRadius = selectionSize.height * 0.5
|
|
self.selectedItemTintBackground.cornerRadius = selectionSize.height * 0.5
|
|
|
|
let selectionFrame = CGRect(origin: CGPoint(x: floor(selectedItemCenter.x - selectionSize.width * 0.5), y: floor(selectedItemCenter.y - selectionSize.height * 0.5)), size: selectionSize)
|
|
|
|
self.selectedItemBackground.bounds = CGRect(origin: CGPoint(), size: selectionFrame.size)
|
|
self.selectedItemTintBackground.bounds = CGRect(origin: CGPoint(), size: selectionFrame.size)
|
|
|
|
if self.selectedItemBackground.opacity == 0.0 {
|
|
self.selectedItemBackground.position = selectionFrame.center
|
|
self.selectedItemTintBackground.position = selectionFrame.center
|
|
|
|
self.selectedItemBackground.opacity = 1.0
|
|
self.selectedItemTintBackground.opacity = 1.0
|
|
|
|
ComponentTransition.immediate.setScale(layer: self.selectedItemBackground, scale: 1.0)
|
|
ComponentTransition.immediate.setScale(layer: self.selectedItemTintBackground, scale: 1.0)
|
|
|
|
if !transition.animation.isImmediate {
|
|
self.selectedItemBackground.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
self.selectedItemTintBackground.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
|
|
self.selectedItemBackground.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, damping: 92.0)
|
|
self.selectedItemTintBackground.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, damping: 92.0)
|
|
}
|
|
} else {
|
|
if self.selectedItemBackground.position != selectionFrame.center {
|
|
transition.setPosition(layer: self.selectedItemBackground, position: selectionFrame.center)
|
|
transition.setPosition(layer: self.selectedItemTintBackground, position: selectionFrame.center)
|
|
|
|
if case let .curve(duration, _) = transition.animation {
|
|
ComponentTransition.immediate.setScale(layer: self.selectedItemBackground, scale: 1.0)
|
|
ComponentTransition.immediate.setScale(layer: self.selectedItemTintBackground, scale: 1.0)
|
|
|
|
self.selectedItemBackground.animateKeyframes(values: [1.0 as NSNumber, 0.75 as NSNumber, 1.0 as NSNumber], duration: duration, keyPath: "transform.scale")
|
|
self.selectedItemTintBackground.animateKeyframes(values: [1.0 as NSNumber, 0.75 as NSNumber, 1.0 as NSNumber], duration: duration, keyPath: "transform.scale")
|
|
} else {
|
|
transition.setScale(layer: self.selectedItemBackground, scale: 1.0)
|
|
transition.setScale(layer: self.selectedItemTintBackground, scale: 1.0)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
transition.setAlpha(layer: self.selectedItemBackground, alpha: 0.0)
|
|
transition.setScale(layer: self.selectedItemBackground, scale: 0.8)
|
|
transition.setAlpha(layer: self.selectedItemTintBackground, alpha: 0.0)
|
|
transition.setScale(layer: self.selectedItemTintBackground, scale: 0.8)
|
|
}
|
|
|
|
let scrollBounds = self.scrollView.bounds
|
|
let textOffset = max(0.0, scrollBounds.minX - (itemLayout.itemStartX - itemLayout.textFrame.maxX - itemLayout.textSpacing))
|
|
|
|
transition.setPosition(view: self.textContainerView, position: self.scrollView.center)
|
|
transition.setBounds(view: self.textContainerView, bounds: CGRect(origin: CGPoint(x: textOffset, y: 0.0), size: scrollBounds.size))
|
|
|
|
transition.setPosition(view: self.tintTextContainerView, position: self.scrollView.center)
|
|
transition.setBounds(view: self.tintTextContainerView, bounds: CGRect(origin: CGPoint(x: textOffset, y: 0.0), size: scrollBounds.size))
|
|
}
|
|
|
|
func update(component: EmojiSearchSearchBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
self.componentState = state
|
|
|
|
let textColor: UIColor
|
|
if component.theme.overallDarkAppearance && component.forceNeedsVibrancy {
|
|
textColor = component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor.withMultipliedAlpha(0.3)
|
|
} else {
|
|
textColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
|
|
}
|
|
|
|
let textSize = self.textView.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(Text(
|
|
text: component.strings.Common_Search,
|
|
font: Font.regular(17.0),
|
|
color: textColor
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - 32.0, height: 100.0)
|
|
)
|
|
let _ = self.tintTextView.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(Text(
|
|
text: component.strings.Common_Search,
|
|
font: Font.regular(17.0),
|
|
color: .black
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - 32.0, height: 100.0)
|
|
)
|
|
|
|
let itemLayout = ItemLayout(containerSize: availableSize, textSize: textSize, itemCount: component.categories?.groups.count ?? 0)
|
|
self.itemLayout = itemLayout
|
|
|
|
if let textComponentView = self.textView.view {
|
|
if textComponentView.superview == nil {
|
|
self.textContainerView.addSubview(textComponentView)
|
|
}
|
|
transition.setFrame(view: textComponentView, frame: itemLayout.textFrame)
|
|
}
|
|
if let tintTextComponentView = self.tintTextView.view {
|
|
if tintTextComponentView.superview == nil {
|
|
self.tintTextContainerView.addSubview(tintTextComponentView)
|
|
}
|
|
transition.setFrame(view: tintTextComponentView, frame: itemLayout.textFrame)
|
|
}
|
|
|
|
self.ignoreScrolling = true
|
|
if self.scrollView.bounds.size != availableSize {
|
|
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
|
}
|
|
if case .active(true) = component.textInputState {
|
|
transition.setBoundsOrigin(view: self.scrollView, origin: CGPoint())
|
|
}
|
|
if self.scrollView.contentSize != itemLayout.contentSize {
|
|
self.scrollView.contentSize = itemLayout.contentSize
|
|
}
|
|
self.ignoreScrolling = false
|
|
|
|
let maskFrame = CGRect(origin: CGPoint(), size: availableSize)
|
|
transition.setFrame(view: self.roundMaskView, frame: maskFrame)
|
|
self.roundMaskView.update(diameter: maskFrame.height)
|
|
transition.setFrame(view: self.tintRoundMaskView, frame: maskFrame)
|
|
self.tintRoundMaskView.update(diameter: maskFrame.height)
|
|
|
|
self.updateScrolling(transition: transition, fromScrolling: false)
|
|
|
|
switch component.textInputState {
|
|
case let .active(hasText):
|
|
if hasText {
|
|
self.disableInteraction = false
|
|
self.isUserInteractionEnabled = false
|
|
} else {
|
|
self.disableInteraction = true
|
|
self.isUserInteractionEnabled = true
|
|
}
|
|
self.textView.view?.isHidden = hasText
|
|
self.tintTextView.view?.isHidden = hasText
|
|
case .inactive:
|
|
self.disableInteraction = false
|
|
self.isUserInteractionEnabled = true
|
|
self.textView.view?.isHidden = false
|
|
self.tintTextView.view?.isHidden = false
|
|
}
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|