945bb34b5e
There is a case when one handed mode triggered(Activated), all apps
drawer is very easy to trigger while user swipe up around NavBar
region to exit one handed mode. Since System Gesture monitor regsion
is small on screen bottom, swipe-up gesture usually cross over NavBar
monitor region and invoke launcher touch controller intercept touch
event and introduce unexpectedly trigger all apps drawer.
Adding onOneHandedModeStateChanged(boolean activated) for controller
be able to adjust the touch slop by multiplier, we can set a larger
multiplier when the visible window size translate become smaller
and make swipe gesture not too sensitive.
Test: manual swipe up to swich "home <-> all apps" and monitor
minDisplacement of SingleAxisSwipeDetector
Test: Trigger one handed mode and swipe up to exit one handed mode
check the minDisplacement of SingleAxisSwipeDetector
Bug: 186235522
Change-Id: I9729cd408d85b2b22582bf800e28d1471fc06980
274 lines
12 KiB
Java
274 lines
12 KiB
Java
/*
|
|
* 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.launcher3.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.launcher3.util.VibratorWrapper;
|
|
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.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 */);
|
|
}
|
|
}
|
|
}
|