diff --git a/res/layout/preference_animated_image.xml b/res/layout/preference_animated_image.xml index e7d9b52518a..0ab8adf275d 100644 --- a/res/layout/preference_animated_image.xml +++ b/res/layout/preference_animated_image.xml @@ -31,4 +31,14 @@ android:focusable="false" android:clickable="false" android:adjustViewBounds="true"/> + + \ No newline at end of file diff --git a/src/com/android/settings/accessibility/AnimatedImagePreference.java b/src/com/android/settings/accessibility/AnimatedImagePreference.java index 2ca13f33fe2..c707e5cc0a7 100644 --- a/src/com/android/settings/accessibility/AnimatedImagePreference.java +++ b/src/com/android/settings/accessibility/AnimatedImagePreference.java @@ -17,9 +17,14 @@ package com.android.settings.accessibility; import android.content.Context; -import android.graphics.drawable.AnimatedImageDrawable; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Animatable2; +import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; import android.widget.ImageView; import androidx.preference.Preference; @@ -27,15 +32,31 @@ import androidx.preference.PreferenceViewHolder; import com.android.settings.R; +import com.airbnb.lottie.LottieAnimationView; +import com.airbnb.lottie.LottieDrawable; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.Objects; + /** * A custom {@link ImageView} preference for showing animated or static image, such as animated webp and static png. */ public class AnimatedImagePreference extends Preference { + private static final String TAG = "AnimatedImagePreference"; private Uri mImageUri; private int mMaxHeight = -1; + private final Animatable2.AnimationCallback mAnimationCallback = + new Animatable2.AnimationCallback() { + @Override + public void onAnimationEnd(Drawable drawable) { + ((Animatable2) drawable).start(); + } + }; + AnimatedImagePreference(Context context) { super(context); setLayoutResource(R.layout.preference_animated_image); @@ -46,21 +67,27 @@ public class AnimatedImagePreference extends Preference { super.onBindViewHolder(holder); final ImageView imageView = holder.itemView.findViewById(R.id.animated_img); - if (imageView == null) { + final LottieAnimationView lottieView = holder.itemView.findViewById(R.id.lottie_view); + if (imageView == null || lottieView == null) { return; } if (mImageUri != null) { - imageView.setImageURI(mImageUri); + resetAnimations(imageView, lottieView); + hideAllChildViews(holder.itemView); - final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AnimatedImageDrawable) { - ((AnimatedImageDrawable) drawable).start(); + imageView.setImageURI(mImageUri); + if (imageView.getDrawable() != null) { + startAnimationWith(imageView); + } else { + // The lottie image from the raw folder also returns null. + startLottieAnimationWith(lottieView); } } if (mMaxHeight > -1) { imageView.setMaxHeight(mMaxHeight); + lottieView.setMaxHeight(mMaxHeight); } } @@ -87,4 +114,68 @@ public class AnimatedImagePreference extends Preference { notifyChanged(); } } + + private void startAnimationWith(ImageView imageView) { + startAnimation(imageView.getDrawable()); + + imageView.setVisibility(View.VISIBLE); + } + + private void startLottieAnimationWith(LottieAnimationView lottieView) { + final InputStream inputStream = getInputStreamFromUri(mImageUri); + Objects.requireNonNull(inputStream, "Invalid resource."); + lottieView.setAnimation(inputStream, /* cacheKey= */ null); + lottieView.setRepeatCount(LottieDrawable.INFINITE); + lottieView.playAnimation(); + + lottieView.setVisibility(View.VISIBLE); + } + + private void startAnimation(Drawable drawable) { + if (!(drawable instanceof Animatable)) { + return; + } + + if (drawable instanceof Animatable2) { + ((Animatable2) drawable).registerAnimationCallback(mAnimationCallback); + } else if (drawable instanceof AnimationDrawable) { + ((AnimationDrawable) drawable).setOneShot(false); + } + + ((Animatable) drawable).start(); + } + + private void resetAnimations(ImageView imageView, LottieAnimationView lottieView) { + resetAnimation(imageView.getDrawable()); + + lottieView.cancelAnimation(); + } + + private void resetAnimation(Drawable drawable) { + if (!(drawable instanceof Animatable)) { + return; + } + + if (drawable instanceof Animatable2) { + ((Animatable2) drawable).clearAnimationCallbacks(); + } + + ((Animatable) drawable).stop(); + } + + private InputStream getInputStreamFromUri(Uri uri) { + try { + return getContext().getContentResolver().openInputStream(uri); + } catch (FileNotFoundException e) { + Log.w(TAG, "Cannot find content uri: " + uri, e); + return null; + } + } + + private void hideAllChildViews(View itemView) { + final ViewGroup viewGroup = (ViewGroup) itemView; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + viewGroup.getChildAt(i).setVisibility(View.GONE); + } + } } diff --git a/tests/robotests/src/com/android/settings/accessibility/AnimatedImagePreferenceTest.java b/tests/robotests/src/com/android/settings/accessibility/AnimatedImagePreferenceTest.java index c3dd7b5af33..4bce0bb264b 100644 --- a/tests/robotests/src/com/android/settings/accessibility/AnimatedImagePreferenceTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/AnimatedImagePreferenceTest.java @@ -18,13 +18,19 @@ package com.android.settings.accessibility; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import android.content.ContentResolver; import android.content.Context; import android.graphics.drawable.AnimatedImageDrawable; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.AnimationDrawable; import android.net.Uri; import android.view.LayoutInflater; import android.view.View; @@ -34,18 +40,22 @@ import androidx.preference.PreferenceViewHolder; import com.android.settings.R; +import com.airbnb.lottie.LottieAnimationView; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import java.io.InputStream; + /** Tests for {@link AnimatedImagePreference}. */ @RunWith(RobolectricTestRunner.class) public class AnimatedImagePreferenceTest { + private final Context mContext = RuntimeEnvironment.application; private Uri mImageUri; private View mRootView; private PreferenceViewHolder mViewHolder; @@ -54,32 +64,53 @@ public class AnimatedImagePreferenceTest { @Spy private ImageView mImageView; - @Mock - private AnimatedImageDrawable mAnimatedImageDrawable; - @Before public void init() { MockitoAnnotations.initMocks(this); - final Context context = RuntimeEnvironment.application; - final LayoutInflater inflater = LayoutInflater.from(context); + final LayoutInflater inflater = LayoutInflater.from(mContext); mRootView = spy(inflater.inflate(R.layout.preference_animated_image, /* root= */ null)); mViewHolder = spy(PreferenceViewHolder.createInstanceForTests(mRootView)); - mImageView = spy(new ImageView(context)); + mImageView = spy(new ImageView(mContext)); - mAnimatedImagePreference = new AnimatedImagePreference(context); + mAnimatedImagePreference = new AnimatedImagePreference(mContext); mImageUri = new Uri.Builder().build(); } @Test - public void readImageUri_animatedImage_startAnimation() { + public void playAnimation_animatedImageDrawable_success() { + final AnimatedImageDrawable drawable = mock(AnimatedImageDrawable.class); doReturn(mImageView).when(mRootView).findViewById(R.id.animated_img); - doReturn(mAnimatedImageDrawable).when(mImageView).getDrawable(); + doReturn(drawable).when(mImageView).getDrawable(); mAnimatedImagePreference.setImageUri(mImageUri); mAnimatedImagePreference.onBindViewHolder(mViewHolder); - verify(mAnimatedImageDrawable).start(); + verify(drawable).start(); + } + + @Test + public void playAnimation_animatedVectorDrawable_success() { + final AnimatedVectorDrawable drawable = mock(AnimatedVectorDrawable.class); + doReturn(mImageView).when(mRootView).findViewById(R.id.animated_img); + doReturn(drawable).when(mImageView).getDrawable(); + + mAnimatedImagePreference.setImageUri(mImageUri); + mAnimatedImagePreference.onBindViewHolder(mViewHolder); + + verify(drawable).start(); + } + + @Test + public void playAnimation_animationDrawable_success() { + final AnimationDrawable drawable = mock(AnimationDrawable.class); + doReturn(mImageView).when(mRootView).findViewById(R.id.animated_img); + doReturn(drawable).when(mImageView).getDrawable(); + + mAnimatedImagePreference.setImageUri(mImageUri); + mAnimatedImagePreference.onBindViewHolder(mViewHolder); + + verify(drawable).start(); } @Test @@ -102,4 +133,22 @@ public class AnimatedImagePreferenceTest { assertThat(mImageView.getMaxHeight()).isEqualTo(maxHeight); } + + @Test + public void setImageUriAndRebindViewHolder_lottieImageFromRawFolder_setAnimation() { + final int fakeLottieResId = 111111; + final Uri lottieImageUri = + new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(mContext.getPackageName()) + .appendPath(String.valueOf(fakeLottieResId)) + .build(); + final LottieAnimationView lottieView = spy(new LottieAnimationView(mContext)); + doReturn(mImageView).when(mRootView).findViewById(R.id.animated_img); + doReturn(lottieView).when(mRootView).findViewById(R.id.lottie_view); + + mAnimatedImagePreference.setImageUri(lottieImageUri); + mAnimatedImagePreference.onBindViewHolder(mViewHolder); + + verify(lottieView).setAnimation(any(InputStream.class), eq(null)); + } }