Ongoing work on the updated entity input

This commit is contained in:
Ali
2022-07-05 19:16:06 +02:00
parent c69c578e1d
commit e99cefa2d6
51 changed files with 1827 additions and 317 deletions

View File

@@ -38,6 +38,7 @@ swift_library(
"//submodules/PremiumUI:PremiumUI",
"//submodules/StickerPackPreviewUI:StickerPackPreviewUI",
"//submodules/UndoUI:UndoUI",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
],
visibility = [
"//visibility:public",

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -464,6 +464,7 @@ public final class GifPagerContentComponent: Component {
relativeOffset: relativeOffset,
absoluteOffsetToTopEdge: offsetToTopEdge,
absoluteOffsetToBottomEdge: offsetToBottomEdge,
isReset: false,
isInteracting: isInteracting,
transition: transition
))