mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Improve text spoiler animations
This commit is contained in:
parent
9ea80ff9dc
commit
0e668a5fa2
@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user