chromium/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridView.java

// Copyright 2019 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.tasks.tab_management;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.ImageView;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.core.content.res.ResourcesCompat;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;

import org.chromium.chrome.browser.quick_delete.QuickDeleteAnimationGradientDrawable;
import org.chromium.chrome.browser.tasks.tab_management.TabProperties.TabActionState;
import org.chromium.chrome.tab_ui.R;
import org.chromium.components.browser_ui.widget.selectable_list.SelectableItemViewBase;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;

// TODO(crbug.com/339038505): De-dupe logic in TabListView.
/** Holds the view for a selectable tab grid. */
public class TabGridView extends SelectableItemViewBase<Integer> {
    private static final long RESTORE_ANIMATION_DURATION_MS = 50;
    private static final long BASE_ANIMATION_DURATION_MS = 218;
    private static final float ZOOM_IN_SCALE = 0.8f;

    private static WeakReference<Bitmap> sCloseButtonBitmapWeakRef;

    @IntDef({
        AnimationStatus.SELECTED_CARD_ZOOM_IN,
        AnimationStatus.SELECTED_CARD_ZOOM_OUT,
        AnimationStatus.HOVERED_CARD_ZOOM_IN,
        AnimationStatus.HOVERED_CARD_ZOOM_OUT,
        AnimationStatus.CARD_RESTORE
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface AnimationStatus {
        int CARD_RESTORE = 0;
        int SELECTED_CARD_ZOOM_OUT = 1;
        int SELECTED_CARD_ZOOM_IN = 2;
        int HOVERED_CARD_ZOOM_OUT = 3;
        int HOVERED_CARD_ZOOM_IN = 4;
        int NUM_ENTRIES = 5;
    }

    @IntDef({
        QuickDeleteAnimationStatus.TAB_HIDE,
        QuickDeleteAnimationStatus.TAB_PREPARE,
        QuickDeleteAnimationStatus.TAB_RESTORE
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface QuickDeleteAnimationStatus {
        int TAB_RESTORE = 0;
        int TAB_PREPARE = 1;
        int TAB_HIDE = 2;
        int NUM_ENTRIES = 3;
    }

    private boolean mIsAnimating;
    private @TabActionState int mTabActionState = TabActionState.UNSET;
    private @Nullable ObjectAnimator mQuickDeleteAnimation;
    private @Nullable QuickDeleteAnimationGradientDrawable mQuickDeleteAnimationDrawable;
    private ImageView mActionButton;

    public TabGridView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setSelectionOnLongClick(false);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mActionButton = findViewById(R.id.action_button);
    }

    /**
     * Play the zoom-in and zoom-out animations for tab grid card.
     *
     * @param status The target animation status in {@link AnimationStatus}.
     */
    void scaleTabGridCardView(@AnimationStatus int status) {
        assert mTabActionState != TabActionState.UNSET;
        assert status < AnimationStatus.NUM_ENTRIES;

        final View backgroundView = fastFindViewById(R.id.background_view);
        final View contentView = fastFindViewById(R.id.content_view);
        boolean isZoomIn =
                status == AnimationStatus.SELECTED_CARD_ZOOM_IN
                        || status == AnimationStatus.HOVERED_CARD_ZOOM_IN;
        boolean isHovered =
                status == AnimationStatus.HOVERED_CARD_ZOOM_IN
                        || status == AnimationStatus.HOVERED_CARD_ZOOM_OUT;
        boolean isRestore = status == AnimationStatus.CARD_RESTORE;
        long duration = isRestore ? RESTORE_ANIMATION_DURATION_MS : BASE_ANIMATION_DURATION_MS;
        float scale = isZoomIn ? ZOOM_IN_SCALE : 1f;
        View animateView = isHovered ? contentView : this;

        if (status == AnimationStatus.HOVERED_CARD_ZOOM_IN) {
            backgroundView.setVisibility(View.VISIBLE);
        }

        AnimatorSet scaleAnimator = new AnimatorSet();
        scaleAnimator.addListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        if (!isZoomIn) {
                            backgroundView.setVisibility(View.GONE);
                        }
                        mIsAnimating = false;
                    }
                });

        ObjectAnimator scaleX = ObjectAnimator.ofFloat(animateView, View.SCALE_X, scale);
        ObjectAnimator scaleY = ObjectAnimator.ofFloat(animateView, View.SCALE_Y, scale);
        scaleX.setDuration(duration);
        scaleY.setDuration(duration);
        scaleAnimator.play(scaleX).with(scaleY);
        mIsAnimating = true;
        scaleAnimator.start();
    }

