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