diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java index e91dd94c715df..f04b0e338959f 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java @@ -18,18 +18,29 @@ package com.android.settingslib.widget; import android.content.Context; import android.content.res.TypedArray; +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.AttributeSet; import android.util.Log; import android.view.View; +import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.FrameLayout; import android.widget.ImageView; -import androidx.annotation.VisibleForTesting; +import androidx.annotation.RawRes; import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; +import androidx.vectordrawable.graphics.drawable.Animatable2Compat; import com.airbnb.lottie.LottieAnimationView; +import com.airbnb.lottie.LottieDrawable; + +import java.io.FileNotFoundException; +import java.io.InputStream; /** * IllustrationPreference is a preference that can play lottie format animation @@ -40,11 +51,32 @@ public class IllustrationPreference extends Preference { private static final boolean IS_ENABLED_LOTTIE_ADAPTIVE_COLOR = false; - private int mAnimationId; + private int mImageResId; private boolean mIsAutoScale; - private LottieAnimationView mIllustrationView; + private Uri mImageUri; + private Drawable mImageDrawable; private View mMiddleGroundView; - private FrameLayout mMiddleGroundLayout; + + private final Animatable2.AnimationCallback mAnimationCallback = + new Animatable2.AnimationCallback() { + @Override + public void onAnimationEnd(Drawable drawable) { + ((Animatable) drawable).start(); + } + }; + + private final Animatable2Compat.AnimationCallback mAnimationCallbackCompat = + new Animatable2Compat.AnimationCallback() { + @Override + public void onAnimationEnd(Drawable drawable) { + ((Animatable) drawable).start(); + } + }; + + public IllustrationPreference(Context context) { + super(context); + init(context, /* attrs= */ null); + } public IllustrationPreference(Context context, AttributeSet attrs) { super(context, attrs); @@ -65,10 +97,11 @@ public class IllustrationPreference extends Preference { @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); - if (mAnimationId == 0) { - Log.w(TAG, "Invalid illustration resource id."); - return; - } + + final FrameLayout middleGroundLayout = + (FrameLayout) holder.findViewById(R.id.middleground_layout); + final LottieAnimationView illustrationView = + (LottieAnimationView) holder.findViewById(R.id.lottie_view); // To solve the problem of non-compliant illustrations, we set the frame height // to 300dp and set the length of the short side of the screen to @@ -81,73 +114,208 @@ public class IllustrationPreference extends Preference { lp.width = screenWidth < screenHeight ? screenWidth : screenHeight; illustrationFrame.setLayoutParams(lp); - mMiddleGroundLayout = (FrameLayout) holder.findViewById(R.id.middleground_layout); - mIllustrationView = (LottieAnimationView) holder.findViewById(R.id.lottie_view); - mIllustrationView.setAnimation(mAnimationId); - mIllustrationView.loop(true); - mIllustrationView.playAnimation(); - if (mIsAutoScale) { - enableAnimationAutoScale(mIsAutoScale); - } - if (mMiddleGroundView != null) { - enableMiddleGroundView(); - } - if (IS_ENABLED_LOTTIE_ADAPTIVE_COLOR) { - ColorUtils.applyDynamicColors(getContext(), mIllustrationView); - } - } + handleImageWithAnimation(illustrationView); - @VisibleForTesting - boolean isAnimating() { - return mIllustrationView.isAnimating(); + if (mIsAutoScale) { + illustrationView.setScaleType(mIsAutoScale + ? ImageView.ScaleType.CENTER_CROP + : ImageView.ScaleType.CENTER_INSIDE); + } + + handleMiddleGroundView(middleGroundLayout); + + if (IS_ENABLED_LOTTIE_ADAPTIVE_COLOR) { + ColorUtils.applyDynamicColors(getContext(), illustrationView); + } } /** - * Set the middle ground view to preference. The user + * Sets the middle ground view to preference. The user * can overlay a view on top of the animation. */ public void setMiddleGroundView(View view) { - mMiddleGroundView = view; - if (mMiddleGroundLayout == null) { - return; + if (view != mMiddleGroundView) { + mMiddleGroundView = view; + notifyChanged(); } - enableMiddleGroundView(); } /** - * Remove the middle ground view of preference. + * Removes the middle ground view of preference. */ public void removeMiddleGroundView() { - if (mMiddleGroundLayout == null) { - return; - } - mMiddleGroundLayout.removeAllViews(); - mMiddleGroundLayout.setVisibility(View.GONE); + mMiddleGroundView = null; + notifyChanged(); } /** * Enables the auto scale feature of animation view. */ public void enableAnimationAutoScale(boolean enable) { - mIsAutoScale = enable; - if (mIllustrationView == null) { - return; + if (enable != mIsAutoScale) { + mIsAutoScale = enable; + notifyChanged(); } - mIllustrationView.setScaleType( - mIsAutoScale ? ImageView.ScaleType.CENTER_CROP : ImageView.ScaleType.CENTER_INSIDE); } /** - * Set the lottie illustration resource id. + * Sets the lottie illustration resource id. */ public void setLottieAnimationResId(int resId) { - mAnimationId = resId; + if (resId != mImageResId) { + resetImageResourceCache(); + mImageResId = resId; + notifyChanged(); + } } - private void enableMiddleGroundView() { - mMiddleGroundLayout.removeAllViews(); - mMiddleGroundLayout.addView(mMiddleGroundView); - mMiddleGroundLayout.setVisibility(View.VISIBLE); + /** + * Sets image drawable to display image in {@link LottieAnimationView} + * + * @param imageDrawable the drawable of an image + */ + public void setImageDrawable(Drawable imageDrawable) { + if (imageDrawable != mImageDrawable) { + resetImageResourceCache(); + mImageDrawable = imageDrawable; + notifyChanged(); + } + } + + /** + * Sets image uri to display image in {@link LottieAnimationView} + * + * @param imageUri the Uri of an image + */ + public void setImageUri(Uri imageUri) { + if (imageUri != mImageUri) { + resetImageResourceCache(); + mImageUri = imageUri; + notifyChanged(); + } + } + + private void resetImageResourceCache() { + mImageDrawable = null; + mImageUri = null; + mImageResId = 0; + } + + private void handleMiddleGroundView(ViewGroup middleGroundLayout) { + middleGroundLayout.removeAllViews(); + + if (mMiddleGroundView != null) { + middleGroundLayout.addView(mMiddleGroundView); + middleGroundLayout.setVisibility(View.VISIBLE); + } else { + middleGroundLayout.setVisibility(View.GONE); + } + } + + private void handleImageWithAnimation(LottieAnimationView illustrationView) { + if (mImageDrawable != null) { + resetAnimations(illustrationView); + illustrationView.setImageDrawable(mImageDrawable); + final Drawable drawable = illustrationView.getDrawable(); + if (drawable != null) { + startAnimation(drawable); + } + } + + if (mImageUri != null) { + resetAnimations(illustrationView); + illustrationView.setImageURI(mImageUri); + final Drawable drawable = illustrationView.getDrawable(); + if (drawable != null) { + startAnimation(drawable); + } else { + // The lottie image from the raw folder also returns null because the ImageView + // couldn't handle it now. + startLottieAnimationWith(illustrationView, mImageUri); + } + } + + if (mImageResId > 0) { + resetAnimations(illustrationView); + illustrationView.setImageResource(mImageResId); + final Drawable drawable = illustrationView.getDrawable(); + if (drawable != null) { + startAnimation(drawable); + } else { + // The lottie image from the raw folder also returns null because the ImageView + // couldn't handle it now. + startLottieAnimationWith(illustrationView, mImageResId); + } + } + } + + private void startAnimation(Drawable drawable) { + if (!(drawable instanceof Animatable)) { + return; + } + + if (drawable instanceof Animatable2) { + ((Animatable2) drawable).registerAnimationCallback(mAnimationCallback); + } else if (drawable instanceof Animatable2Compat) { + ((Animatable2Compat) drawable).registerAnimationCallback(mAnimationCallbackCompat); + } else if (drawable instanceof AnimationDrawable) { + ((AnimationDrawable) drawable).setOneShot(false); + } + + ((Animatable) drawable).start(); + } + + private static void startLottieAnimationWith(LottieAnimationView illustrationView, + Uri imageUri) { + try { + final InputStream inputStream = + getInputStreamFromUri(illustrationView.getContext(), imageUri); + illustrationView.setAnimation(inputStream, /* cacheKey= */ null); + illustrationView.setRepeatCount(LottieDrawable.INFINITE); + illustrationView.playAnimation(); + } catch (IllegalStateException e) { + Log.w(TAG, "Invalid illustration image uri: " + imageUri, e); + } + } + + private static void startLottieAnimationWith(LottieAnimationView illustrationView, + @RawRes int rawRes) { + try { + illustrationView.setAnimation(rawRes); + illustrationView.setRepeatCount(LottieDrawable.INFINITE); + illustrationView.playAnimation(); + } catch (IllegalStateException e) { + Log.w(TAG, "Invalid illustration resource id: " + rawRes, e); + } + } + + private static void resetAnimations(LottieAnimationView illustrationView) { + resetAnimation(illustrationView.getDrawable()); + + illustrationView.cancelAnimation(); + } + + private static void resetAnimation(Drawable drawable) { + if (!(drawable instanceof Animatable)) { + return; + } + + if (drawable instanceof Animatable2) { + ((Animatable2) drawable).clearAnimationCallbacks(); + } else if (drawable instanceof Animatable2Compat) { + ((Animatable2Compat) drawable).clearAnimationCallbacks(); + } + + ((Animatable) drawable).stop(); + } + + private static InputStream getInputStreamFromUri(Context context, Uri uri) { + try { + return context.getContentResolver().openInputStream(uri); + } catch (FileNotFoundException e) { + Log.w(TAG, "Cannot find content uri: " + uri, e); + return null; + } } private void init(Context context, AttributeSet attrs) { @@ -157,7 +325,7 @@ public class IllustrationPreference extends Preference { if (attrs != null) { final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LottieAnimationView, 0 /*defStyleAttr*/, 0 /*defStyleRes*/); - mAnimationId = a.getResourceId(R.styleable.LottieAnimationView_lottie_rawRes, 0); + mImageResId = a.getResourceId(R.styleable.LottieAnimationView_lottie_rawRes, 0); a.recycle(); } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java index 89b0fe72ca168..ea9be04527be1 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java @@ -18,12 +18,25 @@ package com.android.settingslib.widget; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + 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.util.AttributeSet; import android.view.View; +import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; +import androidx.preference.PreferenceViewHolder; +import androidx.test.core.app.ApplicationProvider; + import com.airbnb.lottie.LottieAnimationView; import org.junit.Before; @@ -33,47 +46,88 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.util.ReflectionHelpers; @RunWith(RobolectricTestRunner.class) public class IllustrationPreferenceTest { @Mock - LottieAnimationView mAnimationView; - - private Context mContext; + private ViewGroup mRootView; + private Uri mImageUri; + private LottieAnimationView mAnimationView; private IllustrationPreference mPreference; + private PreferenceViewHolder mViewHolder; + private FrameLayout mMiddleGroundLayout; + private final Context mContext = ApplicationProvider.getApplicationContext(); @Before public void setUp() { MockitoAnnotations.initMocks(this); - mContext = RuntimeEnvironment.application; + + mImageUri = new Uri.Builder().build(); + mAnimationView = spy(new LottieAnimationView(mContext)); + mMiddleGroundLayout = new FrameLayout(mContext); + final FrameLayout illustrationFrame = new FrameLayout(mContext); + illustrationFrame.setLayoutParams( + new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + doReturn(mMiddleGroundLayout).when(mRootView).findViewById(R.id.middleground_layout); + doReturn(mAnimationView).when(mRootView).findViewById(R.id.lottie_view); + doReturn(illustrationFrame).when(mRootView).findViewById(R.id.illustration_frame); + mViewHolder = spy(PreferenceViewHolder.createInstanceForTests(mRootView)); + final AttributeSet attributeSet = Robolectric.buildAttributeSet().build(); mPreference = new IllustrationPreference(mContext, attributeSet); - ReflectionHelpers.setField(mPreference, "mIllustrationView", mAnimationView); } @Test public void setMiddleGroundView_middleGroundView_shouldVisible() { final View view = new View(mContext); - final FrameLayout layout = new FrameLayout(mContext); - layout.setVisibility(View.GONE); - ReflectionHelpers.setField(mPreference, "mMiddleGroundView", view); - ReflectionHelpers.setField(mPreference, "mMiddleGroundLayout", layout); + mMiddleGroundLayout.setVisibility(View.GONE); mPreference.setMiddleGroundView(view); + mPreference.onBindViewHolder(mViewHolder); - assertThat(layout.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mMiddleGroundLayout.getVisibility()).isEqualTo(View.VISIBLE); } @Test public void enableAnimationAutoScale_shouldChangeScaleType() { - final LottieAnimationView animationView = new LottieAnimationView(mContext); - ReflectionHelpers.setField(mPreference, "mIllustrationView", animationView); - mPreference.enableAnimationAutoScale(true); + mPreference.onBindViewHolder(mViewHolder); - assertThat(animationView.getScaleType()).isEqualTo(ImageView.ScaleType.CENTER_CROP); + assertThat(mAnimationView.getScaleType()).isEqualTo(ImageView.ScaleType.CENTER_CROP); + } + + @Test + public void playAnimationWithUri_animatedImageDrawable_success() { + final AnimatedImageDrawable drawable = mock(AnimatedImageDrawable.class); + doReturn(drawable).when(mAnimationView).getDrawable(); + + mPreference.setImageUri(mImageUri); + mPreference.onBindViewHolder(mViewHolder); + + verify(drawable).start(); + } + + @Test + public void playAnimationWithUri_animatedVectorDrawable_success() { + final AnimatedVectorDrawable drawable = mock(AnimatedVectorDrawable.class); + doReturn(drawable).when(mAnimationView).getDrawable(); + + mPreference.setImageUri(mImageUri); + mPreference.onBindViewHolder(mViewHolder); + + verify(drawable).start(); + } + + @Test + public void playAnimationWithUri_animationDrawable_success() { + final AnimationDrawable drawable = mock(AnimationDrawable.class); + doReturn(drawable).when(mAnimationView).getDrawable(); + + mPreference.setImageUri(mImageUri); + mPreference.onBindViewHolder(mViewHolder); + + verify(drawable).start(); } }