Improve text spoiler animations

This commit is contained in:
Isaac 2024-05-31 11:50:48 +04:00
parent 9ea80ff9dc
commit 0e668a5fa2
6 changed files with 392 additions and 146 deletions

View File

@ -553,18 +553,18 @@ public extension ContainedViewLayoutTransition {
}
}
func animateFrame(layer: CALayer, from frame: CGRect, to toFrame: CGRect? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
func animateFrame(layer: CALayer, from frame: CGRect, to toFrame: CGRect? = nil, delay: Double = 0.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
switch self {
case .immediate:
case .immediate:
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
layer.animateFrame(from: frame, to: toFrame ?? layer.frame, duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in
if let completion = completion {
completion(true)
completion(result)
}
case let .animated(duration, curve):
layer.animateFrame(from: frame, to: toFrame ?? layer.frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in
if let completion = completion {
completion(result)
}
})
})
}
}

View File

@ -591,6 +591,29 @@ public class InvisibleInkDustNode: ASDisplayNode {
}
}
public func revealWithoutMaskAtLocation(_ location: CGPoint) {
guard !self.isRevealed else {
return
}
self.isRevealed = true
if self.enableAnimations {
self.isExploding = true
self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
self.emitterLayer?.setValue(location, forKeyPath: "emitterBehaviors.fingerAttractor.position")
Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) {
self.isExploding = false
self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled")
}
} else {
self.staticNode?.alpha = 0.0
self.staticNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
}
}
@objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) {
let location = gestureRecognizer.location(in: self.view)
self.revealAtLocation(location)

View File

@ -683,7 +683,22 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
var spoilerExpandRect: CGRect?
if let location = strongSelf.displayContentsUnderSpoilers.location {
spoilerExpandRect = textFrame.size.centered(around: CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY))
strongSelf.displayContentsUnderSpoilers.location = nil
let mappedLocation = CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY)
let getDistance: (CGPoint, CGPoint) -> CGFloat = { a, b in
let v = CGPoint(x: a.x - b.x, y: a.y - b.y)
return sqrt(v.x * v.x + v.y * v.y)
}
var maxDistance: CGFloat = getDistance(mappedLocation, CGPoint(x: 0.0, y: 0.0))
maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: textFrame.width, y: 0.0)))
maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: textFrame.width, y: textFrame.height)))
maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: 0.0, y: textFrame.height)))
let mappedSize = CGSize(width: maxDistance * 2.0, height: maxDistance * 2.0)
spoilerExpandRect = mappedSize.centered(around: mappedLocation)
}
let _ = textApply(InteractiveTextNodeWithEntities.Arguments(
@ -694,8 +709,11 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
attemptSynchronous: synchronousLoads,
textColor: messageTheme.primaryTextColor,
spoilerEffectColor: messageTheme.secondaryTextColor,
animation: animation,
animationArguments: InteractiveTextNode.AnimationArguments(
applyArguments: InteractiveTextNode.ApplyArguments(
animation: animation,
spoilerTextColor: messageTheme.primaryTextColor,
spoilerEffectColor: messageTheme.secondaryTextColor,
areContentAnimationsEnabled: item.context.sharedContext.energyUsageSettings.loopEmoji,
spoilerExpandRect: spoilerExpandRect
)
))
@ -1398,7 +1416,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return
}
self.displayContentsUnderSpoilers = (value, location)
self.displayContentsUnderSpoilers.location = nil
//self.displayContentsUnderSpoilers.location = nil
if let item = self.item {
item.controllerInteraction.requestMessageUpdate(item.message.id, false)
}

View File

