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 }