// 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.omnibox;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Rect;
import android.text.Selection;
import android.util.AttributeSet;
import android.util.Property;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.download.DownloadUtils;
import org.chromium.chrome.browser.ntp.NewTabPage;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.toolbar.ToolbarTablet;
import org.chromium.chrome.browser.widget.animation.CancelAwareAnimatorListener;
import org.chromium.ui.UiUtils;
import org.chromium.ui.base.LocalizationUtils;
import org.chromium.ui.interpolators.BakedBezierInterpolator;
import java.util.ArrayList;
import java.util.List;
/**
* Location bar for tablet form factors.
*/
public class LocationBarTablet extends LocationBarLayout {
private static final int KEYBOARD_MODE_CHANGE_DELAY_MS = 300;
private static final long MAX_NTP_KEYBOARD_FOCUS_DURATION_MS = 200;
private static final int ICON_FADE_ANIMATION_DURATION_MS = 150;
private static final int ICON_FADE_ANIMATION_DELAY_MS = 75;
private static final int WIDTH_CHANGE_ANIMATION_DURATION_MS = 225;
private static final int WIDTH_CHANGE_ANIMATION_DELAY_MS = 75;
private final Property<LocationBarTablet, Float> mUrlFocusChangePercentProperty =
new Property<LocationBarTablet, Float>(Float.class, "") {
@Override
public Float get(LocationBarTablet object) {
return object.mUrlFocusChangePercent;
}
@Override
public void set(LocationBarTablet object, Float value) {
setUrlFocusChangePercent(value);
}
};
private final Property<LocationBarTablet, Float> mWidthChangePercentProperty =
new Property<LocationBarTablet, Float>(Float.class, "") {
@Override
public Float get(LocationBarTablet object) {
return object.mWidthChangePercent;
}
@Override
public void set(LocationBarTablet object, Float value) {
setWidthChangeAnimationPercent(value);
}
};
private final Runnable mKeyboardResizeModeTask = new Runnable() {
@Override
public void run() {
getWindowDelegate().setWindowSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
};
private View mLocationBarIcon;
private View mBookmarkButton;
private View mSaveOfflineButton;
private float mUrlFocusChangePercent;
private Animator mUrlFocusChangeAnimator;
private View[] mTargets;
private final Rect mCachedTargetBounds = new Rect();
// Whether the microphone and bookmark buttons should be shown in the location bar. These
// buttons are hidden if the window size is < 600dp.
private boolean mShouldShowButtonsWhenUnfocused;
private final int mUrlBarEndPaddingWithButtons;
private final int mUrlBarEndPaddingWithoutButtons;
// Variables needed for animating the location bar and toolbar buttons hiding/showing.
private final int mToolbarButtonsWidth;
private final int mMicButtonWidth;
private boolean mAnimatingWidthChange;
private float mWidthChangePercent;
private float mLayoutLeft;
private float mLayoutRight;
private int mToolbarStartPaddingDifference;
/**
* Constructor used to inflate from XML.
*/
public LocationBarTablet(Context context, AttributeSet attrs) {
super(context, attrs);
mShouldShowButtonsWhenUnfocused = true;
// mUrlBar currently does not have any end padding when buttons are visible in the
// unfocused location bar.
mUrlBarEndPaddingWithButtons = 0;
mUrlBarEndPaddingWithoutButtons = getResources().getDimensionPixelOffset(
R.dimen.toolbar_edge_padding);
mToolbarButtonsWidth = getResources().getDimensionPixelOffset(R.dimen.toolbar_button_width)
* ToolbarTablet.HIDEABLE_BUTTON_COUNT;
mMicButtonWidth = getResources().getDimensionPixelOffset(R.dimen.location_bar_icon_width);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mLocationBarIcon = findViewById(R.id.location_bar_icon);
mBookmarkButton = findViewById(R.id.bookmark_button);
mSaveOfflineButton = findViewById(R.id.save_offline_button);
mTargets = new View[] { mUrlBar, mDeleteButton };
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mTargets == null) return true;
View selectedTarget = null;
float selectedDistance = 0;
// newX and newY are in the coordinates of the selectedTarget.
float newX = 0;
float newY = 0;
for (View target : mTargets) {
if (!target.isShown()) continue;
mCachedTargetBounds.set(0, 0, target.getWidth(), target.getHeight());
offsetDescendantRectToMyCoords(target, mCachedTargetBounds);
float x = event.getX();
float y = event.getY();
float dx = distanceToRange(
mCachedTargetBounds.left, mCachedTargetBounds.right, x);
float dy = distanceToRange(
mCachedTargetBounds.top, mCachedTargetBounds.bottom, y);
float distance = Math.abs(dx) + Math.abs(dy);
if (selectedTarget == null || distance < selectedDistance) {
selectedTarget = target;
selectedDistance = distance;
newX = x + dx;
newY = y + dy;
}
}
if (selectedTarget == null) return false;
event.setLocation(newX, newY);
return selectedTarget.onTouchEvent(event);
}
// Returns amount by which to adjust to move value inside the given range.
private static float distanceToRange(float min, float max, float value) {
return value < min ? (min - value) : value > max ? (max - value) : 0;
}
@Override
public void handleUrlFocusAnimation(final boolean hasFocus) {
super.handleUrlFocusAnimation(hasFocus);
removeCallbacks(mKeyboardResizeModeTask);
if (mUrlFocusChangeAnimator != null && mUrlFocusChangeAnimator.isRunning()) {
mUrlFocusChangeAnimator.cancel();
mUrlFocusChangeAnimator = null;
}
if (getToolbarDataProvider().getNewTabPageForCurrentTab() == null) {
finishUrlFocusChange(hasFocus);
return;
}
Rect rootViewBounds = new Rect();
getRootView().getLocalVisibleRect(rootViewBounds);
float screenSizeRatio = (rootViewBounds.height()
/ (float) (Math.max(rootViewBounds.height(), rootViewBounds.width())));
mUrlFocusChangeAnimator =
ObjectAnimator.ofFloat(this, mUrlFocusChangePercentProperty, hasFocus ? 1f : 0f);
mUrlFocusChangeAnimator.setDuration(
(long) (MAX_NTP_KEYBOARD_FOCUS_DURATION_MS * screenSizeRatio));
mUrlFocusChangeAnimator.addListener(new CancelAwareAnimatorListener() {
@Override
public void onEnd(Animator animator) {
finishUrlFocusChange(hasFocus);
}
@Override
public void onCancel(Animator animator) {
setUrlFocusChangeInProgress(false);
}
});
setUrlFocusChangeInProgress(true);
mUrlFocusChangeAnimator.start();
}
private void finishUrlFocusChange(boolean hasFocus) {
if (hasFocus) {
if (mSecurityButton.getVisibility() == VISIBLE) mSecurityButton.setVisibility(GONE);
if (getWindowDelegate().getWindowSoftInputMode()
!= WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) {
getWindowDelegate().setWindowSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
}
UiUtils.showKeyboard(mUrlBar);
} else {
if (mSecurityButton.getVisibility() == GONE
&& mSecurityButton.getDrawable() != null
&& mSecurityButton.getDrawable().getIntrinsicWidth() > 0
&& mSecurityButton.getDrawable().getIntrinsicHeight() > 0) {
mSecurityButton.setVisibility(VISIBLE);
}
UiUtils.hideKeyboard(mUrlBar);
Selection.setSelection(mUrlBar.getText(), 0);
// Convert the keyboard back to resize mode (delay the change for an arbitrary
// amount of time in hopes the keyboard will be completely hidden before making
// this change).
if (getWindowDelegate().getWindowSoftInputMode()
!= WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) {
postDelayed(mKeyboardResizeModeTask, KEYBOARD_MODE_CHANGE_DELAY_MS);
}
}
setUrlFocusChangeInProgress(false);
}
/**
* @param shouldShowButtons Whether buttons should be displayed in the URL bar when it's not
* focused.
*/
public void setShouldShowButtonsWhenUnfocused(boolean shouldShowButtons) {
mShouldShowButtonsWhenUnfocused = shouldShowButtons;
updateButtonVisibility();
ApiCompatibilityUtils.setPaddingRelative(mUrlBar,
ApiCompatibilityUtils.getPaddingStart(mUrlBar),
mUrlBar.getPaddingTop(),
mShouldShowButtonsWhenUnfocused ? mUrlBarEndPaddingWithButtons :
mUrlBarEndPaddingWithoutButtons,
mUrlBar.getPaddingBottom());
}
/**
* Updates percentage of current the URL focus change animation.
* @param percent 1.0 is 100% focused, 0 is completely unfocused.
*/
private void setUrlFocusChangePercent(float percent) {
mUrlFocusChangePercent = percent;
NewTabPage ntp = getToolbarDataProvider().getNewTabPageForCurrentTab();
if (ntp != null) ntp.setUrlFocusChangeAnimationPercent(percent);
}
@Override
public void updateButtonVisibility() {
updateDeleteButtonVisibility();
boolean showBookmarkButton =
mShouldShowButtonsWhenUnfocused && shouldShowPageActionButtons();
mBookmarkButton.setVisibility(showBookmarkButton ? View.VISIBLE : View.GONE);
boolean showSaveOfflineButton =
mShouldShowButtonsWhenUnfocused && shouldShowSaveOfflineButton();
mSaveOfflineButton.setVisibility(showSaveOfflineButton ? View.VISIBLE : View.GONE);
if (showSaveOfflineButton) mSaveOfflineButton.setEnabled(isSaveOfflineButtonEnabled());
if (!mShouldShowButtonsWhenUnfocused) {
updateMicButtonVisiblity(mUrlFocusChangePercent);
} else {
mMicButton.setVisibility(shouldShowMicButton() ? View.VISIBLE : View.GONE);
}
}
@Override
protected void updateLayoutParams() {
// Calculate the bookmark/delete button margins.
int lastButtonSpace;
if (mSaveOfflineButton.getVisibility() == View.VISIBLE) {
MarginLayoutParams saveOfflineLayoutParams =
(MarginLayoutParams) mSaveOfflineButton.getLayoutParams();
lastButtonSpace = ApiCompatibilityUtils.getMarginEnd(saveOfflineLayoutParams);
} else {
MarginLayoutParams micLayoutParams = (MarginLayoutParams) mMicButton.getLayoutParams();
lastButtonSpace = ApiCompatibilityUtils.getMarginEnd(micLayoutParams);
}
if (mMicButton.getVisibility() == View.VISIBLE
|| mSaveOfflineButton.getVisibility() == View.VISIBLE) {
lastButtonSpace += mMicButtonWidth;
}
final MarginLayoutParams deleteLayoutParams =
(MarginLayoutParams) mDeleteButton.getLayoutParams();
final MarginLayoutParams bookmarkLayoutParams =
(MarginLayoutParams) mBookmarkButton.getLayoutParams();
ApiCompatibilityUtils.setMarginEnd(deleteLayoutParams, lastButtonSpace);
ApiCompatibilityUtils.setMarginEnd(bookmarkLayoutParams, lastButtonSpace);
mDeleteButton.setLayoutParams(deleteLayoutParams);
mBookmarkButton.setLayoutParams(bookmarkLayoutParams);
super.updateLayoutParams();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mLayoutLeft = left;
mLayoutRight = right;
if (mAnimatingWidthChange) {
setWidthChangeAnimationPercent(mWidthChangePercent);
}
}
/**
* @param button The {@link View} of the button to show.
* @return An animator to run for the given view when showing buttons in the unfocused location
* bar. This should also be used to create animators for showing toolbar buttons.
*/
public ObjectAnimator createShowButtonAnimator(View button) {
if (button.getVisibility() != View.VISIBLE) {
button.setAlpha(0.f);
}
ObjectAnimator buttonAnimator = ObjectAnimator.ofFloat(button, View.ALPHA, 1.f);
buttonAnimator.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE);
buttonAnimator.setStartDelay(ICON_FADE_ANIMATION_DELAY_MS);
buttonAnimator.setDuration(ICON_FADE_ANIMATION_DURATION_MS);
return buttonAnimator;
}
/**
* @param button The {@link View} of the button to hide.
* @return An animator to run for the given view when hiding buttons in the unfocused location
* bar. This should also be used to create animators for hiding toolbar buttons.
*/
public ObjectAnimator createHideButtonAnimator(View button) {
ObjectAnimator buttonAnimator = ObjectAnimator.ofFloat(button, View.ALPHA, 0.f);
buttonAnimator.setInterpolator(BakedBezierInterpolator.FADE_OUT_CURVE);
buttonAnimator.setDuration(ICON_FADE_ANIMATION_DURATION_MS);
return buttonAnimator;
}
/**
* Creates animators for showing buttons in the unfocused location bar. The buttons fade in
* while width of the location bar gets smaller. There are toolbar buttons that also show at
* the same time, causing the width of the location bar to change.
*
* @param toolbarStartPaddingDifference The difference in the toolbar's start padding between
* the beginning and end of the animation.
* @return An ArrayList of animators to run.
*/
public List<Animator> getShowButtonsWhenUnfocusedAnimators(int toolbarStartPaddingDifference) {
mToolbarStartPaddingDifference = toolbarStartPaddingDifference;
ArrayList<Animator> animators = new ArrayList<>();
Animator widthChangeAnimator = ObjectAnimator.ofFloat(
this, mWidthChangePercentProperty, 0f);
widthChangeAnimator.setDuration(WIDTH_CHANGE_ANIMATION_DURATION_MS);
widthChangeAnimator.setInterpolator(BakedBezierInterpolator.TRANSFORM_CURVE);
widthChangeAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mAnimatingWidthChange = true;
setShouldShowButtonsWhenUnfocused(true);
}
@Override
public void onAnimationEnd(Animator animation) {
// Only reset values if the animation is ending because it's completely finished
// and not because it was canceled.
if (mWidthChangePercent == 0.f) {
mAnimatingWidthChange = false;
resetValuesAfterAnimation();
}
}
});
animators.add(widthChangeAnimator);
// When buttons show in the unfocused location bar, either the delete button or bookmark
// button will be showing. If the delete button is currently showing, the bookmark button
// should not fade in.
if (mDeleteButton.getVisibility() != View.VISIBLE) {
animators.add(createShowButtonAnimator(mBookmarkButton));
}
if (shouldShowSaveOfflineButton()) {
animators.add(createShowButtonAnimator(mSaveOfflineButton));
// If the microphone button is already fully visible, don't animate its appearance.
} else if (mMicButton.getVisibility() != View.VISIBLE || mMicButton.getAlpha() != 1.f) {
animators.add(createShowButtonAnimator(mMicButton));
}
return animators;
}
/**
* Creates animators for hiding buttons in the unfocused location bar. The buttons fade out
* while width of the location bar gets larger. There are toolbar buttons that also hide at the
* same time, causing the width of the location bar to change.
*
* @param toolbarStartPaddingDifference The difference in the toolbar's start padding between
* the beginning and end of the animation.
* @return An ArrayList of animators to run.
*/
public List<Animator> getHideButtonsWhenUnfocusedAnimators(int toolbarStartPaddingDifference) {
mToolbarStartPaddingDifference = toolbarStartPaddingDifference;
ArrayList<Animator> animators = new ArrayList<>();
Animator widthChangeAnimator =
ObjectAnimator.ofFloat(this, mWidthChangePercentProperty, 1f);
widthChangeAnimator.setStartDelay(WIDTH_CHANGE_ANIMATION_DELAY_MS);
widthChangeAnimator.setDuration(WIDTH_CHANGE_ANIMATION_DURATION_MS);
widthChangeAnimator.setInterpolator(BakedBezierInterpolator.TRANSFORM_CURVE);
widthChangeAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mAnimatingWidthChange = true;
}
@Override
public void onAnimationEnd(Animator animation) {
// Only reset values if the animation is ending because it's completely finished
// and not because it was canceled.
if (mWidthChangePercent == 1.f) {
mAnimatingWidthChange = false;
resetValuesAfterAnimation();
setShouldShowButtonsWhenUnfocused(false);
}
}
});
animators.add(widthChangeAnimator);
// When buttons show in the unfocused location bar, either the delete button or bookmark
// button will be showing. If the delete button is currently showing, the bookmark button
// should not fade out.
if (mDeleteButton.getVisibility() != View.VISIBLE) {
animators.add(createHideButtonAnimator(mBookmarkButton));
}
if (shouldShowSaveOfflineButton() && mSaveOfflineButton.getVisibility() == View.VISIBLE) {
animators.add(createHideButtonAnimator(mSaveOfflineButton));
} else if (!(mUrlBar.isFocused() && mDeleteButton.getVisibility() != View.VISIBLE)) {
// If the save offline button isn't enabled, the microphone button always shows when
// buttons are shown in the unfocused location bar. When buttons are hidden in the
// unfocused location bar, the microphone shows if the location bar is focused and the
// delete button isn't showing. The microphone button should not be hidden if the
// url bar is currently focused and the delete button isn't showing.
animators.add(createHideButtonAnimator(mMicButton));
}
return animators;
}
/**
* Resets the alpha and translation X for all views affected by the animations for showing or
* hiding buttons.
*/
private void resetValuesAfterAnimation() {
mMicButton.setTranslationX(0);
mDeleteButton.setTranslationX(0);
mBookmarkButton.setTranslationX(0);
mSaveOfflineButton.setTranslationX(0);
mLocationBarIcon.setTranslationX(0);
mUrlBar.setTranslationX(0);
mMicButton.setAlpha(1.f);
mDeleteButton.setAlpha(1.f);
mBookmarkButton.setAlpha(1.f);
mSaveOfflineButton.setAlpha(1.f);
}
/**
* Updates completion percentage for the location bar width change animation.
* @param percent How complete the animation is, where 0 represents the normal width (toolbar
* buttons fully visible) and 1.f represents the expanded width (toolbar buttons
* fully hidden).
*/
private void setWidthChangeAnimationPercent(float percent) {
mWidthChangePercent = percent;
float offset = (mToolbarButtonsWidth + mToolbarStartPaddingDifference) * percent;
if (LocalizationUtils.isLayoutRtl()) {
// The location bar's right edge is its regular layout position when toolbar buttons are
// completely visible and its layout position + mToolbarButtonsWidth when toolbar
// buttons are completely hidden.
setRight((int) (mLayoutRight + offset));
} else {
// The location bar's left edge is it's regular layout position when toolbar buttons are
// completely visible and its layout position - mToolbarButtonsWidth when they are
// completely hidden.
setLeft((int) (mLayoutLeft - offset));
}
// As the location bar's right edge moves right (increases) or left edge moves left
// (decreases), the child views' translation X increases, keeping them visually in the same
// location for the duration of the animation.
int deleteOffset = (int) (mMicButtonWidth * percent);
setChildTranslationsForWidthChangeAnimation((int) offset, deleteOffset);
}
/**
* Sets the translation X values for child views during the width change animation. This
* compensates for the change to the left/right position of the location bar and ensures child
* views stay in the same spot visually during the animation.
*
* The delete button is special because if it's visible during the animation its start and end
* location are not the same. When buttons are shown in the unfocused location bar, the delete
* button is left of the microphone. When buttons are not shown in the unfocused location bar,
* the delete button is aligned with the left edge of the location bar.
*
* @param offset The offset to use for the child views.
* @param deleteOffset The additional offset to use for the delete button.
*/
private void setChildTranslationsForWidthChangeAnimation(int offset, int deleteOffset) {
if (!ApiCompatibilityUtils.isLayoutRtl(this)) {
// When the location bar layout direction is LTR, the buttons at the end (left side)
// of the location bar need to stick to the left edge.
if (mSaveOfflineButton.getVisibility() == View.VISIBLE) {
mSaveOfflineButton.setTranslationX(offset);
} else {
mMicButton.setTranslationX(offset);
}
if (mDeleteButton.getVisibility() == View.VISIBLE) {
mDeleteButton.setTranslationX(offset + deleteOffset);
} else {
mBookmarkButton.setTranslationX(offset);
}
} else {
// When the location bar layout direction is RTL, the location bar icon and url
// container at the start (right side) of the location bar need to stick to the right
// edge.
mLocationBarIcon.setTranslationX(offset);
mUrlBar.setTranslationX(offset);
if (mDeleteButton.getVisibility() == View.VISIBLE) {
mDeleteButton.setTranslationX(-deleteOffset);
}
}
}
private boolean shouldShowSaveOfflineButton() {
if (!mNativeInitialized || mToolbarDataProvider == null) return false;
Tab tab = mToolbarDataProvider.getTab();
if (tab == null) return false;
// The save offline button should not be shown on native pages. Currently, trying to
// save an offline page in incognito crashes, so don't show it on incognito either.
return DownloadUtils.isDownloadHomeEnabled() && shouldShowPageActionButtons()
&& !tab.isIncognito();
}
private boolean isSaveOfflineButtonEnabled() {
if (mToolbarDataProvider == null) return false;
return DownloadUtils.isAllowedToDownloadPage(mToolbarDataProvider.getTab());
}
private boolean shouldShowPageActionButtons() {
if (!mNativeInitialized) return true;
// If the new downloads UI isn't enabled, the only page action is the bookmark button. It
// should be shown if the delete button isn't showing. If the download UI is enabled, there
// are two actions, bookmark and save offline, and they should be shown if the omnibox isn't
// focused.
return (!shouldShowDeleteButton() && !DownloadUtils.isDownloadHomeEnabled())
|| !(mUrlBar.hasFocus() || mUrlFocusChangeInProgress);
}
private boolean shouldShowMicButton() {
// If the download UI is enabled, the mic button should be only be shown when the url bar
// is focused.
return isVoiceSearchEnabled() && mNativeInitialized
&& (!DownloadUtils.isDownloadHomeEnabled()
|| (mUrlBar.hasFocus() || mUrlFocusChangeInProgress));
}
}