    void hideTabGridCardViewForQuickDelete(
            @QuickDeleteAnimationStatus int status, boolean isIncognito) {
        assert mTabActionState != TabActionState.UNSET;
        assert status < QuickDeleteAnimationStatus.NUM_ENTRIES;

        final ViewGroup contentView = (ViewGroup) fastFindViewById(R.id.content_view);
        if (contentView == null) return;

        if (status == QuickDeleteAnimationStatus.TAB_HIDE) {
            assert mQuickDeleteAnimation != null && mQuickDeleteAnimationDrawable != null;

            contentView.setForeground(mQuickDeleteAnimationDrawable);
            mQuickDeleteAnimation.start();
        } else if (status == QuickDeleteAnimationStatus.TAB_PREPARE) {
            Drawable originalForeground = contentView.getForeground();
            int tabHeight = contentView.getHeight();
            mQuickDeleteAnimationDrawable =
                    QuickDeleteAnimationGradientDrawable.createQuickDeleteFadeAnimationDrawable(
                            getContext(), tabHeight, isIncognito);
            mQuickDeleteAnimation = mQuickDeleteAnimationDrawable.createFadeAnimator(tabHeight);

            mQuickDeleteAnimation.addListener(
                    new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            contentView.setVisibility(GONE);
                            contentView.setForeground(originalForeground);
                        }
                    });
        } else if (status == QuickDeleteAnimationStatus.TAB_RESTORE) {
            // Reset to original values to allow the tab to be recycled correctly.
            mQuickDeleteAnimation = null;
            mQuickDeleteAnimationDrawable = null;
            contentView.setVisibility(VISIBLE);
        }
    }

    void setTabActionButtonDrawable(boolean isTabGroup) {
        assert mTabActionState != TabActionState.UNSET;
        if (isTabGroup) {
            mActionButton.setImageDrawable(
                    ResourcesCompat.getDrawable(
                            getResources(), R.drawable.ic_more_vert_24dp, getContext().getTheme()));
        } else {
            setTabActionButtonCloseDrawable();
        }
    }

    void setTabActionState(@TabActionState int tabActionState) {
        if (mTabActionState == tabActionState) return;

        mTabActionState = tabActionState;
        int accessibilityMode = IMPORTANT_FOR_ACCESSIBILITY_YES;
        if (mTabActionState == TabActionState.CLOSABLE) {
            setTabActionButtonCloseDrawable();
        } else if (mTabActionState == TabActionState.SELECTABLE) {
            accessibilityMode = IMPORTANT_FOR_ACCESSIBILITY_NO;
            setTabActionButtonSelectionDrawable();
        }

        mActionButton.setImportantForAccessibility(accessibilityMode);
    }

    private void setTabActionButtonCloseDrawable() {
        assert mTabActionState != TabActionState.UNSET;

        if (sCloseButtonBitmapWeakRef == null || sCloseButtonBitmapWeakRef.get() == null) {
            int closeButtonSize =
                    (int) getResources().getDimension(R.dimen.tab_grid_close_button_size);
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.btn_close);
            sCloseButtonBitmapWeakRef =
                    new WeakReference<>(
                            Bitmap.createScaledBitmap(
                                    bitmap, closeButtonSize, closeButtonSize, true));
            bitmap.recycle();
        }
        mActionButton.setBackground(null);
        mActionButton.setImageBitmap(sCloseButtonBitmapWeakRef.get());
    }

    private void setTabActionButtonSelectionDrawable() {
        assert mTabActionState != TabActionState.UNSET;
        var resources = getResources();
        Drawable selectionListIcon =
                ResourcesCompat.getDrawable(
                        resources,
                        R.drawable.tab_grid_selection_list_icon,
                        getContext().getTheme());

        InsetDrawable drawable =
                new InsetDrawable(
                        selectionListIcon,
                        (int)
                                resources.getDimension(
                                        R.dimen.selection_tab_grid_toggle_button_inset));
        mActionButton.setBackground(drawable);
        mActionButton
                .getBackground()
                .setLevel(resources.getInteger(R.integer.list_item_level_default));
        mActionButton.setImageDrawable(
                AnimatedVectorDrawableCompat.create(
                        getContext(), R.drawable.ic_check_googblue_20dp_animated));
    }

    // SelectableItemViewBase implementation.

    @Override
    protected void updateView(boolean animate) {}

    @Override
    protected void handleNonSelectionClick() {}

    // TODO(crbug.com/339038201): Consider capturing click events and discarding them while not in
    // selection mode.

    // View implementation.

    @Override
    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfo(info);

        if (mTabActionState == TabActionState.SELECTABLE) {
            info.setCheckable(true);
            info.setChecked(isChecked());
        }
    }

    // Testing methods.

    boolean getIsAnimatingForTesting() {
        return mIsAnimating;
    }
}