package com.airbnb.lottie; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.graphics.Rect; import androidx.collection.LongSparseArray; import androidx.collection.SparseArrayCompat; import com.airbnb.lottie.model.Font; import com.airbnb.lottie.model.FontCharacter; import com.airbnb.lottie.model.Marker; import com.airbnb.lottie.model.layer.Layer; import com.airbnb.lottie.utils.LottieValueAnimator; import org.junit.Before; import org.junit.Test; import org.mockito.InOrder; import org.mockito.Mockito; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import static junit.framework.Assert.assertEquals; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.times; public class LottieValueAnimatorUnitTest extends BaseTest { private interface VerifyListener { void verify(InOrder inOrder); } private LottieComposition composition; private LottieValueAnimator animator; private Animator.AnimatorListener spyListener; private InOrder inOrder; private AtomicBoolean isDone; @Before public void setup() { animator = createAnimator(); composition = createComposition(0, 1000); animator.setComposition(composition); spyListener = Mockito.mock(Animator.AnimatorListener.class); isDone = new AtomicBoolean(false); } private LottieValueAnimator createAnimator() { // Choreographer#postFrameCallback hangs with robolectric. return new LottieValueAnimator() { @Override public void postFrameCallback() { running = true; } @Override public void removeFrameCallback() { running = false; } }; } private LottieComposition createComposition(int startFrame, int endFrame) { LottieComposition composition = new LottieComposition(); composition.init(new Rect(), startFrame, endFrame, 1000, new ArrayList<Layer>(), new LongSparseArray<Layer>(0), new HashMap<String, List<Layer>>(0), new HashMap<String, LottieImageAsset>(0), new SparseArrayCompat<FontCharacter>(0), new HashMap<String, Font>(0), new ArrayList<Marker>()); return composition; } @Test public void testInitialState() { assertClose(0f, animator.getFrame()); } @Test public void testResumingMaintainsValue() { animator.setFrame(500); animator.resumeAnimation(); assertClose(500f, animator.getFrame()); } @Test public void testFrameConvertsToAnimatedFraction() { animator.setFrame(500); animator.resumeAnimation(); assertClose(0.5f, animator.getAnimatedFraction()); assertClose(0.5f, animator.getAnimatedValueAbsolute()); } @Test public void testPlayingResetsValue() { animator.setFrame(500); animator.playAnimation(); assertClose(0f, animator.getFrame()); assertClose(0f, animator.getAnimatedFraction()); } @Test public void testReversingMaintainsValue() { animator.setFrame(250); animator.reverseAnimationSpeed(); assertClose(250f, animator.getFrame()); assertClose(0.75f, animator.getAnimatedFraction()); assertClose(0.25f, animator.getAnimatedValueAbsolute()); } @Test public void testReversingWithMinValueMaintainsValue() { animator.setMinFrame(100); animator.setFrame(1000); animator.reverseAnimationSpeed(); assertClose(1000f, animator.getFrame()); assertClose(0f, animator.getAnimatedFraction()); assertClose(1f, animator.getAnimatedValueAbsolute()); } @Test public void testReversingWithMaxValueMaintainsValue() { animator.setMaxFrame(900); animator.reverseAnimationSpeed(); assertClose(0f, animator.getFrame()); assertClose(1f, animator.getAnimatedFraction()); assertClose(0f, animator.getAnimatedValueAbsolute()); } @Test public void testResumeReversingWithMinValueMaintainsValue() { animator.setMaxFrame(900); animator.reverseAnimationSpeed(); animator.resumeAnimation(); assertClose(900f, animator.getFrame()); assertClose(0f, animator.getAnimatedFraction()); assertClose(0.9f, animator.getAnimatedValueAbsolute()); } @Test public void testPlayReversingWithMinValueMaintainsValue() { animator.setMaxFrame(900); animator.reverseAnimationSpeed(); animator.playAnimation(); assertClose(900f, animator.getFrame()); assertClose(0f, animator.getAnimatedFraction()); assertClose(0.9f, animator.getAnimatedValueAbsolute()); } @Test public void testMinAndMaxBothSet() { animator.setMinFrame(200); animator.setMaxFrame(800); animator.setFrame(400); assertClose(0.33333f, animator.getAnimatedFraction()); assertClose(0.4f, animator.getAnimatedValueAbsolute()); animator.reverseAnimationSpeed(); assertClose(400f, animator.getFrame()); assertClose(0.66666f, animator.getAnimatedFraction()); assertClose(0.4f, animator.getAnimatedValueAbsolute()); animator.resumeAnimation(); assertClose(400f, animator.getFrame()); assertClose(0.66666f, animator.getAnimatedFraction()); assertClose(0.4f, animator.getAnimatedValueAbsolute()); animator.playAnimation(); assertClose(800f, animator.getFrame()); assertClose(0f, animator.getAnimatedFraction()); assertClose(0.8f, animator.getAnimatedValueAbsolute()); } @Test public void testSetFrameIntegrity() { animator.setMinAndMaxFrames(200, 800); // setFrame < minFrame should clamp to minFrame animator.setFrame(100); assertEquals(200f, animator.getFrame()); animator.setFrame(900); assertEquals(800f, animator.getFrame()); } @Test(expected = IllegalArgumentException.class) public void testMinAndMaxFrameIntegrity() { animator.setMinAndMaxFrames(800, 200); } @Test public void testDefaultAnimator() { testAnimator(new VerifyListener() { @Override public void verify(InOrder inOrder) { inOrder.verify(spyListener, times(1)).onAnimationStart(animator, false); inOrder.verify(spyListener, times(1)).onAnimationEnd(animator, false); Mockito.verify(spyListener, times(0)).onAnimationCancel(animator); Mockito.verify(spyListener, times(0)).onAnimationRepeat(animator); } }); } @Test public void testReverseAnimator() { animator.reverseAnimationSpeed(); testAnimator(new VerifyListener() { @Override public void verify(InOrder inOrder) { inOrder.verify(spyListener, times(1)).onAnimationStart(animator, true); inOrder.verify(spyListener, times(1)).onAnimationEnd(animator, true); Mockito.verify(spyListener, times(0)).onAnimationCancel(animator); Mockito.verify(spyListener, times(0)).onAnimationRepeat(animator); } }); } @Test public void testLoopingAnimatorOnce() { animator.setRepeatCount(1); testAnimator(new VerifyListener() { @Override public void verify(InOrder inOrder) { Mockito.verify(spyListener, times(1)).onAnimationStart(animator, false); Mockito.verify(spyListener, times(1)).onAnimationRepeat(animator); Mockito.verify(spyListener, times(1)).onAnimationEnd(animator, false); Mockito.verify(spyListener, times(0)).onAnimationCancel(animator); } }); } @Test public void testLoopingAnimatorZeroTimes() { animator.setRepeatCount(0); testAnimator(new VerifyListener() { @Override public void verify(InOrder inOrder) { Mockito.verify(spyListener, times(1)).onAnimationStart(animator, false); Mockito.verify(spyListener, times(0)).onAnimationRepeat(animator); Mockito.verify(spyListener, times(1)).onAnimationEnd(animator, false); Mockito.verify(spyListener, times(0)).onAnimationCancel(animator); } }); } @Test public void testLoopingAnimatorTwice() { animator.setRepeatCount(2); testAnimator(new VerifyListener() { @Override public void verify(InOrder inOrder) { Mockito.verify(spyListener, times(1)).onAnimationStart(animator, false); Mockito.verify(spyListener, times(2)).onAnimationRepeat(animator); Mockito.verify(spyListener, times(1)).onAnimationEnd(animator, false); Mockito.verify(spyListener, times(0)).onAnimationCancel(animator); } }); } @Test public void testLoopingAnimatorOnceReverse() { animator.setFrame(1000); animator.setRepeatCount(1); animator.reverseAnimationSpeed(); testAnimator(new VerifyListener() { @Override public void verify(InOrder inOrder) { inOrder.verify(spyListener, times(1)).onAnimationStart(animator, true); inOrder.verify(spyListener, times(1)).onAnimationRepeat(animator); inOrder.verify(spyListener, times(1)).onAnimationEnd(animator, true); Mockito.verify(spyListener, times(0)).onAnimationCancel(animator); } }); } @Test public void setMinFrameSmallerThanComposition() { animator.setMinFrame(-9000); assertClose(animator.getMinFrame(), composition.getStartFrame()); } @Test public void setMaxFrameLargerThanComposition() { animator.setMaxFrame(9000); assertClose(animator.getMaxFrame(), composition.getEndFrame()); } @Test public void setMinFrameBeforeComposition() { LottieValueAnimator animator = createAnimator(); animator.setMinFrame(100); animator.setComposition(composition); assertClose(100.0f, animator.getMinFrame()); } @Test public void setMaxFrameBeforeComposition() { LottieValueAnimator animator = createAnimator(); animator.setMaxFrame(100); animator.setComposition(composition); assertClose(100.0f, animator.getMaxFrame()); } @Test public void setMinAndMaxFrameBeforeComposition() { LottieValueAnimator animator = createAnimator(); animator.setMinAndMaxFrames(100, 900); animator.setComposition(composition); assertClose(100.0f, animator.getMinFrame()); assertClose(900.0f, animator.getMaxFrame()); } @Test public void setMinFrameAfterComposition() { LottieValueAnimator animator = createAnimator(); animator.setComposition(composition); animator.setMinFrame(100); assertClose(100.0f, animator.getMinFrame()); } @Test public void setMaxFrameAfterComposition() { LottieValueAnimator animator = createAnimator(); animator.setComposition(composition); animator.setMaxFrame(100); assertEquals(100.0f, animator.getMaxFrame()); } @Test public void setMinAndMaxFrameAfterComposition() { LottieValueAnimator animator = createAnimator(); animator.setComposition(composition); animator.setMinAndMaxFrames(100, 900); assertClose(100.0f, animator.getMinFrame()); assertClose(900.0f, animator.getMaxFrame()); } @Test public void maxFrameOfNewShorterComposition() { LottieValueAnimator animator = createAnimator(); animator.setComposition(composition); LottieComposition composition2 = createComposition(0, 500); animator.setComposition(composition2); assertClose(500.0f, animator.getMaxFrame()); } @Test public void maxFrameOfNewLongerComposition() { LottieValueAnimator animator = createAnimator(); animator.setComposition(composition); LottieComposition composition2 = createComposition(0, 1500); animator.setComposition(composition2); assertClose(1500.0f, animator.getMaxFrame()); } @Test public void clearComposition() { animator.clearComposition(); assertClose(0.0f, animator.getMaxFrame()); assertClose(0.0f, animator.getMinFrame()); } @Test public void resetComposition() { animator.clearComposition(); animator.setComposition(composition); assertClose(0.0f, animator.getMinFrame()); assertClose(1000.0f, animator.getMaxFrame()); } @Test public void resetAndSetMinBeforeComposition() { animator.clearComposition(); animator.setMinFrame(100); animator.setComposition(composition); assertClose(100.0f, animator.getMinFrame()); assertClose(1000.0f, animator.getMaxFrame()); } @Test public void resetAndSetMinAterComposition() { animator.clearComposition(); animator.setComposition(composition); animator.setMinFrame(100); assertClose(100.0f, animator.getMinFrame()); assertClose(1000.0f, animator.getMaxFrame()); } private void testAnimator(final VerifyListener verifyListener) { spyListener = Mockito.spy(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { verifyListener.verify(inOrder); isDone.set(true); } }); inOrder = inOrder(spyListener); animator.addListener(spyListener); animator.playAnimation(); while (!isDone.get()) { animator.doFrame(System.nanoTime()); } } /** * Animations don't render on the out frame so if an animation is 1000 frames, the actual end will be 999.99. This causes * actual fractions to be something like .74999 when you might expect 75. */ private static void assertClose(float expected, float actual) { assertEquals(expected, actual, expected * 0.01f); } }