Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move window/view visibility handling from LottieAnimationView to LottieDrawable #1981

Merged
merged 1 commit into from Jan 14, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
79 changes: 7 additions & 72 deletions lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
Expand Up @@ -101,9 +101,6 @@ public void onResult(Throwable result) {
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.
Expand Down Expand Up @@ -173,7 +170,6 @@ private void init(@Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {

setFallbackResource(ta.getResourceId(R.styleable.LottieAnimationView_lottie_fallbackRes, 0));
if (ta.getBoolean(R.styleable.LottieAnimationView_lottie_autoPlay, false)) {
wasAnimatingWhenDetached = true;
autoPlay = true;
}

Expand Down Expand Up @@ -275,7 +271,7 @@ private void init(@Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
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();
Expand Down Expand Up @@ -307,57 +303,11 @@ private void init(@Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
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 (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;
if (!isInEditMode() && autoPlay) {
lottieDrawable.playAnimation();
}
super.onDetachedFromWindow();
}

/**
Expand Down Expand Up @@ -630,12 +580,8 @@ public boolean hasMatte() {
*/
@MainThread
public void playAnimation() {
if (isShown()) {
lottieDrawable.playAnimation();
computeRenderMode();
} else {
playAnimationWhenShown = true;
}
lottieDrawable.playAnimation();
computeRenderMode();
}

/**
Expand All @@ -644,13 +590,8 @@ public void playAnimation() {
*/
@MainThread
public void resumeAnimation() {
if (isShown()) {
lottieDrawable.resumeAnimation();
computeRenderMode();
} else {
playAnimationWhenShown = false;
wasAnimatingWhenNotShown = true;
}
lottieDrawable.resumeAnimation();
computeRenderMode();
}

/**
Expand Down Expand Up @@ -995,19 +936,13 @@ public float getScale() {

@MainThread
public void cancelAnimation() {
wasAnimatingWhenDetached = false;
wasAnimatingWhenNotShown = false;
playAnimationWhenShown = false;
lottieDrawable.cancelAnimation();
computeRenderMode();
}

@MainThread
public void pauseAnimation() {
autoPlay = false;
wasAnimatingWhenDetached = false;
wasAnimatingWhenNotShown = false;
playAnimationWhenShown = false;
lottieDrawable.pauseAnimation();
computeRenderMode();
}
Expand Down
76 changes: 73 additions & 3 deletions lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
Expand Up @@ -63,15 +63,28 @@ private interface LazyCompositionTask {
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() {
Expand Down Expand Up @@ -352,6 +365,9 @@ private void buildCompositionLayer() {
public void clearComposition() {
if (animator.isRunning()) {
animator.cancel();
if (!isVisible()) {
onVisibleAction = OnVisibleAction.NONE;
}
}
composition = null;
compositionLayer = null;
Expand Down Expand Up @@ -478,18 +494,28 @@ public void playAnimation() {
}

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

@MainThread
public void endAnimation() {
lazyCompositionTasks.clear();
animator.endAnimation();
if (!isVisible()) {
onVisibleAction = OnVisibleAction.NONE;
}
}

/**
Expand All @@ -504,11 +530,18 @@ public void resumeAnimation() {
}

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

Expand Down Expand Up @@ -841,6 +874,14 @@ public boolean isAnimating() {
return animator.isRunning();
}

boolean isAnimatingOrWillAnimateOnVisible() {
if (isVisible()) {
return animator.isRunning();
} else {
return onVisibleAction == OnVisibleAction.PLAY || onVisibleAction == OnVisibleAction.RESUME;
}
}

private boolean animationsEnabled() {
return systemAnimationsEnabled || ignoreSystemAnimationsDisabled;
}
Expand Down Expand Up @@ -930,11 +971,17 @@ public LottieComposition getComposition() {
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)
Expand Down Expand Up @@ -1111,6 +1158,29 @@ private Context getContext() {
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.
Expand Down
Expand Up @@ -141,8 +141,7 @@ class FragmentVisibilityTests {
}

val scenario1 = launchFragmentInContainer<TestFragment>()
// Wait for the animation view.
onView(withId(R.id.animation_view))
onIdle()

// Launch a new activity
scenario1.onFragment { fragment ->
Expand Down Expand Up @@ -370,10 +369,7 @@ class FragmentVisibilityTests {

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)) {}
}
}
Expand Down Expand Up @@ -418,6 +414,7 @@ class FragmentVisibilityTests {
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) }
}

Expand Down