Merge "Extend to support the static and animated format image for IllustrationPreference." into sc-dev

This commit is contained in:
PETER LIANG
2021-06-30 00:38:20 +00:00
committed by Android (Google) Code Review
2 changed files with 287 additions and 65 deletions

View File

@@ -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();
}
}

View File

@@ -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();
}
}