Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bb9115aa authored by PETER LIANG's avatar PETER LIANG Committed by Automerger Merge Worker
Browse files

Merge "Extend to support the static and animated format image for...

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

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/15079472

Change-Id: Ibf15a50454fd7540cd8ea0cd4ad9ab2e70d08e03
parents 156829bd dc68f8f3
Loading
Loading
Loading
Loading
+214 −46
Original line number Diff line number Diff line
@@ -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();
        handleImageWithAnimation(illustrationView);

        if (mIsAutoScale) {
            enableAnimationAutoScale(mIsAutoScale);
        }
        if (mMiddleGroundView != null) {
            enableMiddleGroundView();
            illustrationView.setScaleType(mIsAutoScale
                            ? ImageView.ScaleType.CENTER_CROP
                            : ImageView.ScaleType.CENTER_INSIDE);
        }

        handleMiddleGroundView(middleGroundLayout);

        if (IS_ENABLED_LOTTIE_ADAPTIVE_COLOR) {
            ColorUtils.applyDynamicColors(getContext(), mIllustrationView);
            ColorUtils.applyDynamicColors(getContext(), illustrationView);
        }
    }

    @VisibleForTesting
    boolean isAnimating() {
        return mIllustrationView.isAnimating();
    }

    /**
     * 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) {
        if (view != mMiddleGroundView) {
            mMiddleGroundView = view;
        if (mMiddleGroundLayout == null) {
            return;
            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) {
        if (enable != mIsAutoScale) {
            mIsAutoScale = enable;
        if (mIllustrationView == null) {
            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) {
        mAnimationId = resId;
        if (resId != mImageResId) {
            resetImageResourceCache();
            mImageResId = resId;
            notifyChanged();
        }
    }

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

    private void enableMiddleGroundView() {
        mMiddleGroundLayout.removeAllViews();
        mMiddleGroundLayout.addView(mMiddleGroundView);
        mMiddleGroundLayout.setVisibility(View.VISIBLE);
    /**
     * 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();
        }
    }
+70 −16
Original line number Diff line number Diff line
@@ -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(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);

        assertThat(animationView.getScaleType()).isEqualTo(ImageView.ScaleType.CENTER_CROP);
        verify(drawable).start();
    }
}