Loading quickstep/res/values/strings.xml +11 −0 Original line number Diff line number Diff line Loading @@ -134,6 +134,17 @@ <!-- Feedback shown during interactive parts of Overview gesture tutorial when the gesture is horizontal instead of vertical. [CHAR LIMIT=100] --> <string name="overview_gesture_feedback_wrong_swipe_direction" translatable="false">Make sure you swipe straight up and pause</string> <!-- Title shown during interactive part of Assistant gesture tutorial. [CHAR LIMIT=30] --> <string name="assistant_gesture_tutorial_playground_title" translatable="false">Tutorial: Assistant</string> <!-- Subtitle shown during interactive parts of Assistant gesture tutorial. [CHAR LIMIT=60] --> <string name="assistant_gesture_tutorial_playground_subtitle" translatable="false">Try swiping diagonally from a bottom corner of the screen</string> <!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture is started too far from the corner. [CHAR LIMIT=100] --> <string name="assistant_gesture_feedback_swipe_too_far_from_corner" translatable="false">Make sure you swipe from a bottom corner of the screen</string> <!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture doesn't go diagonally enough. [CHAR LIMIT=100] --> <string name="assistant_gesture_feedback_swipe_not_diagonal" translatable="false">Make sure you swipe diagonally</string> <!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture doesn't go far enough. [CHAR LIMIT=100] --> <string name="assistant_gesture_feedback_swipe_not_long_enough" translatable="false">Try swiping further</string> <!-- Title shown on the confirmation screen after successful gesture. [CHAR LIMIT=30] --> <string name="gesture_tutorial_confirm_title" translatable="false">All set</string> <!-- Button text shown on a button on the confirm screen to leave the tutorial. [CHAR LIMIT=14] --> Loading quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java 0 → 100644 +129 −0 Original line number Diff line number Diff line /* * 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.quickstep.interaction.TutorialController.TutorialType.ASSISTANT_COMPLETE; import android.graphics.PointF; import android.view.View; import com.android.launcher3.R; import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult; import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult; /** A {@link TutorialController} for the Assistant tutorial. */ final class AssistantGestureTutorialController extends TutorialController { AssistantGestureTutorialController(AssistantGestureTutorialFragment fragment, TutorialType tutorialType) { super(fragment, tutorialType); } @Override Integer getTitleStringId() { switch (mTutorialType) { case ASSISTANT: return R.string.assistant_gesture_tutorial_playground_title; case ASSISTANT_COMPLETE: return R.string.gesture_tutorial_confirm_title; } return null; } @Override Integer getSubtitleStringId() { if (mTutorialType == TutorialType.ASSISTANT) { return R.string.assistant_gesture_tutorial_playground_subtitle; } return null; } @Override Integer getActionButtonStringId() { if (mTutorialType == ASSISTANT_COMPLETE) { return R.string.gesture_tutorial_action_button_label_done; } return null; } @Override void onActionButtonClicked(View button) { mTutorialFragment.closeTutorial(); } @Override public void onBackGestureAttempted(BackGestureResult result) { switch (mTutorialType) { case ASSISTANT: switch (result) { case BACK_COMPLETED_FROM_LEFT: case BACK_COMPLETED_FROM_RIGHT: case BACK_CANCELLED_FROM_LEFT: case BACK_CANCELLED_FROM_RIGHT: showFeedback(R.string.assistant_gesture_feedback_swipe_too_far_from_corner); break; } break; case ASSISTANT_COMPLETE: if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) { mTutorialFragment.closeTutorial(); } break; } } @Override public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) { switch (mTutorialType) { case ASSISTANT: switch (result) { case HOME_GESTURE_COMPLETED: case OVERVIEW_GESTURE_COMPLETED: case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE: case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE: case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION: case HOME_OR_OVERVIEW_CANCELLED: showFeedback(R.string.assistant_gesture_feedback_swipe_too_far_from_corner); break; case ASSISTANT_COMPLETED: hideFeedback(); hideHandCoachingAnimation(); showRippleEffect( () -> mTutorialFragment.changeController(ASSISTANT_COMPLETE)); break; case ASSISTANT_NOT_STARTED_BAD_ANGLE: showFeedback(R.string.assistant_gesture_feedback_swipe_not_diagonal); break; case ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT: showFeedback(R.string.assistant_gesture_feedback_swipe_not_long_enough); break; } break; case ASSISTANT_COMPLETE: if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) { mTutorialFragment.closeTutorial(); } break; } } @Override public void setAssistantProgress(float progress) { // TODO: Create an animation. } } quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java 0 → 100644 +48 −0 Original line number Diff line number Diff line /* * 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 android.view.MotionEvent; import android.view.View; import com.android.launcher3.R; import com.android.quickstep.interaction.TutorialController.TutorialType; /** Shows the Home gesture interactive tutorial. */ public class AssistantGestureTutorialFragment extends TutorialFragment { @Override int getHandAnimationResId() { return R.drawable.assistant_gesture; } @Override TutorialController createController(TutorialType type) { return new AssistantGestureTutorialController(this, type); } @Override Class<? extends TutorialController> getControllerClass() { return AssistantGestureTutorialController.class; } @Override public boolean onTouch(View view, MotionEvent motionEvent) { if (motionEvent.getAction() == MotionEvent.ACTION_DOWN && mTutorialController != null) { mTutorialController.setRippleHotspot(motionEvent.getX(), motionEvent.getY()); } return super.onTouch(view, motionEvent); } } quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java +0 −5 Original line number Diff line number Diff line Loading @@ -21,8 +21,6 @@ import static com.android.quickstep.interaction.TutorialController.TutorialType. import android.graphics.PointF; import android.view.View; import androidx.annotation.Nullable; import com.android.launcher3.R; import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult; import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult; Loading Loading @@ -156,7 +154,4 @@ final class BackGestureTutorialController extends TutorialController { } } } @Override public void setNavBarGestureProgress(@Nullable Float displacement) {} } quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java +198 −26 Original line number Diff line number Diff line Loading @@ -15,6 +15,10 @@ */ package com.android.quickstep.interaction; import static com.android.launcher3.Utilities.squaredHypot; 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; Loading @@ -22,38 +26,69 @@ import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestu 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.NavBarPosition; import com.android.quickstep.util.TriggerSwipeUpTouchTracker; import com.android.systemui.shared.system.QuickStepContract; /** Utility class to handle home gestures. */ /** 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 int mBottomGestureHeight; 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 boolean mTouchCameFromAssistantCorner; private boolean mTouchCameFromNavBar; private float mDownY; 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) { final Display display = context.getDisplay(); mContext = context; final Display display = mContext.getDisplay(); final int displayRotation; if (display == null) { displayRotation = Surface.ROTATION_0; Loading @@ -61,7 +96,6 @@ public class NavBarGestureHandler implements OnTouchListener, displayRotation = display.getRotation(); display.getRealSize(mDisplaySize); } mDownY = mDisplaySize.y; mSwipeUpTouchTracker = new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/, new NavBarPosition(Mode.NO_BUTTON, displayRotation), Loading @@ -70,6 +104,27 @@ public class NavBarGestureHandler implements OnTouchListener, 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) { Loading @@ -82,7 +137,7 @@ public class NavBarGestureHandler implements OnTouchListener, @Override public void onSwipeUp(boolean wasFling, PointF finalVelocity) { if (mGestureCallback == null) { if (mGestureCallback == null || mAssistantGestureActive) { return; } finalVelocity.set(finalVelocity.x / 1000, finalVelocity.y / 1000); Loading @@ -98,36 +153,128 @@ public class NavBarGestureHandler implements OnTouchListener, @Override public void onSwipeUpCancelled() { if (mGestureCallback != null) { if (mGestureCallback != null && !mAssistantGestureActive) { mGestureCallback.onNavBarGestureAttempted(HOME_OR_OVERVIEW_CANCELLED, new PointF()); } } @Override public boolean onTouch(View view, MotionEvent motionEvent) { int action = motionEvent.getAction(); public boolean onTouch(View view, MotionEvent event) { int action = event.getAction(); boolean intercepted = mSwipeUpTouchTracker.interceptedTouch(); if (action == MotionEvent.ACTION_DOWN) { mDownY = motionEvent.getY(); mTouchCameFromNavBar = mDownY >= mDisplaySize.y - mBottomGestureHeight; if (!mTouchCameFromNavBar) { 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(); } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { break; case MotionEvent.ACTION_MOVE: if (!mAssistantGestureActive) { break; } mLastPos.set(event.getX(), event.getY()); 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: 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(motionEvent.getY() - mDownY); mGestureCallback.setNavBarGestureProgress(event.getY() - mDownPos.y); } mSwipeUpTouchTracker.onMotionEvent(motionEvent); mSwipeUpTouchTracker.onMotionEvent(event); mAssistantGestureDetector.onTouchEvent(event); return intercepted; } /** * 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, Loading @@ -135,7 +282,10 @@ public class NavBarGestureHandler implements OnTouchListener, 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 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. */ Loading @@ -144,6 +294,28 @@ public class NavBarGestureHandler implements OnTouchListener, void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity); /** Indicates how far a touch originating in the nav bar has moved from the nav bar. */ void setNavBarGestureProgress(@Nullable Float displacement); 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; } } } Loading
quickstep/res/values/strings.xml +11 −0 Original line number Diff line number Diff line Loading @@ -134,6 +134,17 @@ <!-- Feedback shown during interactive parts of Overview gesture tutorial when the gesture is horizontal instead of vertical. [CHAR LIMIT=100] --> <string name="overview_gesture_feedback_wrong_swipe_direction" translatable="false">Make sure you swipe straight up and pause</string> <!-- Title shown during interactive part of Assistant gesture tutorial. [CHAR LIMIT=30] --> <string name="assistant_gesture_tutorial_playground_title" translatable="false">Tutorial: Assistant</string> <!-- Subtitle shown during interactive parts of Assistant gesture tutorial. [CHAR LIMIT=60] --> <string name="assistant_gesture_tutorial_playground_subtitle" translatable="false">Try swiping diagonally from a bottom corner of the screen</string> <!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture is started too far from the corner. [CHAR LIMIT=100] --> <string name="assistant_gesture_feedback_swipe_too_far_from_corner" translatable="false">Make sure you swipe from a bottom corner of the screen</string> <!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture doesn't go diagonally enough. [CHAR LIMIT=100] --> <string name="assistant_gesture_feedback_swipe_not_diagonal" translatable="false">Make sure you swipe diagonally</string> <!-- Feedback shown during interactive parts of Assistant gesture tutorial when the gesture doesn't go far enough. [CHAR LIMIT=100] --> <string name="assistant_gesture_feedback_swipe_not_long_enough" translatable="false">Try swiping further</string> <!-- Title shown on the confirmation screen after successful gesture. [CHAR LIMIT=30] --> <string name="gesture_tutorial_confirm_title" translatable="false">All set</string> <!-- Button text shown on a button on the confirm screen to leave the tutorial. [CHAR LIMIT=14] --> Loading
quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java 0 → 100644 +129 −0 Original line number Diff line number Diff line /* * 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.quickstep.interaction.TutorialController.TutorialType.ASSISTANT_COMPLETE; import android.graphics.PointF; import android.view.View; import com.android.launcher3.R; import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult; import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult; /** A {@link TutorialController} for the Assistant tutorial. */ final class AssistantGestureTutorialController extends TutorialController { AssistantGestureTutorialController(AssistantGestureTutorialFragment fragment, TutorialType tutorialType) { super(fragment, tutorialType); } @Override Integer getTitleStringId() { switch (mTutorialType) { case ASSISTANT: return R.string.assistant_gesture_tutorial_playground_title; case ASSISTANT_COMPLETE: return R.string.gesture_tutorial_confirm_title; } return null; } @Override Integer getSubtitleStringId() { if (mTutorialType == TutorialType.ASSISTANT) { return R.string.assistant_gesture_tutorial_playground_subtitle; } return null; } @Override Integer getActionButtonStringId() { if (mTutorialType == ASSISTANT_COMPLETE) { return R.string.gesture_tutorial_action_button_label_done; } return null; } @Override void onActionButtonClicked(View button) { mTutorialFragment.closeTutorial(); } @Override public void onBackGestureAttempted(BackGestureResult result) { switch (mTutorialType) { case ASSISTANT: switch (result) { case BACK_COMPLETED_FROM_LEFT: case BACK_COMPLETED_FROM_RIGHT: case BACK_CANCELLED_FROM_LEFT: case BACK_CANCELLED_FROM_RIGHT: showFeedback(R.string.assistant_gesture_feedback_swipe_too_far_from_corner); break; } break; case ASSISTANT_COMPLETE: if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) { mTutorialFragment.closeTutorial(); } break; } } @Override public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) { switch (mTutorialType) { case ASSISTANT: switch (result) { case HOME_GESTURE_COMPLETED: case OVERVIEW_GESTURE_COMPLETED: case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE: case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE: case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION: case HOME_OR_OVERVIEW_CANCELLED: showFeedback(R.string.assistant_gesture_feedback_swipe_too_far_from_corner); break; case ASSISTANT_COMPLETED: hideFeedback(); hideHandCoachingAnimation(); showRippleEffect( () -> mTutorialFragment.changeController(ASSISTANT_COMPLETE)); break; case ASSISTANT_NOT_STARTED_BAD_ANGLE: showFeedback(R.string.assistant_gesture_feedback_swipe_not_diagonal); break; case ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT: showFeedback(R.string.assistant_gesture_feedback_swipe_not_long_enough); break; } break; case ASSISTANT_COMPLETE: if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) { mTutorialFragment.closeTutorial(); } break; } } @Override public void setAssistantProgress(float progress) { // TODO: Create an animation. } }
quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java 0 → 100644 +48 −0 Original line number Diff line number Diff line /* * 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 android.view.MotionEvent; import android.view.View; import com.android.launcher3.R; import com.android.quickstep.interaction.TutorialController.TutorialType; /** Shows the Home gesture interactive tutorial. */ public class AssistantGestureTutorialFragment extends TutorialFragment { @Override int getHandAnimationResId() { return R.drawable.assistant_gesture; } @Override TutorialController createController(TutorialType type) { return new AssistantGestureTutorialController(this, type); } @Override Class<? extends TutorialController> getControllerClass() { return AssistantGestureTutorialController.class; } @Override public boolean onTouch(View view, MotionEvent motionEvent) { if (motionEvent.getAction() == MotionEvent.ACTION_DOWN && mTutorialController != null) { mTutorialController.setRippleHotspot(motionEvent.getX(), motionEvent.getY()); } return super.onTouch(view, motionEvent); } }
quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java +0 −5 Original line number Diff line number Diff line Loading @@ -21,8 +21,6 @@ import static com.android.quickstep.interaction.TutorialController.TutorialType. import android.graphics.PointF; import android.view.View; import androidx.annotation.Nullable; import com.android.launcher3.R; import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult; import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult; Loading Loading @@ -156,7 +154,4 @@ final class BackGestureTutorialController extends TutorialController { } } } @Override public void setNavBarGestureProgress(@Nullable Float displacement) {} }
quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java +198 −26 Original line number Diff line number Diff line Loading @@ -15,6 +15,10 @@ */ package com.android.quickstep.interaction; import static com.android.launcher3.Utilities.squaredHypot; 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; Loading @@ -22,38 +26,69 @@ import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestu 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.NavBarPosition; import com.android.quickstep.util.TriggerSwipeUpTouchTracker; import com.android.systemui.shared.system.QuickStepContract; /** Utility class to handle home gestures. */ /** 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 int mBottomGestureHeight; 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 boolean mTouchCameFromAssistantCorner; private boolean mTouchCameFromNavBar; private float mDownY; 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) { final Display display = context.getDisplay(); mContext = context; final Display display = mContext.getDisplay(); final int displayRotation; if (display == null) { displayRotation = Surface.ROTATION_0; Loading @@ -61,7 +96,6 @@ public class NavBarGestureHandler implements OnTouchListener, displayRotation = display.getRotation(); display.getRealSize(mDisplaySize); } mDownY = mDisplaySize.y; mSwipeUpTouchTracker = new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/, new NavBarPosition(Mode.NO_BUTTON, displayRotation), Loading @@ -70,6 +104,27 @@ public class NavBarGestureHandler implements OnTouchListener, 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) { Loading @@ -82,7 +137,7 @@ public class NavBarGestureHandler implements OnTouchListener, @Override public void onSwipeUp(boolean wasFling, PointF finalVelocity) { if (mGestureCallback == null) { if (mGestureCallback == null || mAssistantGestureActive) { return; } finalVelocity.set(finalVelocity.x / 1000, finalVelocity.y / 1000); Loading @@ -98,36 +153,128 @@ public class NavBarGestureHandler implements OnTouchListener, @Override public void onSwipeUpCancelled() { if (mGestureCallback != null) { if (mGestureCallback != null && !mAssistantGestureActive) { mGestureCallback.onNavBarGestureAttempted(HOME_OR_OVERVIEW_CANCELLED, new PointF()); } } @Override public boolean onTouch(View view, MotionEvent motionEvent) { int action = motionEvent.getAction(); public boolean onTouch(View view, MotionEvent event) { int action = event.getAction(); boolean intercepted = mSwipeUpTouchTracker.interceptedTouch(); if (action == MotionEvent.ACTION_DOWN) { mDownY = motionEvent.getY(); mTouchCameFromNavBar = mDownY >= mDisplaySize.y - mBottomGestureHeight; if (!mTouchCameFromNavBar) { 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(); } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { break; case MotionEvent.ACTION_MOVE: if (!mAssistantGestureActive) { break; } mLastPos.set(event.getX(), event.getY()); 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: 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(motionEvent.getY() - mDownY); mGestureCallback.setNavBarGestureProgress(event.getY() - mDownPos.y); } mSwipeUpTouchTracker.onMotionEvent(motionEvent); mSwipeUpTouchTracker.onMotionEvent(event); mAssistantGestureDetector.onTouchEvent(event); return intercepted; } /** * 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, Loading @@ -135,7 +282,10 @@ public class NavBarGestureHandler implements OnTouchListener, 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 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. */ Loading @@ -144,6 +294,28 @@ public class NavBarGestureHandler implements OnTouchListener, void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity); /** Indicates how far a touch originating in the nav bar has moved from the nav bar. */ void setNavBarGestureProgress(@Nullable Float displacement); 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; } } }