1 /* 2 * Copyright (C) 2019 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 17 package com.android.settings.accessibility; 18 19 import static android.view.View.GONE; 20 import static android.view.View.VISIBLE; 21 22 import static com.android.settings.accessibility.AccessibilityUtil.UserShortcutType; 23 24 import android.app.settings.SettingsEnums; 25 import android.content.Context; 26 import android.content.DialogInterface; 27 import android.graphics.drawable.Drawable; 28 import android.text.Spannable; 29 import android.text.SpannableString; 30 import android.text.SpannableStringBuilder; 31 import android.text.style.ImageSpan; 32 import android.util.Log; 33 import android.view.Gravity; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.view.Window; 38 import android.widget.FrameLayout; 39 import android.widget.ImageView; 40 import android.widget.LinearLayout; 41 import android.widget.TextSwitcher; 42 import android.widget.TextView; 43 44 import androidx.annotation.AnimRes; 45 import androidx.annotation.DrawableRes; 46 import androidx.annotation.IntDef; 47 import androidx.annotation.NonNull; 48 import androidx.annotation.Nullable; 49 import androidx.annotation.RawRes; 50 import androidx.annotation.VisibleForTesting; 51 import androidx.appcompat.app.AlertDialog; 52 import androidx.core.util.Preconditions; 53 import androidx.core.widget.TextViewCompat; 54 import androidx.viewpager.widget.PagerAdapter; 55 import androidx.viewpager.widget.ViewPager; 56 57 import com.android.settings.R; 58 import com.android.settings.core.SubSettingLauncher; 59 import com.android.settingslib.widget.LottieColorUtils; 60 61 import com.airbnb.lottie.LottieAnimationView; 62 import com.airbnb.lottie.LottieDrawable; 63 64 import java.lang.annotation.Retention; 65 import java.lang.annotation.RetentionPolicy; 66 import java.util.ArrayList; 67 import java.util.List; 68 69 /** 70 * Utility class for creating the dialog that guides users for gesture navigation for 71 * accessibility services. 72 */ 73 public final class AccessibilityGestureNavigationTutorial { 74 private static final String TAG = "AccessibilityGestureNavigationTutorial"; 75 76 /** IntDef enum for dialog type. */ 77 @Retention(RetentionPolicy.SOURCE) 78 @IntDef({ 79 DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON, 80 DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE, 81 DialogType.GESTURE_NAVIGATION_SETTINGS, 82 }) 83 84 private @interface DialogType { 85 int LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON = 0; 86 int LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE = 1; 87 int GESTURE_NAVIGATION_SETTINGS = 2; 88 } 89 AccessibilityGestureNavigationTutorial()90 private AccessibilityGestureNavigationTutorial() {} 91 92 private static final DialogInterface.OnClickListener mOnClickListener = 93 (DialogInterface dialog, int which) -> dialog.dismiss(); 94 95 /** 96 * Displays a dialog that guides users to use accessibility features with accessibility 97 * gestures under system gesture navigation mode. 98 */ showGestureNavigationTutorialDialog(Context context, DialogInterface.OnDismissListener onDismissListener)99 public static AlertDialog showGestureNavigationTutorialDialog(Context context, 100 DialogInterface.OnDismissListener onDismissListener) { 101 final AlertDialog alertDialog = new AlertDialog.Builder(context) 102 .setView(createTutorialDialogContentView(context, 103 DialogType.GESTURE_NAVIGATION_SETTINGS)) 104 .setPositiveButton(R.string.accessibility_tutorial_dialog_button, mOnClickListener) 105 .setOnDismissListener(onDismissListener) 106 .create(); 107 108 alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); 109 alertDialog.setCanceledOnTouchOutside(false); 110 alertDialog.show(); 111 112 return alertDialog; 113 } 114 showAccessibilityGestureTutorialDialog(Context context)115 static AlertDialog showAccessibilityGestureTutorialDialog(Context context) { 116 return createDialog(context, DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE); 117 } 118 createAccessibilityTutorialDialog(Context context, int shortcutTypes)119 static AlertDialog createAccessibilityTutorialDialog(Context context, int shortcutTypes) { 120 return createAccessibilityTutorialDialog(context, shortcutTypes, mOnClickListener); 121 } 122 createAccessibilityTutorialDialog(Context context, int shortcutTypes, @Nullable DialogInterface.OnClickListener actionButtonListener)123 static AlertDialog createAccessibilityTutorialDialog(Context context, int shortcutTypes, 124 @Nullable DialogInterface.OnClickListener actionButtonListener) { 125 126 final int category = SettingsEnums.SWITCH_SHORTCUT_DIALOG_ACCESSIBILITY_BUTTON_SETTINGS; 127 final DialogInterface.OnClickListener linkButtonListener = 128 (dialog, which) -> new SubSettingLauncher(context) 129 .setDestination(AccessibilityButtonFragment.class.getName()) 130 .setSourceMetricsCategory(category) 131 .launch(); 132 133 final AlertDialog alertDialog = new AlertDialog.Builder(context) 134 .setPositiveButton(R.string.accessibility_tutorial_dialog_button, 135 actionButtonListener) 136 .setNegativeButton(R.string.accessibility_tutorial_dialog_link_button, 137 linkButtonListener) 138 .create(); 139 140 final List<TutorialPage> tutorialPages = 141 createShortcutTutorialPages(context, shortcutTypes); 142 Preconditions.checkArgument(!tutorialPages.isEmpty(), 143 /* errorMessage= */ "Unexpected tutorial pages size"); 144 145 final TutorialPageChangeListener.OnPageSelectedCallback callback = index -> { 146 final int pageType = tutorialPages.get(index).getType(); 147 alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility( 148 pageType == UserShortcutType.SOFTWARE ? VISIBLE : GONE); 149 }; 150 151 alertDialog.setView(createShortcutNavigationContentView(context, tutorialPages, callback)); 152 153 // Showing first page won't invoke onPageSelectedCallback. Need to check the first tutorial 154 // page type manually to set correct visibility of the link button. 155 alertDialog.setOnShowListener(dialog -> { 156 final int firstPageType = tutorialPages.get(0).getType(); 157 alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility( 158 firstPageType == UserShortcutType.SOFTWARE ? VISIBLE : GONE); 159 }); 160 161 return alertDialog; 162 } 163 createAccessibilityTutorialDialogForSetupWizard(Context context, int shortcutTypes)164 static AlertDialog createAccessibilityTutorialDialogForSetupWizard(Context context, 165 int shortcutTypes) { 166 return createAccessibilityTutorialDialogForSetupWizard(context, shortcutTypes, 167 mOnClickListener); 168 } 169 createAccessibilityTutorialDialogForSetupWizard(Context context, int shortcutTypes, @Nullable DialogInterface.OnClickListener actionButtonListener)170 static AlertDialog createAccessibilityTutorialDialogForSetupWizard(Context context, 171 int shortcutTypes, @Nullable DialogInterface.OnClickListener actionButtonListener) { 172 173 final int category = SettingsEnums.SWITCH_SHORTCUT_DIALOG_ACCESSIBILITY_BUTTON_SETTINGS; 174 final DialogInterface.OnClickListener linkButtonListener = 175 (dialog, which) -> new SubSettingLauncher(context) 176 .setDestination(AccessibilityButtonFragment.class.getName()) 177 .setSourceMetricsCategory(category) 178 .launch(); 179 180 final AlertDialog alertDialog = new AlertDialog.Builder(context) 181 .setPositiveButton(R.string.accessibility_tutorial_dialog_button, 182 actionButtonListener) 183 .create(); 184 185 final List<TutorialPage> tutorialPages = 186 createShortcutTutorialPages(context, shortcutTypes); 187 Preconditions.checkArgument(!tutorialPages.isEmpty(), 188 /* errorMessage= */ "Unexpected tutorial pages size"); 189 190 alertDialog.setView(createShortcutNavigationContentView(context, tutorialPages, null)); 191 192 return alertDialog; 193 } 194 195 /** 196 * Gets a content View for a dialog to confirm that they want to enable a service. 197 * 198 * @param context A valid context 199 * @param dialogType The type of tutorial dialog 200 * @return A content view suitable for viewing 201 */ createTutorialDialogContentView(Context context, int dialogType)202 private static View createTutorialDialogContentView(Context context, int dialogType) { 203 final LayoutInflater inflater = (LayoutInflater) context.getSystemService( 204 Context.LAYOUT_INFLATER_SERVICE); 205 206 View content = null; 207 208 switch (dialogType) { 209 case DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON: 210 content = inflater.inflate( 211 R.layout.tutorial_dialog_launch_service_by_accessibility_button, null); 212 break; 213 case DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE: 214 content = inflater.inflate( 215 R.layout.tutorial_dialog_launch_service_by_gesture_navigation, null); 216 setupGestureNavigationTextWithImage(context, content); 217 break; 218 case DialogType.GESTURE_NAVIGATION_SETTINGS: 219 content = inflater.inflate( 220 R.layout.tutorial_dialog_launch_by_gesture_navigation_settings, null); 221 setupGestureNavigationTextWithImage(context, content); 222 break; 223 } 224 225 return content; 226 } 227 setupGestureNavigationTextWithImage(Context context, View view)228 private static void setupGestureNavigationTextWithImage(Context context, View view) { 229 final boolean isTouchExploreEnabled = AccessibilityUtil.isTouchExploreEnabled(context); 230 231 final ImageView imageView = view.findViewById(R.id.image); 232 final int gestureSettingsImageResId = 233 isTouchExploreEnabled ? R.drawable.a11y_gesture_navigation_three_finger_preview 234 : R.drawable.a11y_gesture_navigation_two_finger_preview; 235 imageView.setImageResource(gestureSettingsImageResId); 236 237 final TextView textView = view.findViewById(R.id.gesture_tutorial_message); 238 textView.setText(isTouchExploreEnabled 239 ? R.string.accessibility_tutorial_dialog_message_gesture_settings_talkback 240 : R.string.accessibility_tutorial_dialog_message_gesture_settings); 241 } 242 createDialog(Context context, int dialogType)243 private static AlertDialog createDialog(Context context, int dialogType) { 244 final AlertDialog alertDialog = new AlertDialog.Builder(context) 245 .setView(createTutorialDialogContentView(context, dialogType)) 246 .setPositiveButton(R.string.accessibility_tutorial_dialog_button, mOnClickListener) 247 .create(); 248 249 alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); 250 alertDialog.setCanceledOnTouchOutside(false); 251 alertDialog.show(); 252 253 return alertDialog; 254 } 255 256 private static class TutorialPagerAdapter extends PagerAdapter { 257 private final List<TutorialPage> mTutorialPages; TutorialPagerAdapter(List<TutorialPage> tutorialPages)258 private TutorialPagerAdapter(List<TutorialPage> tutorialPages) { 259 this.mTutorialPages = tutorialPages; 260 } 261 262 @NonNull 263 @Override instantiateItem(@onNull ViewGroup container, int position)264 public Object instantiateItem(@NonNull ViewGroup container, int position) { 265 final View itemView = mTutorialPages.get(position).getIllustrationView(); 266 container.addView(itemView); 267 return itemView; 268 } 269 270 @Override getCount()271 public int getCount() { 272 return mTutorialPages.size(); 273 } 274 275 @Override isViewFromObject(@onNull View view, @NonNull Object o)276 public boolean isViewFromObject(@NonNull View view, @NonNull Object o) { 277 return view == o; 278 } 279 280 @Override destroyItem(@onNull ViewGroup container, int position, @NonNull Object object)281 public void destroyItem(@NonNull ViewGroup container, int position, 282 @NonNull Object object) { 283 final View itemView = mTutorialPages.get(position).getIllustrationView(); 284 container.removeView(itemView); 285 } 286 } 287 createImageView(Context context, int imageRes)288 private static ImageView createImageView(Context context, int imageRes) { 289 final ImageView imageView = new ImageView(context); 290 imageView.setImageResource(imageRes); 291 imageView.setAdjustViewBounds(true); 292 293 return imageView; 294 } 295 createIllustrationView(Context context, @DrawableRes int imageRes)296 private static View createIllustrationView(Context context, @DrawableRes int imageRes) { 297 final View illustrationFrame = inflateAndInitIllustrationFrame(context); 298 final LottieAnimationView lottieView = illustrationFrame.findViewById(R.id.image); 299 lottieView.setImageResource(imageRes); 300 301 return illustrationFrame; 302 } 303 createIllustrationViewWithImageRawResource(Context context, @RawRes int imageRawRes)304 private static View createIllustrationViewWithImageRawResource(Context context, 305 @RawRes int imageRawRes) { 306 final View illustrationFrame = inflateAndInitIllustrationFrame(context); 307 final LottieAnimationView lottieView = illustrationFrame.findViewById(R.id.image); 308 lottieView.setFailureListener( 309 result -> Log.w(TAG, "Invalid image raw resource id: " + imageRawRes, 310 result)); 311 lottieView.setAnimation(imageRawRes); 312 lottieView.setRepeatCount(LottieDrawable.INFINITE); 313 LottieColorUtils.applyDynamicColors(context, lottieView); 314 lottieView.playAnimation(); 315 316 return illustrationFrame; 317 } 318 inflateAndInitIllustrationFrame(Context context)319 private static View inflateAndInitIllustrationFrame(Context context) { 320 final LayoutInflater inflater = context.getSystemService(LayoutInflater.class); 321 322 return inflater.inflate(R.layout.accessibility_lottie_animation_view, /* root= */ null); 323 } 324 createShortcutNavigationContentView(Context context, List<TutorialPage> tutorialPages, TutorialPageChangeListener.OnPageSelectedCallback onPageSelectedCallback)325 private static View createShortcutNavigationContentView(Context context, 326 List<TutorialPage> tutorialPages, 327 TutorialPageChangeListener.OnPageSelectedCallback onPageSelectedCallback) { 328 329 final LayoutInflater inflater = context.getSystemService(LayoutInflater.class); 330 final View contentView = inflater.inflate( 331 R.layout.accessibility_shortcut_tutorial_dialog, /* root= */ null); 332 333 final LinearLayout indicatorContainer = contentView.findViewById(R.id.indicator_container); 334 indicatorContainer.setVisibility(tutorialPages.size() > 1 ? VISIBLE : GONE); 335 for (TutorialPage page : tutorialPages) { 336 indicatorContainer.addView(page.getIndicatorIcon()); 337 } 338 tutorialPages.get(/* firstIndex */ 0).getIndicatorIcon().setEnabled(true); 339 340 final TextSwitcher title = contentView.findViewById(R.id.title); 341 title.setFactory(() -> makeTitleView(context)); 342 title.setText(tutorialPages.get(/* firstIndex */ 0).getTitle()); 343 344 final TextSwitcher instruction = contentView.findViewById(R.id.instruction); 345 instruction.setFactory(() -> makeInstructionView(context)); 346 instruction.setText(tutorialPages.get(/* firstIndex */ 0).getInstruction()); 347 348 final ViewPager viewPager = contentView.findViewById(R.id.view_pager); 349 viewPager.setAdapter(new TutorialPagerAdapter(tutorialPages)); 350 viewPager.setContentDescription(context.getString(R.string.accessibility_tutorial_pager, 351 /* firstPage */ 1, tutorialPages.size())); 352 viewPager.setImportantForAccessibility(tutorialPages.size() > 1 353 ? View.IMPORTANT_FOR_ACCESSIBILITY_YES 354 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 355 356 TutorialPageChangeListener listener = new TutorialPageChangeListener(context, viewPager, 357 title, instruction, tutorialPages); 358 listener.setOnPageSelectedCallback(onPageSelectedCallback); 359 360 return contentView; 361 } 362 makeTitleView(Context context)363 private static View makeTitleView(Context context) { 364 final TextView textView = new TextView(context); 365 // Sets the text color, size, style, hint color, and highlight color from the specified 366 // TextAppearance resource. 367 TextViewCompat.setTextAppearance(textView, R.style.AccessibilityDialogTitle); 368 textView.setGravity(Gravity.CENTER); 369 return textView; 370 } 371 makeInstructionView(Context context)372 private static View makeInstructionView(Context context) { 373 final TextView textView = new TextView(context); 374 TextViewCompat.setTextAppearance(textView, R.style.AccessibilityDialogDescription); 375 return textView; 376 } 377 createSoftwareTutorialPage(@onNull Context context)378 private static TutorialPage createSoftwareTutorialPage(@NonNull Context context) { 379 final int type = UserShortcutType.SOFTWARE; 380 final CharSequence title = getSoftwareTitle(context); 381 final View image = createSoftwareImage(context); 382 final CharSequence instruction = getSoftwareInstruction(context); 383 final ImageView indicatorIcon = 384 createImageView(context, R.drawable.ic_accessibility_page_indicator); 385 indicatorIcon.setEnabled(false); 386 387 return new TutorialPage(type, title, image, indicatorIcon, instruction); 388 } 389 createHardwareTutorialPage(@onNull Context context)390 private static TutorialPage createHardwareTutorialPage(@NonNull Context context) { 391 final int type = UserShortcutType.HARDWARE; 392 final CharSequence title = 393 context.getText(R.string.accessibility_tutorial_dialog_title_volume); 394 final View image = 395 createIllustrationView(context, R.drawable.a11y_shortcut_type_hardware); 396 final ImageView indicatorIcon = 397 createImageView(context, R.drawable.ic_accessibility_page_indicator); 398 final CharSequence instruction = 399 context.getText(R.string.accessibility_tutorial_dialog_message_volume); 400 indicatorIcon.setEnabled(false); 401 402 return new TutorialPage(type, title, image, indicatorIcon, instruction); 403 } 404 createTripleTapTutorialPage(@onNull Context context)405 private static TutorialPage createTripleTapTutorialPage(@NonNull Context context) { 406 final int type = UserShortcutType.TRIPLETAP; 407 final CharSequence title = 408 context.getText(R.string.accessibility_tutorial_dialog_title_triple); 409 final View image = 410 createIllustrationViewWithImageRawResource(context, 411 R.raw.a11y_shortcut_type_triple_tap); 412 final CharSequence instruction = 413 context.getText(R.string.accessibility_tutorial_dialog_message_triple); 414 final ImageView indicatorIcon = 415 createImageView(context, R.drawable.ic_accessibility_page_indicator); 416 indicatorIcon.setEnabled(false); 417 418 return new TutorialPage(type, title, image, indicatorIcon, instruction); 419 } 420 421 @VisibleForTesting createShortcutTutorialPages(@onNull Context context, int shortcutTypes)422 static List<TutorialPage> createShortcutTutorialPages(@NonNull Context context, 423 int shortcutTypes) { 424 final List<TutorialPage> tutorialPages = new ArrayList<>(); 425 if ((shortcutTypes & UserShortcutType.SOFTWARE) == UserShortcutType.SOFTWARE) { 426 tutorialPages.add(createSoftwareTutorialPage(context)); 427 } 428 429 if ((shortcutTypes & UserShortcutType.HARDWARE) == UserShortcutType.HARDWARE) { 430 tutorialPages.add(createHardwareTutorialPage(context)); 431 } 432 433 if ((shortcutTypes & UserShortcutType.TRIPLETAP) == UserShortcutType.TRIPLETAP) { 434 tutorialPages.add(createTripleTapTutorialPage(context)); 435 } 436 437 return tutorialPages; 438 } 439 createSoftwareImage(Context context)440 private static View createSoftwareImage(Context context) { 441 int resId; 442 if (AccessibilityUtil.isFloatingMenuEnabled(context)) { 443 resId = R.drawable.a11y_shortcut_type_software_floating; 444 } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { 445 resId = AccessibilityUtil.isTouchExploreEnabled(context) 446 ? R.drawable.a11y_shortcut_type_software_gesture_talkback 447 : R.drawable.a11y_shortcut_type_software_gesture; 448 } else { 449 resId = R.drawable.a11y_shortcut_type_software; 450 } 451 return createIllustrationView(context, resId); 452 } 453 getSoftwareTitle(Context context)454 private static CharSequence getSoftwareTitle(Context context) { 455 int resId; 456 if (AccessibilityUtil.isFloatingMenuEnabled(context)) { 457 resId = R.string.accessibility_tutorial_dialog_title_button; 458 } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { 459 resId = R.string.accessibility_tutorial_dialog_title_gesture; 460 } else { 461 resId = R.string.accessibility_tutorial_dialog_title_button; 462 } 463 return context.getText(resId); 464 } 465 getSoftwareInstruction(Context context)466 private static CharSequence getSoftwareInstruction(Context context) { 467 final SpannableStringBuilder sb = new SpannableStringBuilder(); 468 if (AccessibilityUtil.isFloatingMenuEnabled(context)) { 469 final int resId = R.string.accessibility_tutorial_dialog_message_floating_button; 470 sb.append(context.getText(resId)); 471 } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { 472 final int resId = AccessibilityUtil.isTouchExploreEnabled(context) 473 ? R.string.accessibility_tutorial_dialog_message_gesture_talkback 474 : R.string.accessibility_tutorial_dialog_message_gesture; 475 sb.append(context.getText(resId)); 476 } else { 477 final int resId = R.string.accessibility_tutorial_dialog_message_button; 478 sb.append(getSoftwareInstructionWithIcon(context, context.getText(resId))); 479 } 480 return sb; 481 } 482 getSoftwareInstructionWithIcon(Context context, CharSequence text)483 private static CharSequence getSoftwareInstructionWithIcon(Context context, CharSequence text) { 484 final String message = text.toString(); 485 final SpannableString spannableInstruction = SpannableString.valueOf(message); 486 final int indexIconStart = message.indexOf("%s"); 487 final int indexIconEnd = indexIconStart + 2; 488 final ImageView iconView = new ImageView(context); 489 iconView.setImageDrawable(context.getDrawable(R.drawable.ic_accessibility_new)); 490 final Drawable icon = iconView.getDrawable().mutate(); 491 final ImageSpan imageSpan = new ImageSpan(icon); 492 imageSpan.setContentDescription(""); 493 icon.setBounds(/* left= */ 0, /* top= */ 0, 494 icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); 495 spannableInstruction.setSpan(imageSpan, indexIconStart, indexIconEnd, 496 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 497 498 return spannableInstruction; 499 } 500 501 private static class TutorialPage { 502 private final int mType; 503 private final CharSequence mTitle; 504 private final View mIllustrationView; 505 private final ImageView mIndicatorIcon; 506 private final CharSequence mInstruction; 507 TutorialPage(int type, CharSequence title, View illustrationView, ImageView indicatorIcon, CharSequence instruction)508 TutorialPage(int type, CharSequence title, View illustrationView, ImageView indicatorIcon, 509 CharSequence instruction) { 510 this.mType = type; 511 this.mTitle = title; 512 this.mIllustrationView = illustrationView; 513 this.mIndicatorIcon = indicatorIcon; 514 this.mInstruction = instruction; 515 516 setupIllustrationChildViewsGravity(); 517 } 518 getType()519 public int getType() { 520 return mType; 521 } 522 getTitle()523 public CharSequence getTitle() { 524 return mTitle; 525 } 526 getIllustrationView()527 public View getIllustrationView() { 528 return mIllustrationView; 529 } 530 getIndicatorIcon()531 public ImageView getIndicatorIcon() { 532 return mIndicatorIcon; 533 } 534 getInstruction()535 public CharSequence getInstruction() { 536 return mInstruction; 537 } 538 setupIllustrationChildViewsGravity()539 private void setupIllustrationChildViewsGravity() { 540 final View backgroundView = mIllustrationView.findViewById(R.id.image_background); 541 initViewGravity(backgroundView); 542 543 final View lottieView = mIllustrationView.findViewById(R.id.image); 544 initViewGravity(lottieView); 545 } 546 initViewGravity(@onNull View view)547 private void initViewGravity(@NonNull View view) { 548 final FrameLayout.LayoutParams layoutParams = 549 new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, 550 FrameLayout.LayoutParams.WRAP_CONTENT); 551 layoutParams.gravity = Gravity.CENTER; 552 553 view.setLayoutParams(layoutParams); 554 } 555 } 556 557 private static class TutorialPageChangeListener implements ViewPager.OnPageChangeListener { 558 private int mLastTutorialPagePosition = 0; 559 private final Context mContext; 560 private final TextSwitcher mTitle; 561 private final TextSwitcher mInstruction; 562 private final List<TutorialPage> mTutorialPages; 563 private final ViewPager mViewPager; 564 private OnPageSelectedCallback mOnPageSelectedCallback; 565 TutorialPageChangeListener(Context context, ViewPager viewPager, ViewGroup title, ViewGroup instruction, List<TutorialPage> tutorialPages)566 TutorialPageChangeListener(Context context, ViewPager viewPager, ViewGroup title, 567 ViewGroup instruction, List<TutorialPage> tutorialPages) { 568 this.mContext = context; 569 this.mViewPager = viewPager; 570 this.mTitle = (TextSwitcher) title; 571 this.mInstruction = (TextSwitcher) instruction; 572 this.mTutorialPages = tutorialPages; 573 this.mOnPageSelectedCallback = null; 574 575 this.mViewPager.addOnPageChangeListener(this); 576 } 577 setOnPageSelectedCallback( OnPageSelectedCallback callback)578 public void setOnPageSelectedCallback( 579 OnPageSelectedCallback callback) { 580 this.mOnPageSelectedCallback = callback; 581 } 582 583 @Override onPageScrolled(int position, float positionOffset, int positionOffsetPixels)584 public void onPageScrolled(int position, float positionOffset, 585 int positionOffsetPixels) { 586 // Do nothing. 587 } 588 589 @Override onPageSelected(int position)590 public void onPageSelected(int position) { 591 final boolean isPreviousPosition = 592 mLastTutorialPagePosition > position; 593 @AnimRes 594 final int inAnimationResId = isPreviousPosition 595 ? android.R.anim.slide_in_left 596 : com.android.internal.R.anim.slide_in_right; 597 598 @AnimRes 599 final int outAnimationResId = isPreviousPosition 600 ? android.R.anim.slide_out_right 601 : com.android.internal.R.anim.slide_out_left; 602 603 mTitle.setInAnimation(mContext, inAnimationResId); 604 mTitle.setOutAnimation(mContext, outAnimationResId); 605 mTitle.setText(mTutorialPages.get(position).getTitle()); 606 607 mInstruction.setInAnimation(mContext, inAnimationResId); 608 mInstruction.setOutAnimation(mContext, outAnimationResId); 609 mInstruction.setText(mTutorialPages.get(position).getInstruction()); 610 611 for (TutorialPage page : mTutorialPages) { 612 page.getIndicatorIcon().setEnabled(false); 613 } 614 mTutorialPages.get(position).getIndicatorIcon().setEnabled(true); 615 mLastTutorialPagePosition = position; 616 617 final int currentPageNumber = position + 1; 618 mViewPager.setContentDescription( 619 mContext.getString(R.string.accessibility_tutorial_pager, 620 currentPageNumber, mTutorialPages.size())); 621 622 if (mOnPageSelectedCallback != null) { 623 mOnPageSelectedCallback.onPageSelected(position); 624 } 625 } 626 627 @Override onPageScrollStateChanged(int state)628 public void onPageScrollStateChanged(int state) { 629 // Do nothing. 630 } 631 632 /** The interface that provides a callback method after tutorial page is selected. */ 633 private interface OnPageSelectedCallback { 634 635 /** The callback method after tutorial page is selected. */ onPageSelected(int index)636 void onPageSelected(int index); 637 } 638 } 639 } 640