diff --git a/lottie/src/main/java/com/airbnb/lottie/L.java b/lottie/src/main/java/com/airbnb/lottie/L.java index bf8f326fa3..6798153d8b 100644 --- a/lottie/src/main/java/com/airbnb/lottie/L.java +++ b/lottie/src/main/java/com/airbnb/lottie/L.java @@ -6,6 +6,8 @@ import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; +import com.airbnb.lottie.configurations.reducemotion.ReducedMotionOption; +import com.airbnb.lottie.configurations.reducemotion.SystemReducedMotionOption; import com.airbnb.lottie.network.DefaultLottieNetworkFetcher; import com.airbnb.lottie.network.LottieNetworkCacheProvider; import com.airbnb.lottie.network.LottieNetworkFetcher; @@ -32,6 +34,7 @@ public class L { private static volatile NetworkFetcher networkFetcher; private static volatile NetworkCache networkCache; private static ThreadLocal lottieTrace; + private static ReducedMotionOption reducedMotionOption = new SystemReducedMotionOption(); private L() { } @@ -143,4 +146,10 @@ public static void setDefaultAsyncUpdates(AsyncUpdates asyncUpdates) { public static AsyncUpdates getDefaultAsyncUpdates() { return L.defaultAsyncUpdates; } + + public static void setReducedMotionOption(ReducedMotionOption reducedMotionOption){ + L.reducedMotionOption = reducedMotionOption; + } + + public static ReducedMotionOption getReducedMotionOption(){return reducedMotionOption;} } diff --git a/lottie/src/main/java/com/airbnb/lottie/Lottie.java b/lottie/src/main/java/com/airbnb/lottie/Lottie.java index c6274a39cb..672724be2e 100644 --- a/lottie/src/main/java/com/airbnb/lottie/Lottie.java +++ b/lottie/src/main/java/com/airbnb/lottie/Lottie.java @@ -22,5 +22,6 @@ public static void initialize(@NonNull final LottieConfig lottieConfig) { L.setNetworkCacheEnabled(lottieConfig.enableNetworkCache); L.setDisablePathInterpolatorCache(lottieConfig.disablePathInterpolatorCache); L.setDefaultAsyncUpdates(lottieConfig.defaultAsyncUpdates); + L.setReducedMotionOption(lottieConfig.reducedMotionOption); } } diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java index 05b1518f65..6152fef7b7 100644 --- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java +++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java @@ -260,8 +260,6 @@ private void init(@Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { } ta.recycle(); - - lottieDrawable.setSystemAnimationsAreEnabled(Utils.getAnimationScale(getContext()) != 0f); } @Override public void setImageResource(int resId) { @@ -377,7 +375,10 @@ private void init(@Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { * Defaults to false. * * @param ignore if true animations will run even when they are disabled in the system settings. + * @deprecated Use {@link com.airbnb.lottie.configurations.reducemotion.IgnoreDisabledSystemAnimationsOption} + * instead and set them on the {@link LottieConfig} */ + @Deprecated public void setIgnoreDisabledSystemAnimations(boolean ignore) { lottieDrawable.setIgnoreDisabledSystemAnimations(ignore); } diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieConfig.java b/lottie/src/main/java/com/airbnb/lottie/LottieConfig.java index 09f5e57bcb..d7b3cd5a3f 100644 --- a/lottie/src/main/java/com/airbnb/lottie/LottieConfig.java +++ b/lottie/src/main/java/com/airbnb/lottie/LottieConfig.java @@ -2,7 +2,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import com.airbnb.lottie.configurations.reducemotion.ReducedMotionOption; +import com.airbnb.lottie.configurations.reducemotion.IgnoreDisabledSystemAnimationsOption; +import com.airbnb.lottie.configurations.reducemotion.SystemReducedMotionOption; import com.airbnb.lottie.network.LottieNetworkCacheProvider; import com.airbnb.lottie.network.LottieNetworkFetcher; @@ -21,16 +23,18 @@ public class LottieConfig { final boolean enableNetworkCache; final boolean disablePathInterpolatorCache; final AsyncUpdates defaultAsyncUpdates; + final ReducedMotionOption reducedMotionOption; private LottieConfig(@Nullable LottieNetworkFetcher networkFetcher, @Nullable LottieNetworkCacheProvider cacheProvider, boolean enableSystraceMarkers, boolean enableNetworkCache, boolean disablePathInterpolatorCache, - AsyncUpdates defaultAsyncUpdates) { + AsyncUpdates defaultAsyncUpdates, ReducedMotionOption reducedMotionOption) { this.networkFetcher = networkFetcher; this.cacheProvider = cacheProvider; this.enableSystraceMarkers = enableSystraceMarkers; this.enableNetworkCache = enableNetworkCache; this.disablePathInterpolatorCache = disablePathInterpolatorCache; this.defaultAsyncUpdates = defaultAsyncUpdates; + this.reducedMotionOption = reducedMotionOption; } public static final class Builder { @@ -43,6 +47,7 @@ public static final class Builder { private boolean enableNetworkCache = true; private boolean disablePathInterpolatorCache = true; private AsyncUpdates defaultAsyncUpdates = AsyncUpdates.AUTOMATIC; + private ReducedMotionOption reducedMotionOption = new SystemReducedMotionOption(); /** * Lottie has a default network fetching stack built on {@link java.net.HttpURLConnection}. However, if you would like to hook into your own @@ -122,7 +127,7 @@ public Builder setEnableNetworkCache(boolean enable) { * When parsing animations, Lottie has a path interpolator cache. This cache allows Lottie to reuse PathInterpolators * across an animation. This is desirable in most cases. However, when shared across screenshot tests, it can cause slight * deviations in the rendering due to underlying approximations in the PathInterpolator. - * + *

* The cache is enabled by default and should probably only be disabled for screenshot tests. */ @NonNull @@ -133,6 +138,7 @@ public Builder setDisablePathInterpolatorCache(boolean disable) { /** * Sets the default value for async updates. + * * @see LottieDrawable#setAsyncUpdates(AsyncUpdates) */ @NonNull @@ -141,10 +147,25 @@ public Builder setDefaultAsyncUpdates(AsyncUpdates asyncUpdates) { return this; } + /** + * Provide your own reduce motion option. By default it uses + * {@link SystemReducedMotionOption}, + * which returns ReducedMotionMode.REDUCED_MOTION when the system AnimationScale is set to 0. + *

+ * You can override this behavior by providing your own custom {@link ReducedMotionOption}. + * You can also use {@link IgnoreDisabledSystemAnimationsOption} + * if you want to ignore the system settings and always play the full animation. + */ + @NonNull + public Builder setReducedMotionOption(ReducedMotionOption reducedMotionOption) { + this.reducedMotionOption = reducedMotionOption; + return this; + } + @NonNull public LottieConfig build() { return new LottieConfig(networkFetcher, cacheProvider, enableSystraceMarkers, enableNetworkCache, disablePathInterpolatorCache, - defaultAsyncUpdates); + defaultAsyncUpdates, reducedMotionOption); } } } diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java index dfcae27c7b..edcb668b62 100644 --- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java +++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java @@ -33,6 +33,7 @@ import androidx.annotation.RestrictTo; import com.airbnb.lottie.animation.LPaint; +import com.airbnb.lottie.configurations.reducemotion.ReducedMotionMode; import com.airbnb.lottie.manager.FontAssetManager; import com.airbnb.lottie.manager.ImageAssetManager; import com.airbnb.lottie.model.Font; @@ -1244,7 +1245,11 @@ boolean isAnimatingOrWillAnimateOnVisible() { } private boolean animationsEnabled() { - return systemAnimationsEnabled || ignoreSystemAnimationsDisabled; + if (ignoreSystemAnimationsDisabled) { + return true; + } + return systemAnimationsEnabled && + L.getReducedMotionOption().getCurrentReducedMotionMode(getContext()) == ReducedMotionMode.STANDARD_MOTION; } /** @@ -1256,7 +1261,10 @@ private boolean animationsEnabled() { * - reducedMotion * - reduced_motion * - reduced-motion + * + * @deprecated Use {@link com.airbnb.lottie.configurations.reducemotion.ReducedMotionOption} instead and set them on the {@link LottieConfig} */ + @Deprecated public void setSystemAnimationsAreEnabled(Boolean areEnabled) { systemAnimationsEnabled = areEnabled; } @@ -1269,7 +1277,10 @@ public void setSystemAnimationsAreEnabled(Boolean areEnabled) { * Defaults to false. * * @param ignore if true animations will run even when they are disabled in the system settings. + * @deprecated Use {@link com.airbnb.lottie.configurations.reducemotion.IgnoreDisabledSystemAnimationsOption} + * instead and set them on the {@link LottieConfig} */ + @Deprecated public void setIgnoreDisabledSystemAnimations(boolean ignore) { ignoreSystemAnimationsDisabled = ignore; } diff --git a/lottie/src/main/java/com/airbnb/lottie/configurations/reducemotion/IgnoreDisabledSystemAnimationsOption.java b/lottie/src/main/java/com/airbnb/lottie/configurations/reducemotion/IgnoreDisabledSystemAnimationsOption.java new file mode 100644 index 0000000000..daac9a0215 --- /dev/null +++ b/lottie/src/main/java/com/airbnb/lottie/configurations/reducemotion/IgnoreDisabledSystemAnimationsOption.java @@ -0,0 +1,14 @@ +package com.airbnb.lottie.configurations.reducemotion; + +import android.content.Context; + +/** + * Allows ignoring system animations settings, therefore allowing animations to run even if they are disabled. + */ +public class IgnoreDisabledSystemAnimationsOption implements ReducedMotionOption { + + @Override + public ReducedMotionMode getCurrentReducedMotionMode(Context context) { + return ReducedMotionMode.STANDARD_MOTION; + } +} diff --git a/lottie/src/main/java/com/airbnb/lottie/configurations/reducemotion/ReducedMotionMode.java b/lottie/src/main/java/com/airbnb/lottie/configurations/reducemotion/ReducedMotionMode.java new file mode 100644 index 0000000000..717e0df389 --- /dev/null +++ b/lottie/src/main/java/com/airbnb/lottie/configurations/reducemotion/ReducedMotionMode.java @@ -0,0 +1,16 @@ +package com.airbnb.lottie.configurations.reducemotion; + + +public enum ReducedMotionMode { + /** + * The default behavior where Lottie animations play normally with no overrides. + * By default this mode is used when {@link com.airbnb.lottie.utils.Utils#getAnimationScale(Context)} is not 0. + */ + STANDARD_MOTION, + + /** + * Lottie animations with a "reduced motion" marker will play that marker instead of any other animations. + * By default this mode is used when {@link com.airbnb.lottie.utils.Utils#getAnimationScale(Context)} == 0. + */ + REDUCED_MOTION +} diff --git a/lottie/src/main/java/com/airbnb/lottie/configurations/reducemotion/ReducedMotionOption.java b/lottie/src/main/java/com/airbnb/lottie/configurations/reducemotion/ReducedMotionOption.java new file mode 100644 index 0000000000..85d1ae2a24 --- /dev/null +++ b/lottie/src/main/java/com/airbnb/lottie/configurations/reducemotion/ReducedMotionOption.java @@ -0,0 +1,11 @@ +package com.airbnb.lottie.configurations.reducemotion; + +import android.content.Context; + +public interface ReducedMotionOption { + + /** + * Returns the current reduced motion mode. + */ + ReducedMotionMode getCurrentReducedMotionMode(Context context); +} diff --git a/lottie/src/main/java/com/airbnb/lottie/configurations/reducemotion/SystemReducedMotionOption.java b/lottie/src/main/java/com/airbnb/lottie/configurations/reducemotion/SystemReducedMotionOption.java new file mode 100644 index 0000000000..d69f55efa4 --- /dev/null +++ b/lottie/src/main/java/com/airbnb/lottie/configurations/reducemotion/SystemReducedMotionOption.java @@ -0,0 +1,28 @@ +package com.airbnb.lottie.configurations.reducemotion; + +import android.content.Context; +import com.airbnb.lottie.utils.Utils; + +/** + * Lottie animations with a "reduced motion" marker will play that marker instead of any other animations. + * This class uses {@link com.airbnb.lottie.utils.Utils#getAnimationScale(Context)} to determine if animations are disabled + * and if it should play the reduced motion marker. + * + * If the animation is provided a "reduced motion" + * marker name, they will be shown instead of the first or last frame. Supported marker names are case insensitive, and include: + * - reduced motion + * - reducedMotion + * - reduced_motion + * - reduced-motion + */ +public class SystemReducedMotionOption implements ReducedMotionOption { + + @Override + public ReducedMotionMode getCurrentReducedMotionMode(Context context) { + if (Utils.getAnimationScale(context) != 0f) { + return ReducedMotionMode.STANDARD_MOTION; + } else { + return ReducedMotionMode.REDUCED_MOTION; + } + } +} diff --git a/lottie/src/main/res/values/attrs.xml b/lottie/src/main/res/values/attrs.xml index 31af44a5f3..d9299f36ff 100644 --- a/lottie/src/main/res/values/attrs.xml +++ b/lottie/src/main/res/values/attrs.xml @@ -20,8 +20,8 @@ - + diff --git a/lottie/src/test/java/com/airbnb/lottie/LottieDrawableTest.java b/lottie/src/test/java/com/airbnb/lottie/LottieDrawableTest.java index 9c50952062..9257c01d4e 100644 --- a/lottie/src/test/java/com/airbnb/lottie/LottieDrawableTest.java +++ b/lottie/src/test/java/com/airbnb/lottie/LottieDrawableTest.java @@ -4,6 +4,7 @@ import android.graphics.Rect; import androidx.collection.LongSparseArray; import androidx.collection.SparseArrayCompat; +import com.airbnb.lottie.configurations.reducemotion.ReducedMotionMode; import org.junit.Before; import org.junit.Test; @@ -77,6 +78,7 @@ public void testMinMaxFrame() { @Test public void testPlayWhenSystemAnimationDisabled() { + disableSystemAnimation(); LottieComposition composition = createComposition(31, 391); LottieDrawable drawable = new LottieDrawable(); drawable.addAnimatorListener(animatorListener); @@ -98,4 +100,34 @@ public void testResumeWhenSystemAnimationDisabled() { assertEquals(391, drawable.getFrame()); verify(animatorListener, atLeastOnce()).onAnimationEnd(any(Animator.class), eq(false)); } + + @Test + public void testPlayWhenSystemAnimationDisabledFromLottieConfig() { + disableSystemAnimation(); + LottieComposition composition = createComposition(31, 391); + LottieDrawable drawable = new LottieDrawable(); + drawable.addAnimatorListener(animatorListener); + drawable.setComposition(composition); + drawable.playAnimation(); + assertEquals(391, drawable.getFrame()); + verify(animatorListener, atLeastOnce()).onAnimationEnd(any(Animator.class), eq(false)); + } + + @Test + public void testResumeWhenSystemAnimationDisabledFromLottieConfig() { + disableSystemAnimation(); + LottieComposition composition = createComposition(31, 391); + LottieDrawable drawable = new LottieDrawable(); + drawable.addAnimatorListener(animatorListener); + drawable.setComposition(composition); + drawable.resumeAnimation(); + assertEquals(391, drawable.getFrame()); + verify(animatorListener, atLeastOnce()).onAnimationEnd(any(Animator.class), eq(false)); + } + + private void disableSystemAnimation() { + Lottie.initialize(new LottieConfig.Builder().setReducedMotionOption( + context -> ReducedMotionMode.REDUCED_MOTION + ).build()); + } } diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/DisabledAnimationsTestCase.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/DisabledAnimationsTestCase.kt index b669635bc3..0499c0b613 100644 --- a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/DisabledAnimationsTestCase.kt +++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/DisabledAnimationsTestCase.kt @@ -1,5 +1,8 @@ package com.airbnb.lottie.snapshots.tests +import com.airbnb.lottie.Lottie +import com.airbnb.lottie.LottieConfig +import com.airbnb.lottie.configurations.reducemotion.ReducedMotionMode import com.airbnb.lottie.snapshots.SnapshotTestCase import com.airbnb.lottie.snapshots.SnapshotTestCaseContext import com.airbnb.lottie.snapshots.withDrawable @@ -21,5 +24,30 @@ class DisabledAnimationsTestCase : SnapshotTestCase { drawable.playAnimation() } } + withDrawable("Tests/ReducedMotion.json", "System Animations", "Lottie config Disabled") { drawable -> + withContext(Dispatchers.Main) { + disableSystemAnimation() + drawable.playAnimation() + } + } + + withDrawable("Tests/ReducedMotion.json", "System Animations", "Lottie config enabled") { drawable -> + withContext(Dispatchers.Main) { + disableSystemAnimation(disable = false) + drawable.playAnimation() + } + } + } + + private fun disableSystemAnimation(disable: Boolean = true) { + Lottie.initialize( + LottieConfig.Builder().setReducedMotionOption { + if (disable) { + ReducedMotionMode.REDUCED_MOTION + } else { + ReducedMotionMode.STANDARD_MOTION + } + }.build(), + ) } }