| /* |
| * 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.quickstep.interaction; |
| |
| import static com.android.launcher3.Utilities.squaredHypot; |
| import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC; |
| import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_COMPLETED; |
| import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_BAD_ANGLE; |
| import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT; |
| import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_GESTURE_COMPLETED; |
| import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_NOT_STARTED_TOO_FAR_FROM_EDGE; |
| import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_CANCELLED; |
| import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION; |
| import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_GESTURE_COMPLETED; |
| import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE; |
| |
| import android.animation.ValueAnimator; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Point; |
| import android.graphics.PointF; |
| import android.graphics.RectF; |
| import android.os.SystemClock; |
| import android.view.Display; |
| import android.view.GestureDetector; |
| import android.view.MotionEvent; |
| import android.view.Surface; |
| import android.view.View; |
| import android.view.View.OnTouchListener; |
| import android.view.ViewConfiguration; |
| |
| import androidx.annotation.Nullable; |
| |
| import com.android.launcher3.R; |
| import com.android.launcher3.ResourceUtils; |
| import com.android.launcher3.anim.Interpolators; |
| import com.android.launcher3.util.VibratorWrapper; |
| import com.android.quickstep.SysUINavigationMode.Mode; |
| import com.android.quickstep.util.MotionPauseDetector; |
| import com.android.quickstep.util.NavBarPosition; |
| import com.android.quickstep.util.TriggerSwipeUpTouchTracker; |
| import com.android.systemui.shared.system.QuickStepContract; |
| |
| /** Utility class to handle Home and Assistant gestures. */ |
| public class NavBarGestureHandler implements OnTouchListener, |
| TriggerSwipeUpTouchTracker.OnSwipeUpListener { |
| |
| private static final String LOG_TAG = "NavBarGestureHandler"; |
| private static final long RETRACT_GESTURE_ANIMATION_DURATION_MS = 300; |
| |
| private final Context mContext; |
| private final Point mDisplaySize = new Point(); |
| private final TriggerSwipeUpTouchTracker mSwipeUpTouchTracker; |
| private final int mBottomGestureHeight; |
| private final GestureDetector mAssistantGestureDetector; |
| private final int mAssistantAngleThreshold; |
| private final RectF mAssistantLeftRegion = new RectF(); |
| private final RectF mAssistantRightRegion = new RectF(); |
| private final float mAssistantDragDistThreshold; |
| private final float mAssistantFlingDistThreshold; |
| private final long mAssistantTimeThreshold; |
| private final float mAssistantSquaredSlop; |
| private final PointF mAssistantStartDragPos = new PointF(); |
| private final PointF mDownPos = new PointF(); |
| private final PointF mLastPos = new PointF(); |
| private final MotionPauseDetector mMotionPauseDetector; |
| private boolean mTouchCameFromAssistantCorner; |
| private boolean mTouchCameFromNavBar; |
| private boolean mPassedAssistantSlop; |
| private boolean mAssistantGestureActive; |
| private boolean mLaunchedAssistant; |
| private long mAssistantDragStartTime; |
| private float mAssistantDistance; |
| private float mAssistantTimeFraction; |
| private float mAssistantLastProgress; |
| @Nullable |
| private NavBarGestureAttemptCallback mGestureCallback; |
| |
| NavBarGestureHandler(Context context) { |
| mContext = context; |
| final Display display = mContext.getDisplay(); |
| final int displayRotation; |
| if (display == null) { |
| displayRotation = Surface.ROTATION_0; |
| } else { |
| displayRotation = display.getRotation(); |
| display.getRealSize(mDisplaySize); |
| } |
| mSwipeUpTouchTracker = |
| new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/, |
| new NavBarPosition(Mode.NO_BUTTON, displayRotation), |
| null /*onInterceptTouch*/, this); |
| mMotionPauseDetector = new MotionPauseDetector(context); |
| |
| final Resources resources = context.getResources(); |
| mBottomGestureHeight = |
| ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, resources); |
| mAssistantDragDistThreshold = |
| resources.getDimension(R.dimen.gestures_assistant_drag_threshold); |
| mAssistantFlingDistThreshold = |
| resources.getDimension(R.dimen.gestures_assistant_fling_threshold); |
| mAssistantTimeThreshold = |
| resources.getInteger(R.integer.assistant_gesture_min_time_threshold); |
| mAssistantAngleThreshold = |
| resources.getInteger(R.integer.assistant_gesture_corner_deg_threshold); |
| |
| mAssistantGestureDetector = new GestureDetector(context, new AssistantGestureListener()); |
| int assistantWidth = resources.getDimensionPixelSize(R.dimen.gestures_assistant_width); |
| final float assistantHeight = Math.max(mBottomGestureHeight, |
| QuickStepContract.getWindowCornerRadius(resources)); |
| mAssistantLeftRegion.bottom = mAssistantRightRegion.bottom = mDisplaySize.y; |
| mAssistantLeftRegion.top = mAssistantRightRegion.top = mDisplaySize.y - assistantHeight; |
| mAssistantLeftRegion.left = 0; |
| mAssistantLeftRegion.right = assistantWidth; |
| mAssistantRightRegion.right = mDisplaySize.x; |
| mAssistantRightRegion.left = mDisplaySize.x - assistantWidth; |
| float slop = ViewConfiguration.get(context).getScaledTouchSlop(); |
| mAssistantSquaredSlop = slop * slop; |
| } |
| |
| void registerNavBarGestureAttemptCallback(NavBarGestureAttemptCallback callback) { |
| mGestureCallback = callback; |
| } |
| |
| void unregisterNavBarGestureAttemptCallback() { |
| mGestureCallback = null; |
| } |
| |
| @Override |
| public void onSwipeUp(boolean wasFling, PointF finalVelocity) { |
| if (mGestureCallback == null || mAssistantGestureActive) { |
| return; |
| } |
| finalVelocity.set(finalVelocity.x / 1000, finalVelocity.y / 1000); |
| if (mTouchCameFromNavBar) { |
| mGestureCallback.onNavBarGestureAttempted(wasFling |
| ? HOME_GESTURE_COMPLETED : OVERVIEW_GESTURE_COMPLETED, finalVelocity); |
| } else { |
| mGestureCallback.onNavBarGestureAttempted(wasFling |
| ? HOME_NOT_STARTED_TOO_FAR_FROM_EDGE : OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE, |
| finalVelocity); |
| } |
| } |
| |
| @Override |
| public void onSwipeUpCancelled() { |
| if (mGestureCallback != null && !mAssistantGestureActive) { |
| mGestureCallback.onNavBarGestureAttempted(HOME_OR_OVERVIEW_CANCELLED, new PointF()); |
| } |
| } |
| |
| @Override |
| public boolean onTouch(View view, MotionEvent event) { |
| int action = event.getAction(); |
| boolean intercepted = mSwipeUpTouchTracker.interceptedTouch(); |
| switch (action) { |
| case MotionEvent.ACTION_DOWN: |
| mDownPos.set(event.getX(), event.getY()); |
| mLastPos.set(mDownPos); |
| mTouchCameFromAssistantCorner = |
| mAssistantLeftRegion.contains(event.getX(), event.getY()) |
| || mAssistantRightRegion.contains(event.getX(), event.getY()); |
| mAssistantGestureActive = mTouchCameFromAssistantCorner; |
| mTouchCameFromNavBar = !mTouchCameFromAssistantCorner |
| && mDownPos.y >= mDisplaySize.y - mBottomGestureHeight; |
| if (!mTouchCameFromNavBar && mGestureCallback != null) { |
| mGestureCallback.setNavBarGestureProgress(null); |
| } |
| mLaunchedAssistant = false; |
| mSwipeUpTouchTracker.init(); |
| mMotionPauseDetector.clear(); |
| mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseChanged); |
| break; |
| case MotionEvent.ACTION_MOVE: |
| mLastPos.set(event.getX(), event.getY()); |
| if (!mAssistantGestureActive) { |
| break; |
| } |
| |
| if (!mPassedAssistantSlop) { |
| // Normal gesture, ensure we pass the slop before we start tracking the gesture |
| if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) |
| > mAssistantSquaredSlop) { |
| |
| mPassedAssistantSlop = true; |
| mAssistantStartDragPos.set(mLastPos.x, mLastPos.y); |
| mAssistantDragStartTime = SystemClock.uptimeMillis(); |
| |
| mAssistantGestureActive = isValidAssistantGestureAngle( |
| mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y); |
| if (!mAssistantGestureActive && mGestureCallback != null) { |
| mGestureCallback.onNavBarGestureAttempted( |
| ASSISTANT_NOT_STARTED_BAD_ANGLE, new PointF()); |
| } |
| } |
| } else { |
| // Movement |
| mAssistantDistance = (float) Math.hypot(mLastPos.x - mAssistantStartDragPos.x, |
| mLastPos.y - mAssistantStartDragPos.y); |
| if (mAssistantDistance >= 0) { |
| final long diff = SystemClock.uptimeMillis() - mAssistantDragStartTime; |
| mAssistantTimeFraction = Math.min(diff * 1f / mAssistantTimeThreshold, 1); |
| updateAssistantProgress(); |
| } |
| } |
| break; |
| case MotionEvent.ACTION_UP: |
| case MotionEvent.ACTION_CANCEL: |
| mMotionPauseDetector.clear(); |
| mMotionPauseDetector.setOnMotionPauseListener(null); |
| if (mGestureCallback != null && !intercepted && mTouchCameFromNavBar) { |
| mGestureCallback.onNavBarGestureAttempted( |
| HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, new PointF()); |
| intercepted = true; |
| break; |
| } |
| if (mAssistantGestureActive && !mLaunchedAssistant && mGestureCallback != null) { |
| mGestureCallback.onNavBarGestureAttempted( |
| ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT, new PointF()); |
| ValueAnimator animator = ValueAnimator.ofFloat(mAssistantLastProgress, 0) |
| .setDuration(RETRACT_GESTURE_ANIMATION_DURATION_MS); |
| animator.addUpdateListener(valueAnimator -> { |
| float progress = (float) valueAnimator.getAnimatedValue(); |
| mGestureCallback.setAssistantProgress(progress); |
| }); |
| animator.setInterpolator(Interpolators.DEACCEL_2); |
| animator.start(); |
| } |
| mPassedAssistantSlop = false; |
| break; |
| } |
| if (mTouchCameFromNavBar && mGestureCallback != null) { |
| mGestureCallback.setNavBarGestureProgress(event.getY() - mDownPos.y); |
| } |
| mSwipeUpTouchTracker.onMotionEvent(event); |
| mAssistantGestureDetector.onTouchEvent(event); |
| mMotionPauseDetector.addPosition(event); |
| mMotionPauseDetector.setDisallowPause(mLastPos.y >= mDisplaySize.y - mBottomGestureHeight); |
| return intercepted; |
| } |
| |
| protected void onMotionPauseChanged(boolean isPaused) { |
| if (isPaused) { |
| VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC); |
| } |
| } |
| |
| /** |
| * Determine if angle is larger than threshold for assistant detection |
| */ |
| private boolean isValidAssistantGestureAngle(float deltaX, float deltaY) { |
| float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX)); |
| |
| // normalize so that angle is measured clockwise from horizontal in the bottom right corner |
| // and counterclockwise from horizontal in the bottom left corner |
| angle = angle > 90 ? 180 - angle : angle; |
| return (angle > mAssistantAngleThreshold && angle < 90); |
| } |
| |
| private void updateAssistantProgress() { |
| if (!mLaunchedAssistant) { |
| mAssistantLastProgress = |
| Math.min(mAssistantDistance * 1f / mAssistantDragDistThreshold, 1) |
| * mAssistantTimeFraction; |
| if (mAssistantDistance >= mAssistantDragDistThreshold && mAssistantTimeFraction >= 1) { |
| startAssistant(new PointF()); |
| } else if (mGestureCallback != null) { |
| mGestureCallback.setAssistantProgress(mAssistantLastProgress); |
| } |
| } |
| } |
| |
| private void startAssistant(PointF velocity) { |
| if (mGestureCallback != null) { |
| mGestureCallback.onNavBarGestureAttempted(ASSISTANT_COMPLETED, velocity); |
| } |
| VibratorWrapper.INSTANCE.get(mContext).vibrate(VibratorWrapper.EFFECT_CLICK); |
| mLaunchedAssistant = true; |
| } |
| |
| enum NavBarGestureResult { |
| UNKNOWN, |
| HOME_GESTURE_COMPLETED, |
| OVERVIEW_GESTURE_COMPLETED, |
| HOME_NOT_STARTED_TOO_FAR_FROM_EDGE, |
| OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE, |
| HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, // Side swipe on nav bar. |
| HOME_OR_OVERVIEW_CANCELLED, |
| ASSISTANT_COMPLETED, |
| ASSISTANT_NOT_STARTED_BAD_ANGLE, |
| ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT, |
| } |
| |
| /** Callback to let the UI react to attempted nav bar gestures. */ |
| interface NavBarGestureAttemptCallback { |
| /** Called whenever any touch is completed. */ |
| void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity); |
| |
| /** Indicates how far a touch originating in the nav bar has moved from the nav bar. */ |
| default void setNavBarGestureProgress(@Nullable Float displacement) {} |
| |
| /** Indicates the progress of an Assistant gesture. */ |
| default void setAssistantProgress(float progress) {} |
| } |
| |
| private class AssistantGestureListener extends GestureDetector.SimpleOnGestureListener { |
| @Override |
| public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { |
| if (!mLaunchedAssistant && mTouchCameFromAssistantCorner) { |
| PointF velocity = new PointF(velocityX, velocityY); |
| if (!isValidAssistantGestureAngle(velocityX, -velocityY)) { |
| if (mGestureCallback != null) { |
| mGestureCallback.onNavBarGestureAttempted(ASSISTANT_NOT_STARTED_BAD_ANGLE, |
| velocity); |
| } |
| } else if (mAssistantDistance >= mAssistantFlingDistThreshold) { |
| mAssistantLastProgress = 1; |
| startAssistant(velocity); |
| } |
| } |
| return true; |
| } |
| } |
| } |