From a6635f42e9a6e7ad072b4bd2bf11cbf9005d49bd Mon Sep 17 00:00:00 2001 From: Peter_Liang Date: Wed, 23 Jun 2021 18:20:37 +0800 Subject: [PATCH] Extend to support the static and animated format image for IllustrationPreference. Actions: 1) Migrate and deprecate the widget AnimatedImagePreference into SettingsLib IllustrationPreference. It would extend to support the static (e.g., svg, png), animated (e.g., gif) and lottie format image. 2) Create setImageUri() for IllustrationPreference. 3) Create setImageDrawable() for IllustrationPreference. Bug: 190585192 Test: make RunSettingsLibRoboTests ROBOTEST_FILTER=com.android.settingslib.widget.IllustrationPreferenceTest Change-Id: Iad5bc6af78cca4d57cb56eec104ef2384da7be9f --- .../widget/IllustrationPreference.java | 266 ++++++++++++++---- .../widget/IllustrationPreferenceTest.java | 86 ++++-- 2 files changed, 287 insertions(+), 65 deletions(-) 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(); } }