chromium/chrome/browser/quick_delete/android/java/src/org/chromium/chrome/browser/quick_delete/QuickDeleteAnimationGradientDrawable.java

// Copyright 2024 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.chrome.browser.quick_delete;

import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.animation.Interpolator;

import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
import androidx.annotation.IntRange;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.view.animation.PathInterpolatorCompat;

import com.google.android.material.color.MaterialColors;

import org.chromium.build.annotations.UsedByReflection;
import org.chromium.chrome.browser.night_mode.GlobalNightModeStateProviderHolder;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.ui.interpolators.Interpolators;
import org.chromium.ui.util.ColorUtils;

/** The {@link Drawable} that will be used to run animations for Quick Delete. */
public class QuickDeleteAnimationGradientDrawable extends Drawable {
    private static final String TAG = "QuickDeleteAnimationGradientDrawable";

    /**
     * This multiplier will be used to calculate the height of the gradient relative to tab grid
     * height. The tab grid height changes depending on the screen size, orientation, multi-window
     * mode...
     */
    @FloatRange(from = 0.0F, to = 1.0F)
    private static final float QUICK_DELETE_WIPE_GRADIENT_HEIGHT_MULTIPLIER = 0.35F;

    /**
     * This multiplier will be used to calculate the intersection point between the wipe and fade
     * animation relative to tab grid height. The tab grid height changes depending on the screen
     * size, orientation, multi-window mode...
     */
    @FloatRange(from = 0.0F, to = 1.0F)
    private static final float QUICK_DELETE_ANIMATION_INTERSECTION_MULTIPLIER = 0.5F;

    @IntRange(from = 0L, to = 255L)
    private static final int QUICK_DELETE_GRADIENT_DARK_MODE_MAX_ALPHA = 77;

    @IntRange(from = 0L, to = 255L)
    private static final int QUICK_DELETE_GRADIENT_LIGHT_MODE_MAX_ALPHA = 64;

    private static final int QUICK_DELETE_GRADIENT_EASING_POINTS_NUM = 20;
    private static final Interpolator QUICK_DELETE_WIPE_ANIMATION_INTERPOLATOR =
            PathInterpolatorCompat.create(0.25F, 0F, 0.15F, 1F);
    private static final int QUICK_DELETE_WIPE_ANIMATION_TIME_MS = 1200;
    private static final int QUICK_DELETE_FADE_ANIMATION_TIME_MS = 230;
    private final @NonNull Paint mPaint;
    private final @NonNull LinearGradient mShader;

    /* The value that will be used to translate the gradient across the bounds of a view. */
    private float mTranslationY;

    /**
     * Creates a new {@link QuickDeleteAnimationGradientDrawable} for the wipe animation.
     *
     * @param context The associated {@link Context}.
     * @param tabGridHeight The height of the tab grid. This will be used to determine the height of
     *     the gradient.
     * @param isIncognito Whether the surface is in incognito mode.
     */
    public static QuickDeleteAnimationGradientDrawable createQuickDeleteWipeAnimationDrawable(
            @NonNull Context context, int tabGridHeight, boolean isIncognito) {
        int gradientColor =
                isIncognito
                        ? ContextCompat.getColor(context, R.color.baseline_primary_80)
                        : MaterialColors.getColor(context, R.attr.colorPrimary, TAG);
        boolean useDarkTheme =
                isIncognito || GlobalNightModeStateProviderHolder.getInstance().isInNightMode();

        int h = QUICK_DELETE_GRADIENT_EASING_POINTS_NUM;
        int k =
                useDarkTheme
                        ? QUICK_DELETE_GRADIENT_DARK_MODE_MAX_ALPHA
                        : QUICK_DELETE_GRADIENT_LIGHT_MODE_MAX_ALPHA;
        int[] colors = new int[h + 1];
        for (int i = 0; i <= h; ++i) {
            // Quadratic equation to calculate the alpha value at each easing point to achieve a
            // smoother transition between the colors of the gradient. This should map to an
            // inverted parabola where the vertical shift corresponds to the maximum alpha value and
            // the horizontal shift corresponds to the number of easing points (number of colors) of
            // the gradient, while maintaining an intersection point at (0,0).
            float alphaValue = -4F * k / (h * h) * (i - h / 2F) * (i - h / 2F) + k;
            colors[i] = ColorUtils.setAlphaComponent(gradientColor, Math.round(alphaValue));
        }

        int gradientHeight = (int) (tabGridHeight * QUICK_DELETE_WIPE_GRADIENT_HEIGHT_MULTIPLIER);

        return new QuickDeleteAnimationGradientDrawable(
                context, colors, /* positions= */ null, gradientHeight);
    }

