chromium/chrome/browser/hub/internal/android/java/src/org/chromium/chrome/browser/hub/HubPaneHostView.java

// Copyright 2023 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.hub;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.util.AttributeSet;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.FrameLayout;
import android.widget.ImageView;

import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.core.widget.TextViewCompat;

import org.chromium.base.Callback;
import org.chromium.base.supplier.Supplier;
import org.chromium.ui.interpolators.Interpolators;
import org.chromium.ui.widget.ButtonCompat;

import java.util.Objects;

/** Holds the current pane's {@link View}. */
public class HubPaneHostView extends FrameLayout {
    // Chosen to exactly match the default add/remove animation duration of RecyclerView.
    private static final int FADE_ANIMATION_DURATION_MILLIS = 120;

    private final OnLayoutChangeListener mSnackbarLayoutChangeListener =
            new OnLayoutChangeListener() {
                @Override
                public void onLayoutChange(
                        View v,
                        int left,
                        int top,
                        int right,
                        int bottom,
                        int oldLeft,
                        int oldTop,
                        int oldRight,
                        int oldBottom) {
                    // oldTop being 0 means this is the first layout.
                    if (oldTop == 0) return;

                    endFloatingActionButtonAnimation();

                    int height = bottom - top;
                    int oldHeight = oldBottom - oldTop;
                    // If the height is unchanged there is no need to do anything.
                    if (height == oldHeight) return;

                    int delta = height - oldHeight;
                    ObjectAnimator animator =
                            ObjectAnimator.ofFloat(mActionButton, View.TRANSLATION_Y, -delta);
                    // Keep in sync with SnackbarView.java.
                    animator.setInterpolator(Interpolators.STANDARD_INTERPOLATOR);
                    animator.setDuration(mFloatingActionButtonAnimatorDuration);
                    animator.addListener(
                            new AnimatorListenerAdapter() {
                                @Override
                                public void onAnimationEnd(Animator animation) {
                                    mActionButton.setTranslationY(0);
                                    FrameLayout.LayoutParams params =
                                            (FrameLayout.LayoutParams)
                                                    mActionButton.getLayoutParams();
                                    params.bottomMargin = mOriginalMargin + height;
                                    mActionButton.setLayoutParams(params);
                                    mFloatingActionButtonAnimator = null;
                                }
                            });

                    mFloatingActionButtonAnimator = animator;
                    animator.start();
                }
            };

    private FrameLayout mPaneFrame;
    private ButtonCompat mActionButton;
    private ImageView mHairline;
    private ViewGroup mSnackbarContainer;
    private @Nullable View mCurrentViewRoot;
    private @Nullable Animator mCurrentAnimator;
    private @Nullable Animator mFloatingActionButtonAnimator;

    private int mFloatingActionButtonAnimatorDuration;
    private int mOriginalMargin;

    /** Default {@link FrameLayout} constructor called by inflation. */
    public HubPaneHostView(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        Resources res = getContext().getResources();

        mPaneFrame = findViewById(R.id.pane_frame);
        mActionButton = findViewById(R.id.host_action_button);
        mHairline = findViewById(R.id.pane_top_hairline);
        mFloatingActionButtonAnimatorDuration =
                res.getInteger(android.R.integer.config_mediumAnimTime);
        mOriginalMargin = res.getDimensionPixelSize(R.dimen.floating_action_button_margin);
        mSnackbarContainer = findViewById(R.id.pane_host_view_snackbar_container);
        mSnackbarContainer.addOnLayoutChangeListener(mSnackbarLayoutChangeListener);

        // ButtonCompat's style Flat removes elevation after calling super so it is overridden. Undo
        // this.
        mActionButton.setElevation(
                res.getDimensionPixelSize(R.dimen.floating_action_button_elevation));
    }

