blob: 6ee96fc79c48d37feb5020f345d5204baf638e41 [file] [log] [blame]
/*
* 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;
}
}