chromium/components/browser_ui/widget/android/java/src/org/chromium/components/browser_ui/widget/async_image/AutoAnimatorDrawable.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.browser_ui.widget.async_image;

import android.graphics.drawable.Animatable;
import android.graphics.drawable.Animatable2;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.RotateDrawable;
import android.graphics.drawable.ScaleDrawable;
import android.os.Handler;
import android.os.Looper;

import androidx.annotation.Nullable;
import androidx.appcompat.graphics.drawable.DrawableWrapperCompat;
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * A helper {@link Drawable} that wraps another {@link Drawable} and starts/stops any
 * {@link Animatable} {@link Drawable}s in the {@link Drawable} hierarchy when this {@link Drawable}
 * is shown or hidden.
 */
public class AutoAnimatorDrawable extends DrawableWrapperCompat {
    // Since Drawables default visible to true by default, we might not get a change and start the
    // animation on the first visibility request.
    private boolean mGotVisibilityCall;

    /**
     * Wraps {@code drawable} and returns a new {@link Drawable} that will automatically start
     * animating all sub-drawables if possible when the {@link Drawable} is visible.  Stops
     * animating when the {@link Drawable} is no longer visible.
     * @param drawable The {@link Drawable} to wrap.
     * @return         A new {@link Drawable} that will automaticaly animate or {@code null} if
     *                 {@code drawable} is {@code null}.
     */
    public static Drawable wrap(@Nullable Drawable drawable) {
        if (drawable == null || !shouldWrapDrawable(drawable)) return drawable;
        return new AutoAnimatorDrawable(drawable);
    }

    private AutoAnimatorDrawable(Drawable drawable) {
        super(drawable);
        AutoAnimatorDrawable.attachRestartListeners(this);
    }

    // DrawableWrapperCompat implementation.
    @Override
    public boolean setVisible(boolean visible, boolean restart) {
        boolean changed = super.setVisible(visible, restart);
        if (visible) {
            if (changed || restart || !mGotVisibilityCall) {
                AutoAnimatorDrawable.startAnimatedDrawables(this);
            }
        } else {
            AutoAnimatorDrawable.stopAnimatedDrawables(this);
        }

        mGotVisibilityCall = true;
        return changed;
    }

    private static void startAnimatedDrawables(@Nullable Drawable drawable) {
        AutoAnimatorDrawable.animatedDrawableHelper(drawable, animatable -> animatable.start());
    }

    private static void stopAnimatedDrawables(@Nullable Drawable drawable) {
        AutoAnimatorDrawable.animatedDrawableHelper(drawable, animatable -> animatable.stop());
    }

    private static boolean shouldWrapDrawable(@Nullable Drawable drawable) {
        AtomicBoolean found = new AtomicBoolean();
        AutoAnimatorDrawable.animatedDrawableHelper(drawable, animatable -> found.set(true));
        return found.get();
    }

    private static void attachRestartListeners(@Nullable Drawable drawable) {
        AutoAnimatorDrawable.animatedDrawableHelper(
                drawable,
                animatable -> {
                    if (animatable instanceof Animatable2Compat) {
                        ((Animatable2Compat) animatable)
                                .registerAnimationCallback(LazyHolderCompat.INSTANCE);
                    } else if (animatable instanceof Animatable2) {
                        ((Animatable2) animatable).registerAnimationCallback(LazyHolder.INSTANCE);
                    }
                });
    }

    private static void animatedDrawableHelper(
            @Nullable Drawable drawable, org.chromium.base.Callback<Animatable> consumer) {
        if (drawable == null) return;

        if (drawable instanceof Animatable) {
            consumer.onResult((Animatable) drawable);

            // Assume Animatable drawables can handle animating their own internals/sub drawables.
            return;
        }

        if (drawable != drawable.getCurrent()) {
            // Check obvious cases where the current drawable isn't actually being shown.  This
            // should support all {@link DrawableContainer} instances.
            AutoAnimatorDrawable.animatedDrawableHelper(drawable.getCurrent(), consumer);
        }

        if (drawable instanceof android.graphics.drawable.DrawableWrapper) {
            // Support all modern versions of drawables that wrap other ones.  This won't cover old
            // versions of Android (see below for other if/else blocks).
            AutoAnimatorDrawable.animatedDrawableHelper(
                    ((android.graphics.drawable.DrawableWrapper) drawable).getDrawable(), consumer);
        } else if (drawable instanceof DrawableWrapperCompat) {
            // Support the AppCompat DrawableWrapperCompat.
            AutoAnimatorDrawable.animatedDrawableHelper(
                    ((DrawableWrapperCompat) drawable).getDrawable(), consumer);
        } else if (drawable instanceof LayerDrawable) {
            // Support a LayerDrawable and try to animate all layers.
            LayerDrawable layerDrawable = (LayerDrawable) drawable;
            for (int i = 0; i < layerDrawable.getNumberOfLayers(); i++) {
                AutoAnimatorDrawable.animatedDrawableHelper(layerDrawable.getDrawable(i), consumer);
            }
        } else if (drawable instanceof InsetDrawable) {
            // Support legacy versions of InsetDrawable.
            AutoAnimatorDrawable.animatedDrawableHelper(
                    ((InsetDrawable) drawable).getDrawable(), consumer);
        } else if (drawable instanceof RotateDrawable) {
            // Support legacy versions of RotateDrawable.
            AutoAnimatorDrawable.animatedDrawableHelper(
                    ((RotateDrawable) drawable).getDrawable(), consumer);
        } else if (drawable instanceof ScaleDrawable) {
            // Support legacy versions of ScaleDrawable.
            AutoAnimatorDrawable.animatedDrawableHelper(
                    ((ScaleDrawable) drawable).getDrawable(), consumer);
        }
    }

    private static final class LazyHolder {
        private static final AutoRestarter INSTANCE = new AutoRestarter();
    }

    private static final class LazyHolderCompat {
        private static final AutoRestarterCompat INSTANCE = new AutoRestarterCompat();
    }

    private static final class AutoRestarterCompat extends Animatable2Compat.AnimationCallback {
        private final Handler mHandler = new Handler(Looper.getMainLooper());

        // Animatable2Compat.AnimationCallback implementation.
        @Override
        public void onAnimationEnd(Drawable drawable) {
            if (!(drawable instanceof Animatable)) return;
            mHandler.post(
                    () -> {
                        if (drawable.isVisible()) ((Animatable) drawable).start();
                    });
        }
    }

    private static final class AutoRestarter extends Animatable2.AnimationCallback {
        // Animatable2.AnimationCallback implementation.
        @Override
        public void onAnimationEnd(Drawable drawable) {
            LazyHolderCompat.INSTANCE.onAnimationEnd(drawable);
        }
    }
}