    void setRootView(@Nullable View newRootView) {
        final View oldRootView = mCurrentViewRoot;
        mCurrentViewRoot = newRootView;
        if (mCurrentAnimator != null) {
            mCurrentAnimator.end();
            assert mCurrentAnimator == null;
            endFloatingActionButtonAnimation();
        }

        if (oldRootView != null && newRootView != null) {
            newRootView.setAlpha(0);
            tryAddViewToFrame(newRootView);

            Animator fadeOut = ObjectAnimator.ofFloat(oldRootView, View.ALPHA, 1, 0);
            fadeOut.setDuration(FADE_ANIMATION_DURATION_MILLIS);

            Animator fadeIn = ObjectAnimator.ofFloat(newRootView, View.ALPHA, 0, 1);
            fadeIn.setDuration(FADE_ANIMATION_DURATION_MILLIS);

            AnimatorSet animatorSet = new AnimatorSet();
            animatorSet.playSequentially(fadeOut, fadeIn);
            animatorSet.addListener(
                    new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            mPaneFrame.removeView(oldRootView);
                            oldRootView.setAlpha(1);
                            mCurrentAnimator = null;
                        }
                    });
            mCurrentAnimator = animatorSet;
            animatorSet.start();
        } else if (newRootView == null) {
            mPaneFrame.removeAllViews();
        } else { // oldRootView == null
            tryAddViewToFrame(newRootView);
        }
    }

    void setActionButtonData(@Nullable FullButtonData buttonData) {
        ApplyButtonData.apply(buttonData, mActionButton);
    }

    void setColorScheme(@HubColorScheme int colorScheme) {
        Context context = getContext();

        @ColorInt int backgroundColor = HubColors.getBackgroundColor(context, colorScheme);
        mPaneFrame.setBackgroundColor(backgroundColor);

        ColorStateList iconColor;
        ColorStateList buttonColor;
        @StyleRes int textAppearance;
        if (HubFieldTrial.useAlternativeFabColor()) {
            iconColor =
                    ColorStateList.valueOf(
                            HubColors.getOnPrimaryContainerColor(context, colorScheme));
            buttonColor = HubColors.getPrimaryContainerColorStateList(context, colorScheme);
            textAppearance = HubColors.getTextAppearanceMediumOnPrimaryContainer(colorScheme);
        } else {
            iconColor =
                    ColorStateList.valueOf(
                            HubColors.getOnSecondaryContainerColor(context, colorScheme));
            buttonColor = HubColors.getSecondaryContainerColorStateList(context, colorScheme);
            textAppearance = HubColors.getTextAppearanceMedium(colorScheme);
        }
        TextViewCompat.setCompoundDrawableTintList(mActionButton, iconColor);
        mActionButton.setButtonColor(buttonColor);
        mActionButton.setTextAppearance(textAppearance);

        @ColorInt int hairlineColor = HubColors.getHairlineColor(context, colorScheme);
        mHairline.setImageTintList(ColorStateList.valueOf(hairlineColor));
    }

    void setHairlineVisibility(boolean visible) {
        mHairline.setVisibility(visible ? View.VISIBLE : View.GONE);
    }

    void setFloatingActionButtonConsumer(Callback<Supplier<View>> consumer) {
        consumer.onResult(this::getFloatingActionButton);
    }

    void setSnackbarContainerConsumer(Callback<ViewGroup> consumer) {
        consumer.onResult(mSnackbarContainer);
    }

    private @Nullable View getFloatingActionButton() {
        return mActionButton.getVisibility() == View.VISIBLE ? mActionButton : null;
    }

    private void tryAddViewToFrame(View rootView) {
        ViewParent parent = rootView.getParent();
        if (!Objects.equals(parent, mPaneFrame)) {
            if (parent instanceof ViewGroup viewGroup) {
                viewGroup.removeView(rootView);
            }
            mPaneFrame.addView(rootView);
        }
    }

    private void endFloatingActionButtonAnimation() {
        if (mFloatingActionButtonAnimator != null) {
            mFloatingActionButtonAnimator.end();
            assert mFloatingActionButtonAnimator == null;
        }
    }

    OnLayoutChangeListener getSnackbarLayoutChangeListenerForTesting() {
        return mSnackbarLayoutChangeListener;
    }
}