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 android.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.animation.AnimatorSet; 25 import android.animation.ObjectAnimator; 26 import android.animation.ValueAnimator; 27 import android.annotation.ColorRes; 28 import android.content.Context; 29 import android.content.pm.PackageManager; 30 import android.graphics.drawable.AnimatedVectorDrawable; 31 import android.graphics.drawable.ColorDrawable; 32 import android.graphics.drawable.RippleDrawable; 33 import android.util.Log; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.view.accessibility.AccessibilityEvent; 37 import android.widget.Button; 38 import android.widget.FrameLayout; 39 import android.widget.ImageView; 40 import android.widget.RelativeLayout; 41 import android.widget.TextView; 42 43 import androidx.annotation.CallSuper; 44 import androidx.annotation.DrawableRes; 45 import androidx.annotation.LayoutRes; 46 import androidx.annotation.NonNull; 47 import androidx.annotation.Nullable; 48 import androidx.annotation.StringRes; 49 import androidx.appcompat.app.AlertDialog; 50 import androidx.appcompat.content.res.AppCompatResources; 51 52 import com.android.launcher3.R; 53 import com.android.launcher3.Utilities; 54 import com.android.launcher3.anim.AnimatorListeners; 55 import com.android.launcher3.views.ClipIconView; 56 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback; 57 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback; 58 59 import java.util.ArrayList; 60 61 abstract class TutorialController implements BackGestureAttemptCallback, 62 NavBarGestureAttemptCallback { 63 64 private static final String TAG = "TutorialController"; 65 66 private static final float FINGER_DOT_VISIBLE_ALPHA = 0.6f; 67 private static final float FINGER_DOT_SMALL_SCALE = 0.7f; 68 private static final int FINGER_DOT_ANIMATION_DURATION_MILLIS = 500; 69 70 private static final String PIXEL_TIPS_APP_PACKAGE_NAME = "com.google.android.apps.tips"; 71 private static final CharSequence DEFAULT_PIXEL_TIPS_APP_NAME = "Pixel Tips"; 72 73 private static final int FEEDBACK_ANIMATION_MS = 133; 74 private static final int RIPPLE_VISIBLE_MS = 300; 75 private static final int GESTURE_ANIMATION_DELAY_MS = 1500; 76 private static final int ADVANCE_TUTORIAL_TIMEOUT_MS = 4000; 77 private static final long GESTURE_ANIMATION_PAUSE_DURATION_MILLIS = 1000; 78 79 final TutorialFragment mTutorialFragment; 80 TutorialType mTutorialType; 81 final Context mContext; 82 83 final TextView mCloseButton; 84 final ViewGroup mFeedbackView; 85 final TextView mFeedbackTitleView; 86 final ImageView mEdgeGestureVideoView; 87 final RelativeLayout mFakeLauncherView; 88 final FrameLayout mFakeHotseatView; 89 @Nullable View mHotseatIconView; 90 final ClipIconView mFakeIconView; 91 final FrameLayout mFakeTaskView; 92 final AnimatedTaskbarView mFakeTaskbarView; 93 final AnimatedTaskView mFakePreviousTaskView; 94 final View mRippleView; 95 final RippleDrawable mRippleDrawable; 96 final Button mActionButton; 97 final TutorialStepIndicator mTutorialStepView; 98 final ImageView mFingerDotView; 99 private final AlertDialog mSkipTutorialDialog; 100 101 private boolean mGestureCompleted = false; 102 103 // These runnables should be used when posting callbacks to their views and cleared from their 104 // views before posting new callbacks. 105 private final Runnable mTitleViewCallback; 106 @Nullable private Runnable mFeedbackViewCallback; 107 @Nullable private Runnable mFakeTaskViewCallback; 108 @Nullable private Runnable mFakeTaskbarViewCallback; 109 private final Runnable mShowFeedbackRunnable; 110 TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType)111 TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) { 112 mTutorialFragment = tutorialFragment; 113 mTutorialType = tutorialType; 114 mContext = mTutorialFragment.getContext(); 115 116 RootSandboxLayout rootView = tutorialFragment.getRootView(); 117 mCloseButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button); 118 mCloseButton.setOnClickListener(button -> showSkipTutorialDialog()); 119 mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view); 120 mFeedbackTitleView = mFeedbackView.findViewById( 121 R.id.gesture_tutorial_fragment_feedback_title); 122 mEdgeGestureVideoView = rootView.findViewById(R.id.gesture_tutorial_edge_gesture_video); 123 mFakeLauncherView = rootView.findViewById(R.id.gesture_tutorial_fake_launcher_view); 124 mFakeHotseatView = rootView.findViewById(R.id.gesture_tutorial_fake_hotseat_view); 125 mFakeIconView = rootView.findViewById(R.id.gesture_tutorial_fake_icon_view); 126 mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view); 127 mFakeTaskbarView = rootView.findViewById(R.id.gesture_tutorial_fake_taskbar_view); 128 mFakePreviousTaskView = 129 rootView.findViewById(R.id.gesture_tutorial_fake_previous_task_view); 130 mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view); 131 mRippleDrawable = (RippleDrawable) mRippleView.getBackground(); 132 mActionButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button); 133 mTutorialStepView = 134 rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_tutorial_step); 135 mFingerDotView = rootView.findViewById(R.id.gesture_tutorial_finger_dot); 136 mSkipTutorialDialog = createSkipTutorialDialog(); 137 138 mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent( 139 AccessibilityEvent.TYPE_VIEW_FOCUSED); 140 mShowFeedbackRunnable = () -> { 141 mFeedbackView.setAlpha(0f); 142 mFeedbackView.setScaleX(0.95f); 143 mFeedbackView.setScaleY(0.95f); 144 mFeedbackView.setVisibility(View.VISIBLE); 145 mFeedbackView.animate() 146 .setDuration(FEEDBACK_ANIMATION_MS) 147 .alpha(1f) 148 .scaleX(1f) 149 .scaleY(1f) 150 .withEndAction(() -> { 151 if (mGestureCompleted && !mTutorialFragment.isAtFinalStep()) { 152 if (mFeedbackViewCallback != null) { 153 mFeedbackView.removeCallbacks(mFeedbackViewCallback); 154 } 155 mFeedbackViewCallback = mTutorialFragment::continueTutorial; 156 mFeedbackView.postDelayed(mFeedbackViewCallback, 157 ADVANCE_TUTORIAL_TIMEOUT_MS); 158 } 159 }) 160 .start(); 161 mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS); 162 }; 163 } 164 showSkipTutorialDialog()165 private void showSkipTutorialDialog() { 166 if (mSkipTutorialDialog != null) { 167 mSkipTutorialDialog.show(); 168 } 169 } 170 getHotseatIconTop()171 public int getHotseatIconTop() { 172 return mHotseatIconView == null 173 ? 0 : mFakeHotseatView.getTop() + mHotseatIconView.getTop(); 174 } 175 getHotseatIconLeft()176 public int getHotseatIconLeft() { 177 return mHotseatIconView == null 178 ? 0 : mFakeHotseatView.getLeft() + mHotseatIconView.getLeft(); 179 } 180 setTutorialType(TutorialType tutorialType)181 void setTutorialType(TutorialType tutorialType) { 182 mTutorialType = tutorialType; 183 } 184 185 @LayoutRes getMockHotseatResId()186 protected int getMockHotseatResId() { 187 return mTutorialFragment.isLargeScreen() 188 ? R.layout.gesture_tutorial_foldable_mock_hotseat 189 : R.layout.gesture_tutorial_mock_hotseat; 190 } 191 192 @LayoutRes getMockAppTaskLayoutResId()193 protected int getMockAppTaskLayoutResId() { 194 return View.NO_ID; 195 } 196 197 @ColorRes getMockPreviousAppTaskThumbnailColorResId()198 protected int getMockPreviousAppTaskThumbnailColorResId() { 199 return R.color.gesture_tutorial_fake_previous_task_view_color; 200 } 201 202 @DrawableRes getMockAppIconResId()203 public int getMockAppIconResId() { 204 return R.drawable.default_sandbox_app_icon; 205 } 206 207 @DrawableRes getMockWallpaperResId()208 public int getMockWallpaperResId() { 209 return R.drawable.default_sandbox_wallpaper; 210 } 211 fadeTaskViewAndRun(Runnable r)212 void fadeTaskViewAndRun(Runnable r) { 213 mFakeTaskView.animate().alpha(0).setListener(AnimatorListeners.forSuccessCallback(r)); 214 } 215 216 @StringRes getIntroductionTitle()217 public Integer getIntroductionTitle() { 218 return null; 219 } 220 221 @StringRes getIntroductionSubtitle()222 public Integer getIntroductionSubtitle() { 223 return null; 224 } 225 226 @StringRes getSuccessFeedbackSubtitle()227 public Integer getSuccessFeedbackSubtitle() { 228 return null; 229 } 230 showFeedback()231 void showFeedback() { 232 if (mGestureCompleted) { 233 mFeedbackView.setTranslationY(0); 234 return; 235 } 236 Animator gestureAnimation = mTutorialFragment.getGestureAnimation(); 237 AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation(); 238 if (gestureAnimation != null && edgeAnimation != null) { 239 playFeedbackAnimation(gestureAnimation, edgeAnimation, mShowFeedbackRunnable, true); 240 } 241 } 242 243 /** 244 * Show feedback reflecting a successful gesture attempt. 245 **/ showSuccessFeedback()246 void showSuccessFeedback() { 247 showFeedback(getSuccessFeedbackSubtitle(), true); 248 } 249 250 /** 251 * Show feedback reflecting a failed gesture attempt. 252 * 253 * @param subtitleResId Resource of the text to display. 254 **/ showFeedback(int subtitleResId)255 void showFeedback(int subtitleResId) { 256 showFeedback(subtitleResId, false); 257 } 258 259 /** 260 * Show feedback reflecting the result of a gesture attempt. 261 * 262 * @param isGestureSuccessful Whether the tutorial feedback's action button should be shown. 263 **/ showFeedback(int subtitleResId, boolean isGestureSuccessful)264 void showFeedback(int subtitleResId, boolean isGestureSuccessful) { 265 showFeedback( 266 isGestureSuccessful 267 ? R.string.gesture_tutorial_nice : R.string.gesture_tutorial_try_again, 268 subtitleResId, 269 isGestureSuccessful, 270 false); 271 } 272 showFeedback( int titleResId, int subtitleResId, boolean isGestureSuccessful, boolean useGestureAnimationDelay)273 void showFeedback( 274 int titleResId, 275 int subtitleResId, 276 boolean isGestureSuccessful, 277 boolean useGestureAnimationDelay) { 278 mFeedbackTitleView.removeCallbacks(mTitleViewCallback); 279 if (mFeedbackViewCallback != null) { 280 mFeedbackView.removeCallbacks(mFeedbackViewCallback); 281 mFeedbackViewCallback = null; 282 } 283 284 mFeedbackTitleView.setText(titleResId); 285 TextView subtitle = 286 mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_feedback_subtitle); 287 subtitle.setText(subtitleResId); 288 if (isGestureSuccessful) { 289 if (mTutorialFragment.isAtFinalStep()) { 290 showActionButton(); 291 } 292 293 if (mFakeTaskViewCallback != null) { 294 mFakeTaskView.removeCallbacks(mFakeTaskViewCallback); 295 mFakeTaskViewCallback = null; 296 } 297 } 298 mGestureCompleted = isGestureSuccessful; 299 300 Animator gestureAnimation = mTutorialFragment.getGestureAnimation(); 301 AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation(); 302 if (!isGestureSuccessful && gestureAnimation != null && edgeAnimation != null) { 303 playFeedbackAnimation( 304 gestureAnimation, 305 edgeAnimation, 306 mShowFeedbackRunnable, 307 useGestureAnimationDelay); 308 return; 309 } else { 310 mTutorialFragment.releaseFeedbackAnimation(); 311 } 312 mFeedbackViewCallback = mShowFeedbackRunnable; 313 314 mFeedbackView.post(mFeedbackViewCallback); 315 } 316 isGestureCompleted()317 public boolean isGestureCompleted() { 318 return mGestureCompleted; 319 } 320 hideFeedback()321 void hideFeedback() { 322 cancelQueuedGestureAnimation(); 323 mFeedbackView.clearAnimation(); 324 mFeedbackView.setVisibility(View.INVISIBLE); 325 } 326 cancelQueuedGestureAnimation()327 void cancelQueuedGestureAnimation() { 328 if (mFeedbackViewCallback != null) { 329 mFeedbackView.removeCallbacks(mFeedbackViewCallback); 330 mFeedbackViewCallback = null; 331 } 332 if (mFakeTaskViewCallback != null) { 333 mFakeTaskView.removeCallbacks(mFakeTaskViewCallback); 334 mFakeTaskViewCallback = null; 335 } 336 if (mFakeTaskbarViewCallback != null) { 337 mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback); 338 mFakeTaskbarViewCallback = null; 339 } 340 mFeedbackTitleView.removeCallbacks(mTitleViewCallback); 341 } 342 playFeedbackAnimation( @onNull Animator gestureAnimation, @NonNull AnimatedVectorDrawable edgeAnimation, @NonNull Runnable onStartRunnable, boolean useGestureAnimationDelay)343 private void playFeedbackAnimation( 344 @NonNull Animator gestureAnimation, 345 @NonNull AnimatedVectorDrawable edgeAnimation, 346 @NonNull Runnable onStartRunnable, 347 boolean useGestureAnimationDelay) { 348 349 if (gestureAnimation.isRunning()) { 350 gestureAnimation.cancel(); 351 } 352 if (edgeAnimation.isRunning()) { 353 edgeAnimation.reset(); 354 } 355 gestureAnimation.addListener(new AnimatorListenerAdapter() { 356 @Override 357 public void onAnimationStart(Animator animation) { 358 super.onAnimationStart(animation); 359 360 mEdgeGestureVideoView.setVisibility(GONE); 361 if (edgeAnimation.isRunning()) { 362 edgeAnimation.stop(); 363 } 364 365 if (!useGestureAnimationDelay) { 366 onStartRunnable.run(); 367 } 368 } 369 370 @Override 371 public void onAnimationEnd(Animator animation) { 372 super.onAnimationEnd(animation); 373 374 mEdgeGestureVideoView.setVisibility(View.VISIBLE); 375 edgeAnimation.start(); 376 377 gestureAnimation.removeListener(this); 378 } 379 }); 380 381 cancelQueuedGestureAnimation(); 382 if (useGestureAnimationDelay) { 383 mFeedbackViewCallback = onStartRunnable; 384 mFakeTaskViewCallback = gestureAnimation::start; 385 386 mFeedbackView.post(mFeedbackViewCallback); 387 mFakeTaskView.postDelayed(mFakeTaskViewCallback, GESTURE_ANIMATION_DELAY_MS); 388 } else { 389 gestureAnimation.start(); 390 } 391 } 392 setRippleHotspot(float x, float y)393 void setRippleHotspot(float x, float y) { 394 mRippleDrawable.setHotspot(x, y); 395 } 396 showRippleEffect(@ullable Runnable onCompleteRunnable)397 void showRippleEffect(@Nullable Runnable onCompleteRunnable) { 398 mRippleDrawable.setState( 399 new int[] {android.R.attr.state_pressed, android.R.attr.state_enabled}); 400 mRippleView.postDelayed(() -> { 401 mRippleDrawable.setState(new int[] {}); 402 if (onCompleteRunnable != null) { 403 onCompleteRunnable.run(); 404 } 405 }, RIPPLE_VISIBLE_MS); 406 } 407 onActionButtonClicked(View button)408 void onActionButtonClicked(View button) { 409 mTutorialFragment.continueTutorial(); 410 } 411 412 @CallSuper transitToController()413 void transitToController() { 414 hideFeedback(); 415 hideActionButton(); 416 updateCloseButton(); 417 updateSubtext(); 418 updateDrawables(); 419 updateLayout(); 420 421 mGestureCompleted = false; 422 if (mFakeHotseatView != null) { 423 mFakeHotseatView.setVisibility(View.INVISIBLE); 424 } 425 } 426 updateCloseButton()427 void updateCloseButton() { 428 mCloseButton.setTextAppearance(Utilities.isDarkTheme(mContext) 429 ? R.style.TextAppearance_GestureTutorial_Feedback_Subtext 430 : R.style.TextAppearance_GestureTutorial_Feedback_Subtext_Dark); 431 } 432 hideActionButton()433 void hideActionButton() { 434 mCloseButton.setVisibility(View.VISIBLE); 435 // Invisible to maintain the layout. 436 mActionButton.setVisibility(View.INVISIBLE); 437 mActionButton.setOnClickListener(null); 438 } 439 showActionButton()440 void showActionButton() { 441 mCloseButton.setVisibility(GONE); 442 mActionButton.setVisibility(View.VISIBLE); 443 mActionButton.setOnClickListener(this::onActionButtonClicked); 444 } 445 hideFakeTaskbar(boolean animateToHotseat)446 void hideFakeTaskbar(boolean animateToHotseat) { 447 if (!mTutorialFragment.isLargeScreen()) { 448 return; 449 } 450 if (mFakeTaskbarViewCallback != null) { 451 mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback); 452 } 453 if (animateToHotseat) { 454 mFakeTaskbarViewCallback = () -> 455 mFakeTaskbarView.animateDisappearanceToHotseat(mFakeHotseatView); 456 } else { 457 mFakeTaskbarViewCallback = mFakeTaskbarView::animateDisappearanceToBottom; 458 } 459 mFakeTaskbarView.post(mFakeTaskbarViewCallback); 460 } 461 showFakeTaskbar(boolean animateFromHotseat)462 void showFakeTaskbar(boolean animateFromHotseat) { 463 if (!mTutorialFragment.isLargeScreen()) { 464 return; 465 } 466 if (mFakeTaskbarViewCallback != null) { 467 mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback); 468 } 469 if (animateFromHotseat) { 470 mFakeTaskbarViewCallback = () -> 471 mFakeTaskbarView.animateAppearanceFromHotseat(mFakeHotseatView); 472 } else { 473 mFakeTaskbarViewCallback = mFakeTaskbarView::animateAppearanceFromBottom; 474 } 475 mFakeTaskbarView.post(mFakeTaskbarViewCallback); 476 } 477 updateFakeAppTaskViewLayout(@ayoutRes int mockAppTaskLayoutResId)478 void updateFakeAppTaskViewLayout(@LayoutRes int mockAppTaskLayoutResId) { 479 updateFakeViewLayout(mFakeTaskView, mockAppTaskLayoutResId); 480 } 481 updateFakeViewLayout(ViewGroup view, @LayoutRes int mockLayoutResId)482 void updateFakeViewLayout(ViewGroup view, @LayoutRes int mockLayoutResId) { 483 view.removeAllViews(); 484 if (mockLayoutResId != NO_ID) { 485 view.addView( 486 inflate(mContext, mockLayoutResId, null), 487 new FrameLayout.LayoutParams( 488 ViewGroup.LayoutParams.MATCH_PARENT, 489 ViewGroup.LayoutParams.MATCH_PARENT)); 490 } 491 } 492 updateSubtext()493 private void updateSubtext() { 494 mTutorialStepView.setTutorialProgress( 495 mTutorialFragment.getCurrentStep(), mTutorialFragment.getNumSteps()); 496 } 497 updateDrawables()498 private void updateDrawables() { 499 if (mContext != null) { 500 mTutorialFragment.getRootView().setBackground(AppCompatResources.getDrawable( 501 mContext, getMockWallpaperResId())); 502 mTutorialFragment.updateFeedbackAnimation(); 503 mFakeLauncherView.setBackgroundColor( 504 mContext.getColor(R.color.gesture_tutorial_fake_wallpaper_color)); 505 updateFakeViewLayout(mFakeHotseatView, getMockHotseatResId()); 506 mHotseatIconView = mFakeHotseatView.findViewById(R.id.hotseat_icon_1); 507 updateFakeViewLayout(mFakeTaskView, getMockAppTaskLayoutResId()); 508 mFakeTaskView.animate().alpha(1).setListener( 509 AnimatorListeners.forSuccessCallback(() -> mFakeTaskView.animate().cancel())); 510 mFakePreviousTaskView.setFakeTaskViewFillColor(mContext.getResources().getColor( 511 getMockPreviousAppTaskThumbnailColorResId())); 512 mFakeIconView.setBackground(AppCompatResources.getDrawable( 513 mContext, getMockAppIconResId())); 514 } 515 } 516 updateLayout()517 private void updateLayout() { 518 if (mContext != null) { 519 RelativeLayout.LayoutParams feedbackLayoutParams = 520 (RelativeLayout.LayoutParams) mFeedbackView.getLayoutParams(); 521 feedbackLayoutParams.setMarginStart(mContext.getResources().getDimensionPixelSize( 522 mTutorialFragment.isLargeScreen() 523 ? R.dimen.gesture_tutorial_foldable_feedback_margin_start_end 524 : R.dimen.gesture_tutorial_feedback_margin_start_end)); 525 feedbackLayoutParams.setMarginEnd(mContext.getResources().getDimensionPixelSize( 526 mTutorialFragment.isLargeScreen() 527 ? R.dimen.gesture_tutorial_foldable_feedback_margin_start_end 528 : R.dimen.gesture_tutorial_feedback_margin_start_end)); 529 530 mFakeTaskbarView.setVisibility(mTutorialFragment.isLargeScreen() ? View.VISIBLE : GONE); 531 } 532 } 533 createSkipTutorialDialog()534 private AlertDialog createSkipTutorialDialog() { 535 if (mContext instanceof GestureSandboxActivity) { 536 GestureSandboxActivity sandboxActivity = (GestureSandboxActivity) mContext; 537 View contentView = View.inflate( 538 sandboxActivity, R.layout.gesture_tutorial_dialog, null); 539 AlertDialog tutorialDialog = new AlertDialog 540 .Builder(sandboxActivity, R.style.Theme_AppCompat_Dialog_Alert) 541 .setView(contentView) 542 .create(); 543 544 PackageManager packageManager = mContext.getPackageManager(); 545 CharSequence tipsAppName = DEFAULT_PIXEL_TIPS_APP_NAME; 546 547 try { 548 tipsAppName = packageManager.getApplicationLabel( 549 packageManager.getApplicationInfo( 550 PIXEL_TIPS_APP_PACKAGE_NAME, PackageManager.GET_META_DATA)); 551 } catch (PackageManager.NameNotFoundException e) { 552 Log.e(TAG, 553 "Could not find app label for package name: " 554 + PIXEL_TIPS_APP_PACKAGE_NAME 555 + ". Defaulting to 'Pixel Tips.'", 556 e); 557 } 558 559 TextView subtitleTextView = (TextView) contentView.findViewById( 560 R.id.gesture_tutorial_dialog_subtitle); 561 if (subtitleTextView != null) { 562 subtitleTextView.setText( 563 mContext.getString(R.string.skip_tutorial_dialog_subtitle, tipsAppName)); 564 } else { 565 Log.w(TAG, "No subtitle view in the skip tutorial dialog to update."); 566 } 567 568 Button cancelButton = (Button) contentView.findViewById( 569 R.id.gesture_tutorial_dialog_cancel_button); 570 if (cancelButton != null) { 571 cancelButton.setOnClickListener( 572 v -> tutorialDialog.dismiss()); 573 } else { 574 Log.w(TAG, "No cancel button in the skip tutorial dialog to update."); 575 } 576 577 Button confirmButton = contentView.findViewById( 578 R.id.gesture_tutorial_dialog_confirm_button); 579 if (confirmButton != null) { 580 confirmButton.setOnClickListener(v -> { 581 sandboxActivity.closeTutorial(); 582 tutorialDialog.dismiss(); 583 }); 584 } else { 585 Log.w(TAG, "No confirm button in the skip tutorial dialog to update."); 586 } 587 588 tutorialDialog.getWindow().setBackgroundDrawable( 589 new ColorDrawable(sandboxActivity.getColor(android.R.color.transparent))); 590 591 return tutorialDialog; 592 } 593 594 return null; 595 } 596 createFingerDotAppearanceAnimatorSet()597 protected AnimatorSet createFingerDotAppearanceAnimatorSet() { 598 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat( 599 mFingerDotView, View.ALPHA, 0f, FINGER_DOT_VISIBLE_ALPHA); 600 ObjectAnimator yScaleAnimator = ObjectAnimator.ofFloat( 601 mFingerDotView, View.SCALE_Y, FINGER_DOT_SMALL_SCALE, 1f); 602 ObjectAnimator xScaleAnimator = ObjectAnimator.ofFloat( 603 mFingerDotView, View.SCALE_X, FINGER_DOT_SMALL_SCALE, 1f); 604 ArrayList<Animator> animators = new ArrayList<>(); 605 606 animators.add(alphaAnimator); 607 animators.add(xScaleAnimator); 608 animators.add(yScaleAnimator); 609 610 AnimatorSet appearanceAnimatorSet = new AnimatorSet(); 611 612 appearanceAnimatorSet.playTogether(animators); 613 appearanceAnimatorSet.setDuration(FINGER_DOT_ANIMATION_DURATION_MILLIS); 614 615 return appearanceAnimatorSet; 616 } 617 createFingerDotDisappearanceAnimatorSet()618 protected AnimatorSet createFingerDotDisappearanceAnimatorSet() { 619 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat( 620 mFingerDotView, View.ALPHA, FINGER_DOT_VISIBLE_ALPHA, 0f); 621 ObjectAnimator yScaleAnimator = ObjectAnimator.ofFloat( 622 mFingerDotView, View.SCALE_Y, 1f, FINGER_DOT_SMALL_SCALE); 623 ObjectAnimator xScaleAnimator = ObjectAnimator.ofFloat( 624 mFingerDotView, View.SCALE_X, 1f, FINGER_DOT_SMALL_SCALE); 625 ArrayList<Animator> animators = new ArrayList<>(); 626 627 animators.add(alphaAnimator); 628 animators.add(xScaleAnimator); 629 animators.add(yScaleAnimator); 630 631 AnimatorSet appearanceAnimatorSet = new AnimatorSet(); 632 633 appearanceAnimatorSet.playTogether(animators); 634 appearanceAnimatorSet.setDuration(FINGER_DOT_ANIMATION_DURATION_MILLIS); 635 636 return appearanceAnimatorSet; 637 } 638 createAnimationPause()639 protected Animator createAnimationPause() { 640 return ValueAnimator.ofFloat(0f, 1f).setDuration(GESTURE_ANIMATION_PAUSE_DURATION_MILLIS); 641 } 642 643 /** Denotes the type of the tutorial. */ 644 enum TutorialType { 645 BACK_NAVIGATION, 646 BACK_NAVIGATION_COMPLETE, 647 HOME_NAVIGATION, 648 HOME_NAVIGATION_COMPLETE, 649 OVERVIEW_NAVIGATION, 650 OVERVIEW_NAVIGATION_COMPLETE, 651 ASSISTANT, 652 ASSISTANT_COMPLETE, 653 SANDBOX_MODE 654 } 655 } 656