mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
Ongoing work on the updated entity input
This commit is contained in:
@@ -38,6 +38,7 @@ swift_library(
|
||||
"//submodules/PremiumUI:PremiumUI",
|
||||
"//submodules/StickerPackPreviewUI:StickerPackPreviewUI",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -230,7 +230,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
var width: CGFloat
|
||||
var containerInsets: UIEdgeInsets
|
||||
var itemGroupLayouts: [ItemGroupLayout]
|
||||
var itemSize: CGFloat
|
||||
var nativeItemSize: CGFloat
|
||||
let visibleItemSize: CGFloat
|
||||
var horizontalSpacing: CGFloat
|
||||
var verticalSpacing: CGFloat
|
||||
var verticalGroupSpacing: CGFloat
|
||||
@@ -241,14 +242,17 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.width = width
|
||||
self.containerInsets = containerInsets
|
||||
|
||||
let minItemsPerRow: Int
|
||||
let minSpacing: CGFloat
|
||||
switch itemLayoutType {
|
||||
case .compact:
|
||||
self.itemSize = 36.0
|
||||
minItemsPerRow = 8
|
||||
self.nativeItemSize = 36.0
|
||||
self.verticalSpacing = 9.0
|
||||
minSpacing = 9.0
|
||||
case .detailed:
|
||||
self.itemSize = 76.0
|
||||
minItemsPerRow = 5
|
||||
self.nativeItemSize = 76.0
|
||||
self.verticalSpacing = 2.0
|
||||
minSpacing = 2.0
|
||||
}
|
||||
@@ -257,8 +261,11 @@ public final class EmojiPagerContentComponent: Component {
|
||||
|
||||
let itemHorizontalSpace = width - self.containerInsets.left - self.containerInsets.right
|
||||
|
||||
self.itemsPerRow = Int((itemHorizontalSpace + minSpacing) / (self.itemSize + minSpacing))
|
||||
self.horizontalSpacing = floor((itemHorizontalSpace - self.itemSize * CGFloat(self.itemsPerRow)) / CGFloat(self.itemsPerRow - 1))
|
||||
self.itemsPerRow = max(minItemsPerRow, Int((itemHorizontalSpace + minSpacing) / (self.nativeItemSize + minSpacing)))
|
||||
|
||||
self.visibleItemSize = floor((itemHorizontalSpace - CGFloat(self.itemsPerRow - 1) * minSpacing) / CGFloat(self.itemsPerRow))
|
||||
|
||||
self.horizontalSpacing = floor((itemHorizontalSpace - self.visibleItemSize * CGFloat(self.itemsPerRow)) / CGFloat(self.itemsPerRow - 1))
|
||||
|
||||
var verticalGroupOrigin: CGFloat = self.containerInsets.top
|
||||
self.itemGroupLayouts = []
|
||||
@@ -269,7 +276,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
|
||||
let numRowsInGroup = (itemGroup.itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow
|
||||
let groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * self.itemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing)
|
||||
let groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * self.visibleItemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing)
|
||||
self.itemGroupLayouts.append(ItemGroupLayout(
|
||||
frame: CGRect(origin: CGPoint(x: 0.0, y: verticalGroupOrigin), size: groupContentSize),
|
||||
id: itemGroup.id,
|
||||
@@ -290,12 +297,12 @@ public final class EmojiPagerContentComponent: Component {
|
||||
|
||||
return CGRect(
|
||||
origin: CGPoint(
|
||||
x: self.containerInsets.left + CGFloat(column) * (self.itemSize + self.horizontalSpacing),
|
||||
y: groupLayout.frame.minY + groupLayout.itemTopOffset + CGFloat(row) * (self.itemSize + self.verticalSpacing)
|
||||
x: self.containerInsets.left + CGFloat(column) * (self.visibleItemSize + self.horizontalSpacing),
|
||||
y: groupLayout.frame.minY + groupLayout.itemTopOffset + CGFloat(row) * (self.visibleItemSize + self.verticalSpacing)
|
||||
),
|
||||
size: CGSize(
|
||||
width: self.itemSize,
|
||||
height: self.itemSize
|
||||
width: self.visibleItemSize,
|
||||
height: self.visibleItemSize
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -310,9 +317,9 @@ public final class EmojiPagerContentComponent: Component {
|
||||
continue
|
||||
}
|
||||
let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -group.frame.minY - group.itemTopOffset)
|
||||
var minVisibleRow = Int(floor((offsetRect.minY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing)))
|
||||
var minVisibleRow = Int(floor((offsetRect.minY - self.verticalSpacing) / (self.visibleItemSize + self.verticalSpacing)))
|
||||
minVisibleRow = max(0, minVisibleRow)
|
||||
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing)))
|
||||
let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.visibleItemSize + self.verticalSpacing)))
|
||||
|
||||
let minVisibleIndex = minVisibleRow * self.itemsPerRow
|
||||
let maxVisibleIndex = min(group.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1)
|
||||
@@ -330,6 +337,60 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
final class ItemPlaceholderView: UIView {
|
||||
private let shimmerView: PortalSourceView?
|
||||
private var placeholderView: PortalView?
|
||||
private let placeholderMaskLayer: SimpleLayer
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
file: TelegramMediaFile,
|
||||
shimmerView: PortalSourceView?,
|
||||
color: UIColor?,
|
||||
size: CGSize
|
||||
) {
|
||||
self.shimmerView = shimmerView
|
||||
self.placeholderView = PortalView()
|
||||
self.placeholderMaskLayer = SimpleLayer()
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
if let placeholderView = self.placeholderView, let shimmerView = self.shimmerView {
|
||||
placeholderView.view.clipsToBounds = true
|
||||
placeholderView.view.layer.mask = self.placeholderMaskLayer
|
||||
self.addSubview(placeholderView.view)
|
||||
shimmerView.addPortal(view: placeholderView)
|
||||
}
|
||||
|
||||
Queue.concurrentDefaultQueue().async { [weak self] in
|
||||
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: color ?? .black) {
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let _ = color {
|
||||
strongSelf.layer.contents = image.cgImage
|
||||
} else {
|
||||
strongSelf.placeholderMaskLayer.contents = image.cgImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(size: CGSize) {
|
||||
if let placeholderView = self.placeholderView {
|
||||
placeholderView.view.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
self.placeholderMaskLayer.frame = CGRect(origin: CGPoint(), size: size)
|
||||
}
|
||||
}
|
||||
|
||||
final class ItemLayer: MultiAnimationRenderTarget {
|
||||
struct Key: Hashable {
|
||||
var groupId: AnyHashable
|
||||
@@ -353,7 +414,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
private var displayPlaceholder: Bool = false
|
||||
private(set) var displayPlaceholder: Bool = false
|
||||
let onUpdateDisplayPlaceholder: (Bool) -> Void
|
||||
|
||||
init(
|
||||
item: Item,
|
||||
@@ -366,11 +428,13 @@ public final class EmojiPagerContentComponent: Component {
|
||||
placeholderColor: UIColor,
|
||||
blurredBadgeColor: UIColor,
|
||||
displayPremiumBadgeIfAvailable: Bool,
|
||||
pointSize: CGSize
|
||||
pointSize: CGSize,
|
||||
onUpdateDisplayPlaceholder: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.item = item
|
||||
self.file = file
|
||||
self.placeholderColor = placeholderColor
|
||||
self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder
|
||||
|
||||
let scale = min(2.0, UIScreenScale)
|
||||
let pixelSize = CGSize(width: pointSize.width * scale, height: pointSize.height * scale)
|
||||
@@ -414,17 +478,21 @@ public final class EmojiPagerContentComponent: Component {
|
||||
|
||||
if attemptSynchronousLoad {
|
||||
if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) {
|
||||
self.displayPlaceholder = true
|
||||
|
||||
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) {
|
||||
self.contents = image.cgImage
|
||||
}
|
||||
self.updateDisplayPlaceholder(displayPlaceholder: true)
|
||||
}
|
||||
|
||||
loadAnimation()
|
||||
} else {
|
||||
let _ = renderer.loadFirstFrame(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { _ in
|
||||
let _ = renderer.loadFirstFrame(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { [weak self] success in
|
||||
loadAnimation()
|
||||
|
||||
if !success {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.updateDisplayPlaceholder(displayPlaceholder: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if let dimensions = file.dimensions {
|
||||
@@ -458,7 +526,19 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
|
||||
override public init(layer: Any) {
|
||||
preconditionFailure()
|
||||
guard let layer = layer as? ItemLayer else {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
self.item = layer.item
|
||||
|
||||
self.file = layer.file
|
||||
self.placeholderColor = layer.placeholderColor
|
||||
self.size = layer.size
|
||||
|
||||
self.onUpdateDisplayPlaceholder = { _ in }
|
||||
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
@@ -492,34 +572,65 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
|
||||
self.displayPlaceholder = displayPlaceholder
|
||||
let file = self.file
|
||||
let size = self.size
|
||||
let placeholderColor = self.placeholderColor
|
||||
self.onUpdateDisplayPlaceholder(displayPlaceholder)
|
||||
|
||||
Queue.concurrentDefaultQueue().async { [weak self] in
|
||||
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) {
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if strongSelf.displayPlaceholder {
|
||||
strongSelf.contents = image.cgImage
|
||||
/*if displayPlaceholder {
|
||||
if self.placeholderView == nil {
|
||||
self.placeholderView = PortalView()
|
||||
if let placeholderView = self.placeholderView, let shimmerView = self.shimmerView {
|
||||
self.addSublayer(placeholderView.view.layer)
|
||||
placeholderView.view.frame = self.bounds
|
||||
shimmerView.addPortal(view: placeholderView)
|
||||
}
|
||||
}
|
||||
if self.placeholderMaskLayer == nil {
|
||||
self.placeholderMaskLayer = SimpleLayer()
|
||||
self.placeholderView?.view.layer.mask = self.placeholderMaskLayer
|
||||
}
|
||||
let file = self.file
|
||||
let size = self.size
|
||||
//let placeholderColor = self.placeholderColor
|
||||
|
||||
Queue.concurrentDefaultQueue().async { [weak self] in
|
||||
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: .black) {
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if strongSelf.displayPlaceholder {
|
||||
strongSelf.placeholderMaskLayer?.contents = image.cgImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let placeholderView = self.placeholderView {
|
||||
self.placeholderView = nil
|
||||
placeholderView.view.layer.removeFromSuperlayer()
|
||||
}
|
||||
if let _ = self.placeholderMaskLayer {
|
||||
self.placeholderMaskLayer = nil
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
private final class ContentScrollView: UIScrollView, PagerExpandableScrollView {
|
||||
}
|
||||
|
||||
private let scrollView: ContentScrollView
|
||||
private let shimmerHostView: PortalSourceView
|
||||
private let standaloneShimmerEffect: StandaloneShimmerEffect
|
||||
|
||||
private let scrollView: ContentScrollView
|
||||
private let boundsChangeTrackerLayer = SimpleLayer()
|
||||
private var effectiveVisibleSize: CGSize = CGSize()
|
||||
|
||||
private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:]
|
||||
private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:]
|
||||
private var visibleGroupHeaders: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var keepTopPanelVisibleUntilScrollingInput: Bool = false
|
||||
|
||||
private var component: EmojiPagerContentComponent?
|
||||
private var pagerEnvironment: PagerComponentChildEnvironment?
|
||||
@@ -532,10 +643,24 @@ public final class EmojiPagerContentComponent: Component {
|
||||
private weak var peekController: PeekController?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.shimmerHostView = PortalSourceView()
|
||||
|
||||
self.standaloneShimmerEffect = StandaloneShimmerEffect()
|
||||
|
||||
self.scrollView = ContentScrollView()
|
||||
self.scrollView.layer.anchorPoint = CGPoint()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.shimmerHostView.alpha = 0.0
|
||||
self.addSubview(self.shimmerHostView)
|
||||
|
||||
self.boundsChangeTrackerLayer.opacity = 0.0
|
||||
self.layer.addSublayer(self.boundsChangeTrackerLayer)
|
||||
self.boundsChangeTrackerLayer.didEnterHierarchy = { [weak self] in
|
||||
self?.standaloneShimmerEffect.updateLayer()
|
||||
}
|
||||
|
||||
self.scrollView.delaysContentTouches = false
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
@@ -546,8 +671,11 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.clipsToBounds = false
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
//self.clipsToBounds = true
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
|
||||
/*self.useSublayerTransformForActivation = false
|
||||
@@ -747,7 +875,9 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.scrollView.setContentOffset(self.scrollView.contentOffset, animated: false)
|
||||
self.ignoreScrolling = wasIgnoringScrollingEvents
|
||||
|
||||
self.scrollView.scrollRectToVisible(CGRect(origin: group.frame.origin.offsetBy(dx: 0.0, dy: floor(-itemLayout.verticalGroupSpacing / 2.0)), size: CGSize(width: 1.0, height: self.scrollView.bounds.height)), animated: true)
|
||||
self.keepTopPanelVisibleUntilScrollingInput = true
|
||||
|
||||
self.scrollView.scrollRectToVisible(CGRect(origin: group.frame.origin.offsetBy(dx: 0.0, dy: floor(-itemLayout.verticalGroupSpacing / 2.0) - 41.0), size: CGSize(width: 1.0, height: self.scrollView.bounds.height)), animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -780,6 +910,11 @@ public final class EmojiPagerContentComponent: Component {
|
||||
private var previousScrollingOffset: ScrollingOffsetState?
|
||||
|
||||
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
if self.keepTopPanelVisibleUntilScrollingInput {
|
||||
self.keepTopPanelVisibleUntilScrollingInput = false
|
||||
|
||||
self.updateScrollingOffset(isReset: true, transition: .immediate)
|
||||
}
|
||||
if let presentation = scrollView.layer.presentation() {
|
||||
scrollView.bounds = presentation.bounds
|
||||
scrollView.layer.removeAllAnimations()
|
||||
@@ -793,7 +928,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
|
||||
self.updateVisibleItems(attemptSynchronousLoads: false)
|
||||
|
||||
self.updateScrollingOffset(transition: .immediate)
|
||||
self.updateScrollingOffset(isReset: false, transition: .immediate)
|
||||
}
|
||||
|
||||
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
@@ -812,21 +947,28 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.snapScrollingOffsetToInsets()
|
||||
}
|
||||
|
||||
private func updateScrollingOffset(transition: Transition) {
|
||||
private func updateScrollingOffset(isReset: Bool, transition: Transition) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
let isInteracting = scrollView.isDragging || scrollView.isDecelerating
|
||||
if let previousScrollingOffsetValue = self.previousScrollingOffset {
|
||||
if let previousScrollingOffsetValue = self.previousScrollingOffset, !self.keepTopPanelVisibleUntilScrollingInput {
|
||||
let currentBounds = scrollView.bounds
|
||||
let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0)
|
||||
let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY)
|
||||
|
||||
let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue.value
|
||||
self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate(
|
||||
relativeOffset: relativeOffset,
|
||||
absoluteOffsetToTopEdge: offsetToTopEdge,
|
||||
absoluteOffsetToBottomEdge: offsetToBottomEdge,
|
||||
isInteracting: isInteracting,
|
||||
transition: transition
|
||||
))
|
||||
if case .detailed = component.itemLayoutType {
|
||||
self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate(
|
||||
relativeOffset: relativeOffset,
|
||||
absoluteOffsetToTopEdge: offsetToTopEdge,
|
||||
absoluteOffsetToBottomEdge: offsetToBottomEdge,
|
||||
isReset: isReset,
|
||||
isInteracting: isInteracting,
|
||||
transition: transition
|
||||
))
|
||||
}
|
||||
}
|
||||
self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting)
|
||||
}
|
||||
@@ -855,7 +997,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
currentBounds.origin.y = self.snappedContentOffset(proposedOffset: currentBounds.minY)
|
||||
transition.setBounds(view: self.scrollView, bounds: currentBounds)
|
||||
|
||||
self.updateScrollingOffset(transition: transition)
|
||||
self.updateScrollingOffset(isReset: false, transition: transition)
|
||||
}
|
||||
|
||||
private func updateVisibleItems(attemptSynchronousLoads: Bool) {
|
||||
@@ -868,14 +1010,17 @@ public final class EmojiPagerContentComponent: Component {
|
||||
var validIds = Set<ItemLayer.Key>()
|
||||
var validGroupHeaderIds = Set<AnyHashable>()
|
||||
|
||||
for groupItems in itemLayout.visibleItems(for: self.scrollView.bounds) {
|
||||
if topVisibleGroupId == nil {
|
||||
topVisibleGroupId = groupItems.id
|
||||
}
|
||||
|
||||
let effectiveVisibleBounds = CGRect(origin: self.scrollView.bounds.origin, size: self.effectiveVisibleSize)
|
||||
let topVisibleDetectionBounds = effectiveVisibleBounds.offsetBy(dx: 0.0, dy: 41.0)
|
||||
|
||||
for groupItems in itemLayout.visibleItems(for: effectiveVisibleBounds) {
|
||||
let itemGroup = component.itemGroups[groupItems.groupIndex]
|
||||
let itemGroupLayout = itemLayout.itemGroupLayouts[groupItems.groupIndex]
|
||||
|
||||
if topVisibleGroupId == nil && itemGroupLayout.frame.intersects(topVisibleDetectionBounds) {
|
||||
topVisibleGroupId = groupItems.id
|
||||
}
|
||||
|
||||
if let title = itemGroup.title {
|
||||
validGroupHeaderIds.insert(itemGroup.id)
|
||||
let groupHeaderView: ComponentView<Empty>
|
||||
@@ -888,7 +1033,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
let groupHeaderSize = groupHeaderView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: title, font: Font.medium(12.0), color: theme.chat.inputMediaPanel.stickersSectionTextColor
|
||||
text: title.uppercased(), font: Font.medium(12.0), color: theme.chat.inputMediaPanel.stickersSectionTextColor
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemLayout.contentSize.width - itemLayout.containerInsets.left - itemLayout.containerInsets.right, height: 100.0)
|
||||
@@ -906,14 +1051,21 @@ public final class EmojiPagerContentComponent: Component {
|
||||
let itemId = ItemLayer.Key(groupId: itemGroup.id, fileId: item.file.fileId)
|
||||
validIds.insert(itemId)
|
||||
|
||||
let itemDimensions = item.file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0)
|
||||
let itemNativeFitSize = itemDimensions.fitted(CGSize(width: itemLayout.nativeItemSize, height: itemLayout.nativeItemSize))
|
||||
let itemVisibleFitSize = itemDimensions.fitted(CGSize(width: itemLayout.visibleItemSize, height: itemLayout.visibleItemSize))
|
||||
|
||||
var updateItemLayerPlaceholder = false
|
||||
let itemLayer: ItemLayer
|
||||
if let current = self.visibleItemLayers[itemId] {
|
||||
itemLayer = current
|
||||
} else {
|
||||
updateItemLayerPlaceholder = true
|
||||
|
||||
itemLayer = ItemLayer(
|
||||
item: item,
|
||||
context: component.context,
|
||||
groupId: "keyboard-\(Int(itemLayout.itemSize))",
|
||||
groupId: "keyboard-\(Int(itemLayout.nativeItemSize))",
|
||||
attemptSynchronousLoad: attemptSynchronousLoads,
|
||||
file: item.file,
|
||||
cache: component.animationCache,
|
||||
@@ -921,15 +1073,72 @@ public final class EmojiPagerContentComponent: Component {
|
||||
placeholderColor: theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1),
|
||||
blurredBadgeColor: theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(0.5),
|
||||
displayPremiumBadgeIfAvailable: true,
|
||||
pointSize: CGSize(width: itemLayout.itemSize, height: itemLayout.itemSize)
|
||||
pointSize: itemNativeFitSize,
|
||||
onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if displayPlaceholder {
|
||||
if let itemLayer = strongSelf.visibleItemLayers[itemId] {
|
||||
let placeholderView: ItemPlaceholderView
|
||||
if let current = strongSelf.visibleItemPlaceholderViews[itemId] {
|
||||
placeholderView = current
|
||||
} else {
|
||||
placeholderView = ItemPlaceholderView(
|
||||
context: component.context,
|
||||
file: item.file,
|
||||
shimmerView: strongSelf.shimmerHostView,
|
||||
color: nil,
|
||||
size: itemNativeFitSize
|
||||
)
|
||||
strongSelf.visibleItemPlaceholderViews[itemId] = placeholderView
|
||||
strongSelf.scrollView.insertSubview(placeholderView, at: 0)
|
||||
}
|
||||
placeholderView.frame = itemLayer.frame
|
||||
placeholderView.update(size: placeholderView.bounds.size)
|
||||
|
||||
strongSelf.updateShimmerIfNeeded()
|
||||
}
|
||||
} else {
|
||||
if let placeholderView = strongSelf.visibleItemPlaceholderViews[itemId] {
|
||||
strongSelf.visibleItemPlaceholderViews.removeValue(forKey: itemId)
|
||||
placeholderView.removeFromSuperview()
|
||||
|
||||
strongSelf.updateShimmerIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
self.scrollView.layer.addSublayer(itemLayer)
|
||||
self.visibleItemLayers[itemId] = itemLayer
|
||||
}
|
||||
|
||||
let itemFrame = itemLayout.frame(groupIndex: groupItems.groupIndex, itemIndex: index)
|
||||
itemLayer.position = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
|
||||
itemLayer.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
|
||||
var itemFrame = itemLayout.frame(groupIndex: groupItems.groupIndex, itemIndex: index)
|
||||
|
||||
itemFrame.origin.x += floor((itemFrame.width - itemVisibleFitSize.width) / 2.0)
|
||||
itemFrame.origin.y += floor((itemFrame.height - itemVisibleFitSize.height) / 2.0)
|
||||
itemFrame.size = itemVisibleFitSize
|
||||
|
||||
let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
|
||||
let itemBounds = CGRect(origin: CGPoint(), size: itemFrame.size)
|
||||
if itemLayer.position != itemPosition {
|
||||
itemLayer.position = itemPosition
|
||||
}
|
||||
if itemLayer.bounds != itemBounds {
|
||||
itemLayer.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
|
||||
}
|
||||
|
||||
if let placeholderView = self.visibleItemPlaceholderViews[itemId] {
|
||||
if placeholderView.layer.position != itemPosition || placeholderView.layer.bounds != itemBounds {
|
||||
placeholderView.frame = itemFrame
|
||||
placeholderView.update(size: itemFrame.size)
|
||||
}
|
||||
} else if updateItemLayerPlaceholder {
|
||||
if itemLayer.displayPlaceholder {
|
||||
itemLayer.onUpdateDisplayPlaceholder(true)
|
||||
}
|
||||
}
|
||||
|
||||
itemLayer.isVisibleForAnimations = true
|
||||
}
|
||||
}
|
||||
@@ -943,6 +1152,10 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
for id in removedIds {
|
||||
self.visibleItemLayers.removeValue(forKey: id)
|
||||
|
||||
if let view = self.visibleItemPlaceholderViews.removeValue(forKey: id) {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
var removedGroupHeaderIds: [AnyHashable] = []
|
||||
@@ -961,14 +1174,31 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateShimmerIfNeeded() {
|
||||
if self.visibleItemPlaceholderViews.isEmpty {
|
||||
self.standaloneShimmerEffect.layer = nil
|
||||
} else {
|
||||
self.standaloneShimmerEffect.layer = self.shimmerHostView.layer
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: EmojiPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
self.theme = environment[EntityKeyboardChildEnvironment.self].value.theme
|
||||
self.activeItemUpdated = environment[EntityKeyboardChildEnvironment.self].value.getContentActiveItemUpdated(component.id)
|
||||
|
||||
let keyboardChildEnvironment = environment[EntityKeyboardChildEnvironment.self].value
|
||||
|
||||
self.theme = keyboardChildEnvironment.theme
|
||||
self.activeItemUpdated = keyboardChildEnvironment.getContentActiveItemUpdated(component.id)
|
||||
|
||||
let pagerEnvironment = environment[PagerComponentChildEnvironment.self].value
|
||||
self.pagerEnvironment = pagerEnvironment
|
||||
|
||||
transition.setFrame(view: self.shimmerHostView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
let shimmerBackgroundColor = keyboardChildEnvironment.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.08)
|
||||
let shimmerForegroundColor = keyboardChildEnvironment.theme.list.itemBlocksBackgroundColor.withMultipliedAlpha(0.15)
|
||||
self.standaloneShimmerEffect.update(background: shimmerBackgroundColor, foreground: shimmerForegroundColor)
|
||||
|
||||
var itemGroups: [ItemGroupDescription] = []
|
||||
for itemGroup in component.itemGroups {
|
||||
itemGroups.append(ItemGroupDescription(
|
||||
@@ -982,7 +1212,28 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
self.ignoreScrolling = true
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
transition.setPosition(view: self.scrollView, position: CGPoint())
|
||||
let previousSize = self.scrollView.bounds.size
|
||||
self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: availableSize)
|
||||
|
||||
if availableSize.height > previousSize.height || transition.animation.isImmediate {
|
||||
self.boundsChangeTrackerLayer.removeAllAnimations()
|
||||
self.boundsChangeTrackerLayer.bounds = self.scrollView.bounds
|
||||
self.effectiveVisibleSize = self.scrollView.bounds.size
|
||||
} else {
|
||||
self.effectiveVisibleSize = CGSize(width: availableSize.width, height: max(self.effectiveVisibleSize.height, availableSize.height))
|
||||
transition.setBounds(layer: self.boundsChangeTrackerLayer, bounds: self.scrollView.bounds, completion: { [weak self] completed in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let effectiveVisibleSize = strongSelf.scrollView.bounds.size
|
||||
if strongSelf.effectiveVisibleSize != effectiveVisibleSize {
|
||||
strongSelf.effectiveVisibleSize = effectiveVisibleSize
|
||||
strongSelf.updateVisibleItems(attemptSynchronousLoads: false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if self.scrollView.contentSize != itemLayout.contentSize {
|
||||
self.scrollView.contentSize = itemLayout.contentSize
|
||||
}
|
||||
@@ -992,7 +1243,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: scrollView.isDragging || scrollView.isDecelerating)
|
||||
self.ignoreScrolling = false
|
||||
|
||||
self.updateVisibleItems(attemptSynchronousLoads: true)
|
||||
self.updateVisibleItems(attemptSynchronousLoads: !(scrollView.isDragging || scrollView.isDecelerating))
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
||||
@@ -47,11 +47,14 @@ public final class EntityKeyboardComponent: Component {
|
||||
public let stickerContent: EmojiPagerContentComponent
|
||||
public let gifContent: GifPagerContentComponent
|
||||
public let defaultToEmojiTab: Bool
|
||||
public let externalTopPanelContainer: UIView?
|
||||
public let externalTopPanelContainer: PagerExternalTopPanelContainer?
|
||||
public let topPanelExtensionUpdated: (CGFloat, Transition) -> Void
|
||||
public let hideInputUpdated: (Bool, Transition) -> Void
|
||||
public let hideInputUpdated: (Bool, Bool, Transition) -> Void
|
||||
public let switchToTextInput: () -> Void
|
||||
public let makeSearchContainerNode: (EntitySearchContentType) -> EntitySearchContainerNode
|
||||
public let deviceMetrics: DeviceMetrics
|
||||
public let hiddenInputHeight: CGFloat
|
||||
public let isExpanded: Bool
|
||||
|
||||
public init(
|
||||
theme: PresentationTheme,
|
||||
@@ -60,11 +63,14 @@ public final class EntityKeyboardComponent: Component {
|
||||
stickerContent: EmojiPagerContentComponent,
|
||||
gifContent: GifPagerContentComponent,
|
||||
defaultToEmojiTab: Bool,
|
||||
externalTopPanelContainer: UIView?,
|
||||
externalTopPanelContainer: PagerExternalTopPanelContainer?,
|
||||
topPanelExtensionUpdated: @escaping (CGFloat, Transition) -> Void,
|
||||
hideInputUpdated: @escaping (Bool, Transition) -> Void,
|
||||
hideInputUpdated: @escaping (Bool, Bool, Transition) -> Void,
|
||||
switchToTextInput: @escaping () -> Void,
|
||||
makeSearchContainerNode: @escaping (EntitySearchContentType) -> EntitySearchContainerNode,
|
||||
deviceMetrics: DeviceMetrics
|
||||
deviceMetrics: DeviceMetrics,
|
||||
hiddenInputHeight: CGFloat,
|
||||
isExpanded: Bool
|
||||
) {
|
||||
self.theme = theme
|
||||
self.bottomInset = bottomInset
|
||||
@@ -75,8 +81,11 @@ public final class EntityKeyboardComponent: Component {
|
||||
self.externalTopPanelContainer = externalTopPanelContainer
|
||||
self.topPanelExtensionUpdated = topPanelExtensionUpdated
|
||||
self.hideInputUpdated = hideInputUpdated
|
||||
self.switchToTextInput = switchToTextInput
|
||||
self.makeSearchContainerNode = makeSearchContainerNode
|
||||
self.deviceMetrics = deviceMetrics
|
||||
self.hiddenInputHeight = hiddenInputHeight
|
||||
self.isExpanded = isExpanded
|
||||
}
|
||||
|
||||
public static func ==(lhs: EntityKeyboardComponent, rhs: EntityKeyboardComponent) -> Bool {
|
||||
@@ -104,6 +113,12 @@ public final class EntityKeyboardComponent: Component {
|
||||
if lhs.deviceMetrics != rhs.deviceMetrics {
|
||||
return false
|
||||
}
|
||||
if lhs.hiddenInputHeight != rhs.hiddenInputHeight {
|
||||
return false
|
||||
}
|
||||
if lhs.isExpanded != rhs.isExpanded {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -118,13 +133,14 @@ public final class EntityKeyboardComponent: Component {
|
||||
private var searchComponent: EntitySearchContentComponent?
|
||||
|
||||
private var topPanelExtension: CGFloat?
|
||||
private var isTopPanelExpanded: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.pagerView = ComponentHostView<EntityKeyboardChildEnvironment>()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
//self.clipsToBounds = true
|
||||
self.disablesInteractiveTransitionGestureRecognizer = true
|
||||
|
||||
self.addSubview(self.pagerView)
|
||||
@@ -146,7 +162,8 @@ public final class EntityKeyboardComponent: Component {
|
||||
let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>()
|
||||
contents.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(component.gifContent)))
|
||||
var topGifItems: [EntityKeyboardTopPanelComponent.Item] = []
|
||||
topGifItems.append(EntityKeyboardTopPanelComponent.Item(
|
||||
topGifItems.removeAll()
|
||||
/*topGifItems.append(EntityKeyboardTopPanelComponent.Item(
|
||||
id: "recent",
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Media/RecentTabIcon",
|
||||
@@ -161,7 +178,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||
maxSize: CGSize(width: 30.0, height: 30.0))
|
||||
)
|
||||
))
|
||||
))*/
|
||||
contentTopPanels.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(EntityKeyboardTopPanelComponent(
|
||||
theme: component.theme,
|
||||
items: topGifItems,
|
||||
@@ -199,20 +216,21 @@ public final class EntityKeyboardComponent: Component {
|
||||
"recent": "Chat/Input/Media/RecentTabIcon",
|
||||
"premium": "Chat/Input/Media/PremiumIcon"
|
||||
]
|
||||
if let iconName = iconMapping[id] {
|
||||
let titleMapping: [String: String] = [
|
||||
"recent": "Recent",
|
||||
"premium": "Premium"
|
||||
]
|
||||
if let iconName = iconMapping[id], let title = titleMapping[id] {
|
||||
topStickerItems.append(EntityKeyboardTopPanelComponent.Item(
|
||||
id: itemGroup.id,
|
||||
content: AnyComponent(Button(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: iconName,
|
||||
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||
maxSize: CGSize(width: 30.0, height: 30.0)
|
||||
)),
|
||||
action: { [weak self] in
|
||||
content: AnyComponent(EntityKeyboardIconTopPanelComponent(
|
||||
imageName: iconName,
|
||||
theme: component.theme,
|
||||
title: title,
|
||||
pressed: { [weak self] in
|
||||
self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.id)
|
||||
}
|
||||
).minSize(CGSize(width: 30.0, height: 30.0))
|
||||
)
|
||||
))
|
||||
))
|
||||
}
|
||||
} else {
|
||||
@@ -224,6 +242,8 @@ public final class EntityKeyboardComponent: Component {
|
||||
file: itemGroup.items[0].file,
|
||||
animationCache: component.stickerContent.animationCache,
|
||||
animationRenderer: component.stickerContent.animationRenderer,
|
||||
theme: component.theme,
|
||||
title: itemGroup.title ?? "",
|
||||
pressed: { [weak self] in
|
||||
self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.id)
|
||||
}
|
||||
@@ -277,6 +297,8 @@ public final class EntityKeyboardComponent: Component {
|
||||
file: itemGroup.items[0].file,
|
||||
animationCache: component.emojiContent.animationCache,
|
||||
animationRenderer: component.emojiContent.animationRenderer,
|
||||
theme: component.theme,
|
||||
title: itemGroup.title ?? "",
|
||||
pressed: { [weak self] in
|
||||
self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.id)
|
||||
}
|
||||
@@ -294,6 +316,20 @@ public final class EntityKeyboardComponent: Component {
|
||||
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||
maxSize: nil
|
||||
))))
|
||||
contentAccessoryLeftButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Media/EntityInputGlobeIcon",
|
||||
tintColor: component.theme.chat.inputMediaPanel.panelIconColor,
|
||||
maxSize: nil
|
||||
)),
|
||||
action: { [weak self] in
|
||||
guard let strongSelf = self, let component = strongSelf.component else {
|
||||
return
|
||||
}
|
||||
component.switchToTextInput()
|
||||
}
|
||||
).minSize(CGSize(width: 38.0, height: 38.0)))))
|
||||
let deleteBackwards = component.emojiContent.inputInteraction.deleteBackwards
|
||||
contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button(
|
||||
content: AnyComponent(BundleIconComponent(
|
||||
name: "Chat/Input/Media/EntityInputClearIcon",
|
||||
@@ -301,9 +337,20 @@ public final class EntityKeyboardComponent: Component {
|
||||
maxSize: nil
|
||||
)),
|
||||
action: {
|
||||
component.emojiContent.inputInteraction.deleteBackwards()
|
||||
deleteBackwards()
|
||||
}
|
||||
).minSize(CGSize(width: 38.0, height: 38.0)))))
|
||||
).withHoldAction({
|
||||
deleteBackwards()
|
||||
}).minSize(CGSize(width: 38.0, height: 38.0)))))
|
||||
|
||||
let panelHideBehavior: PagerComponentPanelHideBehavior
|
||||
if self.searchComponent != nil {
|
||||
panelHideBehavior = .hide
|
||||
} else if component.isExpanded {
|
||||
panelHideBehavior = .show
|
||||
} else {
|
||||
panelHideBehavior = .hideOnScroll
|
||||
}
|
||||
|
||||
let pagerSize = self.pagerView.update(
|
||||
transition: transition,
|
||||
@@ -319,7 +366,8 @@ public final class EntityKeyboardComponent: Component {
|
||||
color: component.theme.chat.inputMediaPanel.stickersBackgroundColor.withMultipliedAlpha(0.75)
|
||||
)),
|
||||
topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent(
|
||||
theme: component.theme
|
||||
theme: component.theme,
|
||||
overflowHeight: component.hiddenInputHeight
|
||||
)),
|
||||
externalTopPanelContainer: component.externalTopPanelContainer,
|
||||
bottomPanel: AnyComponent(EntityKeyboardBottomPanelComponent(
|
||||
@@ -335,7 +383,13 @@ public final class EntityKeyboardComponent: Component {
|
||||
}
|
||||
strongSelf.topPanelExtensionUpdated(height: panelState.topPanelHeight, transition: transition)
|
||||
},
|
||||
hidePanels: self.searchComponent != nil
|
||||
isTopPanelExpandedUpdated: { [weak self] isExpanded, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.isTopPanelExpandedUpdated(isExpanded: isExpanded, transition: transition)
|
||||
},
|
||||
panelHideBehavior: panelHideBehavior
|
||||
)),
|
||||
environment: {
|
||||
EntityKeyboardChildEnvironment(
|
||||
@@ -426,6 +480,18 @@ public final class EntityKeyboardComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
private func isTopPanelExpandedUpdated(isExpanded: Bool, transition: Transition) {
|
||||
if self.isTopPanelExpanded != isExpanded {
|
||||
self.isTopPanelExpanded = isExpanded
|
||||
}
|
||||
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
component.hideInputUpdated(self.isTopPanelExpanded, false, transition)
|
||||
}
|
||||
|
||||
private func openSearch() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
@@ -451,7 +517,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
}
|
||||
)
|
||||
//self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||
component.hideInputUpdated(true, Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||
component.hideInputUpdated(true, true, Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,7 +530,7 @@ public final class EntityKeyboardComponent: Component {
|
||||
}
|
||||
self.searchComponent = nil
|
||||
//self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
component.hideInputUpdated(false, Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
component.hideInputUpdated(false, false, Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
}
|
||||
|
||||
private func scrollToItemGroup(contentId: String, groupId: AnyHashable) {
|
||||
|
||||
@@ -9,11 +9,14 @@ import Postbox
|
||||
|
||||
final class EntityKeyboardTopContainerPanelEnvironment: Equatable {
|
||||
let visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)>
|
||||
let isExpandedUpdated: (Bool, Transition) -> Void
|
||||
|
||||
init(
|
||||
visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)>
|
||||
visibilityFractionUpdated: ActionSlot<(CGFloat, Transition)>,
|
||||
isExpandedUpdated: @escaping (Bool, Transition) -> Void
|
||||
) {
|
||||
self.visibilityFractionUpdated = visibilityFractionUpdated
|
||||
self.isExpandedUpdated = isExpandedUpdated
|
||||
}
|
||||
|
||||
static func ==(lhs: EntityKeyboardTopContainerPanelEnvironment, rhs: EntityKeyboardTopContainerPanelEnvironment) -> Bool {
|
||||
@@ -28,17 +31,23 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
|
||||
typealias EnvironmentType = PagerComponentPanelEnvironment<EntityKeyboardTopContainerPanelEnvironment>
|
||||
|
||||
let theme: PresentationTheme
|
||||
let overflowHeight: CGFloat
|
||||
|
||||
init(
|
||||
theme: PresentationTheme
|
||||
theme: PresentationTheme,
|
||||
overflowHeight: CGFloat
|
||||
) {
|
||||
self.theme = theme
|
||||
self.overflowHeight = overflowHeight
|
||||
}
|
||||
|
||||
static func ==(lhs: EntityKeyboardTopContainerPanelComponent, rhs: EntityKeyboardTopContainerPanelComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.overflowHeight != rhs.overflowHeight {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -46,6 +55,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
|
||||
private final class PanelView {
|
||||
let view = ComponentHostView<EntityKeyboardTopContainerPanelEnvironment>()
|
||||
let visibilityFractionUpdated = ActionSlot<(CGFloat, Transition)>()
|
||||
var isExpanded: Bool = false
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
@@ -57,8 +67,13 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
|
||||
|
||||
private var visibilityFraction: CGFloat = 1.0
|
||||
|
||||
private var hasExpandedPanels: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.disablesInteractiveKeyboardGestureRecognizer = true
|
||||
self.disablesInteractiveTransitionGestureRecognizer = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -94,7 +109,7 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
|
||||
let panel = panelEnvironment.contentTopPanels[index]
|
||||
let indexOffset = index - centralIndex
|
||||
|
||||
let panelFrame = CGRect(origin: CGPoint(x: CGFloat(indexOffset) * availableSize.width, y: 0.0), size: CGSize(width: availableSize.width, height: intrinsicHeight))
|
||||
let panelFrame = CGRect(origin: CGPoint(x: CGFloat(indexOffset) * availableSize.width, y: -component.overflowHeight), size: CGSize(width: availableSize.width, height: intrinsicHeight + component.overflowHeight))
|
||||
|
||||
let isInBounds = visibleBounds.intersects(panelFrame)
|
||||
let isPartOfTransition: Bool
|
||||
@@ -118,12 +133,19 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
|
||||
self.addSubview(panelView.view)
|
||||
}
|
||||
|
||||
let panelId = panel.id
|
||||
let _ = panelView.view.update(
|
||||
transition: panelTransition,
|
||||
component: panel.component,
|
||||
environment: {
|
||||
EntityKeyboardTopContainerPanelEnvironment(
|
||||
visibilityFractionUpdated: panelView.visibilityFractionUpdated
|
||||
visibilityFractionUpdated: panelView.visibilityFractionUpdated,
|
||||
isExpandedUpdated: { [weak self] isExpanded, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.panelIsExpandedUpdated(id: panelId, isExpanded: isExpanded, transition: transition)
|
||||
}
|
||||
)
|
||||
},
|
||||
containerSize: panelFrame.size
|
||||
@@ -171,6 +193,45 @@ final class EntityKeyboardTopContainerPanelComponent: Component {
|
||||
transition.setSublayerTransform(view: panelView.view, transform: CATransform3DMakeTranslation(0.0, -panelView.view.bounds.height / 2.0 * (1.0 - value), 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
private func panelIsExpandedUpdated(id: AnyHashable, isExpanded: Bool, transition: Transition) {
|
||||
guard let panelView = self.panelViews[id] else {
|
||||
return
|
||||
}
|
||||
panelView.isExpanded = isExpanded
|
||||
|
||||
var hasExpanded = false
|
||||
for (_, panel) in self.panelViews {
|
||||
if panel.isExpanded {
|
||||
hasExpanded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if self.hasExpandedPanels != hasExpanded {
|
||||
self.hasExpandedPanels = hasExpanded
|
||||
|
||||
self.panelEnvironment?.isExpandedUpdated(self.hasExpandedPanels, transition)
|
||||
}
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.alpha.isZero {
|
||||
return nil
|
||||
}
|
||||
for view in self.subviews.reversed() {
|
||||
if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
let result = super.hitTest(point, with: event)
|
||||
if result != self {
|
||||
return result
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
@@ -9,14 +10,17 @@ import Postbox
|
||||
import AnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import AccountContext
|
||||
import MultilineTextComponent
|
||||
|
||||
final class EntityKeyboardAnimationTopPanelComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
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(
|
||||
@@ -24,12 +28,16 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -46,13 +54,21 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
|
||||
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)
|
||||
@@ -73,6 +89,8 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
|
||||
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(
|
||||
@@ -89,15 +107,88 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
|
||||
placeholderColor: .lightGray,
|
||||
blurredBadgeColor: .clear,
|
||||
displayPremiumBadgeIfAvailable: false,
|
||||
pointSize: CGSize(width: 28.0, height: 28.0)
|
||||
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)
|
||||
itemLayer.frame = CGRect(origin: CGPoint(), size: CGSize(width: 28.0, height: 28.0))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return CGSize(width: 28.0, height: 28.0)
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,14 +201,151 @@ final class EntityKeyboardAnimationTopPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
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<Empty>
|
||||
let content: AnyComponent<EntityKeyboardTopPanelItemEnvironment>
|
||||
|
||||
init(id: AnyHashable, content: AnyComponent<Empty>) {
|
||||
init(id: AnyHashable, content: AnyComponent<EntityKeyboardTopPanelItemEnvironment>) {
|
||||
self.id = id
|
||||
self.content = content
|
||||
}
|
||||
@@ -165,34 +393,42 @@ final class EntityKeyboardTopPanelComponent: Component {
|
||||
final class View: UIView, UIScrollViewDelegate {
|
||||
private struct ItemLayout {
|
||||
let sideInset: CGFloat = 7.0
|
||||
let itemSize: CGFloat = 32.0
|
||||
let innerItemSize: CGFloat = 28.0
|
||||
let itemSize: CGSize
|
||||
let innerItemSize: CGSize
|
||||
let itemSpacing: CGFloat = 15.0
|
||||
let itemCount: Int
|
||||
let contentSize: CGSize
|
||||
let isExpanded: Bool
|
||||
|
||||
init(itemCount: Int) {
|
||||
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 + CGFloat(max(0, itemCount - 1)) * itemSpacing, height: 41.0)
|
||||
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 + self.itemSpacing), y: floor((self.contentSize.height - self.itemSize) / 2.0)), size: CGSize(width: self.itemSize, height: self.itemSize))
|
||||
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 {
|
||||
var frame = self.containerFrame(at: index)
|
||||
frame.origin.x += floor((self.itemSize - self.innerItemSize)) / 2.0
|
||||
frame.origin.y += floor((self.itemSize - self.innerItemSize)) / 2.0
|
||||
frame.size = CGSize(width: self.innerItemSize, height: self.innerItemSize)
|
||||
return frame
|
||||
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 + self.itemSpacing)))
|
||||
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 + self.itemSpacing)))
|
||||
let maxVisibleColumn = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize.width + self.itemSpacing)))
|
||||
|
||||
let minVisibleIndex = minVisibleColumn
|
||||
let maxVisibleIndex = min(maxVisibleColumn, self.itemCount - 1)
|
||||
@@ -202,15 +438,23 @@ final class EntityKeyboardTopPanelComponent: Component {
|
||||
}
|
||||
|
||||
private let scrollView: UIScrollView
|
||||
private var itemViews: [AnyHashable: ComponentHostView<Empty>] = [:]
|
||||
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()
|
||||
@@ -222,7 +466,9 @@ final class EntityKeyboardTopPanelComponent: Component {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -236,6 +482,8 @@ final class EntityKeyboardTopPanelComponent: Component {
|
||||
|
||||
self.scrollView.addSubview(self.highlightedIconBackgroundView)
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return false
|
||||
@@ -253,38 +501,98 @@ final class EntityKeyboardTopPanelComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
self.updateVisibleItems(attemptSynchronousLoads: false)
|
||||
self.updateVisibleItems(attemptSynchronousLoads: false, transition: .immediate)
|
||||
}
|
||||
|
||||
private func updateVisibleItems(attemptSynchronousLoads: Bool) {
|
||||
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: self.scrollView.bounds)
|
||||
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)
|
||||
|
||||
let itemView: ComponentHostView<Empty>
|
||||
var itemTransition = transition
|
||||
let itemView: ComponentHostView<EntityKeyboardTopPanelItemEnvironment>
|
||||
if let current = self.itemViews[item.id] {
|
||||
itemView = current
|
||||
} else {
|
||||
itemView = ComponentHostView<Empty>()
|
||||
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: .immediate,
|
||||
transition: itemTransition,
|
||||
component: item.content,
|
||||
environment: {},
|
||||
environment: {
|
||||
EntityKeyboardTopPanelItemEnvironment(isExpanded: itemLayout.isExpanded)
|
||||
},
|
||||
containerSize: itemOuterFrame.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)
|
||||
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] = []
|
||||
@@ -305,22 +613,77 @@ final class EntityKeyboardTopPanelComponent: Component {
|
||||
}
|
||||
self.component = component
|
||||
|
||||
let intrinsicHeight: CGFloat = 41.0
|
||||
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 itemLayout = ItemLayout(itemCount: component.items.count)
|
||||
let previousItemLayout = self.itemLayout
|
||||
let itemLayout = ItemLayout(itemCount: component.items.count, isExpanded: isExpanded, height: availableSize.height)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
self.ignoreScrolling = true
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: intrinsicHeight)))
|
||||
|
||||
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 previousDistanceToItemFraction = (previousItemFrame.minX - self.scrollView.bounds.minX) / previousItemFrame.width
|
||||
let newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - floor(previousDistanceToItemFraction * 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: true)
|
||||
self.updateVisibleItems(attemptSynchronousLoads: !(self.scrollView.isDragging || self.scrollView.isDecelerating), transition: transition)
|
||||
|
||||
environment[EntityKeyboardTopContainerPanelEnvironment.self].value.visibilityFractionUpdated.connect { [weak self] (fraction, transition) in
|
||||
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
|
||||
}
|
||||
@@ -359,6 +722,8 @@ final class EntityKeyboardTopPanelComponent: Component {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
self.activeContentItemId = itemId
|
||||
|
||||
var found = false
|
||||
for i in 0 ..< component.items.count {
|
||||
@@ -366,7 +731,10 @@ final class EntityKeyboardTopPanelComponent: Component {
|
||||
found = true
|
||||
self.highlightedIconBackgroundView.isHidden = false
|
||||
let itemFrame = itemLayout.containerFrame(at: i)
|
||||
transition.setFrame(view: self.highlightedIconBackgroundView, frame: itemFrame)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -464,6 +464,7 @@ public final class GifPagerContentComponent: Component {
|
||||
relativeOffset: relativeOffset,
|
||||
absoluteOffsetToTopEdge: offsetToTopEdge,
|
||||
absoluteOffsetToBottomEdge: offsetToBottomEdge,
|
||||
isReset: false,
|
||||
isInteracting: isInteracting,
|
||||
transition: transition
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user