blob: d494929177c8201da9ff760b69470ebd76f41522 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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.toolbar.menu_button;
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.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import androidx.annotation.DrawableRes;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.ViewCompat;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ThemeColorProvider;
import org.chromium.chrome.browser.ThemeColorProvider.TintObserver;
import org.chromium.chrome.browser.omaha.UpdateMenuItemHelper;
import org.chromium.chrome.browser.omaha.UpdateMenuItemHelper.MenuButtonState;
import org.chromium.chrome.browser.toolbar.ToolbarColors;
import org.chromium.chrome.browser.ui.appmenu.AppMenuButtonHelper;
import org.chromium.components.browser_ui.widget.animation.Interpolators;
import org.chromium.components.browser_ui.widget.highlight.PulseDrawable;
import org.chromium.ui.interpolators.BakedBezierInterpolator;
/**
* The overflow menu button.
*/
public class MenuButton extends FrameLayout implements TintObserver {
/** The {@link ImageButton} for the menu button. */
private ImageButton mMenuImageButton;
/** The view for the update badge. */
private ImageView mUpdateBadgeView;
private boolean mUseLightDrawables;
private AppMenuButtonHelper mAppMenuButtonHelper;
private boolean mHighlightingMenu;
private PulseDrawable mHighlightDrawable;
private boolean mSuppressAppMenuUpdateBadge;
private AnimatorSet mMenuBadgeAnimatorSet;
private boolean mIsMenuBadgeAnimationRunning;
/** A provider that notifies components when the theme color changes.*/
private ThemeColorProvider mThemeColorProvider;
private BitmapDrawable mMenuImageButtonAnimationDrawable;
private BitmapDrawable mUpdateBadgeAnimationDrawable;
public MenuButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuImageButton = findViewById(R.id.menu_button);
mUpdateBadgeView = findViewById(R.id.menu_badge);
}
public void setAppMenuButtonHelper(AppMenuButtonHelper appMenuButtonHelper) {
mAppMenuButtonHelper = appMenuButtonHelper;
mMenuImageButton.setOnTouchListener(mAppMenuButtonHelper);
mMenuImageButton.setAccessibilityDelegate(mAppMenuButtonHelper.getAccessibilityDelegate());
}
public ImageButton getImageButton() {
return mMenuImageButton;
}
/**
* Sets the update badge to visible.
*
* @param visible Whether the update badge should be visible. Always sets visibility to GONE
* if the update type does not require a badge.
* TODO(crbug.com/865801): Clean this up when MenuButton and UpdateMenuItemHelper is MVCed.
*/
private void setUpdateBadgeVisibility(boolean visible) {
if (mUpdateBadgeView == null) return;
mUpdateBadgeView.setVisibility(visible ? View.VISIBLE : View.GONE);
if (visible) updateImageResources();
updateContentDescription(visible);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed) {
updateImageResources();
}
}
private void updateImageResources() {
mMenuImageButtonAnimationDrawable = (BitmapDrawable) mMenuImageButton.getDrawable()
.getConstantState()
.newDrawable()
.mutate();
mMenuImageButtonAnimationDrawable.setBounds(mMenuImageButton.getPaddingLeft(),
mMenuImageButton.getPaddingTop(),
mMenuImageButton.getWidth() - mMenuImageButton.getPaddingRight(),
mMenuImageButton.getHeight() - mMenuImageButton.getPaddingBottom());
mMenuImageButtonAnimationDrawable.setGravity(Gravity.CENTER);
int color = ToolbarColors.getThemedToolbarIconTint(getContext(), mUseLightDrawables)
.getDefaultColor();
mMenuImageButtonAnimationDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
// As an optimization, don't re-calculate drawable state for the update badge unless we
// intend to actually show it.
MenuButtonState buttonState = UpdateMenuItemHelper.getInstance().getUiState().buttonState;
if (buttonState == null || mUpdateBadgeView == null) return;
@DrawableRes
int drawable = mUseLightDrawables ? buttonState.lightBadgeIcon : buttonState.darkBadgeIcon;
mUpdateBadgeView.setImageDrawable(
ApiCompatibilityUtils.getDrawable(getResources(), drawable));
mUpdateBadgeAnimationDrawable = (BitmapDrawable) mUpdateBadgeView.getDrawable()
.getConstantState()
.newDrawable()
.mutate();
mUpdateBadgeAnimationDrawable.setBounds(mUpdateBadgeView.getPaddingLeft(),
mUpdateBadgeView.getPaddingTop(),
mUpdateBadgeView.getWidth() - mUpdateBadgeView.getPaddingRight(),
mUpdateBadgeView.getHeight() - mUpdateBadgeView.getPaddingBottom());
mUpdateBadgeAnimationDrawable.setGravity(Gravity.CENTER);
}
/**
* Show the update badge on the app menu button.
* @param animate Whether to animate the showing of the update badge.
*/
public void showAppMenuUpdateBadgeIfAvailable(boolean animate) {
if (mUpdateBadgeView == null || mMenuImageButton == null || mSuppressAppMenuUpdateBadge
|| !isBadgeAvailable()) {
return;
}
updateImageResources();
updateContentDescription(true);
if (!animate || mIsMenuBadgeAnimationRunning) {
setUpdateBadgeVisibility(true);
return;
}
// Set initial states.
mUpdateBadgeView.setAlpha(0.f);
mUpdateBadgeView.setVisibility(View.VISIBLE);
mMenuBadgeAnimatorSet = createShowUpdateBadgeAnimation(mMenuImageButton, mUpdateBadgeView);
mMenuBadgeAnimatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mIsMenuBadgeAnimationRunning = true;
}
@Override
public void onAnimationEnd(Animator animation) {
mIsMenuBadgeAnimationRunning = false;
}
@Override
public void onAnimationCancel(Animator animation) {
mIsMenuBadgeAnimationRunning = false;
}
});
mMenuBadgeAnimatorSet.start();
}
/**
* Remove the update badge on the app menu button.
* @param animate Whether to animate the hiding of the update badge.
*/
public void removeAppMenuUpdateBadge(boolean animate) {
if (mUpdateBadgeView == null || !isShowingAppMenuUpdateBadge()) return;
updateContentDescription(false);
if (!animate) {
setUpdateBadgeVisibility(false);
return;
}
if (mIsMenuBadgeAnimationRunning && mMenuBadgeAnimatorSet != null) {
mMenuBadgeAnimatorSet.cancel();
}
// Set initial states.
mMenuImageButton.setAlpha(0.f);
mMenuBadgeAnimatorSet = createHideUpdateBadgeAnimation(mMenuImageButton, mUpdateBadgeView);
mMenuBadgeAnimatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mIsMenuBadgeAnimationRunning = true;
}
@Override
public void onAnimationEnd(Animator animation) {
mIsMenuBadgeAnimationRunning = false;
}
@Override
public void onAnimationCancel(Animator animation) {
mIsMenuBadgeAnimationRunning = false;
}
});
mMenuBadgeAnimatorSet.start();
}
/**
* @param suppress Whether to prevent the update badge from being show. This is currently only
* used to prevent the badge from being shown in the tablet tab switcher.
*/
public void setAppMenuUpdateBadgeSuppressed(boolean suppress) {
mSuppressAppMenuUpdateBadge = suppress;
if (mSuppressAppMenuUpdateBadge) {
removeAppMenuUpdateBadge(false);
} else {
showAppMenuUpdateBadgeIfAvailable(false);
}
}
/**
* @return Whether the update badge is showing.
*/
public boolean isShowingAppMenuUpdateBadge() {
return mUpdateBadgeView != null && mUpdateBadgeView.getVisibility() == View.VISIBLE;
}
private static boolean isBadgeAvailable() {
return UpdateMenuItemHelper.getInstance().getUiState().buttonState != null;
}
/**
* Sets the content description for the menu button.
* @param isUpdateBadgeVisible Whether the update menu badge is visible.
*/
private void updateContentDescription(boolean isUpdateBadgeVisible) {
if (isUpdateBadgeVisible) {
MenuButtonState buttonState =
UpdateMenuItemHelper.getInstance().getUiState().buttonState;
assert buttonState != null : "No button state when trying to show the badge.";
mMenuImageButton.setContentDescription(
getResources().getString(buttonState.menuContentDescription));
} else {
mMenuImageButton.setContentDescription(
getResources().getString(R.string.accessibility_toolbar_btn_menu));
}
}
/**
* Sets the menu button's background depending on whether or not we are highlighting and whether
* or not we are using light or dark assets.
*/
public void setMenuButtonHighlightDrawable() {
// Return if onFinishInflate didn't finish
if (mMenuImageButton == null) return;
if (mHighlightingMenu) {
if (mHighlightDrawable == null) {
mHighlightDrawable = PulseDrawable.createCircle(getContext());
mHighlightDrawable.setInset(ViewCompat.getPaddingStart(mMenuImageButton),
mMenuImageButton.getPaddingTop(),
ViewCompat.getPaddingEnd(mMenuImageButton),
mMenuImageButton.getPaddingBottom());
}
mHighlightDrawable.setUseLightPulseColor(
getContext().getResources(), mUseLightDrawables);
setBackground(mHighlightDrawable);
mHighlightDrawable.start();
} else {
setBackground(null);
}
}
public void setMenuButtonHighlight(boolean highlight) {
mHighlightingMenu = highlight;
setMenuButtonHighlightDrawable();
}
public void setThemeColorProvider(ThemeColorProvider themeColorProvider) {
mThemeColorProvider = themeColorProvider;
mThemeColorProvider.addTintObserver(this);
onTintChanged(themeColorProvider.getTint(), themeColorProvider.useLight());
}
/**
* Draws the current visual state of this component for the purposes of rendering the tab
* switcher animation, setting the alpha to fade the view by the appropriate amount.
* @param canvas Canvas to draw to.
* @param alpha Integer (0-255) alpha level to draw at.
*/
public void drawTabSwitcherAnimationOverlay(Canvas canvas, int alpha) {
Drawable drawable = getTabSwitcherAnimationDrawable();
drawable.setAlpha(alpha);
drawable.draw(canvas);
}
@Override
public void onTintChanged(ColorStateList tintList, boolean useLight) {
ApiCompatibilityUtils.setImageTintList(mMenuImageButton, tintList);
mUseLightDrawables = useLight;
updateImageResources();
}
public void destroy() {
if (mThemeColorProvider != null) {
mThemeColorProvider.removeTintObserver(this);
mThemeColorProvider = null;
}
}
@VisibleForTesting
Drawable getTabSwitcherAnimationDrawable() {
if (mUpdateBadgeAnimationDrawable == null && mMenuImageButtonAnimationDrawable == null) {
updateImageResources();
}
return isShowingAppMenuUpdateBadge() ? mUpdateBadgeAnimationDrawable
: mMenuImageButtonAnimationDrawable;
}
/**
* Creates an {@link AnimatorSet} for showing the update badge that is displayed on top
* of the app menu button.
*
* @param menuButton The {@link View} containing the app menu button.
* @param menuBadge The {@link View} containing the update badge.
* @return An {@link AnimatorSet} to run when showing the update badge.
*/
private static AnimatorSet createShowUpdateBadgeAnimation(
final View menuButton, final View menuBadge) {
// Create badge ObjectAnimators.
ObjectAnimator badgeFadeAnimator = ObjectAnimator.ofFloat(menuBadge, View.ALPHA, 1.f);
badgeFadeAnimator.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE);
int pixelTranslation =
menuBadge.getResources().getDimensionPixelSize(R.dimen.menu_badge_translation_y);
ObjectAnimator badgeTranslateYAnimator =
ObjectAnimator.ofFloat(menuBadge, View.TRANSLATION_Y, pixelTranslation, 0.f);
badgeTranslateYAnimator.setInterpolator(BakedBezierInterpolator.TRANSFORM_CURVE);
// Create menu button ObjectAnimator.
ObjectAnimator menuButtonFadeAnimator = ObjectAnimator.ofFloat(menuButton, View.ALPHA, 0.f);
menuButtonFadeAnimator.setInterpolator(Interpolators.LINEAR_INTERPOLATOR);
// Create AnimatorSet and listeners.
AnimatorSet set = new AnimatorSet();
set.playTogether(badgeFadeAnimator, badgeTranslateYAnimator, menuButtonFadeAnimator);
set.setDuration(350);
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Make sure the menu button is visible again.
menuButton.setAlpha(1.f);
}
@Override
public void onAnimationCancel(Animator animation) {
// Jump to the end state if the animation is canceled.
menuBadge.setAlpha(1.f);
menuBadge.setTranslationY(0.f);
menuButton.setAlpha(1.f);
}
});
return set;
}
/**
* Creates an {@link AnimatorSet} for hiding the update badge that is displayed on top
* of the app menu button.
*
* @param menuButton The {@link View} containing the app menu button.
* @param menuBadge The {@link View} containing the update badge.
* @return An {@link AnimatorSet} to run when hiding the update badge.
*/
private static AnimatorSet createHideUpdateBadgeAnimation(
final View menuButton, final View menuBadge) {
// Create badge ObjectAnimator.
ObjectAnimator badgeFadeAnimator = ObjectAnimator.ofFloat(menuBadge, View.ALPHA, 0.f);
badgeFadeAnimator.setInterpolator(BakedBezierInterpolator.FADE_OUT_CURVE);
// Create menu button ObjectAnimator.
ObjectAnimator menuButtonFadeAnimator = ObjectAnimator.ofFloat(menuButton, View.ALPHA, 1.f);
menuButtonFadeAnimator.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE);
// Create AnimatorSet and listeners.
AnimatorSet set = new AnimatorSet();
set.playTogether(badgeFadeAnimator, menuButtonFadeAnimator);
set.setDuration(200);
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
menuBadge.setVisibility(View.GONE);
}
@Override
public void onAnimationCancel(Animator animation) {
// Jump to the end state if the animation is canceled.
menuButton.setAlpha(1.f);
menuBadge.setVisibility(View.GONE);
}
});
return set;
}
}