Redesign the gesture tutorial for the Back gesture

This change adds in a new animation and layout to change the existing back tutorial as part of the effort to redesign gesture navigation education for users. This temporarily uses placeholder animations for the overview gesture. Large screen animations are also added for Home and Back tutorial.

Flag: ENABLE_NEW_GESTURE_NAV_TUTORIAL
Bug: 241813570
Bug: 253521922
Bug: 253520701
Test: Manual
Change-Id: Ied18b88a83a3b673a7cf40fd33b6013f24998e44
This commit is contained in:
Saumya Prakash 2023-03-03 23:03:51 +00:00
parent f22345eace
commit fd79b4dfbd
18 changed files with 268 additions and 43 deletions

View File

@ -103,6 +103,7 @@
<activity android:name="com.android.quickstep.interaction.GestureSandboxActivity"
android:autoRemoveFromRecents="true"
android:excludeFromRecents="true"
android:theme="@style/GestureTutorialActivity"
android:exported="true">
<intent-filter>
<action android:name="com.android.quickstep.action.GESTURE_SANDBOX"/>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2023 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.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingVertical="26dp"
android:paddingHorizontal="56dp">
<View
android:id="@+id/hotseat_icon_1"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon_home"
android:clipToOutline="true"
app:layout_constraintBottom_toTopOf="@id/hotseat_icon_2"
app:layout_constraintVertical_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/hotseat_icon_2"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon_home"
android:clipToOutline="true"
app:layout_constraintBottom_toTopOf="@id/hotseat_icon_3"
app:layout_constraintTop_toBottomOf="@id/hotseat_icon_1"
app:layout_constraintStart_toStartOf="parent" />
<View
android:id="@+id/hotseat_icon_3"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon_home"
android:clipToOutline="true"
app:layout_constraintBottom_toTopOf="@id/hotseat_icon_4"
app:layout_constraintTop_toBottomOf="@id/hotseat_icon_2"
app:layout_constraintStart_toStartOf="parent" />
<View
android:id="@+id/hotseat_icon_4"
android:layout_width="@dimen/gesture_tutorial_hotseat_icon_size"
android:layout_height="@dimen/gesture_tutorial_hotseat_icon_size"
android:background="@drawable/hotseat_icon_home"
android:clipToOutline="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/hotseat_icon_3"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2023 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.
-->
<View
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/back_gesture_tutorial_background"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@ -118,6 +118,14 @@
android:scaleType="fitXY"
android:visibility="gone" />
<View
android:id="@+id/exiting_app_back"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:background="@color/gesture_back_tutorial_exiting_app"
android:visibility="gone" />
<RelativeLayout
android:id="@+id/full_gesture_demonstration"
android:layout_width="match_parent"
@ -193,43 +201,33 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_fragment_feedback_subtitle" />
<ImageView
android:id="@+id/gesture_tutorial_checkbox_bg"
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/checkmark_animation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
android:layout_marginTop="100dp"
android:background="@drawable/gesture_tutorial_complete_checkmark_bg"
android:gravity="center"
android:scaleType="centerCrop"
app:lottie_loop="false"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/gesture_tutorial_fragment_action_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_fragment_feedback_subtitle" />
<ImageView
android:id="@+id/gesture_tutorial_checkbox"
android:layout_width="124dp"
android:layout_height="124dp"
android:background="@drawable/gesture_tutorial_complete_checkmark"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/gesture_tutorial_checkbox_bg"
app:layout_constraintEnd_toEndOf="@id/gesture_tutorial_checkbox_bg"
app:layout_constraintStart_toStartOf="@id/gesture_tutorial_checkbox_bg"
app:layout_constraintTop_toTopOf="@id/gesture_tutorial_checkbox_bg" />
<Button
android:id="@+id/gesture_tutorial_fragment_action_button"
style="@style/TextAppearance.GestureTutorial.ButtonLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:background="@drawable/gesture_tutorial_action_button_background"
android:stateListAnimator="@null"
android:text="@string/gesture_tutorial_action_button_label"
android:visibility="gone"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/gesture_tutorial_checkbox_bg" />
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/checkmark_animation" />
</androidx.constraintlayout.widget.ConstraintLayout>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -45,8 +45,8 @@
<!-- Redesigned gesture navigation tutorial -->
<color name="gesture_home_tutorial_background">#FFB399</color>
<color name="gesture_home_tutorial_swipe_up_rect">#3857C7</color>
<color name="gesture_back_tutorial_background">#F3A5B9</color>
<color name="gesture_back_tutorial_swipe_rect">#217500</color>
<color name="gesture_back_tutorial_exiting_app">#F3A5B9</color>
<color name="gesture_back_tutorial_background">#3857C7</color>
<color name="gesture_overview_tutorial_swipe_rect">#7E44AD</color>
<!-- Mock hotseat -->

View File

@ -121,6 +121,8 @@
<dimen name="gesture_tutorial_multi_row_task_view_spacing">72dp</dimen>
<dimen name="gesture_tutorial_small_task_view_corner_radius">18dp</dimen>
<dimen name="gesture_tutorial_mock_taskbar_height">80dp</dimen>
<dimen name="gesture_tutorial_back_gesture_exiting_app_margin">8dp</dimen>
<dimen name="gesture_tutorial_back_gesture_end_corner_radius">36dp</dimen>
<!-- Gesture Tutorial mock conversations -->
<dimen name="gesture_tutorial_message_icon_size">44dp</dimen>

View File

@ -120,6 +120,8 @@
<string name="back_gesture_intro_subtitle">To go back to the last screen, swipe from the left or right edge to the middle of the screen.</string>
<!-- Introduction subtitle for the Back gesture tutorial that will be spoken by screen readers. [CHAR LIMIT=200] -->
<string name="back_gesture_spoken_intro_subtitle">To go back to the last screen, swipe with 2 fingers from the left or right edge to the middle of the screen.</string>
<!-- Title of the gesture tutorial section educating users on how to go back to the previous screen. [CHAR LIMIT=100] -->
<string name="back_gesture_tutorial_title">Go back</string>
<string name="home_gesture_feedback_swipe_too_far_from_edge">Make sure you swipe up from the bottom edge of the screen.</string>
<!-- Feedback shown during interactive parts of Home gesture tutorial when the Overview gesture is detected. [CHAR LIMIT=100] -->

View File

@ -230,4 +230,9 @@
<item name="android:textColor">?android:attr/textColorPrimary</item>
<item name="lineHeight">20sp</item>
</style>
<style name="GestureTutorialActivity"
parent="@style/AppTheme">
<item name="background">@android:color/transparent</item>
</style>
</resources>

View File