    /**
     * Creates a new {@link QuickDeleteAnimationGradientDrawable} for the fade animation.
     *
     * @param context The associated {@link Context}.
     * @param tabHeight The height of the tab in the tab grid. This will be used to determine the
     *     height of the gradient.
     * @param isIncognito Whether the surface is in incognito mode.
     */
    public static QuickDeleteAnimationGradientDrawable createQuickDeleteFadeAnimationDrawable(
            @NonNull Context context, int tabHeight, boolean isIncognito) {
        // The color of the background behind the tab.
        int backgroundColor = ChromeColors.getPrimaryBackgroundColor(context, isIncognito);

        int[] colors = new int[] {Color.TRANSPARENT, backgroundColor, backgroundColor};

        return new QuickDeleteAnimationGradientDrawable(
                context, colors, /* positions= */ null, tabHeight);
    }

    /**
     * Creates an instance of QuickDeleteAnimationGradientDrawable with specified height and colors.
     *
     * @param context The associated {@link Context}.
     * @param colors The sRGB colors to be distributed along the gradient line.
     * @param positions May be null. The relative positions [0..1] of each corresponding color in
     *     the colors array. If this is null, the the colors are distributed evenly along the
     *     gradient line.
     * @param gradientHeight The height of the gradient.
     */
    public QuickDeleteAnimationGradientDrawable(
            @NonNull Context context,
            @NonNull @ColorInt int[] colors,
            @Nullable float[] positions,
            int gradientHeight) {
        super();
        assert positions == null || colors.length == positions.length;
        float gradientHeightInPixels = dpToPixels(context, gradientHeight);

        mShader =
                new LinearGradient(
                        0.0F,
                        0.0F,
                        0.0F,
                        gradientHeightInPixels,
                        colors,
                        positions,
                        Shader.TileMode.CLAMP);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setShader(mShader);
    }

    /**
     * @param parentViewHeight The height of the view that the animation should run on.
     * @return The {@link Animator} that can be used to start the wipe animation.
     */
    public ObjectAnimator createWipeAnimator(int parentViewHeight) {
        ObjectAnimator animator = createAnimator(parentViewHeight, -parentViewHeight);
        animator.setInterpolator(QUICK_DELETE_WIPE_ANIMATION_INTERPOLATOR);
        animator.setDuration(QUICK_DELETE_WIPE_ANIMATION_TIME_MS);
        return animator;
    }

    /**
     * @param parentViewHeight The height of the view that the animation should run on.
     * @return The {@link Animator} that can be used to start the fade animation.
     */
    public ObjectAnimator createFadeAnimator(int parentViewHeight) {
        ObjectAnimator animator = createAnimator(parentViewHeight, -parentViewHeight * 2);
        animator.setInterpolator(Interpolators.LINEAR_INTERPOLATOR);
        animator.setDuration(QUICK_DELETE_FADE_ANIMATION_TIME_MS);
        return animator;
    }

    public static int getAnimationsIntersectionHeight(int parentViewHeight) {
        int gradientHeight =
                (int) (parentViewHeight * QUICK_DELETE_WIPE_GRADIENT_HEIGHT_MULTIPLIER);
        return (int) (gradientHeight * QUICK_DELETE_ANIMATION_INTERSECTION_MULTIPLIER);
    }

    /** Returns the current translationY value of the {@link LinearGradient} shader. */
    @UsedByReflection("QuickDeleteAnimationGradientDrawable.java")
    public float getTranslationY() {
        return mTranslationY;
    }

    /**
     * Sets the current translationY value of the {@link LinearGradient} shader. Generally this
     * method should only be called by the running animation (but must be accessible for {@link
     * android.animation.ObjectAnimator}).
     */
    @UsedByReflection("QuickDeleteAnimationGradientDrawable.java")
    @Keep
    public void setTranslationY(float value) {
        mTranslationY = value;
        invalidateSelf();
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        Matrix matrix = new Matrix();
        mShader.getLocalMatrix(matrix);
        matrix.setTranslate(0, mTranslationY);

        mShader.setLocalMatrix(matrix);
        canvas.drawPaint(mPaint);
    }

    @Override
    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        mPaint.setColorFilter(colorFilter);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    private ObjectAnimator createAnimator(float startValue, float endValue) {
        return ObjectAnimator.ofFloat(this, "translationY", startValue, endValue);
    }

    private float dpToPixels(@NonNull Context context, float dp) {
        Resources res = context.getResources();
        DisplayMetrics metrics = res.getDisplayMetrics();
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics);
    }
}