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