Index: chrome/android/java_staging/src/org/chromium/chrome/browser/compositor/layouts/phone/stack/Stack.java |
diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/compositor/layouts/phone/stack/Stack.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/compositor/layouts/phone/stack/Stack.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..2953cace28c9181e456f4ebb9b250d85d7c142a3 |
--- /dev/null |
+++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/compositor/layouts/phone/stack/Stack.java |
@@ -0,0 +1,2541 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.chrome.browser.compositor.layouts.phone.stack; |
+ |
+import android.animation.Animator; |
+import android.animation.AnimatorListenerAdapter; |
+import android.animation.AnimatorSet; |
+import android.content.Context; |
+import android.content.res.Resources; |
+import android.graphics.RectF; |
+import android.view.animation.AccelerateDecelerateInterpolator; |
+import android.view.animation.Interpolator; |
+ |
+import com.google.android.apps.chrome.R; |
+ |
+import org.chromium.base.annotations.SuppressFBWarnings; |
+import org.chromium.base.metrics.RecordUserAction; |
+import org.chromium.chrome.browser.Tab; |
+import org.chromium.chrome.browser.compositor.layouts.ChromeAnimation; |
+import org.chromium.chrome.browser.compositor.layouts.Layout; |
+import org.chromium.chrome.browser.compositor.layouts.Layout.Orientation; |
+import org.chromium.chrome.browser.compositor.layouts.components.LayoutTab; |
+import org.chromium.chrome.browser.compositor.layouts.eventfilter.EdgeSwipeEventFilter.ScrollDirection; |
+import org.chromium.chrome.browser.compositor.layouts.phone.StackLayout; |
+import org.chromium.chrome.browser.compositor.layouts.phone.stack.StackAnimation.OverviewAnimationType; |
+import org.chromium.chrome.browser.tabmodel.TabModel; |
+import org.chromium.chrome.browser.tabmodel.TabModelUtils; |
+import org.chromium.chrome.browser.util.MathUtils; |
+import org.chromium.ui.base.LocalizationUtils; |
+ |
+/** |
+ * Handles all the drawing and events of a stack of stackTabs. |
+ * |
+ * @VisibleForTesting |
+ */ |
+public class Stack { |
+ public static final int MAX_NUMBER_OF_STACKED_TABS_TOP = 3; |
+ public static final int MAX_NUMBER_OF_STACKED_TABS_BOTTOM = 3; |
+ |
+ private static final float STACK_PORTRAIT_Y_OFFSET_PROPORTION = -0.8f; |
+ private static final float STACK_LANDSCAPE_START_OFFSET_PROPORTION = -0.7f; |
+ private static final float STACK_LANDSCAPE_Y_OFFSET_PROPORTION = -0.5f; |
+ |
+ public enum DragLock { NONE, SCROLL, DISCARD } |
+ |
+ /** |
+ * The percentage of the screen that defines the spacing between tabs by default (no pinch). |
+ */ |
+ public static final float SPACING_SCREEN = 0.26f; |
+ |
+ /** |
+ * The percentage of the screen to cover for the discarded tab to be fully transparent. |
+ */ |
+ public static final float DISCARD_RANGE_SCREEN = 0.7f; |
+ |
+ /** |
+ * The percentage the tab need to be dragged to actually discard the card. |
+ */ |
+ private static final float DISCARD_COMMIT_THRESHOLD = 0.4f; |
+ |
+ /** |
+ * The percentage of the side of the tab that is inactive to swipe to discard. As this is |
+ * a distance computed from both edges, meaningful value ranges in [0 ... 0.5]. |
+ */ |
+ private static final float DISCARD_SAFE_SELECTION_PCTG = 0.1f; |
+ |
+ /** |
+ * The minimum scale the tab can reach when being discarded by a click. |
+ */ |
+ private static final float DISCARD_END_SCALE_CLICK = 0.7f; |
+ |
+ /** |
+ * The minimum scale the tab can reach when being discarded by a swipe. |
+ */ |
+ private static final float DISCARD_END_SCALE_SWIPE = 0.5f; |
+ |
+ /** |
+ * The delta time applied on the velocity from the fling. This is to compute the kick to help |
+ * discarding a card. |
+ */ |
+ private static final float DISCARD_FLING_DT = 1.0f / 45.0f; |
+ |
+ /** |
+ * The maximum contribution of the fling. This is in percentage of the range. |
+ */ |
+ private static final float DISCARD_FLING_MAX_CONTRIBUTION = 0.4f; |
+ |
+ /** |
+ * How much to scale the max overscroll angle when tabs are tilting backwards. |
+ */ |
+ private static final float BACKWARDS_TILT_SCALE = 0.5f; |
+ |
+ /** |
+ * When overscrolling towards the top or left of the screen, what portion of |
+ * the overscroll should be devoted to sliding the tabs together. The rest |
+ * of the overscroll is used for tilting. |
+ */ |
+ private static final float OVERSCROLL_TOP_SLIDE_PCTG = 0.25f; |
+ |
+ /** |
+ * Scale max under/over scroll by this amount when flinging. |
+ */ |
+ private static final float MAX_OVER_FLING_SCALE = 0.5f; |
+ |
+ /** |
+ * mMaxUnderScroll is determined by multing mMaxOverScroll with |
+ * MAX_UNDER_SCROLL_SCALE |
+ */ |
+ private static final float MAX_UNDER_SCROLL_SCALE = 2.0f; |
+ |
+ /** |
+ * Drags that are mostly horizontal (within 30 degrees) signal that |
+ * a user is discarding a tab. |
+ */ |
+ private static final float DRAG_ANGLE_THRESHOLD = (float) Math.tan(Math.toRadians(30.0)); |
+ |
+ /** |
+ * Reset the scroll mode after this number of milliseconds of inactivity or small motions. |
+ */ |
+ private static final long DRAG_TIME_THRESHOLD = 400; |
+ |
+ /** |
+ * Minimum motion threshold to lock the scroll mode. |
+ */ |
+ private static final float DRAG_MOTION_THRESHOLD_DP = 1.25f; |
+ |
+ /** |
+ * The number of attempt to get the full roll overscroll animation. |
+ */ |
+ private static final int OVERSCROLL_FULL_ROLL_TRIGGER = 5; |
+ |
+ /** |
+ * Percentage of the screen to wrap the scroll space. |
+ */ |
+ private static final float SCROLL_WARP_PCTG = 0.4f; |
+ |
+ /** |
+ * Percentage of the screen a swipe gesture must traverse before it is allowed to be canceled. |
+ */ |
+ private static final float SWIPE_LANDSCAPE_THRESHOLD = 0.19f; |
+ |
+ /** |
+ * How far to place the tab to the left of the user's finger when swiping in dp. This keeps the |
+ * tab under the user's finger. |
+ */ |
+ private static final float LANDSCAPE_SWIPE_DRAG_TAB_OFFSET_DP = 40.f; |
+ |
+ // External References |
+ private TabModel mTabModel; |
+ |
+ // True when the stack is still visible for animation but it is going to be empty. |
+ private boolean mIsDying; |
+ |
+ // Screen State Variables |
+ private int mSpacing; |
+ private float mWarpSize; |
+ private StackTab[] mStackTabs; // mStackTabs can be null if there are no tabs |
+ |
+ private int mLongPressSelected = -1; |
+ |
+ // During pinch, the finger the closest to the bottom of the stack changes the scrolling |
+ // and the other finger locally stretches the spacing between the tabs. |
+ private int mPinch0TabIndex = -1; |
+ private int mPinch1TabIndex = -1; |
+ private float mLastPinch0Offset; |
+ private float mLastPinch1Offset; |
+ |
+ // Current progress of the 'even out' phase. This progress as the screen get scrolled. |
+ private float mEvenOutProgress = 1.0f; |
+ // Rate to even out all the tabs. |
+ private float mEvenOutRate = 1.0f; // This will be updated from dimens.xml |
+ |
+ // Overscroll |
+ private StackScroller mScroller; |
+ private float mOverScrollOffset; |
+ private int mOverScrollDerivative; |
+ private int mOverScrollCounter; |
+ private float mMaxOverScroll; // This will be updated from dimens.xml |
+ private float mMaxUnderScroll; |
+ private float mMaxOverScrollAngle; // This will be updated from values.xml |
+ private float mMaxOverScrollSlide; |
+ private final Interpolator mOverScrollAngleInterpolator = |
+ new AccelerateDecelerateInterpolator(); |
+ private final Interpolator mUnderScrollAngleInterpolator = |
+ ChromeAnimation.getDecelerateInterpolator(); |
+ private final Interpolator mOverscrollSlideInterpolator = |
+ new AccelerateDecelerateInterpolator(); |
+ |
+ // Drag Lock |
+ private DragLock mDragLock = DragLock.NONE; |
+ private long mLastScrollUpdate = 0; |
+ private float mMinScrollMotion = 0; |
+ |
+ // Scrolling Variables |
+ private float mScrollTarget = 0; |
+ private float mScrollOffset = 0; |
+ private float mScrollOffsetForDyingTabs = 0; |
+ private float mCurrentScrollDirection = 0.0f; |
+ private StackTab mScrollingTab = null; |
+ |
+ // Swipe Variables |
+ private float mSwipeUnboundScrollOffset; |
+ private float mSwipeBoundedScrollOffset; |
+ private boolean mSwipeIsCancelable; |
+ private boolean mSwipeCanScroll; |
+ private boolean mInSwipe; |
+ |
+ // Discard |
+ private StackTab mDiscardingTab; |
+ |
+ // We can't initialize mDiscardDirection here using LocalizationUtils.isRtl() because it will |
+ // involve a jni call. Instead, mDiscardDirection will be initialized in Show(). |
+ private float mDiscardDirection = Float.NaN; |
+ |
+ private float mMinSpacing; // This will be updated from dimens.xml |
+ |
+ private boolean mRecomputePosition = true; |
+ |
+ private int mReferenceOrderIndex = -1; |
+ |
+ // Orientation Variables |
+ private int mCurrentMode = Orientation.PORTRAIT; |
+ |
+ // Animation Variables |
+ private OverviewAnimationType mOverviewAnimationType = OverviewAnimationType.NONE; |
+ private StackAnimation mAnimationFactory; |
+ private StackViewAnimation mViewAnimationFactory; |
+ |
+ // Running set of animations applied to tabs. |
+ private ChromeAnimation<?> mTabAnimations; |
+ private AnimatorSet mViewAnimations; |
+ |
+ // The parent Layout |
+ private final StackLayout mLayout; |
+ |
+ // Border values |
+ private float mBorderTransparentTop; |
+ private float mBorderTransparentSide; |
+ // TODO(dtrainor): Expose 9-patch padding from resource manager. |
+ private float mBorderTopPadding; |
+ private float mBorderLeftPadding; |
+ |
+ private final AnimatorListenerAdapter mViewAnimatorListener = new AnimatorListenerAdapter() { |
+ @Override |
+ public void onAnimationCancel(Animator animation) { |
+ mLayout.requestUpdate(); |
+ } |
+ |
+ @Override |
+ public void onAnimationEnd(Animator animation) { |
+ mLayout.requestUpdate(); |
+ } |
+ }; |
+ |
+ /** |
+ * @param layout The parent layout. |
+ */ |
+ public Stack(Context context, StackLayout layout) { |
+ mLayout = layout; |
+ contextChanged(context); |
+ } |
+ |
+ /** |
+ * @param tabmodel The model to attach to this stack. |
+ */ |
+ public void setTabModel(TabModel tabmodel) { |
+ mTabModel = tabmodel; |
+ } |
+ |
+ /** |
+ * @return The {@link StackTab}s currently being rendered by the tab stack. |
+ * @VisibleForTesting |
+ */ |
+ @SuppressFBWarnings("EI_EXPOSE_REP") |
+ public StackTab[] getTabs() { |
+ return mStackTabs; |
+ } |
+ |
+ /** |
+ * @return The number of tabs in the tab stack. |
+ * @VisibleForTesting |
+ */ |
+ public int getCount() { |
+ return mStackTabs != null ? mStackTabs.length : 0; |
+ } |
+ |
+ /** |
+ * @return The number of visible tabs in the tab stack. |
+ */ |
+ public int getVisibleCount() { |
+ int visibleCount = 0; |
+ if (mStackTabs != null) { |
+ for (int i = 0; i < mStackTabs.length; ++i) { |
+ if (mStackTabs[i].getLayoutTab().isVisible()) visibleCount++; |
+ } |
+ } |
+ return visibleCount; |
+ } |
+ |
+ /* |
+ * Main Interaction Methods for the rest of the application |
+ * |
+ * |
+ * These methods are the main entry points for the model to tell the |
+ * view that something has changed. The rest of the application can |
+ * alert this class that something in the tab stack has changed or that |
+ * the user has decided to enter the tab switcher. |
+ * |
+ */ |
+ |
+ /** |
+ * Triggers the closing motions. |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param id The id of the tab that get closed. |
+ */ |
+ public void tabClosingEffect(long time, int id) { |
+ if (mStackTabs == null) return; |
+ |
+ // |id| cannot be used to access the particular tab in the model. |
+ // The tab is already gone from the model by this point. |
+ |
+ int newIndex = 0; |
+ boolean needAnimation = false; |
+ for (int i = 0; i < mStackTabs.length; ++i) { |
+ if (mStackTabs[i].getId() == id) { |
+ // Mark the {@link StackTab} as dying so that when the animation is |
+ // finished we can clear it out of the stack. This supports |
+ // multiple {@link StackTab} deletions. |
+ needAnimation |= !mStackTabs[i].isDying(); |
+ mStackTabs[i].setDying(true); |
+ } else { |
+ // Update the {@link StackTab} with a new index here. This makes sure the |
+ // {@link LayoutTab} end up in the proper place. |
+ mStackTabs[i].setNewIndex(newIndex++); |
+ } |
+ } |
+ |
+ if (needAnimation) { |
+ mScrollOffsetForDyingTabs = mScrollOffset; |
+ mSpacing = computeSpacing(newIndex); |
+ |
+ startAnimation(time, OverviewAnimationType.DISCARD); |
+ } |
+ |
+ if (newIndex == 0) { |
+ mIsDying = true; |
+ } |
+ } |
+ |
+ /** |
+ * Animates all the tabs closing at once. |
+ * |
+ * @param time The current time of the app in ms. |
+ */ |
+ public void tabsAllClosingEffect(long time) { |
+ boolean needAnimation = false; |
+ |
+ if (mStackTabs != null) { |
+ for (int i = 0; i < mStackTabs.length; ++i) { |
+ needAnimation |= !mStackTabs[i].isDying(); |
+ mStackTabs[i].setDying(true); |
+ } |
+ } else { |
+ // This needs to be set to true to handle the case where both the normal and incognito |
+ // tabs are being closed. |
+ needAnimation = true; |
+ } |
+ |
+ if (needAnimation) { |
+ mScrollOffsetForDyingTabs = mScrollOffset; |
+ mSpacing = computeSpacing(0); |
+ |
+ if (mStackTabs != null) { |
+ boolean isRtl = |
+ !((mCurrentMode == Orientation.PORTRAIT) ^ LocalizationUtils.isLayoutRtl()); |
+ for (int i = 0; i < mStackTabs.length; i++) { |
+ StackTab tab = mStackTabs[i]; |
+ tab.setDiscardOriginY(0.f); |
+ tab.setDiscardOriginX( |
+ isRtl ? 0.f : tab.getLayoutTab().getOriginalContentWidth()); |
+ tab.setDiscardFromClick(true); |
+ } |
+ } |
+ startAnimation(time, OverviewAnimationType.DISCARD_ALL); |
+ } |
+ |
+ mIsDying = true; |
+ } |
+ |
+ /** |
+ * Animates a new tab opening. |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param id The id of the new tab to animate. |
+ */ |
+ public void tabCreated(long time, int id) { |
+ if (!createTabHelper(id)) return; |
+ |
+ mIsDying = false; |
+ finishAnimation(time); |
+ startAnimation(time, OverviewAnimationType.NEW_TAB_OPENED, |
+ TabModelUtils.getTabIndexById(mTabModel, id), TabModel.INVALID_TAB_INDEX, false); |
+ } |
+ |
+ /** |
+ * Animates the closing of the stack. Focusing on the selected tab. |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param id The id of the tab to select. |
+ */ |
+ public void tabSelectingEffect(long time, int id) { |
+ int index = TabModelUtils.getTabIndexById(mTabModel, id); |
+ startAnimation(time, OverviewAnimationType.TAB_FOCUSED, index, -1, false); |
+ } |
+ |
+ /** |
+ * Called set up the tab stack to the initial state when it is entered. |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param focused Whether or not the stack was focused when entering. |
+ */ |
+ public void stackEntered(long time, boolean focused) { |
+ // Don't request new thumbnails until the animation is over. We should |
+ // have cached the visible ones already. |
+ boolean finishImmediately = !focused; |
+ mSpacing = computeSpacing(mStackTabs != null ? mStackTabs.length : 0); |
+ resetAllScrollOffset(); |
+ startAnimation(time, OverviewAnimationType.ENTER_STACK, finishImmediately); |
+ } |
+ |
+ /** |
+ * @return Whether or not the TabModel represented by this TabStackState should be displayed. |
+ */ |
+ public boolean isDisplayable() { |
+ return !mTabModel.isIncognito() || (!mIsDying && mTabModel.getCount() > 0); |
+ } |
+ |
+ private float getDefaultDiscardDirection() { |
+ return (mCurrentMode == Orientation.LANDSCAPE && LocalizationUtils.isLayoutRtl()) ? -1.0f |
+ : 1.0f; |
+ } |
+ |
+ /** |
+ * show is called to set up the initial variables, and must always be called before |
+ * displaying the stack. |
+ */ |
+ public void show() { |
+ mDiscardDirection = getDefaultDiscardDirection(); |
+ |
+ // Reinitialize the roll over counter for each tabswitcher session. |
+ mOverScrollCounter = 0; |
+ |
+ // TODO: Recreating the stack {@link StackTab} here might be overkill. Will these |
+ // already exist in the cache? Check to make sure it makes sense. |
+ createStackTabs(false); |
+ } |
+ |
+ /* |
+ * Animation Start and Finish Methods |
+ * |
+ * This method kicks off animations by using the |
+ * TabSwitcherAnimationFactory to create an AnimatorSet. |
+ */ |
+ |
+ /** |
+ * Starts an animation on the stack. |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param type The type of the animation to start. |
+ */ |
+ private void startAnimation(long time, OverviewAnimationType type) { |
+ startAnimation(time, type, TabModel.INVALID_TAB_INDEX, false); |
+ } |
+ |
+ /** |
+ * Starts an animation on the stack. |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param type The type of the animation to start. |
+ * @param finishImmediately Whether the animation jumps straight to the end. |
+ */ |
+ private void startAnimation(long time, OverviewAnimationType type, boolean finishImmediately) { |
+ startAnimation(time, type, TabModel.INVALID_TAB_INDEX, finishImmediately); |
+ } |
+ |
+ /** |
+ * Starts an animation on the stack. |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param type The type of the animation to start. |
+ * @param sourceIndex The source index needed by some animation types. |
+ * @param finishImmediately Whether the animation jumps straight to the end. |
+ */ |
+ private void startAnimation( |
+ long time, OverviewAnimationType type, int sourceIndex, boolean finishImmediately) { |
+ startAnimation(time, type, mTabModel.index(), sourceIndex, finishImmediately); |
+ } |
+ |
+ private void startAnimation(long time, OverviewAnimationType type, int focusIndex, |
+ int sourceIndex, boolean finishImmediately) { |
+ if (!canUpdateAnimation(time, type, sourceIndex, finishImmediately)) { |
+ // We need to finish animations started earlier before we start |
+ // off a new one. |
+ finishAnimation(time); |
+ // Stop movement while the animation takes place. |
+ stopScrollingMovement(time); |
+ } |
+ |
+ if (mAnimationFactory != null && mViewAnimationFactory != null) { |
+ mOverviewAnimationType = type; |
+ |
+ // First try to build a View animation. Then fallback to the compositor animation if |
+ // one isn't created. |
+ mViewAnimations = mViewAnimationFactory.createAnimatorSetForType( |
+ type, mStackTabs, mLayout.getViewContainer(), mTabModel, focusIndex); |
+ |
+ if (mViewAnimations != null) { |
+ mViewAnimations.addListener(mViewAnimatorListener); |
+ } else { |
+ // Build the AnimatorSet using the TabSwitcherAnimationFactory. |
+ // This will give us the appropriate AnimatorSet based on the current |
+ // state of the tab switcher and the OverviewAnimationType specified. |
+ mTabAnimations = |
+ mAnimationFactory.createAnimatorSetForType(type, mStackTabs, focusIndex, |
+ sourceIndex, mSpacing, mScrollOffset, mWarpSize, getDiscardRange()); |
+ } |
+ |
+ if (mTabAnimations != null) mTabAnimations.start(); |
+ if (mViewAnimations != null) mViewAnimations.start(); |
+ if (mTabAnimations != null || mViewAnimations != null) { |
+ mLayout.onStackAnimationStarted(); |
+ } |
+ |
+ if ((mTabAnimations == null && mViewAnimations == null) || finishImmediately) { |
+ finishAnimation(time); |
+ } |
+ } |
+ |
+ requestUpdate(); |
+ } |
+ |
+ /** |
+ * Performs the necessary actions to finish the current animation. |
+ * |
+ * @param time The current time of the app in ms. |
+ */ |
+ private void finishAnimation(long time) { |
+ if (mTabAnimations != null) mTabAnimations.updateAndFinish(); |
+ if (mViewAnimations != null) mViewAnimations.end(); |
+ if (mTabAnimations != null || mViewAnimations != null) mLayout.onStackAnimationFinished(); |
+ |
+ switch (mOverviewAnimationType) { |
+ case ENTER_STACK: |
+ mLayout.uiDoneEnteringStack(); |
+ break; |
+ case FULL_ROLL: |
+ springBack(time); |
+ break; |
+ case TAB_FOCUSED: |
+ // Purposeful fall through |
+ case NEW_TAB_OPENED: |
+ // Nothing to do. |
+ break; |
+ case DISCARD_ALL: |
+ mLayout.uiDoneClosingAllTabs(mTabModel.isIncognito()); |
+ cleanupStackTabState(); |
+ break; |
+ case UNDISCARD: |
+ // Purposeful fall through because if UNDISCARD animation updated DISCARD animation, |
+ // DISCARD animation clean up below is not called so UNDISCARD is responsible for |
+ // cleaning it up. |
+ case DISCARD: |
+ // Remove all dying tabs from mStackTabs. |
+ if (mStackTabs != null) { |
+ // Request for the model to be updated. |
+ for (int i = 0; i < mStackTabs.length; ++i) { |
+ StackTab tab = mStackTabs[i]; |
+ if (tab.isDying()) { |
+ mLayout.uiDoneClosingTab( |
+ time, tab.getId(), true, mTabModel.isIncognito()); |
+ } |
+ } |
+ } |
+ cleanupStackTabState(); |
+ break; |
+ default: |
+ break; |
+ } |
+ |
+ if (mOverviewAnimationType != OverviewAnimationType.NONE) { |
+ // sync the scrollTarget and scrollOffset; |
+ setScrollTarget(mScrollOffset, true); |
+ mOverviewAnimationType = OverviewAnimationType.NONE; |
+ } |
+ mTabAnimations = null; |
+ mViewAnimations = null; |
+ } |
+ |
+ private void cleanupStackTabState() { |
+ if (mStackTabs != null) { |
+ // First count the number of tabs that are still alive. |
+ int nNumberOfLiveTabs = 0; |
+ for (int i = 0; i < mStackTabs.length; ++i) { |
+ if (mStackTabs[i].isDying()) { |
+ mLayout.releaseTabLayout(mStackTabs[i].getLayoutTab()); |
+ } else { |
+ nNumberOfLiveTabs++; |
+ } |
+ } |
+ |
+ if (nNumberOfLiveTabs == 0) { |
+ // We have no more live {@link StackTab}. Just clean all tab related states. |
+ cleanupTabs(); |
+ } else if (nNumberOfLiveTabs < mStackTabs.length) { |
+ // If any tabs have died, we need to remove them from mStackTabs. |
+ |
+ StackTab[] oldTabs = mStackTabs; |
+ mStackTabs = new StackTab[nNumberOfLiveTabs]; |
+ |
+ int newIndex = 0; |
+ for (int i = 0; i < oldTabs.length; ++i) { |
+ if (!oldTabs[i].isDying()) { |
+ mStackTabs[newIndex] = oldTabs[i]; |
+ mStackTabs[newIndex].setNewIndex(newIndex); |
+ newIndex++; |
+ } |
+ } |
+ assert newIndex == nNumberOfLiveTabs; |
+ } |
+ } |
+ |
+ mDiscardDirection = getDefaultDiscardDirection(); |
+ } |
+ |
+ /** |
+ * Ensure that there are no dying tabs by finishing the current animation. |
+ * |
+ * @param time The current time of the app in ms. |
+ */ |
+ public void ensureCleaningUpDyingTabs(long time) { |
+ finishAnimation(time); |
+ } |
+ |
+ /** |
+ * Decide if the animation can be started without cleaning up the current animation. |
+ * @param time The current time of the app in ms. |
+ * @param type The type of the animation to start. |
+ * @param sourceIndex The source index needed by some animation types. |
+ * @param finishImmediately Whether the animation jumps straight to the end. |
+ * @return true, if we can start the animation without cleaning up the current |
+ * animation. |
+ */ |
+ private boolean canUpdateAnimation( |
+ long time, OverviewAnimationType type, int sourceIndex, boolean finishImmediately) { |
+ if (mAnimationFactory != null) { |
+ if ((mOverviewAnimationType == OverviewAnimationType.DISCARD |
+ || mOverviewAnimationType == OverviewAnimationType.UNDISCARD |
+ || mOverviewAnimationType == OverviewAnimationType.DISCARD_ALL) |
+ && (type == OverviewAnimationType.DISCARD |
+ || type == OverviewAnimationType.UNDISCARD |
+ || type == OverviewAnimationType.DISCARD_ALL)) { |
+ return true; |
+ } |
+ } |
+ return false; |
+ } |
+ |
+ /** |
+ * Cancel scrolling animation which is a part of discarding animation. |
+ * @return true if the animation is canceled, false, if there is nothing to cancel. |
+ */ |
+ private boolean cancelDiscardScrollingAnimation() { |
+ if (mOverviewAnimationType == OverviewAnimationType.DISCARD |
+ || mOverviewAnimationType == OverviewAnimationType.UNDISCARD |
+ || mOverviewAnimationType == OverviewAnimationType.DISCARD_ALL) { |
+ mTabAnimations.cancel(null, StackTab.Property.SCROLL_OFFSET); |
+ return true; |
+ } |
+ return false; |
+ } |
+ |
+ /** |
+ * Checks any Android view animations to see if they have finished yet. |
+ * @param time The current time of the app in ms. |
+ * @param jumpToEnd Whether to finish the animation. |
+ * @return Whether the animation was finished. |
+ */ |
+ public boolean onUpdateViewAnimation(long time, boolean jumpToEnd) { |
+ boolean finished = true; |
+ if (mViewAnimations != null) { |
+ finished = !mViewAnimations.isRunning(); |
+ finishAnimationsIfDone(time, jumpToEnd); |
+ } |
+ return finished; |
+ } |
+ |
+ /** |
+ * Steps the animation forward and updates all the animated values. |
+ * @param time The current time of the app in ms. |
+ * @param jumpToEnd Whether to finish the animation. |
+ * @return Whether the animation was finished. |
+ */ |
+ public boolean onUpdateCompositorAnimations(long time, boolean jumpToEnd) { |
+ if (!jumpToEnd) updateScrollOffset(time); |
+ |
+ boolean finished = true; |
+ if (mTabAnimations != null) { |
+ if (jumpToEnd) { |
+ finished = mTabAnimations.finished(); |
+ } else { |
+ finished = mTabAnimations.update(time); |
+ } |
+ finishAnimationsIfDone(time, jumpToEnd); |
+ } |
+ |
+ if (jumpToEnd) forceScrollStop(); |
+ return finished; |
+ } |
+ |
+ private void finishAnimationsIfDone(long time, boolean jumpToEnd) { |
+ boolean hasViewAnimations = mViewAnimations != null; |
+ boolean hasTabAnimations = mTabAnimations != null; |
+ boolean hasAnimations = hasViewAnimations || hasTabAnimations; |
+ boolean isViewFinished = hasViewAnimations ? !mViewAnimations.isRunning() : true; |
+ boolean isTabFinished = hasTabAnimations ? mTabAnimations.finished() : true; |
+ |
+ boolean shouldFinish = jumpToEnd && hasAnimations; |
+ shouldFinish |= hasAnimations && (!hasViewAnimations || isViewFinished) |
+ && (!hasTabAnimations || isTabFinished); |
+ if (shouldFinish) finishAnimation(time); |
+ } |
+ |
+ /** |
+ * Determines which action was specified by the user's drag. |
+ * |
+ * @param scrollDrag The number of pixels moved in the scroll direction. |
+ * @param discardDrag The number of pixels moved in the discard direction. |
+ * @return The current lock mode or a hint if the motion was not strong enough |
+ * to fully lock the mode. |
+ */ |
+ private DragLock computeDragLock(float scrollDrag, float discardDrag) { |
+ scrollDrag = Math.abs(scrollDrag); |
+ discardDrag = Math.abs(discardDrag); |
+ DragLock hintLock = (discardDrag * DRAG_ANGLE_THRESHOLD) > scrollDrag ? DragLock.DISCARD |
+ : DragLock.SCROLL; |
+ // If the user paused the drag for too long, re-determine what the new action is. |
+ long timeMillisecond = System.currentTimeMillis(); |
+ if ((timeMillisecond - mLastScrollUpdate) > DRAG_TIME_THRESHOLD) { |
+ mDragLock = DragLock.NONE; |
+ } |
+ // Select the scroll lock if enough conviction is put into scrolling. |
+ if ((mDragLock == DragLock.NONE && Math.abs(scrollDrag - discardDrag) > mMinScrollMotion) |
+ || (mDragLock == DragLock.DISCARD && discardDrag > mMinScrollMotion) |
+ || (mDragLock == DragLock.SCROLL && scrollDrag > mMinScrollMotion)) { |
+ mLastScrollUpdate = timeMillisecond; |
+ if (mDragLock == DragLock.NONE) { |
+ mDragLock = hintLock; |
+ } |
+ } |
+ // Returns a hint of the lock so we can show feedback even if the lock is not committed yet. |
+ return mDragLock == DragLock.NONE ? hintLock : mDragLock; |
+ } |
+ |
+ /* |
+ * User Input Routines: |
+ * |
+ * The input routines that process gestures and click touches. These |
+ * are the main way to interact with the view directly. Other input |
+ * paths happen when model changes impact the view. This can happen |
+ * as a result of some of these actions or from other user input (ie: |
+ * from the Toolbar). These are ignored if an animation is currently |
+ * in progress. |
+ */ |
+ |
+ /** |
+ * Called on drag event (from scroll events in the gesture detector). |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param x The x coordinate of the end of the drag event. |
+ * @param y The y coordinate of the end of the drag event. |
+ * @param amountX The number of pixels dragged in the x direction since the last event. |
+ * @param amountY The number of pixels dragged in the y direction since the last event. |
+ */ |
+ public void drag(long time, float x, float y, float amountX, float amountY) { |
+ float scrollDrag, discardDrag; |
+ if (mCurrentMode == Orientation.PORTRAIT) { |
+ discardDrag = amountX; |
+ scrollDrag = amountY; |
+ } else { |
+ discardDrag = amountY; |
+ scrollDrag = LocalizationUtils.isLayoutRtl() ? -amountX : amountX; |
+ } |
+ DragLock hintLock = computeDragLock(scrollDrag, discardDrag); |
+ if (hintLock == DragLock.DISCARD) { |
+ discard(x, y, amountX, amountY); |
+ } else { |
+ // Only cancel the current discard attempt if the scroll lock is committed: |
+ // by using mDragLock instead of hintLock. |
+ if (mDragLock == DragLock.SCROLL && mDiscardingTab != null) { |
+ commitDiscard(time, false); |
+ } |
+ scroll(x, y, LocalizationUtils.isLayoutRtl() ? -amountX : amountX, amountY, false); |
+ } |
+ requestUpdate(); |
+ } |
+ |
+ /** |
+ * Discards and updates the position based on the input event values. |
+ * |
+ * @param x The x coordinate of the end of the drag event. |
+ * @param y The y coordinate of the end of the drag event. |
+ * @param amountX The number of pixels dragged in the x direction since the last event. |
+ * @param amountY The number of pixels dragged in the y direction since the last event. |
+ */ |
+ private void discard(float x, float y, float amountX, float amountY) { |
+ if (mStackTabs == null |
+ || (mOverviewAnimationType != OverviewAnimationType.NONE |
+ && mOverviewAnimationType != OverviewAnimationType.DISCARD |
+ && mOverviewAnimationType != OverviewAnimationType.DISCARD_ALL |
+ && mOverviewAnimationType != OverviewAnimationType.UNDISCARD)) { |
+ return; |
+ } |
+ |
+ if (mDiscardingTab == null) { |
+ if (!mInSwipe) { |
+ mDiscardingTab = getTabAtPositon(x, y); |
+ } else { |
+ if (mTabModel.index() < 0) return; |
+ mDiscardingTab = mStackTabs[mTabModel.index()]; |
+ } |
+ |
+ if (mDiscardingTab != null) { |
+ cancelDiscardScrollingAnimation(); |
+ |
+ // Make sure we are well within the tab in the discard direction. |
+ RectF target = mDiscardingTab.getLayoutTab().getClickTargetBounds(); |
+ float distanceToEdge; |
+ float edgeToEdge; |
+ if (mCurrentMode == Orientation.PORTRAIT) { |
+ mDiscardDirection = 1.0f; |
+ distanceToEdge = Math.max(target.left - x, x - target.right); |
+ edgeToEdge = target.width(); |
+ } else { |
+ mDiscardDirection = 2.0f - 4.0f * (x / mLayout.getWidth()); |
+ mDiscardDirection = MathUtils.clamp(mDiscardDirection, -1.0f, 1.0f); |
+ distanceToEdge = Math.max(target.top - y, y - target.bottom); |
+ edgeToEdge = target.height(); |
+ } |
+ |
+ float scaledDiscardX = x - mDiscardingTab.getLayoutTab().getX(); |
+ float scaledDiscardY = y - mDiscardingTab.getLayoutTab().getY(); |
+ mDiscardingTab.setDiscardOriginX(scaledDiscardX / mDiscardingTab.getScale()); |
+ mDiscardingTab.setDiscardOriginY(scaledDiscardY / mDiscardingTab.getScale()); |
+ mDiscardingTab.setDiscardFromClick(false); |
+ |
+ if (Math.abs(distanceToEdge) < DISCARD_SAFE_SELECTION_PCTG * edgeToEdge) { |
+ mDiscardingTab = null; |
+ } |
+ } |
+ } |
+ if (mDiscardingTab != null) { |
+ float deltaAmount = mCurrentMode == Orientation.PORTRAIT ? amountX : amountY; |
+ mDiscardingTab.addToDiscardAmount(deltaAmount); |
+ } |
+ } |
+ |
+ /** |
+ * Called on touch/tilt scroll event. |
+ * |
+ * @param x The x coordinate of the end of the scroll event. |
+ * @param y The y coordinate of the end of the scroll event. |
+ * @param amountX The number of pixels scrolled in the x direction. |
+ * @param amountY The number of pixels scrolled in the y direction. |
+ * @param isTilt True if the call comes from a tilt event. |
+ */ |
+ private void scroll(float x, float y, float amountX, float amountY, boolean isTilt) { |
+ if ((!mScroller.isFinished() && isTilt) || mStackTabs == null |
+ || (mOverviewAnimationType != OverviewAnimationType.NONE |
+ && mOverviewAnimationType != OverviewAnimationType.DISCARD |
+ && mOverviewAnimationType != OverviewAnimationType.UNDISCARD |
+ && mOverviewAnimationType != OverviewAnimationType.DISCARD_ALL |
+ && mOverviewAnimationType != OverviewAnimationType.ENTER_STACK)) { |
+ return; |
+ } |
+ |
+ float amountScreen = mCurrentMode == Orientation.PORTRAIT ? amountY : amountX; |
+ float amountScroll = amountScreen; |
+ float amountEvenOut = amountScreen; |
+ |
+ // Computes the right amount for the scrolling so the finger matches the tab under it. |
+ float tabScrollSpaceFinal = 0; |
+ if (mScrollingTab == null || isTilt) { |
+ mScrollingTab = getTabAtPositon(x, y); |
+ } |
+ |
+ if (mScrollingTab == null && mInSwipe && mStackTabs != null) { |
+ int index = mTabModel.index(); |
+ if (index >= 0 && index <= mStackTabs.length) mScrollingTab = mStackTabs[index]; |
+ } |
+ |
+ if (mScrollingTab == null) { |
+ if (!isTilt) { |
+ amountScroll = 0; |
+ amountEvenOut = 0; |
+ } |
+ } else if (mScrollingTab.getIndex() == 0) { |
+ amountEvenOut = 0; |
+ } else { |
+ // Find the scroll that make the selected tab move the right |
+ // amount on the screen. |
+ float tabScrollSpace = mScrollingTab.getScrollOffset() + mScrollOffset; |
+ float tabScreen = scrollToScreen(tabScrollSpace); |
+ tabScrollSpaceFinal = screenToScroll(tabScreen + amountScreen); |
+ amountScroll = tabScrollSpaceFinal - tabScrollSpace; |
+ // Matching the finger is too strong of a constraints on the edges. So we make |
+ // sure the end value is not too far from the linear case. |
+ amountScroll = Math.signum(amountScreen) |
+ * MathUtils.clamp(Math.abs(amountScroll), Math.abs(amountScreen) * 0.5f, |
+ Math.abs(amountScreen) * 2.0f); |
+ } |
+ |
+ // Evens out the tabs and correct the scroll amount if needed. |
+ if (evenOutTabs(amountEvenOut, false) && mScrollingTab.getIndex() > 0) { |
+ // Adjust the amount after the even phase |
+ float tabScrollSpace = mScrollingTab.getScrollOffset() + mScrollOffset; |
+ amountScroll = tabScrollSpaceFinal - tabScrollSpace; |
+ } |
+ |
+ // Actually do the scrolling. |
+ setScrollTarget(mScrollTarget + amountScroll, false); |
+ } |
+ |
+ /** |
+ * Evens out auto-magically the cards as the stack get scrolled. |
+ * |
+ * @param amount The amount of scroll performed in pixel. The sign indicates the |
+ * direction. |
+ * @param allowReverseDirection Whether or not to allow corrections in the reverse direction of |
+ * the amount scrolled. |
+ * @return True if any tab had been 'visibly' moved. |
+ */ |
+ private boolean evenOutTabs(float amount, boolean allowReverseDirection) { |
+ if (mStackTabs == null || mOverviewAnimationType != OverviewAnimationType.NONE |
+ || mEvenOutProgress >= 1.0f || amount == 0) { |
+ return false; |
+ } |
+ boolean changed = false; |
+ boolean reverseScrolling = false; |
+ |
+ // The evening out process last until mEvenOutRate reaches 1.0. Tabs blend linearly |
+ // between the current position to a nice evenly scaled pattern. Because we do not store |
+ // the starting position for each tab we need more complicated math to do the blend. |
+ // The absoluteProgress is how much we need progress this step on the [0, 1] scale. |
+ float absoluteProgress = Math.min(Math.abs(amount) * mEvenOutRate, 1.0f - mEvenOutProgress); |
+ // The relativeProgress is how much we need to blend the target to the current to get there. |
+ float relativeProgress = absoluteProgress / (1.0f - mEvenOutProgress); |
+ |
+ float screenMax = getScrollDimensionSize(); |
+ for (int i = 0; i < mStackTabs.length; ++i) { |
+ float source = mStackTabs[i].getScrollOffset(); |
+ float target = screenToScroll(i * mSpacing); |
+ float sourceScreen = Math.min(screenMax, scrollToScreen(source + mScrollTarget)); |
+ float targetScreen = Math.min(screenMax, scrollToScreen(target + mScrollTarget)); |
+ // If the target and the current position matches on the screen then we snap to the |
+ // target. |
+ if (sourceScreen == targetScreen) { |
+ mStackTabs[i].setScrollOffset(target); |
+ continue; |
+ } |
+ float step = source + (target - source) * relativeProgress; |
+ float stepScreen = Math.min(screenMax, scrollToScreen(step + mScrollTarget)); |
+ // If the step can be performed without noticing then we do it. |
+ if (sourceScreen == stepScreen) { |
+ mStackTabs[i].setScrollOffset(step); |
+ continue; |
+ } |
+ // If the scrolling goes in the same direction as the step then the motion is applied. |
+ if ((targetScreen - sourceScreen) * amount > 0 || allowReverseDirection) { |
+ mStackTabs[i].setScrollOffset(step); |
+ changed = true; |
+ } else { |
+ reverseScrolling = true; |
+ } |
+ } |
+ // Only account for progress if the scrolling was in the right direction. It assumes here |
+ // That if any of the tabs was going in the wrong direction then the progress is not |
+ // recorded at all. This is very conservative to avoid poping in the scrolling. It works |
+ // for now but might need to be revisited if we see artifacts. |
+ if (!reverseScrolling) { |
+ mEvenOutProgress += absoluteProgress; |
+ } |
+ return changed; |
+ } |
+ |
+ /** |
+ * Called on touch fling event. Scroll the stack or help to discard a tab. |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param x The y coordinate of the start of the fling event. |
+ * @param y The y coordinate of the start of the fling event. |
+ * @param velocityX The amount of velocity in the x direction. |
+ * @param velocityY The amount of velocity in the y direction. |
+ */ |
+ public void fling(long time, float x, float y, float velocityX, float velocityY) { |
+ if (mDragLock != DragLock.SCROLL && mDiscardingTab != null) { |
+ float velocity = mCurrentMode == Orientation.PORTRAIT ? velocityX : velocityY; |
+ float maxDelta = getDiscardRange() * DISCARD_FLING_MAX_CONTRIBUTION; |
+ float deltaAmount = MathUtils.clamp(velocity * DISCARD_FLING_DT, -maxDelta, maxDelta); |
+ mDiscardingTab.addToDiscardAmount(deltaAmount); |
+ } else if (mOverviewAnimationType == OverviewAnimationType.NONE && mScroller.isFinished() |
+ && mOverScrollOffset == 0 && getTabIndexAtPositon(x, y) >= 0) { |
+ float velocity = mCurrentMode == Orientation.PORTRAIT |
+ ? velocityY |
+ : (LocalizationUtils.isLayoutRtl() ? -velocityX : velocityX); |
+ // Fling only overscrolls when the stack is fully unfolded. |
+ mScroller.fling(0, (int) mScrollTarget, 0, (int) velocity, 0, 0, |
+ (int) getMinScroll(false), (int) getMaxScroll(false), 0, |
+ (int) ((velocity > 0 ? mMaxOverScroll : mMaxUnderScroll) |
+ * MAX_OVER_FLING_SCALE), |
+ time); |
+ |
+ // Set the target to the final scroll position to make sure |
+ // the offset finally gets there regardless of what happens. |
+ // We override this when the user interrupts the fling though. |
+ setScrollTarget(mScroller.getFinalY(), false); |
+ } |
+ } |
+ |
+ /** |
+ * Get called on down touch event. |
+ * |
+ * @param time The current time of the app in ms. |
+ */ |
+ public void onDown(long time) { |
+ mDragLock = DragLock.NONE; |
+ if (mOverviewAnimationType == OverviewAnimationType.NONE) { |
+ stopScrollingMovement(time); |
+ } |
+ // Resets the scrolling state. |
+ mScrollingTab = null; |
+ commitDiscard(time, false); |
+ } |
+ |
+ /** |
+ * Get called on long press touch event. |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param x The x coordinate in pixel inside the stack view. |
+ * @param y The y coordinate in pixel inside the stack view. |
+ */ |
+ public void onLongPress(long time, float x, float y) { |
+ if (mOverviewAnimationType == OverviewAnimationType.NONE) { |
+ mLongPressSelected = getTabIndexAtPositon(x, y); |
+ if (mLongPressSelected >= 0) { |
+ startAnimation(time, OverviewAnimationType.VIEW_MORE, mLongPressSelected, false); |
+ mEvenOutProgress = 0.0f; |
+ } |
+ } |
+ } |
+ |
+ /** |
+ * Called when at least 2 touch events are detected. |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param x0 The x coordinate of the first touch event. |
+ * @param y0 The y coordinate of the first touch event. |
+ * @param x1 The x coordinate of the second touch event. |
+ * @param y1 The y coordinate of the second touch event. |
+ * @param firstEvent The pinch is the first of a sequence of pinch events. |
+ */ |
+ public void onPinch(long time, float x0, float y0, float x1, float y1, boolean firstEvent) { |
+ if ((mOverviewAnimationType != OverviewAnimationType.START_PINCH |
+ && mOverviewAnimationType != OverviewAnimationType.NONE) || mStackTabs == null) { |
+ return; |
+ } |
+ if (mPinch0TabIndex < 0) startAnimation(time, OverviewAnimationType.START_PINCH); |
+ |
+ // Reordering the fingers so pinch0 is always the closest to the top of the stack. |
+ // This allows simpler math down the line where we assume that |
+ // pinch0TabIndex <= pinch0TabIndex |
+ // It also means that crossing the finger will separate the tabs again. |
+ boolean inverse = (mCurrentMode == Orientation.PORTRAIT) |
+ ? y0 > y1 |
+ : LocalizationUtils.isLayoutRtl() ? (x0 <= x1) : (x0 > x1); |
+ float pinch0X = inverse ? x1 : x0; |
+ float pinch0Y = inverse ? y1 : y0; |
+ float pinch1X = inverse ? x0 : x1; |
+ float pinch1Y = inverse ? y0 : y1; |
+ float pinch0Offset = (mCurrentMode == Orientation.PORTRAIT) |
+ ? pinch0Y |
+ : LocalizationUtils.isLayoutRtl() ? -pinch0X : pinch0X; |
+ float pinch1Offset = (mCurrentMode == Orientation.PORTRAIT) |
+ ? pinch1Y |
+ : LocalizationUtils.isLayoutRtl() ? -pinch1X : pinch1X; |
+ |
+ if (firstEvent) { |
+ // Resets pinch and scrolling state. |
+ mPinch0TabIndex = -1; |
+ mPinch1TabIndex = -1; |
+ mScrollingTab = null; |
+ commitDiscard(time, false); |
+ } |
+ int pinch0TabIndex = mPinch0TabIndex; |
+ int pinch1TabIndex = mPinch1TabIndex; |
+ if (mPinch0TabIndex < 0) { |
+ pinch0TabIndex = getTabIndexAtPositon(pinch0X, pinch0Y); |
+ pinch1TabIndex = getTabIndexAtPositon(pinch1X, pinch1Y); |
+ // If any of them is invalid we invalidate both. |
+ if (pinch0TabIndex < 0 || pinch1TabIndex < 0) { |
+ pinch0TabIndex = -1; |
+ pinch1TabIndex = -1; |
+ } |
+ } |
+ |
+ if (pinch0TabIndex >= 0 && mPinch0TabIndex == pinch0TabIndex |
+ && mPinch1TabIndex == pinch1TabIndex) { |
+ final float minScrollTarget = getMinScroll(false); |
+ final float maxScrollTarget = getMaxScroll(false); |
+ final float oldScrollTarget = |
+ MathUtils.clamp(mScrollTarget, minScrollTarget, maxScrollTarget); |
+ // pinch0TabIndex > pinch1TabIndex is unexpected but we do not want to exit |
+ // ungracefully so process it as if the tabs were the same. |
+ if (pinch0TabIndex >= pinch1TabIndex) { |
+ // If one tab is pinched then we only scroll. |
+ float screenDelta0 = pinch0Offset - mLastPinch0Offset; |
+ if (pinch0TabIndex == 0) { |
+ // Linear scroll on the top tab for the overscroll to kick-in linearly. |
+ setScrollTarget(oldScrollTarget + screenDelta0, false); |
+ } else { |
+ float tab0ScrollSpace = |
+ mStackTabs[pinch0TabIndex].getScrollOffset() + oldScrollTarget; |
+ float tab0Screen = scrollToScreen(tab0ScrollSpace); |
+ float tab0ScrollFinal = screenToScroll(tab0Screen + screenDelta0); |
+ setScrollTarget( |
+ tab0ScrollFinal - mStackTabs[pinch0TabIndex].getScrollOffset(), false); |
+ } |
+ // This is the common case of the pinch, 2 fingers on 2 different tabs. |
+ } else { |
+ // Find the screen space position before and after the scroll so the tab 0 matches |
+ // the finger 0 motion. |
+ float screenDelta0 = pinch0Offset - mLastPinch0Offset; |
+ float tab0ScreenBefore = approxScreen(mStackTabs[pinch0TabIndex], oldScrollTarget); |
+ float tab0ScreenAfter = tab0ScreenBefore + screenDelta0; |
+ |
+ // Find the screen space position before and after the scroll so the tab 1 matches |
+ // the finger 1 motion. |
+ float screenDelta1 = pinch1Offset - mLastPinch1Offset; |
+ float tab1ScreenBefore = approxScreen(mStackTabs[pinch1TabIndex], oldScrollTarget); |
+ float tab1ScreenAfter = tab1ScreenBefore + screenDelta1; |
+ |
+ // Heuristic: the scroll is defined by half the change of the first pinched tab. |
+ // The rational is that it looks nice this way :)... Scrolling creates a sliding |
+ // effect. When a finger does not move then it is expected that none of the tabs |
+ // past that steady finger should move. This does the job. |
+ float globalScrollBefore = screenToScroll(tab0ScreenBefore); |
+ float globalScrollAfter = screenToScroll((tab0ScreenAfter + tab0ScreenBefore) / 2); |
+ setScrollTarget(oldScrollTarget + globalScrollAfter - globalScrollBefore, true); |
+ |
+ // Evens out the tabs in between |
+ float minScreen = tab0ScreenAfter; |
+ float maxScreen = tab0ScreenAfter; |
+ for (int i = pinch0TabIndex; i <= pinch1TabIndex; i++) { |
+ float screenBefore = approxScreen(mStackTabs[i], oldScrollTarget); |
+ float t = (screenBefore - tab0ScreenBefore) |
+ / (tab1ScreenBefore - tab0ScreenBefore); |
+ float screenAfter = (1 - t) * tab0ScreenAfter + t * tab1ScreenAfter; |
+ screenAfter = Math.max(minScreen, screenAfter); |
+ screenAfter = Math.min(maxScreen, screenAfter); |
+ minScreen = screenAfter + StackTab.sStackedTabVisibleSize; |
+ maxScreen = screenAfter + mStackTabs[i].getSizeInScrollDirection(mCurrentMode); |
+ float newScrollOffset = screenToScroll(screenAfter) - mScrollTarget; |
+ mStackTabs[i].setScrollOffset(newScrollOffset); |
+ } |
+ |
+ // Push a bit the tabs bellow pinch1. |
+ float delta1 = tab1ScreenAfter - tab1ScreenBefore; |
+ for (int i = pinch1TabIndex + 1; i < mStackTabs.length; i++) { |
+ delta1 /= 2; |
+ float screenAfter = approxScreen(mStackTabs[i], oldScrollTarget) + delta1; |
+ screenAfter = Math.max(minScreen, screenAfter); |
+ screenAfter = Math.min(maxScreen, screenAfter); |
+ minScreen = screenAfter + StackTab.sStackedTabVisibleSize; |
+ maxScreen = screenAfter + mStackTabs[i].getSizeInScrollDirection(mCurrentMode); |
+ mStackTabs[i].setScrollOffset(screenToScroll(screenAfter) - mScrollTarget); |
+ } |
+ |
+ // Pull a bit the tabs above pinch0. |
+ minScreen = tab0ScreenAfter; |
+ maxScreen = tab0ScreenAfter; |
+ float posScreen = tab0ScreenAfter; |
+ float delta0 = tab0ScreenAfter - tab0ScreenBefore; |
+ for (int i = pinch0TabIndex - 1; i > 0; i--) { |
+ delta0 /= 2; |
+ minScreen = posScreen - mStackTabs[i].getSizeInScrollDirection(mCurrentMode); |
+ maxScreen = posScreen - StackTab.sStackedTabVisibleSize; |
+ float screenAfter = approxScreen(mStackTabs[i], oldScrollTarget) + delta0; |
+ screenAfter = Math.max(minScreen, screenAfter); |
+ screenAfter = Math.min(maxScreen, screenAfter); |
+ mStackTabs[i].setScrollOffset(screenToScroll(screenAfter) - mScrollTarget); |
+ } |
+ } |
+ } |
+ mPinch0TabIndex = pinch0TabIndex; |
+ mPinch1TabIndex = pinch1TabIndex; |
+ mLastPinch0Offset = pinch0Offset; |
+ mLastPinch1Offset = pinch1Offset; |
+ mEvenOutProgress = 0.0f; |
+ requestUpdate(); |
+ } |
+ |
+ /** |
+ * Commits or release the that currently being considered for discard. This function |
+ * also triggers the associated animations. |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param allowDiscard Whether to allow to discard the tab currently being considered |
+ * for discard. |
+ */ |
+ private void commitDiscard(long time, boolean allowDiscard) { |
+ if (mDiscardingTab == null) return; |
+ |
+ assert mStackTabs != null; |
+ StackTab discarded = mDiscardingTab; |
+ if (Math.abs(discarded.getDiscardAmount()) / getDiscardRange() > DISCARD_COMMIT_THRESHOLD |
+ && allowDiscard) { |
+ mLayout.uiRequestingCloseTab(time, discarded.getId()); |
+ RecordUserAction.record("MobileStackViewSwipeCloseTab"); |
+ RecordUserAction.record("MobileTabClosed"); |
+ } else { |
+ startAnimation(time, OverviewAnimationType.UNDISCARD); |
+ } |
+ mDiscardingTab = null; |
+ requestUpdate(); |
+ } |
+ |
+ /** |
+ * Called on touch up or cancel event. |
+ */ |
+ public void onUpOrCancel(long time) { |
+ // Make sure the bottom tab always goes back to the top of the screen. |
+ if (mPinch0TabIndex >= 0) { |
+ startAnimation(time, OverviewAnimationType.REACH_TOP); |
+ requestUpdate(); |
+ } |
+ // Commit or uncommit discard tab |
+ commitDiscard(time, true); |
+ |
+ resetInputActionIndices(); |
+ |
+ springBack(time); |
+ } |
+ |
+ /** |
+ * Bounces back if we happen to overscroll the stack. |
+ */ |
+ private void springBack(long time) { |
+ if (mScroller.isFinished()) { |
+ int minScroll = (int) getMinScroll(false); |
+ int maxScroll = (int) getMaxScroll(false); |
+ if (mScrollTarget < minScroll || mScrollTarget > maxScroll) { |
+ mScroller.springBack(0, (int) mScrollTarget, 0, 0, minScroll, maxScroll, time); |
+ setScrollTarget(MathUtils.clamp(mScrollTarget, minScroll, maxScroll), false); |
+ requestUpdate(); |
+ } |
+ } |
+ } |
+ |
+ /** |
+ * Called on touch click event. |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param x The x coordinate in pixel inside the stack view. |
+ * @param y The y coordinate in pixel inside the stack view. |
+ */ |
+ public void click(long time, float x, float y) { |
+ if (mOverviewAnimationType != OverviewAnimationType.NONE |
+ && mOverviewAnimationType != OverviewAnimationType.DISCARD |
+ && mOverviewAnimationType != OverviewAnimationType.UNDISCARD |
+ && mOverviewAnimationType != OverviewAnimationType.DISCARD_ALL) { |
+ return; |
+ } |
+ int clicked = getTabIndexAtPositon(x, y, LayoutTab.getTouchSlop()); |
+ if (clicked >= 0) { |
+ // Check if the click was within the boundaries of the close button defined by its |
+ // visible coordinates. |
+ boolean isRtl = |
+ !((mCurrentMode == Orientation.PORTRAIT) ^ LocalizationUtils.isLayoutRtl()); |
+ if (mStackTabs[clicked].getLayoutTab().checkCloseHitTest(x, y, isRtl)) { |
+ // Tell the model to close the tab because the close button was pressed. The model |
+ // will then trigger a notification which will start the actual close process here |
+ // if necessary. |
+ StackTab tab = mStackTabs[clicked]; |
+ final float halfCloseBtnWidth = LayoutTab.CLOSE_BUTTON_WIDTH_DP / 2.f; |
+ final float halfCloseBtnHeight = mBorderTopPadding / 2.f; |
+ final float contentWidth = tab.getLayoutTab().getOriginalContentWidth(); |
+ |
+ tab.setDiscardOriginY(halfCloseBtnHeight); |
+ tab.setDiscardOriginX(isRtl ? halfCloseBtnWidth : contentWidth - halfCloseBtnWidth); |
+ tab.setDiscardFromClick(true); |
+ mLayout.uiRequestingCloseTab(time, tab.getId()); |
+ RecordUserAction.record("MobileStackViewCloseTab"); |
+ RecordUserAction.record("MobileTabClosed"); |
+ } else { |
+ // Let the model know that a new {@link LayoutTab} was selected. The model will |
+ // notify us if we need to do anything visual. setIndex() will possibly switch the |
+ // models and broadcast the event. |
+ mLayout.uiSelectingTab(time, mStackTabs[clicked].getId()); |
+ } |
+ } |
+ } |
+ |
+ /* |
+ * Initialization and Utility Methods |
+ */ |
+ |
+ /** |
+ * @param context The current Android's context. |
+ */ |
+ public void contextChanged(Context context) { |
+ Resources res = context.getResources(); |
+ final float pxToDp = 1.0f / res.getDisplayMetrics().density; |
+ |
+ mMinScrollMotion = DRAG_MOTION_THRESHOLD_DP; |
+ final float maxOverScrollPx = res.getDimensionPixelOffset(R.dimen.over_scroll); |
+ final float maxUnderScrollPx = Math.round(maxOverScrollPx * MAX_UNDER_SCROLL_SCALE); |
+ mMaxOverScroll = maxOverScrollPx * pxToDp; |
+ mMaxUnderScroll = maxUnderScrollPx * pxToDp; |
+ mMaxOverScrollAngle = res.getInteger(R.integer.over_scroll_angle); |
+ mMaxOverScrollSlide = res.getDimensionPixelOffset(R.dimen.over_scroll_slide) * pxToDp; |
+ mEvenOutRate = 1.0f / (res.getDimension(R.dimen.even_out_scrolling) * pxToDp); |
+ mMinSpacing = res.getDimensionPixelOffset(R.dimen.min_spacing) * pxToDp; |
+ mBorderTransparentTop = |
+ res.getDimension(R.dimen.tabswitcher_border_frame_transparent_top) * pxToDp; |
+ mBorderTransparentSide = |
+ res.getDimension(R.dimen.tabswitcher_border_frame_transparent_side) * pxToDp; |
+ mBorderTopPadding = res.getDimension(R.dimen.tabswitcher_border_frame_padding_top) * pxToDp; |
+ mBorderLeftPadding = |
+ res.getDimension(R.dimen.tabswitcher_border_frame_padding_left) * pxToDp; |
+ |
+ // Just in case the density has changed, rebuild the OverScroller. |
+ mScroller = new StackScroller(context); |
+ } |
+ |
+ /** |
+ * @param width The new width of the layout. |
+ * @param height The new height of the layout. |
+ * @param orientation The new orientation of the layout. |
+ */ |
+ public void notifySizeChanged(float width, float height, int orientation) { |
+ updateCurrentMode(orientation); |
+ } |
+ |
+ private float getScrollDimensionSize() { |
+ return mCurrentMode == Orientation.PORTRAIT ? mLayout.getHeightMinusTopControls() |
+ : mLayout.getWidth(); |
+ } |
+ |
+ /** |
+ * Gets the tab instance at the requested position. |
+ * |
+ * @param x The x coordinate where to perform the hit test. |
+ * @param y The y coordinate where to perform the hit test. |
+ * @return The instance of the tab selected. null if none. |
+ */ |
+ private StackTab getTabAtPositon(float x, float y) { |
+ int tabIndexAtPosition = getTabIndexAtPositon(x, y, 0); |
+ return tabIndexAtPosition < 0 ? null : mStackTabs[tabIndexAtPosition]; |
+ } |
+ |
+ /** |
+ * Gets the tab index at the requested position. |
+ * |
+ * @param x The x coordinate where to perform the hit test. |
+ * @param y The y coordinate where to perform the hit test. |
+ * @return The index of the tab selected. -1 if none. |
+ */ |
+ private int getTabIndexAtPositon(float x, float y) { |
+ return getTabIndexAtPositon(x, y, 0); |
+ } |
+ |
+ /** |
+ * Gets the tab index at the requested position. |
+ * |
+ * @param x The x coordinate where to perform the hit test. |
+ * @param y The y coordinate where to perform the hit test. |
+ * @param slop The acceptable distance to a tab for it to be considered. |
+ * @return The index of the tab selected. -1 if none. |
+ */ |
+ private int getTabIndexAtPositon(float x, float y, float slop) { |
+ int closestIndex = -1; |
+ float closestDistance = mLayout.getHeight() + mLayout.getWidth(); |
+ if (mStackTabs != null) { |
+ for (int i = mStackTabs.length - 1; i >= 0; --i) { |
+ // This is a fail safe. We should never have a situation where a dying |
+ // {@link LayoutTab} can get accessed (the animation check should catch it). |
+ if (!mStackTabs[i].isDying() && mStackTabs[i].getLayoutTab().isVisible()) { |
+ float d = mStackTabs[i].getLayoutTab().computeDistanceTo(x, y); |
+ // Strict '<' is very important here because we might have several tab at the |
+ // same place and we want the one above. |
+ if (d < closestDistance) { |
+ closestIndex = i; |
+ closestDistance = d; |
+ if (d == 0) break; |
+ } |
+ } |
+ } |
+ } |
+ return closestDistance <= slop ? closestIndex : -1; |
+ } |
+ |
+ /** |
+ * ComputeTabPosition pass 1: |
+ * Combine the overall stack scale with the animated tab scale. |
+ * |
+ * @param stackRect The frame of the stack. |
+ */ |
+ private void computeTabScaleAlphaDepthHelper(RectF stackRect) { |
+ final float stackScale = getStackScale(stackRect); |
+ final float discardRange = getDiscardRange(); |
+ |
+ for (int i = 0; i < mStackTabs.length; ++i) { |
+ assert mStackTabs[i] != null; |
+ StackTab stackTab = mStackTabs[i]; |
+ LayoutTab layoutTab = stackTab.getLayoutTab(); |
+ final float discard = stackTab.getDiscardAmount(); |
+ |
+ // Scale |
+ float discardScale = |
+ computeDiscardScale(discard, discardRange, stackTab.getDiscardFromClick()); |
+ layoutTab.setScale(stackTab.getScale() * discardScale * stackScale); |
+ layoutTab.setBorderScale(discardScale); |
+ |
+ // Alpha |
+ float discardAlpha = computeDiscardAlpha(discard, discardRange); |
+ layoutTab.setAlpha(stackTab.getAlpha() * discardAlpha); |
+ } |
+ } |
+ |
+ /** |
+ * ComputeTabPosition pass 2: |
+ * Adjust the scroll offsets of each tab so no there is no void in between tabs. |
+ */ |
+ private void computeTabScrollOffsetHelper() { |
+ float maxScrollOffset = Float.MAX_VALUE; |
+ for (int i = 0; i < mStackTabs.length; ++i) { |
+ if (mStackTabs[i].isDying()) continue; |
+ |
+ float tabScrollOffset = Math.min(maxScrollOffset, mStackTabs[i].getScrollOffset()); |
+ mStackTabs[i].setScrollOffset(tabScrollOffset); |
+ |
+ float maxScreenScrollOffset = scrollToScreen(mScrollOffset + tabScrollOffset); |
+ maxScrollOffset = -mScrollOffset |
+ + screenToScroll(maxScreenScrollOffset |
+ + mStackTabs[i].getSizeInScrollDirection(mCurrentMode)); |
+ } |
+ } |
+ |
+ /** |
+ * ComputeTabPosition pass 3: |
+ * Compute the position of the tabs. Adjust for top and bottom stacking. |
+ * |
+ * @param stackRect The frame of the stack. |
+ */ |
+ private void computeTabOffsetHelper(RectF stackRect) { |
+ final boolean portrait = mCurrentMode == Orientation.PORTRAIT; |
+ |
+ // Precompute the position using scroll offset and top stacking. |
+ final float parentWidth = stackRect.width(); |
+ final float parentHeight = stackRect.height(); |
+ final float overscrollPercent = computeOverscrollPercent(); |
+ final float scrollOffset = |
+ MathUtils.clamp(mScrollOffset, getMinScroll(false), getMaxScroll(false)); |
+ final float stackScale = getStackScale(stackRect); |
+ |
+ int stackedCount = 0; |
+ float minStackedPosition = 0.0f; |
+ for (int i = 0; i < mStackTabs.length; ++i) { |
+ assert mStackTabs[i] != null; |
+ StackTab stackTab = mStackTabs[i]; |
+ LayoutTab layoutTab = stackTab.getLayoutTab(); |
+ |
+ // Position |
+ final float stackScrollOffset = |
+ stackTab.isDying() ? mScrollOffsetForDyingTabs : scrollOffset; |
+ float screenScrollOffset = approxScreen(stackTab, stackScrollOffset); |
+ |
+ // Resolve top stacking |
+ screenScrollOffset = Math.max(minStackedPosition, screenScrollOffset); |
+ if (stackedCount < MAX_NUMBER_OF_STACKED_TABS_TOP) { |
+ // This make sure all the tab get stacked up as one when all the tabs do a |
+ // full roll animation. |
+ final float tiltXcos = (float) Math.cos(Math.toRadians(layoutTab.getTiltX())); |
+ final float tiltYcos = (float) Math.cos(Math.toRadians(layoutTab.getTiltY())); |
+ float collapse = Math.min(Math.abs(tiltXcos), Math.abs(tiltYcos)); |
+ collapse *= layoutTab.getAlpha(); |
+ minStackedPosition += StackTab.sStackedTabVisibleSize * collapse; |
+ } |
+ stackedCount += stackTab.isDying() ? 0 : 1; |
+ if (overscrollPercent < 0) { |
+ // Oversroll at the top of the screen. For the first |
+ // OVERSCROLL_TOP_SLIDE_PCTG of the overscroll, slide the tabs |
+ // together so they completely overlap. After that, stop scrolling the tabs. |
+ screenScrollOffset += |
+ (overscrollPercent / OVERSCROLL_TOP_SLIDE_PCTG) * screenScrollOffset; |
+ screenScrollOffset = Math.max(0, screenScrollOffset); |
+ } |
+ |
+ // Note: All the Offsets except for centering shouldn't depend on the tab's scaling |
+ // because it interferes the scaling center. |
+ |
+ // Centers the tab in its parent. |
+ float xIn = (parentWidth - layoutTab.getScaledContentWidth()) / 2.0f; |
+ float yIn = (parentHeight - layoutTab.getScaledContentHeight()) / 2.0f; |
+ |
+ // We want slight offset from the center so that multiple tab browsing |
+ // have more space to its expanding direction. e.g., On portrait mode, |
+ // there will be more space on the bottom than top. |
+ final float horizontalPadding = |
+ (parentWidth |
+ - layoutTab.getOriginalContentWidth() * StackAnimation.SCALE_AMOUNT |
+ * stackScale) / 2.0f; |
+ final float verticalPadding = |
+ (parentHeight |
+ - layoutTab.getOriginalContentHeight() * StackAnimation.SCALE_AMOUNT |
+ * stackScale) / 2.0f; |
+ |
+ if (portrait) { |
+ yIn += STACK_PORTRAIT_Y_OFFSET_PROPORTION * verticalPadding; |
+ yIn += screenScrollOffset; |
+ } else { |
+ if (LocalizationUtils.isLayoutRtl()) { |
+ xIn -= STACK_LANDSCAPE_START_OFFSET_PROPORTION * horizontalPadding; |
+ xIn -= screenScrollOffset; |
+ } else { |
+ xIn += STACK_LANDSCAPE_START_OFFSET_PROPORTION * horizontalPadding; |
+ xIn += screenScrollOffset; |
+ } |
+ yIn += STACK_LANDSCAPE_Y_OFFSET_PROPORTION * verticalPadding; |
+ } |
+ |
+ layoutTab.setX(xIn); |
+ layoutTab.setY(yIn); |
+ } |
+ |
+ // Resolve bottom stacking |
+ stackedCount = 0; |
+ float maxStackedPosition = |
+ portrait ? mLayout.getHeightMinusTopControls() : mLayout.getWidth(); |
+ for (int i = mStackTabs.length - 1; i >= 0; i--) { |
+ assert mStackTabs[i] != null; |
+ StackTab stackTab = mStackTabs[i]; |
+ LayoutTab layoutTab = stackTab.getLayoutTab(); |
+ if (stackTab.isDying()) continue; |
+ |
+ float pos; |
+ if (portrait) { |
+ pos = layoutTab.getY(); |
+ layoutTab.setY(Math.min(pos, maxStackedPosition)); |
+ } else if (LocalizationUtils.isLayoutRtl()) { |
+ // On RTL landscape, pos is a distance between tab's right and mLayout's right. |
+ float posOffset = mLayout.getWidth() |
+ - layoutTab.getOriginalContentWidth() * StackAnimation.SCALE_AMOUNT |
+ * stackScale; |
+ pos = -layoutTab.getX() + posOffset; |
+ layoutTab.setX(-Math.min(pos, maxStackedPosition) + posOffset); |
+ } else { |
+ pos = layoutTab.getX(); |
+ layoutTab.setX(Math.min(pos, maxStackedPosition)); |
+ } |
+ if (pos >= maxStackedPosition && stackedCount < MAX_NUMBER_OF_STACKED_TABS_BOTTOM) { |
+ maxStackedPosition -= StackTab.sStackedTabVisibleSize; |
+ stackedCount++; |
+ } |
+ } |
+ |
+ // final position blend |
+ final float discardRange = getDiscardRange(); |
+ for (int i = 0; i < mStackTabs.length; ++i) { |
+ assert mStackTabs[i] != null; |
+ StackTab stackTab = mStackTabs[i]; |
+ LayoutTab layoutTab = stackTab.getLayoutTab(); |
+ |
+ final float xIn = layoutTab.getX() + stackTab.getXInStackOffset(); |
+ final float yIn = layoutTab.getY() + stackTab.getYInStackOffset(); |
+ final float xOut = stackTab.getXOutOfStack(); |
+ final float yOut = stackTab.getYOutOfStack(); |
+ float x = MathUtils.interpolate(xOut, xIn, stackTab.getXInStackInfluence()); |
+ float y = MathUtils.interpolate(yOut, yIn, stackTab.getYInStackInfluence()); |
+ |
+ // Discard offsets |
+ if (stackTab.getDiscardAmount() != 0) { |
+ float discard = stackTab.getDiscardAmount(); |
+ boolean fromClick = stackTab.getDiscardFromClick(); |
+ float scale = computeDiscardScale(discard, discardRange, fromClick); |
+ float deltaX = stackTab.getDiscardOriginX() |
+ - stackTab.getLayoutTab().getOriginalContentWidth() / 2.f; |
+ float deltaY = stackTab.getDiscardOriginY() |
+ - stackTab.getLayoutTab().getOriginalContentHeight() / 2.f; |
+ float discardOffset = fromClick ? 0.f : discard; |
+ if (portrait) { |
+ x += discardOffset + deltaX * (1.f - scale); |
+ y += deltaY * (1.f - scale); |
+ } else { |
+ x += deltaX * (1.f - scale); |
+ y += discardOffset + deltaY * (1.f - scale); |
+ } |
+ } |
+ |
+ // Finally apply the stack translation |
+ layoutTab.setX(stackRect.left + x); |
+ layoutTab.setY(stackRect.top + y); |
+ } |
+ } |
+ |
+ /** |
+ * ComputeTabPosition pass 5: |
+ * Computes the clipping, visibility and adjust overall alpha if needed. |
+ */ |
+ private void computeTabClippingVisibilityHelper() { |
+ // alpha override, clipping and culling. |
+ final boolean portrait = mCurrentMode == Orientation.PORTRAIT; |
+ |
+ // Iterate through each tab starting at the top of the stack and working |
+ // backwards. Set the clip on each tab such that it does not extend past |
+ // the beginning of the tab above it. clipOffset is used to keep track |
+ // of where the previous tab started. |
+ float clipOffset; |
+ if (portrait) { |
+ // portrait LTR & RTL |
+ clipOffset = mLayout.getHeight() + StackTab.sStackedTabVisibleSize; |
+ } else if (!LocalizationUtils.isLayoutRtl()) { |
+ // landscape LTR |
+ clipOffset = mLayout.getWidth() + StackTab.sStackedTabVisibleSize; |
+ } else { |
+ // landscape RTL |
+ clipOffset = -StackTab.sStackedTabVisibleSize; |
+ } |
+ |
+ for (int i = mStackTabs.length - 1; i >= 0; i--) { |
+ LayoutTab layoutTab = mStackTabs[i].getLayoutTab(); |
+ layoutTab.setVisible(true); |
+ |
+ // Don't bother with clipping tabs that are dying, rotating, with an X offset, or |
+ // non-opaque. |
+ if (mStackTabs[i].isDying() || mStackTabs[i].getXInStackOffset() != 0.0f |
+ || layoutTab.getAlpha() < 1.0f) { |
+ layoutTab.setClipOffset(0.0f, 0.0f); |
+ layoutTab.setClipSize(Float.MAX_VALUE, Float.MAX_VALUE); |
+ continue; |
+ } |
+ |
+ // The beginning, size, and clipped size of the current tab. |
+ float tabOffset, tabSize, tabClippedSize, borderAdjustmentSize, insetBorderPadding; |
+ if (portrait) { |
+ // portrait LTR & RTL |
+ tabOffset = layoutTab.getY(); |
+ tabSize = layoutTab.getScaledContentHeight(); |
+ tabClippedSize = Math.min(tabSize, clipOffset - tabOffset); |
+ borderAdjustmentSize = mBorderTransparentTop; |
+ insetBorderPadding = mBorderTopPadding; |
+ } else if (!LocalizationUtils.isLayoutRtl()) { |
+ // landscape LTR |
+ tabOffset = layoutTab.getX(); |
+ tabSize = layoutTab.getScaledContentWidth(); |
+ tabClippedSize = Math.min(tabSize, clipOffset - tabOffset); |
+ borderAdjustmentSize = mBorderTransparentSide; |
+ insetBorderPadding = 0; |
+ } else { |
+ // landscape RTL |
+ tabOffset = layoutTab.getX() + layoutTab.getScaledContentWidth(); |
+ tabSize = layoutTab.getScaledContentWidth(); |
+ tabClippedSize = Math.min(tabSize, tabOffset - clipOffset); |
+ borderAdjustmentSize = -mBorderTransparentSide; |
+ insetBorderPadding = 0; |
+ } |
+ |
+ float absBorderAdjustmentSize = Math.abs(borderAdjustmentSize); |
+ |
+ if (tabClippedSize <= absBorderAdjustmentSize) { |
+ // If the tab is completed covered, don't bother drawing it at all. |
+ layoutTab.setVisible(false); |
+ layoutTab.setDrawDecoration(true); |
+ } else { |
+ // Fade the tab as it gets too close to the next one. This helps |
+ // prevent overlapping shadows from becoming too dark. |
+ float fade = MathUtils.clamp(((tabClippedSize - absBorderAdjustmentSize) |
+ / StackTab.sStackedTabVisibleSize), |
+ 0, 1); |
+ layoutTab.setDecorationAlpha(fade); |
+ |
+ // When tabs tilt forward, it will expose more of the tab |
+ // underneath. To compensate, make the clipping size larger. |
+ // Note, this calculation is only an estimate that seems to |
+ // work. |
+ float clipScale = 1.0f; |
+ if (layoutTab.getTiltX() > 0 || ((!portrait && LocalizationUtils.isLayoutRtl()) |
+ ? layoutTab.getTiltY() < 0 |
+ : layoutTab.getTiltY() > 0)) { |
+ final float tilt = |
+ Math.max(layoutTab.getTiltX(), Math.abs(layoutTab.getTiltY())); |
+ clipScale += (tilt / mMaxOverScrollAngle) * 0.60f; |
+ } |
+ |
+ float scaledTabClippedSize = Math.min(tabClippedSize * clipScale, tabSize); |
+ // Set the clip |
+ layoutTab.setClipOffset((!portrait && LocalizationUtils.isLayoutRtl()) |
+ ? (tabSize - scaledTabClippedSize) |
+ : 0, |
+ 0); |
+ layoutTab.setClipSize(portrait ? Float.MAX_VALUE : scaledTabClippedSize, |
+ portrait ? scaledTabClippedSize : Float.MAX_VALUE); |
+ } |
+ |
+ // Clip the next tab where this tab begins. |
+ if (i > 0) { |
+ LayoutTab nextLayoutTab = mStackTabs[i - 1].getLayoutTab(); |
+ if (nextLayoutTab.getScale() <= layoutTab.getScale()) { |
+ clipOffset = tabOffset; |
+ } else { |
+ clipOffset = tabOffset + tabClippedSize * layoutTab.getScale(); |
+ } |
+ |
+ // Extend the border just a little bit. Otherwise, the |
+ // rounded borders will intersect and make it look like the |
+ // content is actually smaller. |
+ clipOffset += borderAdjustmentSize; |
+ |
+ if (layoutTab.getBorderAlpha() < 1.f && layoutTab.getToolbarAlpha() < 1.f) { |
+ clipOffset += insetBorderPadding; |
+ } |
+ } |
+ } |
+ } |
+ |
+ /** |
+ * ComputeTabPosition pass 6: |
+ * Updates the visibility sorting value to use to figure out which thumbnails to load. |
+ * |
+ * @param stackRect The frame of the stack. |
+ */ |
+ private void computeTabVisibilitySortingHelper(RectF stackRect) { |
+ int referenceIndex = mReferenceOrderIndex; |
+ if (referenceIndex == -1) { |
+ int centerIndex = |
+ getTabIndexAtPositon(mLayout.getWidth() / 2.0f, mLayout.getHeight() / 2.0f); |
+ // Alter the center to take into account the scrolling direction. |
+ if (mCurrentScrollDirection > 0) centerIndex++; |
+ if (mCurrentScrollDirection < 0) centerIndex--; |
+ referenceIndex = MathUtils.clamp(centerIndex, 0, mStackTabs.length - 1); |
+ } |
+ |
+ final float width = mLayout.getWidth(); |
+ final float height = mLayout.getHeight(); |
+ final float left = MathUtils.clamp(stackRect.left, 0, width); |
+ final float right = MathUtils.clamp(stackRect.right, 0, width); |
+ final float top = MathUtils.clamp(stackRect.top, 0, height); |
+ final float bottom = MathUtils.clamp(stackRect.bottom, 0, height); |
+ final float stackArea = (right - left) * (bottom - top); |
+ final float layoutArea = Math.max(width * height, 1.0f); |
+ final float stackVisibilityMultiplier = stackArea / layoutArea; |
+ |
+ for (int i = 0; i < mStackTabs.length; i++) { |
+ mStackTabs[i].updateStackVisiblityValue(stackVisibilityMultiplier); |
+ mStackTabs[i].updateVisiblityValue(referenceIndex); |
+ } |
+ } |
+ |
+ /** |
+ * Determine the current amount of overscroll. If the value is 0, there is |
+ * no overscroll. If the value is < 0, tabs are overscrolling towards the |
+ * top or or left. If the value is > 0, tabs are overscrolling towards the |
+ * bottom or right. |
+ */ |
+ private float computeOverscrollPercent() { |
+ if (mOverScrollOffset >= 0) { |
+ return mOverScrollOffset / mMaxOverScroll; |
+ } else { |
+ return mOverScrollOffset / mMaxUnderScroll; |
+ } |
+ } |
+ |
+ /** |
+ * ComputeTabPosition pass 4: |
+ * Update the tilt of each tab. |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param stackRect The frame of the stack. |
+ */ |
+ private void computeTabTiltHelper(long time, RectF stackRect) { |
+ final boolean portrait = mCurrentMode == Orientation.PORTRAIT; |
+ final float parentWidth = stackRect.width(); |
+ final float parentHeight = stackRect.height(); |
+ final float overscrollPercent = computeOverscrollPercent(); |
+ |
+ // All the animations that sets the tilt value must be listed here. |
+ if (mOverviewAnimationType == OverviewAnimationType.START_PINCH |
+ || mOverviewAnimationType == OverviewAnimationType.DISCARD |
+ || mOverviewAnimationType == OverviewAnimationType.FULL_ROLL |
+ || mOverviewAnimationType == OverviewAnimationType.TAB_FOCUSED |
+ || mOverviewAnimationType == OverviewAnimationType.UNDISCARD |
+ || mOverviewAnimationType == OverviewAnimationType.DISCARD_ALL) { |
+ // Let the animation handle setting tilt values |
+ } else if (mPinch0TabIndex >= 0 || overscrollPercent == 0.0f |
+ || mOverviewAnimationType == OverviewAnimationType.REACH_TOP) { |
+ // Keep tabs flat during pinch |
+ for (int i = 0; i < mStackTabs.length; ++i) { |
+ StackTab stackTab = mStackTabs[i]; |
+ LayoutTab layoutTab = stackTab.getLayoutTab(); |
+ layoutTab.setTiltX(0, 0); |
+ layoutTab.setTiltY(0, 0); |
+ } |
+ } else if (overscrollPercent < 0) { |
+ if (mOverScrollCounter >= OVERSCROLL_FULL_ROLL_TRIGGER) { |
+ startAnimation(time, OverviewAnimationType.FULL_ROLL); |
+ mOverScrollCounter = 0; |
+ // Remove overscroll so when the animation finishes the overscroll won't |
+ // be bothering. |
+ setScrollTarget( |
+ MathUtils.clamp(mScrollOffset, getMinScroll(false), getMaxScroll(false)), |
+ false); |
+ } else { |
+ // Handle tilting tabs backwards (top or left of the tab goes away |
+ // from the camera). Each tab pivots the same amount around the |
+ // same point on the screen. The pivot point is the middle of the |
+ // top tab. |
+ |
+ float tilt = 0; |
+ if (overscrollPercent < -OVERSCROLL_TOP_SLIDE_PCTG) { |
+ // Start tilting tabs after they're done sliding together. |
+ float scaledOverscroll = (overscrollPercent + OVERSCROLL_TOP_SLIDE_PCTG) |
+ / (1 - OVERSCROLL_TOP_SLIDE_PCTG); |
+ tilt = mUnderScrollAngleInterpolator.getInterpolation(-scaledOverscroll) |
+ * -mMaxOverScrollAngle * BACKWARDS_TILT_SCALE; |
+ } |
+ |
+ float pivotOffset = 0; |
+ LayoutTab topTab = mStackTabs[mStackTabs.length - 1].getLayoutTab(); |
+ pivotOffset = portrait ? topTab.getScaledContentHeight() / 2 + topTab.getY() |
+ : topTab.getScaledContentWidth() / 2 + topTab.getX(); |
+ |
+ for (int i = 0; i < mStackTabs.length; ++i) { |
+ StackTab stackTab = mStackTabs[i]; |
+ LayoutTab layoutTab = stackTab.getLayoutTab(); |
+ if (portrait) { |
+ layoutTab.setTiltX(tilt, pivotOffset - layoutTab.getY()); |
+ } else { |
+ layoutTab.setTiltY(LocalizationUtils.isLayoutRtl() ? -tilt : tilt, |
+ pivotOffset - layoutTab.getX()); |
+ } |
+ } |
+ } |
+ } else { |
+ // Handle tilting tabs forwards (top or left of the tab comes |
+ // towards the camera). Each tab pivots around a point 1/3 of the |
+ // way down from the top/left of itself. The angle angle is scaled |
+ // based on its distance away from the top/left. |
+ |
+ float tilt = mOverScrollAngleInterpolator.getInterpolation(overscrollPercent) |
+ * mMaxOverScrollAngle; |
+ float offset = mOverscrollSlideInterpolator.getInterpolation(overscrollPercent) |
+ * mMaxOverScrollSlide; |
+ |
+ for (int i = 0; i < mStackTabs.length; ++i) { |
+ StackTab stackTab = mStackTabs[i]; |
+ LayoutTab layoutTab = stackTab.getLayoutTab(); |
+ if (portrait) { |
+ // portrait LTR & RTL |
+ float adjust = MathUtils.clamp((layoutTab.getY() / parentHeight) + 0.50f, 0, 1); |
+ layoutTab.setTiltX(tilt * adjust, layoutTab.getScaledContentHeight() / 3); |
+ layoutTab.setY(layoutTab.getY() + offset); |
+ } else if (LocalizationUtils.isLayoutRtl()) { |
+ // landscape RTL |
+ float adjust = MathUtils.clamp(-(layoutTab.getX() / parentWidth) + 0.50f, 0, 1); |
+ layoutTab.setTiltY(-tilt * adjust, layoutTab.getScaledContentWidth() * 2 / 3); |
+ layoutTab.setX(layoutTab.getX() - offset); |
+ } else { |
+ // landscape LTR |
+ float adjust = MathUtils.clamp((layoutTab.getX() / parentWidth) + 0.50f, 0, 1); |
+ layoutTab.setTiltY(tilt * adjust, layoutTab.getScaledContentWidth() / 3); |
+ layoutTab.setX(layoutTab.getX() + offset); |
+ } |
+ } |
+ } |
+ } |
+ |
+ /** |
+ * Computes the {@link LayoutTab} position from the stack and the stackTab data. |
+ * |
+ * @param time The current time of the app in ms. |
+ * @param stackRect The rectangle the stack should be drawn into. It may change over frames. |
+ */ |
+ public void computeTabPosition(long time, RectF stackRect) { |
+ if (mStackTabs == null || mStackTabs.length == 0) return; |
+ |
+ if (!mRecomputePosition) return; |
+ mRecomputePosition = false; |
+ |
+ // Step 1: Updates the {@link LayoutTab} scale, alpha and depth values. |
+ computeTabScaleAlphaDepthHelper(stackRect); |
+ |
+ // Step 2: Fix tab scroll offsets to avoid gaps. |
+ computeTabScrollOffsetHelper(); |
+ |
+ // Step 3: Compute the actual position. |
+ computeTabOffsetHelper(stackRect); |
+ |
+ // Step 4: Update the tilt of each tab. |
+ computeTabTiltHelper(time, stackRect); |
+ |
+ // Step 5: Clipping, visibility and adjust overall alpha. |
+ computeTabClippingVisibilityHelper(); |
+ |
+ // Step 6: Update visibility sorting for prioritizing thumbnail texture request. |
+ computeTabVisibilitySortingHelper(stackRect); |
+ } |
+ |
+ /** |
+ * @param stackFocus The current amount of focus of the stack [0 .. 1] |
+ * @param orderIndex The index in the stack of the focused tab. -1 to ask the |
+ * stack to compute it. |
+ */ |
+ public void setStackFocusInfo(float stackFocus, int orderIndex) { |
+ if (mStackTabs == null) return; |
+ mReferenceOrderIndex = orderIndex; |
+ for (int i = 0; i < mStackTabs.length; i++) { |
+ mStackTabs[i].getLayoutTab().setBorderCloseButtonAlpha(stackFocus); |
+ } |
+ } |
+ |
+ /** |
+ * Reverts the closure of the tab specified by {@code tabId}. This will run an undiscard |
+ * animation on that tab. |
+ * @param time The current time of the app in ms. |
+ * @param tabId The id of the tab to animate. |
+ */ |
+ public void undoClosure(long time, int tabId) { |
+ createStackTabs(true); |
+ if (mStackTabs == null) return; |
+ |
+ for (int i = 0; i < mStackTabs.length; i++) { |
+ StackTab tab = mStackTabs[i]; |
+ |
+ if (tab.getId() == tabId) { |
+ tab.setDiscardAmount(getDiscardRange()); |
+ tab.setDying(false); |
+ tab.getLayoutTab().setMaxContentHeight(mLayout.getHeightMinusTopControls()); |
+ } |
+ } |
+ |
+ mSpacing = computeSpacing(mStackTabs.length); |
+ startAnimation(time, OverviewAnimationType.UNDISCARD); |
+ } |
+ |
+ /** |
+ * Creates the {@link StackTab}s needed for display and populates {@link #mStackTabs}. |
+ * It is called from show() at the beginning of every new draw phase. It tries to reuse old |
+ * {@link StackTab} instead of creating new ones every time. |
+ * @param restoreState Whether or not to restore the {@link LayoutTab} state when we rebuild the |
+ * {@link StackTab}s. There are some properties like maximum content size |
+ * or whether or not to show the toolbar that might have to be restored if |
+ * we're calling this while the switcher is already visible. |
+ */ |
+ private void createStackTabs(boolean restoreState) { |
+ final int count = mTabModel.getCount(); |
+ if (count == 0) { |
+ cleanupTabs(); |
+ } else { |
+ StackTab[] oldTabs = mStackTabs; |
+ mStackTabs = new StackTab[count]; |
+ |
+ final boolean isIncognito = mTabModel.isIncognito(); |
+ final boolean needTitle = !mLayout.isHiding(); |
+ for (int i = 0; i < count; ++i) { |
+ Tab tab = mTabModel.getTabAt(i); |
+ int tabId = tab != null ? tab.getId() : Tab.INVALID_TAB_ID; |
+ mStackTabs[i] = findTabById(oldTabs, tabId); |
+ |
+ float maxContentWidth = -1.f; |
+ float maxContentHeight = -1.f; |
+ |
+ if (mStackTabs[i] != null && mStackTabs[i].getLayoutTab() != null && restoreState) { |
+ maxContentWidth = mStackTabs[i].getLayoutTab().getMaxContentWidth(); |
+ maxContentHeight = mStackTabs[i].getLayoutTab().getMaxContentHeight(); |
+ } |
+ |
+ LayoutTab layoutTab = mLayout.createLayoutTab(tabId, isIncognito, |
+ Layout.SHOW_CLOSE_BUTTON, needTitle, maxContentWidth, maxContentHeight); |
+ layoutTab.setInsetBorderVertical(true); |
+ layoutTab.setShowToolbar(true); |
+ layoutTab.setToolbarAlpha(0.f); |
+ layoutTab.setAnonymizeToolbar(mTabModel.index() != i); |
+ |
+ if (mStackTabs[i] == null) { |
+ mStackTabs[i] = new StackTab(layoutTab); |
+ } else { |
+ mStackTabs[i].setLayoutTab(layoutTab); |
+ } |
+ |
+ mStackTabs[i].setNewIndex(i); |
+ // The initial enterStack animation will take care of |
+ // positioning, scaling, etc. |
+ } |
+ } |
+ } |
+ |
+ private StackTab findTabById(StackTab[] layoutTabs, int id) { |
+ if (layoutTabs == null) return null; |
+ final int count = layoutTabs.length; |
+ for (int i = 0; i < count; i++) { |
+ if (layoutTabs[i].getId() == id) return layoutTabs[i]; |
+ } |
+ return null; |
+ } |
+ |
+ /** |
+ * Creates a {@link StackTab}. |
+ * This function should ONLY be called from {@link #tabCreated(long, int)} and nowhere else. |
+ * |
+ * @param id The id of the tab. |
+ * @return Whether the tab has successfully been created and added. |
+ */ |
+ private boolean createTabHelper(int id) { |
+ if (TabModelUtils.getTabById(mTabModel, id) == null) return false; |
+ |
+ // Check to see if the tab already exists in our model. This is |
+ // just to cover the case where stackEntered and then tabCreated() |
+ // called in a row. |
+ if (mStackTabs != null) { |
+ final int count = mStackTabs.length; |
+ for (int i = 0; i < count; ++i) { |
+ if (mStackTabs[i].getId() == id) { |
+ return false; |
+ } |
+ } |
+ } |
+ |
+ createStackTabs(true); |
+ |
+ return true; |
+ } |
+ |
+ private int computeSpacing(int layoutTabCount) { |
+ // This redetermines the proper spacing for the {@link StackTab}. It takes in |
+ // a parameter for the size instead of using the mStackTabs.length |
+ // property because we could be setting the spacing for a delete |
+ // before the tab has been removed (will help with animations). |
+ int spacing = 0; |
+ if (layoutTabCount > 1) { |
+ final float dimension = getScrollDimensionSize(); |
+ int minSpacing = (int) Math.max(dimension * SPACING_SCREEN, mMinSpacing); |
+ if (mStackTabs != null) { |
+ for (int i = 0; i < mStackTabs.length; i++) { |
+ assert mStackTabs[i] != null; |
+ if (!mStackTabs[i].isDying()) { |
+ minSpacing = (int) Math.min( |
+ minSpacing, mStackTabs[i].getSizeInScrollDirection(mCurrentMode)); |
+ } |
+ } |
+ } |
+ spacing = (int) ((dimension - 20) / (layoutTabCount * .8f)); |
+ spacing = Math.max(spacing, minSpacing); |
+ } |
+ return spacing; |
+ } |
+ |
+ private float getStackScale(RectF stackRect) { |
+ return mCurrentMode == Orientation.PORTRAIT |
+ ? stackRect.width() / mLayout.getWidth() |
+ : stackRect.height() / mLayout.getHeightMinusTopControls(); |
+ } |
+ |
+ private void setScrollTarget(float offset, boolean immediate) { |
+ // Ensure that the stack cannot be scrolled too far in either direction. |
+ // mScrollOffset is clamped between [-min, 0], where offset 0 has the |
+ // farthest back tab (the first tab) at the top, with everything else |
+ // pulled down, and -min has the tab at the top of the stack (the last |
+ // tab) is pulled up and fully visible. |
+ final boolean overscroll = allowOverscroll(); |
+ mScrollTarget = MathUtils.clamp(offset, getMinScroll(overscroll), getMaxScroll(overscroll)); |
+ if (immediate) mScrollOffset = mScrollTarget; |
+ mCurrentScrollDirection = Math.signum(mScrollTarget - mScrollOffset); |
+ } |
+ |
+ private float getMinScroll(boolean allowUnderScroll) { |
+ float maxOffset = 0; |
+ if (mStackTabs != null) { |
+ // The tabs are not always ordered so we need to browse them all. |
+ for (int i = 0; i < mStackTabs.length; i++) { |
+ if (!mStackTabs[i].isDying() && mStackTabs[i].getLayoutTab().isVisible()) { |
+ maxOffset = Math.max(mStackTabs[i].getScrollOffset(), maxOffset); |
+ } |
+ } |
+ } |
+ return (allowUnderScroll ? -mMaxUnderScroll : 0) - maxOffset; |
+ } |
+ |
+ /** |
+ * Gets the max scroll value. |
+ * |
+ * @param allowOverscroll True if overscroll is allowed. |
+ */ |
+ private float getMaxScroll(boolean allowOverscroll) { |
+ if (mStackTabs == null || !allowOverscroll) { |
+ return 0; |
+ } else { |
+ return mMaxOverScroll; |
+ } |
+ } |
+ |
+ private void stopScrollingMovement(long time) { |
+ // We have to cancel the fling if it is in progress. |
+ if (mScroller.computeScrollOffset(time)) { |
+ // Set the current offset and target to the current scroll |
+ // position so the {@link StackTab}s won't scroll anymore. |
+ setScrollTarget(mScroller.getCurrY(), true /* immediate */); |
+ |
+ // Tell the scroller to finish scrolling. |
+ mScroller.forceFinished(true); |
+ } else { |
+ // If we aren't scrolling just set the target to the current |
+ // offset so we don't move anymore. |
+ setScrollTarget(mScrollOffset, false); |
+ } |
+ } |
+ |
+ private boolean allowOverscroll() { |
+ // All the animations that want to leave the tilt value to be set by the overscroll must |
+ // be added here. |
+ return (mOverviewAnimationType == OverviewAnimationType.NONE |
+ || mOverviewAnimationType == OverviewAnimationType.VIEW_MORE |
+ || mOverviewAnimationType == OverviewAnimationType.ENTER_STACK) |
+ && mPinch0TabIndex < 0; |
+ } |
+ |
+ /** |
+ * Smoothes input signal. The definition of the input is lower than the |
+ * pixel density of the screen so we need to smooth the input to give the illusion of smooth |
+ * animation on screen from chunky inputs. |
+ * The combination of 20 pixels and 0.9f ensures that the output is not more than 2 pixels away |
+ * from the target. |
+ * TODO: This has nothing to do with time, just draw rate. |
+ * Is this okay or do we want to have the interpolation based on the time elapsed? |
+ * @param current The current value of the signal. |
+ * @param input The raw input value. |
+ * @return The smoothed signal. |
+ */ |
+ private float smoothInput(float current, float input) { |
+ current = MathUtils.clamp(current, input - 20, input + 20); |
+ return MathUtils.interpolate(current, input, 0.9f); |
+ } |
+ |
+ private void forceScrollStop() { |
+ mScroller.forceFinished(true); |
+ updateOverscrollOffset(); |
+ mScrollTarget = mScrollOffset; |
+ } |
+ |
+ private void updateScrollOffset(long time) { |
+ // If we are still scrolling, which is determined by a disparity |
+ // between our scroll offset and our scroll target, we need |
+ // to try to move closer to that position. |
+ if (mScrollOffset != mScrollTarget) { |
+ if (mScroller.computeScrollOffset(time)) { |
+ final float newScrollOffset = mScroller.getCurrY(); |
+ evenOutTabs(newScrollOffset - mScrollOffset, true); |
+ // We are currently in the process of being flinged. Just |
+ // ask the scroller for the new position. |
+ mScrollOffset = newScrollOffset; |
+ } else { |
+ // We are just being dragged or scrolled, not flinged. This |
+ // means we should move closer to our target quickly but not |
+ // quickly enough to show the stuttering that could be |
+ // exposed by the touch event rate. |
+ mScrollOffset = smoothInput(mScrollOffset, mScrollTarget); |
+ } |
+ requestUpdate(); |
+ } else { |
+ // Make sure that the scroller is marked as finished when the destination is reached. |
+ mScroller.forceFinished(true); |
+ } |
+ updateOverscrollOffset(); |
+ } |
+ |
+ private void updateOverscrollOffset() { |
+ float clamped = MathUtils.clamp(mScrollOffset, getMinScroll(false), getMaxScroll(false)); |
+ if (!allowOverscroll()) { |
+ mScrollOffset = clamped; |
+ } |
+ float overscroll = mScrollOffset - clamped; |
+ |
+ // Counts the number of overscroll push in the same direction in a row. |
+ int derivativeState = (int) Math.signum(Math.abs(mOverScrollOffset) - Math.abs(overscroll)); |
+ if (derivativeState != mOverScrollDerivative && derivativeState == 1 && overscroll < 0) { |
+ mOverScrollCounter++; |
+ } else if (overscroll > 0 || mCurrentMode == Orientation.LANDSCAPE) { |
+ mOverScrollCounter = 0; |
+ } |
+ mOverScrollDerivative = derivativeState; |
+ |
+ mOverScrollOffset = overscroll; |
+ } |
+ |
+ private void resetAllScrollOffset() { |
+ if (mTabModel == null) return; |
+ // Reset the scroll position to put the important {@link StackTab} into focus. |
+ // This does not scroll the {@link StackTab}s there but rather moves everything |
+ // there immediately. |
+ // The selected tab is supposed to show at the center of the screen. |
+ float maxTabsPerPage = getScrollDimensionSize() / mSpacing; |
+ float centerOffsetIndex = maxTabsPerPage / 2.0f - 0.5f; |
+ final int count = mTabModel.getCount(); |
+ final int index = mTabModel.index(); |
+ if (index < centerOffsetIndex || count <= maxTabsPerPage) { |
+ mScrollOffset = 0; |
+ } else if (index == count - 1 && Math.ceil(maxTabsPerPage) < count) { |
+ mScrollOffset = (maxTabsPerPage - count - 1) * mSpacing; |
+ } else if ((count - index - 1) < centerOffsetIndex) { |
+ mScrollOffset = (maxTabsPerPage - count) * mSpacing; |
+ } else { |
+ mScrollOffset = (centerOffsetIndex - index) * mSpacing; |
+ } |
+ // Reset the scroll offset of the tabs too. |
+ if (mStackTabs != null) { |
+ for (int i = 0; i < mStackTabs.length; i++) { |
+ mStackTabs[i].setScrollOffset(screenToScroll(i * mSpacing)); |
+ } |
+ } |
+ setScrollTarget(mScrollOffset, false); |
+ } |
+ |
+ private float approxScreen(StackTab tab, float globalScrollOffset) { |
+ return StackTab.scrollToScreen(tab.getScrollOffset() + globalScrollOffset, mWarpSize); |
+ } |
+ |
+ private float scrollToScreen(float scrollSpace) { |
+ return StackTab.scrollToScreen(scrollSpace, mWarpSize); |
+ } |
+ |
+ private float screenToScroll(float screenSpace) { |
+ return StackTab.screenToScroll(screenSpace, mWarpSize); |
+ } |
+ |
+ /** |
+ * @return The range of the discard action. At the end of the +/- range the discarded tab |
+ * will be fully transparent. |
+ */ |
+ private float getDiscardRange() { |
+ return getRange(DISCARD_RANGE_SCREEN); |
+ } |
+ |
+ private float getRange(float range) { |
+ return range * (mCurrentMode == Orientation.PORTRAIT ? mLayout.getWidth() |
+ : mLayout.getHeightMinusTopControls()); |
+ } |
+ |
+ /** |
+ * Computes the scale of the tab based on its discard status. |
+ * |
+ * @param amount The discard amount. |
+ * @param range The range of the absolute value of discard amount. |
+ * @param fromClick Whether or not the discard was from a click or a swipe. |
+ * @return The scale of the tab to use to draw the tab. |
+ */ |
+ public static float computeDiscardScale(float amount, float range, boolean fromClick) { |
+ if (Math.abs(amount) < 1.0f) return 1.0f; |
+ float t = amount / range; |
+ float endScale = fromClick ? DISCARD_END_SCALE_CLICK : DISCARD_END_SCALE_SWIPE; |
+ return MathUtils.interpolate(1.0f, endScale, Math.abs(t)); |
+ } |
+ |
+ /** |
+ * Computes the alpha value of the tab based on its discard status. |
+ * |
+ * @param amount The discard amount. |
+ * @param range The range of the absolute value of discard amount. |
+ * @return The alpha value that need to be applied on the tab. |
+ */ |
+ public static float computeDiscardAlpha(float amount, float range) { |
+ if (Math.abs(amount) < 1.0f) return 1.0f; |
+ float t = amount / range; |
+ t = MathUtils.clamp(t, -1.0f, 1.0f); |
+ return 1.f - Math.abs(t); |
+ } |
+ |
+ private void updateCurrentMode(int orientation) { |
+ mCurrentMode = orientation; |
+ mDiscardDirection = getDefaultDiscardDirection(); |
+ setWarpState(true, false); |
+ final float opaqueTopPadding = mBorderTopPadding - mBorderTransparentTop; |
+ mAnimationFactory = StackAnimation.createAnimationFactory(mLayout.getWidth(), |
+ mLayout.getHeight(), mLayout.getHeightMinusTopControls(), mBorderTopPadding, |
+ opaqueTopPadding, mBorderLeftPadding, mCurrentMode); |
+ float dpToPx = mLayout.getContext().getResources().getDisplayMetrics().density; |
+ mViewAnimationFactory = new StackViewAnimation(dpToPx, mLayout.getWidth()); |
+ if (mStackTabs == null) return; |
+ float width = mLayout.getWidth(); |
+ float height = mLayout.getHeightMinusTopControls(); |
+ for (int i = 0; i < mStackTabs.length; i++) { |
+ LayoutTab tab = mStackTabs[i].getLayoutTab(); |
+ if (tab == null) continue; |
+ tab.setMaxContentWidth(width); |
+ tab.setMaxContentHeight(height); |
+ } |
+ } |
+ |
+ /** |
+ * Called to release everything. Called well after the view has been really hidden. |
+ */ |
+ public void cleanupTabs() { |
+ mStackTabs = null; |
+ resetInputActionIndices(); |
+ } |
+ |
+ /** |
+ * Resets all the indices that are pointing to tabs for various features. |
+ */ |
+ private void resetInputActionIndices() { |
+ mPinch0TabIndex = -1; |
+ mPinch1TabIndex = -1; |
+ mScrollingTab = null; |
+ mDiscardingTab = null; |
+ mLongPressSelected = -1; |
+ } |
+ |
+ /** |
+ * Invalidates the current graphics and force to recomputes tab placements. |
+ */ |
+ public void requestUpdate() { |
+ mRecomputePosition = true; |
+ mLayout.requestUpdate(); |
+ } |
+ |
+ /** |
+ * Reset session based parameters. |
+ * Called before the a session starts. Before the show, regardless if the stack is displayable. |
+ */ |
+ public void reset() { |
+ mIsDying = false; |
+ } |
+ |
+ /** |
+ * Whether or not the tab positions warp from linear to nonlinear as the tabs approach the edge |
+ * of the screen. This allows us to move the tabs to linear space to track finger movements, |
+ * but also move them back to non-linear space without any visible change to the user. |
+ * @param canWarp Whether or not the tabs are allowed to warp. |
+ * @param adjustCurrentTabs Whether or not to change the tab positions so there's no visible |
+ * difference after the change. |
+ */ |
+ private void setWarpState(boolean canWarp, boolean adjustCurrentTabs) { |
+ float warp = canWarp ? getScrollDimensionSize() * SCROLL_WARP_PCTG : 0.f; |
+ |
+ if (mStackTabs != null && adjustCurrentTabs && Float.compare(warp, mWarpSize) != 0) { |
+ float scrollOffset = |
+ MathUtils.clamp(mScrollOffset, getMinScroll(false), getMaxScroll(false)); |
+ for (int i = 0; i < mStackTabs.length; i++) { |
+ StackTab tab = mStackTabs[i]; |
+ float tabScrollOffset = tab.getScrollOffset(); |
+ float tabScrollSpace = tabScrollOffset + scrollOffset; |
+ float tabScreen = StackTab.scrollToScreen(tabScrollSpace, mWarpSize); |
+ float tabScrollSpaceFinal = StackTab.screenToScroll(tabScreen, warp); |
+ float scrollDelta = tabScrollSpaceFinal - tabScrollSpace; |
+ tab.setScrollOffset(tabScrollOffset + scrollDelta); |
+ } |
+ } |
+ |
+ mWarpSize = warp; |
+ } |
+ |
+ /** |
+ * Called when the swipe animation get initiated. It gives a chance to initialize everything. |
+ * @param time The current time of the app in ms. |
+ * @param direction The direction the swipe is in. |
+ * @param x The horizontal coordinate the swipe started at in dp. |
+ * @param y The vertical coordinate the swipe started at in dp. |
+ */ |
+ public void swipeStarted(long time, ScrollDirection direction, float x, float y) { |
+ if (direction != ScrollDirection.DOWN) return; |
+ |
+ // Turn off warping the tabs because we need them to track the user's finger. |
+ setWarpState(false, false); |
+ |
+ // Restart the enter stack animation with the new warp values. |
+ startAnimation(time, OverviewAnimationType.ENTER_STACK); |
+ |
+ // Update the scroll offset to put the focused tab at the top. |
+ final int index = mTabModel.index(); |
+ |
+ if (mCurrentMode == Orientation.PORTRAIT) { |
+ mScrollOffset = -index * mSpacing; |
+ } else { |
+ mScrollOffset = -index * mSpacing + x - LANDSCAPE_SWIPE_DRAG_TAB_OFFSET_DP; |
+ mScrollOffset = |
+ MathUtils.clamp(mScrollOffset, getMinScroll(false), getMaxScroll(false)); |
+ } |
+ setScrollTarget(mScrollOffset, true); |
+ |
+ // Don't let the tabs even out during this scroll. |
+ mEvenOutProgress = 1.f; |
+ |
+ // Set up the tracking scroll parameters. |
+ mSwipeUnboundScrollOffset = mScrollOffset; |
+ mSwipeBoundedScrollOffset = mScrollOffset; |
+ |
+ // Reset other state. |
+ mSwipeIsCancelable = false; |
+ mSwipeCanScroll = false; |
+ mInSwipe = true; |
+ } |
+ |
+ /** |
+ * Updates a swipe gesture. |
+ * @param time The current time of the app in ms. |
+ * @param x The horizontal coordinate the swipe is currently at in dp. |
+ * @param y The vertical coordinate the swipe is currently at in dp. |
+ * @param dx The horizontal delta since the last update in dp. |
+ * @param dy The vertical delta since the last update in dp. |
+ * @param tx The horizontal difference between the start and the current position in dp. |
+ * @param ty The vertical difference between the start and the current position in dp. |
+ */ |
+ public void swipeUpdated(long time, float x, float y, float dx, float dy, float tx, float ty) { |
+ if (!mInSwipe) return; |
+ |
+ final float toolbarSize = mLayout.getHeight() - mLayout.getHeightMinusTopControls(); |
+ if (ty > toolbarSize) mSwipeCanScroll = true; |
+ if (!mSwipeCanScroll) return; |
+ |
+ final int index = mTabModel.index(); |
+ |
+ // Check to make sure the index is still valid. |
+ if (index < 0 || index >= mStackTabs.length) { |
+ assert false : "Tab index out of bounds in Stack#swipeUpdated()"; |
+ return; |
+ } |
+ |
+ final float delta = mCurrentMode == Orientation.PORTRAIT ? dy : dx; |
+ |
+ // Update the unbound scroll offset, tracking delta regardless of constraints. |
+ mSwipeUnboundScrollOffset += delta; |
+ |
+ // Figure out the new constrained position. |
+ final float minScroll = getMinScroll(true); |
+ final float maxScroll = getMaxScroll(true); |
+ float offset = MathUtils.clamp(mSwipeUnboundScrollOffset, minScroll, maxScroll); |
+ |
+ final float constrainedDelta = offset - mSwipeBoundedScrollOffset; |
+ mSwipeBoundedScrollOffset = offset; |
+ |
+ if (constrainedDelta == 0.f) return; |
+ |
+ if (mCurrentMode == Orientation.PORTRAIT) { |
+ dy = constrainedDelta; |
+ } else { |
+ dx = constrainedDelta; |
+ } |
+ |
+ // Propagate the new drag event. |
+ drag(time, x, y, dx, dy); |
+ |
+ // Figure out if the user has scrolled down enough that they can scroll back up and exit. |
+ if (mCurrentMode == Orientation.PORTRAIT) { |
+ // The cancelable threshold is determined by the top position of the tab in the stack. |
+ final float discardOffset = mStackTabs[index].getScrollOffset(); |
+ final boolean beyondThreshold = -mScrollOffset < discardOffset; |
+ |
+ // Allow the user to cancel in the future if they're beyond the threshold. |
+ mSwipeIsCancelable |= beyondThreshold; |
+ |
+ // If the user can cancel the swipe and they're back behind the threshold, cancel. |
+ if (mSwipeIsCancelable && !beyondThreshold) swipeCancelled(time); |
+ } else { |
+ // The cancelable threshold is determined by the top position of the tab. |
+ final float discardOffset = mStackTabs[index].getLayoutTab().getY(); |
+ |
+ boolean aboveThreshold = discardOffset < getRange(SWIPE_LANDSCAPE_THRESHOLD); |
+ |
+ mSwipeIsCancelable |= !aboveThreshold; |
+ |
+ if (mSwipeIsCancelable && aboveThreshold) swipeCancelled(time); |
+ } |
+ } |
+ |
+ /** |
+ * Called when the swipe ends; most likely on finger up event. It gives a chance to start |
+ * an ending animation to exit the mode gracefully. |
+ * @param time The current time of the app in ms. |
+ */ |
+ public void swipeFinished(long time) { |
+ if (!mInSwipe) return; |
+ |
+ mInSwipe = false; |
+ |
+ // Reset the warp state and mark the tabs to even themselves out. |
+ setWarpState(true, true); |
+ mEvenOutProgress = 0.f; |
+ |
+ onUpOrCancel(time); |
+ } |
+ |
+ /** |
+ * Called when the user has cancelled a swipe; most likely if they have dragged their finger |
+ * back to the starting position. Some handlers will throw swipeFinished() instead. |
+ * @param time The current time of the app in ms. |
+ */ |
+ public void swipeCancelled(long time) { |
+ if (!mInSwipe) return; |
+ |
+ mDiscardingTab = null; |
+ |
+ mInSwipe = false; |
+ |
+ setWarpState(true, true); |
+ mEvenOutProgress = 0.f; |
+ |
+ // Select the current tab so we exit the switcher. |
+ Tab tab = TabModelUtils.getCurrentTab(mTabModel); |
+ mLayout.uiSelectingTab(time, tab != null ? tab.getId() : Tab.INVALID_TAB_ID); |
+ } |
+ |
+ /** |
+ * Fling from a swipe gesture. |
+ * @param time The current time of the app in ms. |
+ * @param x The horizontal coordinate the swipe is currently at in dp. |
+ * @param y The vertical coordinate the swipe is currently at in dp. |
+ * @param tx The horizontal difference between the start and the current position in dp. |
+ * @param ty The vertical difference between the start and the current position in dp. |
+ * @param vx The horizontal velocity of the fling. |
+ * @param vy The vertical velocity of the fling. |
+ */ |
+ public void swipeFlingOccurred( |
+ long time, float x, float y, float tx, float ty, float vx, float vy) { |
+ if (!mInSwipe) return; |
+ |
+ // Propagate the fling data. |
+ fling(time, x, y, vx, vy); |
+ |
+ onUpOrCancel(time); |
+ } |
+} |