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.compositor.layouts;
6 
7 import android.content.Context;
8 import android.content.res.Resources;
9 import android.graphics.RectF;
10 
11 import org.chromium.base.MathUtils;
12 import org.chromium.base.metrics.RecordUserAction;
13 import org.chromium.chrome.R;
14 import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
15 import org.chromium.chrome.browser.compositor.LayerTitleCache;
16 import org.chromium.chrome.browser.compositor.layouts.components.LayoutTab;
17 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
18 import org.chromium.chrome.browser.compositor.layouts.eventfilter.BlackHoleEventFilter;
19 import org.chromium.chrome.browser.compositor.layouts.eventfilter.ScrollDirection;
20 import org.chromium.chrome.browser.compositor.scene_layer.TabListSceneLayer;
21 import org.chromium.chrome.browser.layouts.EventFilter;
22 import org.chromium.chrome.browser.layouts.LayoutType;
23 import org.chromium.chrome.browser.layouts.animation.CompositorAnimator;
24 import org.chromium.chrome.browser.layouts.scene_layer.SceneLayer;
25 import org.chromium.chrome.browser.tab.Tab;
26 import org.chromium.chrome.browser.tabmodel.TabModel;
27 import org.chromium.chrome.browser.tabmodel.TabModelUtils;
28 import org.chromium.components.browser_ui.widget.animation.Interpolators;
29 import org.chromium.ui.base.LocalizationUtils;
30 import org.chromium.ui.resources.ResourceManager;
31 
32 import java.util.ArrayList;
33 import java.util.List;
34 
35 /**
36  * Layout defining the animation and positioning of the tabs during the edge swipe effect.
37  */
38 public class ToolbarSwipeLayout extends Layout {
39     private static final boolean ANONYMIZE_NON_FOCUSED_TAB = true;
40 
41     // Unit is millisecond / screen.
42     private static final float ANIMATION_SPEED_SCREEN_MS = 500.0f;
43 
44     // The time duration of the animation for switch to tab, Unit is millisecond.
45     private static final long SWITCH_TO_TAB_DURATION_MS = 350;
46 
47     // This is the time step used to move the offset based on fling
48     private static final float FLING_TIME_STEP = 1.0f / 30.0f;
49 
50     // This is the max contribution from fling in screen size percentage.
51     private static final float FLING_MAX_CONTRIBUTION = 0.5f;
52 
53     private LayoutTab mLeftTab;
54     private LayoutTab mRightTab;
55     private LayoutTab mFromTab; // Set to either mLeftTab or mRightTab.
56     private LayoutTab mToTab; // Set to mLeftTab or mRightTab or null if it is not determined.
57 
58     // Whether or not to show the toolbar.
59     private boolean mMoveToolbar;
60 
61     // Offsets are in pixels [0, width].
62     private float mOffsetStart;
63     private float mOffset;
64     private float mOffsetTarget;
65 
66     // These will be set from dimens.xml
67     private final float mSpaceBetweenTabs;
68     private final float mCommitDistanceFromEdge;
69 
70     private final BlackHoleEventFilter mBlackHoleEventFilter;
71     private final TabListSceneLayer mSceneLayer;
72 
73     /**
74      * @param context             The current Android's context.
75      * @param updateHost          The {@link LayoutUpdateHost} view for this layout.
76      * @param renderHost          The {@link LayoutRenderHost} view for this layout.
77      */
ToolbarSwipeLayout( Context context, LayoutUpdateHost updateHost, LayoutRenderHost renderHost)78     public ToolbarSwipeLayout(
79             Context context, LayoutUpdateHost updateHost, LayoutRenderHost renderHost) {
80         super(context, updateHost, renderHost);
81         mBlackHoleEventFilter = new BlackHoleEventFilter(context);
82         Resources res = context.getResources();
83         final float pxToDp = 1.0f / res.getDisplayMetrics().density;
84         mCommitDistanceFromEdge = res.getDimension(R.dimen.toolbar_swipe_commit_distance) * pxToDp;
85         mSpaceBetweenTabs = res.getDimension(R.dimen.toolbar_swipe_space_between_tabs) * pxToDp;
86         mSceneLayer = new TabListSceneLayer();
87     }
88 
89     /**
90      * @param moveToolbar Whether or not swiping this layout should also move the toolbar as well as
91      *                    the content.
92      */
setMovesToolbar(boolean moveToolbar)93     public void setMovesToolbar(boolean moveToolbar) {
94         mMoveToolbar = moveToolbar;
95     }
96 
97     @Override
getViewportMode()98     public @ViewportMode int getViewportMode() {
99         // This seems counter-intuitive, but if the toolbar moves the android view is not showing.
100         // That means the compositor has to draw it and therefore needs the fullscreen viewport.
101         // Likewise, when the android view is showing, the compositor controls do not draw and the
102         // content needs to pretend it does to draw correctly.
103         // TODO(mdjones): Remove toolbar_impact_height from tab_layer.cc so this makes more sense.
104         return mMoveToolbar ? ViewportMode.ALWAYS_FULLSCREEN
105                             : ViewportMode.ALWAYS_SHOWING_BROWSER_CONTROLS;
106     }
107 
108     @Override
forceHideBrowserControlsAndroidView()109     public boolean forceHideBrowserControlsAndroidView() {
110         // If the toolbar moves, the android browser controls need to be hidden.
111         return super.forceHideBrowserControlsAndroidView() || mMoveToolbar;
112     }
113 
114     @Override
show(long time, boolean animate)115     public void show(long time, boolean animate) {
116         super.show(time, animate);
117         init();
118         if (mTabModelSelector == null) return;
119         Tab tab = mTabModelSelector.getCurrentTab();
120         if (tab != null && tab.isNativePage()) mTabContentManager.cacheTabThumbnail(tab);
121 
122         TabModel model = mTabModelSelector.getCurrentModel();
123         if (model == null) return;
124         int fromTabId = mTabModelSelector.getCurrentTabId();
125         if (fromTabId == TabModel.INVALID_TAB_INDEX) return;
126         mFromTab = createLayoutTab(fromTabId, model.isIncognito(), NO_CLOSE_BUTTON, NEED_TITLE);
127         prepareLayoutTabForSwipe(mFromTab, false);
128     }
129 
swipeStarted(long time, @ScrollDirection int direction, float x, float y)130     public void swipeStarted(long time, @ScrollDirection int direction, float x, float y) {
131         if (mTabModelSelector == null || mToTab != null || direction == ScrollDirection.DOWN) {
132             return;
133         }
134 
135         boolean dragFromLeftEdge = direction == ScrollDirection.RIGHT;
136         // Finish off any other animations.
137         forceAnimationToFinish();
138 
139         // Determine which tabs we're showing.
140         TabModel model = mTabModelSelector.getCurrentModel();
141         if (model == null) return;
142         int fromIndex = model.index();
143         if (fromIndex == TabModel.INVALID_TAB_INDEX) return;
144 
145         // On RTL, edge-dragging to the left is the next tab.
146         int toIndex = (LocalizationUtils.isLayoutRtl() ^ dragFromLeftEdge) ? fromIndex - 1
147                                                                            : fromIndex + 1;
148 
149         prepareSwipeTabAnimation(direction, fromIndex, toIndex);
150     }
151 
152     /**
153      * Prepare the tabs sliding animations. This method need to be called before
154      * {@link #doTabSwitchAnimation(int, float, float, long)}.
155      * @param direction The direction of the slide.
156      * @param fromIndex The index of the tab which will be switched from.
157      * @param toIndex The index of the tab which will be switched to.
158      */
prepareSwipeTabAnimation( @crollDirection int direction, int fromIndex, int toIndex)159     private void prepareSwipeTabAnimation(
160             @ScrollDirection int direction, int fromIndex, int toIndex) {
161         boolean dragFromLeftEdge = direction == ScrollDirection.RIGHT;
162 
163         int leftIndex = dragFromLeftEdge ? toIndex : fromIndex;
164         int rightIndex = !dragFromLeftEdge ? toIndex : fromIndex;
165         int leftTabId = Tab.INVALID_TAB_ID;
166         int rightTabId = Tab.INVALID_TAB_ID;
167 
168         TabModel model = mTabModelSelector.getCurrentModel();
169         if (0 <= leftIndex && leftIndex < model.getCount()) {
170             leftTabId = model.getTabAt(leftIndex).getId();
171             mLeftTab = createLayoutTab(leftTabId, model.isIncognito(), NO_CLOSE_BUTTON, NEED_TITLE);
172             prepareLayoutTabForSwipe(mLeftTab, leftIndex != fromIndex);
173         }
174         if (0 <= rightIndex && rightIndex < model.getCount()) {
175             rightTabId = model.getTabAt(rightIndex).getId();
176             mRightTab =
177                     createLayoutTab(rightTabId, model.isIncognito(), NO_CLOSE_BUTTON, NEED_TITLE);
178             prepareLayoutTabForSwipe(mRightTab, rightIndex != fromIndex);
179         }
180         // Prioritize toTabId because fromTabId likely has a live layer.
181         int fromTabId = dragFromLeftEdge ? rightTabId : leftTabId;
182         int toTabId = !dragFromLeftEdge ? rightTabId : leftTabId;
183         List<Integer> visibleTabs = new ArrayList<Integer>();
184         if (toTabId != Tab.INVALID_TAB_ID) visibleTabs.add(toTabId);
185         if (fromTabId != Tab.INVALID_TAB_ID) visibleTabs.add(fromTabId);
186         updateCacheVisibleIds(visibleTabs);
187 
188         mToTab = null;
189 
190         // Reset the tab offsets.
191         mOffsetStart = dragFromLeftEdge ? 0 : getWidth();
192         mOffset = 0;
193         mOffsetTarget = 0;
194 
195         if (mLeftTab != null && mRightTab != null) {
196             mLayoutTabs = new LayoutTab[] {mLeftTab, mRightTab};
197         } else if (mLeftTab != null) {
198             mLayoutTabs = new LayoutTab[] {mLeftTab};
199         } else if (mRightTab != null) {
200             mLayoutTabs = new LayoutTab[] {mRightTab};
201         } else {
202             mLayoutTabs = null;
203         }
204 
205         requestUpdate();
206     }
207 
prepareLayoutTabForSwipe(LayoutTab layoutTab, boolean anonymizeToolbar)208     private void prepareLayoutTabForSwipe(LayoutTab layoutTab, boolean anonymizeToolbar) {
209         assert layoutTab != null;
210         if (layoutTab.shouldStall()) layoutTab.setSaturation(0.0f);
211         float heightDp = layoutTab.getOriginalContentHeight();
212         layoutTab.setClipSize(layoutTab.getOriginalContentWidth(), heightDp);
213         layoutTab.setScale(1.f);
214         layoutTab.setBorderScale(1.f);
215         layoutTab.setDecorationAlpha(0.f);
216         layoutTab.setY(0.f);
217         layoutTab.setShowToolbar(mMoveToolbar);
218         layoutTab.setAnonymizeToolbar(anonymizeToolbar && ANONYMIZE_NON_FOCUSED_TAB);
219     }
220 
swipeUpdated(long time, float x, float y, float dx, float dy, float tx, float ty)221     public void swipeUpdated(long time, float x, float y, float dx, float dy, float tx, float ty) {
222         mOffsetTarget = MathUtils.clamp(mOffsetStart + tx, 0, getWidth()) - mOffsetStart;
223         requestUpdate();
224     }
225 
swipeFlingOccurred( long time, float x, float y, float tx, float ty, float vx, float vy)226     public void swipeFlingOccurred(
227             long time, float x, float y, float tx, float ty, float vx, float vy) {
228         // Use the velocity to add on final step which simulate a fling.
229         final float kickRangeX = getWidth() * FLING_MAX_CONTRIBUTION;
230         final float kickRangeY = getHeight() * FLING_MAX_CONTRIBUTION;
231         final float kickX = MathUtils.clamp(vx * FLING_TIME_STEP, -kickRangeX, kickRangeX);
232         final float kickY = MathUtils.clamp(vy * FLING_TIME_STEP, -kickRangeY, kickRangeY);
233         swipeUpdated(time, x, y, 0, 0, tx + kickX, ty + kickY);
234     }
235 
swipeFinished(long time)236     public void swipeFinished(long time) {
237         if (mFromTab == null || mTabModelSelector == null) return;
238 
239         // Figures out the tab to snap to and how to animate to it.
240         float commitDistance = Math.min(mCommitDistanceFromEdge, getWidth() / 3);
241         float offsetTo = 0;
242         mToTab = mFromTab;
243         if (mOffsetTarget > commitDistance && mLeftTab != null) {
244             mToTab = mLeftTab;
245             offsetTo += getWidth();
246         } else if (mOffsetTarget < -commitDistance && mRightTab != null) {
247             mToTab = mRightTab;
248             offsetTo -= getWidth();
249         }
250 
251         if (mToTab != mFromTab) {
252             RecordUserAction.record("MobileSideSwipeFinished");
253         }
254 
255         startHiding(mToTab.getId(), false);
256 
257         float start = mOffsetTarget;
258         float end = offsetTo;
259         long duration = (long) (ANIMATION_SPEED_SCREEN_MS * Math.abs(start - end) / getWidth());
260         doTabSwitchAnimation(mToTab.getId(), start, end, duration);
261     }
262 
263     /**
264      * Perform the tabs sliding animations. {@link #prepareSwipeTabAnimation(int, int, int)} need to
265      * be called before calling this method.
266      * @param tabId The id of the tab which will be switched to.
267      * @param start The start point of X coordinate for the animation.
268      * @param end The end point of X coordinate for the animation.
269      * @param duration The animation duration in millisecond.
270      */
doTabSwitchAnimation(int tabId, float start, float end, long duration)271     private void doTabSwitchAnimation(int tabId, float start, float end, long duration) {
272         // Animate gracefully the end of the swiping effect.
273         forceAnimationToFinish();
274 
275         if (duration <= 0) return;
276 
277         CompositorAnimator offsetAnimation =
278                 CompositorAnimator.ofFloat(getAnimationHandler(), start, end, duration, null);
279         offsetAnimation.addUpdateListener(animator -> {
280             mOffset = animator.getAnimatedValue();
281             mOffsetTarget = mOffset;
282         });
283         offsetAnimation.start();
284     }
285 
swipeCancelled(long time)286     public void swipeCancelled(long time) {
287         swipeFinished(time);
288     }
289 
290     @Override
updateLayout(long time, long dt)291     protected void updateLayout(long time, long dt) {
292         super.updateLayout(time, dt);
293 
294         if (mFromTab == null) return;
295         // In case the draw function get called before swipeStarted()
296         if (mLeftTab == null && mRightTab == null) mRightTab = mFromTab;
297 
298         mOffset = smoothInput(mOffset, mOffsetTarget);
299         boolean needUpdate = Math.abs(mOffset - mOffsetTarget) >= 0.1f;
300 
301         float rightX = 0.0f;
302         float leftX = 0.0f;
303 
304         final boolean doEdge = mLeftTab != null ^ mRightTab != null;
305 
306         if (doEdge) {
307             float progress = mOffset / getWidth();
308             float direction = Math.signum(progress);
309             float smoothedProgress =
310                     Interpolators.DECELERATE_INTERPOLATOR.getInterpolation(Math.abs(progress));
311 
312             float maxSlide = getWidth() / 5.f;
313             rightX = direction * smoothedProgress * maxSlide;
314             leftX = rightX;
315         } else {
316             float progress = mOffset / getWidth();
317             progress += mOffsetStart == 0.0f ? 0.0f : 1.0f;
318             progress = MathUtils.clamp(progress, 0.0f, 1.0f);
319 
320             assert mLeftTab != null;
321             assert mRightTab != null;
322             rightX = MathUtils.interpolate(0.0f, getWidth() + mSpaceBetweenTabs, progress);
323             // The left tab must be aligned on the right if the image is smaller than the screen.
324             leftX = rightX - mSpaceBetweenTabs
325                     - Math.min(getWidth(), mLeftTab.getOriginalContentWidth());
326             // Compute final x post scale and ensure the tab's center point never passes the
327             // center point of the screen.
328             float screenCenterX = getWidth() / 2;
329             rightX = Math.max(screenCenterX - mRightTab.getFinalContentWidth() / 2, rightX);
330             leftX = Math.min(screenCenterX - mLeftTab.getFinalContentWidth() / 2, leftX);
331         }
332 
333         if (mLeftTab != null) {
334             mLeftTab.setX(leftX);
335             needUpdate = updateSnap(dt, mLeftTab) || needUpdate;
336         }
337 
338         if (mRightTab != null) {
339             mRightTab.setX(rightX);
340             needUpdate = updateSnap(dt, mRightTab) || needUpdate;
341         }
342 
343         if (needUpdate) requestUpdate();
344     }
345 
346     /**
347      * Smoothes input signal. The definition of the input is lower than the
348      * pixel density of the screen so we need to smooth the input to give the illusion of smooth
349      * animation on screen from chunky inputs.
350      * The combination of 30 pixels and 0.8f ensures that the output is not more than 6 pixels away
351      * from the target.
352      * TODO(dtrainor): This has nothing to do with time, just draw rate.
353      *       Is this okay or do we want to have the interpolation based on the time elapsed?
354      * @param current The current value of the signal.
355      * @param input The raw input value.
356      * @return The smoothed signal.
357      */
smoothInput(float current, float input)358     private float smoothInput(float current, float input) {
359         current = MathUtils.clamp(current, input - 30, input + 30);
360         return MathUtils.interpolate(current, input, 0.8f);
361     }
362 
init()363     private void init() {
364         mLayoutTabs = null;
365         mFromTab = null;
366         mLeftTab = null;
367         mRightTab = null;
368         mToTab = null;
369         mOffsetStart = 0;
370         mOffset = 0;
371         mOffsetTarget = 0;
372     }
373 
374     @Override
getEventFilter()375     protected EventFilter getEventFilter() {
376         return mBlackHoleEventFilter;
377     }
378 
379     @Override
getSceneLayer()380     protected SceneLayer getSceneLayer() {
381         return mSceneLayer;
382     }
383 
384     @Override
updateSceneLayer(RectF viewport, RectF contentViewport, LayerTitleCache layerTitleCache, TabContentManager tabContentManager, ResourceManager resourceManager, BrowserControlsStateProvider browserControls)385     protected void updateSceneLayer(RectF viewport, RectF contentViewport,
386             LayerTitleCache layerTitleCache, TabContentManager tabContentManager,
387             ResourceManager resourceManager, BrowserControlsStateProvider browserControls) {
388         super.updateSceneLayer(viewport, contentViewport, layerTitleCache, tabContentManager,
389                 resourceManager, browserControls);
390         assert mSceneLayer != null;
391         // contentViewport is intentionally passed for both parameters below.
392         mSceneLayer.pushLayers(getContext(), contentViewport, contentViewport, this,
393                 layerTitleCache, tabContentManager, resourceManager, browserControls,
394                 SceneLayer.INVALID_RESOURCE_ID, 0, 0);
395     }
396 
397     @Override
getLayoutType()398     public int getLayoutType() {
399         return LayoutType.TOOLBAR_SWIPE;
400     }
401 
402     /**
403      * Perform the tabs sliding animations. If the new tab's index is smaller than the old one, new
404      * tab slide in from left, and old one slide out to right, and vice versa.
405      * @param toTabId The id of the next tab which will be switched to.
406      * @param fromTabId The id of the previous tab which will be switched out.
407      */
switchToTab(int toTabId, int fromTabId)408     public void switchToTab(int toTabId, int fromTabId) {
409         int fromTabIndex =
410                 TabModelUtils.getTabIndexById(mTabModelSelector.getCurrentModel(), fromTabId);
411         int toTabIndex =
412                 TabModelUtils.getTabIndexById(mTabModelSelector.getCurrentModel(), toTabId);
413         prepareSwipeTabAnimation(
414                 fromTabIndex < toTabIndex ? ScrollDirection.LEFT : ScrollDirection.RIGHT,
415                 fromTabIndex, toTabIndex);
416 
417         mToTab = fromTabIndex < toTabIndex ? mRightTab : mLeftTab;
418         float end = fromTabIndex < toTabIndex ? -getWidth() : getWidth();
419         startHiding(toTabId, false);
420         doTabSwitchAnimation(toTabId, 0f, end, SWITCH_TO_TAB_DURATION_MS);
421     }
422 }
423