1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3;
18 
19 import android.animation.TimeInterpolator;
20 import android.content.Context;
21 import android.view.MotionEvent;
22 import android.view.ScaleGestureDetector;
23 
24 import com.android.launcher3.util.TouchController;
25 
26 /**
27  * Detects pinches and animates the Workspace to/from overview mode.
28  *
29  * Usage: Pass MotionEvents to onInterceptTouchEvent() and onTouchEvent(). This class will handle
30  * the pinch detection, and use {@link PinchAnimationManager} to handle the animations.
31  *
32  * @see PinchThresholdManager
33  * @see PinchAnimationManager
34  */
35 public class PinchToOverviewListener extends ScaleGestureDetector.SimpleOnScaleGestureListener
36         implements TouchController {
37     private static final float OVERVIEW_PROGRESS = 0f;
38     private static final float WORKSPACE_PROGRESS = 1f;
39     /**
40      * The velocity threshold at which a pinch will be completed instead of canceled,
41      * even if the first threshold has not been passed. Measured in progress / millisecond
42      */
43     private static final float FLING_VELOCITY = 0.003f;
44 
45     private ScaleGestureDetector mPinchDetector;
46     private Launcher mLauncher;
47     private Workspace mWorkspace = null;
48     private boolean mPinchStarted = false;
49     private float mPreviousProgress;
50     private float mProgressDelta;
51     private long mPreviousTimeMillis;
52     private long mTimeDelta;
53     private boolean mPinchCanceled = false;
54     private TimeInterpolator mInterpolator;
55 
56     private PinchThresholdManager mThresholdManager;
57     private PinchAnimationManager mAnimationManager;
58 
PinchToOverviewListener(Launcher launcher)59     public PinchToOverviewListener(Launcher launcher) {
60         mLauncher = launcher;
61         mPinchDetector = new ScaleGestureDetector((Context) mLauncher, this);
62     }
63 
onControllerInterceptTouchEvent(MotionEvent ev)64     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
65         mPinchDetector.onTouchEvent(ev);
66         return mPinchStarted;
67     }
68 
onControllerTouchEvent(MotionEvent ev)69     public boolean onControllerTouchEvent(MotionEvent ev) {
70         if (mPinchStarted) {
71             if (ev.getPointerCount() > 2) {
72                 // Using more than two fingers causes weird behavior, so just cancel the pinch.
73                 cancelPinch(mPreviousProgress, -1);
74             } else {
75                 return mPinchDetector.onTouchEvent(ev);
76             }
77         }
78         return false;
79     }
80 
81     @Override
onScaleBegin(ScaleGestureDetector detector)82     public boolean onScaleBegin(ScaleGestureDetector detector) {
83         if (mLauncher.mState != Launcher.State.WORKSPACE || mLauncher.isOnCustomContent()) {
84             // Don't listen for the pinch gesture if on all apps, widget picker, -1, etc.
85             return false;
86         }
87         if (mAnimationManager != null && mAnimationManager.isAnimating()) {
88             // Don't listen for the pinch gesture if we are already animating from a previous one.
89             return false;
90         }
91         if (mLauncher.isWorkspaceLocked()) {
92             // Don't listen for the pinch gesture if the workspace isn't ready.
93             return false;
94         }
95         if (mWorkspace == null) {
96             mWorkspace = mLauncher.getWorkspace();
97             mThresholdManager = new PinchThresholdManager(mWorkspace);
98             mAnimationManager = new PinchAnimationManager(mLauncher);
99         }
100         if (mWorkspace.isSwitchingState() || mWorkspace.mScrollInteractionBegan) {
101             // Don't listen for the pinch gesture while switching state, as it will cause a jump
102             // once the state switching animation is complete.
103             return false;
104         }
105         if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
106             // Don't listen for the pinch gesture if a floating view is open.
107             return false;
108         }
109 
110         mPreviousProgress = mWorkspace.isInOverviewMode() ? OVERVIEW_PROGRESS : WORKSPACE_PROGRESS;
111         mPreviousTimeMillis = System.currentTimeMillis();
112         mInterpolator = mWorkspace.isInOverviewMode() ? new LogDecelerateInterpolator(100, 0)
113                 : new LogAccelerateInterpolator(100, 0);
114         mPinchStarted = true;
115         mWorkspace.onPrepareStateTransition(true);
116         return true;
117     }
118 
119     @Override
onScaleEnd(ScaleGestureDetector detector)120     public void onScaleEnd(ScaleGestureDetector detector) {
121         super.onScaleEnd(detector);
122 
123         float progressVelocity = mProgressDelta / mTimeDelta;
124         float passedThreshold = mThresholdManager.getPassedThreshold();
125         boolean isFling = mWorkspace.isInOverviewMode() && progressVelocity >= FLING_VELOCITY
126                 || !mWorkspace.isInOverviewMode() && progressVelocity <= -FLING_VELOCITY;
127         boolean shouldCancelPinch = !isFling && passedThreshold < PinchThresholdManager.THRESHOLD_ONE;
128         // If we are going towards overview, mPreviousProgress is how much further we need to
129         // go, since it is going from 1 to 0. If we are going to workspace, we want
130         // 1 - mPreviousProgress.
131         float remainingProgress = mPreviousProgress;
132         if (mWorkspace.isInOverviewMode() || shouldCancelPinch) {
133             remainingProgress = 1f - mPreviousProgress;
134         }
135         int duration = computeDuration(remainingProgress, progressVelocity);
136         if (shouldCancelPinch) {
137             cancelPinch(mPreviousProgress, duration);
138         } else if (passedThreshold < PinchThresholdManager.THRESHOLD_THREE) {
139             float toProgress = mWorkspace.isInOverviewMode() ?
140                     WORKSPACE_PROGRESS : OVERVIEW_PROGRESS;
141             mAnimationManager.animateToProgress(mPreviousProgress, toProgress, duration,
142                     mThresholdManager);
143         } else {
144             mThresholdManager.reset();
145             mWorkspace.onEndStateTransition();
146         }
147         mPinchStarted = false;
148         mPinchCanceled = false;
149     }
150 
151     /**
152      * Compute the amount of time required to complete the transition based on the current pinch
153      * speed. If this time is too long, instead return the normal duration, ignoring the speed.
154      */
155     private int computeDuration(float remainingProgress, float progressVelocity) {
156         float progressSpeed = Math.abs(progressVelocity);
157         int remainingMillis = (int) (remainingProgress / progressSpeed);
158         return Math.min(remainingMillis, mAnimationManager.getNormalOverviewTransitionDuration());
159     }
160 
161     /**
162      * Cancels the current pinch, returning back to where the pinch started (either workspace or
163      * overview). If duration is -1, the default overview transition duration is used.
164      */
165     private void cancelPinch(float currentProgress, int duration) {
166         if (mPinchCanceled) return;
167         mPinchCanceled = true;
168         float toProgress = mWorkspace.isInOverviewMode() ? OVERVIEW_PROGRESS : WORKSPACE_PROGRESS;
169         mAnimationManager.animateToProgress(currentProgress, toProgress, duration,
170                 mThresholdManager);
171         mPinchStarted = false;
172     }
173 
174     @Override
175     public boolean onScale(ScaleGestureDetector detector) {
176         if (mThresholdManager.getPassedThreshold() == PinchThresholdManager.THRESHOLD_THREE) {
177             // We completed the pinch, so stop listening to further movement until user lets go.
178             return true;
179         }
180         if (mLauncher.getDragController().isDragging()) {
181             mLauncher.getDragController().cancelDrag();
182         }
183 
184         float pinchDist = detector.getCurrentSpan() - detector.getPreviousSpan();
185         if (pinchDist < 0 && mWorkspace.isInOverviewMode() ||
186                 pinchDist > 0 && !mWorkspace.isInOverviewMode()) {
187             // Pinching the wrong way, so ignore.
188             return false;
189         }
190         // Pinch distance must equal the workspace width before switching states.
191         int pinchDistanceToCompleteTransition = mWorkspace.getWidth();
192         float overviewScale = mWorkspace.getOverviewModeShrinkFactor();
193         float initialWorkspaceScale = mWorkspace.isInOverviewMode() ? overviewScale : 1f;
194         float pinchScale = initialWorkspaceScale + pinchDist / pinchDistanceToCompleteTransition;
195         // Bound the scale between the overview scale and the normal workspace scale (1f).
196         pinchScale = Math.max(overviewScale, Math.min(pinchScale, 1f));
197         // Progress ranges from 0 to 1, where 0 corresponds to the overview scale and 1
198         // corresponds to the normal workspace scale (1f).
199         float progress = (pinchScale - overviewScale) / (1f - overviewScale);
200         float interpolatedProgress = mInterpolator.getInterpolation(progress);
201 
202         mAnimationManager.setAnimationProgress(interpolatedProgress);
203         float passedThreshold = mThresholdManager.updateAndAnimatePassedThreshold(
204                 interpolatedProgress, mAnimationManager);
205         if (passedThreshold == PinchThresholdManager.THRESHOLD_THREE) {
206             return true;
207         }
208 
209         mProgressDelta = interpolatedProgress - mPreviousProgress;
210         mPreviousProgress = interpolatedProgress;
211         mTimeDelta = System.currentTimeMillis() - mPreviousTimeMillis;
212         mPreviousTimeMillis = System.currentTimeMillis();
213         return false;
214     }
215 }