mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Fix fast-send morph starting position
The bubble-in-text-input morph ran concurrently with listNode's scroll-to-item sublayerTransform animation, but its source rect was computed via CALayer.convert which uses model transforms only and so missed the pending additive sublayerTransform. The morph's `from` ended up calibrated to the post-scroll state while playback started from the pre-scroll state, offsetting the bubble at t=0. Add convertAnimatingSourceRectFromWindow: walks the layer chain top-down and at each parent->child step subtracts the parent's pending additive sublayerTransform translation in the parent's bounds space before descending, so the result reflects the t=0 presentation state even through rotations/scales later in the chain. Parent animatingItemNode under itemNode so listNode is on its ancestor chain and its pending sublayerTransform is picked up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
493f3103b3
commit
6c2f370a8f
1 changed files with 69 additions and 6 deletions
|
|
@ -31,6 +31,64 @@ private func convertAnimatingSourceRect(_ rect: CGRect, fromView: UIView, toView
|
|||
}
|
||||
}
|
||||
|
||||
private func pendingAdditiveSublayerTranslation(_ layer: CALayer) -> CGPoint {
|
||||
guard let keys = layer.animationKeys() else { return .zero }
|
||||
var result = CGPoint.zero
|
||||
for key in keys {
|
||||
guard let anim = layer.animation(forKey: key) as? CABasicAnimation,
|
||||
anim.keyPath == "sublayerTransform",
|
||||
anim.isAdditive,
|
||||
let fromValue = anim.fromValue as? NSValue else {
|
||||
continue
|
||||
}
|
||||
let t = fromValue.caTransform3DValue
|
||||
result.x += t.m41
|
||||
result.y += t.m42
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/// Convert a rect expressed in window coordinates to `toView`'s local coordinates,
|
||||
/// accounting for pending additive `sublayerTransform` animations on any ancestor
|
||||
/// of `toView`. Returns the position in `toView.bounds` that will render at
|
||||
/// `windowRect` visually at t=0 of the pending animations (once CA commits them).
|
||||
///
|
||||
/// Standard `toView.layer.convert(windowRect, from: nil)` reads model transforms
|
||||
/// only, so it yields the position that will render at `windowRect` *at t=end* of
|
||||
/// any pending additive animations (since additive animations don't change model
|
||||
/// values). For source-side morph calibration, where the snapshot was captured at
|
||||
/// pre-animation state, the t=0 position is what we want.
|
||||
///
|
||||
/// Walks the layer chain top-down from the root to `toView`. At each parent→child
|
||||
/// step it subtracts the parent's pending additive `sublayerTransform` translation
|
||||
/// *in the parent's own bounds coord space*, then does the standard one-step
|
||||
/// `convert(_:to:)` into the child. Applying the correction at the right level
|
||||
/// (rather than flat-summing the translations in the destination space) lets
|
||||
/// `CALayer.convert` propagate each correction through any remaining
|
||||
/// transforms — child `transform`, further ancestors' own model
|
||||
/// `sublayerTransform`, etc. — so the result is correct even when the chain
|
||||
/// contains non-translation transforms (rotations, scales).
|
||||
private func convertAnimatingSourceRectFromWindow(_ windowRect: CGRect, toView: UIView) -> CGRect {
|
||||
var chain: [CALayer] = []
|
||||
var layer: CALayer? = toView.layer
|
||||
while let cur = layer {
|
||||
chain.append(cur)
|
||||
layer = cur.superlayer
|
||||
}
|
||||
chain.reverse()
|
||||
|
||||
var r = windowRect
|
||||
for i in 0..<(chain.count - 1) {
|
||||
let parent = chain[i]
|
||||
let child = chain[i + 1]
|
||||
|
||||
let pending = pendingAdditiveSublayerTranslation(parent)
|
||||
let adjustedR = r.offsetBy(dx: -pending.x, dy: -pending.y)
|
||||
r = parent.convert(adjustedR, to: child)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
private final class OverlayTransitionContainerNode: ViewControllerTracingNode {
|
||||
override init() {
|
||||
super.init()
|
||||
|
|
@ -388,9 +446,11 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran
|
|||
func beginAnimation() {
|
||||
if let portalTargetView = self.portalTargetView {
|
||||
portalTargetView.view.alpha = 0.0
|
||||
portalTargetView.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, delay: 0.15)
|
||||
portalTargetView.view.layer.allowsGroupOpacity = true
|
||||
portalTargetView.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.14, delay: 0.14)
|
||||
|
||||
self.portalSourceView.layer.animateAlpha(from: 0.01, to: 1.0, duration: 0.12, delay: 0.15)
|
||||
self.portalSourceView.layer.allowsGroupOpacity = true
|
||||
self.portalSourceView.layer.animateAlpha(from: 0.01, to: 1.0, duration: 0.1, delay: 0.12)
|
||||
}
|
||||
|
||||
let verticalDuration: Double = ChatMessageTransitionNodeImpl.animationDuration
|
||||
|
|
@ -428,12 +488,12 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran
|
|||
|
||||
let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view)
|
||||
|
||||
let sourceRect = self.view.convert(initialTextInput.sourceRect, from: nil)
|
||||
//let sourceRect = self.view.convert(initialTextInput.sourceRect, from: nil)
|
||||
let sourceRect = convertAnimatingSourceRectFromWindow(initialTextInput.sourceRect, toView: self.view)
|
||||
let sourceBackgroundAbsoluteRect = initialTextInput.backgroundView.frame.offsetBy(dx: sourceRect.minX, dy: sourceRect.minY)
|
||||
let sourceAbsoluteRect = CGRect(origin: CGPoint(x: sourceBackgroundAbsoluteRect.minX, y: sourceBackgroundAbsoluteRect.maxY - self.contextSourceNode.contentRect.height), size: self.contextSourceNode.contentRect.size)
|
||||
|
||||
let textInput = ChatMessageTransitionNodeImpl.Source.TextInput(backgroundView: initialTextInput.backgroundView, contentView: initialTextInput.contentView, sourceRect: sourceRect, scrollOffset: initialTextInput.scrollOffset)
|
||||
|
||||
textInput.backgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: sourceAbsoluteRect.height - sourceBackgroundAbsoluteRect.height), size: textInput.backgroundView.bounds.size)
|
||||
textInput.contentView.frame = textInput.contentView.frame.offsetBy(dx: 0.0, dy: sourceAbsoluteRect.height - sourceBackgroundAbsoluteRect.height)
|
||||
|
||||
|
|
@ -477,6 +537,7 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran
|
|||
|
||||
self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY)
|
||||
self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size)
|
||||
|
||||
self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: verticalCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
|
@ -484,6 +545,7 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran
|
|||
strongSelf.endAnimation()
|
||||
})
|
||||
self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: horizontalCurve.mediaTimingFunction, additive: true)
|
||||
|
||||
self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), horizontalCurve, horizontalDuration)
|
||||
self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), verticalCurve, verticalDuration)
|
||||
|
||||
|
|
@ -1098,8 +1160,10 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran
|
|||
overlayController.displayNode.addSubnode(animatingItemNode)
|
||||
animatingItemNode.overlayController = overlayController
|
||||
self.listNode.context.sharedContext.mainWindow?.presentInGlobalOverlay(overlayController)
|
||||
animatingItemNode.frame = self.bounds
|
||||
default:
|
||||
self.addSubnode(animatingItemNode)
|
||||
animatingItemNode.frame = CGRect()
|
||||
itemNode.addSubnode(animatingItemNode)
|
||||
}
|
||||
|
||||
animatingItemNode.animationEnded = { [weak self, weak animatingItemNode] in
|
||||
|
|
@ -1120,7 +1184,6 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran
|
|||
}
|
||||
}
|
||||
|
||||
animatingItemNode.frame = self.bounds
|
||||
animatingItemNode.beginAnimation()
|
||||
|
||||
self.onTransitionEvent(.animated(duration: ChatMessageTransitionNodeImpl.animationDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue