2017-10-27 11:05:26 -07:00
|
|
|
/*
|
2018-03-14 17:51:49 -07:00
|
|
|
* Copyright (C) 2019 The Android Open Source Project
|
2017-10-27 11:05:26 -07:00
|
|
|
*
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2018-03-14 17:51:49 -07:00
|
|
|
package com.android.launcher3.touch;
|
2017-10-27 11:05:26 -07:00
|
|
|
|
|
|
|
|
import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
|
|
|
|
|
|
|
|
|
|
import android.animation.Animator;
|
|
|
|
|
import android.animation.AnimatorListenerAdapter;
|
|
|
|
|
import android.animation.ValueAnimator;
|
|
|
|
|
import android.util.Log;
|
|
|
|
|
import android.view.MotionEvent;
|
|
|
|
|
|
2017-12-12 12:02:29 -08:00
|
|
|
import com.android.launcher3.Launcher;
|
|
|
|
|
import com.android.launcher3.LauncherState;
|
|
|
|
|
import com.android.launcher3.Utilities;
|
2017-10-27 11:05:26 -07:00
|
|
|
import com.android.launcher3.anim.AnimatorPlaybackController;
|
2018-03-14 17:51:49 -07:00
|
|
|
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
|
|
|
|
|
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
|
|
|
|
|
import com.android.launcher3.util.TouchController;
|
2017-10-27 11:05:26 -07:00
|
|
|
|
|
|
|
|
/**
|
2018-03-14 17:51:49 -07:00
|
|
|
* TouchController for handling state changes
|
2017-10-27 11:05:26 -07:00
|
|
|
*/
|
2018-03-14 17:51:49 -07:00
|
|
|
public abstract class AbstractStateChangeTouchController extends AnimatorListenerAdapter
|
2017-10-27 11:05:26 -07:00
|
|
|
implements TouchController, SwipeDetector.Listener {
|
|
|
|
|
|
2018-03-14 17:51:49 -07:00
|
|
|
private static final String TAG = "ASCTouchController";
|
|
|
|
|
public static final float RECATCH_REJECTION_FRACTION = .0875f;
|
|
|
|
|
public static final int SINGLE_FRAME_MS = 16;
|
2017-10-27 11:05:26 -07:00
|
|
|
|
|
|
|
|
// Progress after which the transition is assumed to be a success in case user does not fling
|
2018-03-14 17:51:49 -07:00
|
|
|
public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
|
2017-10-27 11:05:26 -07:00
|
|
|
|
2017-12-12 12:02:29 -08:00
|
|
|
protected final Launcher mLauncher;
|
2018-02-01 14:46:13 -08:00
|
|
|
protected final SwipeDetector mDetector;
|
2017-10-27 11:05:26 -07:00
|
|
|
|
|
|
|
|
private boolean mNoIntercept;
|
2018-03-14 17:51:49 -07:00
|
|
|
protected int mStartContainerType;
|
2017-10-27 11:05:26 -07:00
|
|
|
|
2018-03-14 17:51:49 -07:00
|
|
|
protected LauncherState mFromState;
|
2018-02-16 03:23:38 +00:00
|
|
|
protected LauncherState mToState;
|
2018-03-14 17:51:49 -07:00
|
|
|
protected AnimatorPlaybackController mCurrentAnimation;
|
2017-10-27 11:05:26 -07:00
|
|
|
|
|
|
|
|
private float mStartProgress;
|
|
|
|
|
// Ratio of transition process [0, 1] to drag displacement (px)
|
|
|
|
|
private float mProgressMultiplier;
|
2018-03-28 11:21:49 -07:00
|
|
|
private float mDisplacementShift;
|
2017-10-27 11:05:26 -07:00
|
|
|
|
2018-03-14 17:51:49 -07:00
|
|
|
public AbstractStateChangeTouchController(Launcher l, SwipeDetector.Direction dir) {
|
2017-10-27 11:05:26 -07:00
|
|
|
mLauncher = l;
|
2018-01-11 12:42:05 -08:00
|
|
|
mDetector = new SwipeDetector(l, this, dir);
|
2017-10-27 11:05:26 -07:00
|
|
|
}
|
|
|
|
|
|
2018-03-14 17:51:49 -07:00
|
|
|
protected abstract boolean canInterceptTouch(MotionEvent ev);
|
2017-12-12 12:02:29 -08:00
|
|
|
|
2018-03-14 17:51:49 -07:00
|
|
|
/**
|
|
|
|
|
* Initializes the {@code mFromState} and {@code mToState} and swipe direction to use for
|
2018-03-28 11:21:49 -07:00
|
|
|
* the detector. In case of disabling swipe, return 0.
|
2018-03-14 17:51:49 -07:00
|
|
|
*/
|
|
|
|
|
protected abstract int getSwipeDirection(MotionEvent ev);
|
2017-10-27 11:05:26 -07:00
|
|
|
|
|
|
|
|
@Override
|
2018-03-14 17:51:49 -07:00
|
|
|
public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
|
2017-10-27 11:05:26 -07:00
|
|
|
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
|
|
|
|
|
mNoIntercept = !canInterceptTouch(ev);
|
|
|
|
|
if (mNoIntercept) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now figure out which direction scroll events the controller will start
|
|
|
|
|
// calling the callbacks.
|
|
|
|
|
final int directionsToDetectScroll;
|
|
|
|
|
boolean ignoreSlopWhenSettling = false;
|
|
|
|
|
|
|
|
|
|
if (mCurrentAnimation != null) {
|
|
|
|
|
if (mCurrentAnimation.getProgressFraction() > 1 - RECATCH_REJECTION_FRACTION) {
|
|
|
|
|
directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
|
|
|
|
|
} else if (mCurrentAnimation.getProgressFraction() < RECATCH_REJECTION_FRACTION ) {
|
|
|
|
|
directionsToDetectScroll = SwipeDetector.DIRECTION_NEGATIVE;
|
|
|
|
|
} else {
|
|
|
|
|
directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
|
|
|
|
|
ignoreSlopWhenSettling = true;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2017-12-12 12:02:29 -08:00
|
|
|
directionsToDetectScroll = getSwipeDirection(ev);
|
2018-03-14 17:51:49 -07:00
|
|
|
if (directionsToDetectScroll == 0) {
|
|
|
|
|
mNoIntercept = true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2017-10-27 11:05:26 -07:00
|
|
|
}
|
|
|
|
|
mDetector.setDetectableScrollConditions(
|
|
|
|
|
directionsToDetectScroll, ignoreSlopWhenSettling);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mNoIntercept) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onControllerTouchEvent(ev);
|
|
|
|
|
return mDetector.isDraggingOrSettling();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
2018-03-14 17:51:49 -07:00
|
|
|
public final boolean onControllerTouchEvent(MotionEvent ev) {
|
2017-10-27 11:05:26 -07:00
|
|
|
return mDetector.onTouchEvent(ev);
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-14 17:51:49 -07:00
|
|
|
protected float getShiftRange() {
|
|
|
|
|
return mLauncher.getAllAppsController().getShiftRange();
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-28 11:21:49 -07:00
|
|
|
protected abstract LauncherState getTargetState(LauncherState fromState,
|
|
|
|
|
boolean isDragTowardPositive);
|
|
|
|
|
|
2018-03-14 17:51:49 -07:00
|
|
|
protected abstract float initCurrentAnimation();
|
|
|
|
|
|
2018-03-28 11:21:49 -07:00
|
|
|
private boolean reinitCurrentAnimation(boolean reachedToState, boolean isDragTowardPositive) {
|
|
|
|
|
LauncherState newFromState = mFromState == null ? mLauncher.getStateManager().getState()
|
|
|
|
|
: reachedToState ? mToState : mFromState;
|
|
|
|
|
LauncherState newToState = getTargetState(newFromState, isDragTowardPositive);
|
|
|
|
|
|
|
|
|
|
if (newFromState == mFromState && newToState == mToState || (newFromState == newToState)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mFromState = newFromState;
|
|
|
|
|
mToState = newToState;
|
|
|
|
|
|
|
|
|
|
mStartProgress = 0;
|
|
|
|
|
mProgressMultiplier = initCurrentAnimation();
|
|
|
|
|
mCurrentAnimation.getTarget().addListener(this);
|
|
|
|
|
mCurrentAnimation.dispatchOnStart();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-27 11:05:26 -07:00
|
|
|
@Override
|
|
|
|
|
public void onDragStart(boolean start) {
|
|
|
|
|
if (mCurrentAnimation == null) {
|
2018-03-28 11:21:49 -07:00
|
|
|
mFromState = mToState = null;
|
|
|
|
|
reinitCurrentAnimation(false, mDetector.wasInitialTouchPositive());
|
|
|
|
|
mDisplacementShift = 0;
|
2017-10-27 11:05:26 -07:00
|
|
|
} else {
|
|
|
|
|
mCurrentAnimation.pause();
|
|
|
|
|
mStartProgress = mCurrentAnimation.getProgressFraction();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean onDrag(float displacement, float velocity) {
|
2018-03-28 11:21:49 -07:00
|
|
|
float deltaProgress = mProgressMultiplier * (displacement - mDisplacementShift);
|
|
|
|
|
float progress = deltaProgress + mStartProgress;
|
|
|
|
|
updateProgress(progress);
|
|
|
|
|
boolean isDragTowardPositive = (displacement - mDisplacementShift) < 0;
|
|
|
|
|
if (progress <= 0) {
|
|
|
|
|
if (reinitCurrentAnimation(false, isDragTowardPositive)) {
|
|
|
|
|
mDisplacementShift = displacement;
|
|
|
|
|
}
|
|
|
|
|
} else if (progress >= 1) {
|
|
|
|
|
if (reinitCurrentAnimation(true, isDragTowardPositive)) {
|
|
|
|
|
mDisplacementShift = displacement;
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-10-27 11:05:26 -07:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-21 09:32:27 -07:00
|
|
|
protected void updateProgress(float fraction) {
|
|
|
|
|
mCurrentAnimation.setPlayFraction(fraction);
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-27 11:05:26 -07:00
|
|
|
@Override
|
|
|
|
|
public void onDragEnd(float velocity, boolean fling) {
|
2018-03-14 17:51:49 -07:00
|
|
|
final int logAction;
|
2017-10-27 11:05:26 -07:00
|
|
|
final LauncherState targetState;
|
|
|
|
|
final float progress = mCurrentAnimation.getProgressFraction();
|
|
|
|
|
|
|
|
|
|
if (fling) {
|
2018-03-14 17:51:49 -07:00
|
|
|
logAction = Touch.FLING;
|
|
|
|
|
targetState =
|
|
|
|
|
Float.compare(Math.signum(velocity), Math.signum(mProgressMultiplier)) == 0
|
|
|
|
|
? mToState : mFromState;
|
|
|
|
|
// snap to top or bottom using the release velocity
|
|
|
|
|
} else {
|
|
|
|
|
logAction = Touch.SWIPE;
|
|
|
|
|
targetState = (progress > SUCCESS_TRANSITION_PROGRESS) ? mToState : mFromState;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final float endProgress;
|
|
|
|
|
final float startProgress;
|
|
|
|
|
final long duration;
|
|
|
|
|
|
|
|
|
|
if (targetState == mToState) {
|
|
|
|
|
endProgress = 1;
|
|
|
|
|
if (progress >= 1) {
|
|
|
|
|
duration = 0;
|
|
|
|
|
startProgress = 1;
|
2017-10-27 11:05:26 -07:00
|
|
|
} else {
|
2018-03-14 17:51:49 -07:00
|
|
|
startProgress = Utilities.boundToRange(
|
2018-03-21 09:32:27 -07:00
|
|
|
progress + velocity * SINGLE_FRAME_MS * mProgressMultiplier, 0f, 1f);
|
2018-03-14 17:51:49 -07:00
|
|
|
duration = SwipeDetector.calculateDuration(velocity,
|
|
|
|
|
endProgress - Math.max(progress, 0));
|
2017-10-27 11:05:26 -07:00
|
|
|
}
|
|
|
|
|
} else {
|
2018-03-14 17:51:49 -07:00
|
|
|
endProgress = 0;
|
|
|
|
|
if (progress <= 0) {
|
|
|
|
|
duration = 0;
|
|
|
|
|
startProgress = 0;
|
2017-10-27 11:05:26 -07:00
|
|
|
} else {
|
2018-03-14 17:51:49 -07:00
|
|
|
startProgress = Utilities.boundToRange(
|
2018-03-21 09:32:27 -07:00
|
|
|
progress + velocity * SINGLE_FRAME_MS * mProgressMultiplier, 0f, 1f);
|
2018-03-14 17:51:49 -07:00
|
|
|
duration = SwipeDetector.calculateDuration(velocity,
|
|
|
|
|
Math.min(progress, 1) - endProgress);
|
2017-10-27 11:05:26 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-14 17:51:49 -07:00
|
|
|
mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState, logAction));
|
2017-10-27 11:05:26 -07:00
|
|
|
ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
|
2018-03-14 17:51:49 -07:00
|
|
|
anim.setFloatValues(startProgress, endProgress);
|
2018-03-21 09:32:27 -07:00
|
|
|
updateSwipeCompleteAnimation(anim, duration, targetState, velocity, fling);
|
2017-10-27 11:05:26 -07:00
|
|
|
anim.start();
|
|
|
|
|
}
|
2017-12-12 12:02:29 -08:00
|
|
|
|
2018-03-21 09:32:27 -07:00
|
|
|
protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
|
|
|
|
|
LauncherState targetState, float velocity, boolean isFling) {
|
|
|
|
|
animator.setDuration(expectedDuration)
|
|
|
|
|
.setInterpolator(scrollInterpolatorForVelocity(velocity));
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-14 17:51:49 -07:00
|
|
|
protected int getDirectionForLog() {
|
|
|
|
|
return mToState.ordinal > mFromState.ordinal ? Direction.UP : Direction.DOWN;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
|
|
|
|
|
if (targetState != mFromState) {
|
|
|
|
|
// Transition complete. log the action
|
|
|
|
|
mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
|
|
|
|
|
getDirectionForLog(),
|
|
|
|
|
mStartContainerType,
|
|
|
|
|
mFromState.containerType,
|
|
|
|
|
mToState.containerType,
|
|
|
|
|
mLauncher.getWorkspace().getCurrentPage());
|
|
|
|
|
}
|
|
|
|
|
clearState();
|
|
|
|
|
mLauncher.getStateManager().goToState(targetState, false /* animated */);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void clearState() {
|
|
|
|
|
mCurrentAnimation = null;
|
|
|
|
|
mDetector.finishedScrolling();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onAnimationCancel(Animator animation) {
|
|
|
|
|
if (mCurrentAnimation != null && animation == mCurrentAnimation.getOriginalTarget()) {
|
|
|
|
|
Log.e(TAG, "Who dare cancel the animation when I am in control", new Exception());
|
|
|
|
|
clearState();
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-10-27 11:05:26 -07:00
|
|
|
}
|