Reaction improvements

This commit is contained in:
Ali 2021-12-27 22:36:34 +04:00
parent d3164fa4cd
commit 8ef10ee6c2
4 changed files with 298 additions and 161 deletions

View File

@ -11,88 +11,167 @@ import UIKit
import WebPBinding import WebPBinding
import AnimatedAvatarSetNode import AnimatedAvatarSetNode
fileprivate final class CounterLayer: SimpleLayer { public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
fileprivate final class Layout { fileprivate final class ContainerButtonNode: HighlightTrackingButtonNode {
struct Spec: Equatable { struct Colors: Equatable {
let clippingHeight: CGFloat var background: UInt32
var stringComponents: [String] var foreground: UInt32
var backgroundColor: UInt32 var extractedBackground: UInt32
var foregroundColor: UInt32 var extractedForeground: UInt32
} }
let spec: Spec struct Counter: Equatable {
let size: CGSize var frame: CGRect
var components: [CounterLayout.Component]
}
let image: UIImage struct Layout: Equatable {
var colors: Colors
var baseSize: CGSize
var counter: Counter?
}
private var isExtracted: Bool = false
private var currentLayout: Layout?
init() {
super.init(pointerStyle: nil)
}
func update(layout: Layout) {
if self.currentLayout != layout {
self.currentLayout = layout
self.updateBackgroundImage(animated: false)
}
}
func updateIsExtracted(isExtracted: Bool, animated: Bool) {
if self.isExtracted != isExtracted {
self.isExtracted = isExtracted
self.updateBackgroundImage(animated: animated)
}
}
private func updateBackgroundImage(animated: Bool) {
guard let layout = self.currentLayout else {
return
}
let image = generateImage(layout.baseSize, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
let backgroundColor: UIColor
let foregroundColor: UIColor
if self.isExtracted {
backgroundColor = UIColor(argb: layout.colors.extractedBackground)
foregroundColor = UIColor(argb: layout.colors.extractedForeground)
} else {
backgroundColor = UIColor(argb: layout.colors.background)
foregroundColor = UIColor(argb: layout.colors.foreground)
}
context.setBlendMode(.copy)
context.setFillColor(backgroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.height, height: size.height)))
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height)))
context.fill(CGRect(origin: CGPoint(x: size.height / 2.0, y: 0.0), size: CGSize(width: size.width - size.height, height: size.height)))
if let counter = layout.counter {
context.setBlendMode(foregroundColor.alpha < 1.0 ? .copy : .normal)
var totalComponentWidth: CGFloat = 0.0
for component in counter.components {
totalComponentWidth += component.bounds.width
}
var textOrigin: CGFloat = size.width - counter.frame.width - 8.0 + floorToScreenPixels((counter.frame.width - totalComponentWidth) / 2.0)
for component in counter.components {
let string = NSAttributedString(string: component.string, font: Font.medium(11.0), textColor: foregroundColor)
string.draw(at: component.bounds.origin.offsetBy(dx: textOrigin, dy: floorToScreenPixels(size.height - component.bounds.height) / 2.0))
textOrigin += component.bounds.width
}
}
UIGraphicsPopContext()
})?.stretchableImage(withLeftCapWidth: Int(layout.baseSize.height / 2.0), topCapHeight: Int(layout.baseSize.height / 2.0))
if let image = image {
let previousContents = self.layer.contents
ASDisplayNodeSetResizableContents(self.layer, image)
if animated, let previousContents = previousContents {
self.layer.animate(from: previousContents as! CGImage, to: image.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2)
}
}
}
}
fileprivate final class CounterLayout {
struct Spec: Equatable {
var stringComponents: [String]
}
struct Component: Equatable {
var string: String
var bounds: CGRect
}
private static let maxDigitWidth: CGFloat = {
var maxWidth: CGFloat = 0.0
for i in 0 ..< 9 {
let string = NSAttributedString(string: "\(i)", font: Font.medium(11.0), textColor: .black)
let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
maxWidth = max(maxWidth, boundingRect.width)
}
return ceil(maxWidth)
}()
let spec: Spec
let components: [Component]
let size: CGSize
init( init(
spec: Spec, spec: Spec,
size: CGSize, components: [Component],
image: UIImage size: CGSize
) { ) {
self.spec = spec self.spec = spec
self.components = components
self.size = size self.size = size
self.image = image
} }
static func calculate(spec: Spec, previousLayout: Layout?) -> Layout { static func calculate(spec: Spec, previousLayout: CounterLayout?) -> CounterLayout {
let image: UIImage let size: CGSize
let components: [Component]
if let previousLayout = previousLayout, previousLayout.spec == spec { if let previousLayout = previousLayout, previousLayout.spec == spec {
image = previousLayout.image size = previousLayout.size
components = previousLayout.components
} else { } else {
let textColor = UIColor(argb: spec.foregroundColor) var resultSize = CGSize()
let string = NSAttributedString(string: spec.stringComponents.joined(separator: ""), font: Font.medium(11.0), textColor: textColor) var resultComponents: [Component] = []
for component in spec.stringComponents {
let string = NSAttributedString(string: component, font: Font.medium(11.0), textColor: .black)
let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) let boundingRect = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil)
image = generateImage(CGSize(width: boundingRect.size.width, height: spec.clippingHeight), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) resultComponents.append(Component(string: component, bounds: boundingRect))
/*context.setFillColor(UIColor(argb: spec.backgroundColor).cgColor)
context.fill(CGRect(origin: CGPoint(), size: size)) resultSize.width += CounterLayout.maxDigitWidth
if textColor.alpha < 1.0 { resultSize.height = max(resultSize.height, boundingRect.height)
context.setBlendMode(.copy) }
}*/ size = CGSize(width: ceil(resultSize.width), height: ceil(resultSize.height))
context.translateBy(x: 0.0, y: (size.height - boundingRect.size.height) / 2.0) components = resultComponents
UIGraphicsPushContext(context)
string.draw(at: CGPoint())
UIGraphicsPopContext()
})!
} }
return Layout( return CounterLayout(
spec: spec, spec: spec,
size: image.size, components: components,
image: image size: size
) )
} }
} }
var layout: Layout?
override init(layer: Any) {
super.init(layer: layer)
}
override init() {
super.init()
self.masksToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(layout: Layout, animation: ListViewItemUpdateAnimation) {
/*if animation.isAnimated, let previousContents = self.contents {
self.animate(from: previousContents as! CGImage, to: layout.image.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2)
} else {*/
self.contents = layout.image.cgImage
//}
self.layout = layout
}
}
public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
fileprivate final class Layout { fileprivate final class Layout {
struct Spec: Equatable { struct Spec: Equatable {
var component: ReactionButtonComponent var component: ReactionButtonComponent
@ -106,11 +185,12 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
let imageFrame: CGRect let imageFrame: CGRect
let counter: CounterLayer.Layout? let counterLayout: CounterLayout?
let counterFrame: CGRect? let counterFrame: CGRect?
let backgroundImage: UIImage let backgroundLayout: ContainerButtonNode.Layout
let extractedBackgroundImage: UIImage //let backgroundImage: UIImage
//let extractedBackgroundImage: UIImage
let size: CGSize let size: CGSize
@ -120,10 +200,11 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
clippingHeight: CGFloat, clippingHeight: CGFloat,
sideInsets: CGFloat, sideInsets: CGFloat,
imageFrame: CGRect, imageFrame: CGRect,
counter: CounterLayer.Layout?, counterLayout: CounterLayout?,
counterFrame: CGRect?, counterFrame: CGRect?,
backgroundImage: UIImage, backgroundLayout: ContainerButtonNode.Layout,
extractedBackgroundImage: UIImage, //backgroundImage: UIImage,
//extractedBackgroundImage: UIImage,
size: CGSize size: CGSize
) { ) {
self.spec = spec self.spec = spec
@ -131,14 +212,15 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
self.clippingHeight = clippingHeight self.clippingHeight = clippingHeight
self.sideInsets = sideInsets self.sideInsets = sideInsets
self.imageFrame = imageFrame self.imageFrame = imageFrame
self.counter = counter self.counterLayout = counterLayout
self.counterFrame = counterFrame self.counterFrame = counterFrame
self.backgroundImage = backgroundImage self.backgroundLayout = backgroundLayout
self.extractedBackgroundImage = extractedBackgroundImage //self.backgroundImage = backgroundImage
//self.extractedBackgroundImage = extractedBackgroundImage
self.size = size self.size = size
} }
static func calculate(spec: Spec, currentLayout: Layout?, currentCounter: CounterLayer.Layout?) -> Layout { static func calculate(spec: Spec, currentLayout: Layout?) -> Layout {
let clippingHeight: CGFloat = 22.0 let clippingHeight: CGFloat = 22.0
let sideInsets: CGFloat = 8.0 let sideInsets: CGFloat = 8.0
let height: CGFloat = 30.0 let height: CGFloat = 30.0
@ -161,7 +243,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
let imageFrame = CGRect(origin: CGPoint(x: sideInsets, y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize) let imageFrame = CGRect(origin: CGPoint(x: sideInsets, y: floorToScreenPixels((height - imageSize.height) / 2.0)), size: imageSize)
var previousDisplayCounter: String? /*var previousDisplayCounter: String?
if let currentLayout = currentLayout { if let currentLayout = currentLayout {
if currentLayout.spec.component.avatarPeers.isEmpty { if currentLayout.spec.component.avatarPeers.isEmpty {
previousDisplayCounter = countString(Int64(spec.component.count)) previousDisplayCounter = countString(Int64(spec.component.count))
@ -170,9 +252,9 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
var currentDisplayCounter: String? var currentDisplayCounter: String?
if spec.component.avatarPeers.isEmpty { if spec.component.avatarPeers.isEmpty {
currentDisplayCounter = countString(Int64(spec.component.count)) currentDisplayCounter = countString(Int64(spec.component.count))
} }*/
let backgroundImage: UIImage /*let backgroundImage: UIImage
let extractedBackgroundImage: UIImage let extractedBackgroundImage: UIImage
if let currentLayout = currentLayout, currentLayout.spec.component.isSelected == spec.component.isSelected, currentLayout.spec.component.colors == spec.component.colors, previousDisplayCounter == currentDisplayCounter { if let currentLayout = currentLayout, currentLayout.spec.component.isSelected == spec.component.isSelected, currentLayout.spec.component.colors == spec.component.colors, previousDisplayCounter == currentDisplayCounter {
backgroundImage = currentLayout.backgroundImage backgroundImage = currentLayout.backgroundImage
@ -228,9 +310,9 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
UIGraphicsPopContext() UIGraphicsPopContext()
})!.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0)) })!.stretchableImage(withLeftCapWidth: Int(height / 2.0), topCapHeight: Int(height / 2.0))
} }*/
var counter: CounterLayer.Layout? var counterLayout: CounterLayout?
var counterFrame: CGRect? var counterFrame: CGRect?
var size = CGSize(width: imageSize.width + sideInsets * 2.0, height: height) var size = CGSize(width: imageSize.width + sideInsets * 2.0, height: height)
@ -242,36 +324,53 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
size.width -= 2.0 size.width -= 2.0
} }
} else { } else {
let counterSpec = CounterLayer.Layout.Spec( let counterSpec = CounterLayout.Spec(
clippingHeight: clippingHeight, stringComponents: counterComponents
stringComponents: counterComponents,
backgroundColor: backgroundColor,
foregroundColor: spec.component.isSelected ? spec.component.colors.selectedForeground : spec.component.colors.deselectedForeground
) )
let counterValue: CounterLayer.Layout let counterValue: CounterLayout
if let currentCounter = currentCounter, currentCounter.spec == counterSpec { if let currentCounter = currentLayout?.counterLayout, currentCounter.spec == counterSpec {
counterValue = currentCounter counterValue = currentCounter
} else { } else {
counterValue = CounterLayer.Layout.calculate( counterValue = CounterLayout.calculate(
spec: counterSpec, spec: counterSpec,
previousLayout: currentCounter previousLayout: currentLayout?.counterLayout
) )
} }
counter = counterValue counterLayout = counterValue
size.width += spacing + counterValue.size.width size.width += spacing + counterValue.size.width
counterFrame = CGRect(origin: CGPoint(x: sideInsets + imageSize.width + spacing, y: floorToScreenPixels((height - counterValue.size.height) / 2.0)), size: counterValue.size) counterFrame = CGRect(origin: CGPoint(x: size.width - sideInsets - counterValue.size.width, y: floorToScreenPixels((height - counterValue.size.height) / 2.0)), size: counterValue.size)
} }
let backgroundColors = ReactionButtonAsyncNode.ContainerButtonNode.Colors(
background: spec.component.isSelected ? spec.component.colors.selectedBackground : spec.component.colors.deselectedBackground,
foreground: spec.component.isSelected ? spec.component.colors.selectedForeground : spec.component.colors.deselectedForeground,
extractedBackground: spec.component.colors.extractedBackground,
extractedForeground: spec.component.colors.extractedForeground
)
var backgroundCounter: ReactionButtonAsyncNode.ContainerButtonNode.Counter?
if let counterLayout = counterLayout, let counterFrame = counterFrame {
backgroundCounter = ReactionButtonAsyncNode.ContainerButtonNode.Counter(
frame: counterFrame,
components: counterLayout.components
)
}
let backgroundLayout = ContainerButtonNode.Layout(
colors: backgroundColors,
baseSize: CGSize(width: height + 18.0, height: height),
counter: backgroundCounter
)
return Layout( return Layout(
spec: spec, spec: spec,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
clippingHeight: clippingHeight, clippingHeight: clippingHeight,
sideInsets: sideInsets, sideInsets: sideInsets,
imageFrame: imageFrame, imageFrame: imageFrame,
counter: counter, counterLayout: counterLayout,
counterFrame: counterFrame, counterFrame: counterFrame,
backgroundImage: backgroundImage, backgroundLayout: backgroundLayout,
extractedBackgroundImage: extractedBackgroundImage, //backgroundImage: backgroundImage,
//extractedBackgroundImage: extractedBackgroundImage,
size: size size: size
) )
} }
@ -280,16 +379,15 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
private var layout: Layout? private var layout: Layout?
public let containerNode: ContextExtractedContentContainingNode public let containerNode: ContextExtractedContentContainingNode
private let buttonNode: HighlightTrackingButtonNode private let buttonNode: ContainerButtonNode
public let iconView: UIImageView public let iconView: UIImageView
private var counterLayer: CounterLayer?
private var avatarsView: AnimatedAvatarSetView? private var avatarsView: AnimatedAvatarSetView?
private let iconImageDisposable = MetaDisposable() private let iconImageDisposable = MetaDisposable()
override init() { override init() {
self.containerNode = ContextExtractedContentContainingNode() self.containerNode = ContextExtractedContentContainingNode()
self.buttonNode = HighlightTrackingButtonNode() self.buttonNode = ContainerButtonNode()
self.iconView = UIImageView() self.iconView = UIImageView()
self.iconView.isUserInteractionEnabled = false self.iconView.isUserInteractionEnabled = false
@ -317,25 +415,20 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
self.isGestureEnabled = true self.isGestureEnabled = true
self.containerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, _ in self.containerNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, _ in
guard let strongSelf = self, let layout = strongSelf.layout else { guard let strongSelf = self else {
return return
} }
strongSelf.buttonNode.updateIsExtracted(isExtracted: isExtracted, animated: true)
let backgroundImage = isExtracted ? layout.extractedBackgroundImage : layout.backgroundImage /*let backgroundImage = isExtracted ? layout.extractedBackgroundImage : layout.backgroundImage
let previousContents = strongSelf.buttonNode.layer.contents let previousContents = strongSelf.buttonNode.layer.contents
let backgroundCapInsets = backgroundImage.capInsets
if backgroundCapInsets.left.isZero && backgroundCapInsets.top.isZero {
strongSelf.buttonNode.layer.contentsScale = backgroundImage.scale
strongSelf.buttonNode.layer.contents = backgroundImage.cgImage
} else {
ASDisplayNodeSetResizableContents(strongSelf.buttonNode.layer, backgroundImage) ASDisplayNodeSetResizableContents(strongSelf.buttonNode.layer, backgroundImage)
}
if let previousContents = previousContents { if let previousContents = previousContents {
strongSelf.buttonNode.layer.animate(from: previousContents as! CGImage, to: backgroundImage.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2) strongSelf.buttonNode.layer.animate(from: previousContents as! CGImage, to: backgroundImage.cgImage!, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2)
} }*/
} }
} }
@ -360,13 +453,8 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
self.containerNode.contentRect = CGRect(origin: CGPoint(), size: layout.size) self.containerNode.contentRect = CGRect(origin: CGPoint(), size: layout.size)
animation.animator.updateFrame(layer: self.buttonNode.layer, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil) animation.animator.updateFrame(layer: self.buttonNode.layer, frame: CGRect(origin: CGPoint(), size: layout.size), completion: nil)
let backgroundCapInsets = layout.backgroundImage.capInsets //ASDisplayNodeSetResizableContents(self.buttonNode.layer, layout.backgroundImage)
if backgroundCapInsets.left.isZero && backgroundCapInsets.top.isZero { self.buttonNode.update(layout: layout.backgroundLayout)
self.buttonNode.layer.contentsScale = layout.backgroundImage.scale
self.buttonNode.layer.contents = layout.backgroundImage.cgImage
} else {
ASDisplayNodeSetResizableContents(self.buttonNode.layer, layout.backgroundImage)
}
animation.animator.updateFrame(layer: self.iconView.layer, frame: layout.imageFrame, completion: nil) animation.animator.updateFrame(layer: self.iconView.layer, frame: layout.imageFrame, completion: nil)
@ -387,33 +475,6 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
} }
} }
if let counter = layout.counter, let counterFrame = layout.counterFrame {
let counterLayer: CounterLayer
var counterAnimation = animation
if let current = self.counterLayer {
counterLayer = current
} else {
counterAnimation = .None
counterLayer = CounterLayer()
self.counterLayer = counterLayer
//self.layer.addSublayer(counterLayer)
if animation.isAnimated {
counterLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
counterAnimation.animator.updateFrame(layer: counterLayer, frame: counterFrame, completion: nil)
counterLayer.apply(layout: counter, animation: counterAnimation)
} else if let counterLayer = self.counterLayer {
self.counterLayer = nil
if animation.isAnimated {
animation.animator.updateAlpha(layer: counterLayer, alpha: 0.0, completion: { [weak counterLayer] _ in
counterLayer?.removeFromSuperlayer()
})
} else {
counterLayer.removeFromSuperlayer()
}
}
if !layout.spec.component.avatarPeers.isEmpty { if !layout.spec.component.avatarPeers.isEmpty {
let avatarsView: AnimatedAvatarSetView let avatarsView: AnimatedAvatarSetView
if let current = self.avatarsView { if let current = self.avatarsView {
@ -459,7 +520,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceNode {
if let currentLayout = currentLayout, currentLayout.spec == spec { if let currentLayout = currentLayout, currentLayout.spec == spec {
layout = currentLayout layout = currentLayout
} else { } else {
layout = Layout.calculate(spec: spec, currentLayout: currentLayout, currentCounter: currentLayout?.counter) layout = Layout.calculate(spec: spec, currentLayout: currentLayout)
} }
return (size: layout.size, apply: { animation in return (size: layout.size, apply: { animation in

View File

@ -176,6 +176,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
cloudSourcePoint = max(rect.minX + rect.height / 2.0, anchorRect.minX) cloudSourcePoint = max(rect.minX + rect.height / 2.0, anchorRect.minX)
} }
if self.highlightedReaction != nil {
rect.origin.x -= 2.0
}
return (rect, isLeftAligned, cloudSourcePoint) return (rect, isLeftAligned, cloudSourcePoint)
} }
@ -193,6 +197,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
let visibleBounds = self.scrollNode.view.bounds let visibleBounds = self.scrollNode.view.bounds
self.previewingItemContainer.bounds = visibleBounds self.previewingItemContainer.bounds = visibleBounds
let highlightedReactionIndex = self.items.firstIndex(where: { $0.reaction == self.highlightedReaction })
var validIndices = Set<Int>() var validIndices = Set<Int>()
for i in 0 ..< self.items.count { for i in 0 ..< self.items.count {
let columnIndex = i let columnIndex = i
@ -200,15 +206,24 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
let itemOffsetY: CGFloat = -1.0 let itemOffsetY: CGFloat = -1.0
let baseItemFrame = CGRect(origin: CGPoint(x: sideInset + column * (itemSize + itemSpacing), y: verticalInset + floor((rowHeight - itemSize) / 2.0) + itemOffsetY), size: CGSize(width: itemSize, height: itemSize)) var baseItemFrame = CGRect(origin: CGPoint(x: sideInset + column * (itemSize + itemSpacing), y: verticalInset + floor((rowHeight - itemSize) / 2.0) + itemOffsetY), size: CGSize(width: itemSize, height: itemSize))
if let highlightedReactionIndex = highlightedReactionIndex {
if i > highlightedReactionIndex {
baseItemFrame.origin.x += 4.0
} else if i == highlightedReactionIndex {
baseItemFrame.origin.x += 2.0
}
}
if visibleBounds.intersects(baseItemFrame) { if visibleBounds.intersects(baseItemFrame) {
validIndices.insert(i) validIndices.insert(i)
var itemFrame = baseItemFrame var itemFrame = baseItemFrame
let isPreviewing = false var isPreviewing = false
if self.highlightedReaction == self.items[i].reaction { if self.highlightedReaction == self.items[i].reaction {
itemFrame = itemFrame.insetBy(dx: -4.0, dy: -4.0).offsetBy(dx: 0.0, dy: 0.0) let updatedSize = CGSize(width: floor(itemFrame.width * 1.66), height: floor(itemFrame.height * 1.66))
//isPreviewing = true itemFrame = CGRect(origin: CGPoint(x: itemFrame.midX - updatedSize.width / 2.0, y: itemFrame.maxY + 4.0 - updatedSize.height), size: updatedSize)
isPreviewing = true
} }
var animateIn = false var animateIn = false
@ -226,16 +241,24 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
if !itemNode.isExtracted { if !itemNode.isExtracted {
if isPreviewing { if isPreviewing {
/*if itemNode.supernode !== self.previewingItemContainer { if itemNode.supernode !== self.previewingItemContainer {
self.previewingItemContainer.addSubnode(itemNode) self.previewingItemContainer.addSubnode(itemNode)
}*/ }
} else {
/*if itemNode.supernode !== self.scrollNode {
self.scrollNode.addSubnode(itemNode)
}*/
} }
transition.updateFrame(node: itemNode, frame: itemFrame, beginWithCurrentState: true) transition.updateFrame(node: itemNode, frame: itemFrame, beginWithCurrentState: true, completion: { [weak self, weak itemNode] completed in
guard let strongSelf = self, let itemNode = itemNode else {
return
}
if !completed {
return
}
if !isPreviewing {
if itemNode.supernode !== strongSelf.scrollNode {
strongSelf.scrollNode.addSubnode(itemNode)
}
}
})
itemNode.updateLayout(size: itemFrame.size, isExpanded: false, isPreviewing: isPreviewing, transition: transition) itemNode.updateLayout(size: itemFrame.size, isExpanded: false, isPreviewing: isPreviewing, transition: transition)
if animateIn { if animateIn {
@ -272,6 +295,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
if visibleContentWidth > size.width - sideInset * 2.0 { if visibleContentWidth > size.width - sideInset * 2.0 {
visibleContentWidth = size.width - sideInset * 2.0 visibleContentWidth = size.width - sideInset * 2.0
} }
if self.highlightedReaction != nil {
visibleContentWidth += 4.0
}
let contentHeight = verticalInset * 2.0 + rowHeight let contentHeight = verticalInset * 2.0 + rowHeight
@ -282,10 +308,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
let (backgroundFrame, isLeftAligned, cloudSourcePoint) = self.calculateBackgroundFrame(containerSize: CGSize(width: size.width, height: size.height), insets: backgroundInsets, anchorRect: anchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)) let (backgroundFrame, isLeftAligned, cloudSourcePoint) = self.calculateBackgroundFrame(containerSize: CGSize(width: size.width, height: size.height), insets: backgroundInsets, anchorRect: anchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight))
self.isLeftAligned = isLeftAligned self.isLeftAligned = isLeftAligned
transition.updateFrame(node: self.contentContainer, frame: backgroundFrame) transition.updateFrame(node: self.contentContainer, frame: backgroundFrame, beginWithCurrentState: true)
transition.updateFrame(view: self.contentContainerMask, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) transition.updateFrame(view: self.contentContainerMask, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size), beginWithCurrentState: true)
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size), beginWithCurrentState: true)
transition.updateFrame(node: self.previewingItemContainer, frame: backgroundFrame) transition.updateFrame(node: self.previewingItemContainer, frame: backgroundFrame, beginWithCurrentState: true)
self.scrollNode.view.contentSize = CGSize(width: completeContentWidth, height: backgroundFrame.size.height) self.scrollNode.view.contentSize = CGSize(width: completeContentWidth, height: backgroundFrame.size.height)
self.updateScrolling(transition: transition) self.updateScrolling(transition: transition)
@ -436,12 +462,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
targetView.isHidden = true targetView.isHidden = true
} }
let itemSize: CGFloat = 40.0
itemNode.isExtracted = true itemNode.isExtracted = true
let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view) let selfSourceRect = itemNode.view.convert(itemNode.view.bounds, to: self.view)
let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) let selfTargetRect = self.view.convert(targetView.bounds, from: targetView)
let expandedScale: CGFloat = 4.0 let expandedScale: CGFloat = 4.0
let expandedSize = CGSize(width: floor(selfSourceRect.width * expandedScale), height: floor(selfSourceRect.height * expandedScale)) let expandedSize = CGSize(width: floor(itemSize * expandedScale), height: floor(itemSize * expandedScale))
var expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize) var expandedFrame = CGRect(origin: CGPoint(x: floor(selfTargetRect.midX - expandedSize.width / 2.0), y: floor(selfTargetRect.midY - expandedSize.height / 2.0)), size: expandedSize)
if expandedFrame.minX < -floor(expandedFrame.width * 0.05) { if expandedFrame.minX < -floor(expandedFrame.width * 0.05) {
@ -518,7 +546,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} }
public func highlightGestureMoved(location: CGPoint) { public func highlightGestureMoved(location: CGPoint) {
let highlightedReaction = self.reaction(at: location)?.reaction let highlightedReaction = self.previewReaction(at: location)?.reaction
if self.highlightedReaction != highlightedReaction { if self.highlightedReaction != highlightedReaction {
self.highlightedReaction = highlightedReaction self.highlightedReaction = highlightedReaction
if self.hapticFeedback == nil { if self.hapticFeedback == nil {
@ -545,6 +573,38 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} }
} }
private func previewReaction(at point: CGPoint) -> ReactionContextItem? {
let scrollPoint = self.view.convert(point, to: self.scrollNode.view)
if !self.scrollNode.bounds.contains(scrollPoint) {
return nil
}
let itemSize: CGFloat = 40.0
var closestItem: (index: Int, distance: CGFloat)?
for (index, itemNode) in self.visibleItemNodes {
let intersectionItemFrame = CGRect(origin: CGPoint(x: itemNode.frame.midX - itemSize / 2.0, y: itemNode.frame.midY - 1.0), size: CGSize(width: itemSize, height: 2.0))
if !self.scrollNode.bounds.contains(intersectionItemFrame) {
continue
}
let distance = abs(scrollPoint.x - intersectionItemFrame.midX)
if let (_, currentDistance) = closestItem {
if currentDistance > distance {
closestItem = (index, distance)
}
} else {
closestItem = (index, distance)
}
}
if let closestItem = closestItem {
return self.visibleItemNodes[closestItem.index]?.item
}
return nil
}
public func reaction(at point: CGPoint) -> ReactionContextItem? { public func reaction(at point: CGPoint) -> ReactionContextItem? {
for i in 0 ..< 2 { for i in 0 ..< 2 {
let touchInset: CGFloat = i == 0 ? 0.0 : 8.0 let touchInset: CGFloat = i == 0 ? 0.0 : 8.0

View File

@ -189,11 +189,18 @@ final class ReactionNode: ASDisplayNode {
stillAnimationNode.position = animationFrame.center stillAnimationNode.position = animationFrame.center
stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size)
stillAnimationNode.updateLayout(size: animationFrame.size) stillAnimationNode.updateLayout(size: animationFrame.size)
stillAnimationNode.started = { [weak self] in stillAnimationNode.started = { [weak self, weak stillAnimationNode] in
guard let strongSelf = self else { guard let strongSelf = self, let stillAnimationNode = stillAnimationNode, strongSelf.stillAnimationNode === stillAnimationNode else {
return return
} }
strongSelf.staticAnimationNode.alpha = 0.0 strongSelf.staticAnimationNode.alpha = 0.0
if let animateInAnimationNode = strongSelf.animateInAnimationNode, !animateInAnimationNode.alpha.isZero {
animateInAnimationNode.alpha = 0.0
animateInAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1)
strongSelf.staticAnimationNode.isHidden = false
}
} }
stillAnimationNode.visibility = true stillAnimationNode.visibility = true
@ -213,7 +220,7 @@ final class ReactionNode: ASDisplayNode {
transition.updateTransformScale(node: stillAnimationNode, scale: animationFrame.size.width / stillAnimationNode.bounds.width, beginWithCurrentState: true) transition.updateTransformScale(node: stillAnimationNode, scale: animationFrame.size.width / stillAnimationNode.bounds.width, beginWithCurrentState: true)
stillAnimationNode.alpha = 0.0 stillAnimationNode.alpha = 0.0
stillAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.14, completion: { [weak self, weak stillAnimationNode] _ in stillAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, completion: { [weak self, weak stillAnimationNode] _ in
guard let strongSelf = self, let stillAnimationNode = stillAnimationNode else { guard let strongSelf = self, let stillAnimationNode = stillAnimationNode else {
return return
} }

View File

@ -233,6 +233,7 @@ public extension EngineMessageReactionListContext.State {
init(message: EngineMessage, reaction: String?) { init(message: EngineMessage, reaction: String?) {
var totalCount = 0 var totalCount = 0
var hasOutgoingReaction = false var hasOutgoingReaction = false
var items: [EngineMessageReactionListContext.Item] = []
if let reactionsAttribute = message._asMessage().reactionsAttribute { if let reactionsAttribute = message._asMessage().reactionsAttribute {
for messageReaction in reactionsAttribute.reactions { for messageReaction in reactionsAttribute.reactions {
if reaction == nil || messageReaction.value == reaction { if reaction == nil || messageReaction.value == reaction {
@ -242,12 +243,20 @@ public extension EngineMessageReactionListContext.State {
totalCount += Int(messageReaction.count) totalCount += Int(messageReaction.count)
} }
} }
for recentPeer in reactionsAttribute.recentPeers {
if let peer = message.peers[recentPeer.peerId] {
items.append(EngineMessageReactionListContext.Item(peer: EnginePeer(peer), reaction: recentPeer.value))
}
}
}
if items.count != totalCount {
items.removeAll()
} }
self.init( self.init(
hasOutgoingReaction: hasOutgoingReaction, hasOutgoingReaction: hasOutgoingReaction,
totalCount: totalCount, totalCount: totalCount,
items: [], items: items,
canLoadMore: totalCount != 0 canLoadMore: items.count != totalCount && totalCount != 0
) )
} }
} }