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.Context;
import android.content.res.TypedArray; 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.AttributeSet;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.LayoutParams;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import androidx.annotation.VisibleForTesting; import androidx.annotation.RawRes;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder; import androidx.preference.PreferenceViewHolder;
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
import com.airbnb.lottie.LottieAnimationView; 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 * 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 static final boolean IS_ENABLED_LOTTIE_ADAPTIVE_COLOR = false;
private int mAnimationId; private int mImageResId;
private boolean mIsAutoScale; private boolean mIsAutoScale;
private LottieAnimationView mIllustrationView; private Uri mImageUri;
private Drawable mImageDrawable;
private View mMiddleGroundView; 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) { public IllustrationPreference(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
@@ -65,10 +97,11 @@ public class IllustrationPreference extends Preference {
@Override @Override
public void onBindViewHolder(PreferenceViewHolder holder) { public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder); super.onBindViewHolder(holder);
if (mAnimationId == 0) {
Log.w(TAG, "Invalid illustration resource id."); final FrameLayout middleGroundLayout =
return; (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 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 // 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; lp.width = screenWidth < screenHeight ? screenWidth : screenHeight;
illustrationFrame.setLayoutParams(lp); illustrationFrame.setLayoutParams(lp);
mMiddleGroundLayout = (FrameLayout) holder.findViewById(R.id.middleground_layout); handleImageWithAnimation(illustrationView);
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);
}
}
@VisibleForTesting if (mIsAutoScale) {
boolean isAnimating() { illustrationView.setScaleType(mIsAutoScale
return mIllustrationView.isAnimating(); ? 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. * can overlay a view on top of the animation.
*/ */
public void setMiddleGroundView(View view) { public void setMiddleGroundView(View view) {
mMiddleGroundView = view; if (view != mMiddleGroundView) {
if (mMiddleGroundLayout == null) { mMiddleGroundView = view;
return; notifyChanged();
} }
enableMiddleGroundView();
} }
/** /**
* Remove the middle ground view of preference. * Removes the middle ground view of preference.
*/ */
public void removeMiddleGroundView() { public void removeMiddleGroundView() {
if (mMiddleGroundLayout == null) { mMiddleGroundView = null;
return; notifyChanged();
}
mMiddleGroundLayout.removeAllViews();
mMiddleGroundLayout.setVisibility(View.GONE);
} }
/** /**
* Enables the auto scale feature of animation view. * Enables the auto scale feature of animation view.
*/ */
public void enableAnimationAutoScale(boolean enable) { public void enableAnimationAutoScale(boolean enable) {
mIsAutoScale = enable; if (enable != mIsAutoScale) {
if (mIllustrationView == null) { mIsAutoScale = enable;
return; 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) { public void setLottieAnimationResId(int resId) {
mAnimationId = resId; if (resId != mImageResId) {
resetImageResourceCache();
mImageResId = resId;
notifyChanged();
}
} }
private void enableMiddleGroundView() { /**
mMiddleGroundLayout.removeAllViews(); * Sets image drawable to display image in {@link LottieAnimationView}
mMiddleGroundLayout.addView(mMiddleGroundView); *
mMiddleGroundLayout.setVisibility(View.VISIBLE); * @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) { private void init(Context context, AttributeSet attrs) {
@@ -157,7 +325,7 @@ public class IllustrationPreference extends Preference {
if (attrs != null) { if (attrs != null) {
final TypedArray a = context.obtainStyledAttributes(attrs, final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.LottieAnimationView, 0 /*defStyleAttr*/, 0 /*defStyleRes*/); 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(); a.recycle();
} }
} }

View File

@@ -18,12 +18,25 @@ package com.android.settingslib.widget;
import static com.google.common.truth.Truth.assertThat; 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.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.util.AttributeSet;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import androidx.preference.PreferenceViewHolder;
import androidx.test.core.app.ApplicationProvider;
import com.airbnb.lottie.LottieAnimationView; import com.airbnb.lottie.LottieAnimationView;
import org.junit.Before; import org.junit.Before;
@@ -33,47 +46,88 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric; import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.util.ReflectionHelpers;
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class IllustrationPreferenceTest { public class IllustrationPreferenceTest {
@Mock @Mock
LottieAnimationView mAnimationView; private ViewGroup mRootView;
private Uri mImageUri;
private Context mContext; private LottieAnimationView mAnimationView;
private IllustrationPreference mPreference; private IllustrationPreference mPreference;
private PreferenceViewHolder mViewHolder;
private FrameLayout mMiddleGroundLayout;
private final Context mContext = ApplicationProvider.getApplicationContext();
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); 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(); final AttributeSet attributeSet = Robolectric.buildAttributeSet().build();
mPreference = new IllustrationPreference(mContext, attributeSet); mPreference = new IllustrationPreference(mContext, attributeSet);
ReflectionHelpers.setField(mPreference, "mIllustrationView", mAnimationView);
} }
@Test @Test
public void setMiddleGroundView_middleGroundView_shouldVisible() { public void setMiddleGroundView_middleGroundView_shouldVisible() {
final View view = new View(mContext); final View view = new View(mContext);
final FrameLayout layout = new FrameLayout(mContext); mMiddleGroundLayout.setVisibility(View.GONE);
layout.setVisibility(View.GONE);
ReflectionHelpers.setField(mPreference, "mMiddleGroundView", view);
ReflectionHelpers.setField(mPreference, "mMiddleGroundLayout", layout);
mPreference.setMiddleGroundView(view); mPreference.setMiddleGroundView(view);
mPreference.onBindViewHolder(mViewHolder);
assertThat(layout.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mMiddleGroundLayout.getVisibility()).isEqualTo(View.VISIBLE);
} }
@Test @Test
public void enableAnimationAutoScale_shouldChangeScaleType() { public void enableAnimationAutoScale_shouldChangeScaleType() {
final LottieAnimationView animationView = new LottieAnimationView(mContext);
ReflectionHelpers.setField(mPreference, "mIllustrationView", animationView);
mPreference.enableAnimationAutoScale(true); 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();
} }
} }