blob: fc338b93445243e05cb28af9a183179f1ee0a9a6 [file] [log] [blame]
package com.airbnb.lottie.samples
import android.animation.Animator
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
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.model.LottieCompositionCache
import com.nhaarman.mockitokotlin2.mock
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
@RunWith(AndroidJUnit4::class)
@LargeTest
class FragmentVisibilityTests {
@Before
fun setup() {
LottieCompositionCache.getInstance().clear()
}
@Test
fun testAutoPlay() {
class TestFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.auto_play, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
IdlingRegistry.getInstance().register(LottieIdlingResource(view.findViewById(R.id.animation_view)))
}
}
launchFragmentInContainer<TestFragment>()
onView(withId(R.id.animation_view)).check(matches(isAnimating()))
}
/**
* https://github.com/airbnb/lottie-android/issues/1216
*/
@Test
fun testPauseBeforeCompositionLoadedStopsAutoPlay() {
class TestFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.auto_play, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val animationView = requireView().findViewById<LottieAnimationView>(R.id.animation_view)
animationView.pauseAnimation()
IdlingRegistry.getInstance().register(LottieIdlingResource(animationView))
}
}
launchFragmentInContainer<TestFragment>()
onView(withId(R.id.animation_view)).check(matches(isNotAnimating()))
}
@Test
fun testDoesntAnimateWithAutoplayWhenGone() {
class TestFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.auto_play_gone, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
IdlingRegistry.getInstance().register(LottieIdlingResource(view.findViewById(R.id.animation_view)))
}
}
val scenario = launchFragmentInContainer<TestFragment>()
onView(withId(R.id.animation_view)).check(matches(isNotAnimating()))
scenario.onFragment { frag ->
frag.requireView().findViewById<LottieAnimationView>(R.id.animation_view).isVisible = true
}
onView(withId(R.id.animation_view)).check(matches(isAnimating()))
}
@Test
fun testDoesntStopOnPause() {
class TestFragment : Fragment() {
lateinit var animationView: LottieAnimationView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.auto_play, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
animationView = view.findViewById<LottieAnimationView>(R.id.animation_view)
IdlingRegistry.getInstance().register(LottieIdlingResource(animationView))
AlertDialog.Builder(requireContext()).setTitle("This is a dialog").show()
}
}
val scenario = launchFragmentInContainer<TestFragment>(themeResId = R.style.Theme_AppCompat)
scenario.moveToState(Lifecycle.State.RESUMED)
scenario.onFragment { fragment ->
// Wait for the animation view.
onView(withId(R.id.animation_view))
// We have to use a property reference because the Fragment isn't resumed.
assertTrue(fragment.animationView.isAnimating)
}
}
@Test
fun testStopsWhenLaunchingAnotherActivity() {
class TestFragment : Fragment() {
lateinit var animationView: LottieAnimationView
val animationListener = mock<Animator.AnimatorListener>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.auto_play, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
animationView = view.findViewById<LottieAnimationView>(R.id.animation_view)
animationView.addAnimatorListener(animationListener)
IdlingRegistry.getInstance().register(LottieIdlingResource(animationView))
}
}
val scenario1 = launchFragmentInContainer<TestFragment>()
// Wait for the animation view.
onView(withId(R.id.animation_view))
// Launch a new activity
scenario1.onFragment { fragment ->
fragment.requireContext().startActivity(Intent(fragment.requireContext(), EmptyActivity::class.java))
}
onView(withId(R.id.finish))
// Wait for the original Fragment to go from RESUMED to CREATED (Stopped).
scenario1.waitForState(Lifecycle.State.CREATED)
scenario1.onFragment { fragment ->
assertFalse(fragment.animationView.isAnimating)
}
onView(withId(R.id.finish)).perform(ViewActions.click())
scenario1.waitForState(Lifecycle.State.RESUMED)
scenario1.onFragment { fragment ->
onView(withId(R.id.animation_view))
assertTrue(fragment.animationView.isAnimating)
}
}
@Test
fun testRecyclerViewCanAutoPlayInOnBind() {
class TestFragment : Fragment() {
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>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return object : RecyclerView.ViewHolder(LottieAnimationView(parent.context).apply { id = R.id.animation_view }) {}
}
override fun getItemCount(): Int = 1
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder.itemView as LottieAnimationView).apply {
repeatMode = LottieDrawable.RESTART
setAnimation(R.raw.heart)
playAnimation()
IdlingRegistry.getInstance().register(LottieIdlingResource(this))
}
}
}
}
}
}
launchFragmentInContainer<TestFragment>()
onView(withId(R.id.animation_view)).check(matches(isAnimating()))
}
@Test
fun testDoesntAutoplay() {
class TestFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.no_auto_play, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
IdlingRegistry.getInstance().register(LottieIdlingResource(view.findViewById(R.id.animation_view)))
}
}
launchFragmentInContainer<TestFragment>()
onView(withId(R.id.animation_view)).check(matches(isNotAnimating()))
}
@Test
fun testDoesntAutoplayWhenManuallyPausedHiddenAndShown() {
class TestFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.auto_play, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
IdlingRegistry.getInstance().register(LottieIdlingResource(view.findViewById(R.id.animation_view)))
}
}
val scenario = launchFragmentInContainer<TestFragment>()
onView(withId(R.id.animation_view)).check(matches(isAnimating()))
scenario.onFragment { frag ->
frag.animationView.pauseAnimation()
frag.animationView.isVisible = false
}
scenario.onFragment { frag ->
frag.animationView.isVisible = true
}
onView(withId(R.id.animation_view)).check(matches(isNotAnimating()))
}
@Test
fun testCanPlayWhenCalledRightAway() {
class TestFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.no_auto_play, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val animationView = view.findViewById<LottieAnimationView>(R.id.animation_view)
animationView.playAnimation()
IdlingRegistry.getInstance().register(LottieIdlingResource(animationView))
}
}
launchFragmentInContainer<TestFragment>()
onView(withId(R.id.animation_view)).check(matches(isAnimating()))
}
@Test
fun testResumesWhenManuallyPlayedHiddenAndShown() {
class TestFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.no_auto_play, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val animationView = view.findViewById<LottieAnimationView>(R.id.animation_view)
animationView.playAnimation()
IdlingRegistry.getInstance().register(LottieIdlingResource(animationView))
}
}
val scenario = launchFragmentInContainer<TestFragment>()
onView(withId(R.id.animation_view)).check(matches(isAnimating()))
scenario.onFragment { it.animationView.isVisible = false }
onView(withId(R.id.animation_view)).check(matches(isNotAnimating()))
scenario.onFragment { it.animationView.isVisible = true }
onView(withId(R.id.animation_view)).check(matches(isAnimating()))
}
@Test
fun testDoesntPlayWhenAncestorIsNotVisibleButResumesWhenMadeVisible() {
class TestFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.auto_play, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<View>(R.id.container).isVisible = false
IdlingRegistry.getInstance().register(LottieIdlingResource(view.findViewById(R.id.animation_view)))
}
}
val scenario = launchFragmentInContainer<TestFragment>()
onView(withId(R.id.animation_view)).check(matches(isNotAnimating()))
scenario.onFragment { fragment ->
fragment.requireView().findViewById<View>(R.id.container).isVisible = true
}
onView(withId(R.id.animation_view)).check(matches(isAnimating()))
}
@Test
fun testPausesWhenScrolledOffScreenAndResumesWhenComesBack() {
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) = if (position == 0) 0 else 1
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 {
// 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()}"))
}
}
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) }
}
@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) }
}
@Test
fun testResumesWhenStateSavedAndRestored() {
class TestFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.auto_play, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
IdlingRegistry.getInstance().register(LottieIdlingResource(view.findViewById(R.id.animation_view)))
}
}
val scenario = launchFragmentInContainer<TestFragment>()
onView(withId(R.id.animation_view)).check(matches(isAnimating()))
scenario.onFragment { frag ->
val savedState = SparseArray<Parcelable>()
val animationView = frag.requireView().findViewById<LottieAnimationView>(R.id.animation_view)
val parent = animationView.parent as ViewGroup
val fragmentViewParent = frag.requireView().parent as ViewGroup
fragmentViewParent.removeView(parent)
parent.saveHierarchyState(savedState)
fragmentViewParent.addView(parent)
parent.restoreHierarchyState(savedState)
}
onView(withId(R.id.animation_view)).check(matches(isAnimating()))
}
private fun FragmentScenario<*>.waitForState(desiredState: Lifecycle.State) {
var isState = false
while (!isState) {
onFragment { fragment ->
isState = fragment.lifecycle.currentState == desiredState
Thread.sleep(200)
}
}
}
private val Fragment.animationView get() = requireView().findViewById<LottieAnimationView>(R.id.animation_view)
}