Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,50 @@ package com.swmansion.reanimated.css

import android.view.View
import android.view.ViewTreeObserver
import java.util.Collections
import java.util.WeakHashMap

/**
* A platform transition animates the property React Native itself writes, so a commit can
* overwrite it mid-flight. Re-asserting just before the frame is drawn covers every writer -
* a Fabric mount, the synchronous props path, the animation backend - without knowing which
* one ran, since they all run earlier in the same frame.
*
* [repair] returns whether anything is still animating.
* overwrite it mid-flight; re-asserting just before draw covers every writer, since they
* all run earlier in the same frame. [repair] returns whether anything is still animating.
*/
internal class CSSPlatformTransitionReconciler(
private val repair: () -> Boolean,
) {
/** Keyed by window: getViewTreeObserver is per window, not per view. */
private val tracked = HashSet<ViewTreeObserver>()
/**
* Keyed by window (getViewTreeObserver is per window). Weakly held: a destroyed
* window's observer stays isAlive forever and never draws, so a strong set would
* pin one observer per torn-down window.
*/
private val tracked: MutableSet<ViewTreeObserver> =
Collections.newSetFromMap(WeakHashMap())

/** Views awaiting attach, so repeated starts do not stack one listener each. */
private val pendingAttach: MutableSet<View> =
Collections.newSetFromMap(WeakHashMap())

fun track(view: View) {
// A window torn down mid-animation never draws again, so its listener never retires.
if (!view.isAttachedToWindow) {
// A detached view's floating observer dies on attach, leaving its listener
// unremovable; register only once the view is genuinely attached.
if (!pendingAttach.add(view)) return
view.addOnAttachStateChangeListener(
object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(attached: View) {
attached.removeOnAttachStateChangeListener(this)
pendingAttach.remove(attached)
track(attached)
}

override fun onViewDetachedFromWindow(detached: View) {
detached.removeOnAttachStateChangeListener(this)
pendingAttach.remove(detached)
}
},
)
return
}
tracked.removeAll { !it.isAlive }

val observer = view.viewTreeObserver
Expand All @@ -29,9 +56,7 @@ internal class CSSPlatformTransitionReconciler(
override fun onPreDraw(): Boolean {
if (repair()) return true

// Retiring here rather than when the registry empties: cancelling an
// animator runs its end callback part way through installing the
// replacement, when the registry is briefly empty.
// Nothing left to defend: retire until the next track().
if (observer.isAlive) observer.removeOnPreDrawListener(this)
tracked.remove(observer)
return true
Expand Down
Loading
Loading