OLD | NEW |
(Empty) | |
| 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.Rect; |
| 10 import android.view.animation.DecelerateInterpolator; |
| 11 import android.view.animation.Interpolator; |
| 12 |
| 13 import com.google.android.apps.chrome.R; |
| 14 |
| 15 import org.chromium.base.metrics.RecordUserAction; |
| 16 import org.chromium.chrome.browser.Tab; |
| 17 import org.chromium.chrome.browser.compositor.LayerTitleCache; |
| 18 import org.chromium.chrome.browser.compositor.layouts.ChromeAnimation.Animatable
; |
| 19 import org.chromium.chrome.browser.compositor.layouts.components.LayoutTab; |
| 20 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager; |
| 21 import org.chromium.chrome.browser.compositor.layouts.eventfilter.EdgeSwipeEvent
Filter.ScrollDirection; |
| 22 import org.chromium.chrome.browser.compositor.layouts.eventfilter.EventFilter; |
| 23 import org.chromium.chrome.browser.compositor.scene_layer.SceneLayer; |
| 24 import org.chromium.chrome.browser.compositor.scene_layer.TabListSceneLayer; |
| 25 import org.chromium.chrome.browser.fullscreen.ChromeFullscreenManager; |
| 26 import org.chromium.chrome.browser.tabmodel.TabModel; |
| 27 import org.chromium.chrome.browser.util.MathUtils; |
| 28 import org.chromium.ui.base.LocalizationUtils; |
| 29 import org.chromium.ui.resources.ResourceManager; |
| 30 |
| 31 import java.util.ArrayList; |
| 32 import java.util.List; |
| 33 |
| 34 /** |
| 35 * Layout defining the animation and positioning of the tabs during the edge swi
pe effect. |
| 36 */ |
| 37 public class ToolbarSwipeLayout extends Layout implements Animatable<ToolbarSwip
eLayout.Property> { |
| 38 /** |
| 39 * Animation properties |
| 40 */ |
| 41 public enum Property { |
| 42 OFFSET, |
| 43 } |
| 44 |
| 45 private static final boolean ANONYMIZE_NON_FOCUSED_TAB = true; |
| 46 |
| 47 // Unit is millisecond / screen. |
| 48 private static final float ANIMATION_SPEED_SCREEN = 500.0f; |
| 49 |
| 50 // This is the time step used to move the offset based on fling |
| 51 private static final float FLING_TIME_STEP = 1.0f / 30.0f; |
| 52 |
| 53 // This is the max contribution from fling in screen size percentage. |
| 54 private static final float FLING_MAX_CONTRIBUTION = 0.5f; |
| 55 |
| 56 private LayoutTab mLeftTab; |
| 57 private LayoutTab mRightTab; |
| 58 private LayoutTab mFromTab; // Set to either mLeftTab or mRightTab. |
| 59 private LayoutTab mToTab; // Set to mLeftTab or mRightTab or null if it is n
ot determined. |
| 60 |
| 61 // Whether or not to show the toolbar. |
| 62 private boolean mMoveToolbar; |
| 63 |
| 64 // Offsets are in pixels [0, width]. |
| 65 private float mOffsetStart; |
| 66 private float mOffset; |
| 67 private float mOffsetTarget; |
| 68 |
| 69 // These will be set from dimens.xml |
| 70 private final float mSpaceBetweenTabs; |
| 71 private final float mCommitDistanceFromEdge; |
| 72 |
| 73 private final TabListSceneLayer mSceneLayer; |
| 74 |
| 75 private final Interpolator mEdgeInterpolator = new DecelerateInterpolator(); |
| 76 |
| 77 /** |
| 78 * @param context The current Android's context. |
| 79 * @param updateHost The {@link LayoutUpdateHost} view for this lay
out. |
| 80 * @param renderHost The {@link LayoutRenderHost} view for this lay
out. |
| 81 * @param eventFilter The {@link EventFilter} that is needed for thi
s view. |
| 82 */ |
| 83 public ToolbarSwipeLayout(Context context, LayoutUpdateHost updateHost, |
| 84 LayoutRenderHost renderHost, EventFilter eventFilter) { |
| 85 super(context, updateHost, renderHost, eventFilter); |
| 86 Resources res = context.getResources(); |
| 87 final float pxToDp = 1.0f / res.getDisplayMetrics().density; |
| 88 mCommitDistanceFromEdge = res.getDimension(R.dimen.toolbar_swipe_commit_
distance) * pxToDp; |
| 89 mSpaceBetweenTabs = res.getDimension(R.dimen.toolbar_swipe_space_between
_tabs) * pxToDp; |
| 90 mSceneLayer = new TabListSceneLayer(); |
| 91 } |
| 92 |
| 93 /** |
| 94 * @param moveToolbar Whether or not swiping this layout should also move th
e toolbar as well as |
| 95 * the content. |
| 96 */ |
| 97 public void setMovesToolbar(boolean moveToolbar) { |
| 98 mMoveToolbar = moveToolbar; |
| 99 } |
| 100 |
| 101 @Override |
| 102 public int getSizingFlags() { |
| 103 return mMoveToolbar ? SizingFlags.HELPER_HIDE_TOOLBAR_IMMEDIATE |
| 104 : SizingFlags.HELPER_NO_FULLSCREEN_SUPPORT; |
| 105 } |
| 106 |
| 107 @Override |
| 108 public void show(long time, boolean animate) { |
| 109 super.show(time, animate); |
| 110 init(); |
| 111 if (mTabModelSelector == null) return; |
| 112 Tab tab = mTabModelSelector.getCurrentTab(); |
| 113 if (tab != null && tab.isNativePage()) mTabContentManager.cacheTabThumbn
ail(tab); |
| 114 |
| 115 TabModel model = mTabModelSelector.getCurrentModel(); |
| 116 if (model == null) return; |
| 117 int fromTabId = mTabModelSelector.getCurrentTabId(); |
| 118 if (fromTabId == TabModel.INVALID_TAB_INDEX) return; |
| 119 mFromTab = createLayoutTab(fromTabId, model.isIncognito(), NO_CLOSE_BUTT
ON, NEED_TITLE); |
| 120 prepareLayoutTabForSwipe(mFromTab, false); |
| 121 } |
| 122 |
| 123 @Override |
| 124 public void swipeStarted(long time, ScrollDirection direction, float x, floa
t y) { |
| 125 if (mTabModelSelector == null || mToTab != null || direction == ScrollDi
rection.DOWN) { |
| 126 return; |
| 127 } |
| 128 |
| 129 boolean dragFromLeftEdge = direction == ScrollDirection.RIGHT; |
| 130 // Finish off any other animations. |
| 131 forceAnimationToFinish(); |
| 132 |
| 133 // Determine which tabs we're showing. |
| 134 TabModel model = mTabModelSelector.getCurrentModel(); |
| 135 if (model == null) return; |
| 136 int fromIndex = model.index(); |
| 137 if (fromIndex == TabModel.INVALID_TAB_INDEX) return; |
| 138 |
| 139 // On RTL, edge-dragging to the left is the next tab. |
| 140 int toIndex = (LocalizationUtils.isLayoutRtl() ^ dragFromLeftEdge) ? fro
mIndex - 1 |
| 141 : fro
mIndex + 1; |
| 142 int leftIndex = dragFromLeftEdge ? toIndex : fromIndex; |
| 143 int rightIndex = !dragFromLeftEdge ? toIndex : fromIndex; |
| 144 |
| 145 List<Integer> visibleTabs = new ArrayList<Integer>(); |
| 146 if (0 <= leftIndex && leftIndex < model.getCount()) { |
| 147 int leftTabId = model.getTabAt(leftIndex).getId(); |
| 148 mLeftTab = createLayoutTab(leftTabId, model.isIncognito(), NO_CLOSE_
BUTTON, NEED_TITLE); |
| 149 prepareLayoutTabForSwipe(mLeftTab, leftIndex != fromIndex); |
| 150 visibleTabs.add(leftTabId); |
| 151 } |
| 152 if (0 <= rightIndex && rightIndex < model.getCount()) { |
| 153 int rightTabId = model.getTabAt(rightIndex).getId(); |
| 154 mRightTab = |
| 155 createLayoutTab(rightTabId, model.isIncognito(), NO_CLOSE_BU
TTON, NEED_TITLE); |
| 156 prepareLayoutTabForSwipe(mRightTab, rightIndex != fromIndex); |
| 157 visibleTabs.add(rightTabId); |
| 158 } |
| 159 |
| 160 updateCacheVisibleIds(visibleTabs); |
| 161 |
| 162 mToTab = null; |
| 163 |
| 164 // Reset the tab offsets. |
| 165 mOffsetStart = dragFromLeftEdge ? 0 : getWidth(); |
| 166 mOffset = 0; |
| 167 mOffsetTarget = 0; |
| 168 |
| 169 if (mLeftTab != null && mRightTab != null) { |
| 170 mLayoutTabs = new LayoutTab[] {mLeftTab, mRightTab}; |
| 171 } else if (mLeftTab != null) { |
| 172 mLayoutTabs = new LayoutTab[] {mLeftTab}; |
| 173 } else if (mRightTab != null) { |
| 174 mLayoutTabs = new LayoutTab[] {mRightTab}; |
| 175 } else { |
| 176 mLayoutTabs = null; |
| 177 } |
| 178 |
| 179 requestUpdate(); |
| 180 } |
| 181 |
| 182 private void prepareLayoutTabForSwipe(LayoutTab layoutTab, boolean anonymize
Toolbar) { |
| 183 assert layoutTab != null; |
| 184 if (layoutTab.shouldStall()) layoutTab.setSaturation(0.0f); |
| 185 layoutTab.setScale(1.f); |
| 186 layoutTab.setBorderScale(1.f); |
| 187 layoutTab.setDecorationAlpha(0.f); |
| 188 layoutTab.setY(0.f); |
| 189 layoutTab.setShowToolbar(mMoveToolbar); |
| 190 layoutTab.setAnonymizeToolbar(anonymizeToolbar && ANONYMIZE_NON_FOCUSED_
TAB); |
| 191 } |
| 192 |
| 193 @Override |
| 194 public void swipeUpdated(long time, float x, float y, float dx, float dy, fl
oat tx, float ty) { |
| 195 mOffsetTarget = MathUtils.clamp(mOffsetStart + tx, 0, getWidth()) - mOff
setStart; |
| 196 requestUpdate(); |
| 197 } |
| 198 |
| 199 @Override |
| 200 public void swipeFlingOccurred( |
| 201 long time, float x, float y, float tx, float ty, float vx, float vy)
{ |
| 202 // Use the velocity to add on final step which simulate a fling. |
| 203 final float kickRangeX = getWidth() * FLING_MAX_CONTRIBUTION; |
| 204 final float kickRangeY = getHeight() * FLING_MAX_CONTRIBUTION; |
| 205 final float kickX = MathUtils.clamp(vx * FLING_TIME_STEP, -kickRangeX, k
ickRangeX); |
| 206 final float kickY = MathUtils.clamp(vy * FLING_TIME_STEP, -kickRangeY, k
ickRangeY); |
| 207 swipeUpdated(time, x, y, 0, 0, tx + kickX, ty + kickY); |
| 208 } |
| 209 |
| 210 @Override |
| 211 public void swipeFinished(long time) { |
| 212 if (mFromTab == null || mTabModelSelector == null) return; |
| 213 |
| 214 // Figures out the tab to snap to and how to animate to it. |
| 215 float commitDistance = Math.min(mCommitDistanceFromEdge, getWidth() / 3)
; |
| 216 float offsetTo = 0; |
| 217 mToTab = mFromTab; |
| 218 if (mOffsetTarget > commitDistance && mLeftTab != null) { |
| 219 mToTab = mLeftTab; |
| 220 offsetTo += getWidth(); |
| 221 } else if (mOffsetTarget < -commitDistance && mRightTab != null) { |
| 222 mToTab = mRightTab; |
| 223 offsetTo -= getWidth(); |
| 224 } |
| 225 |
| 226 if (mToTab != mFromTab) { |
| 227 RecordUserAction.record("MobileSideSwipeFinished"); |
| 228 } |
| 229 |
| 230 startHiding(mToTab.getId(), false); |
| 231 |
| 232 // Animate gracefully the end of the swiping effect. |
| 233 forceAnimationToFinish(); |
| 234 float start = mOffsetTarget; |
| 235 float end = offsetTo; |
| 236 long duration = (long) (ANIMATION_SPEED_SCREEN * Math.abs(start - end) /
getWidth()); |
| 237 if (duration > 0) { |
| 238 addToAnimation(this, Property.OFFSET, start, end, duration, 0); |
| 239 } |
| 240 |
| 241 requestRender(); |
| 242 } |
| 243 |
| 244 @Override |
| 245 public void swipeCancelled(long time) { |
| 246 swipeFinished(time); |
| 247 } |
| 248 |
| 249 @Override |
| 250 protected void updateLayout(long time, long dt) { |
| 251 super.updateLayout(time, dt); |
| 252 |
| 253 if (mFromTab == null) return; |
| 254 // In case the draw function get called before swipeStarted() |
| 255 if (mLeftTab == null && mRightTab == null) mRightTab = mFromTab; |
| 256 |
| 257 mOffset = smoothInput(mOffset, mOffsetTarget); |
| 258 boolean needUpdate = Math.abs(mOffset - mOffsetTarget) >= 0.1f; |
| 259 |
| 260 float rightX = 0.0f; |
| 261 float leftX = 0.0f; |
| 262 |
| 263 final boolean doEdge = mLeftTab != null ^ mRightTab != null; |
| 264 |
| 265 if (doEdge) { |
| 266 float progress = mOffset / getWidth(); |
| 267 float direction = Math.signum(progress); |
| 268 float smoothedProgress = mEdgeInterpolator.getInterpolation(Math.abs
(progress)); |
| 269 |
| 270 float maxSlide = getWidth() / 5.f; |
| 271 rightX = direction * smoothedProgress * maxSlide; |
| 272 leftX = rightX; |
| 273 } else { |
| 274 float progress = mOffset / getWidth(); |
| 275 progress += mOffsetStart == 0.0f ? 0.0f : 1.0f; |
| 276 progress = MathUtils.clamp(progress, 0.0f, 1.0f); |
| 277 |
| 278 assert mLeftTab != null; |
| 279 assert mRightTab != null; |
| 280 rightX = MathUtils.interpolate(0.0f, getWidth() + mSpaceBetweenTabs,
progress); |
| 281 // The left tab must be aligned on the right if the image is smaller
than the screen. |
| 282 leftX = rightX - mSpaceBetweenTabs |
| 283 - Math.min(getWidth(), mLeftTab.getOriginalContentWidth()); |
| 284 // Compute final x post scale and ensure the tab's center point neve
r passes the |
| 285 // center point of the screen. |
| 286 float screenCenterX = getWidth() / 2; |
| 287 rightX = Math.max(screenCenterX - mRightTab.getFinalContentWidth() /
2, rightX); |
| 288 leftX = Math.min(screenCenterX - mLeftTab.getFinalContentWidth() / 2
, leftX); |
| 289 } |
| 290 |
| 291 if (mLeftTab != null) { |
| 292 mLeftTab.setX(leftX); |
| 293 needUpdate = mLeftTab.updateSnap(dt) || needUpdate; |
| 294 } |
| 295 |
| 296 if (mRightTab != null) { |
| 297 mRightTab.setX(rightX); |
| 298 needUpdate = mRightTab.updateSnap(dt) || needUpdate; |
| 299 } |
| 300 if (needUpdate) requestUpdate(); |
| 301 } |
| 302 |
| 303 /** |
| 304 * Smoothes input signal. The definition of the input is lower than the |
| 305 * pixel density of the screen so we need to smooth the input to give the il
lusion of smooth |
| 306 * animation on screen from chunky inputs. |
| 307 * The combination of 30 pixels and 0.8f ensures that the output is not more
than 6 pixels away |
| 308 * from the target. |
| 309 * TODO(dtrainor): This has nothing to do with time, just draw rate. |
| 310 * Is this okay or do we want to have the interpolation based on the t
ime elapsed? |
| 311 * @param current The current value of the signal. |
| 312 * @param input The raw input value. |
| 313 * @return The smoothed signal. |
| 314 */ |
| 315 private float smoothInput(float current, float input) { |
| 316 current = MathUtils.clamp(current, input - 30, input + 30); |
| 317 return MathUtils.interpolate(current, input, 0.8f); |
| 318 } |
| 319 |
| 320 private void init() { |
| 321 mLayoutTabs = null; |
| 322 mFromTab = null; |
| 323 mLeftTab = null; |
| 324 mRightTab = null; |
| 325 mToTab = null; |
| 326 mOffsetStart = 0; |
| 327 mOffset = 0; |
| 328 mOffsetTarget = 0; |
| 329 } |
| 330 |
| 331 /** |
| 332 * Sets a property for an animation. |
| 333 * @param prop The property to update |
| 334 * @param value New value of the property |
| 335 */ |
| 336 @Override |
| 337 public void setProperty(Property prop, float value) { |
| 338 if (prop == Property.OFFSET) { |
| 339 mOffset = value; |
| 340 mOffsetTarget = mOffset; |
| 341 } |
| 342 } |
| 343 |
| 344 @Override |
| 345 protected SceneLayer getSceneLayer() { |
| 346 return mSceneLayer; |
| 347 } |
| 348 |
| 349 @Override |
| 350 protected void updateSceneLayer(Rect viewport, Rect contentViewport, |
| 351 LayerTitleCache layerTitleCache, TabContentManager tabContentManager
, |
| 352 ResourceManager resourceManager, ChromeFullscreenManager fullscreenM
anager) { |
| 353 super.updateSceneLayer(viewport, contentViewport, layerTitleCache, tabCo
ntentManager, |
| 354 resourceManager, fullscreenManager); |
| 355 assert mSceneLayer != null; |
| 356 mSceneLayer.pushLayers(getContext(), viewport, contentViewport, this, la
yerTitleCache, |
| 357 tabContentManager, resourceManager); |
| 358 } |
| 359 } |
OLD | NEW |