Pinch to zoom out into overview mode.
am: dadb304b46

* commit 'dadb304b4682998c43b9c07bef45b9a9380f3287':
  Pinch to zoom out into overview mode.
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 2d52341..fc828da 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -3136,6 +3136,7 @@
         getDragLayer().sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
     }
 
+    @Override
     public boolean onLongClick(View v) {
         if (!isDraggingEnabled()) return false;
         if (isWorkspaceLocked()) return false;
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index bc15338..fc8bb45 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -1445,6 +1445,8 @@
             mLastMotionXRemainder = 0;
             onScrollInteractionBegin();
             pageBeginMoving();
+            // Stop listening for things like pinches.
+            requestDisallowInterceptTouchEvent(true);
         }
     }
 
diff --git a/src/com/android/launcher3/PinchAnimationManager.java b/src/com/android/launcher3/PinchAnimationManager.java
new file mode 100644
index 0000000..3302cb6
--- /dev/null
+++ b/src/com/android/launcher3/PinchAnimationManager.java
@@ -0,0 +1,285 @@
+package com.android.launcher3;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.util.Log;
+import android.view.View;
+
+import static com.android.launcher3.Workspace.State.NORMAL;
+import static com.android.launcher3.Workspace.State.OVERVIEW;
+
+/**
+ * Manages the animations that play as the user pinches to/from overview mode.
+ *
+ *  It will look like this pinching in:
+ * - Workspace scales down
+ * - At some threshold 1, hotseat and QSB fade out (full animation)
+ * - At a later threshold 2, panel buttons fade in and scrim fades in
+ * - At a final threshold 3, snap to overview
+ *
+ * Pinching out:
+ * - Workspace scales up
+ * - At threshold 1, panel buttons fade out
+ * - At threshold 2, hotseat and QSB fade in and scrim fades out
+ * - At threshold 3, snap to workspace
+ *
+ * @see PinchToOverviewListener
+ * @see PinchThresholdManager
+ */
+public class PinchAnimationManager {
+    private static final String TAG = "PinchAnimationManager";
+
+    private static final int THRESHOLD_ANIM_DURATION = 150;
+
+    private Launcher mLauncher;
+    private Workspace mWorkspace;
+
+    private float mOverviewScale;
+    private float mOverviewTranslationY;
+    private int mNormalOverviewTransitionDuration;
+    private final int[] mVisiblePageRange = new int[2];
+    private boolean mIsAnimating;
+
+    // Animators
+    private Animator mShowPageIndicatorAnimator;
+    private Animator mShowHotseatAnimator;
+    private Animator mShowOverviewPanelButtonsAnimator;
+    private Animator mShowScrimAnimator;
+    private Animator mHidePageIndicatorAnimator;
+    private Animator mHideHotseatAnimator;
+    private Animator mHideOverviewPanelButtonsAnimator;
+    private Animator mHideScrimAnimator;
+
+    public PinchAnimationManager(Launcher launcher) {
+        mLauncher = launcher;
+        mWorkspace = launcher.mWorkspace;
+
+        mOverviewScale = mWorkspace.getOverviewModeShrinkFactor();
+        mOverviewTranslationY = mWorkspace.getOverviewModeTranslationY();
+        mNormalOverviewTransitionDuration = mWorkspace.getStateTransitionAnimation()
+                .mOverviewTransitionTime;
+
+        initializeAnimators();
+    }
+
+    private void initializeAnimators() {
+        mShowPageIndicatorAnimator = new LauncherViewPropertyAnimator(
+                mWorkspace.getPageIndicator()).alpha(1f).withLayer();
+        mShowPageIndicatorAnimator.setInterpolator(null);
+
+        mShowHotseatAnimator = new LauncherViewPropertyAnimator(mLauncher.getHotseat())
+                .alpha(1f).withLayer();
+        mShowHotseatAnimator.setInterpolator(null);
+
+        mShowOverviewPanelButtonsAnimator = new LauncherViewPropertyAnimator(
+                mLauncher.getOverviewPanel()).alpha(1f).withLayer();
+        mShowOverviewPanelButtonsAnimator.setInterpolator(null);
+
+        mShowScrimAnimator = ObjectAnimator.ofFloat(mLauncher.getDragLayer(), "backgroundAlpha",
+                mWorkspace.getStateTransitionAnimation().mWorkspaceScrimAlpha);
+        mShowScrimAnimator.setInterpolator(null);
+
+        mHidePageIndicatorAnimator = new LauncherViewPropertyAnimator(
+                mWorkspace.getPageIndicator()).alpha(0f).withLayer();
+        mHidePageIndicatorAnimator.setInterpolator(null);
+        mHidePageIndicatorAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mWorkspace.getPageIndicator() != null) {
+                    mWorkspace.getPageIndicator().setVisibility(View.INVISIBLE);
+                }
+            }
+        });
+
+        mHideHotseatAnimator = new LauncherViewPropertyAnimator(mLauncher.getHotseat())
+                .alpha(0f).withLayer();
+        mHideHotseatAnimator.setInterpolator(null);
+        mHideHotseatAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mLauncher.getHotseat().setVisibility(View.INVISIBLE);
+            }
+        });
+
+        mHideOverviewPanelButtonsAnimator = new LauncherViewPropertyAnimator(
+                mLauncher.getOverviewPanel()).alpha(0f).withLayer();
+        mHideOverviewPanelButtonsAnimator.setInterpolator(null);
+        mHideOverviewPanelButtonsAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mLauncher.getOverviewPanel().setVisibility(View.INVISIBLE);
+            }
+        });
+
+        mHideScrimAnimator = ObjectAnimator.ofFloat(mLauncher.getDragLayer(), "backgroundAlpha", 0f);
+        mHideScrimAnimator.setInterpolator(null);
+    }
+
+    public int getNormalOverviewTransitionDuration() {
+        return mNormalOverviewTransitionDuration;
+    }
+
+    /**
+     * Interpolate from {@param currentProgress} to {@param toProgress}, calling
+     * {@link #setAnimationProgress(float)} throughout the duration. If duration is -1,
+     * the default overview transition duration is used.
+     */
+    public void animateToProgress(float currentProgress, float toProgress, int duration,
+            final PinchThresholdManager thresholdManager) {
+        if (duration == -1) {
+            duration = mNormalOverviewTransitionDuration;
+        }
+        ValueAnimator animator = ValueAnimator.ofFloat(currentProgress, toProgress);
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+               @Override
+               public void onAnimationUpdate(ValueAnimator animation) {
+                   float pinchProgress = (Float) animation.getAnimatedValue();
+                   setAnimationProgress(pinchProgress);
+                   thresholdManager.updateAndAnimatePassedThreshold(pinchProgress,
+                           PinchAnimationManager.this);
+               }
+           }
+        );
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mIsAnimating = false;
+                thresholdManager.reset();
+            }
+        });
+        animator.setDuration(duration).start();
+        mIsAnimating = true;
+    }
+
+    public boolean isAnimating() {
+        return mIsAnimating;
+    }
+
+    /**
+     * Animates to the specified progress. This should be called repeatedly throughout the pinch
+     * gesture to run animations that interpolate throughout the gesture.
+     * @param interpolatedProgress The progress from 0 to 1, where 0 is overview and 1 is workspace.
+     */
+    public void setAnimationProgress(float interpolatedProgress) {
+        float interpolatedScale = interpolatedProgress * (1f - mOverviewScale) + mOverviewScale;
+        float interpolatedTranslationY = (1f - interpolatedProgress) * mOverviewTranslationY;
+        mWorkspace.setScaleX(interpolatedScale);
+        mWorkspace.setScaleY(interpolatedScale);
+        mWorkspace.setTranslationY(interpolatedTranslationY);
+        setOverviewPanelsAlpha(1f - interpolatedProgress, 0);
+
+        // Make sure adjacent pages, except custom content page, are visible while scaling.
+        mWorkspace.setCustomContentVisibility(View.INVISIBLE);
+        mWorkspace.invalidate();
+    }
+
+    /**
+     * Animates certain properties based on which threshold was passed, and in what direction. The
+     * starting state must also be taken into account because the thresholds mean different things
+     * when going from workspace to overview and vice versa.
+     * @param threshold One of {@link PinchThresholdManager#THRESHOLD_ONE},
+     *                  {@link PinchThresholdManager#THRESHOLD_TWO}, or
+     *                  {@link PinchThresholdManager#THRESHOLD_THREE}
+     * @param startState {@link Workspace.State#NORMAL} or {@link Workspace.State#OVERVIEW}.
+     * @param goingTowards {@link Workspace.State#NORMAL} or {@link Workspace.State#OVERVIEW}.
+ *                     Note that this doesn't have to be the opposite of startState;
+     */
+    public void animateThreshold(float threshold, Workspace.State startState,
+            Workspace.State goingTowards) {
+        if (threshold == PinchThresholdManager.THRESHOLD_ONE) {
+            if (startState == OVERVIEW) {
+                animateOverviewPanelButtons(goingTowards == OVERVIEW);
+            } else if (startState == NORMAL) {
+                animateHotseatAndPageIndicator(goingTowards == NORMAL);
+                animateQsb(goingTowards == NORMAL);
+            }
+        } else if (threshold == PinchThresholdManager.THRESHOLD_TWO) {
+            if (startState == OVERVIEW) {
+                animateHotseatAndPageIndicator(goingTowards == NORMAL);
+                animateQsb(goingTowards == NORMAL);
+                animateScrim(goingTowards == OVERVIEW);
+            } else if (startState == NORMAL) {
+                animateOverviewPanelButtons(goingTowards == OVERVIEW);
+                animateScrim(goingTowards == OVERVIEW);
+            }
+        } else if (threshold == PinchThresholdManager.THRESHOLD_THREE) {
+            // Passing threshold 3 ends the pinch and snaps to the new state.
+            if (startState == OVERVIEW && goingTowards == NORMAL) {
+                mLauncher.showWorkspace(true);
+                mWorkspace.snapToPage(mWorkspace.getPageNearestToCenterOfScreen());
+            } else if (startState == NORMAL && goingTowards == OVERVIEW) {
+                mLauncher.showOverviewMode(true);
+            }
+        } else {
+            Log.e(TAG, "Received unknown threshold to animate: " + threshold);
+        }
+    }
+
+    private void setOverviewPanelsAlpha(float alpha, int duration) {
+        mWorkspace.getVisiblePages(mVisiblePageRange);
+        for (int i = mVisiblePageRange[0]; i <= mVisiblePageRange[1]; i++) {
+            View page = mWorkspace.getPageAt(i);
+            if (!mWorkspace.shouldDrawChild(page)) {
+                continue;
+            }
+            if (duration == 0) {
+                ((CellLayout) page).setBackgroundAlpha(alpha);
+            } else {
+                ObjectAnimator.ofFloat(page, "backgroundAlpha", alpha)
+                        .setDuration(duration).start();
+            }
+        }
+    }
+
+    private void animateHotseatAndPageIndicator(boolean show) {
+        if (show) {
+            mLauncher.getHotseat().setVisibility(View.VISIBLE);
+            mShowHotseatAnimator.setDuration(THRESHOLD_ANIM_DURATION).start();
+            if (mWorkspace.getPageIndicator() != null) {
+                // There aren't page indicators in landscape mode on phones, hence the null check.
+                mWorkspace.getPageIndicator().setVisibility(View.VISIBLE);
+                mShowPageIndicatorAnimator.setDuration(THRESHOLD_ANIM_DURATION).start();
+            }
+        } else {
+            mHideHotseatAnimator.setDuration(THRESHOLD_ANIM_DURATION).start();
+            if (mWorkspace.getPageIndicator() != null) {
+                // There aren't page indicators in landscape mode on phones, hence the null check.
+                mHidePageIndicatorAnimator.setDuration(THRESHOLD_ANIM_DURATION).start();
+            }
+        }
+    }
+
+    private void animateQsb(boolean show) {
+        SearchDropTargetBar.State searchBarState = show ? SearchDropTargetBar.State.SEARCH_BAR
+                : SearchDropTargetBar.State.INVISIBLE;
+        mLauncher.getSearchDropTargetBar().animateToState(searchBarState, THRESHOLD_ANIM_DURATION);
+    }
+
+    private void animateOverviewPanelButtons(boolean show) {
+        if (show) {
+            mLauncher.getOverviewPanel().setVisibility(View.VISIBLE);
+            mShowOverviewPanelButtonsAnimator.setDuration(THRESHOLD_ANIM_DURATION).start();
+        } else {
+            mHideOverviewPanelButtonsAnimator.setDuration(THRESHOLD_ANIM_DURATION).start();
+        }
+    }
+
+    private void animateScrim(boolean show) {
+        // We reninitialize the animators here so that they have the correct start values.
+        if (show) {
+            mShowScrimAnimator = ObjectAnimator.ofFloat(mLauncher.getDragLayer(), "backgroundAlpha",
+                    mWorkspace.getStateTransitionAnimation().mWorkspaceScrimAlpha);
+            mShowScrimAnimator.setInterpolator(null);
+            mShowScrimAnimator.setDuration(mNormalOverviewTransitionDuration).start();
+        } else {
+            mHideScrimAnimator.setupStartValues();
+            mHideScrimAnimator = ObjectAnimator.ofFloat(mLauncher.getDragLayer(), "backgroundAlpha",
+                    0f);
+            mHideScrimAnimator.setInterpolator(null);
+            mHideScrimAnimator.setDuration(mNormalOverviewTransitionDuration).start();
+            mHideScrimAnimator.setDuration(mNormalOverviewTransitionDuration).start();
+        }
+    }
+}
diff --git a/src/com/android/launcher3/PinchThresholdManager.java b/src/com/android/launcher3/PinchThresholdManager.java
new file mode 100644
index 0000000..d79a631
--- /dev/null
+++ b/src/com/android/launcher3/PinchThresholdManager.java
@@ -0,0 +1,77 @@
+package com.android.launcher3;
+
+/**
+ * Keeps track of when thresholds are passed during a pinch gesture,
+ * used to inform {@link PinchAnimationManager} throughout.
+ *
+ * @see PinchToOverviewListener
+ * @see PinchAnimationManager
+ */
+public class PinchThresholdManager {
+    public static final float THRESHOLD_ZERO = 0.0f;
+    public static final float THRESHOLD_ONE = 0.40f;
+    public static final float THRESHOLD_TWO = 0.70f;
+    public static final float THRESHOLD_THREE = 0.95f;
+
+    private Workspace mWorkspace;
+
+    private float mPassedThreshold = THRESHOLD_ZERO;
+
+    public PinchThresholdManager(Workspace workspace) {
+        mWorkspace = workspace;
+    }
+
+    /**
+     * Uses the pinch progress to determine whether a threshold has been passed,
+     * and asks the {@param animationManager} to animate if so.
+     * @param progress From 0 to 1, where 0 is overview and 1 is workspace.
+     * @param animationManager Animates the threshold change if one is passed.
+     * @return The last passed threshold, one of
+     *         {@link PinchThresholdManager#THRESHOLD_ZERO},
+     *         {@link PinchThresholdManager#THRESHOLD_ONE},
+     *         {@link PinchThresholdManager#THRESHOLD_TWO}, or
+     *         {@link PinchThresholdManager#THRESHOLD_THREE}
+     */
+    public float updateAndAnimatePassedThreshold(float progress,
+            PinchAnimationManager animationManager) {
+        if (!mWorkspace.isInOverviewMode()) {
+            // Invert the progress, because going from workspace to overview is 1 to 0.
+            progress = 1f - progress;
+        }
+
+        float previousPassedThreshold = mPassedThreshold;
+
+        if (progress < THRESHOLD_ONE) {
+            mPassedThreshold = THRESHOLD_ZERO;
+        } else if (progress < THRESHOLD_TWO) {
+            mPassedThreshold = THRESHOLD_ONE;
+        } else if (progress < THRESHOLD_THREE) {
+            mPassedThreshold = THRESHOLD_TWO;
+        } else {
+            mPassedThreshold = THRESHOLD_THREE;
+        }
+
+        if (mPassedThreshold != previousPassedThreshold) {
+            Workspace.State fromState = mWorkspace.isInOverviewMode() ? Workspace.State.OVERVIEW
+                    : Workspace.State.NORMAL;
+            Workspace.State toState = mWorkspace.isInOverviewMode() ? Workspace.State.NORMAL
+                    : Workspace.State.OVERVIEW;
+            float thresholdToAnimate = mPassedThreshold;
+            if (mPassedThreshold < previousPassedThreshold) {
+                // User reversed pinch, so heading back to the state that they started from.
+                toState = fromState;
+                thresholdToAnimate = previousPassedThreshold;
+            }
+            animationManager.animateThreshold(thresholdToAnimate, fromState, toState);
+        }
+        return mPassedThreshold;
+    }
+
+    public float getPassedThreshold() {
+        return mPassedThreshold;
+    }
+
+    public void reset() {
+        mPassedThreshold = THRESHOLD_ZERO;
+    }
+}
diff --git a/src/com/android/launcher3/PinchToOverviewListener.java b/src/com/android/launcher3/PinchToOverviewListener.java
new file mode 100644
index 0000000..ef47485
--- /dev/null
+++ b/src/com/android/launcher3/PinchToOverviewListener.java
@@ -0,0 +1,179 @@
+package com.android.launcher3;
+
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+/**
+ * Detects pinches and animates the Workspace to/from overview mode.
+ *
+ * Usage: Pass MotionEvents to onInterceptTouchEvent() and onTouchEvent(). This class will handle
+ * the pinch detection, and use {@link PinchAnimationManager} to handle the animations.
+ *
+ * @see PinchThresholdManager
+ * @see PinchAnimationManager
+ */
+public class PinchToOverviewListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+    private static final float OVERVIEW_PROGRESS = 0f;
+    private static final float WORKSPACE_PROGRESS = 1f;
+
+    private ScaleGestureDetector mPinchDetector;
+    private Launcher mLauncher;
+    private Workspace mWorkspace = null;
+    private boolean mPinchStarted = false;
+    private float mPreviousProgress;
+    private float mProgressDelta;
+    private long mPreviousTimeMillis;
+    private long mTimeDelta;
+    private boolean mPinchCanceled = false;
+    private TimeInterpolator mInterpolator;
+
+    private PinchThresholdManager mThresholdManager;
+    private PinchAnimationManager mAnimationManager;
+
+    public PinchToOverviewListener(Launcher launcher) {
+        mLauncher = launcher;
+        mPinchDetector = new ScaleGestureDetector((Context) mLauncher, this);
+    }
+
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        mPinchDetector.onTouchEvent(ev);
+        return mPinchStarted;
+    }
+
+    public void onTouchEvent(MotionEvent ev) {
+        if (mPinchStarted) {
+            if (ev.getPointerCount() > 2) {
+                // Using more than two fingers causes weird behavior, so just cancel the pinch.
+                cancelPinch(mPreviousProgress, -1);
+            } else {
+                mPinchDetector.onTouchEvent(ev);
+            }
+        }
+    }
+
+    @Override
+    public boolean onScaleBegin(ScaleGestureDetector detector) {
+        if (mLauncher.mState != Launcher.State.WORKSPACE || mLauncher.isOnCustomContent()) {
+            // Don't listen for the pinch gesture if on all apps, widget picker, -1, etc.
+            return false;
+        }
+        if (mAnimationManager != null && mAnimationManager.isAnimating()) {
+            // Don't listen for the pinch gesture if we are already animating from a previous one.
+            return false;
+        }
+        if (mWorkspace == null) {
+            mWorkspace = mLauncher.mWorkspace;
+            mThresholdManager = new PinchThresholdManager(mWorkspace);
+            mAnimationManager = new PinchAnimationManager(mLauncher);
+        }
+        if (mWorkspace.isSwitchingState() || mWorkspace.mScrollInteractionBegan) {
+            // Don't listen to pinches occurring while switching state, as it will cause a jump
+            // once the state switching animation is complete.
+            return false;
+        }
+
+        mPreviousProgress = mWorkspace.isInOverviewMode() ? OVERVIEW_PROGRESS : WORKSPACE_PROGRESS;
+        mPreviousTimeMillis = System.currentTimeMillis();
+        mInterpolator = mWorkspace.isInOverviewMode() ? new LogDecelerateInterpolator(100, 0)
+                : new LogAccelerateInterpolator(100, 0);
+        mPinchStarted = true;
+        return true;
+    }
+
+    @Override
+    public void onScaleEnd(ScaleGestureDetector detector) {
+        super.onScaleEnd(detector);
+
+        float passedThreshold = mThresholdManager.getPassedThreshold();
+        // If we are going towards overview, mPreviousProgress is how much further we need to
+        // go, since it is going from 1 to 0. If we are going to workspace, we want
+        // 1 - mPreviousProgress.
+        float remainingProgress = mPreviousProgress;
+        if (mWorkspace.isInOverviewMode() || passedThreshold < PinchThresholdManager.THRESHOLD_ONE) {
+            remainingProgress = 1f - mPreviousProgress;
+        }
+        int duration = computeDuration(remainingProgress, mProgressDelta, mTimeDelta);
+
+        if (passedThreshold < PinchThresholdManager.THRESHOLD_ONE) {
+            cancelPinch(mPreviousProgress, duration);
+        } else if (passedThreshold < PinchThresholdManager.THRESHOLD_THREE) {
+            float toProgress = mWorkspace.isInOverviewMode() ?
+                    WORKSPACE_PROGRESS : OVERVIEW_PROGRESS;
+            mAnimationManager.animateToProgress(mPreviousProgress, toProgress, duration,
+                    mThresholdManager);
+        } else {
+            mThresholdManager.reset();
+        }
+        mPinchStarted = false;
+        mPinchCanceled = false;
+    }
+
+    /**
+     * Compute the amount of time required to complete the transition based on the current pinch
+     * speed. If this time is too long, instead return the normal duration, ignoring the speed.
+     */
+    private int computeDuration(float remainingProgress, float progressDelta, long timeDelta) {
+        float progressSpeed = Math.abs(progressDelta) / timeDelta;
+        int remainingMillis = (int) (remainingProgress / progressSpeed);
+        return Math.min(remainingMillis, mAnimationManager.getNormalOverviewTransitionDuration());
+    }
+
+    /**
+     * Cancels the current pinch, returning back to where the pinch started (either workspace or
+     * overview). If duration is -1, the default overview transition duration is used.
+     */
+    private void cancelPinch(float currentProgress, int duration) {
+        if (mPinchCanceled) return;
+        mPinchCanceled = true;
+        float toProgress = mWorkspace.isInOverviewMode() ? OVERVIEW_PROGRESS : WORKSPACE_PROGRESS;
+        mAnimationManager.animateToProgress(currentProgress, toProgress, duration,
+                mThresholdManager);
+        mPinchStarted = false;
+    }
+
+    @Override
+    public boolean onScale(ScaleGestureDetector detector) {
+        if (mThresholdManager.getPassedThreshold() == PinchThresholdManager.THRESHOLD_THREE) {
+            // We completed the pinch, so stop listening to further movement until user lets go.
+            return true;
+        }
+        if (mLauncher.getDragController().isDragging()) {
+            mLauncher.getDragController().cancelDrag();
+        }
+
+        float pinchDist = detector.getCurrentSpan() - detector.getPreviousSpan();
+        if (pinchDist < 0 && mWorkspace.isInOverviewMode() ||
+                pinchDist > 0 && !mWorkspace.isInOverviewMode()) {
+            // Pinching the wrong way, so ignore.
+            return false;
+        }
+        // Pinch distance must equal the workspace width before switching states.
+        int pinchDistanceToCompleteTransition = mWorkspace.getWidth();
+        float overviewScale = mWorkspace.getOverviewModeShrinkFactor();
+        float initialWorkspaceScale = mWorkspace.isInOverviewMode() ? overviewScale : 1f;
+        float pinchScale = initialWorkspaceScale + pinchDist / pinchDistanceToCompleteTransition;
+        // Bound the scale between the overview scale and the normal workspace scale (1f).
+        pinchScale = Math.max(overviewScale, Math.min(pinchScale, 1f));
+        // Progress ranges from 0 to 1, where 0 corresponds to the overview scale and 1
+        // corresponds to the normal workspace scale (1f).
+        float progress = (pinchScale - overviewScale) / (1f - overviewScale);
+        float interpolatedProgress = mInterpolator.getInterpolation(progress);
+
+        mAnimationManager.setAnimationProgress(interpolatedProgress);
+        float passedThreshold = mThresholdManager.updateAndAnimatePassedThreshold(
+                interpolatedProgress, mAnimationManager);
+        if (passedThreshold == PinchThresholdManager.THRESHOLD_THREE) {
+            return true;
+        }
+
+        mProgressDelta = interpolatedProgress - mPreviousProgress;
+        mPreviousProgress = interpolatedProgress;
+        mTimeDelta = System.currentTimeMillis() - mPreviousTimeMillis;
+        mPreviousTimeMillis = System.currentTimeMillis();
+        return false;
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 0f8d834..a74fa0d 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -1900,6 +1900,10 @@
         return -(workspaceHeight - scaledHeight) / 2;
     }
 
