blob: 018350e4f82ddee6b2024267a0b31a10ee2f5efd [file] [log] [blame]
// Copyright 2018 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.content.Context;
import android.content.res.Resources;
import org.chromium.chrome.R;
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.util.MathUtils;
import org.chromium.ui.base.LocalizationUtils;
/**
* The overlapping tab stack we use when the HorizontalTabSwitcherAndroid flag is not enabled.
*/
public class OverlappingStack extends Stack {
private static final float SCALE_AMOUNT = 0.90f;
/**
* The percentage of the screen that defines the spacing between tabs by default (no pinch).
*/
private static final float SPACING_SCREEN = 0.26f;
/**
* Percentage of the screen to wrap the scroll space.
*/
private static final float SCROLL_WARP_PCTG = 0.4f;
/**
* 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).
*/
private static final float STACK_PORTRAIT_Y_OFFSET_PROPORTION = -0.8f;
/**
* 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).
*/
private static final float STACK_LANDSCAPE_START_OFFSET_PROPORTION = -0.7f;
/**
* 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).
*/
private static final float STACK_LANDSCAPE_Y_OFFSET_PROPORTION = -0.5f;
private float mWarpSize;
// 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.xlm
private float mMinSpacing; // This will be updated from dimens.xml
/**
* @param layout The parent layout.
*/
public OverlappingStack(Context context, StackLayoutBase layout) {
super(context, layout);
}
@Override
public float getScaleAmount() {
return SCALE_AMOUNT;
}
@Override
protected 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;
}
@Override
public void onLongPress(long time, float x, float y) {
if (mOverviewAnimationType == OverviewAnimationType.NONE) {
int longPressSelected = getTabIndexAtPositon(x, y);
if (longPressSelected >= 0) {
startAnimation(time, OverviewAnimationType.VIEW_MORE, longPressSelected, false);
mEvenOutProgress = 0.0f;
}
}
}
@Override
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 = (tab1ScreenBefore == tab0ScreenBefore)
? 1
: ((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;
mLayout.requestUpdate();
}
@Override
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);
mLayout.requestUpdate();
}
super.onUpOrCancel(time);
}
@Override
protected 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);
mLayout.requestUpdate();
}
}
}
/**
* @param context The current Android's context.
*/
@Override
public void contextChanged(Context context) {
super.contextChanged(context);
Resources res = context.getResources();
final float pxToDp = 1.0f / res.getDisplayMetrics().density;
mEvenOutRate = 1.0f / (res.getDimension(R.dimen.even_out_scrolling) * pxToDp);
mMinSpacing = res.getDimensionPixelOffset(R.dimen.min_spacing) * pxToDp;
}
@Override
protected boolean shouldStackTabsAtTop() {
return true;
}
@Override
protected boolean shouldStackTabsAtBottom() {
return true;
}
@Override
protected float getStackPortraitYOffsetProportion() {
return STACK_PORTRAIT_Y_OFFSET_PROPORTION;
}
@Override
protected float getStackLandscapeStartOffsetProportion() {
return STACK_LANDSCAPE_START_OFFSET_PROPORTION;
}
@Override
protected float getStackLandscapeYOffsetProportion() {
return STACK_LANDSCAPE_Y_OFFSET_PROPORTION;
}
@Override
protected float getSpacingScreen() {
return SPACING_SCREEN;
}
@Override
protected boolean shouldCloseGapsBetweenTabs() {
return true;
}
@Override
protected 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);
mLayout.releaseResourcesForTab(layoutTab);
} 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;
}
}
}
}
@Override
protected int computeReferenceIndex() {
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--;
return MathUtils.clamp(centerIndex, 0, mStackTabs.length - 1);
}
@Override
protected 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;
}
@Override
protected int computeSpacing(int layoutTabCount) {
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;
}
@Override
protected boolean allowOverscroll() {
return super.allowOverscroll() && mPinch0TabIndex < 0;
}
@Override
protected void resetAllScrollOffset() {
if (mTabList == 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 = mTabList.getCount();
final int index = mTabList.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);
}
/**
* Unwarps x so it matches the above warp function.
* @see #scrollToScreen(float)
*
* [-oo, 0] -> -warpSize
* [0, warpSize] -> 2 * warpSize * sqrt(x / warpSize).
* [warpSize, +oo] -> x + warpSize
* @param x The screen space offset.
* @param warpSize The size in scroll space of the slow down effect.
* @return The offset in scroll space corresponding to the offset on screen.
*/
private float screenToScroll(float x, float warpSize) {
if (x <= 0) return 0;
if (x >= warpSize) return x + warpSize;
return (float) Math.sqrt(x * warpSize) * 2;
}
/**
* Public version of screenToScroll(float, float) that uses the current warp size.
* @param scrollSpace The offset in screen space.
* @return The offset in scroll space corresponding to the offset on screen.
*/
@Override
public float screenToScroll(float screenSpace) {
return screenToScroll(screenSpace, mWarpSize);
}
/**
* The scroll space does not map linearly to the screen so it creates a nice slow down
* effect at the top of the screen while scrolling.
* Warps x so it matches y(x) = x - warpSize on the positive side and 0 on the negative side
* with a smooth transition between [0, 2 * warpSize].
* @see #screenToScroll(float)
*
* [-oo, 0] -> 0
* [0, 2 * warpSize] -> warpSize * ((x-warpSize) / 2 * warpSize + 0.5) ^ 2.
* [2 * warpSize, +oo] -> x
* @param x The offset in scroll space.
* @param warpSize The size in scroll space of the slow down effect.
* @return The offset on screen corresponding to the scroll space offset.
*/
private float scrollToScreen(float x, float warpSize) {
if (x <= 0) return 0;
if (x >= 2 * warpSize) return x - warpSize;
x = (x - warpSize) / (2.0f * warpSize) + 0.5f;
return x * x * warpSize;
}
/**
* Public version of scrollToScreen(float, float) that uses the current warp size.
* 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.
*/
@Override
public float scrollToScreen(float scrollSpace) {
return scrollToScreen(scrollSpace, mWarpSize);
}
@Override
public float getMaxTabHeight() {
return mLayout.getHeightMinusBrowserControls();
}
@Override
protected void updateCurrentMode(int orientation) {
setWarpState(true, false);
super.updateCurrentMode(orientation);
}
@Override
protected void resetInputActionIndices() {
super.resetInputActionIndices();
mPinch0TabIndex = -1;
mPinch1TabIndex = -1;
}
/**
* 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 = scrollToScreen(tabScrollSpace, mWarpSize);
float tabScrollSpaceFinal = screenToScroll(tabScreen, warp);
float scrollDelta = tabScrollSpaceFinal - tabScrollSpace;
tab.setScrollOffset(tabScrollOffset + scrollDelta);
}
}
mWarpSize = warp;
}
@Override
public void swipeStarted(long time, @ScrollDirection int 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);
super.swipeStarted(time, direction, x, y);
// Don't let the tabs even out during this scroll.
mEvenOutProgress = 1.f;
}
@Override
public void swipeFinished(long time) {
if (!mInSwipe) return;
// Mark the tabs to even themselves out.
mEvenOutProgress = 0.f;
// Reset the warp state.
setWarpState(true, true);
super.swipeFinished(time);
}
@Override
public void swipeCancelled(long time) {
if (!mInSwipe) return;
mEvenOutProgress = 0.f;
setWarpState(true, true);
super.swipeCancelled(time);
}
}