blob: ef6f53e8f51812aa30404a77aeb51b76fe708941 [file] [log] [blame]
/*
* Copyright (C) 2020 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.uioverrides.touchcontrollers;
import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR;
import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
import static com.android.launcher3.LauncherState.HINT_STATE;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
import static com.android.quickstep.util.VibratorWrapper.OVERVIEW_HAPTIC;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.graphics.PointF;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.AnimatorControllerWithResistance;
import com.android.quickstep.util.MotionPauseDetector;
import com.android.quickstep.util.OverviewToHomeAnim;
import com.android.quickstep.util.VibratorWrapper;
import com.android.quickstep.views.RecentsView;
/**
* Touch controller which handles swipe and hold from the nav bar to go to Overview. Swiping above
* the nav bar falls back to go to All Apps. Swiping from the nav bar without holding goes to the
* first home screen instead of to Overview.
*/
public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouchController {
private static final float ONE_HANDED_ACTIVATED_SLOP_MULTIPLIER = 2.5f;
// How much of the movement to use for translating overview after swipe and hold.
private static final float OVERVIEW_MOVEMENT_FACTOR = 0.25f;
private static final long TRANSLATION_ANIM_MIN_DURATION_MS = 80;
private static final float TRANSLATION_ANIM_VELOCITY_DP_PER_MS = 0.8f;
private final RecentsView mRecentsView;
private final MotionPauseDetector mMotionPauseDetector;
private final float mMotionPauseMinDisplacement;
private boolean mDidTouchStartInNavBar;
private boolean mStartedOverview;
private boolean mReachedOverview;
// The last recorded displacement before we reached overview.
private PointF mStartDisplacement = new PointF();
private float mStartY;
private AnimatorPlaybackController mOverviewResistYAnim;
// Normal to Hint animation has flag SKIP_OVERVIEW, so we update this scrim with this animator.
private ObjectAnimator mNormalToHintOverviewScrimAnimator;
public NoButtonNavbarToOverviewTouchController(Launcher l) {
super(l);
mRecentsView = l.getOverviewPanel();
mMotionPauseDetector = new MotionPauseDetector(l);
mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
}
@Override
protected boolean canInterceptTouch(MotionEvent ev) {
mDidTouchStartInNavBar = (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
return super.canInterceptTouch(ev);
}
@Override
protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
if (fromState == NORMAL && mDidTouchStartInNavBar) {
return HINT_STATE;
} else if (fromState == OVERVIEW && isDragTowardPositive) {
// Don't allow swiping up to all apps.
return OVERVIEW;
}
return super.getTargetState(fromState, isDragTowardPositive);
}
@Override
protected float initCurrentAnimation() {
float progressMultiplier = super.initCurrentAnimation();
if (mToState == HINT_STATE) {
// Track the drag across the entire height of the screen.
progressMultiplier = -1f / mLauncher.getDeviceProfile().heightPx;
}
return progressMultiplier;
}
@Override
public void onDragStart(boolean start, float startDisplacement) {
super.onDragStart(start, startDisplacement);
mMotionPauseDetector.clear();
if (handlingOverviewAnim()) {
mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseDetected);
}
if (mFromState == NORMAL && mToState == HINT_STATE) {
mNormalToHintOverviewScrimAnimator = ObjectAnimator.ofArgb(
mLauncher.getScrimView(),
VIEW_BACKGROUND_COLOR,
mFromState.getWorkspaceScrimColor(mLauncher),
mToState.getWorkspaceScrimColor(mLauncher));
}
mStartedOverview = false;
mReachedOverview = false;
mOverviewResistYAnim = null;
}
@Override
protected void updateProgress(float fraction) {
super.updateProgress(fraction);
if (mNormalToHintOverviewScrimAnimator != null) {
mNormalToHintOverviewScrimAnimator.setCurrentFraction(fraction);
}
}
@Override
public void onDragEnd(float velocity) {
if (mStartedOverview) {
goToOverviewOrHomeOnDragEnd(velocity);
} else {
super.onDragEnd(velocity);
}
mMotionPauseDetector.clear();
mNormalToHintOverviewScrimAnimator = null;
if (mLauncher.isInState(OVERVIEW)) {
// Normally we would cleanup the state based on mCurrentAnimation, but since we stop
// using that when we pause to go to Overview, we need to clean up ourselves.
clearState();
}
}
@Override
protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
LauncherState targetState, float velocity, boolean isFling) {
super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState, velocity,
isFling);
if (targetState == HINT_STATE) {
// Normally we compute the duration based on the velocity and distance to the given
// state, but since the hint state tracks the entire screen without a clear endpoint, we
// need to manually set the duration to a reasonable value.
animator.setDuration(HINT_STATE.getTransitionDuration(mLauncher));
}
}
private void onMotionPauseDetected() {
if (mCurrentAnimation == null) {
return;
}
mNormalToHintOverviewScrimAnimator = null;
mCurrentAnimation.getTarget().addListener(newCancelListener(() ->
mLauncher.getStateManager().goToState(OVERVIEW, true, forSuccessCallback(() -> {
mOverviewResistYAnim = AnimatorControllerWithResistance
.createRecentsResistanceFromOverviewAnim(mLauncher, null)
.createPlaybackController();
mReachedOverview = true;
maybeSwipeInteractionToOverviewComplete();
}))));
mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
mCurrentAnimation.dispatchOnCancel();
mStartedOverview = true;
VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
}
private void maybeSwipeInteractionToOverviewComplete() {
if (mReachedOverview && !mDetector.isDraggingState()) {
onSwipeInteractionCompleted(OVERVIEW);
}
}
private boolean handlingOverviewAnim() {
int stateFlags = SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags();
return mDidTouchStartInNavBar && mStartState == NORMAL
&& (stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0;
}
@Override
public boolean onDrag(float yDisplacement, float xDisplacement, MotionEvent event) {
if (mStartedOverview) {
if (!mReachedOverview) {
mStartDisplacement.set(xDisplacement, yDisplacement);
mStartY = event.getY();
} else {
mRecentsView.setTranslationX((xDisplacement - mStartDisplacement.x)
* OVERVIEW_MOVEMENT_FACTOR);
float yProgress = (mStartDisplacement.y - yDisplacement) / mStartY;
if (yProgress > 0 && mOverviewResistYAnim != null) {
mOverviewResistYAnim.setPlayFraction(yProgress);
} else {
mRecentsView.setTranslationY((yDisplacement - mStartDisplacement.y)
* OVERVIEW_MOVEMENT_FACTOR);
}
}
}
float upDisplacement = -yDisplacement;
mMotionPauseDetector.setDisallowPause(!handlingOverviewAnim()
|| upDisplacement < mMotionPauseMinDisplacement);
mMotionPauseDetector.addPosition(event);
// Stay in Overview.
return mStartedOverview || super.onDrag(yDisplacement, xDisplacement, event);
}
private void goToOverviewOrHomeOnDragEnd(float velocity) {
boolean goToHomeInsteadOfOverview = !mMotionPauseDetector.isPaused();
if (goToHomeInsteadOfOverview) {
new OverviewToHomeAnim(mLauncher, () -> onSwipeInteractionCompleted(NORMAL))
.animateWithVelocity(velocity);
}
if (mReachedOverview) {
float distanceDp = dpiFromPx(Math.max(
Math.abs(mRecentsView.getTranslationX()),
Math.abs(mRecentsView.getTranslationY())));
long duration = (long) Math.max(TRANSLATION_ANIM_MIN_DURATION_MS,
distanceDp / TRANSLATION_ANIM_VELOCITY_DP_PER_MS);
mRecentsView.animate()
.translationX(0)
.translationY(0)
.setInterpolator(ACCEL_DEACCEL)
.setDuration(duration)
.withEndAction(goToHomeInsteadOfOverview
? null
: this::maybeSwipeInteractionToOverviewComplete);
if (!goToHomeInsteadOfOverview) {
// Return to normal properties for the overview state.
StateAnimationConfig config = new StateAnimationConfig();
config.duration = duration;
LauncherState state = mLauncher.getStateManager().getState();
mLauncher.getStateManager().createAtomicAnimation(state, state, config).start();
}
}
}
private float dpiFromPx(float pixels) {
return Utilities.dpiFromPx(pixels, mLauncher.getResources().getDisplayMetrics().densityDpi);
}
@Override
public void onOneHandedModeStateChanged(boolean activated) {
if (activated) {
mDetector.setTouchSlopMultiplier(ONE_HANDED_ACTIVATED_SLOP_MULTIPLIER);
} else {
// Reset touch slop multiplier to default 1.0f
mDetector.setTouchSlopMultiplier(1f /* default */);
}
}
}