Merge "Extend to support the static and animated format image for IllustrationPreference." into sc-dev
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user