@ -15,18 +15,24 @@
*/
package com.android.quickstep.interaction;
import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION;
import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION_COMPLETE;
import android.annotation.LayoutRes;
import android.graphics.PointF;
import android.view.View;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.Interpolators;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
/** A {@link TutorialController} for the Back tutorial. */
final class BackGestureTutorialController extends TutorialController {
private static final float Y_TRANSLATION_SMOOTHENING_FACTOR = .2f;
private static final float EXITING_APP_MIN_SIZE_PERCENTAGE = .8f;
BackGestureTutorialController(BackGestureTutorialFragment fragment, TutorialType tutorialType) {
super(fragment, tutorialType);
@ -34,7 +40,9 @@ final class BackGestureTutorialController extends TutorialController {
@Override
public int getIntroductionTitle() {
return R.string.back_gesture_intro_title;
return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
? R.string.back_gesture_tutorial_title
: R.string.back_gesture_intro_title;
}
@Override
@ -61,27 +69,32 @@ final class BackGestureTutorialController extends TutorialController {
@Override
protected int getGestureLottieAnimationId() {
// TODO(b/253521922): Change to correct LottieAnimationView
return R.raw.home_gesture_tutorial_animation;
return mTutorialFragment.isLargeScreen()
? R.raw.back_gesture_tutorial_tablet_animation
: R.raw.back_gesture_tutorial_animation;
}
@LayoutRes
int getMockAppTaskCurrentPageLayoutResId() {
return mTutorialFragment.isLargeScreen()
? R.layout.gesture_tutorial_tablet_mock_conversation
: R.layout.gesture_tutorial_mock_conversation;
return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
? R.layout.back_gesture_tutorial_background
: mTutorialFragment.isLargeScreen()
? R.layout.gesture_tutorial_tablet_mock_conversation
: R.layout.gesture_tutorial_mock_conversation;
}
@LayoutRes
int getMockAppTaskPreviousPageLayoutResId() {
return mTutorialFragment.isLargeScreen()
? R.layout.gesture_tutorial_tablet_mock_conversation_list
: R.layout.gesture_tutorial_mock_conversation_list;
return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
? R.layout.back_gesture_tutorial_background
: mTutorialFragment.isLargeScreen()
? R.layout.gesture_tutorial_tablet_mock_conversation_list
: R.layout.gesture_tutorial_mock_conversation_list;
}
@Override
protected int getSwipeActionColorResId() {
return R.color.gesture_back_tutorial_swipe_rect;
return R.color.gesture_back_tutorial_background;
}
@Override
@ -102,11 +115,59 @@ final class BackGestureTutorialController extends TutorialController {
}
}
@Override
public void onBackGestureProgress(float diffx, float diffy, boolean isLeftGesture) {
if (isGestureCompleted()) {
return;
}
float normalizedSwipeProgress = Math.abs(diffx / mScreenWidth);
float smoothedExitingAppScale = Utilities.mapBoundToRange(
normalizedSwipeProgress,
/* lowerBound = */ 0f,
/* upperBound = */ 1f,
/* toMin = */ 1f,
/* toMax = */ EXITING_APP_MIN_SIZE_PERCENTAGE,
Interpolators.DEACCEL);
// shrink the exiting app as we progress through the back gesture
mExitingAppView.setPivotX(isLeftGesture ? mScreenWidth : 0);
mExitingAppView.setPivotY(mScreenHeight / 2f);
mExitingAppView.setScaleX(smoothedExitingAppScale);
mExitingAppView.setScaleY(smoothedExitingAppScale);
mExitingAppView.setTranslationY(diffy * Y_TRANSLATION_SMOOTHENING_FACTOR);
mExitingAppView.setTranslationX(Utilities.mapBoundToRange(
normalizedSwipeProgress,
/* lowerBound = */ 0f,
/* upperBound = */ 1f,
/* toMin = */ 0,
/* toMax = */ mExitingAppMargin,
Interpolators.DEACCEL)
* (isLeftGesture ? -1 : 1));
// round the corners of the exiting app as we progress through the back gesture
mExitingAppRadius = (int) Utilities.mapBoundToRange(
normalizedSwipeProgress,
/* lowerBound = */ 0f,
/* upperBound = */ 1f,
/* toMin = */ mExitingAppStartingCornerRadius,
/* toMax = */ mExitingAppEndingCornerRadius,
Interpolators.EMPHASIZED_DECELERATE);
mExitingAppView.invalidateOutline();
}
private void handleBackAttempt(BackGestureResult result) {
if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
resetViewsForBackGesture();
}
switch (result) {
case BACK_COMPLETED_FROM_LEFT:
case BACK_COMPLETED_FROM_RIGHT:
mTutorialFragment.releaseFeedbackAnimation();
if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
mExitingAppView.setVisibility(View.GONE);
}
updateFakeAppTaskViewLayout(getMockAppTaskPreviousPageLayoutResId());
showSuccessFeedback();
break;

View File

@ -15,6 +15,8 @@
*/
package com.android.quickstep.interaction;
import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
@ -29,8 +31,8 @@ import android.view.ViewGroup.LayoutParams;
import androidx.annotation.Nullable;
import com.android.launcher3.testing.shared.ResourceUtils;
import com.android.launcher3.Utilities;
import com.android.launcher3.testing.shared.ResourceUtils;
import com.android.launcher3.util.DisplayController;
/**
@ -207,7 +209,11 @@ public class EdgeBackGestureHandler implements OnTouchListener {
mThresholdCrossed = true;
}
}
}
if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
mGestureCallback.onBackGestureProgress(ev.getX() - mDownPoint.x,
ev.getY() - mDownPoint.y, mEdgeBackPanel.getIsLeftPanel());
}
// forward touch
@ -242,5 +248,8 @@ public class EdgeBackGestureHandler implements OnTouchListener {
interface BackGestureAttemptCallback {
/** Called whenever any touch is completed. */
void onBackGestureAttempted(BackGestureResult result);
/** Called when the back gesture is recognized and is in progress. */
default void onBackGestureProgress(float diffx, float diffy, boolean isLeftGesture) {}
}
}

View File

@ -70,7 +70,9 @@ final class HomeGestureTutorialController extends SwipeUpGestureTutorialControll
@Override
protected int getGestureLottieAnimationId() {
return R.raw.home_gesture_tutorial_animation;
return mTutorialFragment.isLargeScreen()
? R.raw.home_gesture_tutorial_tablet_animation
: R.raw.home_gesture_tutorial_animation;
}
@Override

View File

@ -30,12 +30,15 @@ import android.annotation.ColorRes;
import android.annotation.RawRes;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.RippleDrawable;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.accessibility.AccessibilityEvent;
import android.widget.Button;
import android.widget.FrameLayout;
@ -59,6 +62,7 @@ import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.views.ClipIconView;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback;
import com.android.systemui.shared.system.QuickStepContract;
import com.airbnb.lottie.LottieAnimationView;
@ -81,6 +85,11 @@ abstract class TutorialController implements BackGestureAttemptCallback,
private static final int GESTURE_ANIMATION_DELAY_MS = 1500;
private static final int ADVANCE_TUTORIAL_TIMEOUT_MS = 2000;
private static final long GESTURE_ANIMATION_PAUSE_DURATION_MILLIS = 1000;
protected float mExitingAppEndingCornerRadius;
protected float mExitingAppStartingCornerRadius;
protected int mScreenHeight;
protected float mScreenWidth;
protected float mExitingAppMargin;
final TutorialFragment mTutorialFragment;
TutorialType mTutorialType;
@ -103,10 +112,14 @@ abstract class TutorialController implements BackGestureAttemptCallback,
final RippleDrawable mRippleDrawable;
final TutorialStepIndicator mTutorialStepView;
final ImageView mFingerDotView;
private final Rect mExitingAppRect = new Rect();
protected View mExitingAppView;
protected int mExitingAppRadius;
private final AlertDialog mSkipTutorialDialog;
private boolean mGestureCompleted = false;
private LottieAnimationView mAnimatedGestureDemonstration;
private LottieAnimationView mCheckmarkAnimation;
private RelativeLayout mFullGestureDemonstration;
// These runnables should be used when posting callbacks to their views and cleared from their
@ -147,13 +160,28 @@ abstract class TutorialController implements BackGestureAttemptCallback,
mSkipTutorialDialog = createSkipTutorialDialog();
if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
mAnimatedGestureDemonstration = mTutorialFragment.getRootView().findViewById(
mFullGestureDemonstration = rootView.findViewById(R.id.full_gesture_demonstration);
mCheckmarkAnimation = rootView.findViewById(R.id.checkmark_animation);
mAnimatedGestureDemonstration = rootView.findViewById(
R.id.gesture_demonstration_animations);
mFullGestureDemonstration = mTutorialFragment.getRootView().findViewById(
R.id.full_gesture_demonstration);
mExitingAppView = rootView.findViewById(R.id.exiting_app_back);
mScreenWidth = mTutorialFragment.getDeviceProfile().widthPx;
mScreenHeight = mTutorialFragment.getDeviceProfile().heightPx;
mExitingAppMargin = mContext.getResources().getDimensionPixelSize(
R.dimen.gesture_tutorial_back_gesture_exiting_app_margin);
mExitingAppStartingCornerRadius = QuickStepContract.getWindowCornerRadius(mContext);
mExitingAppEndingCornerRadius = mContext.getResources().getDimensionPixelSize(
R.dimen.gesture_tutorial_back_gesture_end_corner_radius);
mFeedbackTitleView.setText(getIntroductionTitle());
mFeedbackSubtitleView.setText(getIntroductionSubtitle());
mExitingAppView.setClipToOutline(true);
mExitingAppView.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(mExitingAppRect, mExitingAppRadius);
}
});
}
mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent(
@ -373,10 +401,8 @@ abstract class TutorialController implements BackGestureAttemptCallback,
}
private void showSuccessPage() {
mFeedbackView.findViewById(
R.id.gesture_tutorial_checkbox_bg).setVisibility(View.VISIBLE);
mFeedbackView.findViewById(
R.id.gesture_tutorial_checkbox).setVisibility(View.VISIBLE);
mCheckmarkAnimation.setVisibility(View.VISIBLE);
mCheckmarkAnimation.playAnimation();
mFeedbackTitleView.setTextAppearance(R.style.TextAppearance_GestureTutorial_SuccessTitle);
mFeedbackSubtitleView.setTextAppearance(
R.style.TextAppearance_GestureTutorial_SuccessSubtitle);
@ -496,7 +522,17 @@ abstract class TutorialController implements BackGestureAttemptCallback,
updateLayout();
if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
startGestureAnimation();
mCheckmarkAnimation.setAnimation(mTutorialFragment.isAtFinalStep()
? R.raw.checkmark_animation_end
: R.raw.checkmark_animation_in_progress);
if (!isGestureCompleted()) {
mCheckmarkAnimation.setVisibility(GONE);
startGestureAnimation();
if (mTutorialType == TutorialType.BACK_NAVIGATION) {
resetViewsForBackGesture();
}
}
} else {
hideFeedback();
hideActionButton();
@ -508,6 +544,23 @@ abstract class TutorialController implements BackGestureAttemptCallback,
}
}
protected void resetViewsForBackGesture() {
mFakeTaskView.setVisibility(View.VISIBLE);
mFakeTaskView.setBackgroundColor(
mContext.getColor(R.color.gesture_back_tutorial_background));
mExitingAppView.setVisibility(View.VISIBLE);
// reset the exiting app's dimensions
mExitingAppRect.set(0, 0, (int) mScreenWidth, (int) mScreenHeight);
mExitingAppRadius = 0;
mExitingAppView.resetPivot();
mExitingAppView.setScaleX(1f);
mExitingAppView.setScaleY(1f);
mExitingAppView.setTranslationX(0);
mExitingAppView.setTranslationY(0);
mExitingAppView.invalidateOutline();
}
private void startGestureAnimation() {
mAnimatedGestureDemonstration.setAnimation(getGestureLottieAnimationId());
mAnimatedGestureDemonstration.playAnimation();

View File

@ -312,7 +312,6 @@ abstract class TutorialFragment extends Fragment implements OnTouchListener {
if (mEdgeAnimation != null && mEdgeAnimation.isRunning()) {
mEdgeAnimation.stop();
}
mEdgeGestureVideoView.setVisibility(View.GONE);
}