/* * 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 */); } } }