blob: 742c07d99efd893552f21528e4b20ca5d10a824c [file] [log] [blame]
// 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.support.annotation.IntDef;
import android.util.Pair;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.compositor.animation.CompositorAnimator;
import org.chromium.chrome.browser.compositor.animation.FloatProperty;
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.ScrollDirection;
import org.chromium.chrome.browser.compositor.layouts.phone.StackLayoutBase;
import org.chromium.chrome.browser.compositor.layouts.phone.stack.StackAnimation.OverviewAnimationType;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabList;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.util.MathUtils;
import org.chromium.ui.base.LocalizationUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Iterator;
/**
* Handles all the drawing and events of a stack of stackTabs.
*
* @VisibleForTesting
*/
public abstract class Stack implements ChromeAnimation.Animatable {
@IntDef({Property.SCROLL_OFFSET})
@Retention(RetentionPolicy.SOURCE)
public @interface Property {
int SCROLL_OFFSET = 0;
}
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;
@IntDef({DragLock.NONE, DragLock.SCROLL, DragLock.DISCARD})
@Retention(RetentionPolicy.SOURCE)
public @interface DragLock {
int NONE = 0;
int SCROLL = 1;
int DISCARD = 2;
}
/**
* 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
protected TabList mTabList;
// True when the stack is still visible for animation but it is going to be empty.
private boolean mIsDying;
// Screen State Variables
protected int mSpacing;
protected StackTab[] mStackTabs; // mStackTabs can be null if there are no tabs
// Overscroll
protected StackScroller mScroller;
private float mOverScrollOffset;
private int mOverScrollDerivative;
private int mOverScrollCounter;
private float mMaxOverScroll; // This will be updated from dimens.xml
protected float mMaxUnderScroll;
protected float mMaxOverScrollAngle; // This will be updated from values.xml
private float mMaxOverScrollSlide;
private final Interpolator mOverScrollAngleInterpolator =
new AccelerateDecelerateInterpolator();
private final Interpolator mUnderScrollAngleInterpolator =
CompositorAnimator.DECELERATE_INTERPOLATOR;
private final Interpolator mOverscrollSlideInterpolator =
new AccelerateDecelerateInterpolator();
// Drag Lock
private @DragLock int mDragLock = DragLock.NONE;
private long mLastScrollUpdate;
private float mMinScrollMotion;
// Scrolling Variables
protected float mScrollTarget;
protected float mScrollOffset;
private float mScrollOffsetForDyingTabs;
protected float mCurrentScrollDirection;
protected StackTab mScrollingTab;
// Swipe Variables
private float mSwipeUnboundScrollOffset;
private float mSwipeBoundedScrollOffset;
private boolean mSwipeIsCancelable;
private boolean mSwipeCanScroll;
protected boolean mInSwipe;
// Discard
protected 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 int mReferenceOrderIndex = -1;
// Orientation Variables
protected @Orientation int mCurrentMode = Orientation.PORTRAIT;
// Animation Variables
protected @OverviewAnimationType int mOverviewAnimationType = OverviewAnimationType.NONE;
private StackAnimation mAnimationFactory;
private StackViewAnimation mViewAnimationFactory;
// Running set of animations applied to tabs.
private ChromeAnimation<?> mTabAnimations;
private Pair<AnimatorSet, ArrayList<FloatProperty>> mAnimatorSetTabAnimations;
private Animator mViewAnimations;
// The parent Layout
protected final StackLayoutBase mLayout;
// Border values
protected float mBorderTransparentTop;
protected float mBorderTransparentSide;
// TODO(dtrainor): Expose 9-patch padding from resource manager.
protected float mBorderTopPadding;
private float mBorderLeftPadding;
private boolean mIsStackForCurrentTabList;
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, StackLayoutBase layout) {
mLayout = layout;
contextChanged(context);
}
/**
* @param tabList The list to attach to this stack.
*/
public void setTabList(TabList tabList) {
mTabList = tabList;
}
/**
* @return The TabList associated with this stack.
*/
public TabList getTabList() {
return mTabList;
}
/**
* @return The {@link StackTab}s currently being rendered by the tab stack.
* @VisibleForTesting
*/
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;
}
/**
* The scale the tabs should be currently shown at (may change based on how many are open).
*/
public abstract float getScaleAmount();
/**
* @return The index of the currently centered tab. If we're not currently snapped to a tab
* (e.g. we're in the process of animating a scroll or the user is currently dragging),
* returns the index of the tab closest to the center.
*/
public abstract int getCenteredTabIndex();
/*
* 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;
}
/**
* @return True if we should put the close button on the right side of the tab, or false if
* we should put it on the left. This method already accounts for RTL flipping.
*/
private boolean isCloseButtonOnRight() {
if (ChromeFeatureList.isEnabled(ChromeFeatureList.HORIZONTAL_TAB_SWITCHER_ANDROID)) {
return !LocalizationUtils.isLayoutRtl();
}
return mCurrentMode == Orientation.PORTRAIT ^ LocalizationUtils.isLayoutRtl();
}
/**
* 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) {
for (int i = 0; i < mStackTabs.length; i++) {
StackTab tab = mStackTabs[i];
tab.setDiscardOriginY(0.f);
tab.setDiscardOriginX(isCloseButtonOnRight()
? tab.getLayoutTab().getOriginalContentWidth()
: 0.f);
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(mTabList, id), TabList.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(mTabList, 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 TabList represented by this TabStackState should be displayed.
*/
public boolean isDisplayable() {
return !mTabList.isIncognito() || (!mIsDying && mTabList.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.
* @param isStackForCurrentTabList Whether this {@link Stack} is for the current tab list.
*/
public void show(boolean isStackForCurrentTabList) {
mIsStackForCurrentTabList = isStackForCurrentTabList;
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.
*/
protected void startAnimation(long time, @OverviewAnimationType int type) {
startAnimation(time, type, TabList.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 int type, boolean finishImmediately) {
startAnimation(time, type, TabList.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.
*/
protected void startAnimation(long time, @OverviewAnimationType int type, int sourceIndex,
boolean finishImmediately) {
startAnimation(time, type, mTabList.index(), sourceIndex, finishImmediately);
}
private void startAnimation(long time, @OverviewAnimationType int 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.createAnimatorForType(
type, mStackTabs, mLayout.getViewContainer(), mTabList, 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.createChromeAnimationSetForType(type, this,
mStackTabs, focusIndex, sourceIndex, mSpacing, getDiscardRange());
mAnimatorSetTabAnimations = mAnimationFactory.createAnimatorSetForType(type, this,
mStackTabs, focusIndex, sourceIndex, mSpacing, getDiscardRange());
}
if (mTabAnimations != null) mTabAnimations.start();
if (mAnimatorSetTabAnimations != null) mAnimatorSetTabAnimations.first.start();
if (mViewAnimations != null) mViewAnimations.start();
if (mTabAnimations != null || mAnimatorSetTabAnimations != null
|| mViewAnimations != null) {
mLayout.onStackAnimationStarted();
}
if ((mTabAnimations == null && mAnimatorSetTabAnimations == null
&& mViewAnimations == null)
|| finishImmediately) {
finishAnimation(time);
}
}
mLayout.requestUpdate();
}
/**
* Performs the necessary actions to finish the current animation.
*
* @param time The current time of the app in ms.
*/
protected void finishAnimation(long time) {
if (mTabAnimations != null) mTabAnimations.updateAndFinish();
if (mAnimatorSetTabAnimations != null) mAnimatorSetTabAnimations.first.end();
if (mViewAnimations != null) mViewAnimations.end();
if (mTabAnimations != null || mAnimatorSetTabAnimations != null
|| mViewAnimations != null) {
mLayout.onStackAnimationFinished();
}
switch (mOverviewAnimationType) {
case OverviewAnimationType.ENTER_STACK:
mLayout.uiDoneEnteringStack();
break;
case OverviewAnimationType.FULL_ROLL:
for (int i = 0; i < mStackTabs.length; i++) {
mStackTabs[i].getLayoutTab().setTiltX(0, 0);
mStackTabs[i].getLayoutTab().setTiltY(0, 0);
}
springBack(time);
break;
case OverviewAnimationType.TAB_FOCUSED:
// Purposeful fall through
case OverviewAnimationType.NEW_TAB_OPENED:
// Nothing to do.
break;
case OverviewAnimationType.DISCARD_ALL:
mLayout.uiDoneClosingAllTabs(mTabList.isIncognito());
cleanupStackTabState();
break;
case OverviewAnimationType.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 OverviewAnimationType.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, mTabList.isIncognito());
}
}
}
cleanupStackTabState();
break;
default:
break;
}
// sync the scrollTarget and scrollOffset. For ENTER_STACK animation, don't sync to
// ensure the tab can tilt back.
if (mOverviewAnimationType != OverviewAnimationType.NONE
&& mOverviewAnimationType != OverviewAnimationType.ENTER_STACK
&& mScroller.isFinished()) {
setScrollTarget(mScrollOffset, true);
}
mOverviewAnimationType = OverviewAnimationType.NONE;
mTabAnimations = null;
mAnimatorSetTabAnimations = 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 int 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);
if (mAnimatorSetTabAnimations != null) {
Iterator<FloatProperty> propertyIterator =
mAnimatorSetTabAnimations.second.iterator();
Iterator<Animator> animatorIterator =
mAnimatorSetTabAnimations.first.getChildAnimations().iterator();
while (animatorIterator.hasNext()) {
CompositorAnimator a = (CompositorAnimator) animatorIterator.next();
if (propertyIterator.next() == StackTab.SCROLL_OFFSET) a.cancel();
}
}
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 chromeAnimationsFinished = true;
if (mTabAnimations != null) {
chromeAnimationsFinished =
jumpToEnd ? mTabAnimations.finished() : mTabAnimations.update(time);
}
boolean animatorSetFinished = true;
if (mAnimatorSetTabAnimations != null) {
animatorSetFinished = jumpToEnd ? true : !mAnimatorSetTabAnimations.first.isRunning();
}
if (mTabAnimations != null || mAnimatorSetTabAnimations != null) {
finishAnimationsIfDone(time, jumpToEnd);
}
if (jumpToEnd) forceScrollStop();
return chromeAnimationsFinished && animatorSetFinished;
}
private void finishAnimationsIfDone(long time, boolean jumpToEnd) {
boolean hasViewAnimations = mViewAnimations != null;
boolean isViewFinished = hasViewAnimations ? !mViewAnimations.isRunning() : true;
boolean hasTabAnimations = mTabAnimations != null;
boolean isTabFinished = hasTabAnimations ? mTabAnimations.finished() : true;
boolean hasAnimatorSetTabAnimations = mAnimatorSetTabAnimations != null;
boolean isAnimatorSetTabFinished =
hasAnimatorSetTabAnimations ? !mAnimatorSetTabAnimations.first.isRunning() : true;
boolean hasAnimations =
hasViewAnimations || hasTabAnimations || hasAnimatorSetTabAnimations;
boolean shouldFinish = jumpToEnd && hasAnimations;
shouldFinish |= hasAnimations && (!hasViewAnimations || isViewFinished)
&& (!hasTabAnimations || isTabFinished)
&& (!hasAnimatorSetTabAnimations || isAnimatorSetTabFinished);
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 int computeDragLock(float scrollDrag, float discardDrag) {
scrollDrag = Math.abs(scrollDrag);
discardDrag = Math.abs(discardDrag);
@DragLock
int 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
int 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);
}
mLayout.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 (mTabList.index() < 0) return;
mDiscardingTab = mStackTabs[mTabList.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 = mTabList.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);
}
/**
* OverlappingStack implements this to auto-magically the cards as the stack get scrolled.
* NonOverlappingStack just ignores this call.
*
* @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.
*/
protected abstract boolean evenOutTabs(float amount, boolean allowReverseDirection);
/**
* 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 abstract void onLongPress(long time, float x, float y);
/**
* 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 abstract void onPinch(
long time, float x0, float y0, float x1, float y1, boolean firstEvent);
/**
* 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.
*/
protected 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;
mLayout.requestUpdate();
}
/**
* Called on touch up or cancel event.
*/
public void onUpOrCancel(long time) {
// Commit or uncommit discard tab
commitDiscard(time, true);
resetInputActionIndices();
springBack(time);
}
/**
* Bounces the scroll position back to a valid value (e.g. to correct an overscroll or
* implement snapping).
*/
protected abstract void springBack(long time);
/**
* 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.
if (mStackTabs[clicked].getLayoutTab().checkCloseHitTest(x, y)) {
// 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(isCloseButtonOnRight() ? contentWidth - halfCloseBtnWidth
: 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;
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, @Orientation int orientation) {
updateCurrentMode(orientation);
// Changing the orientation can change which side of the tab we want to show the close
// button on (if the horizontal tab switcher experiment is not enabled).
if (mStackTabs == null) return;
boolean closeButtonIsOnRight = isCloseButtonOnRight();
for (int i = 0; i < mStackTabs.length; i++) {
mStackTabs[i].getLayoutTab().setCloseButtonIsOnRight(closeButtonIsOnRight);
}
}
protected float getScrollDimensionSize() {
return mCurrentMode == Orientation.PORTRAIT ? mLayout.getHeightMinusBrowserControls()
: 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.
*/
protected 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));
}
}
/**
* @return Whether or not to enable logic that gives the tabs a "stacked" appearance at the
* top (in portrait mode) or left (in landscape mode).
*/
protected abstract boolean shouldStackTabsAtTop();
/**
* @return Whether or not to enable logic that gives the tabs a "stacked" appearance at the
* bottom (in portrait mode) or right (in landscape mode).
*/
protected abstract boolean shouldStackTabsAtBottom();
/**
* @return How much the stack should adjust the y position of each LayoutTab in portrait
* mode (as a fraction of the amount space that would be above and below the tab if
* it were centered).
*/
protected abstract float getStackPortraitYOffsetProportion();
/**
* @return How much the stack should adjust the x position of each LayoutTab in landscape
* mode (as a fraction of the amount space that would be to the left and right of
* the tab if it were centered).
*/
protected abstract float getStackLandscapeStartOffsetProportion();
/**
* @return How much the stack should adjust the x position of each LayoutTab in portrait
* mode (as a fraction of the amount space that would be above and below the tab if
* it were centered).
*/
protected abstract float getStackLandscapeYOffsetProportion();
/**
* 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);
if (shouldStackTabsAtTop()) {
// 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() * getScaleAmount() * stackScale)
/ 2.0f;
final float verticalPadding =
(parentHeight
- layoutTab.getOriginalContentHeight() * getScaleAmount() * stackScale)
/ 2.0f;
if (portrait) {
yIn += getStackPortraitYOffsetProportion() * verticalPadding;
yIn += screenScrollOffset;
} else {
if (LocalizationUtils.isLayoutRtl()) {
xIn -= getStackLandscapeStartOffsetProportion() * horizontalPadding;
xIn -= screenScrollOffset;
} else {
xIn += getStackLandscapeStartOffsetProportion() * horizontalPadding;
xIn += screenScrollOffset;
}
yIn += getStackLandscapeYOffsetProportion() * verticalPadding;
}
layoutTab.setX(xIn);
layoutTab.setY(yIn);
}
if (shouldStackTabsAtBottom()) {
// Resolve bottom stacking
stackedCount = 0;
float maxStackedPosition =
portrait ? mLayout.getHeightMinusBrowserControls() : 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() * getScaleAmount() * 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.
*/
protected abstract void computeTabClippingVisibilityHelper();
/**
* Computes the index that should be assumed to be the currently centered tab, for purposes
* of prioritizing which thumbnails to render.
*/
protected abstract int computeReferenceIndex();
/**
* 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) referenceIndex = computeReferenceIndex();
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;
}
}
/**
* Update the tilt of each tab for full roll if necessary.
*
* @param time The current time of the app in ms.
* @param stackRect The frame of the stack.
*/
private void fullRollHelper(long time, RectF stackRect) {
if (mOverviewAnimationType != OverviewAnimationType.FULL_ROLL
&& computeOverscrollPercent() < 0
&& 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);
}
}
/** Whether or not to apply logic to enforce that there are no gaps between tabs. */
protected abstract boolean shouldCloseGapsBetweenTabs();
/**
* 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;
// Step 1: Updates the {@link LayoutTab} scale, alpha and depth values.
computeTabScaleAlphaDepthHelper(stackRect);
if (shouldCloseGapsBetweenTabs()) {
// Step 2: Fix tab scroll offsets to avoid gaps.
computeTabScrollOffsetHelper();
}
// Step 3: Compute the actual position.
computeTabOffsetHelper(stackRect);
// Step 4: Test if the full-roll animation needs to be run.
fullRollHelper(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(getMaxTabHeight());
}
}
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 = mTabList.getCount();
if (count == 0) {
cleanupTabs();
} else {
StackTab[] oldTabs = mStackTabs;
mStackTabs = new StackTab[count];
final boolean isIncognito = mTabList.isIncognito();
final boolean needTitle = !mLayout.isHiding();
for (int i = 0; i < count; ++i) {
Tab tab = mTabList.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(!mIsStackForCurrentTabList || mTabList.index() != i);
layoutTab.setCloseButtonIsOnRight(isCloseButtonOnRight());
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(mTabList, 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;
}
/**
* @return The percentage of the screen that defines the spacing between tabs by default (no
* pinch).
*/
protected abstract float getSpacingScreen();
/**
* 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).
* @param layoutTabCount The number of layout tabs currently in the Stack.
* @return How far apart the tabs should be spaced (modulo certain
* adjustments, such as non-linear warping).
*/
protected abstract int computeSpacing(int layoutTabCount);
private float getStackScale(RectF stackRect) {
return mCurrentMode == Orientation.PORTRAIT
? stackRect.width() / mLayout.getWidth()
: stackRect.height() / mLayout.getHeightMinusBrowserControls();
}
protected 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);
}
/**
* Gets the min scroll value.
*
* @param allowUnderScroll True if underscroll is allowed.
*/
protected abstract float getMinScroll(boolean allowUnderScroll);
/**
* Gets the max scroll value.
*
* @param allowOverscroll True if overscroll is allowed.
*/
protected 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);
}
}
protected 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;
}
/**
* 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);
}
protected 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);
}
mLayout.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;
}
/**
* Called when the stack is opened to reset all the tab and scroll positions.
*/
protected abstract void resetAllScrollOffset();
protected float approxScreen(StackTab tab, float globalScrollOffset) {
return scrollToScreen(tab.getScrollOffset() + globalScrollOffset);
}
/**
* Maps from scroll coordinates to screen coordinates.
* @param scrollSpace The offset in scroll space.
* @return The offset on screen corresponding to the scroll space offset.
*/
public abstract float scrollToScreen(float scrollSpace);
/**
* Maps from screen coordinates to scroll coordinates. This allows Stack subclasses (e.g.
* OverlappingStack) to use non-linear scrolling.
* @param scrollSpace The offset in screen space.
* @return The offset in scroll space corresponding to the offset on screen.
*/
public abstract float screenToScroll(float screenSpace);
/**
* @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.getHeightMinusBrowserControls());
}
/**
* @return The maximum height of a layout tab in the tab switcher.
*/
public abstract float getMaxTabHeight();
/**
* @return The current spacing between tabs.
*/
public float getSpacing() {
return mSpacing;
}
/**
* @return The current overall scroll offset for the Stack.
*/
public float getScrollOffset() {
return mScrollOffset;
}
/**
* 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);
}
protected void updateCurrentMode(@Orientation int orientation) {
if (ChromeFeatureList.isEnabled(ChromeFeatureList.HORIZONTAL_TAB_SWITCHER_ANDROID)) {
mCurrentMode = Orientation.LANDSCAPE;
} else {
mCurrentMode = orientation;
}
mDiscardDirection = getDefaultDiscardDirection();
final float opaqueTopPadding = mBorderTopPadding - mBorderTransparentTop;
mAnimationFactory = StackAnimation.createAnimationFactory(this, mLayout.getWidth(),
mLayout.getHeight(), mLayout.getTopBrowserControlsHeight(), mBorderTopPadding,
opaqueTopPadding, mBorderLeftPadding, mCurrentMode);
mViewAnimationFactory = new StackViewAnimation(mLayout.getContext().getResources());
if (mStackTabs == null) return;
float width = mLayout.getWidth();
for (int i = 0; i < mStackTabs.length; i++) {
LayoutTab tab = mStackTabs[i].getLayoutTab();
if (tab == null) continue;
tab.setMaxContentWidth(width);
tab.setMaxContentHeight(getMaxTabHeight());
}
}
/**
* 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.
*/
protected void resetInputActionIndices() {
mScrollingTab = null;
mDiscardingTab = null;
}
/**
* Reset session based parameters.
* Called before the a session starts. Before the show, regardless if the stack is
* displayable.
*/
public void reset() {
mIsDying = false;
}
/**
* 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 int direction, float x, float y) {
if (direction != ScrollDirection.DOWN) return;
// 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 = mTabList.index();
if (mCurrentMode == Orientation.PORTRAIT
|| ChromeFeatureList.isEnabled(ChromeFeatureList.HORIZONTAL_TAB_SWITCHER_ANDROID)) {
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);
// 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.getTopBrowserControlsHeight();
if (ty > toolbarSize) mSwipeCanScroll = true;
if (!mSwipeCanScroll) return;
final int index = mTabList.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;
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;
// Select the current tab so we exit the switcher.
Tab tab = TabModelUtils.getCurrentTab(mTabList);
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);
}
@Override
public void setProperty(@Property int prop, float val) {
if (prop == Property.SCROLL_OFFSET) setScrollTarget(val, true);
}
@Override
public void onPropertyAnimationFinished(@Property int prop) {}
}