Move window/view visibility handling from LottieAnimationView to LottieDrawable
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index 30c02c3..3b14018 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -101,9 +101,6 @@
private String animationName;
private @RawRes int animationResId;
- private boolean playAnimationWhenShown = false;
- private boolean wasAnimatingWhenNotShown = false;
- private boolean wasAnimatingWhenDetached = false;
/**
* When we set a new composition, we set LottieDrawable to null then back again so that ImageView re-checks its bounds.
* However, this causes the drawable to get unscheduled briefly. Normally, we would pause the animation but in this case, we don't want to.
@@ -173,7 +170,6 @@
setFallbackResource(ta.getResourceId(R.styleable.LottieAnimationView_lottie_fallbackRes, 0));
if (ta.getBoolean(R.styleable.LottieAnimationView_lottie_autoPlay, false)) {
- wasAnimatingWhenDetached = true;
autoPlay = true;
}
@@ -275,7 +271,7 @@
ss.animationName = animationName;
ss.animationResId = animationResId;
ss.progress = lottieDrawable.getProgress();
- ss.isAnimating = lottieDrawable.isAnimating() || (!ViewCompat.isAttachedToWindow(this) && wasAnimatingWhenDetached);
+ ss.isAnimating = lottieDrawable.isAnimatingOrWillAnimateOnVisible();
ss.imageAssetsFolder = lottieDrawable.getImageAssetsFolder();
ss.repeatMode = lottieDrawable.getRepeatMode();
ss.repeatCount = lottieDrawable.getRepeatCount();
@@ -307,57 +303,11 @@
setRepeatCount(ss.repeatCount);
}
- @Override
- protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
- // This can happen on older versions of Android because onVisibilityChanged gets called from the
- // constructor of View so this will get called before lottieDrawable gets initialized.
- // https://github.com/airbnb/lottie-android/issues/1143
- // A simple null check on lottieDrawable would not work because when using Proguard optimization, a
- // null check on a final field gets removed. As "usually" final fields cannot be null.
- // However because this is called by super (View) before the initializer of the LottieAnimationView
- // is called, it actually can be null here.
- // Working around this by using a non final boolean that is set to true after the class initializer
- // has run.
- if (!isInitialized) {
- return;
- }
- if (isShown()) {
- if (wasAnimatingWhenNotShown) {
- resumeAnimation();
- } else if (playAnimationWhenShown) {
- playAnimation();
- }
- wasAnimatingWhenNotShown = false;
- playAnimationWhenShown = false;
- } else {
- if (isAnimating()) {
- pauseAnimation();
- wasAnimatingWhenNotShown = true;
- }
- }
- }
-
@Override protected void onAttachedToWindow() {
super.onAttachedToWindow();
- if (!isInEditMode() && (autoPlay || wasAnimatingWhenDetached)) {
- playAnimation();
- // Autoplay from xml should only apply once.
- autoPlay = false;
- wasAnimatingWhenDetached = false;
+ if (!isInEditMode() && autoPlay) {
+ lottieDrawable.playAnimation();
}
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- // This is needed to mimic newer platform behavior.
- // https://stackoverflow.com/a/53625860/715633
- onVisibilityChanged(this, getVisibility());
- }
- }
-
- @Override protected void onDetachedFromWindow() {
- if (isAnimating()) {
- cancelAnimation();
- wasAnimatingWhenDetached = true;
- }
- super.onDetachedFromWindow();
}
/**
@@ -630,12 +580,8 @@
*/
@MainThread
public void playAnimation() {
- if (isShown()) {
- lottieDrawable.playAnimation();
- computeRenderMode();
- } else {
- playAnimationWhenShown = true;
- }
+ lottieDrawable.playAnimation();
+ computeRenderMode();
}
/**
@@ -644,13 +590,8 @@
*/
@MainThread
public void resumeAnimation() {
- if (isShown()) {
- lottieDrawable.resumeAnimation();
- computeRenderMode();
- } else {
- playAnimationWhenShown = false;
- wasAnimatingWhenNotShown = true;
- }
+ lottieDrawable.resumeAnimation();
+ computeRenderMode();
}
/**
@@ -995,9 +936,6 @@
@MainThread
public void cancelAnimation() {
- wasAnimatingWhenDetached = false;
- wasAnimatingWhenNotShown = false;
- playAnimationWhenShown = false;
lottieDrawable.cancelAnimation();
computeRenderMode();
}
@@ -1005,9 +943,6 @@
@MainThread
public void pauseAnimation() {
autoPlay = false;
- wasAnimatingWhenDetached = false;
- wasAnimatingWhenNotShown = false;
- playAnimationWhenShown = false;
lottieDrawable.pauseAnimation();
computeRenderMode();
}
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
index 5882ef2..ee9602f 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
@@ -63,15 +63,28 @@
void run(LottieComposition composition);
}
+ /**
+ * Internal record keeping of the desired play state when {@link #isVisible()} transitions to or is false.
+ *
+ * If the animation was playing when it becomes invisible or play/pause is called on it while it is invisible, it will
+ * store the state and then take the appropriate action when the drawable becomes visible again.
+ */
+ private enum OnVisibleAction {
+ NONE,
+ PLAY,
+ RESUME,
+ }
+
private LottieComposition composition;
private final LottieValueAnimator animator = new LottieValueAnimator();
private float scale = 1f;
- //Call animationsEnabled() instead of using these fields directly
+ // Call animationsEnabled() instead of using these fields directly.
private boolean systemAnimationsEnabled = true;
private boolean ignoreSystemAnimationsDisabled = false;
private boolean safeMode = false;
+ private OnVisibleAction onVisibleAction = OnVisibleAction.NONE;
private final ArrayList<LazyCompositionTask> lazyCompositionTasks = new ArrayList<>();
private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@@ -352,6 +365,9 @@
public void clearComposition() {
if (animator.isRunning()) {
animator.cancel();
+ if (!isVisible()) {
+ onVisibleAction = OnVisibleAction.NONE;
+ }
}
composition = null;
compositionLayer = null;
@@ -478,11 +494,18 @@
}
if (animationsEnabled() || getRepeatCount() == 0) {
- animator.playAnimation();
+ if (isVisible()) {
+ animator.playAnimation();
+ } else {
+ onVisibleAction = OnVisibleAction.PLAY;
+ }
}
if (!animationsEnabled()) {
setFrame((int) (getSpeed() < 0 ? getMinFrame() : getMaxFrame()));
animator.endAnimation();
+ if (!isVisible()) {
+ onVisibleAction = OnVisibleAction.NONE;
+ }
}
}
@@ -490,6 +513,9 @@
public void endAnimation() {
lazyCompositionTasks.clear();
animator.endAnimation();
+ if (!isVisible()) {
+ onVisibleAction = OnVisibleAction.NONE;
+ }
}
/**
@@ -504,11 +530,18 @@
}
if (animationsEnabled() || getRepeatCount() == 0) {
- animator.resumeAnimation();
+ if (isVisible()) {
+ animator.resumeAnimation();
+ } else {
+ onVisibleAction = OnVisibleAction.RESUME;
+ }
}
if (!animationsEnabled()) {
setFrame((int) (getSpeed() < 0 ? getMinFrame() : getMaxFrame()));
animator.endAnimation();
+ if (!isVisible()) {
+ onVisibleAction = OnVisibleAction.NONE;
+ }
}
}
@@ -841,6 +874,14 @@
return animator.isRunning();
}
+ boolean isAnimatingOrWillAnimateOnVisible() {
+ if (isVisible()) {
+ return animator.isRunning();
+ } else {
+ return onVisibleAction == OnVisibleAction.PLAY || onVisibleAction == OnVisibleAction.RESUME;
+ }
+ }
+
private boolean animationsEnabled() {
return systemAnimationsEnabled || ignoreSystemAnimationsDisabled;
}
@@ -930,11 +971,17 @@
public void cancelAnimation() {
lazyCompositionTasks.clear();
animator.cancel();
+ if (!isVisible()) {
+ onVisibleAction = OnVisibleAction.NONE;
+ }
}
public void pauseAnimation() {
lazyCompositionTasks.clear();
animator.pauseAnimation();
+ if (!isVisible()) {
+ onVisibleAction = OnVisibleAction.NONE;
+ }
}
@FloatRange(from = 0f, to = 1f)
@@ -1111,6 +1158,29 @@
return null;
}
+ @Override public boolean setVisible(boolean visible, boolean restart) {
+ // Sometimes, setVisible(false) gets called twice in a row. If we don't check wasNotVisibleAlready, we could
+ // wind up clearing the onVisibleAction value for the second call.
+ boolean wasNotVisibleAlready = !isVisible();
+ boolean ret = super.setVisible(visible, restart);
+
+ if (visible) {
+ if (onVisibleAction == OnVisibleAction.PLAY) {
+ playAnimation();
+ } else if (onVisibleAction == OnVisibleAction.RESUME) {
+ resumeAnimation();
+ }
+ } else {
+ if (animator.isRunning()) {
+ pauseAnimation();
+ onVisibleAction = OnVisibleAction.RESUME;
+ } else if (!wasNotVisibleAlready) {
+ onVisibleAction = OnVisibleAction.NONE;
+ }
+ }
+ return ret;
+ }
+
/**
* These Drawable.Callback methods proxy the calls so that this is the drawable that is
* actually invalidated, not a child one which will not pass the view's validateDrawable check.
diff --git a/sample/src/androidTest/java/com/airbnb/lottie/samples/FragmentVisibilityTests.kt b/sample/src/androidTest/java/com/airbnb/lottie/samples/FragmentVisibilityTests.kt
index f8f8001..dbfbfed 100644
--- a/sample/src/androidTest/java/com/airbnb/lottie/samples/FragmentVisibilityTests.kt
+++ b/sample/src/androidTest/java/com/airbnb/lottie/samples/FragmentVisibilityTests.kt
@@ -141,8 +141,7 @@
}
val scenario1 = launchFragmentInContainer<TestFragment>()
- // Wait for the animation view.
- onView(withId(R.id.animation_view))
+ onIdle()
// Launch a new activity
scenario1.onFragment { fragment ->
@@ -370,10 +369,7 @@
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 }
- ) {}
+ 0 -> object : RecyclerView.ViewHolder(LottieAnimationView(parent.context).apply { id = R.id.animation_view }) {}
else -> object : RecyclerView.ViewHolder(TextView(parent.context)) {}
}
}
@@ -418,6 +414,7 @@
scenario.onFragment { assertFalse(it.animationView!!.isAnimating) }
scenario.onFragment { it.requireView().scrollBy(0, 10_000) }
scenario.onFragment { it.requireView().scrollBy(0, -10_000) }
+ // Animation already ended. Making sure it isn't playing again.
scenario.onFragment { assertFalse(it.animationView!!.isAnimating) }
}