@ -352,6 +352,44 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
}
}
public weak var mirrorLayer: CALayer? {
didSet {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.contents = self.contents
var customColor = self.contentTintColor
if let file = self.file {
if file.isCustomTemplateEmoji {
customColor = self.dynamicColor
}
}
if customColor != nil {
if self.layerTintColor == nil {
setLayerContentsMaskMode(mirrorLayer, true)
}
} else {
if self.layerTintColor != nil {
setLayerContentsMaskMode(mirrorLayer, false)
}
}
if let customColor {
Transition.immediate.setTintColor(layer: mirrorLayer, color: customColor)
} else {
self.layerTintColor = nil
}
}
}
}
override public var contents: Any? {
didSet {
if let mirrorLayer = self.mirrorLayer {
mirrorLayer.contents = self.contents
}
}
}
public convenience init(context: AccountContext, userLocation: MediaResourceUserLocation, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize, dynamicColor: UIColor? = nil, loopCount: Int? = nil) {
self.init(
context: .account(context),

View File

@ -7,6 +7,8 @@ import AppBundle
import ComponentFlow
import TextFormat
import MessageInlineBlockBackgroundView
import InvisibleInkDustNode
import EmojiTextAttachmentView
private let defaultFont = UIFont.systemFont(ofSize: 15.0)
@ -1073,10 +1075,24 @@ private func addAttachment(attachment: UIImage, line: InteractiveTextNodeLine, a
}
open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecognizerDelegate {
public final class AnimationArguments {
public final class ApplyArguments {
public let animation: ListViewItemUpdateAnimation
public let spoilerTextColor: UIColor
public let spoilerEffectColor: UIColor
public let areContentAnimationsEnabled: Bool
public let spoilerExpandRect: CGRect?
public init(spoilerExpandRect: CGRect?) {
public init(
animation: ListViewItemUpdateAnimation,
spoilerTextColor: UIColor,
spoilerEffectColor: UIColor,
areContentAnimationsEnabled: Bool,
spoilerExpandRect: CGRect?
) {
self.animation = animation
self.spoilerTextColor = spoilerTextColor
self.spoilerEffectColor = spoilerEffectColor
self.areContentAnimationsEnabled = areContentAnimationsEnabled
self.spoilerExpandRect = spoilerExpandRect
}
}
@ -1154,10 +1170,10 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
continue
}
guard let item = contentItemLayer.item else {
guard let params = contentItemLayer.params else {
continue
}
guard let blockQuote = item.segment.blockQuote else {
guard let blockQuote = params.item.segment.blockQuote else {
continue
}
if blockQuote.isCollapsed == nil {
@ -1684,12 +1700,12 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
return calculateLayoutV2(attributedString: attributedString, minimumNumberOfLines: minimumNumberOfLines, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, verticalAlignment: verticalAlignment, lineSpacingFactor: lineSpacingFactor, cutout: cutout, insets: insets, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, customTruncationToken: customTruncationToken, expandedBlocks: expandedBlocks)
}
private func updateContentItems(animation: ListViewItemUpdateAnimation, animationArguments: AnimationArguments?) {
private func updateContentItems(arguments: ApplyArguments) {
guard let cachedLayout = self.cachedLayout else {
return
}
let animateContents = self.isDisplayingContentsUnderSpoilers != nil && self.isDisplayingContentsUnderSpoilers != cachedLayout.displayContentsUnderSpoilers && animation.isAnimated
let animateContents = self.isDisplayingContentsUnderSpoilers != nil && self.isDisplayingContentsUnderSpoilers != cachedLayout.displayContentsUnderSpoilers && arguments.animation.isAnimated
let synchronous = animateContents
self.isDisplayingContentsUnderSpoilers = cachedLayout.displayContentsUnderSpoilers
@ -1735,14 +1751,14 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
let contentItemFrame = CGRect(origin: CGPoint(x: segmentRect.minX, y: segmentRect.minY), size: CGSize(width: contentItem.size.width, height: contentItem.size.height))
var contentItemAnimation = animation
var contentItemAnimation = arguments.animation
let contentItemLayer: TextContentItemLayer
var itemSpoilerExpandRect: CGRect?
var itemAnimateContents = animateContents && contentItemAnimation.isAnimated
if let current = self.contentItemLayers[itemId] {
contentItemLayer = current
if animation.isAnimated, let spoilerExpandRect = animationArguments?.spoilerExpandRect {
if arguments.animation.isAnimated, let spoilerExpandRect = arguments.spoilerExpandRect {
itemSpoilerExpandRect = spoilerExpandRect.offsetBy(dx: -contentItemFrame.minX, dy: -contentItemFrame.minY)
itemAnimateContents = true
}
@ -1754,7 +1770,12 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
}
contentItemLayer.update(
item: contentItem,
params: TextContentItemLayer.Params(
item: contentItem,
spoilerTextColor: arguments.spoilerTextColor,
spoilerEffectColor: arguments.spoilerEffectColor,
areContentAnimationsEnabled: arguments.areContentAnimationsEnabled
),
animation: contentItemAnimation,
synchronously: synchronous,
animateContents: itemAnimateContents,
@ -1810,7 +1831,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
}
}
public static func asyncLayout(_ maybeNode: InteractiveTextNode?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (ListViewItemUpdateAnimation, AnimationArguments?) -> InteractiveTextNode) {
public static func asyncLayout(_ maybeNode: InteractiveTextNode?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (ApplyArguments) -> InteractiveTextNode) {
let existingLayout: InteractiveTextNodeLayout? = maybeNode?.cachedLayout
return { arguments in
@ -1852,10 +1873,10 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn
let node = maybeNode ?? InteractiveTextNode()
return (layout, { animation, animationArguments in
return (layout, { arguments in
if node.cachedLayout !== layout {
node.cachedLayout = layout
node.updateContentItems(animation: animation, animationArguments: animationArguments)
node.updateContentItems(arguments: arguments)
}
return node
@ -1899,6 +1920,25 @@ final class TextContentItem {
}
final class TextContentItemLayer: SimpleLayer {
final class Params {
let item: TextContentItem
let spoilerTextColor: UIColor
let spoilerEffectColor: UIColor
let areContentAnimationsEnabled: Bool
init(
item: TextContentItem,
spoilerTextColor: UIColor,
spoilerEffectColor: UIColor,
areContentAnimationsEnabled: Bool
) {
self.item = item
self.spoilerTextColor = spoilerTextColor
self.spoilerEffectColor = spoilerEffectColor
self.areContentAnimationsEnabled = areContentAnimationsEnabled
}
}
final class RenderMask {
let image: UIImage
let isOpaque: Bool
@ -2193,10 +2233,15 @@ final class TextContentItemLayer: SimpleLayer {
}
}
private(set) var item: TextContentItem?
private(set) var params: Params?
let renderNode: RenderNode
private var contentMaskNode: ASImageNode?
private var overlayContentLayer: SimpleLayer?
private var overlayContentMaskNode: ASImageNode?
private var spoilerEffectNode: InvisibleInkDustNode?
private var blockBackgroundView: MessageInlineBlockBackgroundView?
private var quoteTypeIconNode: ASImageNode?
private var blockExpandArrow: SimpleLayer?
@ -2224,19 +2269,19 @@ final class TextContentItemLayer: SimpleLayer {
}
func update(
item: TextContentItem,
params: Params,
animation: ListViewItemUpdateAnimation,
synchronously: Bool,
animateContents: Bool,
spoilerExpandRect: CGRect?
) {
self.item = item
self.params = params
let contentFrame = CGRect(origin: CGPoint(), size: item.size)
let contentFrame = CGRect(origin: CGPoint(), size: params.item.size)
var effectiveContentFrame = contentFrame
var contentMask: RenderMask?
if let blockQuote = item.segment.blockQuote {
if let blockQuote = params.item.segment.blockQuote {
let blockBackgroundView: MessageInlineBlockBackgroundView
if let current = self.blockBackgroundView {
blockBackgroundView = current
@ -2257,19 +2302,19 @@ final class TextContentItemLayer: SimpleLayer {
}
blockExpandArrow.layerTintColor = blockQuote.tintColor.cgColor
let blockBackgroundFrame = blockQuote.frame.offsetBy(dx: item.contentOffset.x, dy: item.contentOffset.y + 4.0)
let blockBackgroundFrame = blockQuote.frame.offsetBy(dx: params.item.contentOffset.x, dy: params.item.contentOffset.y + 4.0)
if animation.isAnimated {
self.isAnimating = true
self.currentAnimationId += 1
let animationId = self.currentAnimationId
animation.animator.updateFrame(layer: blockBackgroundView.layer, frame: blockBackgroundFrame, completion: { [weak self] completed in
guard completed, let self, self.currentAnimationId == animationId, let item = self.item else {
guard completed, let self, self.currentAnimationId == animationId, let params = self.params else {
return
}
self.isAnimating = false
self.update(
item: item,
params: params,
animation: .None,
synchronously: true,
animateContents: false,
@ -2326,7 +2371,7 @@ final class TextContentItemLayer: SimpleLayer {
animation.animator.updateBounds(layer: blockExpandArrow, bounds: CGRect(origin: CGPoint(), size: expandArrowFrame.size), completion: nil)
animation.animator.updateTransform(layer: blockExpandArrow, transform: CATransform3DMakeRotation(isCollapsed ? 0.0 : CGFloat.pi, 0.0, 0.0, 1.0), completion: nil)
let contentMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: blockBackgroundFrame.minY - contentFrame.minY), size: CGSize(width: contentFrame.width, height: blockBackgroundFrame.height))
let contentMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.minY - blockBackgroundFrame.minY), size: CGSize(width: contentFrame.width, height: blockBackgroundFrame.height))
contentMask = RenderMask(image: expandableBlockMaskImage, isOpaque: !isCollapsed, frame: contentMaskFrame)
effectiveContentFrame.size.height = ceil(contentMaskFrame.height - contentMaskFrame.minY)
} else {
@ -2391,44 +2436,221 @@ final class TextContentItemLayer: SimpleLayer {
}
}
if !params.item.segment.spoilers.isEmpty {
let spoilerEffectNode: InvisibleInkDustNode
if let current = self.spoilerEffectNode {
spoilerEffectNode = current
} else {
spoilerEffectNode = InvisibleInkDustNode(textNode: nil, enableAnimations: params.areContentAnimationsEnabled)
self.spoilerEffectNode = spoilerEffectNode
}
spoilerEffectNode.frame = contentFrame
spoilerEffectNode.update(
size: contentFrame.size,
color: params.spoilerEffectColor,
textColor: params.spoilerTextColor,
rects: params.item.segment.spoilers.map { $0.1.offsetBy(dx: 0.0 + params.item.contentOffset.x, dy: params.item.contentOffset.y + 0.0).insetBy(dx: 1.0, dy: 1.0) },
wordRects: params.item.segment.spoilerWords.map { $0.1.offsetBy(dx: params.item.contentOffset.x + 0.0, dy: params.item.contentOffset.y + 0.0).insetBy(dx: 1.0, dy: 1.0) }
)
} else {
if let spoilerEffectNode = self.spoilerEffectNode {
self.spoilerEffectNode = nil
spoilerEffectNode.layer.removeFromSuperlayer()
}
}
if self.spoilerEffectNode != nil {
let overlayContentLayer: SimpleLayer
if let current = self.overlayContentLayer {
overlayContentLayer = current
animation.animator.updateFrame(layer: overlayContentLayer, frame: effectiveContentFrame, completion: nil)
} else {
overlayContentLayer = SimpleLayer()
self.overlayContentLayer = overlayContentLayer
overlayContentLayer.masksToBounds = true
self.addSublayer(overlayContentLayer)
overlayContentLayer.frame = effectiveContentFrame
}
if let spoilerEffectNode = self.spoilerEffectNode {
if spoilerEffectNode.layer.superlayer !== overlayContentLayer {
overlayContentLayer.addSublayer(spoilerEffectNode.layer)
}
}
} else {
if let overlayContentLayer = self.overlayContentLayer {
self.overlayContentLayer = nil
overlayContentLayer.removeFromSuperlayer()
}
}
self.currentContentMask = contentMask
self.renderNode.params = RenderParams(size: contentFrame.size, item: item, mask: staticContentMask)
self.renderNode.params = RenderParams(size: contentFrame.size, item: params.item, mask: staticContentMask)
if synchronously {
if let spoilerExpandRect {
let _ = spoilerExpandRect
if let spoilerExpandRect, animation.isAnimated {
let localSpoilerExpandRect = spoilerExpandRect.offsetBy(dx: -self.renderNode.frame.minX, dy: -self.renderNode.frame.minY)
let revealAnimationDuration: CGFloat = 0.55
let revealTransition: ContainedViewLayoutTransition = .animated(duration: revealAnimationDuration, curve: .easeInOut)
let previousContents = self.renderNode.layer.contents
let copyContentsLayer = SimpleLayer()
copyContentsLayer.frame = self.renderNode.frame
copyContentsLayer.contents = previousContents
copyContentsLayer.masksToBounds = self.renderNode.layer.masksToBounds
copyContentsLayer.contentsGravity = self.renderNode.layer.contentsGravity
copyContentsLayer.contentsScale = self.renderNode.layer.contentsScale
for sublayer in self.renderNode.layer.sublayers ?? [] {
let copySublayer = SimpleLayer()
copySublayer.contentsScale = sublayer.contentsScale
copySublayer.position = sublayer.position
copySublayer.bounds = sublayer.bounds
copySublayer.transform = sublayer.transform
copySublayer.opacity = sublayer.opacity
copySublayer.isHidden = sublayer.isHidden
if let sublayer = sublayer as? InlineStickerItemLayer {
sublayer.mirrorLayer = copySublayer
} else {
copySublayer.contents = sublayer.contents
}
copyContentsLayer.addSublayer(copySublayer)
}
self.renderNode.layer.superlayer?.insertSublayer(copyContentsLayer, below: self.renderNode.layer)
self.renderNode.displayImmediately()
let maskFrame = self.renderNode.frame
let rectangularExpandedSide = max(localSpoilerExpandRect.width, localSpoilerExpandRect.height)
// The gradient starts at 0.7
let adjustedExpandedSide = ceil(rectangularExpandedSide * 1.3)
let rectangularExpandedRect = CGSize(width: adjustedExpandedSide, height: adjustedExpandedSide).centered(around: spoilerExpandRect.center)
let maskFrame = self.renderNode.bounds
let maskLayer = SimpleLayer()
maskLayer.masksToBounds = true
self.renderNode.layer.mask = maskLayer
maskLayer.frame = maskFrame
self.addSublayer(maskLayer)
let maskGradientLayer = SimpleGradientLayer()
maskGradientLayer.frame = CGRect(origin: CGPoint(), size: maskFrame.size)
setupSpoilerExpansionMaskGradient(
gradientLayer: maskGradientLayer,
centerLocation: CGPoint(
x: 0.5,
y: 0.5
),
radius: CGSize(
width: 1.5,
height: 1.5
),
inverse: false
)
animateRadialExpansionMask(maskLayer: maskLayer, expandedRect: rectangularExpandedRect, transition: revealTransition, inverse: false, completion: { [weak self] in
guard let self, let params = self.params else {
return
}
self.renderNode.layer.mask = nil
self.update(
params: params,
animation: .None,
synchronously: true,
animateContents: false,
spoilerExpandRect: nil
)
})
let copyMaskLayer = SimpleLayer()
copyMaskLayer.masksToBounds = true
copyContentsLayer.mask = copyMaskLayer
copyMaskLayer.frame = maskFrame
animateRadialExpansionMask(maskLayer: copyMaskLayer, expandedRect: rectangularExpandedRect, transition: revealTransition, inverse: true, completion: { [weak copyContentsLayer] in
copyContentsLayer?.removeFromSuperlayer()
})
if let spoilerEffectNode = self.spoilerEffectNode {
let spoilerMaskLayer = SimpleLayer()
spoilerMaskLayer.masksToBounds = true
spoilerEffectNode.layer.mask = spoilerMaskLayer
spoilerMaskLayer.frame = maskFrame
let spoilerLocalPosition = self.convert(rectangularExpandedRect.center, to: spoilerEffectNode.layer)
spoilerEffectNode.revealWithoutMaskAtLocation(spoilerLocalPosition)
animateRadialExpansionMask(maskLayer: spoilerMaskLayer, expandedRect: rectangularExpandedRect, transition: revealTransition, inverse: true, completion: { [weak self] in
guard let self, let spoilerEffectNode = self.spoilerEffectNode else {
return
}
spoilerEffectNode.layer.mask = nil
spoilerEffectNode.layer.opacity = 0.0
})
}
} else {
let previousContents = self.renderNode.layer.contents
self.renderNode.displayImmediately()
if animateContents, let previousContents {
animation.transition.animateContents(layer: self.renderNode.layer, from: previousContents)
}
if let spoilerEffectNode = self.spoilerEffectNode {
animation.transition.updateAlpha(layer: spoilerEffectNode.layer, alpha: params.item.displayContentsUnderSpoilers ? 0.0 : 1.0)
}
}
} else {
self.renderNode.setNeedsDisplay()
if let spoilerEffectNode = self.spoilerEffectNode {
animation.transition.updateAlpha(layer: spoilerEffectNode.layer, alpha: params.item.displayContentsUnderSpoilers ? 0.0 : 1.0)
}
}
}
}
private func animateRadialExpansionMask(maskLayer: CALayer, expandedRect: CGRect, transition: ContainedViewLayoutTransition, inverse: Bool, completion: @escaping () -> Void) {
let maskGradientLayer = SimpleGradientLayer()
maskLayer.addSublayer(maskGradientLayer)
maskGradientLayer.frame = expandedRect
setupSpoilerExpansionMaskGradient(
gradientLayer: maskGradientLayer,
centerLocation: CGPoint(
x: 0.5,
y: 0.5
),
radius: CGSize(
width: 0.5,
height: 0.5
),
inverse: inverse
)
let minGradientFrame = CGSize(width: 1.0, height: 1.0).centered(around: expandedRect.center)
transition.animateFrame(layer: maskGradientLayer, from: minGradientFrame, delay: 0.1, completion: { _ in
completion()
})
if inverse {
let outerBoundsSourceRect = minGradientFrame.insetBy(dx: 0.5, dy: 0.5)
let outerBoundsDestinationRect = expandedRect.insetBy(dx: 0.5, dy: 0.5)
for sideIndex in 0 ..< 4 {
let copyMaskOuterBoundsTopLayer = SimpleLayer()
copyMaskOuterBoundsTopLayer.backgroundColor = UIColor.white.cgColor
maskLayer.addSublayer(copyMaskOuterBoundsTopLayer)
let sourceFrame: CGRect
let destinationFrame: CGRect
// Top, left, bottom, right
if sideIndex == 0 {
sourceFrame = CGRect(origin: CGPoint(x: 0.0, y: outerBoundsSourceRect.minY - expandedRect.height), size: expandedRect.size)
destinationFrame = CGRect(origin: CGPoint(x: 0.0, y: outerBoundsDestinationRect.minY - expandedRect.height), size: expandedRect.size)
} else if sideIndex == 1 {
sourceFrame = CGRect(origin: CGPoint(x: outerBoundsSourceRect.minX - expandedRect.width, y: 0.0), size: expandedRect.size)
destinationFrame = CGRect(origin: CGPoint(x: outerBoundsDestinationRect.minX - expandedRect.width, y: 0.0), size: expandedRect.size)
} else if sideIndex == 2 {
sourceFrame = CGRect(origin: CGPoint(x: 0.0, y: outerBoundsSourceRect.maxY), size: expandedRect.size)
destinationFrame = CGRect(origin: CGPoint(x: 0.0, y: outerBoundsDestinationRect.maxY), size: expandedRect.size)
} else {
sourceFrame = CGRect(origin: CGPoint(x: outerBoundsSourceRect.maxX, y: 0.0), size: expandedRect.size)
destinationFrame = CGRect(origin: CGPoint(x: outerBoundsDestinationRect.maxX, y: 0.0), size: expandedRect.size)
}
copyMaskOuterBoundsTopLayer.frame = destinationFrame
transition.animateFrame(layer: copyMaskOuterBoundsTopLayer, from: sourceFrame, delay: 0.1)
}
}
}

View File

@ -11,7 +11,6 @@ import AnimationCache
import MultiAnimationRenderer
import TelegramCore
import EmojiTextAttachmentView
import InvisibleInkDustNode
private final class InlineStickerItem: Hashable {
let emoji: ChatTextInputTextCustomEmojiAttribute
@ -64,8 +63,7 @@ public final class InteractiveTextNodeWithEntities {
public let attemptSynchronous: Bool
public let textColor: UIColor
public let spoilerEffectColor: UIColor
public let animation: ListViewItemUpdateAnimation
public let animationArguments: InteractiveTextNode.AnimationArguments?
public let applyArguments: InteractiveTextNode.ApplyArguments
public init(
context: AccountContext,
@ -75,8 +73,7 @@ public final class InteractiveTextNodeWithEntities {
attemptSynchronous: Bool,
textColor: UIColor,
spoilerEffectColor: UIColor,
animation: ListViewItemUpdateAnimation,
animationArguments: InteractiveTextNode.AnimationArguments?
applyArguments: InteractiveTextNode.ApplyArguments
) {
self.context = context
self.cache = cache
@ -85,8 +82,7 @@ public final class InteractiveTextNodeWithEntities {
self.attemptSynchronous = attemptSynchronous
self.textColor = textColor
self.spoilerEffectColor = spoilerEffectColor
self.animation = animation
self.animationArguments = animationArguments
self.applyArguments = applyArguments
}
public func withUpdatedPlaceholderColor(_ color: UIColor) -> Arguments {
@ -98,8 +94,7 @@ public final class InteractiveTextNodeWithEntities {
attemptSynchronous: self.attemptSynchronous,
textColor: self.textColor,
spoilerEffectColor: self.spoilerEffectColor,
animation: self.animation,
animationArguments: self.animationArguments
applyArguments: self.applyArguments
)
}
}
@ -116,7 +111,6 @@ public final class InteractiveTextNodeWithEntities {
public let textNode: InteractiveTextNode
private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayerData] = [:]
private var dustEffectNodes: [Int: InvisibleInkDustNode] = [:]
private var displayContentsUnderSpoilers: Bool?
private var enableLooping: Bool = true
@ -149,7 +143,7 @@ public final class InteractiveTextNodeWithEntities {
self.textNode = textNode
}
public static func asyncLayout(_ maybeNode: InteractiveTextNodeWithEntities?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (InteractiveTextNodeWithEntities.Arguments?) -> InteractiveTextNodeWithEntities) {
public static func asyncLayout(_ maybeNode: InteractiveTextNodeWithEntities?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (InteractiveTextNodeWithEntities.Arguments) -> InteractiveTextNodeWithEntities) {
let makeLayout = InteractiveTextNode.asyncLayout(maybeNode?.textNode)
return { [weak maybeNode] arguments in
var updatedString: NSAttributedString?
@ -218,44 +212,40 @@ public final class InteractiveTextNodeWithEntities {
let (layout, apply) = makeLayout(arguments.withAttributedString(updatedString))
return (layout, { applyArguments in
let animation: ListViewItemUpdateAnimation = applyArguments?.animation ?? .None
let animation: ListViewItemUpdateAnimation = applyArguments.applyArguments.animation
let result = apply(animation, applyArguments?.animationArguments)
let result = apply(applyArguments.applyArguments)
if let maybeNode = maybeNode {
if let applyArguments = applyArguments {
maybeNode.updateInteractiveContents(
context: applyArguments.context,
cache: applyArguments.cache,
renderer: applyArguments.renderer,
textLayout: layout,
placeholderColor: applyArguments.placeholderColor,
attemptSynchronousLoad: false,
textColor: applyArguments.textColor,
spoilerEffectColor: applyArguments.spoilerEffectColor,
animation: animation,
animationArguments: applyArguments.animationArguments
)
}
maybeNode.updateInteractiveContents(
context: applyArguments.context,
cache: applyArguments.cache,
renderer: applyArguments.renderer,
textLayout: layout,
placeholderColor: applyArguments.placeholderColor,
attemptSynchronousLoad: false,
textColor: applyArguments.textColor,
spoilerEffectColor: applyArguments.spoilerEffectColor,
animation: animation,
applyArguments: applyArguments.applyArguments
)
return maybeNode
} else {
let resultNode = InteractiveTextNodeWithEntities(textNode: result)
if let applyArguments = applyArguments {
resultNode.updateInteractiveContents(
context: applyArguments.context,
cache: applyArguments.cache,
renderer: applyArguments.renderer,
textLayout: layout,
placeholderColor: applyArguments.placeholderColor,
attemptSynchronousLoad: false,
textColor: applyArguments.textColor,
spoilerEffectColor: applyArguments.spoilerEffectColor,
animation: .None,
animationArguments: nil
)
}
resultNode.updateInteractiveContents(
context: applyArguments.context,
cache: applyArguments.cache,
renderer: applyArguments.renderer,
textLayout: layout,
placeholderColor: applyArguments.placeholderColor,
attemptSynchronousLoad: false,
textColor: applyArguments.textColor,
spoilerEffectColor: applyArguments.spoilerEffectColor,
animation: .None,
applyArguments: applyArguments.applyArguments
)
return resultNode
}
@ -281,7 +271,7 @@ public final class InteractiveTextNodeWithEntities {
textColor: UIColor,
spoilerEffectColor: UIColor,
animation: ListViewItemUpdateAnimation,
animationArguments: InteractiveTextNode.AnimationArguments?
applyArguments: InteractiveTextNode.ApplyArguments
) {
self.enableLooping = context.sharedContext.energyUsageSettings.loopEmoji
@ -289,18 +279,16 @@ public final class InteractiveTextNodeWithEntities {
if let textLayout {
displayContentsUnderSpoilers = textLayout.displayContentsUnderSpoilers
}
let previousDisplayContentsUnderSpoilers = self.displayContentsUnderSpoilers
self.displayContentsUnderSpoilers = displayContentsUnderSpoilers
var nextIndexById: [Int64: Int] = [:]
var validIds: [InlineStickerItemLayer.Key] = []
var validDustEffectIds: [Int] = []
if let textLayout {
for i in 0 ..< textLayout.segments.count {
let segment = textLayout.segments[i]
guard let segmentLayer = self.textNode.segmentLayer(index: i), let segmentItem = segmentLayer.item else {
guard let segmentLayer = self.textNode.segmentLayer(index: i), let segmentParams = segmentLayer.params else {
continue
}
@ -322,8 +310,8 @@ public final class InteractiveTextNodeWithEntities {
itemFrame.origin.x = floorToScreenPixels(itemFrame.origin.x)
itemFrame.origin.y = floorToScreenPixels(itemFrame.origin.y)
itemFrame.origin.x += segmentItem.contentOffset.x
itemFrame.origin.y += segmentItem.contentOffset.y
itemFrame.origin.x += segmentParams.item.contentOffset.x
itemFrame.origin.y += segmentParams.item.contentOffset.y
let itemLayerData: InlineStickerItemLayerData
var itemLayerTransition = animation.transition
@ -341,45 +329,13 @@ public final class InteractiveTextNodeWithEntities {
self.inlineStickerItemLayers[id] = itemLayerData
segmentLayer.renderNode.layer.addSublayer(itemLayerData.itemLayer)
itemLayerData.itemLayer.isVisibleForAnimations = self.enableLooping && self.isItemVisible(itemRect: itemFrame.offsetBy(dx: -segmentItem.contentOffset.x, dy: -segmentItem.contentOffset.x))
itemLayerData.itemLayer.isVisibleForAnimations = self.enableLooping && self.isItemVisible(itemRect: itemFrame.offsetBy(dx: -segmentParams.item.contentOffset.x, dy: -segmentParams.item.contentOffset.x))
}
itemLayerTransition.updateAlpha(layer: itemLayerData.itemLayer, alpha: item.isHiddenBySpoiler ? 0.0 : 1.0)
itemLayerData.itemLayer.frame = itemFrame
itemLayerData.rect = itemFrame.offsetBy(dx: -segmentItem.contentOffset.x, dy: -segmentItem.contentOffset.y)
}
}
if !segment.spoilers.isEmpty {
validDustEffectIds.append(i)
let dustEffectNode: InvisibleInkDustNode
if let current = self.dustEffectNodes[i] {
dustEffectNode = current
if dustEffectNode.layer.superlayer !== segmentLayer.renderNode.layer {
segmentLayer.renderNode.layer.addSublayer(dustEffectNode.layer)
}
} else {
dustEffectNode = InvisibleInkDustNode(textNode: nil, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency)
self.dustEffectNodes[i] = dustEffectNode
segmentLayer.renderNode.layer.addSublayer(dustEffectNode.layer)
}
let dustNodeFrame = CGRect(origin: CGPoint(), size: segmentItem.size).insetBy(dx: -3.0, dy: -3.0)
dustEffectNode.frame = dustNodeFrame
dustEffectNode.update(
size: dustNodeFrame.size,
color: spoilerEffectColor,
textColor: textColor,
rects: segment.spoilers.map { $0.1.offsetBy(dx: 3.0 + segmentItem.contentOffset.x, dy: segmentItem.contentOffset.y + 3.0).insetBy(dx: 1.0, dy: 1.0) },
wordRects: segment.spoilerWords.map { $0.1.offsetBy(dx: segmentItem.contentOffset.x + 3.0, dy: segmentItem.contentOffset.y + 3.0).insetBy(dx: 1.0, dy: 1.0) }
)
if let previousDisplayContentsUnderSpoilers, previousDisplayContentsUnderSpoilers != displayContentsUnderSpoilers, displayContentsUnderSpoilers, let currentSpoilerExpandRect = animationArguments?.spoilerExpandRect {
let spoilerLocalPosition = self.textNode.layer.convert(currentSpoilerExpandRect.center, to: dustEffectNode.layer)
dustEffectNode.revealAtLocation(spoilerLocalPosition)
} else {
dustEffectNode.update(revealed: displayContentsUnderSpoilers, animated: previousDisplayContentsUnderSpoilers != nil && animation.isAnimated)
itemLayerData.rect = itemFrame.offsetBy(dx: -segmentParams.item.contentOffset.x, dy: -segmentParams.item.contentOffset.y)
}
}
}
@ -395,16 +351,5 @@ public final class InteractiveTextNodeWithEntities {
for key in removeKeys {
self.inlineStickerItemLayers.removeValue(forKey: key)
}
var removeDustEffectIds: [Int] = []
for (id, dustEffectNode) in self.dustEffectNodes {
if !validDustEffectIds.contains(id) {
removeDustEffectIds.append(id)
dustEffectNode.removeFromSupernode()
}
}
for id in removeDustEffectIds {
self.dustEffectNodes.removeValue(forKey: id)
}
}
}