1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.quickstep.interaction; 17 18 import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; 19 import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION; 20 import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION_COMPLETE; 21 22 import android.annotation.LayoutRes; 23 import android.graphics.PointF; 24 import android.view.View; 25 26 import com.android.app.animation.Interpolators; 27 import com.android.launcher3.R; 28 import com.android.launcher3.Utilities; 29 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult; 30 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult; 31 import com.android.quickstep.util.LottieAnimationColorUtils; 32 33 import java.util.Map; 34 35 /** A {@link TutorialController} for the Back tutorial. */ 36 final class BackGestureTutorialController extends TutorialController { 37 private static final float Y_TRANSLATION_SMOOTHENING_FACTOR = .2f; 38 private static final float EXITING_APP_MIN_SIZE_PERCENTAGE = .8f; 39 BackGestureTutorialController(BackGestureTutorialFragment fragment, TutorialType tutorialType)40 BackGestureTutorialController(BackGestureTutorialFragment fragment, TutorialType tutorialType) { 41 super(fragment, tutorialType); 42 // Set the Lottie animation colors specifically for the Back gesture 43 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 44 LottieAnimationColorUtils.updateToArgbColors( 45 mAnimatedGestureDemonstration, 46 Map.of(".onSurfaceBack", fragment.mRootView.mColorOnSurfaceBack, 47 ".surfaceBack", fragment.mRootView.mColorSurfaceBack, 48 ".secondaryBack", fragment.mRootView.mColorSecondaryBack)); 49 50 LottieAnimationColorUtils.updateToArgbColors( 51 mCheckmarkAnimation, 52 Map.of(".checkmark", 53 Utilities.isDarkTheme(mContext) 54 ? fragment.mRootView.mColorOnSurfaceBack 55 : fragment.mRootView.mColorSecondaryBack, 56 ".checkmarkBackground", fragment.mRootView.mColorSurfaceBack)); 57 } 58 } 59 60 @Override getIntroductionTitle()61 public int getIntroductionTitle() { 62 return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() 63 ? R.string.back_gesture_tutorial_title 64 : R.string.back_gesture_intro_title; 65 } 66 67 @Override getIntroductionSubtitle()68 public int getIntroductionSubtitle() { 69 return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() 70 ? R.string.back_gesture_tutorial_subtitle 71 : R.string.back_gesture_intro_subtitle; 72 } 73 74 @Override getSpokenIntroductionSubtitle()75 public int getSpokenIntroductionSubtitle() { 76 return R.string.back_gesture_spoken_intro_subtitle; 77 } 78 79 @Override getSuccessFeedbackTitle()80 public int getSuccessFeedbackTitle() { 81 return R.string.gesture_tutorial_nice; 82 } 83 84 @Override getSuccessFeedbackSubtitle()85 public int getSuccessFeedbackSubtitle() { 86 return mTutorialFragment.isAtFinalStep() 87 ? R.string.back_gesture_feedback_complete_without_follow_up 88 : ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() 89 ? R.string.back_gesture_feedback_complete_with_follow_up 90 : R.string.back_gesture_feedback_complete_with_overview_follow_up; 91 } 92 93 @Override getTitleTextAppearance()94 public int getTitleTextAppearance() { 95 return R.style.TextAppearance_GestureTutorial_MainTitle_Back; 96 } 97 98 @Override getSuccessTitleTextAppearance()99 public int getSuccessTitleTextAppearance() { 100 return R.style.TextAppearance_GestureTutorial_MainTitle_Success_Back; 101 } 102 103 @Override getDoneButtonTextAppearance()104 public int getDoneButtonTextAppearance() { 105 return R.style.TextAppearance_GestureTutorial_ButtonLabel_Back; 106 } 107 108 @Override getDoneButtonColor()109 public int getDoneButtonColor() { 110 return Utilities.isDarkTheme(mContext) 111 ? mTutorialFragment.mRootView.mColorOnSurfaceBack 112 : mTutorialFragment.mRootView.mColorSecondaryBack; 113 } 114 115 @Override getMockAppTaskLayoutResId()116 protected int getMockAppTaskLayoutResId() { 117 return getMockAppTaskCurrentPageLayoutResId(); 118 } 119 120 @Override getGestureLottieAnimationId()121 protected int getGestureLottieAnimationId() { 122 return mTutorialFragment.isLargeScreen() 123 ? mTutorialFragment.isFoldable() 124 ? R.raw.back_gesture_tutorial_open_foldable_animation 125 : R.raw.back_gesture_tutorial_tablet_animation 126 : R.raw.back_gesture_tutorial_animation; 127 } 128 129 @LayoutRes getMockAppTaskCurrentPageLayoutResId()130 int getMockAppTaskCurrentPageLayoutResId() { 131 return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() 132 ? R.layout.back_gesture_tutorial_background 133 : mTutorialFragment.isLargeScreen() 134 ? R.layout.gesture_tutorial_tablet_mock_conversation 135 : R.layout.gesture_tutorial_mock_conversation; 136 } 137 138 @LayoutRes getMockAppTaskPreviousPageLayoutResId()139 int getMockAppTaskPreviousPageLayoutResId() { 140 return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() 141 ? R.layout.back_gesture_tutorial_background 142 : mTutorialFragment.isLargeScreen() 143 ? R.layout.gesture_tutorial_tablet_mock_conversation_list 144 : R.layout.gesture_tutorial_mock_conversation_list; 145 } 146 147 @Override getFakeLauncherColor()148 protected int getFakeLauncherColor() { 149 return mTutorialFragment.mRootView.mColorSurfaceContainer; 150 } 151 152 @Override getExitingAppColor()153 protected int getExitingAppColor() { 154 return mTutorialFragment.mRootView.mColorSurfaceBack; 155 } 156 157 @Override onBackGestureAttempted(BackGestureResult result)158 public void onBackGestureAttempted(BackGestureResult result) { 159 if (isGestureCompleted()) { 160 return; 161 } 162 switch (mTutorialType) { 163 case BACK_NAVIGATION: 164 handleBackAttempt(result); 165 break; 166 case BACK_NAVIGATION_COMPLETE: 167 if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT 168 || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) { 169 mTutorialFragment.close(); 170 } 171 break; 172 } 173 } 174 175 @Override onBackGestureProgress(float diffx, float diffy, boolean isLeftGesture)176 public void onBackGestureProgress(float diffx, float diffy, boolean isLeftGesture) { 177 if (isGestureCompleted()) { 178 return; 179 } 180 181 float normalizedSwipeProgress = Math.abs(diffx / mScreenWidth); 182 float smoothedExitingAppScale = Utilities.mapBoundToRange( 183 normalizedSwipeProgress, 184 /* lowerBound = */ 0f, 185 /* upperBound = */ 1f, 186 /* toMin = */ 1f, 187 /* toMax = */ EXITING_APP_MIN_SIZE_PERCENTAGE, 188 Interpolators.DECELERATE); 189 190 // shrink the exiting app as we progress through the back gesture 191 mExitingAppView.setPivotX(isLeftGesture ? mScreenWidth : 0); 192 mExitingAppView.setPivotY(mScreenHeight / 2f); 193 mExitingAppView.setScaleX(smoothedExitingAppScale); 194 mExitingAppView.setScaleY(smoothedExitingAppScale); 195 mExitingAppView.setTranslationY(diffy * Y_TRANSLATION_SMOOTHENING_FACTOR); 196 mExitingAppView.setTranslationX(Utilities.mapBoundToRange( 197 normalizedSwipeProgress, 198 /* lowerBound = */ 0f, 199 /* upperBound = */ 1f, 200 /* toMin = */ 0, 201 /* toMax = */ mExitingAppMargin, 202 Interpolators.DECELERATE) 203 * (isLeftGesture ? -1 : 1)); 204 205 // round the corners of the exiting app as we progress through the back gesture 206 mExitingAppRadius = (int) Utilities.mapBoundToRange( 207 normalizedSwipeProgress, 208 /* lowerBound = */ 0f, 209 /* upperBound = */ 1f, 210 /* toMin = */ mExitingAppStartingCornerRadius, 211 /* toMax = */ mExitingAppEndingCornerRadius, 212 Interpolators.EMPHASIZED_DECELERATE); 213 mExitingAppView.invalidateOutline(); 214 } 215 handleBackAttempt(BackGestureResult result)216 private void handleBackAttempt(BackGestureResult result) { 217 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 218 resetViewsForBackGesture(); 219 } 220 221 switch (result) { 222 case BACK_COMPLETED_FROM_LEFT: 223 case BACK_COMPLETED_FROM_RIGHT: 224 mTutorialFragment.releaseFeedbackAnimation(); 225 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 226 mExitingAppView.setVisibility(View.GONE); 227 } 228 updateFakeAppTaskViewLayout(getMockAppTaskPreviousPageLayoutResId()); 229 showSuccessFeedback(); 230 break; 231 case BACK_CANCELLED_FROM_LEFT: 232 case BACK_CANCELLED_FROM_RIGHT: 233 showFeedback(R.string.back_gesture_feedback_cancelled); 234 break; 235 case BACK_NOT_STARTED_TOO_FAR_FROM_EDGE: 236 showFeedback(R.string.back_gesture_feedback_swipe_too_far_from_edge); 237 break; 238 case BACK_NOT_STARTED_IN_NAV_BAR_REGION: 239 showFeedback(R.string.back_gesture_feedback_swipe_in_nav_bar); 240 break; 241 } 242 } 243 244 @Override onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity)245 public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) { 246 if (isGestureCompleted()) { 247 return; 248 } 249 if (mTutorialType == BACK_NAVIGATION_COMPLETE) { 250 if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) { 251 mTutorialFragment.close(); 252 } 253 } else if (mTutorialType == BACK_NAVIGATION) { 254 switch (result) { 255 case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE: 256 case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE: 257 case HOME_OR_OVERVIEW_CANCELLED: 258 showFeedback(R.string.back_gesture_feedback_swipe_too_far_from_edge); 259 break; 260 case HOME_GESTURE_COMPLETED: 261 case OVERVIEW_GESTURE_COMPLETED: 262 case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION: 263 default: 264 showFeedback(R.string.back_gesture_feedback_swipe_in_nav_bar); 265 266 } 267 } 268 } 269 } 270