summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/PinchToOverviewListener.java
blob: 42515d1fcf6bc05e041d1fb2145dc4801c3c999d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.launcher3;

import android.animation.TimeInterpolator;
import android.content.Context;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;

import com.android.launcher3.util.TouchController;

/**
 * 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
        implements TouchController {
    private static final float OVERVIEW_PROGRESS = 0f;
    private static final float WORKSPACE_PROGRESS = 1f;
    /**
     * The velocity threshold at which a pinch will be completed instead of canceled,
     * even if the first threshold has not been passed. Measured in progress / millisecond
     */
    private static final float FLING_VELOCITY = 0.003f;

    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 onControllerInterceptTouchEvent(MotionEvent ev) {
        mPinchDetector.onTouchEvent(ev);
        return mPinchStarted;
    }

    public boolean onControllerTouchEvent(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 {
                return mPinchDetector.onTouchEvent(ev);
            }
        }
        return false;
    }

    @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 (mLauncher.isWorkspaceLocked()) {
            // Don't listen for the pinch gesture if the workspace isn't ready.
            return false;
        }
        if (mWorkspace == null) {
            mWorkspace = mLauncher.getWorkspace();
            mThresholdManager = new PinchThresholdManager(mWorkspace);
            mAnimationManager = new PinchAnimationManager(mLauncher);
        }
        if (mWorkspace.isSwitchingState() || mWorkspace.mScrollInteractionBegan) {
            // Don't listen for the pinch gesture while switching state, as it will cause a jump
            // once the state switching animation is complete.
            return false;
        }
        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
            // Don't listen for the pinch gesture if a floating view is open.
            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;
        mWorkspace.onPrepareStateTransition(true);
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        super.onScaleEnd(detector);

        float progressVelocity = mProgressDelta / mTimeDelta;
        float passedThreshold = mThresholdManager.getPassedThreshold();
        boolean isFling = mWorkspace.isInOverviewMode() && progressVelocity >= FLING_VELOCITY
                || !mWorkspace.isInOverviewMode() && progressVelocity <= -FLING_VELOCITY;
        boolean shouldCancelPinch = !isFling && passedThreshold < PinchThresholdManager.THRESHOLD_ONE;
        // 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() || shouldCancelPinch) {
            remainingProgress = 1f - mPreviousProgress;
        }
        int duration = computeDuration(remainingProgress, progressVelocity);
        if (shouldCancelPinch) {
            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();
            mWorkspace.onEndStateTransition();
        }
        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 progressVelocity) {
        float progressSpeed = Math.abs(progressVelocity);
        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;
    }
}