chromium/chrome/android/java/src/org/chromium/chrome/browser/gesturenav/NavigationBubble.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.gesturenav;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff.Mode;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation.AnimationListener;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.core.widget.ImageViewCompat;

import org.chromium.chrome.R;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.ui.util.ColorUtils;

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

/**
 * View class for a bubble used in gesture navigation UI that consists of an icon
 * and an optional text.
 */
public class NavigationBubble extends LinearLayout {
    /**
     * Target to close when gesture navigation takes place on the beginning
     * of the navigation history. It can close either the current tab or
     * chrome itself (putting it background).
     */
    @IntDef({CloseTarget.NONE, CloseTarget.TAB, CloseTarget.APP})
    @Retention(RetentionPolicy.SOURCE)
    @interface CloseTarget {
        int NONE = 0;
        int TAB = 1;
        int APP = 2;
    }

    private static final int COLOR_TRANSITION_DURATION_MS = 250;

    private static final float FADE_ALPHA = 0.5f;

    private static final int FADE_DURATION_MS = 400;

    private final ValueAnimator mColorAnimator;
    private final int mColorPrimary;
    private final int mBlack;
    private final String mCloseApp;
    private final String mCloseTab;

    private class ColorUpdateListener implements ValueAnimator.AnimatorUpdateListener {
        private int mStart;
        private int mEnd;

        private void setTransitionColors(int start, int end) {
            mStart = start;
            mEnd = end;
        }

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float fraction = (float) animation.getAnimatedValue();
            ImageViewCompat.setImageTintList(
                    mIcon,
                    ColorStateList.valueOf(ColorUtils.getColorWithOverlay(mStart, mEnd, fraction)));
        }
    }

    private final ColorUpdateListener mColorUpdateListener;

    private TextView mText;
    private ImageView mIcon;
    private AnimationListener mListener;

    // True if arrow bubble is faded out.
    private boolean mArrowFaded;

    private @CloseTarget int mCloseTarget;

    /** Constructor for inflating from XML. */
    public NavigationBubble(Context context) {
        this(context, null);
    }

    public NavigationBubble(Context context, AttributeSet attrs) {
        super(context, attrs);

        mBlack = getContext().getColor(R.color.navigation_bubble_arrow);
        mColorPrimary = SemanticColorUtils.getDefaultIconColorAccent1(getContext());

        mColorUpdateListener = new ColorUpdateListener();
        mColorAnimator = ValueAnimator.ofFloat(0, 1).setDuration(COLOR_TRANSITION_DURATION_MS);
        mColorAnimator.addUpdateListener(mColorUpdateListener);
        getBackground()
                .setColorFilter(
                        SemanticColorUtils.getNavigationBubbleBackgroundColor(context),
                        Mode.MULTIPLY);
        mCloseApp =
                getResources()
                        .getString(
                                R.string.overscroll_navigation_close_chrome,
                                getContext().getString(R.string.app_name));
        mCloseTab = getResources().getString(R.string.overscroll_navigation_close_tab);
        mCloseTarget = CloseTarget.NONE;
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mIcon = findViewById(R.id.navigation_bubble_arrow);
        mText = findViewById(R.id.navigation_bubble_text);
    }

    /**
     * Sets {@link AnimationListener} used when the widget disappears at the end of
     * user gesture.
     * @param listener Listener object.
     */
    public void setAnimationListener(AnimationListener listener) {
        mListener = listener;
    }

    /**
     * @return {@code true} if the widget is showing the close chrome indicator text.
     */
    public boolean isShowingCaption() {
        return getTextView().getVisibility() == View.VISIBLE;
    }

    /**
     * Shows or hides the close indicator.
     * @param target Target to close. if {@code NONE}, hide the indicator.
     */
    public void showCaption(@CloseTarget int target) {
        if (target != CloseTarget.NONE && !isShowingCaption()) {
            setCloseIndicator(target);
            getTextView().setVisibility(View.VISIBLE);
            // Measure the width again after the indicator text becomes visible.
            measure(0, 0);
        } else if (target == CloseTarget.NONE && isShowingCaption()) {
            getTextView().setVisibility(View.GONE);
        }
    }

    private void setCloseIndicator(@CloseTarget int target) {
        assert target == CloseTarget.APP || target == CloseTarget.TAB;
        if (mCloseTarget == target) return;
        mCloseTarget = target;
        getTextView().setText(target == CloseTarget.APP ? mCloseApp : mCloseTab);
    }

    @Override
    public void onAnimationStart() {
        super.onAnimationStart();
        if (mListener != null) {
            mListener.onAnimationStart(getAnimation());
        }
    }

    @Override
    public void onAnimationEnd() {
        super.onAnimationEnd();
        if (mListener != null) {
            mListener.onAnimationEnd(getAnimation());
        }
    }

    /**
     * Sets the icon at the start of the icon view.
     * @param icon The resource id pointing to the icon.
     */
    public void setIcon(@DrawableRes int icon) {
        mIcon.setVisibility(ViewGroup.VISIBLE);
        mIcon.setImageResource(icon);
        setImageTint(false);
    }

    /** Sets the correct tinting on the arrow icon. */
    public void setImageTint(boolean navigate) {
        assert mIcon != null;
        mColorUpdateListener.setTransitionColors(
                navigate ? mBlack : mColorPrimary, navigate ? mColorPrimary : mBlack);
        mColorAnimator.start();
    }

    /**
     * Returns the {@link TextView} that contains the label of the widget.
     * @return A {@link TextView}.
     */
    public TextView getTextView() {
        return mText;
    }

    /**
     * Fade out the arrow bubble.
     * @param faded {@code true} if the bubble should be faded.
     * @param animate {@code true} if animation is needed.
     */
    public void setFaded(boolean faded, boolean animate) {
        if (faded == mArrowFaded) return;
        assert mIcon != null;
        animate().alpha(faded ? FADE_ALPHA : 1.f).setDuration(animate ? FADE_DURATION_MS : 0);
        mArrowFaded = faded;
    }
}