22// SPDX-License-Identifier: MPL-2.0
33#if !SKIP_BRIDGE
44import Foundation
5+ import SkipModel
56#if SKIP
67import androidx. compose. animation. Animatable
78import androidx. compose. animation. core. Animatable
@@ -92,6 +93,20 @@ extension View {
9293final class AnimationHolder {
9394 var animation : Animation ?
9495}
96+
97+ final class AnimationTransaction : StateMutationTransaction {
98+ let id : Int
99+ let animation : Animation ?
100+
101+ init ( id: Int , animation: Animation ? ) {
102+ self . id = id
103+ self . animation = animation
104+ }
105+ }
106+
107+ public enum AnimationDebug {
108+ public static var decisionSink : ( ( String ) -> Void ) ?
109+ }
95110#endif
96111
97112// SKIP @bridge
@@ -109,7 +124,13 @@ public struct Animation : Hashable {
109124 var isNested = false
110125 synchronized ( withAnimationLock) {
111126 isNested = _withAnimation != nil
127+ nextWithAnimationTransactionID += 1
128+ let transaction = AnimationTransaction ( id: nextWithAnimationTransactionID, animation: animation)
129+ withAnimationTransactions. append ( transaction)
130+ StateTracking . currentMutationTransaction = transaction
131+ StateTracking . clearMutationReads ( )
112132 _withAnimation = animation
133+ debugLog ( " open withAnimation \( debugDescription ( for: withAnimationTransactions. last) ) nested= \( isNested) " )
113134 }
114135 return isNested
115136 #else
@@ -120,10 +141,17 @@ public struct Animation : Hashable {
120141 // SKIP @bridge
121142 public static func postBodyWithAnimation( ) {
122143 #if SKIP
144+ synchronized ( withAnimationLock) {
145+ debugLog ( " clear withAnimation transactions after body " )
146+ withAnimationTransactions. removeAll ( )
147+ StateTracking . currentMutationTransaction = nil
148+ }
123149 GlobalScope . async ( Dispatchers . Main) {
124150 awaitFrame ( )
125151 synchronized ( withAnimationLock) {
152+ debugLog ( " clear global withAnimation after frame " )
126153 _withAnimation = nil
154+ StateTracking . clearMutationReads ( )
127155 }
128156 }
129157 #endif
@@ -151,25 +179,90 @@ public struct Animation : Hashable {
151179 return isAnimating ? rememberedAnimation : nil
152180 }
153181
182+ /// The current active animation for normal value animatables.
183+ ///
184+ /// Value animatables should be driven by explicit `.animation(..., value:)`
185+ /// environment state or by a transaction attached to the state write that
186+ /// caused the value change. The legacy global `_withAnimation` frame window
187+ /// remains only for transition surfaces that cannot yet be tied to reads.
188+ @Composable static func currentForAnimatable( isAnimating: Bool ) -> Animation ? {
189+ let environmentAnimation = EnvironmentValues . shared. _animation
190+ let transaction = StateTracking . consumeMutationRead ( ) as? AnimationTransaction
191+ let animation = environmentAnimation ?? transaction? . animation
192+
193+ let rememberedAnimationHolder = remember { AnimationHolder ( ) }
194+ let rememberedAnimation = rememberedAnimationHolder. animation
195+ if animation != nil {
196+ rememberedAnimationHolder. animation = animation
197+ } else if !isAnimating {
198+ rememberedAnimationHolder. animation = nil
199+ }
200+
201+ debugDecision ( " animatable transaction= \( debugDescription ( for: transaction) ) environmentAnimation= \( environmentAnimation != nil ) usesAnimation= \( animation != nil ) " )
202+
203+ guard animation == nil else {
204+ return animation
205+ }
206+ return isAnimating ? rememberedAnimation : nil
207+ }
208+
154209 /// Whether we're in a `withAnimation` block.
155210 static var isInWithAnimation : Bool {
156211 synchronized ( withAnimationLock) {
157212 return _withAnimation != nil
158213 }
159214 }
160215
216+ static var currentTransaction : AnimationTransaction ? {
217+ synchronized ( withAnimationLock) {
218+ return withAnimationTransactions. last
219+ }
220+ }
221+
222+ static func debugDescription( for transaction: AnimationTransaction ? ) -> String {
223+ if let transaction {
224+ return " # \( transaction. id) animation= \( transaction. animation != nil ) "
225+ } else {
226+ return " none "
227+ }
228+ }
229+
230+ static func debugLog( _ message: String ) {
231+ if debugWithAnimationTransactions {
232+ android. util. Log. d ( " SkipUI.Animation " , message)
233+ }
234+ }
235+
236+ static func debugDecision( _ message: String ) {
237+ AnimationDebug . decisionSink ? ( message)
238+ debugLog ( message)
239+ }
240+
161241 /// Internal implementation of global `withAnimation` SwiftUI function.
162242 static func withAnimation< Result> ( _ animation: Animation ? = . default, _ body: ( ) throws -> Result ) rethrows -> Result {
163243 let isNested = preBodyWithAnimation ( animation)
164244 defer {
245+ finishBodyWithAnimation ( )
165246 if !isNested {
166247 postBodyWithAnimation ( )
167248 }
168249 }
169250 return body ( )
170251 }
171252
253+ private static func finishBodyWithAnimation( ) {
254+ synchronized ( withAnimationLock) {
255+ if !withAnimationTransactions. isEmpty {
256+ withAnimationTransactions. removeLast ( )
257+ }
258+ StateTracking . currentMutationTransaction = withAnimationTransactions. last
259+ }
260+ }
261+
172262 private static var _withAnimation : Animation ?
263+ private static var withAnimationTransactions : [ AnimationTransaction ] = [ ]
264+ private static var nextWithAnimationTransactionID = 0
265+ private static let debugWithAnimationTransactions = false
173266 private static let withAnimationLock : java . lang . Object = java. lang. Object ( )
174267
175268 private let spec : AnimationSpec < Any >
@@ -458,7 +551,8 @@ public enum AnimationCompletionCriteria : Hashable {
458551 let animatable = remember { Animatable ( resetValue. value ?? value, converter) }
459552 let isAnimating = animatable. isRunning || animatable. value != animatable. targetValue
460553 if isAnimating || animatable. value != value {
461- let animation = Animation . current ( isAnimating: isAnimating)
554+ let animation = Animation . currentForAnimatable ( isAnimating: isAnimating)
555+ Animation . debugDecision ( " animatable change value= \( value) isAnimating= \( isAnimating) usesAnimation= \( animation != nil ) " )
462556 LaunchedEffect ( value, animation) {
463557 if let animation {
464558 if animation. isInfinite {
0 commit comments