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) }
     }