mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
766 lines
33 KiB
Swift
766 lines
33 KiB
Swift
import Foundation
|
|
import SwiftSignalKit
|
|
import UIKit
|
|
import Display
|
|
import ComponentFlow
|
|
import PagerComponent
|
|
import TelegramPresentationData
|
|
import TelegramCore
|
|
import Postbox
|
|
import AnimationCache
|
|
import MultiAnimationRenderer
|
|
import AccountContext
|
|
import MultilineTextComponent
|
|
|
|
final class EntityKeyboardAnimationTopPanelComponent: Component {
|
|
typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment
|
|
|
|
let context: AccountContext
|
|
let file: TelegramMediaFile
|
|
let animationCache: AnimationCache
|
|
let animationRenderer: MultiAnimationRenderer
|
|
let theme: PresentationTheme
|
|
let title: String
|
|
let pressed: () -> Void
|
|
|
|
init(
|
|
context: AccountContext,
|
|
file: TelegramMediaFile,
|
|
animationCache: AnimationCache,
|
|
animationRenderer: MultiAnimationRenderer,
|
|
theme: PresentationTheme,
|
|
title: String,
|
|
pressed: @escaping () -> Void
|
|
) {
|
|
self.context = context
|
|
self.file = file
|
|
self.animationCache = animationCache
|
|
self.animationRenderer = animationRenderer
|
|
self.theme = theme
|
|
self.title = title
|
|
self.pressed = pressed
|
|
}
|
|
|
|
static func ==(lhs: EntityKeyboardAnimationTopPanelComponent, rhs: EntityKeyboardAnimationTopPanelComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.file.fileId != rhs.file.fileId {
|
|
return false
|
|
}
|
|
if lhs.animationCache !== rhs.animationCache {
|
|
return false
|
|
}
|
|
if lhs.animationRenderer !== rhs.animationRenderer {
|
|
return false
|
|
}
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.title != rhs.title {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
var itemLayer: EmojiPagerContentComponent.View.ItemLayer?
|
|
var placeholderView: EmojiPagerContentComponent.View.ItemPlaceholderView?
|
|
var component: EntityKeyboardAnimationTopPanelComponent?
|
|
var titleView: ComponentView<Empty>?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
self.component?.pressed()
|
|
}
|
|
}
|
|
|
|
func update(component: EntityKeyboardAnimationTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
self.component = component
|
|
|
|
let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value
|
|
|
|
if self.itemLayer == nil {
|
|
let itemLayer = EmojiPagerContentComponent.View.ItemLayer(
|
|
item: EmojiPagerContentComponent.Item(
|
|
emoji: "",
|
|
file: component.file,
|
|
stickerPackItem: nil
|
|
),
|
|
context: component.context,
|
|
groupId: "topPanel",
|
|
attemptSynchronousLoad: false,
|
|
file: component.file,
|
|
cache: component.animationCache,
|
|
renderer: component.animationRenderer,
|
|
placeholderColor: .lightGray,
|
|
blurredBadgeColor: .clear,
|
|
displayPremiumBadgeIfAvailable: false,
|
|
pointSize: CGSize(width: 44.0, height: 44.0),
|
|
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateDisplayPlaceholder(displayPlaceholder: displayPlaceholder)
|
|
}
|
|
)
|
|
self.itemLayer = itemLayer
|
|
self.layer.addSublayer(itemLayer)
|
|
|
|
if itemLayer.displayPlaceholder {
|
|
self.updateDisplayPlaceholder(displayPlaceholder: true)
|
|
}
|
|
}
|
|
|
|
let iconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 44.0, height: 44.0) : CGSize(width: 28.0, height: 28.0)
|
|
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) / 2.0), y: 0.0), size: iconSize)
|
|
|
|
if let itemLayer = self.itemLayer {
|
|
transition.setPosition(layer: itemLayer, position: CGPoint(x: iconFrame.midX, y: iconFrame.midY))
|
|
transition.setBounds(layer: itemLayer, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
|
|
itemLayer.isVisibleForAnimations = true
|
|
}
|
|
|
|
if itemEnvironment.isExpanded {
|
|
let titleView: ComponentView<Empty>
|
|
if let current = self.titleView {
|
|
titleView = current
|
|
} else {
|
|
titleView = ComponentView<Empty>()
|
|
self.titleView = titleView
|
|
}
|
|
let titleSize = titleView.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 62.0, height: 100.0)
|
|
)
|
|
if let view = titleView.view {
|
|
if view.superview == nil {
|
|
view.alpha = 0.0
|
|
self.addSubview(view)
|
|
}
|
|
view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height), size: titleSize)
|
|
transition.setAlpha(view: view, alpha: 1.0)
|
|
}
|
|
} else if let titleView = self.titleView {
|
|
self.titleView = nil
|
|
if let view = titleView.view {
|
|
transition.setAlpha(view: view, alpha: 0.0, completion: { [weak view] _ in
|
|
view?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
return availableSize
|
|
}
|
|
|
|
private func updateDisplayPlaceholder(displayPlaceholder: Bool) {
|
|
if displayPlaceholder {
|
|
if self.placeholderView == nil, let component = self.component {
|
|
let placeholderView = EmojiPagerContentComponent.View.ItemPlaceholderView(
|
|
context: component.context,
|
|
file: component.file,
|
|
shimmerView: nil,
|
|
color: component.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08),
|
|
size: CGSize(width: 28.0, height: 28.0)
|
|
)
|
|
self.placeholderView = placeholderView
|
|
self.insertSubview(placeholderView, at: 0)
|
|
placeholderView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 28.0, height: 28.0))
|
|
placeholderView.update(size: CGSize(width: 28.0, height: 28.0))
|
|
}
|
|
} else {
|
|
if let placeholderView = self.placeholderView {
|
|
self.placeholderView = nil
|
|
placeholderView.removeFromSuperview()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
final class EntityKeyboardIconTopPanelComponent: Component {
|
|
typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment
|
|
|
|
let imageName: String
|
|
let theme: PresentationTheme
|
|
let title: String
|
|
let pressed: () -> Void
|
|
|
|
init(
|
|
imageName: String,
|
|
theme: PresentationTheme,
|
|
title: String,
|
|
pressed: @escaping () -> Void
|
|
) {
|
|
self.imageName = imageName
|
|
self.theme = theme
|
|
self.title = title
|
|
self.pressed = pressed
|
|
}
|
|
|
|
static func ==(lhs: EntityKeyboardIconTopPanelComponent, rhs: EntityKeyboardIconTopPanelComponent) -> Bool {
|
|
if lhs.imageName != rhs.imageName {
|
|
return false
|
|
}
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.title != rhs.title {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
let iconView: UIImageView
|
|
var component: EntityKeyboardIconTopPanelComponent?
|
|
var titleView: ComponentView<Empty>?
|
|
|
|
override init(frame: CGRect) {
|
|
self.iconView = UIImageView()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubview(self.iconView)
|
|
|
|
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
self.component?.pressed()
|
|
}
|
|
}
|
|
|
|
func update(component: EntityKeyboardIconTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value
|
|
|
|
if self.component?.imageName != component.imageName {
|
|
self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: component.imageName), color: component.theme.chat.inputMediaPanel.panelIconColor)
|
|
}
|
|
|
|
self.component = component
|
|
|
|
let nativeIconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 44.0, height: 44.0) : CGSize(width: 28.0, height: 28.0)
|
|
let boundingIconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 38.0, height: 38.0) : CGSize(width: 24.0, height: 24.0)
|
|
|
|
let iconSize = (self.iconView.image?.size ?? nativeIconSize).aspectFitted(boundingIconSize)
|
|
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) / 2.0), y: floor((nativeIconSize.height - iconSize.height) / 2.0)), size: iconSize)
|
|
|
|
transition.setFrame(view: self.iconView, frame: iconFrame)
|
|
|
|
if itemEnvironment.isExpanded {
|
|
let titleView: ComponentView<Empty>
|
|
if let current = self.titleView {
|
|
titleView = current
|
|
} else {
|
|
titleView = ComponentView<Empty>()
|
|
self.titleView = titleView
|
|
}
|
|
let titleSize = titleView.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor))
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: 62.0, height: 100.0)
|
|
)
|
|
if let view = titleView.view {
|
|
if view.superview == nil {
|
|
view.alpha = 0.0
|
|
self.addSubview(view)
|
|
}
|
|
view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height), size: titleSize)
|
|
transition.setAlpha(view: view, alpha: 1.0)
|
|
}
|
|
} else if let titleView = self.titleView {
|
|
self.titleView = nil
|
|
if let view = titleView.view {
|
|
transition.setAlpha(view: view, alpha: 0.0, completion: { [weak view] _ in
|
|
view?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
final class EntityKeyboardTopPanelItemEnvironment: Equatable {
|
|
let isExpanded: Bool
|
|
|
|
init(isExpanded: Bool) {
|
|
self.isExpanded = isExpanded
|
|
}
|
|
|
|
static func ==(lhs: EntityKeyboardTopPanelItemEnvironment, rhs: EntityKeyboardTopPanelItemEnvironment) -> Bool {
|
|
if lhs.isExpanded != rhs.isExpanded {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
final class EntityKeyboardTopPanelComponent: Component {
|
|
typealias EnvironmentType = EntityKeyboardTopContainerPanelEnvironment
|
|
|
|
final class Item: Equatable {
|
|
let id: AnyHashable
|
|
let content: AnyComponent<EntityKeyboardTopPanelItemEnvironment>
|
|
|
|
init(id: AnyHashable, content: AnyComponent<EntityKeyboardTopPanelItemEnvironment>) {
|
|
self.id = id
|
|
self.content = content
|
|
}
|
|
|
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
|
if lhs.id != rhs.id {
|
|
return false
|
|
}
|
|
if lhs.content != rhs.content {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|
|
|
|
let theme: PresentationTheme
|
|
let items: [Item]
|
|
let defaultActiveItemId: AnyHashable?
|
|
let activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)>
|
|
|
|
init(
|
|
theme: PresentationTheme,
|
|
items: [Item],
|
|
defaultActiveItemId: AnyHashable? = nil,
|
|
activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)>
|
|
) {
|
|
self.theme = theme
|
|
self.items = items
|
|
self.defaultActiveItemId = defaultActiveItemId
|
|
self.activeContentItemIdUpdated = activeContentItemIdUpdated
|
|
}
|
|
|
|
static func ==(lhs: EntityKeyboardTopPanelComponent, rhs: EntityKeyboardTopPanelComponent) -> Bool {
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.items != rhs.items {
|
|
return false
|
|
}
|
|
if lhs.defaultActiveItemId != rhs.defaultActiveItemId {
|
|
return false
|
|
}
|
|
if lhs.activeContentItemIdUpdated !== rhs.activeContentItemIdUpdated {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
final class View: UIView, UIScrollViewDelegate {
|
|
private struct ItemLayout {
|
|
let sideInset: CGFloat = 7.0
|
|
let itemSize: CGSize
|
|
let innerItemSize: CGSize
|
|
let itemSpacing: CGFloat = 15.0
|
|
let itemCount: Int
|
|
let contentSize: CGSize
|
|
let isExpanded: Bool
|
|
|
|
init(itemCount: Int, isExpanded: Bool, height: CGFloat) {
|
|
self.isExpanded = isExpanded
|
|
self.itemSize = self.isExpanded ? CGSize(width: 54.0, height: 68.0) : CGSize(width: 32.0, height: 32.0)
|
|
self.innerItemSize = self.isExpanded ? CGSize(width: 50.0, height: 62.0) : CGSize(width: 28.0, height: 28.0)
|
|
self.itemCount = itemCount
|
|
self.contentSize = CGSize(width: sideInset * 2.0 + CGFloat(itemCount) * self.itemSize.width + CGFloat(max(0, itemCount - 1)) * itemSpacing, height: height)
|
|
}
|
|
|
|
func containerFrame(at index: Int) -> CGRect {
|
|
return CGRect(origin: CGPoint(x: sideInset + CGFloat(index) * (self.itemSize.width + self.itemSpacing), y: floor((self.contentSize.height - self.itemSize.height) / 2.0)), size: self.itemSize)
|
|
}
|
|
|
|
func contentFrame(containerFrame: CGRect) -> CGRect {
|
|
var frame = containerFrame
|
|
frame.origin.x += floor((self.itemSize.width - self.innerItemSize.width)) / 2.0
|
|
frame.origin.y += floor((self.itemSize.height - self.innerItemSize.height)) / 2.0
|
|
frame.size = self.innerItemSize
|
|
return frame
|
|
}
|
|
|
|
func contentFrame(at index: Int) -> CGRect {
|
|
return self.contentFrame(containerFrame: self.containerFrame(at: index))
|
|
}
|
|
|
|
func visibleItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) {
|
|
let offsetRect = rect.offsetBy(dx: -self.sideInset, dy: 0.0)
|
|
var minVisibleColumn = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize.width + self.itemSpacing)))
|
|
minVisibleColumn = max(0, minVisibleColumn)
|
|
let maxVisibleColumn = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize.width + self.itemSpacing)))
|
|
|
|
let minVisibleIndex = minVisibleColumn
|
|
let maxVisibleIndex = min(maxVisibleColumn, self.itemCount - 1)
|
|
|
|
return (minVisibleIndex, maxVisibleIndex)
|
|
}
|
|
}
|
|
|
|
private let scrollView: UIScrollView
|
|
private var itemViews: [AnyHashable: ComponentHostView<EntityKeyboardTopPanelItemEnvironment>] = [:]
|
|
private var highlightedIconBackgroundView: UIView
|
|
|
|
private var itemLayout: ItemLayout?
|
|
private var ignoreScrolling: Bool = false
|
|
|
|
private var isDragging: Bool = false
|
|
private var draggingStoppedTimer: SwiftSignalKit.Timer?
|
|
|
|
private var isExpanded: Bool = false
|
|
|
|
private var visibilityFraction: CGFloat = 1.0
|
|
|
|
private var activeContentItemId: AnyHashable?
|
|
|
|
private var component: EntityKeyboardTopPanelComponent?
|
|
private var environment: EntityKeyboardTopContainerPanelEnvironment?
|
|
|
|
override init(frame: CGRect) {
|
|
self.scrollView = UIScrollView()
|
|
|
|
self.highlightedIconBackgroundView = UIView()
|
|
self.highlightedIconBackgroundView.isUserInteractionEnabled = false
|
|
self.highlightedIconBackgroundView.layer.cornerRadius = 10.0
|
|
self.highlightedIconBackgroundView.clipsToBounds = true
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.scrollView.layer.anchorPoint = CGPoint()
|
|
self.scrollView.delaysContentTouches = false
|
|
self.scrollView.clipsToBounds = 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 = false
|
|
self.scrollView.showsHorizontalScrollIndicator = false
|
|
self.scrollView.delegate = self
|
|
self.addSubview(self.scrollView)
|
|
|
|
self.scrollView.addSubview(self.highlightedIconBackgroundView)
|
|
|
|
self.clipsToBounds = true
|
|
|
|
self.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return false
|
|
}
|
|
return strongSelf.scrollView.contentOffset.x > 0.0
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
if self.ignoreScrolling {
|
|
return
|
|
}
|
|
|
|
self.updateVisibleItems(attemptSynchronousLoads: false, transition: .immediate)
|
|
}
|
|
|
|
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
self.updateIsDragging(true)
|
|
}
|
|
|
|
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
if !decelerate {
|
|
self.updateIsDragging(false)
|
|
}
|
|
}
|
|
|
|
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
self.updateIsDragging(false)
|
|
}
|
|
|
|
private func updateIsDragging(_ isDragging: Bool) {
|
|
if !isDragging {
|
|
if !self.isDragging {
|
|
return
|
|
}
|
|
|
|
if self.draggingStoppedTimer == nil {
|
|
self.draggingStoppedTimer = SwiftSignalKit.Timer(timeout: 0.8, repeat: false, completion: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.draggingStoppedTimer = nil
|
|
strongSelf.isDragging = false
|
|
guard let environment = strongSelf.environment else {
|
|
return
|
|
}
|
|
environment.isExpandedUpdated(false, Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
|
}, queue: .mainQueue())
|
|
self.draggingStoppedTimer?.start()
|
|
}
|
|
} else {
|
|
self.draggingStoppedTimer?.invalidate()
|
|
self.draggingStoppedTimer = nil
|
|
|
|
if !self.isDragging {
|
|
self.isDragging = true
|
|
|
|
guard let environment = self.environment else {
|
|
return
|
|
}
|
|
environment.isExpandedUpdated(true, Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateVisibleItems(attemptSynchronousLoads: Bool, transition: Transition) {
|
|
guard let component = self.component, let itemLayout = self.itemLayout else {
|
|
return
|
|
}
|
|
|
|
var visibleBounds = self.scrollView.bounds
|
|
visibleBounds.size.width += 200.0
|
|
|
|
var validIds = Set<AnyHashable>()
|
|
let visibleItemRange = itemLayout.visibleItemRange(for: visibleBounds)
|
|
if !component.items.isEmpty && visibleItemRange.maxIndex >= visibleItemRange.minIndex {
|
|
for index in visibleItemRange.minIndex ... visibleItemRange.maxIndex {
|
|
let item = component.items[index]
|
|
validIds.insert(item.id)
|
|
|
|
var itemTransition = transition
|
|
let itemView: ComponentHostView<EntityKeyboardTopPanelItemEnvironment>
|
|
if let current = self.itemViews[item.id] {
|
|
itemView = current
|
|
} else {
|
|
itemTransition = .immediate
|
|
itemView = ComponentHostView<EntityKeyboardTopPanelItemEnvironment>()
|
|
self.scrollView.addSubview(itemView)
|
|
self.itemViews[item.id] = itemView
|
|
}
|
|
|
|
let itemOuterFrame = itemLayout.contentFrame(at: index)
|
|
let itemSize = itemView.update(
|
|
transition: itemTransition,
|
|
component: item.content,
|
|
environment: {
|
|
EntityKeyboardTopPanelItemEnvironment(isExpanded: itemLayout.isExpanded)
|
|
},
|
|
containerSize: itemOuterFrame.size
|
|
)
|
|
let itemFrame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize)
|
|
/*if index == visibleItemRange.minIndex, !itemTransition.animation.isImmediate {
|
|
print("\(index): \(itemView.frame) -> \(itemFrame)")
|
|
}*/
|
|
itemTransition.setFrame(view: itemView, frame: itemFrame)
|
|
}
|
|
}
|
|
var removedIds: [AnyHashable] = []
|
|
for (id, itemView) in self.itemViews {
|
|
if !validIds.contains(id) {
|
|
removedIds.append(id)
|
|
itemView.removeFromSuperview()
|
|
}
|
|
}
|
|
for id in removedIds {
|
|
self.itemViews.removeValue(forKey: id)
|
|
}
|
|
}
|
|
|
|
func update(component: EntityKeyboardTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
if self.component?.theme !== component.theme {
|
|
self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor
|
|
}
|
|
self.component = component
|
|
|
|
if let defaultActiveItemId = component.defaultActiveItemId {
|
|
self.activeContentItemId = defaultActiveItemId
|
|
}
|
|
|
|
let panelEnvironment = environment[EntityKeyboardTopContainerPanelEnvironment.self].value
|
|
self.environment = panelEnvironment
|
|
|
|
let isExpanded = availableSize.height > 41.0
|
|
let wasExpanded = self.isExpanded
|
|
self.isExpanded = isExpanded
|
|
|
|
let intrinsicHeight: CGFloat = availableSize.height
|
|
let height = intrinsicHeight
|
|
|
|
let previousItemLayout = self.itemLayout
|
|
let itemLayout = ItemLayout(itemCount: component.items.count, isExpanded: isExpanded, height: availableSize.height)
|
|
self.itemLayout = itemLayout
|
|
|
|
self.ignoreScrolling = true
|
|
|
|
var updatedBounds: CGRect?
|
|
if wasExpanded != isExpanded, let previousItemLayout = previousItemLayout {
|
|
var visibleBounds = self.scrollView.bounds
|
|
visibleBounds.size.width += 200.0
|
|
|
|
let previousVisibleRange = previousItemLayout.visibleItemRange(for: visibleBounds)
|
|
if previousVisibleRange.minIndex <= previousVisibleRange.maxIndex {
|
|
let previousItemFrame = previousItemLayout.containerFrame(at: previousVisibleRange.minIndex)
|
|
let updatedItemFrame = itemLayout.containerFrame(at: previousVisibleRange.minIndex)
|
|
|
|
let previousDistanceToItem = (previousItemFrame.minX - self.scrollView.bounds.minX)// / previousItemFrame.width
|
|
let newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - previousDistanceToItem/* * updatedItemFrame.width)*/, y: 0.0), size: availableSize)
|
|
updatedBounds = newBounds
|
|
|
|
var updatedVisibleBounds = newBounds
|
|
updatedVisibleBounds.size.width += 200.0
|
|
let updatedVisibleRange = itemLayout.visibleItemRange(for: updatedVisibleBounds)
|
|
|
|
let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.minX, y: previousItemFrame.minY), size: previousItemFrame.size)
|
|
for index in updatedVisibleRange.minIndex ..< updatedVisibleRange.maxIndex {
|
|
let indexDifference = index - previousVisibleRange.minIndex
|
|
if let itemView = self.itemViews[component.items[index].id] {
|
|
let itemContainerOriginX = baseFrame.minX + CGFloat(indexDifference) * (previousItemLayout.itemSize.width + previousItemLayout.itemSpacing)
|
|
let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerOriginX, y: baseFrame.minY), size: baseFrame.size)
|
|
let itemOuterFrame = previousItemLayout.contentFrame(containerFrame: itemContainerFrame)
|
|
|
|
let itemSize = itemView.bounds.size
|
|
itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if self.scrollView.contentSize != itemLayout.contentSize {
|
|
self.scrollView.contentSize = itemLayout.contentSize
|
|
}
|
|
if let updatedBounds = updatedBounds {
|
|
self.scrollView.bounds = updatedBounds
|
|
} else {
|
|
self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: availableSize)
|
|
}
|
|
self.ignoreScrolling = false
|
|
|
|
self.updateVisibleItems(attemptSynchronousLoads: !(self.scrollView.isDragging || self.scrollView.isDecelerating), transition: transition)
|
|
|
|
if let activeContentItemId = self.activeContentItemId {
|
|
if let index = component.items.firstIndex(where: { $0.id == activeContentItemId }) {
|
|
let itemFrame = itemLayout.containerFrame(at: index)
|
|
transition.setPosition(view: self.highlightedIconBackgroundView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY))
|
|
transition.setBounds(view: self.highlightedIconBackgroundView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
|
|
}
|
|
}
|
|
transition.setAlpha(view: self.highlightedIconBackgroundView, alpha: isExpanded ? 0.0 : 1.0)
|
|
|
|
panelEnvironment.visibilityFractionUpdated.connect { [weak self] (fraction, transition) in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.visibilityFractionUpdated(value: fraction, transition: transition)
|
|
}
|
|
|
|
component.activeContentItemIdUpdated.connect { [weak self] (itemId, transition) in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.activeContentItemIdUpdated(itemId: itemId, transition: transition)
|
|
}
|
|
|
|
return CGSize(width: availableSize.width, height: height)
|
|
}
|
|
|
|
private func visibilityFractionUpdated(value: CGFloat, transition: Transition) {
|
|
if self.visibilityFraction == value {
|
|
return
|
|
}
|
|
|
|
self.visibilityFraction = value
|
|
|
|
let scale = max(0.01, self.visibilityFraction)
|
|
|
|
transition.setScale(view: self.highlightedIconBackgroundView, scale: scale)
|
|
transition.setAlpha(view: self.highlightedIconBackgroundView, alpha: self.visibilityFraction)
|
|
|
|
for (_, itemView) in self.itemViews {
|
|
transition.setSublayerTransform(view: itemView, transform: CATransform3DMakeScale(scale, scale, 1.0))
|
|
transition.setAlpha(view: itemView, alpha: self.visibilityFraction)
|
|
}
|
|
}
|
|
|
|
private func activeContentItemIdUpdated(itemId: AnyHashable, transition: Transition) {
|
|
guard let component = self.component, let itemLayout = self.itemLayout else {
|
|
return
|
|
}
|
|
|
|
self.activeContentItemId = itemId
|
|
|
|
var found = false
|
|
for i in 0 ..< component.items.count {
|
|
if component.items[i].id == itemId {
|
|
found = true
|
|
self.highlightedIconBackgroundView.isHidden = false
|
|
let itemFrame = itemLayout.containerFrame(at: i)
|
|
transition.setPosition(view: self.highlightedIconBackgroundView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY))
|
|
transition.setBounds(view: self.highlightedIconBackgroundView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
|
|
|
|
self.scrollView.scrollRectToVisible(itemFrame.insetBy(dx: -6.0, dy: 0.0), animated: true)
|
|
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
self.highlightedIconBackgroundView.isHidden = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|