Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .changeset/lucky-pandas-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@lottiefiles/dotlottie-react-native': patch
---

fix: don't reload the animation when a view scrolls out of view and back

Leaving the window was treated as a teardown, so list recycling and screen
transitions destroyed the player and rebuilt it on the way back, re-reading the
source and restarting from frame 0. Playback now freezes at the current frame
and resumes on return; the player is released only on unmount.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
Expand Down Expand Up @@ -53,6 +54,7 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex
private var hasActiveComposition: Boolean = false
private var isReleased: Boolean = false
private var hasAttachedToWindow: Boolean = false
private var frozenWhileDetached: Boolean = false

private val measureAndLayout =
Runnable {
Expand All @@ -75,6 +77,11 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
setViewTreeLifecycleOwner(this)
composeView.setViewTreeLifecycleOwner(this)
// ComposeView disposes its composition on detach by default, which would
// reload the animation whenever a list recycles the row back into view.
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnLifecycleDestroyed(this)
)
addView(composeView)
ensureStateMachineListener()
// Composition is created once the view is attached (onAttachedToWindow /
Expand Down Expand Up @@ -463,7 +470,7 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex
super.onDetachedFromWindow()
hasAttachedToWindow = false
removeCallbacks(measureAndLayout)
cleanup()
freezeWhileDetached()
}

override fun onAttachedToWindow() {
Expand All @@ -476,9 +483,36 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex
if (!hasActiveComposition) {
renderContent()
}
unfreezeAfterReattach()
scheduleMeasureAndLayout()
}

private fun freezeWhileDetached() {
if (isReleased || !hasActiveComposition || frozenWhileDetached) {
return
}
try {
if (dotLottieController.isPlaying) {
dotLottieController.freeze()
frozenWhileDetached = true
}
} catch (e: Exception) {
android.util.Log.w("DotLottie", "Failed to freeze detached animation: ${e.message}")
}
}

private fun unfreezeAfterReattach() {
if (!frozenWhileDetached) {
return
}
try {
dotLottieController.unFreeze()
frozenWhileDetached = false
} catch (e: Exception) {
android.util.Log.w("DotLottie", "Failed to unfreeze re-attached animation: ${e.message}")
}
}

override fun requestLayout() {
super.requestLayout()
scheduleMeasureAndLayout()
Expand Down Expand Up @@ -551,6 +585,7 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex
} catch (e: Exception) {
android.util.Log.e("DotLottie", "Error during cleanup: ${e.message}")
} finally {
frozenWhileDetached = false
if (hasActiveComposition) {
composeView.disposeComposition()
hasActiveComposition = false
Expand Down
38 changes: 26 additions & 12 deletions ios/DotlottieReactNativeViewManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -301,22 +301,13 @@ struct AnimationView: View {
var body: some View {
if let animation = dataStore.animation {
DotLottieView(dotLottie: animation)
.onDisappear {
cleanupAnimation()
}
} else {
Text("Loading animation...")
.onAppear {
dataStore.createAnimation()
}
}
}



func cleanupAnimation() {
dataStore.cleanupAnimation()
}
}


Expand Down Expand Up @@ -499,6 +490,7 @@ class DotlottieReactNativeView: UIView {
private var isMountedToWindow: Bool = false
private var isReleased: Bool = false
private var pendingAnimationUpdate: Bool = false
private var pausedWhileDetached: Bool = false
let dataStore: Datastore = .init()
var _animation: DotLottieAnimation? {
dataStore.animation
Expand Down Expand Up @@ -532,11 +524,32 @@ class DotlottieReactNativeView: UIView {

if currentlyMounted {
setupHostingControllerIfNeeded()
scheduleAnimationUpdate()
if dataStore.animation == nil {
scheduleAnimationUpdate()
} else {
resumeAfterReattach()
}
} else {
dataStore.cleanupAnimation()
tearDownHostingController()
pauseWhileDetached()
}
}

private func pauseWhileDetached() {
guard !pausedWhileDetached, let animation = _animation, animation.isPlaying() else {
return
}

pausedWhileDetached = true
_ = animation.pause()
}

private func resumeAfterReattach() {
guard pausedWhileDetached else {
return
}

pausedWhileDetached = false
_ = _animation?.play()
}

override func layoutSubviews() {
Expand All @@ -552,6 +565,7 @@ class DotlottieReactNativeView: UIView {
isReleased = true
isMountedToWindow = false
pendingAnimationUpdate = false
pausedWhileDetached = false
dataStore.cleanupAnimation()
tearDownHostingController()
resetEventHandlers()
Expand Down
Loading