// Copyright 2015 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;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.appmenu.AppMenuButtonHelper;
import org.chromium.chrome.browser.compositor.Invalidator;
import org.chromium.chrome.browser.compositor.layouts.LayoutUpdateHost;
import org.chromium.chrome.browser.fullscreen.FullscreenManager;
import org.chromium.chrome.browser.ntp.NewTabPage;
import org.chromium.chrome.browser.omaha.UpdateMenuItemHelper;
import org.chromium.chrome.browser.omnibox.LocationBar;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.util.ViewUtils;
import org.chromium.chrome.browser.widget.TintedImageButton;
import org.chromium.chrome.browser.widget.ToolbarProgressBar;
import org.chromium.ui.UiUtils;
import org.chromium.ui.widget.Toast;
import javax.annotation.Nullable;
/**
* Layout class that contains the base shared logic for manipulating the toolbar component. For
* interaction that are not from Views inside Toolbar hierarchy all interactions should be done
* through {@link Toolbar} rather than using this class directly.
*/
abstract class ToolbarLayout extends FrameLayout implements Toolbar {
protected static final int BACKGROUND_TRANSITION_DURATION_MS = 400;
private Invalidator mInvalidator;
private final int[] mTempPosition = new int[2];
/**
* The ImageButton view that represents the menu button.
*/
protected TintedImageButton mMenuButton;
protected ImageView mMenuBadge;
protected View mMenuButtonWrapper;
private AppMenuButtonHelper mAppMenuButtonHelper;
protected final ColorStateList mDarkModeTint;
protected final ColorStateList mLightModeTint;
private ToolbarDataProvider mToolbarDataProvider;
private ToolbarTabController mToolbarTabController;
@Nullable
private ToolbarProgressBar mProgressBar;
private boolean mNativeLibraryReady;
private boolean mUrlHasFocus;
private long mFirstDrawTimeMs;
protected final int mToolbarHeightWithoutShadow;
private boolean mFindInPageToolbarShowing;
protected boolean mShowMenuBadge;
private AnimatorSet mMenuBadgeAnimatorSet;
private boolean mIsMenuBadgeAnimationRunning;
/**
* Basic constructor for {@link ToolbarLayout}.
*/
public ToolbarLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mToolbarHeightWithoutShadow = getResources().getDimensionPixelOffset(
getToolbarHeightWithoutShadowResId());
mDarkModeTint =
ApiCompatibilityUtils.getColorStateList(getResources(), R.color.dark_mode_tint);
mLightModeTint =
ApiCompatibilityUtils.getColorStateList(getResources(), R.color.light_mode_tint);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mProgressBar = (ToolbarProgressBar) findViewById(R.id.progress);
if (mProgressBar != null) {
removeView(mProgressBar);
mProgressBar.prepareForAttach(mToolbarHeightWithoutShadow);
if (isNativeLibraryReady()) mProgressBar.initializeAnimation();
}
mMenuButton = (TintedImageButton) findViewById(R.id.menu_button);
mMenuBadge = (ImageView) findViewById(R.id.menu_badge);
mMenuButtonWrapper = findViewById(R.id.menu_button_wrapper);
// Initialize the provider to an empty version to avoid null checking everywhere.
mToolbarDataProvider = new ToolbarDataProvider() {
@Override
public boolean isIncognito() {
return false;
}
@Override
public Tab getTab() {
return null;
}
@Override
public String getText() {
return null;
}
@Override
public NewTabPage getNewTabPageForCurrentTab() {
return null;
}
@Override
public int getPrimaryColor() {
return 0;
}
@Override
public boolean isUsingBrandColor() {
return false;
}
};
}
/**
* Quick getter for LayoutParams for a View inside a FrameLayout.
* @param view {@link View} to fetch the layout params for.
* @return {@link LayoutParams} the given {@link View} is currently using.
*/
protected FrameLayout.LayoutParams getFrameLayoutParams(View view) {
return ((FrameLayout.LayoutParams) view.getLayoutParams());
}
/**
* @return The resource id to be used while getting the toolbar height with no shadow.
*/
protected int getToolbarHeightWithoutShadowResId() {
return R.dimen.toolbar_height_no_shadow;
}
/**
* Initialize the external dependencies required for view interaction.
* @param toolbarDataProvider The provider for toolbar data.
* @param tabController The controller that handles interactions with the tab.
* @param appMenuButtonHelper The helper for managing menu button interactions.
*/
public void initialize(ToolbarDataProvider toolbarDataProvider,
ToolbarTabController tabController, AppMenuButtonHelper appMenuButtonHelper) {
mToolbarDataProvider = toolbarDataProvider;
mToolbarTabController = tabController;
mMenuButton.setOnTouchListener(new OnTouchListener() {
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouch(View v, MotionEvent event) {
return onMenuButtonTouchEvent(v, event);
}
});
mAppMenuButtonHelper = appMenuButtonHelper;
}
/** @return Whether or not the event is handled. */
protected boolean onMenuButtonTouchEvent(View v, MotionEvent event) {
return mAppMenuButtonHelper.onTouch(v, event);
}
/**
* This function handles native dependent initialization for this class
*/
public void onNativeLibraryReady() {
mNativeLibraryReady = true;
if (mProgressBar != null) mProgressBar.initializeAnimation();
}
/**
* @return The view containing the menu button and menu button badge.
*/
protected View getMenuButtonWrapper() {
return mMenuButtonWrapper;
}
/**
* @return The {@link ProgressBar} this layout uses.
*/
ToolbarProgressBar getProgressBar() {
return mProgressBar;
}
@Override
public void getPositionRelativeToContainer(View containerView, int[] position) {
ViewUtils.getRelativeDrawPosition(containerView, this, position);
}
/**
* @return The helper for menu button UI interactions.
*/
protected AppMenuButtonHelper getMenuButtonHelper() {
return mAppMenuButtonHelper;
}
/**
* @return Whether or not the native library is loaded and ready.
*/
protected boolean isNativeLibraryReady() {
return mNativeLibraryReady;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
recordFirstDrawTime();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mProgressBar != null) {
ViewGroup controlContainer =
(ViewGroup) getRootView().findViewById(R.id.control_container);
int progressBarPosition = UiUtils.insertAfter(
controlContainer, mProgressBar, (View) getParent());
assert progressBarPosition >= 0;
mProgressBar.setControlContainer(controlContainer);
}
}
/**
* Shows the content description toast for items on the toolbar.
* @param view The view to anchor the toast.
* @param description The string shown in the toast.
* @return Whether a toast has been shown successfully.
*/
protected boolean showAccessibilityToast(View view, CharSequence description) {
if (description == null) return false;
final int screenWidth = getResources().getDisplayMetrics().widthPixels;
final int[] screenPos = new int[2];
view.getLocationOnScreen(screenPos);
final int width = view.getWidth();
Toast toast = Toast.makeText(getContext(), description, Toast.LENGTH_SHORT);
toast.setGravity(
Gravity.TOP | Gravity.END,
screenWidth - screenPos[0] - width / 2,
screenPos[1] + getHeight() / 2);
toast.show();
return true;
}
/**
* @return The provider for toolbar related data.
*/
protected ToolbarDataProvider getToolbarDataProvider() {
return mToolbarDataProvider;
}
/**
* Sets the {@link Invalidator} that will be called when the toolbar attempts to invalidate the
* drawing surface. This will give the object that registers as the host for the
* {@link Invalidator} a chance to defer the actual invalidate to sync drawing.
* @param invalidator An {@link Invalidator} instance.
*/
public void setPaintInvalidator(Invalidator invalidator) {
mInvalidator = invalidator;
}
/**
* Triggers a paint but allows the {@link Invalidator} set by
* {@link #setPaintInvalidator(Invalidator)} to decide when to actually invalidate.
* @param client A {@link Invalidator.Client} instance that wants to be invalidated.
*/
protected void triggerPaintInvalidate(Invalidator.Client client) {
if (mInvalidator == null) {
client.doInvalidate();
} else {
mInvalidator.invalidate(client);
}
}
/**
* Gives inheriting classes the chance to respond to
* {@link org.chromium.chrome.browser.widget.findinpage.FindToolbar} state changes.
* @param showing Whether or not the {@code FindToolbar} will be showing.
*/
protected void handleFindToolbarStateChange(boolean showing) {
mFindInPageToolbarShowing = showing;
}
/**
* Cleans up any code as necessary.
*/
public void destroy() { }
/**
* Sets the FullscreenManager, which controls when the toolbar is shown.
*/
public void setFullscreenManager(FullscreenManager manager) { }
/**
* Sets the OnClickListener that will be notified when the TabSwitcher button is pressed.
* @param listener The callback that will be notified when the TabSwitcher button is pressed.
*/
public void setOnTabSwitcherClickHandler(OnClickListener listener) { }
/**
* Sets the OnClickListener that will be notified when the New Tab button is pressed.
* @param listener The callback that will be notified when the New Tab button is pressed.
*/
public void setOnNewTabClickHandler(OnClickListener listener) { }
/**
* Sets the OnClickListener that will be notified when the bookmark button is pressed.
* @param listener The callback that will be notified when the bookmark button is pressed.
*/
public void setBookmarkClickHandler(OnClickListener listener) { }
/**
* Sets the OnClickListener to notify when the close button is pressed in a custom tab.
* @param listener The callback that will be notified when the close button is pressed.
*/
public void setCustomTabCloseClickHandler(OnClickListener listener) { }
/**
* Sets whether the urlbar should be hidden on first page load.
*/
public void setUrlBarHidden(boolean hide) { }
/**
* @return The name of the publisher of the content if it can be reliably extracted, or null
* otherwise.
*/
public String getContentPublisher() {
return null;
}
/**
* Tells the Toolbar to update what buttons it is currently displaying.
*/
public void updateButtonVisibility() { }
/**
* Gives inheriting classes the chance to update the visibility of the
* back button.
* @param canGoBack Whether or not the current tab has any history to go back to.
*/
protected void updateBackButtonVisibility(boolean canGoBack) { }
/**
* Gives inheriting classes the chance to update the visibility of the
* forward button.
* @param canGoForward Whether or not the current tab has any history to go forward to.
*/
protected void updateForwardButtonVisibility(boolean canGoForward) { }
/**
* Gives inheriting classes the chance to update the visibility of the
* reload button.
* @param isReloading Whether or not the current tab is loading.
*/
protected void updateReloadButtonVisibility(boolean isReloading) { }
/**
* Gives inheriting classes the chance to update the visual status of the
* bookmark button.
* @param isBookmarked Whether or not the current tab is already bookmarked.
* @param editingAllowed Whether or not bookmarks can be modified (added, edited, or removed).
*/
protected void updateBookmarkButton(boolean isBookmarked, boolean editingAllowed) { }
/**
* Gives inheriting classes the chance to respond to accessibility state changes.
* @param enabled Whether or not accessibility is enabled.
*/
protected void onAccessibilityStatusChanged(boolean enabled) { }
/**
* Gives inheriting classes the chance to do the necessary UI operations after Chrome is
* restored to a previously saved state.
*/
protected void onStateRestored() { }
/**
* Gives inheriting classes the chance to update home button UI if home button preference is
* changed.
* @param homeButtonEnabled Whether or not home button is enabled in preference.
*/
protected void onHomeButtonUpdate(boolean homeButtonEnabled) { }
/**
* Triggered when the current tab or model has changed.
* <p>
* As there are cases where you can select a model with no tabs (i.e. having incognito
* tabs but no normal tabs will still allow you to select the normal model), this should
* not guarantee that the model's current tab is non-null.
*/
protected void onTabOrModelChanged() {
NewTabPage ntp = getToolbarDataProvider().getNewTabPageForCurrentTab();
if (ntp != null) {
getLocationBar().onTabLoadingNTP(ntp);
}
getLocationBar().updateMicButtonState();
}
/**
* For extending classes to override and carry out the changes related with the primary color
* for the current tab changing.
*/
protected void onPrimaryColorChanged(boolean shouldAnimate) { }
/**
* Sets the icon drawable that the close button in the toolbar (if any) should show.
*/
public void setCloseButtonImageResource(Drawable drawable) { }
/**
* Sets/adds a custom action button to the {@link ToolbarLayout} if it is supported.
* @param description The content description for the button.
* @param listener The {@link OnClickListener} to use for clicks to the button.
* @param buttonSource The {@link Bitmap} resource to use as the source for the button.
*/
public void setCustomActionButton(Drawable drawable, String description,
OnClickListener listener) { }
/**
* @return The height of the tab strip. Return 0 for toolbars that do not have a tabstrip.
*/
public int getTabStripHeight() {
return getResources().getDimensionPixelSize(R.dimen.tab_strip_height);
}
/**
* Triggered when the content view for the specified tab has changed.
*/
protected void onTabContentViewChanged() {
NewTabPage ntp = getToolbarDataProvider().getNewTabPageForCurrentTab();
if (ntp != null) getLocationBar().onTabLoadingNTP(ntp);
}
@Override
public boolean isReadyForTextureCapture() {
return true;
}
@Override
public boolean setForceTextureCapture(boolean forceTextureCapture) {
return false;
}
@Override
public void setLayoutUpdateHost(LayoutUpdateHost layoutUpdateHost) { }
/**
* @param attached Whether or not the web content is attached to the view heirarchy.
*/
protected void setContentAttached(boolean attached) { }
/**
* Gives inheriting classes the chance to show or hide the TabSwitcher mode of this toolbar.
* @param inTabSwitcherMode Whether or not TabSwitcher mode should be shown or hidden.
* @param showToolbar Whether or not to show the normal toolbar while animating.
* @param delayAnimation Whether or not to delay the animation until after the transition has
* finished (which can be detected by a call to
* {@link #onTabSwitcherTransitionFinished()}).
*/
protected void setTabSwitcherMode(
boolean inTabSwitcherMode, boolean showToolbar, boolean delayAnimation) { }
/**
* Gives inheriting classes the chance to update their state when the TabSwitcher transition has
* finished.
*/
protected void onTabSwitcherTransitionFinished() { }
/**
* Gives inheriting classes the chance to update themselves based on the
* number of tabs in the current TabModel.
* @param numberOfTabs The number of tabs in the current model.
*/
protected void updateTabCountVisuals(int numberOfTabs) { }
/**
* Gives inheriting classes the chance to update themselves based on default search engine
* changes.
*/
protected void onDefaultSearchEngineChanged() { }
@Override
public void getLocationBarContentRect(Rect outRect) {
View container = getLocationBar().getContainerView();
outRect.set(container.getPaddingLeft(), container.getPaddingTop(),
container.getWidth() - container.getPaddingRight(),
container.getHeight() - container.getPaddingBottom());
ViewUtils.getRelativeDrawPosition(
this, getLocationBar().getContainerView(), mTempPosition);
outRect.offset(mTempPosition[0], mTempPosition[1]);
}
@Override
public void setTextureCaptureMode(boolean textureMode) { }
@Override
public boolean shouldIgnoreSwipeGesture() {
return mUrlHasFocus
|| (mAppMenuButtonHelper != null && mAppMenuButtonHelper.isAppMenuActive())
|| mFindInPageToolbarShowing;
}
/**
* @return Whether or not the url bar has focus.
*/
protected boolean urlHasFocus() {
return mUrlHasFocus;
}
/**
* Triggered when the URL input field has gained or lost focus.
* @param hasFocus Whether the URL field has gained focus.
*/
protected void onUrlFocusChange(boolean hasFocus) {
mUrlHasFocus = hasFocus;
}
/**
* Keeps track of the first time the toolbar is drawn.
*/
private void recordFirstDrawTime() {
if (mFirstDrawTimeMs == 0) mFirstDrawTimeMs = SystemClock.elapsedRealtime();
}
/**
* Returns the elapsed realtime in ms of the time at which first draw for the toolbar occurred.
*/
public long getFirstDrawTime() {
return mFirstDrawTimeMs;
}
/**
* Notified when a navigation to a different page has occurred.
*/
protected void onNavigatedToDifferentPage() {
}
/**
* Starts load progress.
*/
protected void startLoadProgress() {
if (mProgressBar != null) {
mProgressBar.start();
}
}
/**
* Sets load progress.
* @param progress The load progress between 0 and 1.
*/
protected void setLoadProgress(float progress) {
if (mProgressBar != null) {
mProgressBar.setProgress(progress);
}
}
/**
* Finishes load progress.
* @param delayed Whether hiding progress bar should be delayed to give enough time for user to
* recognize the last state.
*/
protected void finishLoadProgress(boolean delayed) {
if (mProgressBar != null) {
mProgressBar.finish(delayed);
}
}
/**
* @return True if the progress bar is started.
*/
protected boolean isProgressStarted() {
return mProgressBar != null ? mProgressBar.isStarted() : false;
}
/**
* Finish any toolbar animations.
*/
public void finishAnimations() { }
/**
* @return The current View showing in the Tab.
*/
protected View getCurrentTabView() {
Tab tab = mToolbarDataProvider.getTab();
if (tab != null) {
return tab.getView();
}
return null;
}
/**
* @return Whether or not the toolbar is incognito.
*/
protected boolean isIncognito() {
return mToolbarDataProvider.isIncognito();
}
/**
* @return {@link LocationBar} object this {@link ToolbarLayout} contains.
*/
public abstract LocationBar getLocationBar();
/**
* Navigates the current Tab back.
* @return Whether or not the current Tab did go back.
*/
protected boolean back() {
getLocationBar().hideSuggestions();
return mToolbarTabController != null ? mToolbarTabController.back() : false;
}
/**
* Navigates the current Tab forward.
* @return Whether or not the current Tab did go forward.
*/
protected boolean forward() {
getLocationBar().hideSuggestions();
return mToolbarTabController != null ? mToolbarTabController.forward() : false;
}
/**
* If the page is currently loading, this will trigger the tab to stop. If the page is fully
* loaded, this will trigger a refresh.
*
* <p>The buttons of the toolbar will be updated as a result of making this call.
*/
protected void stopOrReloadCurrentTab() {
getLocationBar().hideSuggestions();
if (mToolbarTabController != null) mToolbarTabController.stopOrReloadCurrentTab();
}
/**
* Opens hompage in the current tab.
*/
protected void openHomepage() {
getLocationBar().hideSuggestions();
if (mToolbarTabController != null) mToolbarTabController.openHomepage();
}
@Override
public void showAppMenuUpdateBadge() {
mShowMenuBadge = true;
}
@Override
public boolean isShowingAppMenuUpdateBadge() {
return mShowMenuBadge;
}
@Override
public void removeAppMenuUpdateBadge(boolean animate) {
boolean wasShowingMenuBadge = mShowMenuBadge;
mShowMenuBadge = false;
setMenuButtonContentDescription(false);
if (!animate || !wasShowingMenuBadge) {
mMenuBadge.setVisibility(View.GONE);
return;
}
if (mIsMenuBadgeAnimationRunning && mMenuBadgeAnimatorSet != null) {
mMenuBadgeAnimatorSet.cancel();
}
// Set initial states.
mMenuButton.setAlpha(0.f);
mMenuBadgeAnimatorSet = UpdateMenuItemHelper.createHideUpdateBadgeAnimation(
mMenuButton, mMenuBadge);
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();
}
/**
* Sets the update badge visibility to VISIBLE and sets the menu button image to the badged
* bitmap.
*/
protected void setAppMenuUpdateBadgeToVisible(boolean animate) {
setMenuButtonContentDescription(true);
if (!animate || mIsMenuBadgeAnimationRunning) {
mMenuBadge.setVisibility(View.VISIBLE);
return;
}
// Set initial states.
mMenuBadge.setAlpha(0.f);
mMenuBadge.setVisibility(View.VISIBLE);
mMenuBadgeAnimatorSet = UpdateMenuItemHelper.createShowUpdateBadgeAnimation(
mMenuButton, mMenuBadge);
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();
}
protected void cancelAppMenuUpdateBadgeAnimation() {
if (mIsMenuBadgeAnimationRunning && mMenuBadgeAnimatorSet != null) {
mMenuBadgeAnimatorSet.cancel();
}
}
/**
* Sets the update menu badge drawable to the light or dark asset.
* @param useLightDrawable Whether the light drawable should be used.
*/
protected void setAppMenuUpdateBadgeDrawable(boolean useLightDrawable) {
mMenuBadge.setImageResource(useLightDrawable ? R.drawable.badge_update_light
: R.drawable.badge_update_dark);
}
/**
* Sets the content description for the menu button.
* @param isUpdateBadgeVisible Whether the update menu badge is visible.
*/
protected void setMenuButtonContentDescription(boolean isUpdateBadgeVisible) {
if (isUpdateBadgeVisible) {
mMenuButton.setContentDescription(getResources().getString(
R.string.accessibility_toolbar_btn_menu_update));
} else {
mMenuButton.setContentDescription(getResources().getString(
R.string.accessibility_toolbar_btn_menu));
}
}
}