| // 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); |
| } |
| |
| /** |
| * 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; |
| } |
| } |