Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(345)

Side by Side Diff: chrome/android/java_staging/src/org/chromium/chrome/browser/compositor/layouts/phone/stack/Stack.java

Issue 1141283003: Upstream oodles of Chrome for Android code into Chromium. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: final patch? Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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.phone.stack;
6
7 import android.animation.Animator;
8 import android.animation.AnimatorListenerAdapter;
9 import android.animation.AnimatorSet;
10 import android.content.Context;
11 import android.content.res.Resources;
12 import android.graphics.RectF;
13 import android.view.animation.AccelerateDecelerateInterpolator;
14 import android.view.animation.Interpolator;
15
16 import com.google.android.apps.chrome.R;
17
18 import org.chromium.base.annotations.SuppressFBWarnings;
19 import org.chromium.base.metrics.RecordUserAction;
20 import org.chromium.chrome.browser.Tab;
21 import org.chromium.chrome.browser.compositor.layouts.ChromeAnimation;
22 import org.chromium.chrome.browser.compositor.layouts.Layout;
23 import org.chromium.chrome.browser.compositor.layouts.Layout.Orientation;
24 import org.chromium.chrome.browser.compositor.layouts.components.LayoutTab;
25 import org.chromium.chrome.browser.compositor.layouts.eventfilter.EdgeSwipeEvent Filter.ScrollDirection;
26 import org.chromium.chrome.browser.compositor.layouts.phone.StackLayout;
27 import org.chromium.chrome.browser.compositor.layouts.phone.stack.StackAnimation .OverviewAnimationType;
28 import org.chromium.chrome.browser.tabmodel.TabModel;
29 import org.chromium.chrome.browser.tabmodel.TabModelUtils;
30 import org.chromium.chrome.browser.util.MathUtils;
31 import org.chromium.ui.base.LocalizationUtils;
32
33 /**
34 * Handles all the drawing and events of a stack of stackTabs.
35 *
36 * @VisibleForTesting
37 */
38 public class Stack {
39 public static final int MAX_NUMBER_OF_STACKED_TABS_TOP = 3;
40 public static final int MAX_NUMBER_OF_STACKED_TABS_BOTTOM = 3;
41
42 private static final float STACK_PORTRAIT_Y_OFFSET_PROPORTION = -0.8f;
43 private static final float STACK_LANDSCAPE_START_OFFSET_PROPORTION = -0.7f;
44 private static final float STACK_LANDSCAPE_Y_OFFSET_PROPORTION = -0.5f;
45
46 public enum DragLock { NONE, SCROLL, DISCARD }
47
48 /**
49 * The percentage of the screen that defines the spacing between tabs by def ault (no pinch).
50 */
51 public static final float SPACING_SCREEN = 0.26f;
52
53 /**
54 * The percentage of the screen to cover for the discarded tab to be fully t ransparent.
55 */
56 public static final float DISCARD_RANGE_SCREEN = 0.7f;
57
58 /**
59 * The percentage the tab need to be dragged to actually discard the card.
60 */
61 private static final float DISCARD_COMMIT_THRESHOLD = 0.4f;
62
63 /**
64 * The percentage of the side of the tab that is inactive to swipe to discar d. As this is
65 * a distance computed from both edges, meaningful value ranges in [0 ... 0. 5].
66 */
67 private static final float DISCARD_SAFE_SELECTION_PCTG = 0.1f;
68
69 /**
70 * The minimum scale the tab can reach when being discarded by a click.
71 */
72 private static final float DISCARD_END_SCALE_CLICK = 0.7f;
73
74 /**
75 * The minimum scale the tab can reach when being discarded by a swipe.
76 */
77 private static final float DISCARD_END_SCALE_SWIPE = 0.5f;
78
79 /**
80 * The delta time applied on the velocity from the fling. This is to compute the kick to help
81 * discarding a card.
82 */
83 private static final float DISCARD_FLING_DT = 1.0f / 45.0f;
84
85 /**
86 * The maximum contribution of the fling. This is in percentage of the range .
87 */
88 private static final float DISCARD_FLING_MAX_CONTRIBUTION = 0.4f;
89
90 /**
91 * How much to scale the max overscroll angle when tabs are tilting backward s.
92 */
93 private static final float BACKWARDS_TILT_SCALE = 0.5f;
94
95 /**
96 * When overscrolling towards the top or left of the screen, what portion of
97 * the overscroll should be devoted to sliding the tabs together. The rest
98 * of the overscroll is used for tilting.
99 */
100 private static final float OVERSCROLL_TOP_SLIDE_PCTG = 0.25f;
101
102 /**
103 * Scale max under/over scroll by this amount when flinging.
104 */
105 private static final float MAX_OVER_FLING_SCALE = 0.5f;
106
107 /**
108 * mMaxUnderScroll is determined by multing mMaxOverScroll with
109 * MAX_UNDER_SCROLL_SCALE
110 */
111 private static final float MAX_UNDER_SCROLL_SCALE = 2.0f;
112
113 /**
114 * Drags that are mostly horizontal (within 30 degrees) signal that
115 * a user is discarding a tab.
116 */
117 private static final float DRAG_ANGLE_THRESHOLD = (float) Math.tan(Math.toRa dians(30.0));
118
119 /**
120 * Reset the scroll mode after this number of milliseconds of inactivity or small motions.
121 */
122 private static final long DRAG_TIME_THRESHOLD = 400;
123
124 /**
125 * Minimum motion threshold to lock the scroll mode.
126 */
127 private static final float DRAG_MOTION_THRESHOLD_DP = 1.25f;
128
129 /**
130 * The number of attempt to get the full roll overscroll animation.
131 */
132 private static final int OVERSCROLL_FULL_ROLL_TRIGGER = 5;
133
134 /**
135 * Percentage of the screen to wrap the scroll space.
136 */
137 private static final float SCROLL_WARP_PCTG = 0.4f;
138
139 /**
140 * Percentage of the screen a swipe gesture must traverse before it is allow ed to be canceled.
141 */
142 private static final float SWIPE_LANDSCAPE_THRESHOLD = 0.19f;
143
144 /**
145 * How far to place the tab to the left of the user's finger when swiping in dp. This keeps the
146 * tab under the user's finger.
147 */
148 private static final float LANDSCAPE_SWIPE_DRAG_TAB_OFFSET_DP = 40.f;
149
150 // External References
151 private TabModel mTabModel;
152
153 // True when the stack is still visible for animation but it is going to be empty.
154 private boolean mIsDying;
155
156 // Screen State Variables
157 private int mSpacing;
158 private float mWarpSize;
159 private StackTab[] mStackTabs; // mStackTabs can be null if there are no tab s
160
161 private int mLongPressSelected = -1;
162
163 // During pinch, the finger the closest to the bottom of the stack changes t he scrolling
164 // and the other finger locally stretches the spacing between the tabs.
165 private int mPinch0TabIndex = -1;
166 private int mPinch1TabIndex = -1;
167 private float mLastPinch0Offset;
168 private float mLastPinch1Offset;
169
170 // Current progress of the 'even out' phase. This progress as the screen get scrolled.
171 private float mEvenOutProgress = 1.0f;
172 // Rate to even out all the tabs.
173 private float mEvenOutRate = 1.0f; // This will be updated from dimens.xml
174
175 // Overscroll
176 private StackScroller mScroller;
177 private float mOverScrollOffset;
178 private int mOverScrollDerivative;
179 private int mOverScrollCounter;
180 private float mMaxOverScroll; // This will be updated from dimens.xml
181 private float mMaxUnderScroll;
182 private float mMaxOverScrollAngle; // This will be updated from values.xml
183 private float mMaxOverScrollSlide;
184 private final Interpolator mOverScrollAngleInterpolator =
185 new AccelerateDecelerateInterpolator();
186 private final Interpolator mUnderScrollAngleInterpolator =
187 ChromeAnimation.getDecelerateInterpolator();
188 private final Interpolator mOverscrollSlideInterpolator =
189 new AccelerateDecelerateInterpolator();
190
191 // Drag Lock
192 private DragLock mDragLock = DragLock.NONE;
193 private long mLastScrollUpdate = 0;
194 private float mMinScrollMotion = 0;
195
196 // Scrolling Variables
197 private float mScrollTarget = 0;
198 private float mScrollOffset = 0;
199 private float mScrollOffsetForDyingTabs = 0;
200 private float mCurrentScrollDirection = 0.0f;
201 private StackTab mScrollingTab = null;
202
203 // Swipe Variables
204 private float mSwipeUnboundScrollOffset;
205 private float mSwipeBoundedScrollOffset;
206 private boolean mSwipeIsCancelable;
207 private boolean mSwipeCanScroll;
208 private boolean mInSwipe;
209
210 // Discard
211 private StackTab mDiscardingTab;
212
213 // We can't initialize mDiscardDirection here using LocalizationUtils.isRtl( ) because it will
214 // involve a jni call. Instead, mDiscardDirection will be initialized in Sho w().
215 private float mDiscardDirection = Float.NaN;
216
217 private float mMinSpacing; // This will be updated from dimens.xml
218
219 private boolean mRecomputePosition = true;
220
221 private int mReferenceOrderIndex = -1;
222
223 // Orientation Variables
224 private int mCurrentMode = Orientation.PORTRAIT;
225
226 // Animation Variables
227 private OverviewAnimationType mOverviewAnimationType = OverviewAnimationType .NONE;
228 private StackAnimation mAnimationFactory;
229 private StackViewAnimation mViewAnimationFactory;
230
231 // Running set of animations applied to tabs.
232 private ChromeAnimation<?> mTabAnimations;
233 private AnimatorSet mViewAnimations;
234
235 // The parent Layout
236 private final StackLayout mLayout;
237
238 // Border values
239 private float mBorderTransparentTop;
240 private float mBorderTransparentSide;
241 // TODO(dtrainor): Expose 9-patch padding from resource manager.
242 private float mBorderTopPadding;
243 private float mBorderLeftPadding;
244
245 private final AnimatorListenerAdapter mViewAnimatorListener = new AnimatorLi stenerAdapter() {
246 @Override
247 public void onAnimationCancel(Animator animation) {
248 mLayout.requestUpdate();
249 }
250
251 @Override
252 public void onAnimationEnd(Animator animation) {
253 mLayout.requestUpdate();
254 }
255 };
256
257 /**
258 * @param layout The parent layout.
259 */
260 public Stack(Context context, StackLayout layout) {
261 mLayout = layout;
262 contextChanged(context);
263 }
264
265 /**
266 * @param tabmodel The model to attach to this stack.
267 */
268 public void setTabModel(TabModel tabmodel) {
269 mTabModel = tabmodel;
270 }
271
272 /**
273 * @return The {@link StackTab}s currently being rendered by the tab stack.
274 * @VisibleForTesting
275 */
276 @SuppressFBWarnings("EI_EXPOSE_REP")
277 public StackTab[] getTabs() {
278 return mStackTabs;
279 }
280
281 /**
282 * @return The number of tabs in the tab stack.
283 * @VisibleForTesting
284 */
285 public int getCount() {
286 return mStackTabs != null ? mStackTabs.length : 0;
287 }
288
289 /**
290 * @return The number of visible tabs in the tab stack.
291 */
292 public int getVisibleCount() {
293 int visibleCount = 0;
294 if (mStackTabs != null) {
295 for (int i = 0; i < mStackTabs.length; ++i) {
296 if (mStackTabs[i].getLayoutTab().isVisible()) visibleCount++;
297 }
298 }
299 return visibleCount;
300 }
301
302 /*
303 * Main Interaction Methods for the rest of the application
304 *
305 *
306 * These methods are the main entry points for the model to tell the
307 * view that something has changed. The rest of the application can
308 * alert this class that something in the tab stack has changed or that
309 * the user has decided to enter the tab switcher.
310 *
311 */
312
313 /**
314 * Triggers the closing motions.
315 *
316 * @param time The current time of the app in ms.
317 * @param id The id of the tab that get closed.
318 */
319 public void tabClosingEffect(long time, int id) {
320 if (mStackTabs == null) return;
321
322 // |id| cannot be used to access the particular tab in the model.
323 // The tab is already gone from the model by this point.
324
325 int newIndex = 0;
326 boolean needAnimation = false;
327 for (int i = 0; i < mStackTabs.length; ++i) {
328 if (mStackTabs[i].getId() == id) {
329 // Mark the {@link StackTab} as dying so that when the animation is
330 // finished we can clear it out of the stack. This supports
331 // multiple {@link StackTab} deletions.
332 needAnimation |= !mStackTabs[i].isDying();
333 mStackTabs[i].setDying(true);
334 } else {
335 // Update the {@link StackTab} with a new index here. This make s sure the
336 // {@link LayoutTab} end up in the proper place.
337 mStackTabs[i].setNewIndex(newIndex++);
338 }
339 }
340
341 if (needAnimation) {
342 mScrollOffsetForDyingTabs = mScrollOffset;
343 mSpacing = computeSpacing(newIndex);
344
345 startAnimation(time, OverviewAnimationType.DISCARD);
346 }
347
348 if (newIndex == 0) {
349 mIsDying = true;
350 }
351 }
352
353 /**
354 * Animates all the tabs closing at once.
355 *
356 * @param time The current time of the app in ms.
357 */
358 public void tabsAllClosingEffect(long time) {
359 boolean needAnimation = false;
360
361 if (mStackTabs != null) {
362 for (int i = 0; i < mStackTabs.length; ++i) {
363 needAnimation |= !mStackTabs[i].isDying();
364 mStackTabs[i].setDying(true);
365 }
366 } else {
367 // This needs to be set to true to handle the case where both the no rmal and incognito
368 // tabs are being closed.
369 needAnimation = true;
370 }
371
372 if (needAnimation) {
373 mScrollOffsetForDyingTabs = mScrollOffset;
374 mSpacing = computeSpacing(0);
375
376 if (mStackTabs != null) {
377 boolean isRtl =
378 !((mCurrentMode == Orientation.PORTRAIT) ^ LocalizationU tils.isLayoutRtl());
379 for (int i = 0; i < mStackTabs.length; i++) {
380 StackTab tab = mStackTabs[i];
381 tab.setDiscardOriginY(0.f);
382 tab.setDiscardOriginX(
383 isRtl ? 0.f : tab.getLayoutTab().getOriginalContentW idth());
384 tab.setDiscardFromClick(true);
385 }
386 }
387 startAnimation(time, OverviewAnimationType.DISCARD_ALL);
388 }
389
390 mIsDying = true;
391 }
392
393 /**
394 * Animates a new tab opening.
395 *
396 * @param time The current time of the app in ms.
397 * @param id The id of the new tab to animate.
398 */
399 public void tabCreated(long time, int id) {
400 if (!createTabHelper(id)) return;
401
402 mIsDying = false;
403 finishAnimation(time);
404 startAnimation(time, OverviewAnimationType.NEW_TAB_OPENED,
405 TabModelUtils.getTabIndexById(mTabModel, id), TabModel.INVALID_T AB_INDEX, false);
406 }
407
408 /**
409 * Animates the closing of the stack. Focusing on the selected tab.
410 *
411 * @param time The current time of the app in ms.
412 * @param id The id of the tab to select.
413 */
414 public void tabSelectingEffect(long time, int id) {
415 int index = TabModelUtils.getTabIndexById(mTabModel, id);
416 startAnimation(time, OverviewAnimationType.TAB_FOCUSED, index, -1, false );
417 }
418
419 /**
420 * Called set up the tab stack to the initial state when it is entered.
421 *
422 * @param time The current time of the app in ms.
423 * @param focused Whether or not the stack was focused when entering.
424 */
425 public void stackEntered(long time, boolean focused) {
426 // Don't request new thumbnails until the animation is over. We should
427 // have cached the visible ones already.
428 boolean finishImmediately = !focused;
429 mSpacing = computeSpacing(mStackTabs != null ? mStackTabs.length : 0);
430 resetAllScrollOffset();
431 startAnimation(time, OverviewAnimationType.ENTER_STACK, finishImmediatel y);
432 }
433
434 /**
435 * @return Whether or not the TabModel represented by this TabStackState sho uld be displayed.
436 */
437 public boolean isDisplayable() {
438 return !mTabModel.isIncognito() || (!mIsDying && mTabModel.getCount() > 0);
439 }
440
441 private float getDefaultDiscardDirection() {
442 return (mCurrentMode == Orientation.LANDSCAPE && LocalizationUtils.isLay outRtl()) ? -1.0f
443 : 1.0f;
444 }
445
446 /**
447 * show is called to set up the initial variables, and must always be called before
448 * displaying the stack.
449 */
450 public void show() {
451 mDiscardDirection = getDefaultDiscardDirection();
452
453 // Reinitialize the roll over counter for each tabswitcher session.
454 mOverScrollCounter = 0;
455
456 // TODO: Recreating the stack {@link StackTab} here might be overkill. Will these
457 // already exist in the cache? Check to make sure it makes sense.
458 createStackTabs(false);
459 }
460
461 /*
462 * Animation Start and Finish Methods
463 *
464 * This method kicks off animations by using the
465 * TabSwitcherAnimationFactory to create an AnimatorSet.
466 */
467
468 /**
469 * Starts an animation on the stack.
470 *
471 * @param time The current time of the app in ms.
472 * @param type The type of the animation to start.
473 */
474 private void startAnimation(long time, OverviewAnimationType type) {
475 startAnimation(time, type, TabModel.INVALID_TAB_INDEX, false);
476 }
477
478 /**
479 * Starts an animation on the stack.
480 *
481 * @param time The current time of the app in ms.
482 * @param type The type of the animation to start.
483 * @param finishImmediately Whether the animation jumps straight to the end.
484 */
485 private void startAnimation(long time, OverviewAnimationType type, boolean f inishImmediately) {
486 startAnimation(time, type, TabModel.INVALID_TAB_INDEX, finishImmediately );
487 }
488
489 /**
490 * Starts an animation on the stack.
491 *
492 * @param time The current time of the app in ms.
493 * @param type The type of the animation to start.
494 * @param sourceIndex The source index needed by some animation types.
495 * @param finishImmediately Whether the animation jumps straight to the end.
496 */
497 private void startAnimation(
498 long time, OverviewAnimationType type, int sourceIndex, boolean fini shImmediately) {
499 startAnimation(time, type, mTabModel.index(), sourceIndex, finishImmedia tely);
500 }
501
502 private void startAnimation(long time, OverviewAnimationType type, int focus Index,
503 int sourceIndex, boolean finishImmediately) {
504 if (!canUpdateAnimation(time, type, sourceIndex, finishImmediately)) {
505 // We need to finish animations started earlier before we start
506 // off a new one.
507 finishAnimation(time);
508 // Stop movement while the animation takes place.
509 stopScrollingMovement(time);
510 }
511
512 if (mAnimationFactory != null && mViewAnimationFactory != null) {
513 mOverviewAnimationType = type;
514
515 // First try to build a View animation. Then fallback to the compos itor animation if
516 // one isn't created.
517 mViewAnimations = mViewAnimationFactory.createAnimatorSetForType(
518 type, mStackTabs, mLayout.getViewContainer(), mTabModel, foc usIndex);
519
520 if (mViewAnimations != null) {
521 mViewAnimations.addListener(mViewAnimatorListener);
522 } else {
523 // Build the AnimatorSet using the TabSwitcherAnimationFactory.
524 // This will give us the appropriate AnimatorSet based on the cu rrent
525 // state of the tab switcher and the OverviewAnimationType speci fied.
526 mTabAnimations =
527 mAnimationFactory.createAnimatorSetForType(type, mStackT abs, focusIndex,
528 sourceIndex, mSpacing, mScrollOffset, mWarpSize, getDiscardRange());
529 }
530
531 if (mTabAnimations != null) mTabAnimations.start();
532 if (mViewAnimations != null) mViewAnimations.start();
533 if (mTabAnimations != null || mViewAnimations != null) {
534 mLayout.onStackAnimationStarted();
535 }
536
537 if ((mTabAnimations == null && mViewAnimations == null) || finishImm ediately) {
538 finishAnimation(time);
539 }
540 }
541
542 requestUpdate();
543 }
544
545 /**
546 * Performs the necessary actions to finish the current animation.
547 *
548 * @param time The current time of the app in ms.
549 */
550 private void finishAnimation(long time) {
551 if (mTabAnimations != null) mTabAnimations.updateAndFinish();
552 if (mViewAnimations != null) mViewAnimations.end();
553 if (mTabAnimations != null || mViewAnimations != null) mLayout.onStackAn imationFinished();
554
555 switch (mOverviewAnimationType) {
556 case ENTER_STACK:
557 mLayout.uiDoneEnteringStack();
558 break;
559 case FULL_ROLL:
560 springBack(time);
561 break;
562 case TAB_FOCUSED:
563 // Purposeful fall through
564 case NEW_TAB_OPENED:
565 // Nothing to do.
566 break;
567 case DISCARD_ALL:
568 mLayout.uiDoneClosingAllTabs(mTabModel.isIncognito());
569 cleanupStackTabState();
570 break;
571 case UNDISCARD:
572 // Purposeful fall through because if UNDISCARD animation updated DI SCARD animation,
573 // DISCARD animation clean up below is not called so UNDISCARD is re sponsible for
574 // cleaning it up.
575 case DISCARD:
576 // Remove all dying tabs from mStackTabs.
577 if (mStackTabs != null) {
578 // Request for the model to be updated.
579 for (int i = 0; i < mStackTabs.length; ++i) {
580 StackTab tab = mStackTabs[i];
581 if (tab.isDying()) {
582 mLayout.uiDoneClosingTab(
583 time, tab.getId(), true, mTabModel.isIncogni to());
584 }
585 }
586 }
587 cleanupStackTabState();
588 break;
589 default:
590 break;
591 }
592
593 if (mOverviewAnimationType != OverviewAnimationType.NONE) {
594 // sync the scrollTarget and scrollOffset;
595 setScrollTarget(mScrollOffset, true);
596 mOverviewAnimationType = OverviewAnimationType.NONE;
597 }
598 mTabAnimations = null;
599 mViewAnimations = null;
600 }
601
602 private void cleanupStackTabState() {
603 if (mStackTabs != null) {
604 // First count the number of tabs that are still alive.
605 int nNumberOfLiveTabs = 0;
606 for (int i = 0; i < mStackTabs.length; ++i) {
607 if (mStackTabs[i].isDying()) {
608 mLayout.releaseTabLayout(mStackTabs[i].getLayoutTab());
609 } else {
610 nNumberOfLiveTabs++;
611 }
612 }
613
614 if (nNumberOfLiveTabs == 0) {
615 // We have no more live {@link StackTab}. Just clean all tab rel ated states.
616 cleanupTabs();
617 } else if (nNumberOfLiveTabs < mStackTabs.length) {
618 // If any tabs have died, we need to remove them from mStackTabs .
619
620 StackTab[] oldTabs = mStackTabs;
621 mStackTabs = new StackTab[nNumberOfLiveTabs];
622
623 int newIndex = 0;
624 for (int i = 0; i < oldTabs.length; ++i) {
625 if (!oldTabs[i].isDying()) {
626 mStackTabs[newIndex] = oldTabs[i];
627 mStackTabs[newIndex].setNewIndex(newIndex);
628 newIndex++;
629 }
630 }
631 assert newIndex == nNumberOfLiveTabs;
632 }
633 }
634
635 mDiscardDirection = getDefaultDiscardDirection();
636 }
637
638 /**
639 * Ensure that there are no dying tabs by finishing the current animation.
640 *
641 * @param time The current time of the app in ms.
642 */
643 public void ensureCleaningUpDyingTabs(long time) {
644 finishAnimation(time);
645 }
646
647 /**
648 * Decide if the animation can be started without cleaning up the current an imation.
649 * @param time The current time of the app in ms.
650 * @param type The type of the animation to start.
651 * @param sourceIndex The source index needed by some animation types.
652 * @param finishImmediately Whether the animation jumps straight to the end.
653 * @return true, if we can start the animation without clea ning up the current
654 * animation.
655 */
656 private boolean canUpdateAnimation(
657 long time, OverviewAnimationType type, int sourceIndex, boolean fini shImmediately) {
658 if (mAnimationFactory != null) {
659 if ((mOverviewAnimationType == OverviewAnimationType.DISCARD
660 || mOverviewAnimationType == OverviewAnimationType.UNDIS CARD
661 || mOverviewAnimationType == OverviewAnimationType.DISCA RD_ALL)
662 && (type == OverviewAnimationType.DISCARD
663 || type == OverviewAnimationType.UNDISCARD
664 || type == OverviewAnimationType.DISCARD_ALL)) {
665 return true;
666 }
667 }
668 return false;
669 }
670
671 /**
672 * Cancel scrolling animation which is a part of discarding animation.
673 * @return true if the animation is canceled, false, if there is nothing to cancel.
674 */
675 private boolean cancelDiscardScrollingAnimation() {
676 if (mOverviewAnimationType == OverviewAnimationType.DISCARD
677 || mOverviewAnimationType == OverviewAnimationType.UNDISCARD
678 || mOverviewAnimationType == OverviewAnimationType.DISCARD_ALL) {
679 mTabAnimations.cancel(null, StackTab.Property.SCROLL_OFFSET);
680 return true;
681 }
682 return false;
683 }
684
685 /**
686 * Checks any Android view animations to see if they have finished yet.
687 * @param time The current time of the app in ms.
688 * @param jumpToEnd Whether to finish the animation.
689 * @return Whether the animation was finished.
690 */
691 public boolean onUpdateViewAnimation(long time, boolean jumpToEnd) {
692 boolean finished = true;
693 if (mViewAnimations != null) {
694 finished = !mViewAnimations.isRunning();
695 finishAnimationsIfDone(time, jumpToEnd);
696 }
697 return finished;
698 }
699
700 /**
701 * Steps the animation forward and updates all the animated values.
702 * @param time The current time of the app in ms.
703 * @param jumpToEnd Whether to finish the animation.
704 * @return Whether the animation was finished.
705 */
706 public boolean onUpdateCompositorAnimations(long time, boolean jumpToEnd) {
707 if (!jumpToEnd) updateScrollOffset(time);
708
709 boolean finished = true;
710 if (mTabAnimations != null) {
711 if (jumpToEnd) {
712 finished = mTabAnimations.finished();
713 } else {
714 finished = mTabAnimations.update(time);
715 }
716 finishAnimationsIfDone(time, jumpToEnd);
717 }
718
719 if (jumpToEnd) forceScrollStop();
720 return finished;
721 }
722
723 private void finishAnimationsIfDone(long time, boolean jumpToEnd) {
724 boolean hasViewAnimations = mViewAnimations != null;
725 boolean hasTabAnimations = mTabAnimations != null;
726 boolean hasAnimations = hasViewAnimations || hasTabAnimations;
727 boolean isViewFinished = hasViewAnimations ? !mViewAnimations.isRunning( ) : true;
728 boolean isTabFinished = hasTabAnimations ? mTabAnimations.finished() : t rue;
729
730 boolean shouldFinish = jumpToEnd && hasAnimations;
731 shouldFinish |= hasAnimations && (!hasViewAnimations || isViewFinished)
732 && (!hasTabAnimations || isTabFinished);
733 if (shouldFinish) finishAnimation(time);
734 }
735
736 /**
737 * Determines which action was specified by the user's drag.
738 *
739 * @param scrollDrag The number of pixels moved in the scroll direction.
740 * @param discardDrag The number of pixels moved in the discard direction.
741 * @return The current lock mode or a hint if the motion was not strong enough
742 * to fully lock the mode.
743 */
744 private DragLock computeDragLock(float scrollDrag, float discardDrag) {
745 scrollDrag = Math.abs(scrollDrag);
746 discardDrag = Math.abs(discardDrag);
747 DragLock hintLock = (discardDrag * DRAG_ANGLE_THRESHOLD) > scrollDrag ? DragLock.DISCARD
748 : DragLock.SCROLL;
749 // If the user paused the drag for too long, re-determine what the new a ction is.
750 long timeMillisecond = System.currentTimeMillis();
751 if ((timeMillisecond - mLastScrollUpdate) > DRAG_TIME_THRESHOLD) {
752 mDragLock = DragLock.NONE;
753 }
754 // Select the scroll lock if enough conviction is put into scrolling.
755 if ((mDragLock == DragLock.NONE && Math.abs(scrollDrag - discardDrag) > mMinScrollMotion)
756 || (mDragLock == DragLock.DISCARD && discardDrag > mMinScrollMot ion)
757 || (mDragLock == DragLock.SCROLL && scrollDrag > mMinScrollMotio n)) {
758 mLastScrollUpdate = timeMillisecond;
759 if (mDragLock == DragLock.NONE) {
760 mDragLock = hintLock;
761 }
762 }
763 // Returns a hint of the lock so we can show feedback even if the lock i s not committed yet.
764 return mDragLock == DragLock.NONE ? hintLock : mDragLock;
765 }
766
767 /*
768 * User Input Routines:
769 *
770 * The input routines that process gestures and click touches. These
771 * are the main way to interact with the view directly. Other input
772 * paths happen when model changes impact the view. This can happen
773 * as a result of some of these actions or from other user input (ie:
774 * from the Toolbar). These are ignored if an animation is currently
775 * in progress.
776 */
777
778 /**
779 * Called on drag event (from scroll events in the gesture detector).
780 *
781 * @param time The current time of the app in ms.
782 * @param x The x coordinate of the end of the drag event.
783 * @param y The y coordinate of the end of the drag event.
784 * @param amountX The number of pixels dragged in the x direction since the last event.
785 * @param amountY The number of pixels dragged in the y direction since the last event.
786 */
787 public void drag(long time, float x, float y, float amountX, float amountY) {
788 float scrollDrag, discardDrag;
789 if (mCurrentMode == Orientation.PORTRAIT) {
790 discardDrag = amountX;
791 scrollDrag = amountY;
792 } else {
793 discardDrag = amountY;
794 scrollDrag = LocalizationUtils.isLayoutRtl() ? -amountX : amountX;
795 }
796 DragLock hintLock = computeDragLock(scrollDrag, discardDrag);
797 if (hintLock == DragLock.DISCARD) {
798 discard(x, y, amountX, amountY);
799 } else {
800 // Only cancel the current discard attempt if the scroll lock is com mitted:
801 // by using mDragLock instead of hintLock.
802 if (mDragLock == DragLock.SCROLL && mDiscardingTab != null) {
803 commitDiscard(time, false);
804 }
805 scroll(x, y, LocalizationUtils.isLayoutRtl() ? -amountX : amountX, a mountY, false);
806 }
807 requestUpdate();
808 }
809
810 /**
811 * Discards and updates the position based on the input event values.
812 *
813 * @param x The x coordinate of the end of the drag event.
814 * @param y The y coordinate of the end of the drag event.
815 * @param amountX The number of pixels dragged in the x direction since the last event.
816 * @param amountY The number of pixels dragged in the y direction since the last event.
817 */
818 private void discard(float x, float y, float amountX, float amountY) {
819 if (mStackTabs == null
820 || (mOverviewAnimationType != OverviewAnimationType.NONE
821 && mOverviewAnimationType != OverviewAnimationType.DI SCARD
822 && mOverviewAnimationType != OverviewAnimationType.DI SCARD_ALL
823 && mOverviewAnimationType != OverviewAnimationType.UN DISCARD)) {
824 return;
825 }
826
827 if (mDiscardingTab == null) {
828 if (!mInSwipe) {
829 mDiscardingTab = getTabAtPositon(x, y);
830 } else {
831 if (mTabModel.index() < 0) return;
832 mDiscardingTab = mStackTabs[mTabModel.index()];
833 }
834
835 if (mDiscardingTab != null) {
836 cancelDiscardScrollingAnimation();
837
838 // Make sure we are well within the tab in the discard direction .
839 RectF target = mDiscardingTab.getLayoutTab().getClickTargetBound s();
840 float distanceToEdge;
841 float edgeToEdge;
842 if (mCurrentMode == Orientation.PORTRAIT) {
843 mDiscardDirection = 1.0f;
844 distanceToEdge = Math.max(target.left - x, x - target.right) ;
845 edgeToEdge = target.width();
846 } else {
847 mDiscardDirection = 2.0f - 4.0f * (x / mLayout.getWidth());
848 mDiscardDirection = MathUtils.clamp(mDiscardDirection, -1.0f , 1.0f);
849 distanceToEdge = Math.max(target.top - y, y - target.bottom) ;
850 edgeToEdge = target.height();
851 }
852
853 float scaledDiscardX = x - mDiscardingTab.getLayoutTab().getX();
854 float scaledDiscardY = y - mDiscardingTab.getLayoutTab().getY();
855 mDiscardingTab.setDiscardOriginX(scaledDiscardX / mDiscardingTab .getScale());
856 mDiscardingTab.setDiscardOriginY(scaledDiscardY / mDiscardingTab .getScale());
857 mDiscardingTab.setDiscardFromClick(false);
858
859 if (Math.abs(distanceToEdge) < DISCARD_SAFE_SELECTION_PCTG * edg eToEdge) {
860 mDiscardingTab = null;
861 }
862 }
863 }
864 if (mDiscardingTab != null) {
865 float deltaAmount = mCurrentMode == Orientation.PORTRAIT ? amountX : amountY;
866 mDiscardingTab.addToDiscardAmount(deltaAmount);
867 }
868 }
869
870 /**
871 * Called on touch/tilt scroll event.
872 *
873 * @param x The x coordinate of the end of the scroll event.
874 * @param y The y coordinate of the end of the scroll event.
875 * @param amountX The number of pixels scrolled in the x direction.
876 * @param amountY The number of pixels scrolled in the y direction.
877 * @param isTilt True if the call comes from a tilt event.
878 */
879 private void scroll(float x, float y, float amountX, float amountY, boolean isTilt) {
880 if ((!mScroller.isFinished() && isTilt) || mStackTabs == null
881 || (mOverviewAnimationType != OverviewAnimationType.NONE
882 && mOverviewAnimationType != OverviewAnimationType.DI SCARD
883 && mOverviewAnimationType != OverviewAnimationType.UN DISCARD
884 && mOverviewAnimationType != OverviewAnimationType.DI SCARD_ALL
885 && mOverviewAnimationType != OverviewAnimationType.EN TER_STACK)) {
886 return;
887 }
888
889 float amountScreen = mCurrentMode == Orientation.PORTRAIT ? amountY : am ountX;
890 float amountScroll = amountScreen;
891 float amountEvenOut = amountScreen;
892
893 // Computes the right amount for the scrolling so the finger matches the tab under it.
894 float tabScrollSpaceFinal = 0;
895 if (mScrollingTab == null || isTilt) {
896 mScrollingTab = getTabAtPositon(x, y);
897 }
898
899 if (mScrollingTab == null && mInSwipe && mStackTabs != null) {
900 int index = mTabModel.index();
901 if (index >= 0 && index <= mStackTabs.length) mScrollingTab = mStack Tabs[index];
902 }
903
904 if (mScrollingTab == null) {
905 if (!isTilt) {
906 amountScroll = 0;
907 amountEvenOut = 0;
908 }
909 } else if (mScrollingTab.getIndex() == 0) {
910 amountEvenOut = 0;
911 } else {
912 // Find the scroll that make the selected tab move the right
913 // amount on the screen.
914 float tabScrollSpace = mScrollingTab.getScrollOffset() + mScrollOffs et;
915 float tabScreen = scrollToScreen(tabScrollSpace);
916 tabScrollSpaceFinal = screenToScroll(tabScreen + amountScreen);
917 amountScroll = tabScrollSpaceFinal - tabScrollSpace;
918 // Matching the finger is too strong of a constraints on the edges. So we make
919 // sure the end value is not too far from the linear case.
920 amountScroll = Math.signum(amountScreen)
921 * MathUtils.clamp(Math.abs(amountScroll), Math.abs(amountScr een) * 0.5f,
922 Math.abs(amountScreen) * 2.0f);
923 }
924
925 // Evens out the tabs and correct the scroll amount if needed.
926 if (evenOutTabs(amountEvenOut, false) && mScrollingTab.getIndex() > 0) {
927 // Adjust the amount after the even phase
928 float tabScrollSpace = mScrollingTab.getScrollOffset() + mScrollOffs et;
929 amountScroll = tabScrollSpaceFinal - tabScrollSpace;
930 }
931
932 // Actually do the scrolling.
933 setScrollTarget(mScrollTarget + amountScroll, false);
934 }
935
936 /**
937 * Evens out auto-magically the cards as the stack get scrolled.
938 *
939 * @param amount The amount of scroll performed in pixel. The sign indicates the
940 * direction.
941 * @param allowReverseDirection Whether or not to allow corrections in the r everse direction of
942 * the amount scrolled.
943 * @return True if any tab had been 'visibly' moved.
944 */
945 private boolean evenOutTabs(float amount, boolean allowReverseDirection) {
946 if (mStackTabs == null || mOverviewAnimationType != OverviewAnimationTyp e.NONE
947 || mEvenOutProgress >= 1.0f || amount == 0) {
948 return false;
949 }
950 boolean changed = false;
951 boolean reverseScrolling = false;
952
953 // The evening out process last until mEvenOutRate reaches 1.0. Tabs ble nd linearly
954 // between the current position to a nice evenly scaled pattern. Because we do not store
955 // the starting position for each tab we need more complicated math to d o the blend.
956 // The absoluteProgress is how much we need progress this step on the [0 , 1] scale.
957 float absoluteProgress = Math.min(Math.abs(amount) * mEvenOutRate, 1.0f - mEvenOutProgress);
958 // The relativeProgress is how much we need to blend the target to the c urrent to get there.
959 float relativeProgress = absoluteProgress / (1.0f - mEvenOutProgress);
960
961 float screenMax = getScrollDimensionSize();
962 for (int i = 0; i < mStackTabs.length; ++i) {
963 float source = mStackTabs[i].getScrollOffset();
964 float target = screenToScroll(i * mSpacing);
965 float sourceScreen = Math.min(screenMax, scrollToScreen(source + mSc rollTarget));
966 float targetScreen = Math.min(screenMax, scrollToScreen(target + mSc rollTarget));
967 // If the target and the current position matches on the screen then we snap to the
968 // target.
969 if (sourceScreen == targetScreen) {
970 mStackTabs[i].setScrollOffset(target);
971 continue;
972 }
973 float step = source + (target - source) * relativeProgress;
974 float stepScreen = Math.min(screenMax, scrollToScreen(step + mScroll Target));
975 // If the step can be performed without noticing then we do it.
976 if (sourceScreen == stepScreen) {
977 mStackTabs[i].setScrollOffset(step);
978 continue;
979 }
980 // If the scrolling goes in the same direction as the step then the motion is applied.
981 if ((targetScreen - sourceScreen) * amount > 0 || allowReverseDirect ion) {
982 mStackTabs[i].setScrollOffset(step);
983 changed = true;
984 } else {
985 reverseScrolling = true;
986 }
987 }
988 // Only account for progress if the scrolling was in the right direction . It assumes here
989 // That if any of the tabs was going in the wrong direction then the pro gress is not
990 // recorded at all. This is very conservative to avoid poping in the scr olling. It works
991 // for now but might need to be revisited if we see artifacts.
992 if (!reverseScrolling) {
993 mEvenOutProgress += absoluteProgress;
994 }
995 return changed;
996 }
997
998 /**
999 * Called on touch fling event. Scroll the stack or help to discard a tab.
1000 *
1001 * @param time The current time of the app in ms.
1002 * @param x The y coordinate of the start of the fling event.
1003 * @param y The y coordinate of the start of the fling event.
1004 * @param velocityX The amount of velocity in the x direction.
1005 * @param velocityY The amount of velocity in the y direction.
1006 */
1007 public void fling(long time, float x, float y, float velocityX, float veloci tyY) {
1008 if (mDragLock != DragLock.SCROLL && mDiscardingTab != null) {
1009 float velocity = mCurrentMode == Orientation.PORTRAIT ? velocityX : velocityY;
1010 float maxDelta = getDiscardRange() * DISCARD_FLING_MAX_CONTRIBUTION;
1011 float deltaAmount = MathUtils.clamp(velocity * DISCARD_FLING_DT, -ma xDelta, maxDelta);
1012 mDiscardingTab.addToDiscardAmount(deltaAmount);
1013 } else if (mOverviewAnimationType == OverviewAnimationType.NONE && mScro ller.isFinished()
1014 && mOverScrollOffset == 0 && getTabIndexAtPositon(x, y) >= 0) {
1015 float velocity = mCurrentMode == Orientation.PORTRAIT
1016 ? velocityY
1017 : (LocalizationUtils.isLayoutRtl() ? -velocityX : velocityX) ;
1018 // Fling only overscrolls when the stack is fully unfolded.
1019 mScroller.fling(0, (int) mScrollTarget, 0, (int) velocity, 0, 0,
1020 (int) getMinScroll(false), (int) getMaxScroll(false), 0,
1021 (int) ((velocity > 0 ? mMaxOverScroll : mMaxUnderScroll)
1022 * MAX_OVER_FLING_SCALE),
1023 time);
1024
1025 // Set the target to the final scroll position to make sure
1026 // the offset finally gets there regardless of what happens.
1027 // We override this when the user interrupts the fling though.
1028 setScrollTarget(mScroller.getFinalY(), false);
1029 }
1030 }
1031
1032 /**
1033 * Get called on down touch event.
1034 *
1035 * @param time The current time of the app in ms.
1036 */
1037 public void onDown(long time) {
1038 mDragLock = DragLock.NONE;
1039 if (mOverviewAnimationType == OverviewAnimationType.NONE) {
1040 stopScrollingMovement(time);
1041 }
1042 // Resets the scrolling state.
1043 mScrollingTab = null;
1044 commitDiscard(time, false);
1045 }
1046
1047 /**
1048 * Get called on long press touch event.
1049 *
1050 * @param time The current time of the app in ms.
1051 * @param x The x coordinate in pixel inside the stack view.
1052 * @param y The y coordinate in pixel inside the stack view.
1053 */
1054 public void onLongPress(long time, float x, float y) {
1055 if (mOverviewAnimationType == OverviewAnimationType.NONE) {
1056 mLongPressSelected = getTabIndexAtPositon(x, y);
1057 if (mLongPressSelected >= 0) {
1058 startAnimation(time, OverviewAnimationType.VIEW_MORE, mLongPress Selected, false);
1059 mEvenOutProgress = 0.0f;
1060 }
1061 }
1062 }
1063
1064 /**
1065 * Called when at least 2 touch events are detected.
1066 *
1067 * @param time The current time of the app in ms.
1068 * @param x0 The x coordinate of the first touch event.
1069 * @param y0 The y coordinate of the first touch event.
1070 * @param x1 The x coordinate of the second touch event.
1071 * @param y1 The y coordinate of the second touch event.
1072 * @param firstEvent The pinch is the first of a sequence of pinch events.
1073 */
1074 public void onPinch(long time, float x0, float y0, float x1, float y1, boole an firstEvent) {
1075 if ((mOverviewAnimationType != OverviewAnimationType.START_PINCH
1076 && mOverviewAnimationType != OverviewAnimationType.NONE) || mSta ckTabs == null) {
1077 return;
1078 }
1079 if (mPinch0TabIndex < 0) startAnimation(time, OverviewAnimationType.STAR T_PINCH);
1080
1081 // Reordering the fingers so pinch0 is always the closest to the top of the stack.
1082 // This allows simpler math down the line where we assume that
1083 // pinch0TabIndex <= pinch0TabIndex
1084 // It also means that crossing the finger will separate the tabs again.
1085 boolean inverse = (mCurrentMode == Orientation.PORTRAIT)
1086 ? y0 > y1
1087 : LocalizationUtils.isLayoutRtl() ? (x0 <= x1) : (x0 > x1);
1088 float pinch0X = inverse ? x1 : x0;
1089 float pinch0Y = inverse ? y1 : y0;
1090 float pinch1X = inverse ? x0 : x1;
1091 float pinch1Y = inverse ? y0 : y1;
1092 float pinch0Offset = (mCurrentMode == Orientation.PORTRAIT)
1093 ? pinch0Y
1094 : LocalizationUtils.isLayoutRtl() ? -pinch0X : pinch0X;
1095 float pinch1Offset = (mCurrentMode == Orientation.PORTRAIT)
1096 ? pinch1Y
1097 : LocalizationUtils.isLayoutRtl() ? -pinch1X : pinch1X;
1098
1099 if (firstEvent) {
1100 // Resets pinch and scrolling state.
1101 mPinch0TabIndex = -1;
1102 mPinch1TabIndex = -1;
1103 mScrollingTab = null;
1104 commitDiscard(time, false);
1105 }
1106 int pinch0TabIndex = mPinch0TabIndex;
1107 int pinch1TabIndex = mPinch1TabIndex;
1108 if (mPinch0TabIndex < 0) {
1109 pinch0TabIndex = getTabIndexAtPositon(pinch0X, pinch0Y);
1110 pinch1TabIndex = getTabIndexAtPositon(pinch1X, pinch1Y);
1111 // If any of them is invalid we invalidate both.
1112 if (pinch0TabIndex < 0 || pinch1TabIndex < 0) {
1113 pinch0TabIndex = -1;
1114 pinch1TabIndex = -1;
1115 }
1116 }
1117
1118 if (pinch0TabIndex >= 0 && mPinch0TabIndex == pinch0TabIndex
1119 && mPinch1TabIndex == pinch1TabIndex) {
1120 final float minScrollTarget = getMinScroll(false);
1121 final float maxScrollTarget = getMaxScroll(false);
1122 final float oldScrollTarget =
1123 MathUtils.clamp(mScrollTarget, minScrollTarget, maxScrollTar get);
1124 // pinch0TabIndex > pinch1TabIndex is unexpected but we do not want to exit
1125 // ungracefully so process it as if the tabs were the same.
1126 if (pinch0TabIndex >= pinch1TabIndex) {
1127 // If one tab is pinched then we only scroll.
1128 float screenDelta0 = pinch0Offset - mLastPinch0Offset;
1129 if (pinch0TabIndex == 0) {
1130 // Linear scroll on the top tab for the overscroll to kick-i n linearly.
1131 setScrollTarget(oldScrollTarget + screenDelta0, false);
1132 } else {
1133 float tab0ScrollSpace =
1134 mStackTabs[pinch0TabIndex].getScrollOffset() + oldSc rollTarget;
1135 float tab0Screen = scrollToScreen(tab0ScrollSpace);
1136 float tab0ScrollFinal = screenToScroll(tab0Screen + screenDe lta0);
1137 setScrollTarget(
1138 tab0ScrollFinal - mStackTabs[pinch0TabIndex].getScro llOffset(), false);
1139 }
1140 // This is the common case of the pinch, 2 fingers on 2 differen t tabs.
1141 } else {
1142 // Find the screen space position before and after the scroll so the tab 0 matches
1143 // the finger 0 motion.
1144 float screenDelta0 = pinch0Offset - mLastPinch0Offset;
1145 float tab0ScreenBefore = approxScreen(mStackTabs[pinch0TabIndex] , oldScrollTarget);
1146 float tab0ScreenAfter = tab0ScreenBefore + screenDelta0;
1147
1148 // Find the screen space position before and after the scroll so the tab 1 matches
1149 // the finger 1 motion.
1150 float screenDelta1 = pinch1Offset - mLastPinch1Offset;
1151 float tab1ScreenBefore = approxScreen(mStackTabs[pinch1TabIndex] , oldScrollTarget);
1152 float tab1ScreenAfter = tab1ScreenBefore + screenDelta1;
1153
1154 // Heuristic: the scroll is defined by half the change of the fi rst pinched tab.
1155 // The rational is that it looks nice this way :)... Scrolling c reates a sliding
1156 // effect. When a finger does not move then it is expected that none of the tabs
1157 // past that steady finger should move. This does the job.
1158 float globalScrollBefore = screenToScroll(tab0ScreenBefore);
1159 float globalScrollAfter = screenToScroll((tab0ScreenAfter + tab0 ScreenBefore) / 2);
1160 setScrollTarget(oldScrollTarget + globalScrollAfter - globalScro llBefore, true);
1161
1162 // Evens out the tabs in between
1163 float minScreen = tab0ScreenAfter;
1164 float maxScreen = tab0ScreenAfter;
1165 for (int i = pinch0TabIndex; i <= pinch1TabIndex; i++) {
1166 float screenBefore = approxScreen(mStackTabs[i], oldScrollTa rget);
1167 float t = (screenBefore - tab0ScreenBefore)
1168 / (tab1ScreenBefore - tab0ScreenBefore);
1169 float screenAfter = (1 - t) * tab0ScreenAfter + t * tab1Scre enAfter;
1170 screenAfter = Math.max(minScreen, screenAfter);
1171 screenAfter = Math.min(maxScreen, screenAfter);
1172 minScreen = screenAfter + StackTab.sStackedTabVisibleSize;
1173 maxScreen = screenAfter + mStackTabs[i].getSizeInScrollDirec tion(mCurrentMode);
1174 float newScrollOffset = screenToScroll(screenAfter) - mScrol lTarget;
1175 mStackTabs[i].setScrollOffset(newScrollOffset);
1176 }
1177
1178 // Push a bit the tabs bellow pinch1.
1179 float delta1 = tab1ScreenAfter - tab1ScreenBefore;
1180 for (int i = pinch1TabIndex + 1; i < mStackTabs.length; i++) {
1181 delta1 /= 2;
1182 float screenAfter = approxScreen(mStackTabs[i], oldScrollTar get) + delta1;
1183 screenAfter = Math.max(minScreen, screenAfter);
1184 screenAfter = Math.min(maxScreen, screenAfter);
1185 minScreen = screenAfter + StackTab.sStackedTabVisibleSize;
1186 maxScreen = screenAfter + mStackTabs[i].getSizeInScrollDirec tion(mCurrentMode);
1187 mStackTabs[i].setScrollOffset(screenToScroll(screenAfter) - mScrollTarget);
1188 }
1189
1190 // Pull a bit the tabs above pinch0.
1191 minScreen = tab0ScreenAfter;
1192 maxScreen = tab0ScreenAfter;
1193 float posScreen = tab0ScreenAfter;
1194 float delta0 = tab0ScreenAfter - tab0ScreenBefore;
1195 for (int i = pinch0TabIndex - 1; i > 0; i--) {
1196 delta0 /= 2;
1197 minScreen = posScreen - mStackTabs[i].getSizeInScrollDirecti on(mCurrentMode);
1198 maxScreen = posScreen - StackTab.sStackedTabVisibleSize;
1199 float screenAfter = approxScreen(mStackTabs[i], oldScrollTar get) + delta0;
1200 screenAfter = Math.max(minScreen, screenAfter);
1201 screenAfter = Math.min(maxScreen, screenAfter);
1202 mStackTabs[i].setScrollOffset(screenToScroll(screenAfter) - mScrollTarget);
1203 }
1204 }
1205 }
1206 mPinch0TabIndex = pinch0TabIndex;
1207 mPinch1TabIndex = pinch1TabIndex;
1208 mLastPinch0Offset = pinch0Offset;
1209 mLastPinch1Offset = pinch1Offset;
1210 mEvenOutProgress = 0.0f;
1211 requestUpdate();
1212 }
1213
1214 /**
1215 * Commits or release the that currently being considered for discard. This function
1216 * also triggers the associated animations.
1217 *
1218 * @param time The current time of the app in ms.
1219 * @param allowDiscard Whether to allow to discard the tab currently being c onsidered
1220 * for discard.
1221 */
1222 private void commitDiscard(long time, boolean allowDiscard) {
1223 if (mDiscardingTab == null) return;
1224
1225 assert mStackTabs != null;
1226 StackTab discarded = mDiscardingTab;
1227 if (Math.abs(discarded.getDiscardAmount()) / getDiscardRange() > DISCARD _COMMIT_THRESHOLD
1228 && allowDiscard) {
1229 mLayout.uiRequestingCloseTab(time, discarded.getId());
1230 RecordUserAction.record("MobileStackViewSwipeCloseTab");
1231 RecordUserAction.record("MobileTabClosed");
1232 } else {
1233 startAnimation(time, OverviewAnimationType.UNDISCARD);
1234 }
1235 mDiscardingTab = null;
1236 requestUpdate();
1237 }
1238
1239 /**
1240 * Called on touch up or cancel event.
1241 */
1242 public void onUpOrCancel(long time) {
1243 // Make sure the bottom tab always goes back to the top of the screen.
1244 if (mPinch0TabIndex >= 0) {
1245 startAnimation(time, OverviewAnimationType.REACH_TOP);
1246 requestUpdate();
1247 }
1248 // Commit or uncommit discard tab
1249 commitDiscard(time, true);
1250
1251 resetInputActionIndices();
1252
1253 springBack(time);
1254 }
1255
1256 /**
1257 * Bounces back if we happen to overscroll the stack.
1258 */
1259 private void springBack(long time) {
1260 if (mScroller.isFinished()) {
1261 int minScroll = (int) getMinScroll(false);
1262 int maxScroll = (int) getMaxScroll(false);
1263 if (mScrollTarget < minScroll || mScrollTarget > maxScroll) {
1264 mScroller.springBack(0, (int) mScrollTarget, 0, 0, minScroll, ma xScroll, time);
1265 setScrollTarget(MathUtils.clamp(mScrollTarget, minScroll, maxScr oll), false);
1266 requestUpdate();
1267 }
1268 }
1269 }
1270
1271 /**
1272 * Called on touch click event.
1273 *
1274 * @param time The current time of the app in ms.
1275 * @param x The x coordinate in pixel inside the stack view.
1276 * @param y The y coordinate in pixel inside the stack view.
1277 */
1278 public void click(long time, float x, float y) {
1279 if (mOverviewAnimationType != OverviewAnimationType.NONE
1280 && mOverviewAnimationType != OverviewAnimationType.DISCARD
1281 && mOverviewAnimationType != OverviewAnimationType.UNDISCARD
1282 && mOverviewAnimationType != OverviewAnimationType.DISCARD_ALL) {
1283 return;
1284 }
1285 int clicked = getTabIndexAtPositon(x, y, LayoutTab.getTouchSlop());
1286 if (clicked >= 0) {
1287 // Check if the click was within the boundaries of the close button defined by its
1288 // visible coordinates.
1289 boolean isRtl =
1290 !((mCurrentMode == Orientation.PORTRAIT) ^ LocalizationUtils .isLayoutRtl());
1291 if (mStackTabs[clicked].getLayoutTab().checkCloseHitTest(x, y, isRtl )) {
1292 // Tell the model to close the tab because the close button was pressed. The model
1293 // will then trigger a notification which will start the actual close process here
1294 // if necessary.
1295 StackTab tab = mStackTabs[clicked];
1296 final float halfCloseBtnWidth = LayoutTab.CLOSE_BUTTON_WIDTH_DP / 2.f;
1297 final float halfCloseBtnHeight = mBorderTopPadding / 2.f;
1298 final float contentWidth = tab.getLayoutTab().getOriginalContent Width();
1299
1300 tab.setDiscardOriginY(halfCloseBtnHeight);
1301 tab.setDiscardOriginX(isRtl ? halfCloseBtnWidth : contentWidth - halfCloseBtnWidth);
1302 tab.setDiscardFromClick(true);
1303 mLayout.uiRequestingCloseTab(time, tab.getId());
1304 RecordUserAction.record("MobileStackViewCloseTab");
1305 RecordUserAction.record("MobileTabClosed");
1306 } else {
1307 // Let the model know that a new {@link LayoutTab} was selected. The model will
1308 // notify us if we need to do anything visual. setIndex() will p ossibly switch the
1309 // models and broadcast the event.
1310 mLayout.uiSelectingTab(time, mStackTabs[clicked].getId());
1311 }
1312 }
1313 }
1314
1315 /*
1316 * Initialization and Utility Methods
1317 */
1318
1319 /**
1320 * @param context The current Android's context.
1321 */
1322 public void contextChanged(Context context) {
1323 Resources res = context.getResources();
1324 final float pxToDp = 1.0f / res.getDisplayMetrics().density;
1325
1326 mMinScrollMotion = DRAG_MOTION_THRESHOLD_DP;
1327 final float maxOverScrollPx = res.getDimensionPixelOffset(R.dimen.over_s croll);
1328 final float maxUnderScrollPx = Math.round(maxOverScrollPx * MAX_UNDER_SC ROLL_SCALE);
1329 mMaxOverScroll = maxOverScrollPx * pxToDp;
1330 mMaxUnderScroll = maxUnderScrollPx * pxToDp;
1331 mMaxOverScrollAngle = res.getInteger(R.integer.over_scroll_angle);
1332 mMaxOverScrollSlide = res.getDimensionPixelOffset(R.dimen.over_scroll_sl ide) * pxToDp;
1333 mEvenOutRate = 1.0f / (res.getDimension(R.dimen.even_out_scrolling) * px ToDp);
1334 mMinSpacing = res.getDimensionPixelOffset(R.dimen.min_spacing) * pxToDp;
1335 mBorderTransparentTop =
1336 res.getDimension(R.dimen.tabswitcher_border_frame_transparent_to p) * pxToDp;
1337 mBorderTransparentSide =
1338 res.getDimension(R.dimen.tabswitcher_border_frame_transparent_si de) * pxToDp;
1339 mBorderTopPadding = res.getDimension(R.dimen.tabswitcher_border_frame_pa dding_top) * pxToDp;
1340 mBorderLeftPadding =
1341 res.getDimension(R.dimen.tabswitcher_border_frame_padding_left) * pxToDp;
1342
1343 // Just in case the density has changed, rebuild the OverScroller.
1344 mScroller = new StackScroller(context);
1345 }
1346
1347 /**
1348 * @param width The new width of the layout.
1349 * @param height The new height of the layout.
1350 * @param orientation The new orientation of the layout.
1351 */
1352 public void notifySizeChanged(float width, float height, int orientation) {
1353 updateCurrentMode(orientation);
1354 }
1355
1356 private float getScrollDimensionSize() {
1357 return mCurrentMode == Orientation.PORTRAIT ? mLayout.getHeightMinusTopC ontrols()
1358 : mLayout.getWidth();
1359 }
1360
1361 /**
1362 * Gets the tab instance at the requested position.
1363 *
1364 * @param x The x coordinate where to perform the hit test.
1365 * @param y The y coordinate where to perform the hit test.
1366 * @return The instance of the tab selected. null if none.
1367 */
1368 private StackTab getTabAtPositon(float x, float y) {
1369 int tabIndexAtPosition = getTabIndexAtPositon(x, y, 0);
1370 return tabIndexAtPosition < 0 ? null : mStackTabs[tabIndexAtPosition];
1371 }
1372
1373 /**
1374 * Gets the tab index at the requested position.
1375 *
1376 * @param x The x coordinate where to perform the hit test.
1377 * @param y The y coordinate where to perform the hit test.
1378 * @return The index of the tab selected. -1 if none.
1379 */
1380 private int getTabIndexAtPositon(float x, float y) {
1381 return getTabIndexAtPositon(x, y, 0);
1382 }
1383
1384 /**
1385 * Gets the tab index at the requested position.
1386 *
1387 * @param x The x coordinate where to perform the hit test.
1388 * @param y The y coordinate where to perform the hit test.
1389 * @param slop The acceptable distance to a tab for it to be considered.
1390 * @return The index of the tab selected. -1 if none.
1391 */
1392 private int getTabIndexAtPositon(float x, float y, float slop) {
1393 int closestIndex = -1;
1394 float closestDistance = mLayout.getHeight() + mLayout.getWidth();
1395 if (mStackTabs != null) {
1396 for (int i = mStackTabs.length - 1; i >= 0; --i) {
1397 // This is a fail safe. We should never have a situation where a dying
1398 // {@link LayoutTab} can get accessed (the animation check shoul d catch it).
1399 if (!mStackTabs[i].isDying() && mStackTabs[i].getLayoutTab().isV isible()) {
1400 float d = mStackTabs[i].getLayoutTab().computeDistanceTo(x, y);
1401 // Strict '<' is very important here because we might have s everal tab at the
1402 // same place and we want the one above.
1403 if (d < closestDistance) {
1404 closestIndex = i;
1405 closestDistance = d;
1406 if (d == 0) break;
1407 }
1408 }
1409 }
1410 }
1411 return closestDistance <= slop ? closestIndex : -1;
1412 }
1413
1414 /**
1415 * ComputeTabPosition pass 1:
1416 * Combine the overall stack scale with the animated tab scale.
1417 *
1418 * @param stackRect The frame of the stack.
1419 */
1420 private void computeTabScaleAlphaDepthHelper(RectF stackRect) {
1421 final float stackScale = getStackScale(stackRect);
1422 final float discardRange = getDiscardRange();
1423
1424 for (int i = 0; i < mStackTabs.length; ++i) {
1425 assert mStackTabs[i] != null;
1426 StackTab stackTab = mStackTabs[i];
1427 LayoutTab layoutTab = stackTab.getLayoutTab();
1428 final float discard = stackTab.getDiscardAmount();
1429
1430 // Scale
1431 float discardScale =
1432 computeDiscardScale(discard, discardRange, stackTab.getDisca rdFromClick());
1433 layoutTab.setScale(stackTab.getScale() * discardScale * stackScale);
1434 layoutTab.setBorderScale(discardScale);
1435
1436 // Alpha
1437 float discardAlpha = computeDiscardAlpha(discard, discardRange);
1438 layoutTab.setAlpha(stackTab.getAlpha() * discardAlpha);
1439 }
1440 }
1441
1442 /**
1443 * ComputeTabPosition pass 2:
1444 * Adjust the scroll offsets of each tab so no there is no void in between t abs.
1445 */
1446 private void computeTabScrollOffsetHelper() {
1447 float maxScrollOffset = Float.MAX_VALUE;
1448 for (int i = 0; i < mStackTabs.length; ++i) {
1449 if (mStackTabs[i].isDying()) continue;
1450
1451 float tabScrollOffset = Math.min(maxScrollOffset, mStackTabs[i].getS crollOffset());
1452 mStackTabs[i].setScrollOffset(tabScrollOffset);
1453
1454 float maxScreenScrollOffset = scrollToScreen(mScrollOffset + tabScro llOffset);
1455 maxScrollOffset = -mScrollOffset
1456 + screenToScroll(maxScreenScrollOffset
1457 + mStackTabs[i].getSizeInScrollDirection(m CurrentMode));
1458 }
1459 }
1460
1461 /**
1462 * ComputeTabPosition pass 3:
1463 * Compute the position of the tabs. Adjust for top and bottom stacking.
1464 *
1465 * @param stackRect The frame of the stack.
1466 */
1467 private void computeTabOffsetHelper(RectF stackRect) {
1468 final boolean portrait = mCurrentMode == Orientation.PORTRAIT;
1469
1470 // Precompute the position using scroll offset and top stacking.
1471 final float parentWidth = stackRect.width();
1472 final float parentHeight = stackRect.height();
1473 final float overscrollPercent = computeOverscrollPercent();
1474 final float scrollOffset =
1475 MathUtils.clamp(mScrollOffset, getMinScroll(false), getMaxScroll (false));
1476 final float stackScale = getStackScale(stackRect);
1477
1478 int stackedCount = 0;
1479 float minStackedPosition = 0.0f;
1480 for (int i = 0; i < mStackTabs.length; ++i) {
1481 assert mStackTabs[i] != null;
1482 StackTab stackTab = mStackTabs[i];
1483 LayoutTab layoutTab = stackTab.getLayoutTab();
1484
1485 // Position
1486 final float stackScrollOffset =
1487 stackTab.isDying() ? mScrollOffsetForDyingTabs : scrollOffse t;
1488 float screenScrollOffset = approxScreen(stackTab, stackScrollOffset) ;
1489
1490 // Resolve top stacking
1491 screenScrollOffset = Math.max(minStackedPosition, screenScrollOffset );
1492 if (stackedCount < MAX_NUMBER_OF_STACKED_TABS_TOP) {
1493 // This make sure all the tab get stacked up as one when all the tabs do a
1494 // full roll animation.
1495 final float tiltXcos = (float) Math.cos(Math.toRadians(layoutTab .getTiltX()));
1496 final float tiltYcos = (float) Math.cos(Math.toRadians(layoutTab .getTiltY()));
1497 float collapse = Math.min(Math.abs(tiltXcos), Math.abs(tiltYcos) );
1498 collapse *= layoutTab.getAlpha();
1499 minStackedPosition += StackTab.sStackedTabVisibleSize * collapse ;
1500 }
1501 stackedCount += stackTab.isDying() ? 0 : 1;
1502 if (overscrollPercent < 0) {
1503 // Oversroll at the top of the screen. For the first
1504 // OVERSCROLL_TOP_SLIDE_PCTG of the overscroll, slide the tabs
1505 // together so they completely overlap. After that, stop scroll ing the tabs.
1506 screenScrollOffset +=
1507 (overscrollPercent / OVERSCROLL_TOP_SLIDE_PCTG) * screen ScrollOffset;
1508 screenScrollOffset = Math.max(0, screenScrollOffset);
1509 }
1510
1511 // Note: All the Offsets except for centering shouldn't depend on th e tab's scaling
1512 // because it interferes the scaling center.
1513
1514 // Centers the tab in its parent.
1515 float xIn = (parentWidth - layoutTab.getScaledContentWidth()) / 2.0f ;
1516 float yIn = (parentHeight - layoutTab.getScaledContentHeight()) / 2. 0f;
1517
1518 // We want slight offset from the center so that multiple tab browsi ng
1519 // have more space to its expanding direction. e.g., On portrait mod e,
1520 // there will be more space on the bottom than top.
1521 final float horizontalPadding =
1522 (parentWidth
1523 - layoutTab.getOriginalContentWidth() * StackAnimati on.SCALE_AMOUNT
1524 * stackScale) / 2.0f;
1525 final float verticalPadding =
1526 (parentHeight
1527 - layoutTab.getOriginalContentHeight() * StackAnimat ion.SCALE_AMOUNT
1528 * stackScale) / 2.0f;
1529
1530 if (portrait) {
1531 yIn += STACK_PORTRAIT_Y_OFFSET_PROPORTION * verticalPadding;
1532 yIn += screenScrollOffset;
1533 } else {
1534 if (LocalizationUtils.isLayoutRtl()) {
1535 xIn -= STACK_LANDSCAPE_START_OFFSET_PROPORTION * horizontalP adding;
1536 xIn -= screenScrollOffset;
1537 } else {
1538 xIn += STACK_LANDSCAPE_START_OFFSET_PROPORTION * horizontalP adding;
1539 xIn += screenScrollOffset;
1540 }
1541 yIn += STACK_LANDSCAPE_Y_OFFSET_PROPORTION * verticalPadding;
1542 }
1543
1544 layoutTab.setX(xIn);
1545 layoutTab.setY(yIn);
1546 }
1547
1548 // Resolve bottom stacking
1549 stackedCount = 0;
1550 float maxStackedPosition =
1551 portrait ? mLayout.getHeightMinusTopControls() : mLayout.getWidt h();
1552 for (int i = mStackTabs.length - 1; i >= 0; i--) {
1553 assert mStackTabs[i] != null;
1554 StackTab stackTab = mStackTabs[i];
1555 LayoutTab layoutTab = stackTab.getLayoutTab();
1556 if (stackTab.isDying()) continue;
1557
1558 float pos;
1559 if (portrait) {
1560 pos = layoutTab.getY();
1561 layoutTab.setY(Math.min(pos, maxStackedPosition));
1562 } else if (LocalizationUtils.isLayoutRtl()) {
1563 // On RTL landscape, pos is a distance between tab's right and m Layout's right.
1564 float posOffset = mLayout.getWidth()
1565 - layoutTab.getOriginalContentWidth() * StackAnimation.S CALE_AMOUNT
1566 * stackScale;
1567 pos = -layoutTab.getX() + posOffset;
1568 layoutTab.setX(-Math.min(pos, maxStackedPosition) + posOffset);
1569 } else {
1570 pos = layoutTab.getX();
1571 layoutTab.setX(Math.min(pos, maxStackedPosition));
1572 }
1573 if (pos >= maxStackedPosition && stackedCount < MAX_NUMBER_OF_STACKE D_TABS_BOTTOM) {
1574 maxStackedPosition -= StackTab.sStackedTabVisibleSize;
1575 stackedCount++;
1576 }
1577 }
1578
1579 // final position blend
1580 final float discardRange = getDiscardRange();
1581 for (int i = 0; i < mStackTabs.length; ++i) {
1582 assert mStackTabs[i] != null;
1583 StackTab stackTab = mStackTabs[i];
1584 LayoutTab layoutTab = stackTab.getLayoutTab();
1585
1586 final float xIn = layoutTab.getX() + stackTab.getXInStackOffset();
1587 final float yIn = layoutTab.getY() + stackTab.getYInStackOffset();
1588 final float xOut = stackTab.getXOutOfStack();
1589 final float yOut = stackTab.getYOutOfStack();
1590 float x = MathUtils.interpolate(xOut, xIn, stackTab.getXInStackInflu ence());
1591 float y = MathUtils.interpolate(yOut, yIn, stackTab.getYInStackInflu ence());
1592
1593 // Discard offsets
1594 if (stackTab.getDiscardAmount() != 0) {
1595 float discard = stackTab.getDiscardAmount();
1596 boolean fromClick = stackTab.getDiscardFromClick();
1597 float scale = computeDiscardScale(discard, discardRange, fromCli ck);
1598 float deltaX = stackTab.getDiscardOriginX()
1599 - stackTab.getLayoutTab().getOriginalContentWidth() / 2. f;
1600 float deltaY = stackTab.getDiscardOriginY()
1601 - stackTab.getLayoutTab().getOriginalContentHeight() / 2 .f;
1602 float discardOffset = fromClick ? 0.f : discard;
1603 if (portrait) {
1604 x += discardOffset + deltaX * (1.f - scale);
1605 y += deltaY * (1.f - scale);
1606 } else {
1607 x += deltaX * (1.f - scale);
1608 y += discardOffset + deltaY * (1.f - scale);
1609 }
1610 }
1611
1612 // Finally apply the stack translation
1613 layoutTab.setX(stackRect.left + x);
1614 layoutTab.setY(stackRect.top + y);
1615 }
1616 }
1617
1618 /**
1619 * ComputeTabPosition pass 5:
1620 * Computes the clipping, visibility and adjust overall alpha if needed.
1621 */
1622 private void computeTabClippingVisibilityHelper() {
1623 // alpha override, clipping and culling.
1624 final boolean portrait = mCurrentMode == Orientation.PORTRAIT;
1625
1626 // Iterate through each tab starting at the top of the stack and working
1627 // backwards. Set the clip on each tab such that it does not extend past
1628 // the beginning of the tab above it. clipOffset is used to keep track
1629 // of where the previous tab started.
1630 float clipOffset;
1631 if (portrait) {
1632 // portrait LTR & RTL
1633 clipOffset = mLayout.getHeight() + StackTab.sStackedTabVisibleSize;
1634 } else if (!LocalizationUtils.isLayoutRtl()) {
1635 // landscape LTR
1636 clipOffset = mLayout.getWidth() + StackTab.sStackedTabVisibleSize;
1637 } else {
1638 // landscape RTL
1639 clipOffset = -StackTab.sStackedTabVisibleSize;
1640 }
1641
1642 for (int i = mStackTabs.length - 1; i >= 0; i--) {
1643 LayoutTab layoutTab = mStackTabs[i].getLayoutTab();
1644 layoutTab.setVisible(true);
1645
1646 // Don't bother with clipping tabs that are dying, rotating, with an X offset, or
1647 // non-opaque.
1648 if (mStackTabs[i].isDying() || mStackTabs[i].getXInStackOffset() != 0.0f
1649 || layoutTab.getAlpha() < 1.0f) {
1650 layoutTab.setClipOffset(0.0f, 0.0f);
1651 layoutTab.setClipSize(Float.MAX_VALUE, Float.MAX_VALUE);
1652 continue;
1653 }
1654
1655 // The beginning, size, and clipped size of the current tab.
1656 float tabOffset, tabSize, tabClippedSize, borderAdjustmentSize, inse tBorderPadding;
1657 if (portrait) {
1658 // portrait LTR & RTL
1659 tabOffset = layoutTab.getY();
1660 tabSize = layoutTab.getScaledContentHeight();
1661 tabClippedSize = Math.min(tabSize, clipOffset - tabOffset);
1662 borderAdjustmentSize = mBorderTransparentTop;
1663 insetBorderPadding = mBorderTopPadding;
1664 } else if (!LocalizationUtils.isLayoutRtl()) {
1665 // landscape LTR
1666 tabOffset = layoutTab.getX();
1667 tabSize = layoutTab.getScaledContentWidth();
1668 tabClippedSize = Math.min(tabSize, clipOffset - tabOffset);
1669 borderAdjustmentSize = mBorderTransparentSide;
1670 insetBorderPadding = 0;
1671 } else {
1672 // landscape RTL
1673 tabOffset = layoutTab.getX() + layoutTab.getScaledContentWidth() ;
1674 tabSize = layoutTab.getScaledContentWidth();
1675 tabClippedSize = Math.min(tabSize, tabOffset - clipOffset);
1676 borderAdjustmentSize = -mBorderTransparentSide;
1677 insetBorderPadding = 0;
1678 }
1679
1680 float absBorderAdjustmentSize = Math.abs(borderAdjustmentSize);
1681
1682 if (tabClippedSize <= absBorderAdjustmentSize) {
1683 // If the tab is completed covered, don't bother drawing it at a ll.
1684 layoutTab.setVisible(false);
1685 layoutTab.setDrawDecoration(true);
1686 } else {
1687 // Fade the tab as it gets too close to the next one. This helps
1688 // prevent overlapping shadows from becoming too dark.
1689 float fade = MathUtils.clamp(((tabClippedSize - absBorderAdjustm entSize)
1690 / StackTab.sStackedTabVisib leSize),
1691 0, 1);
1692 layoutTab.setDecorationAlpha(fade);
1693
1694 // When tabs tilt forward, it will expose more of the tab
1695 // underneath. To compensate, make the clipping size larger.
1696 // Note, this calculation is only an estimate that seems to
1697 // work.
1698 float clipScale = 1.0f;
1699 if (layoutTab.getTiltX() > 0 || ((!portrait && LocalizationUtils .isLayoutRtl())
1700 ? layoutTab.getT iltY() < 0
1701 : layoutTab.getT iltY() > 0)) {
1702 final float tilt =
1703 Math.max(layoutTab.getTiltX(), Math.abs(layoutTab.ge tTiltY()));
1704 clipScale += (tilt / mMaxOverScrollAngle) * 0.60f;
1705 }
1706
1707 float scaledTabClippedSize = Math.min(tabClippedSize * clipScale , tabSize);
1708 // Set the clip
1709 layoutTab.setClipOffset((!portrait && LocalizationUtils.isLayout Rtl())
1710 ? (tabSize - scaledTabClippedSize)
1711 : 0,
1712 0);
1713 layoutTab.setClipSize(portrait ? Float.MAX_VALUE : scaledTabClip pedSize,
1714 portrait ? scaledTabClippedSize : Float.MAX_VALUE);
1715 }
1716
1717 // Clip the next tab where this tab begins.
1718 if (i > 0) {
1719 LayoutTab nextLayoutTab = mStackTabs[i - 1].getLayoutTab();
1720 if (nextLayoutTab.getScale() <= layoutTab.getScale()) {
1721 clipOffset = tabOffset;
1722 } else {
1723 clipOffset = tabOffset + tabClippedSize * layoutTab.getScale ();
1724 }
1725
1726 // Extend the border just a little bit. Otherwise, the
1727 // rounded borders will intersect and make it look like the
1728 // content is actually smaller.
1729 clipOffset += borderAdjustmentSize;
1730
1731 if (layoutTab.getBorderAlpha() < 1.f && layoutTab.getToolbarAlph a() < 1.f) {
1732 clipOffset += insetBorderPadding;
1733 }
1734 }
1735 }
1736 }
1737
1738 /**
1739 * ComputeTabPosition pass 6:
1740 * Updates the visibility sorting value to use to figure out which thumbnail s to load.
1741 *
1742 * @param stackRect The frame of the stack.
1743 */
1744 private void computeTabVisibilitySortingHelper(RectF stackRect) {
1745 int referenceIndex = mReferenceOrderIndex;
1746 if (referenceIndex == -1) {
1747 int centerIndex =
1748 getTabIndexAtPositon(mLayout.getWidth() / 2.0f, mLayout.getH eight() / 2.0f);
1749 // Alter the center to take into account the scrolling direction.
1750 if (mCurrentScrollDirection > 0) centerIndex++;
1751 if (mCurrentScrollDirection < 0) centerIndex--;
1752 referenceIndex = MathUtils.clamp(centerIndex, 0, mStackTabs.length - 1);
1753 }
1754
1755 final float width = mLayout.getWidth();
1756 final float height = mLayout.getHeight();
1757 final float left = MathUtils.clamp(stackRect.left, 0, width);
1758 final float right = MathUtils.clamp(stackRect.right, 0, width);
1759 final float top = MathUtils.clamp(stackRect.top, 0, height);
1760 final float bottom = MathUtils.clamp(stackRect.bottom, 0, height);
1761 final float stackArea = (right - left) * (bottom - top);
1762 final float layoutArea = Math.max(width * height, 1.0f);
1763 final float stackVisibilityMultiplier = stackArea / layoutArea;
1764
1765 for (int i = 0; i < mStackTabs.length; i++) {
1766 mStackTabs[i].updateStackVisiblityValue(stackVisibilityMultiplier);
1767 mStackTabs[i].updateVisiblityValue(referenceIndex);
1768 }
1769 }
1770
1771 /**
1772 * Determine the current amount of overscroll. If the value is 0, there is
1773 * no overscroll. If the value is < 0, tabs are overscrolling towards the
1774 * top or or left. If the value is > 0, tabs are overscrolling towards the
1775 * bottom or right.
1776 */
1777 private float computeOverscrollPercent() {
1778 if (mOverScrollOffset >= 0) {
1779 return mOverScrollOffset / mMaxOverScroll;
1780 } else {
1781 return mOverScrollOffset / mMaxUnderScroll;
1782 }
1783 }
1784
1785 /**
1786 * ComputeTabPosition pass 4:
1787 * Update the tilt of each tab.
1788 *
1789 * @param time The current time of the app in ms.
1790 * @param stackRect The frame of the stack.
1791 */
1792 private void computeTabTiltHelper(long time, RectF stackRect) {
1793 final boolean portrait = mCurrentMode == Orientation.PORTRAIT;
1794 final float parentWidth = stackRect.width();
1795 final float parentHeight = stackRect.height();
1796 final float overscrollPercent = computeOverscrollPercent();
1797
1798 // All the animations that sets the tilt value must be listed here.
1799 if (mOverviewAnimationType == OverviewAnimationType.START_PINCH
1800 || mOverviewAnimationType == OverviewAnimationType.DISCARD
1801 || mOverviewAnimationType == OverviewAnimationType.FULL_ROLL
1802 || mOverviewAnimationType == OverviewAnimationType.TAB_FOCUSED
1803 || mOverviewAnimationType == OverviewAnimationType.UNDISCARD
1804 || mOverviewAnimationType == OverviewAnimationType.DISCARD_ALL) {
1805 // Let the animation handle setting tilt values
1806 } else if (mPinch0TabIndex >= 0 || overscrollPercent == 0.0f
1807 || mOverviewAnimationType == OverviewAnimationType.REACH_TOP) {
1808 // Keep tabs flat during pinch
1809 for (int i = 0; i < mStackTabs.length; ++i) {
1810 StackTab stackTab = mStackTabs[i];
1811 LayoutTab layoutTab = stackTab.getLayoutTab();
1812 layoutTab.setTiltX(0, 0);
1813 layoutTab.setTiltY(0, 0);
1814 }
1815 } else if (overscrollPercent < 0) {
1816 if (mOverScrollCounter >= OVERSCROLL_FULL_ROLL_TRIGGER) {
1817 startAnimation(time, OverviewAnimationType.FULL_ROLL);
1818 mOverScrollCounter = 0;
1819 // Remove overscroll so when the animation finishes the overscro ll won't
1820 // be bothering.
1821 setScrollTarget(
1822 MathUtils.clamp(mScrollOffset, getMinScroll(false), getM axScroll(false)),
1823 false);
1824 } else {
1825 // Handle tilting tabs backwards (top or left of the tab goes aw ay
1826 // from the camera). Each tab pivots the same amount around the
1827 // same point on the screen. The pivot point is the middle of th e
1828 // top tab.
1829
1830 float tilt = 0;
1831 if (overscrollPercent < -OVERSCROLL_TOP_SLIDE_PCTG) {
1832 // Start tilting tabs after they're done sliding together.
1833 float scaledOverscroll = (overscrollPercent + OVERSCROLL_TOP _SLIDE_PCTG)
1834 / (1 - OVERSCROLL_TOP_SLIDE_PCTG);
1835 tilt = mUnderScrollAngleInterpolator.getInterpolation(-scale dOverscroll)
1836 * -mMaxOverScrollAngle * BACKWARDS_TILT_SCALE;
1837 }
1838
1839 float pivotOffset = 0;
1840 LayoutTab topTab = mStackTabs[mStackTabs.length - 1].getLayoutTa b();
1841 pivotOffset = portrait ? topTab.getScaledContentHeight() / 2 + t opTab.getY()
1842 : topTab.getScaledContentWidth() / 2 + to pTab.getX();
1843
1844 for (int i = 0; i < mStackTabs.length; ++i) {
1845 StackTab stackTab = mStackTabs[i];
1846 LayoutTab layoutTab = stackTab.getLayoutTab();
1847 if (portrait) {
1848 layoutTab.setTiltX(tilt, pivotOffset - layoutTab.getY()) ;
1849 } else {
1850 layoutTab.setTiltY(LocalizationUtils.isLayoutRtl() ? -ti lt : tilt,
1851 pivotOffset - layoutTab.getX());
1852 }
1853 }
1854 }
1855 } else {
1856 // Handle tilting tabs forwards (top or left of the tab comes
1857 // towards the camera). Each tab pivots around a point 1/3 of the
1858 // way down from the top/left of itself. The angle angle is scaled
1859 // based on its distance away from the top/left.
1860
1861 float tilt = mOverScrollAngleInterpolator.getInterpolation(overscrol lPercent)
1862 * mMaxOverScrollAngle;
1863 float offset = mOverscrollSlideInterpolator.getInterpolation(overscr ollPercent)
1864 * mMaxOverScrollSlide;
1865
1866 for (int i = 0; i < mStackTabs.length; ++i) {
1867 StackTab stackTab = mStackTabs[i];
1868 LayoutTab layoutTab = stackTab.getLayoutTab();
1869 if (portrait) {
1870 // portrait LTR & RTL
1871 float adjust = MathUtils.clamp((layoutTab.getY() / parentHei ght) + 0.50f, 0, 1);
1872 layoutTab.setTiltX(tilt * adjust, layoutTab.getScaledContent Height() / 3);
1873 layoutTab.setY(layoutTab.getY() + offset);
1874 } else if (LocalizationUtils.isLayoutRtl()) {
1875 // landscape RTL
1876 float adjust = MathUtils.clamp(-(layoutTab.getX() / parentWi dth) + 0.50f, 0, 1);
1877 layoutTab.setTiltY(-tilt * adjust, layoutTab.getScaledConten tWidth() * 2 / 3);
1878 layoutTab.setX(layoutTab.getX() - offset);
1879 } else {
1880 // landscape LTR
1881 float adjust = MathUtils.clamp((layoutTab.getX() / parentWid th) + 0.50f, 0, 1);
1882 layoutTab.setTiltY(tilt * adjust, layoutTab.getScaledContent Width() / 3);
1883 layoutTab.setX(layoutTab.getX() + offset);
1884 }
1885 }
1886 }
1887 }
1888
1889 /**
1890 * Computes the {@link LayoutTab} position from the stack and the stackTab d ata.
1891 *
1892 * @param time The current time of the app in ms.
1893 * @param stackRect The rectangle the stack should be drawn into. It may cha nge over frames.
1894 */
1895 public void computeTabPosition(long time, RectF stackRect) {
1896 if (mStackTabs == null || mStackTabs.length == 0) return;
1897
1898 if (!mRecomputePosition) return;
1899 mRecomputePosition = false;
1900
1901 // Step 1: Updates the {@link LayoutTab} scale, alpha and depth values.
1902 computeTabScaleAlphaDepthHelper(stackRect);
1903
1904 // Step 2: Fix tab scroll offsets to avoid gaps.
1905 computeTabScrollOffsetHelper();
1906
1907 // Step 3: Compute the actual position.
1908 computeTabOffsetHelper(stackRect);
1909
1910 // Step 4: Update the tilt of each tab.
1911 computeTabTiltHelper(time, stackRect);
1912
1913 // Step 5: Clipping, visibility and adjust overall alpha.
1914 computeTabClippingVisibilityHelper();
1915
1916 // Step 6: Update visibility sorting for prioritizing thumbnail texture request.
1917 computeTabVisibilitySortingHelper(stackRect);
1918 }
1919
1920 /**
1921 * @param stackFocus The current amount of focus of the stack [0 .. 1]
1922 * @param orderIndex The index in the stack of the focused tab. -1 to ask th e
1923 * stack to compute it.
1924 */
1925 public void setStackFocusInfo(float stackFocus, int orderIndex) {
1926 if (mStackTabs == null) return;
1927 mReferenceOrderIndex = orderIndex;
1928 for (int i = 0; i < mStackTabs.length; i++) {
1929 mStackTabs[i].getLayoutTab().setBorderCloseButtonAlpha(stackFocus);
1930 }
1931 }
1932
1933 /**
1934 * Reverts the closure of the tab specified by {@code tabId}. This will run an undiscard
1935 * animation on that tab.
1936 * @param time The current time of the app in ms.
1937 * @param tabId The id of the tab to animate.
1938 */
1939 public void undoClosure(long time, int tabId) {
1940 createStackTabs(true);
1941 if (mStackTabs == null) return;
1942
1943 for (int i = 0; i < mStackTabs.length; i++) {
1944 StackTab tab = mStackTabs[i];
1945
1946 if (tab.getId() == tabId) {
1947 tab.setDiscardAmount(getDiscardRange());
1948 tab.setDying(false);
1949 tab.getLayoutTab().setMaxContentHeight(mLayout.getHeightMinusTop Controls());
1950 }
1951 }
1952
1953 mSpacing = computeSpacing(mStackTabs.length);
1954 startAnimation(time, OverviewAnimationType.UNDISCARD);
1955 }
1956
1957 /**
1958 * Creates the {@link StackTab}s needed for display and populates {@link #mS tackTabs}.
1959 * It is called from show() at the beginning of every new draw phase. It tri es to reuse old
1960 * {@link StackTab} instead of creating new ones every time.
1961 * @param restoreState Whether or not to restore the {@link LayoutTab} state when we rebuild the
1962 * {@link StackTab}s. There are some properties like ma ximum content size
1963 * or whether or not to show the toolbar that might have to be restored if
1964 * we're calling this while the switcher is already visi ble.
1965 */
1966 private void createStackTabs(boolean restoreState) {
1967 final int count = mTabModel.getCount();
1968 if (count == 0) {
1969 cleanupTabs();
1970 } else {
1971 StackTab[] oldTabs = mStackTabs;
1972 mStackTabs = new StackTab[count];
1973
1974 final boolean isIncognito = mTabModel.isIncognito();
1975 final boolean needTitle = !mLayout.isHiding();
1976 for (int i = 0; i < count; ++i) {
1977 Tab tab = mTabModel.getTabAt(i);
1978 int tabId = tab != null ? tab.getId() : Tab.INVALID_TAB_ID;
1979 mStackTabs[i] = findTabById(oldTabs, tabId);
1980
1981 float maxContentWidth = -1.f;
1982 float maxContentHeight = -1.f;
1983
1984 if (mStackTabs[i] != null && mStackTabs[i].getLayoutTab() != nul l && restoreState) {
1985 maxContentWidth = mStackTabs[i].getLayoutTab().getMaxContent Width();
1986 maxContentHeight = mStackTabs[i].getLayoutTab().getMaxConten tHeight();
1987 }
1988
1989 LayoutTab layoutTab = mLayout.createLayoutTab(tabId, isIncognito ,
1990 Layout.SHOW_CLOSE_BUTTON, needTitle, maxContentWidth, ma xContentHeight);
1991 layoutTab.setInsetBorderVertical(true);
1992 layoutTab.setShowToolbar(true);
1993 layoutTab.setToolbarAlpha(0.f);
1994 layoutTab.setAnonymizeToolbar(mTabModel.index() != i);
1995
1996 if (mStackTabs[i] == null) {
1997 mStackTabs[i] = new StackTab(layoutTab);
1998 } else {
1999 mStackTabs[i].setLayoutTab(layoutTab);
2000 }
2001
2002 mStackTabs[i].setNewIndex(i);
2003 // The initial enterStack animation will take care of
2004 // positioning, scaling, etc.
2005 }
2006 }
2007 }
2008
2009 private StackTab findTabById(StackTab[] layoutTabs, int id) {
2010 if (layoutTabs == null) return null;
2011 final int count = layoutTabs.length;
2012 for (int i = 0; i < count; i++) {
2013 if (layoutTabs[i].getId() == id) return layoutTabs[i];
2014 }
2015 return null;
2016 }
2017
2018 /**
2019 * Creates a {@link StackTab}.
2020 * This function should ONLY be called from {@link #tabCreated(long, int)} a nd nowhere else.
2021 *
2022 * @param id The id of the tab.
2023 * @return Whether the tab has successfully been created and added.
2024 */
2025 private boolean createTabHelper(int id) {
2026 if (TabModelUtils.getTabById(mTabModel, id) == null) return false;
2027
2028 // Check to see if the tab already exists in our model. This is
2029 // just to cover the case where stackEntered and then tabCreated()
2030 // called in a row.
2031 if (mStackTabs != null) {
2032 final int count = mStackTabs.length;
2033 for (int i = 0; i < count; ++i) {
2034 if (mStackTabs[i].getId() == id) {
2035 return false;
2036 }
2037 }
2038 }
2039
2040 createStackTabs(true);
2041
2042 return true;
2043 }
2044
2045 private int computeSpacing(int layoutTabCount) {
2046 // This redetermines the proper spacing for the {@link StackTab}. It ta kes in
2047 // a parameter for the size instead of using the mStackTabs.length
2048 // property because we could be setting the spacing for a delete
2049 // before the tab has been removed (will help with animations).
2050 int spacing = 0;
2051 if (layoutTabCount > 1) {
2052 final float dimension = getScrollDimensionSize();
2053 int minSpacing = (int) Math.max(dimension * SPACING_SCREEN, mMinSpac ing);
2054 if (mStackTabs != null) {
2055 for (int i = 0; i < mStackTabs.length; i++) {
2056 assert mStackTabs[i] != null;
2057 if (!mStackTabs[i].isDying()) {
2058 minSpacing = (int) Math.min(
2059 minSpacing, mStackTabs[i].getSizeInScrollDirecti on(mCurrentMode));
2060 }
2061 }
2062 }
2063 spacing = (int) ((dimension - 20) / (layoutTabCount * .8f));
2064 spacing = Math.max(spacing, minSpacing);
2065 }
2066 return spacing;
2067 }
2068
2069 private float getStackScale(RectF stackRect) {
2070 return mCurrentMode == Orientation.PORTRAIT
2071 ? stackRect.width() / mLayout.getWidth()
2072 : stackRect.height() / mLayout.getHeightMinusTopControls();
2073 }
2074
2075 private void setScrollTarget(float offset, boolean immediate) {
2076 // Ensure that the stack cannot be scrolled too far in either direction.
2077 // mScrollOffset is clamped between [-min, 0], where offset 0 has the
2078 // farthest back tab (the first tab) at the top, with everything else
2079 // pulled down, and -min has the tab at the top of the stack (the last
2080 // tab) is pulled up and fully visible.
2081 final boolean overscroll = allowOverscroll();
2082 mScrollTarget = MathUtils.clamp(offset, getMinScroll(overscroll), getMax Scroll(overscroll));
2083 if (immediate) mScrollOffset = mScrollTarget;
2084 mCurrentScrollDirection = Math.signum(mScrollTarget - mScrollOffset);
2085 }
2086
2087 private float getMinScroll(boolean allowUnderScroll) {
2088 float maxOffset = 0;
2089 if (mStackTabs != null) {
2090 // The tabs are not always ordered so we need to browse them all.
2091 for (int i = 0; i < mStackTabs.length; i++) {
2092 if (!mStackTabs[i].isDying() && mStackTabs[i].getLayoutTab().isV isible()) {
2093 maxOffset = Math.max(mStackTabs[i].getScrollOffset(), maxOff set);
2094 }
2095 }
2096 }
2097 return (allowUnderScroll ? -mMaxUnderScroll : 0) - maxOffset;
2098 }
2099
2100 /**
2101 * Gets the max scroll value.
2102 *
2103 * @param allowOverscroll True if overscroll is allowed.
2104 */
2105 private float getMaxScroll(boolean allowOverscroll) {
2106 if (mStackTabs == null || !allowOverscroll) {
2107 return 0;
2108 } else {
2109 return mMaxOverScroll;
2110 }
2111 }
2112
2113 private void stopScrollingMovement(long time) {
2114 // We have to cancel the fling if it is in progress.
2115 if (mScroller.computeScrollOffset(time)) {
2116 // Set the current offset and target to the current scroll
2117 // position so the {@link StackTab}s won't scroll anymore.
2118 setScrollTarget(mScroller.getCurrY(), true /* immediate */);
2119
2120 // Tell the scroller to finish scrolling.
2121 mScroller.forceFinished(true);
2122 } else {
2123 // If we aren't scrolling just set the target to the current
2124 // offset so we don't move anymore.
2125 setScrollTarget(mScrollOffset, false);
2126 }
2127 }
2128
2129 private boolean allowOverscroll() {
2130 // All the animations that want to leave the tilt value to be set by the overscroll must
2131 // be added here.
2132 return (mOverviewAnimationType == OverviewAnimationType.NONE
2133 || mOverviewAnimationType == OverviewAnimationType.VIEW_M ORE
2134 || mOverviewAnimationType == OverviewAnimationType.ENTER_ STACK)
2135 && mPinch0TabIndex < 0;
2136 }
2137
2138 /**
2139 * Smoothes input signal. The definition of the input is lower than the
2140 * pixel density of the screen so we need to smooth the input to give the il lusion of smooth
2141 * animation on screen from chunky inputs.
2142 * The combination of 20 pixels and 0.9f ensures that the output is not more than 2 pixels away
2143 * from the target.
2144 * TODO: This has nothing to do with time, just draw rate.
2145 * Is this okay or do we want to have the interpolation based on the t ime elapsed?
2146 * @param current The current value of the signal.
2147 * @param input The raw input value.
2148 * @return The smoothed signal.
2149 */
2150 private float smoothInput(float current, float input) {
2151 current = MathUtils.clamp(current, input - 20, input + 20);
2152 return MathUtils.interpolate(current, input, 0.9f);
2153 }
2154
2155 private void forceScrollStop() {
2156 mScroller.forceFinished(true);
2157 updateOverscrollOffset();
2158 mScrollTarget = mScrollOffset;
2159 }
2160
2161 private void updateScrollOffset(long time) {
2162 // If we are still scrolling, which is determined by a disparity
2163 // between our scroll offset and our scroll target, we need
2164 // to try to move closer to that position.
2165 if (mScrollOffset != mScrollTarget) {
2166 if (mScroller.computeScrollOffset(time)) {
2167 final float newScrollOffset = mScroller.getCurrY();
2168 evenOutTabs(newScrollOffset - mScrollOffset, true);
2169 // We are currently in the process of being flinged. Just
2170 // ask the scroller for the new position.
2171 mScrollOffset = newScrollOffset;
2172 } else {
2173 // We are just being dragged or scrolled, not flinged. This
2174 // means we should move closer to our target quickly but not
2175 // quickly enough to show the stuttering that could be
2176 // exposed by the touch event rate.
2177 mScrollOffset = smoothInput(mScrollOffset, mScrollTarget);
2178 }
2179 requestUpdate();
2180 } else {
2181 // Make sure that the scroller is marked as finished when the destin ation is reached.
2182 mScroller.forceFinished(true);
2183 }
2184 updateOverscrollOffset();
2185 }
2186
2187 private void updateOverscrollOffset() {
2188 float clamped = MathUtils.clamp(mScrollOffset, getMinScroll(false), getM axScroll(false));
2189 if (!allowOverscroll()) {
2190 mScrollOffset = clamped;
2191 }
2192 float overscroll = mScrollOffset - clamped;
2193
2194 // Counts the number of overscroll push in the same direction in a row.
2195 int derivativeState = (int) Math.signum(Math.abs(mOverScrollOffset) - Ma th.abs(overscroll));
2196 if (derivativeState != mOverScrollDerivative && derivativeState == 1 && overscroll < 0) {
2197 mOverScrollCounter++;
2198 } else if (overscroll > 0 || mCurrentMode == Orientation.LANDSCAPE) {
2199 mOverScrollCounter = 0;
2200 }
2201 mOverScrollDerivative = derivativeState;
2202
2203 mOverScrollOffset = overscroll;
2204 }
2205
2206 private void resetAllScrollOffset() {
2207 if (mTabModel == null) return;
2208 // Reset the scroll position to put the important {@link StackTab} into focus.
2209 // This does not scroll the {@link StackTab}s there but rather moves eve rything
2210 // there immediately.
2211 // The selected tab is supposed to show at the center of the screen.
2212 float maxTabsPerPage = getScrollDimensionSize() / mSpacing;
2213 float centerOffsetIndex = maxTabsPerPage / 2.0f - 0.5f;
2214 final int count = mTabModel.getCount();
2215 final int index = mTabModel.index();
2216 if (index < centerOffsetIndex || count <= maxTabsPerPage) {
2217 mScrollOffset = 0;
2218 } else if (index == count - 1 && Math.ceil(maxTabsPerPage) < count) {
2219 mScrollOffset = (maxTabsPerPage - count - 1) * mSpacing;
2220 } else if ((count - index - 1) < centerOffsetIndex) {
2221 mScrollOffset = (maxTabsPerPage - count) * mSpacing;
2222 } else {
2223 mScrollOffset = (centerOffsetIndex - index) * mSpacing;
2224 }
2225 // Reset the scroll offset of the tabs too.
2226 if (mStackTabs != null) {
2227 for (int i = 0; i < mStackTabs.length; i++) {
2228 mStackTabs[i].setScrollOffset(screenToScroll(i * mSpacing));
2229 }
2230 }
2231 setScrollTarget(mScrollOffset, false);
2232 }
2233
2234 private float approxScreen(StackTab tab, float globalScrollOffset) {
2235 return StackTab.scrollToScreen(tab.getScrollOffset() + globalScrollOffse t, mWarpSize);
2236 }
2237
2238 private float scrollToScreen(float scrollSpace) {
2239 return StackTab.scrollToScreen(scrollSpace, mWarpSize);
2240 }
2241
2242 private float screenToScroll(float screenSpace) {
2243 return StackTab.screenToScroll(screenSpace, mWarpSize);
2244 }
2245
2246 /**
2247 * @return The range of the discard action. At the end of the +/- range the discarded tab
2248 * will be fully transparent.
2249 */
2250 private float getDiscardRange() {
2251 return getRange(DISCARD_RANGE_SCREEN);
2252 }
2253
2254 private float getRange(float range) {
2255 return range * (mCurrentMode == Orientation.PORTRAIT ? mLayout.getWidth( )
2256 : mLayout.getHeight MinusTopControls());
2257 }
2258
2259 /**
2260 * Computes the scale of the tab based on its discard status.
2261 *
2262 * @param amount The discard amount.
2263 * @param range The range of the absolute value of discard amount.
2264 * @param fromClick Whether or not the discard was from a click or a swipe.
2265 * @return The scale of the tab to use to draw the tab.
2266 */
2267 public static float computeDiscardScale(float amount, float range, boolean f romClick) {
2268 if (Math.abs(amount) < 1.0f) return 1.0f;
2269 float t = amount / range;
2270 float endScale = fromClick ? DISCARD_END_SCALE_CLICK : DISCARD_END_SCALE _SWIPE;
2271 return MathUtils.interpolate(1.0f, endScale, Math.abs(t));
2272 }
2273
2274 /**
2275 * Computes the alpha value of the tab based on its discard status.
2276 *
2277 * @param amount The discard amount.
2278 * @param range The range of the absolute value of discard amount.
2279 * @return The alpha value that need to be applied on the tab.
2280 */
2281 public static float computeDiscardAlpha(float amount, float range) {
2282 if (Math.abs(amount) < 1.0f) return 1.0f;
2283 float t = amount / range;
2284 t = MathUtils.clamp(t, -1.0f, 1.0f);
2285 return 1.f - Math.abs(t);
2286 }
2287
2288 private void updateCurrentMode(int orientation) {
2289 mCurrentMode = orientation;
2290 mDiscardDirection = getDefaultDiscardDirection();
2291 setWarpState(true, false);
2292 final float opaqueTopPadding = mBorderTopPadding - mBorderTransparentTop ;
2293 mAnimationFactory = StackAnimation.createAnimationFactory(mLayout.getWid th(),
2294 mLayout.getHeight(), mLayout.getHeightMinusTopControls(), mBorde rTopPadding,
2295 opaqueTopPadding, mBorderLeftPadding, mCurrentMode);
2296 float dpToPx = mLayout.getContext().getResources().getDisplayMetrics().d ensity;
2297 mViewAnimationFactory = new StackViewAnimation(dpToPx, mLayout.getWidth( ));
2298 if (mStackTabs == null) return;
2299 float width = mLayout.getWidth();
2300 float height = mLayout.getHeightMinusTopControls();
2301 for (int i = 0; i < mStackTabs.length; i++) {
2302 LayoutTab tab = mStackTabs[i].getLayoutTab();
2303 if (tab == null) continue;
2304 tab.setMaxContentWidth(width);
2305 tab.setMaxContentHeight(height);
2306 }
2307 }
2308
2309 /**
2310 * Called to release everything. Called well after the view has been really hidden.
2311 */
2312 public void cleanupTabs() {
2313 mStackTabs = null;
2314 resetInputActionIndices();
2315 }
2316
2317 /**
2318 * Resets all the indices that are pointing to tabs for various features.
2319 */
2320 private void resetInputActionIndices() {
2321 mPinch0TabIndex = -1;
2322 mPinch1TabIndex = -1;
2323 mScrollingTab = null;
2324 mDiscardingTab = null;
2325 mLongPressSelected = -1;
2326 }
2327
2328 /**
2329 * Invalidates the current graphics and force to recomputes tab placements.
2330 */
2331 public void requestUpdate() {
2332 mRecomputePosition = true;
2333 mLayout.requestUpdate();
2334 }
2335
2336 /**
2337 * Reset session based parameters.
2338 * Called before the a session starts. Before the show, regardless if the st ack is displayable.
2339 */
2340 public void reset() {
2341 mIsDying = false;
2342 }
2343
2344 /**
2345 * Whether or not the tab positions warp from linear to nonlinear as the tab s approach the edge
2346 * of the screen. This allows us to move the tabs to linear space to track finger movements,
2347 * but also move them back to non-linear space without any visible change to the user.
2348 * @param canWarp Whether or not the tabs are allowed to warp.
2349 * @param adjustCurrentTabs Whether or not to change the tab positions so th ere's no visible
2350 * difference after the change.
2351 */
2352 private void setWarpState(boolean canWarp, boolean adjustCurrentTabs) {
2353 float warp = canWarp ? getScrollDimensionSize() * SCROLL_WARP_PCTG : 0.f ;
2354
2355 if (mStackTabs != null && adjustCurrentTabs && Float.compare(warp, mWarp Size) != 0) {
2356 float scrollOffset =
2357 MathUtils.clamp(mScrollOffset, getMinScroll(false), getMaxSc roll(false));
2358 for (int i = 0; i < mStackTabs.length; i++) {
2359 StackTab tab = mStackTabs[i];
2360 float tabScrollOffset = tab.getScrollOffset();
2361 float tabScrollSpace = tabScrollOffset + scrollOffset;
2362 float tabScreen = StackTab.scrollToScreen(tabScrollSpace, mWarpS ize);
2363 float tabScrollSpaceFinal = StackTab.screenToScroll(tabScreen, w arp);
2364 float scrollDelta = tabScrollSpaceFinal - tabScrollSpace;
2365 tab.setScrollOffset(tabScrollOffset + scrollDelta);
2366 }
2367 }
2368
2369 mWarpSize = warp;
2370 }
2371
2372 /**
2373 * Called when the swipe animation get initiated. It gives a chance to initi alize everything.
2374 * @param time The current time of the app in ms.
2375 * @param direction The direction the swipe is in.
2376 * @param x The horizontal coordinate the swipe started at in dp.
2377 * @param y The vertical coordinate the swipe started at in dp.
2378 */
2379 public void swipeStarted(long time, ScrollDirection direction, float x, floa t y) {
2380 if (direction != ScrollDirection.DOWN) return;
2381
2382 // Turn off warping the tabs because we need them to track the user's fi nger.
2383 setWarpState(false, false);
2384
2385 // Restart the enter stack animation with the new warp values.
2386 startAnimation(time, OverviewAnimationType.ENTER_STACK);
2387
2388 // Update the scroll offset to put the focused tab at the top.
2389 final int index = mTabModel.index();
2390
2391 if (mCurrentMode == Orientation.PORTRAIT) {
2392 mScrollOffset = -index * mSpacing;
2393 } else {
2394 mScrollOffset = -index * mSpacing + x - LANDSCAPE_SWIPE_DRAG_TAB_OFF SET_DP;
2395 mScrollOffset =
2396 MathUtils.clamp(mScrollOffset, getMinScroll(false), getMaxSc roll(false));
2397 }
2398 setScrollTarget(mScrollOffset, true);
2399
2400 // Don't let the tabs even out during this scroll.
2401 mEvenOutProgress = 1.f;
2402
2403 // Set up the tracking scroll parameters.
2404 mSwipeUnboundScrollOffset = mScrollOffset;
2405 mSwipeBoundedScrollOffset = mScrollOffset;
2406
2407 // Reset other state.
2408 mSwipeIsCancelable = false;
2409 mSwipeCanScroll = false;
2410 mInSwipe = true;
2411 }
2412
2413 /**
2414 * Updates a swipe gesture.
2415 * @param time The current time of the app in ms.
2416 * @param x The horizontal coordinate the swipe is currently at in dp.
2417 * @param y The vertical coordinate the swipe is currently at in dp.
2418 * @param dx The horizontal delta since the last update in dp.
2419 * @param dy The vertical delta since the last update in dp.
2420 * @param tx The horizontal difference between the start and the current p osition in dp.
2421 * @param ty The vertical difference between the start and the current pos ition in dp.
2422 */
2423 public void swipeUpdated(long time, float x, float y, float dx, float dy, fl oat tx, float ty) {
2424 if (!mInSwipe) return;
2425
2426 final float toolbarSize = mLayout.getHeight() - mLayout.getHeightMinusTo pControls();
2427 if (ty > toolbarSize) mSwipeCanScroll = true;
2428 if (!mSwipeCanScroll) return;
2429
2430 final int index = mTabModel.index();
2431
2432 // Check to make sure the index is still valid.
2433 if (index < 0 || index >= mStackTabs.length) {
2434 assert false : "Tab index out of bounds in Stack#swipeUpdated()";
2435 return;
2436 }
2437
2438 final float delta = mCurrentMode == Orientation.PORTRAIT ? dy : dx;
2439
2440 // Update the unbound scroll offset, tracking delta regardless of constr aints.
2441 mSwipeUnboundScrollOffset += delta;
2442
2443 // Figure out the new constrained position.
2444 final float minScroll = getMinScroll(true);
2445 final float maxScroll = getMaxScroll(true);
2446 float offset = MathUtils.clamp(mSwipeUnboundScrollOffset, minScroll, max Scroll);
2447
2448 final float constrainedDelta = offset - mSwipeBoundedScrollOffset;
2449 mSwipeBoundedScrollOffset = offset;
2450
2451 if (constrainedDelta == 0.f) return;
2452
2453 if (mCurrentMode == Orientation.PORTRAIT) {
2454 dy = constrainedDelta;
2455 } else {
2456 dx = constrainedDelta;
2457 }
2458
2459 // Propagate the new drag event.
2460 drag(time, x, y, dx, dy);
2461
2462 // Figure out if the user has scrolled down enough that they can scroll back up and exit.
2463 if (mCurrentMode == Orientation.PORTRAIT) {
2464 // The cancelable threshold is determined by the top position of the tab in the stack.
2465 final float discardOffset = mStackTabs[index].getScrollOffset();
2466 final boolean beyondThreshold = -mScrollOffset < discardOffset;
2467
2468 // Allow the user to cancel in the future if they're beyond the thre shold.
2469 mSwipeIsCancelable |= beyondThreshold;
2470
2471 // If the user can cancel the swipe and they're back behind the thre shold, cancel.
2472 if (mSwipeIsCancelable && !beyondThreshold) swipeCancelled(time);
2473 } else {
2474 // The cancelable threshold is determined by the top position of the tab.
2475 final float discardOffset = mStackTabs[index].getLayoutTab().getY();
2476
2477 boolean aboveThreshold = discardOffset < getRange(SWIPE_LANDSCAPE_TH RESHOLD);
2478
2479 mSwipeIsCancelable |= !aboveThreshold;
2480
2481 if (mSwipeIsCancelable && aboveThreshold) swipeCancelled(time);
2482 }
2483 }
2484
2485 /**
2486 * Called when the swipe ends; most likely on finger up event. It gives a ch ance to start
2487 * an ending animation to exit the mode gracefully.
2488 * @param time The current time of the app in ms.
2489 */
2490 public void swipeFinished(long time) {
2491 if (!mInSwipe) return;
2492
2493 mInSwipe = false;
2494
2495 // Reset the warp state and mark the tabs to even themselves out.
2496 setWarpState(true, true);
2497 mEvenOutProgress = 0.f;
2498
2499 onUpOrCancel(time);
2500 }
2501
2502 /**
2503 * Called when the user has cancelled a swipe; most likely if they have drag ged their finger
2504 * back to the starting position. Some handlers will throw swipeFinished() instead.
2505 * @param time The current time of the app in ms.
2506 */
2507 public void swipeCancelled(long time) {
2508 if (!mInSwipe) return;
2509
2510 mDiscardingTab = null;
2511
2512 mInSwipe = false;
2513
2514 setWarpState(true, true);
2515 mEvenOutProgress = 0.f;
2516
2517 // Select the current tab so we exit the switcher.
2518 Tab tab = TabModelUtils.getCurrentTab(mTabModel);
2519 mLayout.uiSelectingTab(time, tab != null ? tab.getId() : Tab.INVALID_TAB _ID);
2520 }
2521
2522 /**
2523 * Fling from a swipe gesture.
2524 * @param time The current time of the app in ms.
2525 * @param x The horizontal coordinate the swipe is currently at in dp.
2526 * @param y The vertical coordinate the swipe is currently at in dp.
2527 * @param tx The horizontal difference between the start and the current p osition in dp.
2528 * @param ty The vertical difference between the start and the current pos ition in dp.
2529 * @param vx The horizontal velocity of the fling.
2530 * @param vy The vertical velocity of the fling.
2531 */
2532 public void swipeFlingOccurred(
2533 long time, float x, float y, float tx, float ty, float vx, float vy) {
2534 if (!mInSwipe) return;
2535
2536 // Propagate the fling data.
2537 fling(time, x, y, vx, vy);
2538
2539 onUpOrCancel(time);
2540 }
2541 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698