1 // Copyright 2015 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.omnibox; 6 7 import android.animation.Animator; 8 import android.animation.AnimatorListenerAdapter; 9 import android.animation.ObjectAnimator; 10 import android.content.Context; 11 import android.graphics.Rect; 12 import android.util.AttributeSet; 13 import android.util.Property; 14 import android.view.MotionEvent; 15 import android.view.View; 16 17 import org.chromium.chrome.R; 18 import org.chromium.chrome.browser.download.DownloadUtils; 19 import org.chromium.chrome.browser.tab.Tab; 20 import org.chromium.chrome.browser.toolbar.top.ToolbarTablet; 21 import org.chromium.components.browser_ui.widget.animation.CancelAwareAnimatorListener; 22 import org.chromium.ui.base.LocalizationUtils; 23 import org.chromium.ui.interpolators.BakedBezierInterpolator; 24 25 import java.util.ArrayList; 26 import java.util.List; 27 28 /** 29 * Location bar for tablet form factors. 30 */ 31 class LocationBarTablet extends LocationBarLayout { 32 private static final long MAX_NTP_KEYBOARD_FOCUS_DURATION_MS = 200; 33 34 private static final int ICON_FADE_ANIMATION_DURATION_MS = 150; 35 private static final int ICON_FADE_ANIMATION_DELAY_MS = 75; 36 private static final int WIDTH_CHANGE_ANIMATION_DURATION_MS = 225; 37 private static final int WIDTH_CHANGE_ANIMATION_DELAY_MS = 75; 38 39 private final Property<LocationBarTablet, Float> mUrlFocusChangeFractionProperty = 40 new Property<LocationBarTablet, Float>(Float.class, "") { 41 @Override 42 public Float get(LocationBarTablet object) { 43 return object.mUrlFocusChangeFraction; 44 } 45 46 @Override 47 public void set(LocationBarTablet object, Float value) { 48 setUrlFocusChangeFraction(value); 49 } 50 }; 51 52 private final Property<LocationBarTablet, Float> mWidthChangeFractionProperty = 53 new Property<LocationBarTablet, Float>(Float.class, "") { 54 @Override 55 public Float get(LocationBarTablet object) { 56 return object.mWidthChangeFraction; 57 } 58 59 @Override 60 public void set(LocationBarTablet object, Float value) { 61 setWidthChangeAnimationFraction(value); 62 } 63 }; 64 65 private View mLocationBarIcon; 66 private View mBookmarkButton; 67 private View mSaveOfflineButton; 68 private Animator mUrlFocusChangeAnimator; 69 private View[] mTargets; 70 private final Rect mCachedTargetBounds = new Rect(); 71 72 // Whether the microphone and bookmark buttons should be shown in the location bar. These 73 // buttons are hidden if the window size is < 600dp. 74 private boolean mShouldShowButtonsWhenUnfocused; 75 76 // Variables needed for animating the location bar and toolbar buttons hiding/showing. 77 private final int mToolbarButtonsWidth; 78 private final int mMicButtonWidth; 79 private boolean mAnimatingWidthChange; 80 private float mWidthChangeFraction; 81 private float mLayoutLeft; 82 private float mLayoutRight; 83 private int mToolbarStartPaddingDifference; 84 85 /** 86 * Constructor used to inflate from XML. 87 */ LocationBarTablet(Context context, AttributeSet attrs)88 public LocationBarTablet(Context context, AttributeSet attrs) { 89 super(context, attrs); 90 mShouldShowButtonsWhenUnfocused = true; 91 92 mToolbarButtonsWidth = getResources().getDimensionPixelOffset(R.dimen.toolbar_button_width) 93 * ToolbarTablet.HIDEABLE_BUTTON_COUNT; 94 mMicButtonWidth = getResources().getDimensionPixelOffset(R.dimen.location_bar_icon_width); 95 } 96 97 @Override onFinishInflate()98 protected void onFinishInflate() { 99 super.onFinishInflate(); 100 101 mLocationBarIcon = findViewById(R.id.location_bar_status_icon); 102 mBookmarkButton = findViewById(R.id.bookmark_button); 103 mSaveOfflineButton = findViewById(R.id.save_offline_button); 104 105 mTargets = new View[] {mUrlBar, mDeleteButton}; 106 mStatusCoordinator.setShowIconsWhenUrlFocused(true); 107 mStatusCoordinator.setStatusIconShown(true); 108 } 109 110 @Override onTouchEvent(MotionEvent event)111 public boolean onTouchEvent(MotionEvent event) { 112 if (mTargets == null) return true; 113 114 View selectedTarget = null; 115 float selectedDistance = 0; 116 // newX and newY are in the coordinates of the selectedTarget. 117 float newX = 0; 118 float newY = 0; 119 for (View target : mTargets) { 120 if (!target.isShown()) continue; 121 122 mCachedTargetBounds.set(0, 0, target.getWidth(), target.getHeight()); 123 offsetDescendantRectToMyCoords(target, mCachedTargetBounds); 124 float x = event.getX(); 125 float y = event.getY(); 126 float dx = distanceToRange(mCachedTargetBounds.left, mCachedTargetBounds.right, x); 127 float dy = distanceToRange(mCachedTargetBounds.top, mCachedTargetBounds.bottom, y); 128 float distance = Math.abs(dx) + Math.abs(dy); 129 if (selectedTarget == null || distance < selectedDistance) { 130 selectedTarget = target; 131 selectedDistance = distance; 132 newX = x + dx; 133 newY = y + dy; 134 } 135 } 136 137 if (selectedTarget == null) return false; 138 139 event.setLocation(newX, newY); 140 return selectedTarget.onTouchEvent(event); 141 } 142 143 @Override handleUrlFocusAnimation(final boolean hasFocus)144 public void handleUrlFocusAnimation(final boolean hasFocus) { 145 super.handleUrlFocusAnimation(hasFocus); 146 147 if (mUrlFocusChangeAnimator != null && mUrlFocusChangeAnimator.isRunning()) { 148 mUrlFocusChangeAnimator.cancel(); 149 mUrlFocusChangeAnimator = null; 150 } 151 152 if (mLocationBarDataProvider.getNewTabPageDelegate().isCurrentlyVisible()) { 153 finishUrlFocusChange(hasFocus, /* shouldShowKeyboard= */ hasFocus); 154 return; 155 } 156 157 Rect rootViewBounds = new Rect(); 158 getRootView().getLocalVisibleRect(rootViewBounds); 159 float screenSizeRatio = (rootViewBounds.height() 160 / (float) (Math.max(rootViewBounds.height(), rootViewBounds.width()))); 161 mUrlFocusChangeAnimator = 162 ObjectAnimator.ofFloat(this, mUrlFocusChangeFractionProperty, hasFocus ? 1f : 0f); 163 mUrlFocusChangeAnimator.setDuration( 164 (long) (MAX_NTP_KEYBOARD_FOCUS_DURATION_MS * screenSizeRatio)); 165 mUrlFocusChangeAnimator.addListener(new CancelAwareAnimatorListener() { 166 @Override 167 public void onEnd(Animator animator) { 168 finishUrlFocusChange(hasFocus, /* shouldShowKeyboard= */ hasFocus); 169 } 170 171 @Override 172 public void onCancel(Animator animator) { 173 setUrlFocusChangeInProgress(false); 174 } 175 }); 176 setUrlFocusChangeInProgress(true); 177 mUrlFocusChangeAnimator.start(); 178 } 179 180 /** 181 * Updates progress of current the URL focus change animation. 182 * 183 * @param fraction 1.0 is 100% focused, 0 is completely unfocused. 184 */ 185 @Override setUrlFocusChangeFraction(float fraction)186 public void setUrlFocusChangeFraction(float fraction) { 187 super.setUrlFocusChangeFraction(fraction); 188 mLocationBarDataProvider.getNewTabPageDelegate().setUrlFocusChangeAnimationPercent( 189 fraction); 190 } 191 192 @Override updateButtonVisibility()193 public void updateButtonVisibility() { 194 super.updateButtonVisibility(); 195 196 boolean showBookmarkButton = 197 mShouldShowButtonsWhenUnfocused && shouldShowPageActionButtons(); 198 mBookmarkButton.setVisibility(showBookmarkButton ? View.VISIBLE : View.GONE); 199 200 boolean showSaveOfflineButton = 201 mShouldShowButtonsWhenUnfocused && shouldShowSaveOfflineButton(); 202 mSaveOfflineButton.setVisibility(showSaveOfflineButton ? View.VISIBLE : View.GONE); 203 if (showSaveOfflineButton) mSaveOfflineButton.setEnabled(isSaveOfflineButtonEnabled()); 204 205 if (!mShouldShowButtonsWhenUnfocused) { 206 updateMicButtonVisibility(); 207 } else { 208 mMicButton.setVisibility(shouldShowMicButton() ? View.VISIBLE : View.GONE); 209 } 210 } 211 212 @Override onSuggestionsHidden()213 public void onSuggestionsHidden() { 214 super.onSuggestionsHidden(); 215 mStatusCoordinator.setFirstSuggestionIsSearchType(false); 216 } 217 218 @Override onSuggestionsChanged(String autocompleteText)219 public void onSuggestionsChanged(String autocompleteText) { 220 super.onSuggestionsChanged(autocompleteText); 221 mStatusCoordinator.setFirstSuggestionIsSearchType( 222 mAutocompleteCoordinator.getSuggestionCount() > 0 223 && mAutocompleteCoordinator.getSuggestionAt(0).isSearchSuggestion()); 224 } 225 226 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)227 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 228 int measuredWidth = getMeasuredWidth(); 229 230 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 231 232 if (getMeasuredWidth() != measuredWidth) { 233 setUnfocusedWidth(getMeasuredWidth()); 234 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 235 } 236 } 237 238 @Override onLayout(boolean changed, int left, int top, int right, int bottom)239 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 240 super.onLayout(changed, left, top, right, bottom); 241 mLayoutLeft = left; 242 mLayoutRight = right; 243 244 if (mAnimatingWidthChange) { 245 setWidthChangeAnimationFraction(mWidthChangeFraction); 246 } 247 } 248 249 /** 250 * @param shouldShowButtons Whether buttons should be displayed in the URL bar when it's not 251 * focused. 252 */ setShouldShowButtonsWhenUnfocused(boolean shouldShowButtons)253 public void setShouldShowButtonsWhenUnfocused(boolean shouldShowButtons) { 254 mShouldShowButtonsWhenUnfocused = shouldShowButtons; 255 updateButtonVisibility(); 256 } 257 258 /** 259 * @param button The {@link View} of the button to show. 260 * @return An animator to run for the given view when showing buttons in the unfocused location 261 * bar. This should also be used to create animators for showing toolbar buttons. 262 */ createShowButtonAnimator(View button)263 public ObjectAnimator createShowButtonAnimator(View button) { 264 if (button.getVisibility() != View.VISIBLE) { 265 button.setAlpha(0.f); 266 } 267 ObjectAnimator buttonAnimator = ObjectAnimator.ofFloat(button, View.ALPHA, 1.f); 268 buttonAnimator.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE); 269 buttonAnimator.setStartDelay(ICON_FADE_ANIMATION_DELAY_MS); 270 buttonAnimator.setDuration(ICON_FADE_ANIMATION_DURATION_MS); 271 return buttonAnimator; 272 } 273 274 /** 275 * @param button The {@link View} of the button to hide. 276 * @return An animator to run for the given view when hiding buttons in the unfocused location 277 * bar. This should also be used to create animators for hiding toolbar buttons. 278 */ createHideButtonAnimator(View button)279 public ObjectAnimator createHideButtonAnimator(View button) { 280 ObjectAnimator buttonAnimator = ObjectAnimator.ofFloat(button, View.ALPHA, 0.f); 281 buttonAnimator.setInterpolator(BakedBezierInterpolator.FADE_OUT_CURVE); 282 buttonAnimator.setDuration(ICON_FADE_ANIMATION_DURATION_MS); 283 return buttonAnimator; 284 } 285 286 /** 287 * Creates animators for showing buttons in the unfocused location bar. The buttons fade in 288 * while width of the location bar gets smaller. There are toolbar buttons that also show at 289 * the same time, causing the width of the location bar to change. 290 * 291 * @param toolbarStartPaddingDifference The difference in the toolbar's start padding between 292 * the beginning and end of the animation. 293 * @return An ArrayList of animators to run. 294 */ getShowButtonsWhenUnfocusedAnimators(int toolbarStartPaddingDifference)295 public List<Animator> getShowButtonsWhenUnfocusedAnimators(int toolbarStartPaddingDifference) { 296 mToolbarStartPaddingDifference = toolbarStartPaddingDifference; 297 298 ArrayList<Animator> animators = new ArrayList<>(); 299 300 Animator widthChangeAnimator = 301 ObjectAnimator.ofFloat(this, mWidthChangeFractionProperty, 0f); 302 widthChangeAnimator.setDuration(WIDTH_CHANGE_ANIMATION_DURATION_MS); 303 widthChangeAnimator.setInterpolator(BakedBezierInterpolator.TRANSFORM_CURVE); 304 widthChangeAnimator.addListener(new AnimatorListenerAdapter() { 305 @Override 306 public void onAnimationStart(Animator animation) { 307 mAnimatingWidthChange = true; 308 setShouldShowButtonsWhenUnfocused(true); 309 } 310 311 @Override 312 public void onAnimationEnd(Animator animation) { 313 // Only reset values if the animation is ending because it's completely finished 314 // and not because it was canceled. 315 if (mWidthChangeFraction == 0.f) { 316 mAnimatingWidthChange = false; 317 resetValuesAfterAnimation(); 318 } 319 } 320 }); 321 animators.add(widthChangeAnimator); 322 323 // When buttons show in the unfocused location bar, either the delete button or bookmark 324 // button will be showing. If the delete button is currently showing, the bookmark button 325 // should not fade in. 326 if (mDeleteButton.getVisibility() != View.VISIBLE) { 327 animators.add(createShowButtonAnimator(mBookmarkButton)); 328 } 329 330 if (shouldShowSaveOfflineButton()) { 331 animators.add(createShowButtonAnimator(mSaveOfflineButton)); 332 } else if (mMicButton.getVisibility() != View.VISIBLE || mMicButton.getAlpha() != 1.f) { 333 // If the microphone button is already fully visible, don't animate its appearance. 334 animators.add(createShowButtonAnimator(mMicButton)); 335 } 336 337 return animators; 338 } 339 340 /** 341 * Creates animators for hiding buttons in the unfocused location bar. The buttons fade out 342 * while width of the location bar gets larger. There are toolbar buttons that also hide at the 343 * same time, causing the width of the location bar to change. 344 * 345 * @param toolbarStartPaddingDifference The difference in the toolbar's start padding between 346 * the beginning and end of the animation. 347 * @return An ArrayList of animators to run. 348 */ getHideButtonsWhenUnfocusedAnimators(int toolbarStartPaddingDifference)349 public List<Animator> getHideButtonsWhenUnfocusedAnimators(int toolbarStartPaddingDifference) { 350 mToolbarStartPaddingDifference = toolbarStartPaddingDifference; 351 352 ArrayList<Animator> animators = new ArrayList<>(); 353 354 Animator widthChangeAnimator = 355 ObjectAnimator.ofFloat(this, mWidthChangeFractionProperty, 1f); 356 widthChangeAnimator.setStartDelay(WIDTH_CHANGE_ANIMATION_DELAY_MS); 357 widthChangeAnimator.setDuration(WIDTH_CHANGE_ANIMATION_DURATION_MS); 358 widthChangeAnimator.setInterpolator(BakedBezierInterpolator.TRANSFORM_CURVE); 359 widthChangeAnimator.addListener(new AnimatorListenerAdapter() { 360 @Override 361 public void onAnimationStart(Animator animation) { 362 mAnimatingWidthChange = true; 363 } 364 365 @Override 366 public void onAnimationEnd(Animator animation) { 367 // Only reset values if the animation is ending because it's completely finished 368 // and not because it was canceled. 369 if (mWidthChangeFraction == 1.f) { 370 mAnimatingWidthChange = false; 371 resetValuesAfterAnimation(); 372 setShouldShowButtonsWhenUnfocused(false); 373 } 374 } 375 }); 376 animators.add(widthChangeAnimator); 377 378 // When buttons show in the unfocused location bar, either the delete button or bookmark 379 // button will be showing. If the delete button is currently showing, the bookmark button 380 // should not fade out. 381 if (mDeleteButton.getVisibility() != View.VISIBLE) { 382 animators.add(createHideButtonAnimator(mBookmarkButton)); 383 } 384 385 if (shouldShowSaveOfflineButton() && mSaveOfflineButton.getVisibility() == View.VISIBLE) { 386 animators.add(createHideButtonAnimator(mSaveOfflineButton)); 387 } else if (!(mUrlBar.hasFocus() && mDeleteButton.getVisibility() != View.VISIBLE)) { 388 // If the save offline button isn't enabled, the microphone button always shows when 389 // buttons are shown in the unfocused location bar. When buttons are hidden in the 390 // unfocused location bar, the microphone shows if the location bar is focused and the 391 // delete button isn't showing. The microphone button should not be hidden if the 392 // url bar is currently focused and the delete button isn't showing. 393 animators.add(createHideButtonAnimator(mMicButton)); 394 } 395 396 return animators; 397 } 398 399 /** Returns amount by which to adjust to move value inside the given range. */ distanceToRange(float min, float max, float value)400 private static float distanceToRange(float min, float max, float value) { 401 return value < min ? (min - value) : value > max ? (max - value) : 0; 402 } 403 404 /** 405 * Resets the alpha and translation X for all views affected by the animations for showing or 406 * hiding buttons. 407 */ resetValuesAfterAnimation()408 private void resetValuesAfterAnimation() { 409 mMicButton.setTranslationX(0); 410 mDeleteButton.setTranslationX(0); 411 mBookmarkButton.setTranslationX(0); 412 mSaveOfflineButton.setTranslationX(0); 413 mLocationBarIcon.setTranslationX(0); 414 mUrlBar.setTranslationX(0); 415 416 mMicButton.setAlpha(1.f); 417 mDeleteButton.setAlpha(1.f); 418 mBookmarkButton.setAlpha(1.f); 419 mSaveOfflineButton.setAlpha(1.f); 420 } 421 422 /** 423 * Updates completion progress for the location bar width change animation. 424 * 425 * @param fraction How complete the animation is, where 0 represents the normal width (toolbar 426 * buttons fully visible) and 1.f represents the expanded width (toolbar buttons fully 427 * hidden). 428 */ setWidthChangeAnimationFraction(float fraction)429 private void setWidthChangeAnimationFraction(float fraction) { 430 mWidthChangeFraction = fraction; 431 432 float offset = (mToolbarButtonsWidth + mToolbarStartPaddingDifference) * fraction; 433 434 if (LocalizationUtils.isLayoutRtl()) { 435 // The location bar's right edge is its regular layout position when toolbar buttons are 436 // completely visible and its layout position + mToolbarButtonsWidth when toolbar 437 // buttons are completely hidden. 438 setRight((int) (mLayoutRight + offset)); 439 } else { 440 // The location bar's left edge is it's regular layout position when toolbar buttons are 441 // completely visible and its layout position - mToolbarButtonsWidth when they are 442 // completely hidden. 443 setLeft((int) (mLayoutLeft - offset)); 444 } 445 446 // As the location bar's right edge moves right (increases) or left edge moves left 447 // (decreases), the child views' translation X increases, keeping them visually in the same 448 // location for the duration of the animation. 449 int deleteOffset = (int) (mMicButtonWidth * fraction); 450 setChildTranslationsForWidthChangeAnimation((int) offset, deleteOffset); 451 } 452 453 /** 454 * Sets the translation X values for child views during the width change animation. This 455 * compensates for the change to the left/right position of the location bar and ensures child 456 * views stay in the same spot visually during the animation. 457 * 458 * The delete button is special because if it's visible during the animation its start and end 459 * location are not the same. When buttons are shown in the unfocused location bar, the delete 460 * button is left of the microphone. When buttons are not shown in the unfocused location bar, 461 * the delete button is aligned with the left edge of the location bar. 462 * 463 * @param offset The offset to use for the child views. 464 * @param deleteOffset The additional offset to use for the delete button. 465 */ setChildTranslationsForWidthChangeAnimation(int offset, int deleteOffset)466 private void setChildTranslationsForWidthChangeAnimation(int offset, int deleteOffset) { 467 if (getLayoutDirection() != LAYOUT_DIRECTION_RTL) { 468 // When the location bar layout direction is LTR, the buttons at the end (left side) 469 // of the location bar need to stick to the left edge. 470 if (mSaveOfflineButton.getVisibility() == View.VISIBLE) { 471 mSaveOfflineButton.setTranslationX(offset); 472 } else { 473 mMicButton.setTranslationX(offset); 474 } 475 476 if (mDeleteButton.getVisibility() == View.VISIBLE) { 477 mDeleteButton.setTranslationX(offset + deleteOffset); 478 } else { 479 mBookmarkButton.setTranslationX(offset); 480 } 481 } else { 482 // When the location bar layout direction is RTL, the location bar icon and url 483 // container at the start (right side) of the location bar need to stick to the right 484 // edge. 485 mLocationBarIcon.setTranslationX(offset); 486 mUrlBar.setTranslationX(offset); 487 488 if (mDeleteButton.getVisibility() == View.VISIBLE) { 489 mDeleteButton.setTranslationX(-deleteOffset); 490 } 491 } 492 } 493 shouldShowSaveOfflineButton()494 private boolean shouldShowSaveOfflineButton() { 495 if (!mNativeInitialized || mLocationBarDataProvider == null) return false; 496 Tab tab = mLocationBarDataProvider.getTab(); 497 if (tab == null) return false; 498 // The save offline button should not be shown on native pages. Currently, trying to 499 // save an offline page in incognito crashes, so don't show it on incognito either. 500 return shouldShowPageActionButtons() && !tab.isIncognito(); 501 } 502 isSaveOfflineButtonEnabled()503 private boolean isSaveOfflineButtonEnabled() { 504 if (mLocationBarDataProvider == null) return false; 505 return DownloadUtils.isAllowedToDownloadPage(mLocationBarDataProvider.getTab()); 506 } 507 shouldShowPageActionButtons()508 private boolean shouldShowPageActionButtons() { 509 if (!mNativeInitialized) return true; 510 511 // There are two actions, bookmark and save offline, and they should be shown if the 512 // omnibox isn't focused. 513 return !(mUrlBar.hasFocus() || isUrlFocusChangeInProgress()); 514 } 515 shouldShowMicButton()516 private boolean shouldShowMicButton() { 517 // If the download UI is enabled, the mic button should be only be shown when the url bar 518 // is focused. 519 return mVoiceSearchEnabled && mNativeInitialized 520 && (mUrlBar.hasFocus() || isUrlFocusChangeInProgress()); 521 } 522 } 523