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