+    float getOverviewModeShrinkFactor() {
+        return mOverviewModeShrinkFactor;
+    }
+
     /**
      * Sets the current workspace {@link State}, returning an animation transitioning the workspace
      * to that new state.
@@ -1996,6 +2000,10 @@
 
     void updateCustomContentVisibility() {
         int visibility = mState == Workspace.State.NORMAL ? VISIBLE : INVISIBLE;
+        setCustomContentVisibility(visibility);
+    }
+
+    void setCustomContentVisibility(int visibility) {
         if (hasCustomContent()) {
             mWorkspaceScreens.get(CUSTOM_CONTENT_SCREEN_ID).setVisibility(visibility);
         }
@@ -3526,6 +3534,10 @@
         }
     }
 
+    public WorkspaceStateTransitionAnimation getStateTransitionAnimation() {
+        return mStateTransitionAnimation;
+    }
+
     /**
      * Return the current {@link CellLayout}, correctly picking the destination
      * screen while a scroll is in progress.
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index f16a56c..647ec5e 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -47,6 +47,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetHostView;
+import com.android.launcher3.PinchToOverviewListener;
 import com.android.launcher3.R;
 import com.android.launcher3.SearchDropTargetBar;
 import com.android.launcher3.ShortcutAndWidgetContainer;
@@ -110,6 +111,8 @@
     private Drawable mLeftHoverDrawableActive;
     private Drawable mRightHoverDrawableActive;
 
+    // Related to pinch-to-go-to-overview gesture.
+    private PinchToOverviewListener mPinchListener;
     /**
      * Used to create a new DragLayer from XML.
      *
@@ -134,6 +137,8 @@
     public void setup(Launcher launcher, DragController controller) {
         mLauncher = launcher;
         mDragController = controller;
+
+        mPinchListener = new PinchToOverviewListener(mLauncher);
     }
 
     @Override
@@ -241,6 +246,14 @@
             mTouchCompleteListener = null;
         }
         clearAllResizeFrames();
+
+        Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
+        if (currentFolder == null) {
+            if (mPinchListener.onInterceptTouchEvent(ev)) {
+                // Stop listening for scrolling etc. (onTouchEvent() handles the rest of the pinch.)
+                return true;
+            }
+        }
         return mDragController.onInterceptTouchEvent(ev);
     }
 
@@ -354,6 +367,10 @@
         int x = (int) ev.getX();
         int y = (int) ev.getY();
 
+        // This is only reached if a pinch was started from onInterceptTouchEvent();
+        // this continues sending events for it.
+        mPinchListener.onTouchEvent(ev);
+
         if (action == MotionEvent.ACTION_DOWN) {
             if (handleTouchDown(ev, false)) {
                 return true;