blob: 9aed4ebb5174eac737021ab8d79ab518875a33c0 [file] [log] [blame]
/*
* Copyright (C) 2018 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.touch;
import static com.android.app.animation.Interpolators.scrollInterpolatorForVelocity;
import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
import static com.android.launcher3.LauncherAnimUtils.TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS;
import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.MotionEventsUtils.isTrackpadScroll;
import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS;
import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_OVERVIEW;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP;
import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
import android.animation.Animator.AnimatorListener;
import android.animation.ValueAnimator;
import android.view.MotionEvent;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.util.FlingBlockCheck;
import com.android.launcher3.util.TouchController;
/**
* TouchController for handling state changes
*/
public abstract class AbstractStateChangeTouchController
implements TouchController, SingleAxisSwipeDetector.Listener {
protected final Launcher mLauncher;
protected final SingleAxisSwipeDetector mDetector;
protected final SingleAxisSwipeDetector.Direction mSwipeDirection;
protected final AnimatorListener mClearStateOnCancelListener =
newCancelListener(this::clearState);
private final FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck();
protected int mStartContainerType;
protected LauncherState mStartState;
protected LauncherState mFromState;
protected LauncherState mToState;
protected AnimatorPlaybackController mCurrentAnimation;
protected boolean mGoingBetweenStates = true;
// Ratio of transition process [0, 1] to drag displacement (px)
protected float mProgressMultiplier;
protected boolean mIsTrackpadReverseScroll;
private boolean mNoIntercept;
private boolean mIsLogContainerSet;
private float mStartProgress;
private float mDisplacementShift;
private boolean mCanBlockFling;
private boolean mAllAppsOvershootStarted;
public AbstractStateChangeTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) {
mLauncher = l;
mDetector = new SingleAxisSwipeDetector(l, this, dir);
mSwipeDirection = dir;
}
protected abstract boolean canInterceptTouch(MotionEvent ev);
@Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mNoIntercept = !canInterceptTouch(ev);
if (mNoIntercept) {
return false;
}
mIsTrackpadReverseScroll = !mLauncher.isNaturalScrollingEnabled()
&& isTrackpadScroll(ev);
// Now figure out which direction scroll events the controller will start
// calling the callbacks.
final int directionsToDetectScroll;
boolean ignoreSlopWhenSettling = false;
if (mCurrentAnimation != null) {
directionsToDetectScroll = SingleAxisSwipeDetector.DIRECTION_BOTH;
ignoreSlopWhenSettling = true;
} else {
directionsToDetectScroll = getSwipeDirection();
if (directionsToDetectScroll == 0) {
mNoIntercept = true;
return false;
}
}
mDetector.setDetectableScrollConditions(
directionsToDetectScroll, ignoreSlopWhenSettling);
}
if (mNoIntercept) {
return false;
}
onControllerTouchEvent(ev);
return mDetector.isDraggingOrSettling();
}
private int getSwipeDirection() {
LauncherState fromState = mLauncher.getStateManager().getState();
int swipeDirection = 0;
if (getTargetState(fromState, true /* isDragTowardPositive */) != fromState) {
swipeDirection |= SingleAxisSwipeDetector.DIRECTION_POSITIVE;
}
if (getTargetState(fromState, false /* isDragTowardPositive */) != fromState) {
swipeDirection |= SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
}
return swipeDirection;
}
@Override
public final boolean onControllerTouchEvent(MotionEvent ev) {
return mDetector.onTouchEvent(ev);
}
protected float getShiftRange() {
return mLauncher.getAllAppsController().getShiftRange();
}
/**
* Returns the state to go to from fromState given the drag direction. If there is no state in
* that direction, returns fromState.
*/
protected abstract LauncherState getTargetState(LauncherState fromState,
boolean isDragTowardPositive);
protected abstract float initCurrentAnimation();
private boolean reinitCurrentAnimation(boolean reachedToState, boolean isDragTowardPositive) {
LauncherState newFromState = mFromState == null ? mLauncher.getStateManager().getState()
: reachedToState ? mToState : mFromState;
LauncherState newToState = getTargetState(newFromState, isDragTowardPositive);
onReinitToState(newToState);
if (newFromState == mFromState && newToState == mToState || (newFromState == newToState)) {
return false;
}
mFromState = newFromState;
mToState = newToState;
mStartProgress = 0;
if (mCurrentAnimation != null) {
mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
}
mProgressMultiplier = initCurrentAnimation();
mCurrentAnimation.dispatchOnStart();
return true;
}
protected void onReinitToState(LauncherState newToState) {
}
protected void onReachedFinalState(LauncherState newToState) {
}
@Override
public void onDragStart(boolean start, float startDisplacement) {
mStartState = mLauncher.getStateManager().getState();
mIsLogContainerSet = false;
if (mCurrentAnimation == null) {
mFromState = mStartState;
mToState = null;
cancelAnimationControllers();
reinitCurrentAnimation(false, mDetector.wasInitialTouchPositive());
mDisplacementShift = 0;
} else {
mCurrentAnimation.pause();
mStartProgress = mCurrentAnimation.getProgressFraction();
}
mCanBlockFling = mFromState == NORMAL;
mFlingBlockCheck.unblockFling();
}
@Override
public boolean onDrag(float displacement) {
float deltaProgress = mProgressMultiplier * (displacement - mDisplacementShift);
float progress = deltaProgress + mStartProgress;
updateProgress(progress);
boolean isDragTowardPositive = mSwipeDirection.isPositive(
displacement - mDisplacementShift);
if (progress <= 0) {
if (reinitCurrentAnimation(false, isDragTowardPositive)) {
mDisplacementShift = displacement;
if (mCanBlockFling) {
mFlingBlockCheck.blockFling();
}
}
if (mFromState == LauncherState.ALL_APPS) {
mAllAppsOvershootStarted = true;
mLauncher.getAppsView().onPull(-progress , -progress);
}
} else if (progress >= 1) {
if (reinitCurrentAnimation(true, isDragTowardPositive)) {
mDisplacementShift = displacement;
if (mCanBlockFling) {
mFlingBlockCheck.blockFling();
}
}
if (mToState == LauncherState.ALL_APPS) {
mAllAppsOvershootStarted = true;
// 1f, value when all apps container hit the top
mLauncher.getAppsView().onPull(progress - 1f, progress - 1f);
}
} else {
mFlingBlockCheck.onEvent();
}
return true;
}
@Override
public boolean onDrag(float displacement, MotionEvent ev) {
if (!mIsLogContainerSet) {
if (mStartState == ALL_APPS) {
mStartContainerType = LAUNCHER_STATE_ALLAPPS;
} else if (mStartState == NORMAL) {
mStartContainerType = LAUNCHER_STATE_HOME;
} else if (mStartState == OVERVIEW) {
mStartContainerType = LAUNCHER_STATE_OVERVIEW;
}
mIsLogContainerSet = true;
}
// Only reverse the gesture to open all apps (not close) when trackpad reverse scrolling is
// on.
if (mIsTrackpadReverseScroll && mStartState == NORMAL) {
displacement = -displacement;
}
return onDrag(displacement);
}
protected void updateProgress(float fraction) {
if (mCurrentAnimation == null) {
return;
}
mCurrentAnimation.setPlayFraction(fraction);
}
/**
* Returns animation config for state transition between provided states
*/
protected StateAnimationConfig getConfigForStates(
LauncherState fromState, LauncherState toState) {
return new StateAnimationConfig();
}
@Override
public void onDragEnd(float velocity) {
if (mCurrentAnimation == null) {
// Unlikely, but we may have been canceled just before onDragEnd(). We assume whoever
// canceled us will handle a new state transition to clean up.
return;
}
// Only reverse the gesture to open all apps (not close) when trackpad reverse scrolling is
// on.
if (mIsTrackpadReverseScroll && mStartState == NORMAL) {
velocity = -velocity;
}
boolean fling = mDetector.isFling(velocity);
boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
if (blockedFling) {
fling = false;
}
final LauncherState targetState;
final float progress = mCurrentAnimation.getProgressFraction();
final float progressVelocity = velocity * mProgressMultiplier;
final float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
if (fling) {
targetState =
Float.compare(Math.signum(velocity), Math.signum(mProgressMultiplier)) == 0
? mToState : mFromState;
// snap to top or bottom using the release velocity
} else {
float successTransitionProgress = SUCCESS_TRANSITION_PROGRESS;
if (mLauncher.getDeviceProfile().isTablet
&& (mToState == ALL_APPS || mFromState == ALL_APPS)) {
successTransitionProgress = TABLET_BOTTOM_SHEET_SUCCESS_TRANSITION_PROGRESS;
} else if (!mLauncher.getDeviceProfile().isTablet
&& mToState == ALL_APPS && mFromState == NORMAL) {
successTransitionProgress = AllAppsSwipeController.ALL_APPS_STATE_TRANSITION_MANUAL;
} else if (!mLauncher.getDeviceProfile().isTablet
&& mToState == NORMAL && mFromState == ALL_APPS) {
successTransitionProgress =
1 - AllAppsSwipeController.ALL_APPS_STATE_TRANSITION_MANUAL;
}
targetState =
(interpolatedProgress > successTransitionProgress) ? mToState : mFromState;
}
final float endProgress;
final float startProgress;
final long duration;
// Increase the duration if we prevented the fling, as we are going against a high velocity.
final int durationMultiplier = blockedFling && targetState == mFromState
? LauncherAnimUtils.blockedFlingDurationFactor(velocity) : 1;
if (targetState == mToState) {
endProgress = 1;
if (progress >= 1) {
duration = 0;
startProgress = 1;
} else {
startProgress = Utilities.boundToRange(progress
+ progressVelocity * getSingleFrameMs(mLauncher), 0f, 1f);
duration = BaseSwipeDetector.calculateDuration(velocity,
endProgress - Math.max(progress, 0)) * durationMultiplier;
}
} else {
// Let the state manager know that the animation didn't go to the target state,
// but don't cancel ourselves (we already clean up when the animation completes).
mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
mCurrentAnimation.dispatchOnCancel();
endProgress = 0;
if (progress <= 0) {
duration = 0;
startProgress = 0;
} else {
startProgress = Utilities.boundToRange(progress
+ progressVelocity * getSingleFrameMs(mLauncher), 0f, 1f);
duration = BaseSwipeDetector.calculateDuration(velocity,
Math.min(progress, 1) - endProgress) * durationMultiplier;
}
}
mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState));
ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
anim.setFloatValues(startProgress, endProgress);
updateSwipeCompleteAnimation(anim, duration, targetState, velocity, fling);
mCurrentAnimation.dispatchOnStart();
if (targetState == LauncherState.ALL_APPS) {
if (mAllAppsOvershootStarted) {
mLauncher.getAppsView().onRelease();
mAllAppsOvershootStarted = false;
} else {
mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity, progress);
}
}
anim.start();
}
protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
LauncherState targetState, float velocity, boolean isFling) {
animator.setDuration(expectedDuration)
.setInterpolator(scrollInterpolatorForVelocity(velocity));
}
protected void onSwipeInteractionCompleted(LauncherState targetState) {
onReachedFinalState(mToState);
clearState();
boolean shouldGoToTargetState = mGoingBetweenStates || (mToState != targetState);
if (shouldGoToTargetState) {
goToTargetState(targetState);
} else {
logReachedState(mToState);
}
}
protected void goToTargetState(LauncherState targetState) {
if (!mLauncher.isInState(targetState)) {
// If we're already in the target state, don't jump to it at the end of the animation in
// case the user started interacting with it before the animation finished.
mLauncher.getStateManager().goToState(targetState, false /* animated */,
forEndCallback(() -> logReachedState(targetState)));
} else {
logReachedState(targetState);
}
mLauncher.getRootView().getSysUiScrim().getSysUIMultiplier().animateToValue(1f)
.setDuration(0).start();
}
private void logReachedState(LauncherState targetState) {
if (mStartState == targetState) {
return;
}
// Transition complete. log the action
mLauncher.getStatsLogManager().logger()
.withSrcState(mStartState.statsLogOrdinal)
.withDstState(targetState.statsLogOrdinal)
.withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
.setWorkspace(
LauncherAtom.WorkspaceContainer.newBuilder()
.setPageIndex(mLauncher.getWorkspace().getCurrentPage()))
.build())
.log(StatsLogManager.getLauncherAtomEvent(mStartState.statsLogOrdinal,
targetState.statsLogOrdinal, mToState.ordinal > mFromState.ordinal
? LAUNCHER_UNKNOWN_SWIPEUP
: LAUNCHER_UNKNOWN_SWIPEDOWN));
}
protected void clearState() {
cancelAnimationControllers();
mGoingBetweenStates = true;
mDetector.finishedScrolling();
mDetector.setDetectableScrollConditions(0, false);
mIsTrackpadReverseScroll = false;
}
private void cancelAnimationControllers() {
mCurrentAnimation = null;
}
protected boolean shouldOpenAllApps(boolean isDragTowardPositive) {
return (isDragTowardPositive && !mIsTrackpadReverseScroll)
|| (!isDragTowardPositive && mIsTrackpadReverseScroll);
}
}