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 android.view.View.GONE; 19 import static android.view.View.NO_ID; 20 import static android.view.View.inflate; 21 22 import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; 23 24 import android.animation.Animator; 25 import android.animation.AnimatorListenerAdapter; 26 import android.animation.AnimatorSet; 27 import android.animation.ObjectAnimator; 28 import android.animation.ValueAnimator; 29 import android.annotation.RawRes; 30 import android.content.Context; 31 import android.content.pm.PackageManager; 32 import android.graphics.Color; 33 import android.graphics.Matrix; 34 import android.graphics.Outline; 35 import android.graphics.Rect; 36 import android.graphics.drawable.AnimatedVectorDrawable; 37 import android.graphics.drawable.ColorDrawable; 38 import android.graphics.drawable.RippleDrawable; 39 import android.util.Log; 40 import android.view.View; 41 import android.view.ViewGroup; 42 import android.view.ViewOutlineProvider; 43 import android.view.accessibility.AccessibilityEvent; 44 import android.view.accessibility.AccessibilityManager; 45 import android.widget.Button; 46 import android.widget.FrameLayout; 47 import android.widget.ImageView; 48 import android.widget.RelativeLayout; 49 import android.widget.TextView; 50 51 import androidx.annotation.CallSuper; 52 import androidx.annotation.ColorInt; 53 import androidx.annotation.DrawableRes; 54 import androidx.annotation.LayoutRes; 55 import androidx.annotation.NonNull; 56 import androidx.annotation.Nullable; 57 import androidx.annotation.StringRes; 58 import androidx.annotation.StyleRes; 59 import androidx.appcompat.app.AlertDialog; 60 import androidx.appcompat.content.res.AppCompatResources; 61 62 import com.android.launcher3.DeviceProfile; 63 import com.android.launcher3.R; 64 import com.android.launcher3.Utilities; 65 import com.android.launcher3.anim.AnimatorListeners; 66 import com.android.launcher3.views.ClipIconView; 67 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback; 68 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback; 69 import com.android.systemui.shared.system.QuickStepContract; 70 71 import com.airbnb.lottie.LottieAnimationView; 72 import com.airbnb.lottie.LottieComposition; 73 74 import java.util.ArrayList; 75 76 abstract class TutorialController implements BackGestureAttemptCallback, 77 NavBarGestureAttemptCallback { 78 79 private static final String LOG_TAG = "TutorialController"; 80 81 private static final float FINGER_DOT_VISIBLE_ALPHA = 0.7f; 82 private static final float FINGER_DOT_SMALL_SCALE = 0.7f; 83 private static final int FINGER_DOT_ANIMATION_DURATION_MILLIS = 500; 84 85 private static final String PIXEL_TIPS_APP_PACKAGE_NAME = "com.google.android.apps.tips"; 86 private static final CharSequence DEFAULT_PIXEL_TIPS_APP_NAME = "Pixel Tips"; 87 88 private static final int FEEDBACK_ANIMATION_MS = 133; 89 private static final int RIPPLE_VISIBLE_MS = 300; 90 private static final int GESTURE_ANIMATION_DELAY_MS = 1500; 91 private static final int ADVANCE_TUTORIAL_TIMEOUT_MS = 3000; 92 private static final long GESTURE_ANIMATION_PAUSE_DURATION_MILLIS = 1000; 93 protected float mExitingAppEndingCornerRadius; 94 protected float mExitingAppStartingCornerRadius; 95 protected int mScreenHeight; 96 protected float mScreenWidth; 97 protected float mExitingAppMargin; 98 99 final TutorialFragment mTutorialFragment; 100 TutorialType mTutorialType; 101 final Context mContext; 102 103 final TextView mSkipButton; 104 final Button mDoneButton; 105 final ViewGroup mFeedbackView; 106 final TextView mFeedbackTitleView; 107 final TextView mFeedbackSubtitleView; 108 final ImageView mEdgeGestureVideoView; 109 final RelativeLayout mFakeLauncherView; 110 final FrameLayout mFakeHotseatView; 111 @Nullable View mHotseatIconView; 112 final ClipIconView mFakeIconView; 113 final FrameLayout mFakeTaskView; 114 @Nullable final AnimatedTaskbarView mFakeTaskbarView; 115 final AnimatedTaskView mFakePreviousTaskView; 116 final View mRippleView; 117 final RippleDrawable mRippleDrawable; 118 final TutorialStepIndicator mTutorialStepView; 119 final ImageView mFingerDotView; 120 private final Rect mExitingAppRect = new Rect(); 121 protected View mExitingAppView; 122 protected int mExitingAppRadius; 123 private final AlertDialog mSkipTutorialDialog; 124 125 private boolean mGestureCompleted = false; 126 protected LottieAnimationView mAnimatedGestureDemonstration; 127 protected LottieAnimationView mCheckmarkAnimation; 128 private RelativeLayout mFullGestureDemonstration; 129 130 // These runnables should be used when posting callbacks to their views and cleared from their 131 // views before posting new callbacks. 132 private final Runnable mTitleViewCallback; 133 @Nullable private Runnable mFeedbackViewCallback; 134 @Nullable private Runnable mFakeTaskViewCallback; 135 @Nullable private Runnable mFakeTaskbarViewCallback; 136 private final Runnable mShowFeedbackRunnable; 137 TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType)138 TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) { 139 mTutorialFragment = tutorialFragment; 140 mTutorialType = tutorialType; 141 mContext = mTutorialFragment.getContext(); 142 143 RootSandboxLayout rootView = tutorialFragment.getRootView(); 144 mSkipButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button); 145 mSkipButton.setOnClickListener(button -> showSkipTutorialDialog()); 146 mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view); 147 mFeedbackTitleView = mFeedbackView.findViewById( 148 R.id.gesture_tutorial_fragment_feedback_title); 149 mFeedbackSubtitleView = mFeedbackView.findViewById( 150 R.id.gesture_tutorial_fragment_feedback_subtitle); 151 mEdgeGestureVideoView = rootView.findViewById(R.id.gesture_tutorial_edge_gesture_video); 152 mFakeLauncherView = rootView.findViewById(R.id.gesture_tutorial_fake_launcher_view); 153 mFakeHotseatView = rootView.findViewById(R.id.gesture_tutorial_fake_hotseat_view); 154 mFakeIconView = rootView.findViewById(R.id.gesture_tutorial_fake_icon_view); 155 mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view); 156 mFakeTaskbarView = ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() 157 ? null : rootView.findViewById(R.id.gesture_tutorial_fake_taskbar_view); 158 mFakePreviousTaskView = 159 rootView.findViewById(R.id.gesture_tutorial_fake_previous_task_view); 160 mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view); 161 mRippleDrawable = (RippleDrawable) mRippleView.getBackground(); 162 mDoneButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button); 163 mTutorialStepView = 164 rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_tutorial_step); 165 mFingerDotView = rootView.findViewById(R.id.gesture_tutorial_finger_dot); 166 mSkipTutorialDialog = createSkipTutorialDialog(); 167 168 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 169 mFullGestureDemonstration = rootView.findViewById(R.id.full_gesture_demonstration); 170 mCheckmarkAnimation = rootView.findViewById(R.id.checkmark_animation); 171 mAnimatedGestureDemonstration = rootView.findViewById( 172 R.id.gesture_demonstration_animations); 173 mExitingAppView = rootView.findViewById(R.id.exiting_app_back); 174 mScreenWidth = mTutorialFragment.getDeviceProfile().widthPx; 175 mScreenHeight = mTutorialFragment.getDeviceProfile().heightPx; 176 mExitingAppMargin = mContext.getResources().getDimensionPixelSize( 177 R.dimen.gesture_tutorial_back_gesture_exiting_app_margin); 178 mExitingAppStartingCornerRadius = QuickStepContract.getWindowCornerRadius(mContext); 179 mExitingAppEndingCornerRadius = mContext.getResources().getDimensionPixelSize( 180 R.dimen.gesture_tutorial_back_gesture_end_corner_radius); 181 mAnimatedGestureDemonstration.addLottieOnCompositionLoadedListener( 182 this::createScalingMatrix); 183 184 mFeedbackTitleView.setText(getIntroductionTitle()); 185 mFeedbackSubtitleView.setText(getIntroductionSubtitle()); 186 mExitingAppView.setClipToOutline(true); 187 mExitingAppView.setOutlineProvider(new ViewOutlineProvider() { 188 @Override 189 public void getOutline(View view, Outline outline) { 190 outline.setRoundRect(mExitingAppRect, mExitingAppRadius); 191 } 192 }); 193 } 194 195 mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent( 196 AccessibilityEvent.TYPE_VIEW_FOCUSED); 197 mShowFeedbackRunnable = () -> { 198 mFeedbackView.setAlpha(0f); 199 mFeedbackView.setScaleX(0.95f); 200 mFeedbackView.setScaleY(0.95f); 201 mFeedbackView.setVisibility(View.VISIBLE); 202 mFeedbackView.animate() 203 .setDuration(FEEDBACK_ANIMATION_MS) 204 .alpha(1f) 205 .scaleX(1f) 206 .scaleY(1f) 207 .withEndAction(() -> { 208 if (mGestureCompleted && !mTutorialFragment.isAtFinalStep()) { 209 if (mFeedbackViewCallback != null) { 210 mFeedbackView.removeCallbacks(mFeedbackViewCallback); 211 } 212 mFeedbackViewCallback = mTutorialFragment::continueTutorial; 213 mFeedbackView.postDelayed( 214 mFeedbackViewCallback, 215 AccessibilityManager.getInstance(mContext) 216 .getRecommendedTimeoutMillis( 217 ADVANCE_TUTORIAL_TIMEOUT_MS, 218 AccessibilityManager.FLAG_CONTENT_TEXT)); 219 } 220 }) 221 .start(); 222 mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS); 223 }; 224 } 225 226 /** Scale the Lottie gesture animation to fit the device based on device dimensions */ createScalingMatrix(LottieComposition composition)227 private void createScalingMatrix(LottieComposition composition) { 228 Rect animationBoundsRect = composition.getBounds(); 229 if (animationBoundsRect == null) { 230 mAnimatedGestureDemonstration.setScaleType(ImageView.ScaleType.CENTER_CROP); 231 return; 232 } 233 Matrix scaleMatrix = new Matrix(); 234 float scaleFactor = mScreenWidth / animationBoundsRect.width(); 235 float heightTranslate = (mScreenHeight - (scaleFactor * animationBoundsRect.height())); 236 237 scaleMatrix.postScale(scaleFactor, scaleFactor); 238 scaleMatrix.postTranslate(0, heightTranslate); 239 mAnimatedGestureDemonstration.setImageMatrix(scaleMatrix); 240 } 241 showSkipTutorialDialog()242 private void showSkipTutorialDialog() { 243 if (mSkipTutorialDialog != null) { 244 mSkipTutorialDialog.show(); 245 } 246 } 247 getHotseatIconTop()248 public int getHotseatIconTop() { 249 return mHotseatIconView == null 250 ? 0 : mFakeHotseatView.getTop() + mHotseatIconView.getTop(); 251 } 252 getHotseatIconLeft()253 public int getHotseatIconLeft() { 254 return mHotseatIconView == null 255 ? 0 : mFakeHotseatView.getLeft() + mHotseatIconView.getLeft(); 256 } 257 setTutorialType(TutorialType tutorialType)258 void setTutorialType(TutorialType tutorialType) { 259 mTutorialType = tutorialType; 260 } 261 262 @LayoutRes getMockHotseatResId()263 protected int getMockHotseatResId() { 264 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 265 return mTutorialFragment.isLargeScreen() 266 ? mTutorialFragment.isFoldable() 267 ? R.layout.redesigned_gesture_tutorial_foldable_mock_hotseat 268 : R.layout.redesigned_gesture_tutorial_tablet_mock_hotseat 269 : R.layout.redesigned_gesture_tutorial_mock_hotseat; 270 } else { 271 return mTutorialFragment.isLargeScreen() 272 ? mTutorialFragment.isFoldable() 273 ? R.layout.gesture_tutorial_foldable_mock_hotseat 274 : R.layout.gesture_tutorial_tablet_mock_hotseat 275 : R.layout.gesture_tutorial_mock_hotseat; 276 } 277 } 278 279 @LayoutRes getMockAppTaskLayoutResId()280 protected int getMockAppTaskLayoutResId() { 281 return NO_ID; 282 } 283 284 @RawRes getGestureLottieAnimationId()285 protected int getGestureLottieAnimationId() { 286 return NO_ID; 287 } 288 289 @ColorInt getMockPreviousAppTaskThumbnailColor()290 protected int getMockPreviousAppTaskThumbnailColor() { 291 return mContext.getResources().getColor( 292 R.color.gesture_tutorial_fake_previous_task_view_color); 293 } 294 295 @ColorInt getFakeTaskViewColor()296 protected int getFakeTaskViewColor() { 297 return Color.TRANSPARENT; 298 } 299 300 @ColorInt getFakeLauncherColor()301 protected abstract int getFakeLauncherColor(); 302 303 @ColorInt getExitingAppColor()304 protected int getExitingAppColor() { 305 return Color.TRANSPARENT; 306 } 307 308 @ColorInt getHotseatIconColor()309 protected int getHotseatIconColor() { 310 return Color.TRANSPARENT; 311 } 312 313 @DrawableRes getMockAppIconResId()314 public int getMockAppIconResId() { 315 return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() 316 ? R.drawable.redesigned_hotseat_icon 317 : R.drawable.default_sandbox_app_icon; 318 } 319 320 @DrawableRes getMockWallpaperResId()321 public int getMockWallpaperResId() { 322 return R.drawable.default_sandbox_wallpaper; 323 } 324 fadeTaskViewAndRun(Runnable r)325 void fadeTaskViewAndRun(Runnable r) { 326 mFakeTaskView.animate().alpha(0).setListener(AnimatorListeners.forSuccessCallback(r)); 327 } 328 329 @StringRes getIntroductionTitle()330 public int getIntroductionTitle() { 331 return NO_ID; 332 } 333 334 @StringRes getIntroductionSubtitle()335 public int getIntroductionSubtitle() { 336 return NO_ID; 337 } 338 339 @StringRes getSpokenIntroductionSubtitle()340 public int getSpokenIntroductionSubtitle() { 341 return NO_ID; 342 } 343 344 @StringRes getSuccessFeedbackSubtitle()345 public int getSuccessFeedbackSubtitle() { 346 return NO_ID; 347 } 348 349 @StringRes getSuccessFeedbackTitle()350 public int getSuccessFeedbackTitle() { 351 return NO_ID; 352 } 353 354 @StyleRes getTitleTextAppearance()355 public int getTitleTextAppearance() { 356 return NO_ID; 357 } 358 359 @StyleRes getSuccessTitleTextAppearance()360 public int getSuccessTitleTextAppearance() { 361 return NO_ID; 362 } 363 364 @StyleRes getDoneButtonTextAppearance()365 public int getDoneButtonTextAppearance() { 366 return NO_ID; 367 } 368 369 @ColorInt getDoneButtonColor()370 public abstract int getDoneButtonColor(); 371 showFeedback()372 void showFeedback() { 373 if (mGestureCompleted) { 374 mFeedbackView.setTranslationY(0); 375 return; 376 } 377 Animator gestureAnimation = mTutorialFragment.getGestureAnimation(); 378 AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation(); 379 if (gestureAnimation != null && edgeAnimation != null) { 380 playFeedbackAnimation(gestureAnimation, edgeAnimation, mShowFeedbackRunnable, true); 381 } 382 } 383 384 /** 385 * Only use this when a gesture is completed, but the feedback shouldn't be shown immediately. 386 * In that case, call this method immediately instead. 387 */ setGestureCompleted()388 public void setGestureCompleted() { 389 mGestureCompleted = true; 390 } 391 392 /** 393 * Show feedback reflecting a successful gesture attempt. 394 **/ showSuccessFeedback()395 void showSuccessFeedback() { 396 int successSubtitleResId = getSuccessFeedbackSubtitle(); 397 if (successSubtitleResId == NO_ID) { 398 // Allow crash since this should never be reached with a tutorial controller used in 399 // production. 400 Log.e(LOG_TAG, 401 "Cannot show success feedback for tutorial step: " + mTutorialType 402 + ", no success feedback subtitle", 403 new IllegalStateException()); 404 } 405 showFeedback(successSubtitleResId, true); 406 } 407 408 /** 409 * Show feedback reflecting a failed gesture attempt. 410 * 411 * @param subtitleResId Resource of the text to display. 412 **/ showFeedback(int subtitleResId)413 void showFeedback(int subtitleResId) { 414 showFeedback(subtitleResId, false); 415 } 416 417 /** 418 * Show feedback reflecting the result of a gesture attempt. 419 * 420 * @param isGestureSuccessful Whether the tutorial feedback's action button should be shown. 421 **/ showFeedback(int subtitleResId, boolean isGestureSuccessful)422 void showFeedback(int subtitleResId, boolean isGestureSuccessful) { 423 showFeedback( 424 isGestureSuccessful 425 ? getSuccessFeedbackTitle() : R.string.gesture_tutorial_try_again, 426 subtitleResId, 427 NO_ID, 428 isGestureSuccessful, 429 false); 430 } 431 showFeedback( int titleResId, int subtitleResId, int spokenSubtitleResId, boolean isGestureSuccessful, boolean useGestureAnimationDelay)432 void showFeedback( 433 int titleResId, 434 int subtitleResId, 435 int spokenSubtitleResId, 436 boolean isGestureSuccessful, 437 boolean useGestureAnimationDelay) { 438 mFeedbackTitleView.removeCallbacks(mTitleViewCallback); 439 if (mFeedbackViewCallback != null) { 440 mFeedbackView.removeCallbacks(mFeedbackViewCallback); 441 mFeedbackViewCallback = null; 442 } 443 444 mFeedbackTitleView.setText(titleResId); 445 mFeedbackSubtitleView.setText( 446 ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() || spokenSubtitleResId == NO_ID 447 ? mContext.getText(subtitleResId) 448 : Utilities.wrapForTts( 449 mContext.getText(subtitleResId), 450 mContext.getString(spokenSubtitleResId))); 451 if (isGestureSuccessful) { 452 if (mTutorialFragment.isAtFinalStep()) { 453 showActionButton(); 454 } 455 456 if (mFakeTaskViewCallback != null) { 457 mFakeTaskView.removeCallbacks(mFakeTaskViewCallback); 458 mFakeTaskViewCallback = null; 459 } 460 461 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 462 showSuccessPage(); 463 } 464 } 465 mGestureCompleted = isGestureSuccessful; 466 467 Animator gestureAnimation = mTutorialFragment.getGestureAnimation(); 468 AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation(); 469 if (!isGestureSuccessful && gestureAnimation != null && edgeAnimation != null) { 470 playFeedbackAnimation( 471 gestureAnimation, 472 edgeAnimation, 473 mShowFeedbackRunnable, 474 useGestureAnimationDelay); 475 return; 476 } else { 477 mTutorialFragment.releaseFeedbackAnimation(); 478 } 479 mFeedbackViewCallback = mShowFeedbackRunnable; 480 481 mFeedbackView.post(mFeedbackViewCallback); 482 } 483 showSuccessPage()484 private void showSuccessPage() { 485 pauseAndHideLottieAnimation(); 486 mCheckmarkAnimation.setVisibility(View.VISIBLE); 487 mCheckmarkAnimation.playAnimation(); 488 mFeedbackTitleView.setTextAppearance(mContext, getSuccessTitleTextAppearance()); 489 } 490 isGestureCompleted()491 public boolean isGestureCompleted() { 492 return mGestureCompleted; 493 } 494 hideFeedback()495 void hideFeedback() { 496 if (mFeedbackView.getVisibility() != View.VISIBLE) { 497 return; 498 } 499 cancelQueuedGestureAnimation(); 500 mFeedbackView.clearAnimation(); 501 mFeedbackView.setVisibility(View.INVISIBLE); 502 } 503 cancelQueuedGestureAnimation()504 void cancelQueuedGestureAnimation() { 505 if (mFeedbackViewCallback != null) { 506 mFeedbackView.removeCallbacks(mFeedbackViewCallback); 507 mFeedbackViewCallback = null; 508 } 509 if (mFakeTaskViewCallback != null) { 510 mFakeTaskView.removeCallbacks(mFakeTaskViewCallback); 511 mFakeTaskViewCallback = null; 512 } 513 if (mFakeTaskbarViewCallback != null && mFakeTaskbarView != null) { 514 mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback); 515 mFakeTaskbarViewCallback = null; 516 } 517 mFeedbackTitleView.removeCallbacks(mTitleViewCallback); 518 } 519 playFeedbackAnimation( @onNull Animator gestureAnimation, @NonNull AnimatedVectorDrawable edgeAnimation, @NonNull Runnable onStartRunnable, boolean useGestureAnimationDelay)520 private void playFeedbackAnimation( 521 @NonNull Animator gestureAnimation, 522 @NonNull AnimatedVectorDrawable edgeAnimation, 523 @NonNull Runnable onStartRunnable, 524 boolean useGestureAnimationDelay) { 525 526 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 527 mFeedbackView.setVisibility(View.VISIBLE); 528 mAnimatedGestureDemonstration.setVisibility(View.VISIBLE); 529 mFullGestureDemonstration.setVisibility(View.VISIBLE); 530 mAnimatedGestureDemonstration.playAnimation(); 531 return; 532 } 533 534 if (gestureAnimation.isRunning()) { 535 gestureAnimation.cancel(); 536 } 537 if (edgeAnimation.isRunning()) { 538 edgeAnimation.reset(); 539 } 540 gestureAnimation.addListener(new AnimatorListenerAdapter() { 541 @Override 542 public void onAnimationStart(Animator animation) { 543 super.onAnimationStart(animation); 544 545 mEdgeGestureVideoView.setVisibility(GONE); 546 if (edgeAnimation.isRunning()) { 547 edgeAnimation.stop(); 548 } 549 550 if (!useGestureAnimationDelay) { 551 onStartRunnable.run(); 552 } 553 } 554 555 @Override 556 public void onAnimationEnd(Animator animation) { 557 super.onAnimationEnd(animation); 558 559 mEdgeGestureVideoView.setVisibility(View.VISIBLE); 560 edgeAnimation.start(); 561 562 gestureAnimation.removeListener(this); 563 } 564 }); 565 566 cancelQueuedGestureAnimation(); 567 if (useGestureAnimationDelay) { 568 mFeedbackViewCallback = onStartRunnable; 569 mFakeTaskViewCallback = gestureAnimation::start; 570 571 mFeedbackView.post(mFeedbackViewCallback); 572 mFakeTaskView.postDelayed(mFakeTaskViewCallback, GESTURE_ANIMATION_DELAY_MS); 573 } else { 574 gestureAnimation.start(); 575 } 576 } 577 setRippleHotspot(float x, float y)578 void setRippleHotspot(float x, float y) { 579 mRippleDrawable.setHotspot(x, y); 580 } 581 showRippleEffect(@ullable Runnable onCompleteRunnable)582 void showRippleEffect(@Nullable Runnable onCompleteRunnable) { 583 mRippleDrawable.setState( 584 new int[] {android.R.attr.state_pressed, android.R.attr.state_enabled}); 585 mRippleView.postDelayed(() -> { 586 mRippleDrawable.setState(new int[] {}); 587 if (onCompleteRunnable != null) { 588 onCompleteRunnable.run(); 589 } 590 }, RIPPLE_VISIBLE_MS); 591 } 592 onActionButtonClicked(View button)593 void onActionButtonClicked(View button) { 594 mTutorialFragment.continueTutorial(); 595 } 596 597 @CallSuper transitToController()598 void transitToController() { 599 updateCloseButton(); 600 updateSubtext(); 601 updateDrawables(); 602 updateLayout(); 603 604 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 605 mFeedbackTitleView.setTextAppearance(mContext, getTitleTextAppearance()); 606 mDoneButton.setTextAppearance(mContext, getDoneButtonTextAppearance()); 607 mDoneButton.getBackground().setTint(getDoneButtonColor()); 608 mCheckmarkAnimation.setAnimation(mTutorialFragment.isAtFinalStep() 609 ? R.raw.checkmark_animation_end 610 : R.raw.checkmark_animation_in_progress); 611 if (!isGestureCompleted()) { 612 mCheckmarkAnimation.setVisibility(GONE); 613 startGestureAnimation(); 614 if (mTutorialType == TutorialType.BACK_NAVIGATION) { 615 resetViewsForBackGesture(); 616 } 617 618 } 619 } else { 620 hideFeedback(); 621 hideActionButton(); 622 } 623 624 mGestureCompleted = false; 625 if (mFakeHotseatView != null) { 626 mFakeHotseatView.setVisibility(View.INVISIBLE); 627 } 628 } 629 resetViewsForBackGesture()630 protected void resetViewsForBackGesture() { 631 mFakeTaskView.setVisibility(View.VISIBLE); 632 mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); 633 mExitingAppView.setVisibility(View.VISIBLE); 634 635 // reset the exiting app's dimensions 636 mExitingAppRect.set(0, 0, (int) mScreenWidth, (int) mScreenHeight); 637 mExitingAppRadius = 0; 638 mExitingAppView.resetPivot(); 639 mExitingAppView.setScaleX(1f); 640 mExitingAppView.setScaleY(1f); 641 mExitingAppView.setTranslationX(0); 642 mExitingAppView.setTranslationY(0); 643 mExitingAppView.invalidateOutline(); 644 } 645 startGestureAnimation()646 private void startGestureAnimation() { 647 mAnimatedGestureDemonstration.setAnimation(getGestureLottieAnimationId()); 648 mAnimatedGestureDemonstration.playAnimation(); 649 } 650 updateCloseButton()651 void updateCloseButton() { 652 mSkipButton.setTextAppearance(Utilities.isDarkTheme(mContext) 653 ? R.style.TextAppearance_GestureTutorial_Feedback_Subtext 654 : R.style.TextAppearance_GestureTutorial_Feedback_Subtext_Dark); 655 } 656 hideActionButton()657 void hideActionButton() { 658 mSkipButton.setVisibility(View.VISIBLE); 659 // Invisible to maintain the layout. 660 mDoneButton.setVisibility(View.INVISIBLE); 661 mDoneButton.setOnClickListener(null); 662 } 663 showActionButton()664 void showActionButton() { 665 mSkipButton.setVisibility(GONE); 666 mDoneButton.setVisibility(View.VISIBLE); 667 mDoneButton.setOnClickListener(this::onActionButtonClicked); 668 } 669 hideFakeTaskbar(boolean animateToHotseat)670 void hideFakeTaskbar(boolean animateToHotseat) { 671 if (!mTutorialFragment.isLargeScreen() || mFakeTaskbarView == null) { 672 return; 673 } 674 if (mFakeTaskbarViewCallback != null) { 675 mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback); 676 } 677 if (animateToHotseat) { 678 mFakeTaskbarViewCallback = () -> 679 mFakeTaskbarView.animateDisappearanceToHotseat(mFakeHotseatView); 680 } 681 mFakeTaskbarView.post(mFakeTaskbarViewCallback); 682 } 683 showFakeTaskbar(boolean animateFromHotseat)684 void showFakeTaskbar(boolean animateFromHotseat) { 685 if (!mTutorialFragment.isLargeScreen() || mFakeTaskbarView == null) { 686 return; 687 } 688 if (mFakeTaskbarViewCallback != null) { 689 mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback); 690 } 691 if (animateFromHotseat) { 692 mFakeTaskbarViewCallback = () -> 693 mFakeTaskbarView.animateAppearanceFromHotseat(mFakeHotseatView); 694 } 695 mFakeTaskbarView.post(mFakeTaskbarViewCallback); 696 } 697 updateFakeAppTaskViewLayout(@ayoutRes int mockAppTaskLayoutResId)698 void updateFakeAppTaskViewLayout(@LayoutRes int mockAppTaskLayoutResId) { 699 updateFakeViewLayout(mFakeTaskView, mockAppTaskLayoutResId); 700 } 701 updateFakeViewLayout(ViewGroup view, @LayoutRes int mockLayoutResId)702 void updateFakeViewLayout(ViewGroup view, @LayoutRes int mockLayoutResId) { 703 view.removeAllViews(); 704 if (mockLayoutResId != NO_ID) { 705 view.addView( 706 inflate(mContext, mockLayoutResId, null), 707 new FrameLayout.LayoutParams( 708 ViewGroup.LayoutParams.MATCH_PARENT, 709 ViewGroup.LayoutParams.MATCH_PARENT)); 710 } 711 } 712 updateSubtext()713 private void updateSubtext() { 714 if (!ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 715 mTutorialStepView.setTutorialProgress( 716 mTutorialFragment.getCurrentStep(), mTutorialFragment.getNumSteps()); 717 } 718 } 719 updateHotseatChildViewColor(@ullable View child)720 private void updateHotseatChildViewColor(@Nullable View child) { 721 if (child == null) return; 722 child.getBackground().setTint(getHotseatIconColor()); 723 } 724 updateDrawables()725 private void updateDrawables() { 726 if (mContext != null) { 727 mTutorialFragment.getRootView().setBackground(AppCompatResources.getDrawable( 728 mContext, getMockWallpaperResId())); 729 mTutorialFragment.updateFeedbackAnimation(); 730 mFakeLauncherView.setBackgroundColor(ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() 731 ? getFakeLauncherColor() 732 : mContext.getColor(R.color.gesture_tutorial_fake_wallpaper_color)); 733 updateFakeViewLayout(mFakeHotseatView, getMockHotseatResId()); 734 mHotseatIconView = mFakeHotseatView.findViewById(R.id.hotseat_icon_1); 735 mFakeTaskView.animate().alpha(1).setListener( 736 AnimatorListeners.forSuccessCallback(() -> mFakeTaskView.animate().cancel())); 737 mFakePreviousTaskView.setFakeTaskViewFillColor(getMockPreviousAppTaskThumbnailColor()); 738 mFakeIconView.setBackground(AppCompatResources.getDrawable( 739 mContext, getMockAppIconResId())); 740 741 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 742 mExitingAppView.setBackgroundColor(getExitingAppColor()); 743 mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); 744 updateHotseatChildViewColor(mHotseatIconView); 745 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_2)); 746 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_3)); 747 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_4)); 748 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_5)); 749 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_6)); 750 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_search_bar)); 751 } else { 752 updateFakeViewLayout(mFakeTaskView, getMockAppTaskLayoutResId()); 753 } 754 } 755 } 756 updateLayout()757 private void updateLayout() { 758 if (mContext == null) { 759 return; 760 } 761 RelativeLayout.LayoutParams feedbackLayoutParams = 762 (RelativeLayout.LayoutParams) mFeedbackView.getLayoutParams(); 763 feedbackLayoutParams.setMarginStart(mContext.getResources().getDimensionPixelSize( 764 mTutorialFragment.isLargeScreen() 765 ? R.dimen.gesture_tutorial_tablet_feedback_margin_start_end 766 : R.dimen.gesture_tutorial_feedback_margin_start_end)); 767 feedbackLayoutParams.setMarginEnd(mContext.getResources().getDimensionPixelSize( 768 mTutorialFragment.isLargeScreen() 769 ? R.dimen.gesture_tutorial_tablet_feedback_margin_start_end 770 : R.dimen.gesture_tutorial_feedback_margin_start_end)); 771 feedbackLayoutParams.topMargin = mContext.getResources().getDimensionPixelSize( 772 mTutorialFragment.isLargeScreen() 773 ? R.dimen.gesture_tutorial_tablet_feedback_margin_top 774 : R.dimen.gesture_tutorial_feedback_margin_top); 775 776 if (mFakeTaskbarView != null) { 777 mFakeTaskbarView.setVisibility( 778 mTutorialFragment.isLargeScreen() ? View.VISIBLE : GONE); 779 } 780 781 RelativeLayout.LayoutParams hotseatLayoutParams = 782 (RelativeLayout.LayoutParams) mFakeHotseatView.getLayoutParams(); 783 if (!mTutorialFragment.isLargeScreen()) { 784 DeviceProfile dp = mTutorialFragment.getDeviceProfile(); 785 dp.updateIsSeascape(mContext); 786 787 hotseatLayoutParams.addRule(dp.isLandscape 788 ? (dp.isSeascape() 789 ? RelativeLayout.ALIGN_PARENT_START 790 : RelativeLayout.ALIGN_PARENT_END) 791 : RelativeLayout.ALIGN_PARENT_BOTTOM); 792 } else { 793 hotseatLayoutParams.width = RelativeLayout.LayoutParams.MATCH_PARENT; 794 hotseatLayoutParams.height = RelativeLayout.LayoutParams.WRAP_CONTENT; 795 hotseatLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); 796 hotseatLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_START); 797 hotseatLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_END); 798 } 799 mFakeHotseatView.setLayoutParams(hotseatLayoutParams); 800 } 801 createSkipTutorialDialog()802 private AlertDialog createSkipTutorialDialog() { 803 if (!(mContext instanceof GestureSandboxActivity)) { 804 return null; 805 } 806 GestureSandboxActivity sandboxActivity = (GestureSandboxActivity) mContext; 807 View contentView = View.inflate( 808 sandboxActivity, R.layout.gesture_tutorial_dialog, null); 809 AlertDialog tutorialDialog = new AlertDialog 810 .Builder(sandboxActivity, R.style.Theme_AppCompat_Dialog_Alert) 811 .setView(contentView) 812 .create(); 813 814 PackageManager packageManager = mContext.getPackageManager(); 815 CharSequence tipsAppName = DEFAULT_PIXEL_TIPS_APP_NAME; 816 817 try { 818 tipsAppName = packageManager.getApplicationLabel( 819 packageManager.getApplicationInfo( 820 PIXEL_TIPS_APP_PACKAGE_NAME, PackageManager.GET_META_DATA)); 821 } catch (PackageManager.NameNotFoundException e) { 822 Log.e(LOG_TAG, 823 "Could not find app label for package name: " 824 + PIXEL_TIPS_APP_PACKAGE_NAME 825 + ". Defaulting to 'Pixel Tips.'", 826 e); 827 } 828 829 TextView subtitleTextView = (TextView) contentView.findViewById( 830 R.id.gesture_tutorial_dialog_subtitle); 831 if (subtitleTextView != null) { 832 subtitleTextView.setText( 833 mContext.getString(R.string.skip_tutorial_dialog_subtitle, tipsAppName)); 834 } else { 835 Log.w(LOG_TAG, "No subtitle view in the skip tutorial dialog to update."); 836 } 837 838 Button cancelButton = (Button) contentView.findViewById( 839 R.id.gesture_tutorial_dialog_cancel_button); 840 if (cancelButton != null) { 841 cancelButton.setOnClickListener( 842 v -> tutorialDialog.dismiss()); 843 } else { 844 Log.w(LOG_TAG, "No cancel button in the skip tutorial dialog to update."); 845 } 846 847 Button confirmButton = contentView.findViewById( 848 R.id.gesture_tutorial_dialog_confirm_button); 849 if (confirmButton != null) { 850 confirmButton.setOnClickListener(v -> { 851 mTutorialFragment.closeTutorialStep(true); 852 tutorialDialog.dismiss(); 853 }); 854 } else { 855 Log.w(LOG_TAG, "No confirm button in the skip tutorial dialog to update."); 856 } 857 858 tutorialDialog.getWindow().setBackgroundDrawable( 859 new ColorDrawable(sandboxActivity.getColor(android.R.color.transparent))); 860 861 return tutorialDialog; 862 } 863 createFingerDotAppearanceAnimatorSet()864 protected AnimatorSet createFingerDotAppearanceAnimatorSet() { 865 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat( 866 mFingerDotView, View.ALPHA, 0f, FINGER_DOT_VISIBLE_ALPHA); 867 ObjectAnimator yScaleAnimator = ObjectAnimator.ofFloat( 868 mFingerDotView, View.SCALE_Y, FINGER_DOT_SMALL_SCALE, 1f); 869 ObjectAnimator xScaleAnimator = ObjectAnimator.ofFloat( 870 mFingerDotView, View.SCALE_X, FINGER_DOT_SMALL_SCALE, 1f); 871 ArrayList<Animator> animators = new ArrayList<>(); 872 873 animators.add(alphaAnimator); 874 animators.add(xScaleAnimator); 875 animators.add(yScaleAnimator); 876 877 AnimatorSet appearanceAnimatorSet = new AnimatorSet(); 878 879 appearanceAnimatorSet.playTogether(animators); 880 appearanceAnimatorSet.setDuration(FINGER_DOT_ANIMATION_DURATION_MILLIS); 881 882 return appearanceAnimatorSet; 883 } 884 createFingerDotDisappearanceAnimatorSet()885 protected AnimatorSet createFingerDotDisappearanceAnimatorSet() { 886 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat( 887 mFingerDotView, View.ALPHA, FINGER_DOT_VISIBLE_ALPHA, 0f); 888 ObjectAnimator yScaleAnimator = ObjectAnimator.ofFloat( 889 mFingerDotView, View.SCALE_Y, 1f, FINGER_DOT_SMALL_SCALE); 890 ObjectAnimator xScaleAnimator = ObjectAnimator.ofFloat( 891 mFingerDotView, View.SCALE_X, 1f, FINGER_DOT_SMALL_SCALE); 892 ArrayList<Animator> animators = new ArrayList<>(); 893 894 animators.add(alphaAnimator); 895 animators.add(xScaleAnimator); 896 animators.add(yScaleAnimator); 897 898 AnimatorSet appearanceAnimatorSet = new AnimatorSet(); 899 900 appearanceAnimatorSet.playTogether(animators); 901 appearanceAnimatorSet.setDuration(FINGER_DOT_ANIMATION_DURATION_MILLIS); 902 903 return appearanceAnimatorSet; 904 } 905 createAnimationPause()906 protected Animator createAnimationPause() { 907 return ValueAnimator.ofFloat(0f, 1f).setDuration(GESTURE_ANIMATION_PAUSE_DURATION_MILLIS); 908 } 909 pauseAndHideLottieAnimation()910 void pauseAndHideLottieAnimation() { 911 mAnimatedGestureDemonstration.pauseAnimation(); 912 mAnimatedGestureDemonstration.setVisibility(View.INVISIBLE); 913 mFullGestureDemonstration.setVisibility(View.INVISIBLE); 914 } 915 916 /** Denotes the type of the tutorial. */ 917 enum TutorialType { 918 BACK_NAVIGATION, 919 BACK_NAVIGATION_COMPLETE, 920 HOME_NAVIGATION, 921 HOME_NAVIGATION_COMPLETE, 922 OVERVIEW_NAVIGATION, 923 OVERVIEW_NAVIGATION_COMPLETE 924 } 925 } 926