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