| /* |
| * 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 onInterceptTouchEvent(MotionEvent ev) { |
| mPinchDetector.onTouchEvent(ev); |
| return mPinchStarted; |
| } |
| |
| public boolean 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 { |
| 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 (mWorkspace.getOpenFolder() != null) { |
| // Don't listen for the pinch gesture if a folder 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.onLauncherTransitionPrepare(mLauncher, false, 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.onLauncherTransitionEnd(mLauncher, false, true); |
| } |
| 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; |
| } |
| } |