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.ObjectAnimator; 9 import android.content.Context; 10 import android.graphics.Canvas; 11 import android.graphics.Rect; 12 import android.util.AttributeSet; 13 import android.view.TouchDelegate; 14 import android.view.View; 15 import android.widget.FrameLayout; 16 17 import org.chromium.base.TraceEvent; 18 import org.chromium.chrome.R; 19 import org.chromium.chrome.browser.ntp.NewTabPage; 20 import org.chromium.ui.interpolators.BakedBezierInterpolator; 21 22 import java.util.List; 23 24 /** 25 * A location bar implementation specific for smaller/phone screens. 26 */ 27 class LocationBarPhone extends LocationBarLayout { 28 private static final int ACTION_BUTTON_TOUCH_OVERFLOW_LEFT = 15; 29 30 private View mFirstVisibleFocusedView; 31 private View mUrlBar; 32 private View mStatusView; 33 34 /** 35 * Constructor used to inflate from XML. 36 */ LocationBarPhone(Context context, AttributeSet attrs)37 public LocationBarPhone(Context context, AttributeSet attrs) { 38 super(context, attrs); 39 } 40 41 @Override onFinishInflate()42 protected void onFinishInflate() { 43 super.onFinishInflate(); 44 45 mUrlBar = findViewById(R.id.url_bar); 46 mStatusView = findViewById(R.id.location_bar_status); 47 // Assign the first visible view here only if it hasn't been set by the DSE icon experiment. 48 // See onFinishNativeInitialization ready for when this variable is set for the DSE icon 49 // case. 50 mFirstVisibleFocusedView = 51 mFirstVisibleFocusedView == null ? mUrlBar : mFirstVisibleFocusedView; 52 53 Rect delegateArea = new Rect(); 54 mUrlActionContainer.getHitRect(delegateArea); 55 delegateArea.left -= ACTION_BUTTON_TOUCH_OVERFLOW_LEFT; 56 TouchDelegate touchDelegate = new TouchDelegate(delegateArea, mUrlActionContainer); 57 assert mUrlActionContainer.getParent() == this; 58 mCompositeTouchDelegate.addDelegateForDescendantView(touchDelegate); 59 } 60 61 @Override updateSearchEngineStatusIcon(boolean shouldShowSearchEngineLogo, boolean isSearchEngineGoogle, String searchEngineUrl)62 protected void updateSearchEngineStatusIcon(boolean shouldShowSearchEngineLogo, 63 boolean isSearchEngineGoogle, String searchEngineUrl) { 64 super.updateSearchEngineStatusIcon( 65 shouldShowSearchEngineLogo, isSearchEngineGoogle, searchEngineUrl); 66 67 // The search engine icon will be the first visible focused view when it's showing. 68 shouldShowSearchEngineLogo = SearchEngineLogoUtils.shouldShowSearchEngineLogo( 69 mLocationBarDataProvider.isIncognito()); 70 71 // This branch will be hit if the search engine logo experiment is enabled. 72 if (SearchEngineLogoUtils.isSearchEngineLogoEnabled()) { 73 // Setup the padding once we're loaded, the focused padding changes will happen with 74 // post-layout positioning via setTranslation. This is a byproduct of the way we do the 75 // omnibox un/focus animation which is by writing a function f(x) where x ranges from 76 // 0 (totally unfocused) to 1 (totally focused). Positioning the location bar and it's 77 // children this way doesn't affect the views' bounds (including hit rect). But these 78 // hit rects are preserved for the views that matter (the icon and the url actions 79 // container). 80 int lateralPadding = getResources().getDimensionPixelOffset( 81 R.dimen.sei_location_bar_lateral_padding); 82 setPaddingRelative(lateralPadding, getPaddingTop(), lateralPadding, getPaddingBottom()); 83 } 84 85 // This branch will be hit if the search engine logo experiment is enabled and we should 86 // show the logo. 87 if (shouldShowSearchEngineLogo) { 88 // When the search engine icon is enabled, icons are translations into the parent view's 89 // padding area. Set clip padding to false to prevent them from getting clipped. 90 setClipToPadding(false); 91 } 92 setShowIconsWhenUrlFocused(shouldShowSearchEngineLogo); 93 } 94 95 /** 96 * Updates progress of current the URL focus change animation. 97 * 98 * @param fraction 1.0 is 100% focused, 0 is completely unfocused. 99 */ 100 @Override setUrlFocusChangeFraction(float fraction)101 public void setUrlFocusChangeFraction(float fraction) { 102 super.setUrlFocusChangeFraction(fraction); 103 104 if (fraction > 0f) { 105 mUrlActionContainer.setVisibility(VISIBLE); 106 } else if (fraction == 0f && !isUrlFocusChangeInProgress()) { 107 // If a URL focus change is in progress, then it will handle setting the visibility 108 // correctly after it completes. If done here, it would cause the URL to jump due 109 // to a badly timed layout call. 110 mUrlActionContainer.setVisibility(GONE); 111 } 112 113 updateButtonVisibility(); 114 mStatusCoordinator.setUrlFocusChangePercent(fraction); 115 } 116 117 @Override onUrlFocusChange(boolean hasFocus)118 public void onUrlFocusChange(boolean hasFocus) { 119 if (hasFocus) { 120 // Remove the focus of this view once the URL field has taken focus as this view no 121 // longer needs it. 122 setFocusable(false); 123 setFocusableInTouchMode(false); 124 } 125 setUrlFocusChangeInProgress(true); 126 updateShouldAnimateIconChanges(); 127 super.onUrlFocusChange(hasFocus); 128 } 129 130 @Override drawChild(Canvas canvas, View child, long drawingTime)131 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 132 boolean needsCanvasRestore = false; 133 if (child == mUrlBar && mUrlActionContainer.getVisibility() == VISIBLE) { 134 canvas.save(); 135 136 // Clip the URL bar contents to ensure they do not draw under the URL actions during 137 // focus animations. Based on the RTL state of the location bar, the url actions 138 // container can be on the left or right side, so clip accordingly. 139 if (mUrlBar.getLeft() < mUrlActionContainer.getLeft()) { 140 canvas.clipRect(0, 0, (int) mUrlActionContainer.getX(), getBottom()); 141 } else { 142 canvas.clipRect(mUrlActionContainer.getX() + mUrlActionContainer.getWidth(), 0, 143 getWidth(), getBottom()); 144 } 145 needsCanvasRestore = true; 146 } 147 boolean retVal = super.drawChild(canvas, child, drawingTime); 148 if (needsCanvasRestore) { 149 canvas.restore(); 150 } 151 return retVal; 152 } 153 154 @Override finishUrlFocusChange(boolean hasFocus, boolean shouldShowKeyboard)155 public void finishUrlFocusChange(boolean hasFocus, boolean shouldShowKeyboard) { 156 super.finishUrlFocusChange(hasFocus, shouldShowKeyboard); 157 if (!hasFocus) { 158 mUrlActionContainer.setVisibility(GONE); 159 } 160 mStatusCoordinator.onUrlAnimationFinished(hasFocus); 161 } 162 163 @Override updateButtonVisibility()164 protected void updateButtonVisibility() { 165 super.updateButtonVisibility(); 166 updateMicButtonVisibility(); 167 } 168 169 @Override updateShouldAnimateIconChanges()170 public void updateShouldAnimateIconChanges() { 171 notifyShouldAnimateIconChanges(isUrlBarFocused() || isUrlFocusChangeInProgress()); 172 } 173 174 @Override setShowIconsWhenUrlFocused(boolean showIcon)175 public void setShowIconsWhenUrlFocused(boolean showIcon) { 176 super.setShowIconsWhenUrlFocused(showIcon); 177 mFirstVisibleFocusedView = showIcon ? mStatusView : mUrlBar; 178 mStatusCoordinator.setShowIconsWhenUrlFocused(showIcon); 179 } 180 181 @Override updateVisualsForState()182 public void updateVisualsForState() { 183 super.updateVisualsForState(); 184 boolean isIncognito = mLocationBarDataProvider.isIncognito(); 185 setShowIconsWhenUrlFocused(SearchEngineLogoUtils.shouldShowSearchEngineLogo(isIncognito)); 186 updateStatusVisibility(); 187 } 188 189 @Override onTabLoadingNTP(NewTabPage ntp)190 public void onTabLoadingNTP(NewTabPage ntp) { 191 super.onTabLoadingNTP(ntp); 192 updateStatusVisibility(); 193 } 194 195 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)196 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 197 try (TraceEvent e = TraceEvent.scoped("LocationBarPhone.onMeasure")) { 198 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 199 } 200 } 201 202 @Override onLayout(boolean changed, int left, int top, int right, int bottom)203 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 204 try (TraceEvent e = TraceEvent.scoped("LocationBarPhone.onLayout")) { 205 super.onLayout(changed, left, top, right, bottom); 206 } 207 } 208 209 /** 210 * @return Width of child views before the first view that would be visible when location bar is 211 * focused. The first visible, focused view should be either url bar or status icon. 212 */ getOffsetOfFirstVisibleFocusedView()213 public int getOffsetOfFirstVisibleFocusedView() { 214 int visibleWidth = 0; 215 for (int i = 0; i < getChildCount(); i++) { 216 View child = getChildAt(i); 217 if (child == mFirstVisibleFocusedView) break; 218 if (child.getVisibility() == GONE) continue; 219 visibleWidth += child.getMeasuredWidth(); 220 } 221 return visibleWidth; 222 } 223 224 /** 225 * Populates fade animators of status icon for location bar focus change animation. 226 * @param animators The target list to add animators to. 227 * @param startDelayMs Start delay of fade animation in milliseconds. 228 * @param durationMs Duration of fade animation in milliseconds. 229 * @param targetAlpha Target alpha value. 230 */ populateFadeAnimations( List<Animator> animators, long startDelayMs, long durationMs, float targetAlpha)231 public void populateFadeAnimations( 232 List<Animator> animators, long startDelayMs, long durationMs, float targetAlpha) { 233 for (int i = 0; i < getChildCount(); i++) { 234 View child = getChildAt(i); 235 if (child == mFirstVisibleFocusedView) break; 236 Animator animator = ObjectAnimator.ofFloat(child, ALPHA, targetAlpha); 237 animator.setStartDelay(startDelayMs); 238 animator.setDuration(durationMs); 239 animator.setInterpolator(BakedBezierInterpolator.TRANSFORM_CURVE); 240 animators.add(animator); 241 } 242 } 243 244 /** 245 * Returns {@link FrameLayout.LayoutParams} of the LocationBar view. 246 * 247 * <p>TODO(1133482): Hide this View interaction if possible. 248 * 249 * @see View#getLayoutParams() 250 */ getFrameLayoutParams()251 public FrameLayout.LayoutParams getFrameLayoutParams() { 252 return (FrameLayout.LayoutParams) getLayoutParams(); 253 } 254 255 /** 256 * Calculates the offset required for the focused LocationBar to appear as it's still unfocused 257 * so it can animate to a focused state. 258 * 259 * @param hasFocus True if the LocationBar has focus, this will be true between the focus 260 * animation starting and the unfocus animation starting. 261 * @return The offset for the location bar when showing the dse icon. 262 */ getLocationBarOffsetForFocusAnimation(boolean hasFocus)263 public int getLocationBarOffsetForFocusAnimation(boolean hasFocus) { 264 if (mStatusCoordinator == null) return 0; 265 266 // No offset is required if the experiment is disabled. 267 if (!SearchEngineLogoUtils.shouldShowSearchEngineLogo( 268 mLocationBarDataProvider.isIncognito())) { 269 return 0; 270 } 271 272 // On non-NTP pages, there will always be an icon when unfocused. 273 if (!mLocationBarDataProvider.getNewTabPageDelegate().isCurrentlyVisible()) return 0; 274 275 // This offset is only required when the focus animation is running. 276 if (!hasFocus) return 0; 277 278 // We're on the NTP with the fakebox showing. 279 // The value returned changes based on if the layout is LTR OR RTL. 280 // For LTR, the value is negative because we are making space on the left-hand side. 281 // For RTL, the value is positive because we are pushing the icon further to the 282 // right-hand side. 283 int offset = mStatusCoordinator.getStatusIconWidth() - getAdditionalOffsetForNTP(); 284 return getLayoutDirection() == LAYOUT_DIRECTION_RTL ? offset : -offset; 285 } 286 287 /** 288 * Function used to position the url bar inside the location bar during omnibox animation. 289 * 290 * @param urlExpansionPercent The current expansion percent, 1 is fully focused and 0 is 291 * completely unfocused. 292 * @param hasFocus True if the LocationBar has focus, this will be true between the focus 293 * animation starting and the unfocus animation starting. 294 * @return The X translation for the URL bar, used in the toolbar animation. 295 */ getUrlBarTranslationXForToolbarAnimation( float urlExpansionPercent, boolean hasFocus)296 public float getUrlBarTranslationXForToolbarAnimation( 297 float urlExpansionPercent, boolean hasFocus) { 298 // This will be called before status view is ready. 299 if (mStatusCoordinator == null) return 0; 300 301 // No offset is required if the experiment is disabled. 302 if (!SearchEngineLogoUtils.shouldShowSearchEngineLogo( 303 mLocationBarDataProvider.isIncognito())) { 304 return 0; 305 } 306 307 boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; 308 // The calculation here is: the difference in padding between the focused vs unfocused 309 // states and also accounts for the translation that the status icon will do. In the end, 310 // this translation will be the distance that the url bar needs to travel to arrive at the 311 // desired padding when focused. 312 float translation = 313 urlExpansionPercent * mStatusCoordinator.getEndPaddingPixelSizeOnFocusDelta(); 314 315 if (!hasFocus && mStatusCoordinator.isSearchEngineStatusIconVisible() 316 && SearchEngineLogoUtils.currentlyOnNTP(mLocationBarDataProvider)) { 317 // When: 318 // 1. unfocusing the LocationBar on the NTP. 319 // 2. scrolling the fakebox to the LocationBar on the NTP. 320 // The status icon and the URL bar text overlap in the animation. 321 // 322 // This branch calculates the negative distance the URL bar needs to travel to 323 // completely overlap the status icon and end up in a state that matches the fakebox. 324 float overStatusIconTranslation = translation 325 - (1f - urlExpansionPercent) 326 * (mStatusCoordinator.getStatusIconWidth() 327 - getAdditionalOffsetForNTP()); 328 // The value returned changes based on if the layout is LTR or RTL. 329 // For LTR, the value is negative because the status icon is left of the url bar on the 330 // x/y plane. 331 // For RTL, the value is positive because the status icon is right of the url bar on the 332 // x/y plane. 333 return isRtl ? -overStatusIconTranslation : overStatusIconTranslation; 334 } 335 336 return isRtl ? -translation : translation; 337 } 338 getAdditionalOffsetForNTP()339 private int getAdditionalOffsetForNTP() { 340 return getResources().getDimensionPixelSize(R.dimen.sei_search_box_lateral_padding) 341 - getResources().getDimensionPixelSize(R.dimen.sei_location_bar_lateral_padding); 342 } 343 344 /** Update the status visibility according to the current state held in LocationBar. */ updateStatusVisibility()345 private void updateStatusVisibility() { 346 boolean incognito = mLocationBarDataProvider.isIncognito(); 347 if (!SearchEngineLogoUtils.shouldShowSearchEngineLogo(incognito)) { 348 return; 349 } 350 351 if (SearchEngineLogoUtils.currentlyOnNTP(mLocationBarDataProvider)) { 352 mStatusCoordinator.setStatusIconShown(hasFocus()); 353 } else { 354 mStatusCoordinator.setStatusIconShown(true); 355 } 356 } 357 } 358