Fixed endless recycler view animation. #1340
Additionally - adjusted "testPausesWhenScrolledOffScreenAndResumesWhenComesBack" test.
See more details: #1324
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/FragmentVisibilityTests.kt b/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/FragmentVisibilityTests.kt
index 7f507d6..f929d38 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/FragmentVisibilityTests.kt
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/FragmentVisibilityTests.kt
@@ -15,24 +15,20 @@
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
-import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions
-import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches
-import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieDrawable
-import com.airbnb.lottie.samples.R
import com.airbnb.lottie.model.LottieCompositionCache
import com.nhaarman.mockitokotlin2.mock
-import org.junit.Assert
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.random.Random
@@ -41,7 +37,7 @@
@LargeTest
class FragmentVisibilityTests {
- @Test
+ @Before
fun setup() {
LottieCompositionCache.getInstance().clear()
}
@@ -306,6 +302,8 @@
return RecyclerView(requireContext()).apply {
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
+ var animationWasPlayed = false
+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
0 -> object : RecyclerView.ViewHolder(LottieAnimationView(parent.context).apply { id = R.id.animation_view }) {}
@@ -323,12 +321,17 @@
}
private fun bindLottieHolder(holder: RecyclerView.ViewHolder) {
- animationView = holder.itemView as LottieAnimationView
- (holder.itemView as LottieAnimationView).apply {
- repeatCount = LottieDrawable.INFINITE
- setAnimation(R.raw.heart)
- playAnimation()
- IdlingRegistry.getInstance().register(LottieIdlingResource(this, name = "Lottie ${Random.nextFloat()}"))
+ if (!animationWasPlayed) {
+ animationView = holder.itemView as LottieAnimationView
+ (holder.itemView as LottieAnimationView).apply {
+ // repeatCount = LottieDrawable.INFINITE
+ setAnimation(R.raw.heart)
+ playAnimation()
+ animationWasPlayed = true
+ IdlingRegistry.getInstance().register(LottieIdlingResource(this, name = "Lottie ${Random.nextFloat()}"))
+ }
+ } else {
+ IdlingRegistry.getInstance().register(LottieIdlingAnimationResource(animationView, name = "Lottie finished animation ${Random.nextFloat()}"))
}
}
@@ -347,6 +350,75 @@
scenario.onFragment { assertFalse(it.animationView!!.isAnimating) }
scenario.onFragment { it.requireView().scrollBy(0, -10_000) }
scenario.onFragment { assertTrue(it.animationView!!.isAnimating) }
+ onView(withId(R.id.animation_view)).check(matches(isDisplayed()))
+ scenario.onFragment { assertFalse(it.animationView!!.isAnimating) }
+ }
+
+ @Test
+ fun testPausesWhenScrolledOffScreenAndResumesWhenComesBackWithoutRepeatingWhenFinished() {
+
+ class TestFragment : Fragment() {
+ var animationView: LottieAnimationView? = null
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return RecyclerView(requireContext()).apply {
+ layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
+ adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
+
+ var animationWasPlayed = false
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ return when (viewType) {
+ 0 -> object : RecyclerView.ViewHolder(
+ LottieAnimationView(parent.context)
+ .apply { id = R.id.animation_view }
+ ) {}
+ else -> object : RecyclerView.ViewHolder(TextView(parent.context)) {}
+ }
+ }
+
+ override fun getItemCount(): Int = 1000
+
+ override fun getItemViewType(position: Int) = position
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ if (holder.itemViewType == 0) bindLottieHolder(holder)
+ else bindOtherViewHolder(holder, position)
+ }
+
+ private fun bindLottieHolder(holder: RecyclerView.ViewHolder) {
+ if (!animationWasPlayed) {
+ animationView = holder.itemView as LottieAnimationView
+ (holder.itemView as LottieAnimationView).apply {
+ setAnimation(R.raw.heart)
+ playAnimation()
+ animationWasPlayed = true
+ IdlingRegistry.getInstance().register(LottieIdlingResource(this, name = "Lottie ${Random.nextFloat()}"))
+ }
+ } else {
+ IdlingRegistry.getInstance().register(LottieIdlingAnimationResource(animationView, name = "Lottie finished animation ${Random.nextFloat()}"))
+ }
+ }
+
+ private fun bindOtherViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ (holder.itemView as TextView).text = "Item $position"
+ }
+ }
+ }
+ }
+ }
+
+ val scenario = launchFragmentInContainer<TestFragment>()
+ onView(isAssignableFrom(RecyclerView::class.java)).check(matches(isDisplayed()))
+ scenario.onFragment { assertTrue(it.animationView!!.isAnimating) }
+ scenario.onFragment { it.requireView().scrollBy(0, 10_000) }
+ scenario.onFragment { assertFalse(it.animationView!!.isAnimating) }
+ scenario.onFragment { it.requireView().scrollBy(0, -10_000) }
+ scenario.onFragment { assertTrue(it.animationView!!.isAnimating) }
+ onView(withId(R.id.animation_view)).check(matches(isDisplayed()))
+ scenario.onFragment { assertFalse(it.animationView!!.isAnimating) }
+ scenario.onFragment { it.requireView().scrollBy(0, 10_000) }
+ scenario.onFragment { it.requireView().scrollBy(0, -10_000) }
+ scenario.onFragment { assertFalse(it.animationView!!.isAnimating) }
}
private fun FragmentScenario<*>.waitForState(desiredState: Lifecycle.State) {
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/LottieIdlingAnimationResource.kt b/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/LottieIdlingAnimationResource.kt
new file mode 100644
index 0000000..1cac096
--- /dev/null
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/samples/LottieIdlingAnimationResource.kt
@@ -0,0 +1,37 @@
+package com.airbnb.lottie.samples
+
+import android.animation.Animator
+import androidx.test.espresso.IdlingRegistry
+import androidx.test.espresso.IdlingResource
+import com.airbnb.lottie.LottieAnimationView
+
+class LottieIdlingAnimationResource(animationView: LottieAnimationView?, private val name: String = "Lottie") : IdlingResource {
+
+ init {
+ animationView?.addAnimatorListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator) {
+ isIdle = false
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ isIdle = true
+ callback?.onTransitionToIdle()
+ animationView.removeAllAnimatorListeners()
+ IdlingRegistry.getInstance().unregister(this@LottieIdlingAnimationResource)
+ }
+ })
+ }
+
+ private var callback: IdlingResource.ResourceCallback? = null
+ private var isIdle = animationView?.isAnimating?.not() ?: true
+
+
+ override fun getName() = name
+
+ override fun isIdleNow() = isIdle
+
+ override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
+ this.callback = callback
+ if (isIdle) callback?.onTransitionToIdle()
+ }
+}
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index 7ff6e8e..0c11bac 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -295,6 +295,7 @@
playAnimation();
// Autoplay from xml should only apply once.
autoPlay = false;
+ wasAnimatingWhenDetached = false;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// This is needed to mimic newer platform behavior.