1 // CHECKSTYLE:OFF Generated code
2 /* This file is auto-generated from OnboardingSupportFragment.java.  DO NOT MODIFY. */
3 
4 /*
5  * Copyright (C) 2015 The Android Open Source Project
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 
20 package androidx.leanback.app;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorInflater;
24 import android.animation.AnimatorListenerAdapter;
25 import android.animation.AnimatorSet;
26 import android.animation.ObjectAnimator;
27 import android.animation.TimeInterpolator;
28 import android.app.Fragment;
29 import android.content.Context;
30 import android.graphics.Color;
31 import android.os.Bundle;
32 import android.util.Log;
33 import android.util.TypedValue;
34 import android.view.ContextThemeWrapper;
35 import android.view.Gravity;
36 import android.view.KeyEvent;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.View.OnClickListener;
40 import android.view.View.OnKeyListener;
41 import android.view.ViewGroup;
42 import android.view.ViewTreeObserver.OnPreDrawListener;
43 import android.view.animation.AccelerateInterpolator;
44 import android.view.animation.DecelerateInterpolator;
45 import android.widget.Button;
46 import android.widget.ImageView;
47 import android.widget.TextView;
48 
49 import androidx.annotation.ColorInt;
50 import androidx.annotation.NonNull;
51 import androidx.annotation.Nullable;
52 import androidx.leanback.R;
53 import androidx.leanback.widget.PagingIndicator;
54 
55 import java.util.ArrayList;
56 import java.util.List;
57 
58 /**
59  * An OnboardingFragment provides a common and simple way to build onboarding screen for
60  * applications.
61  * <p>
62  * <h3>Building the screen</h3>
63  * The view structure of onboarding screen is composed of the common parts and custom parts. The
64  * common parts are composed of icon, title, description and page navigator and the custom parts
65  * are composed of background, contents and foreground.
66  * <p>
67  * To build the screen views, the inherited class should override:
68  * <ul>
69  * <li>{@link #onCreateBackgroundView} to provide the background view. Background view has the same
70  * size as the screen and the lowest z-order.</li>
71  * <li>{@link #onCreateContentView} to provide the contents view. The content view is located in
72  * the content area at the center of the screen.</li>
73  * <li>{@link #onCreateForegroundView} to provide the foreground view. Foreground view has the same
74  * size as the screen and the highest z-order</li>
75  * </ul>
76  * <p>
77  * Each of these methods can return {@code null} if the application doesn't want to provide it.
78  * <p>
79  * <h3>Page information</h3>
80  * The onboarding screen may have several pages which explain the functionality of the application.
81  * The inherited class should provide the page information by overriding the methods:
82  * <p>
83  * <ul>
84  * <li>{@link #getPageCount} to provide the number of pages.</li>
85  * <li>{@link #getPageTitle} to provide the title of the page.</li>
86  * <li>{@link #getPageDescription} to provide the description of the page.</li>
87  * </ul>
88  * <p>
89  * Note that the information is used in {@link #onCreateView}, so should be initialized before
90  * calling {@code super.onCreateView}.
91  * <p>
92  * <h3>Animation</h3>
93  * Onboarding screen has three kinds of animations:
94  * <p>
95  * <h4>Logo Splash Animation</a></h4>
96  * When onboarding screen appears, the logo splash animation is played by default. The animation
97  * fades in the logo image, pauses in a few seconds and fades it out.
98  * <p>
99  * In most cases, the logo animation needs to be customized because the logo images of applications
100  * are different from each other, or some applications may want to show their own animations.
101  * <p>
102  * The logo animation can be customized in two ways:
103  * <ul>
104  * <li>The simplest way is to provide the logo image by calling {@link #setLogoResourceId} to show
105  * the default logo animation. This method should be called in {@link Fragment#onCreateView}.</li>
106  * <li>If the logo animation is complex, then override {@link #onCreateLogoAnimation} and return the
107  * {@link Animator} object to run.</li>
108  * </ul>
109  * <p>
110  * If the inherited class provides neither the logo image nor the animation, the logo animation will
111  * be omitted.
112  * <h4>Page enter animation</h4>
113  * After logo animation finishes, page enter animation starts, which causes the header section -
114  * title and description views to fade and slide in. Users can override the default
115  * fade + slide animation by overriding {@link #onCreateTitleAnimator()} &
116  * {@link #onCreateDescriptionAnimator()}. By default we don't animate the custom views but users
117  * can provide animation by overriding {@link #onCreateEnterAnimation}.
118  *
119  * <h4>Page change animation</h4>
120  * When the page changes, the default animations of the title and description are played. The
121  * inherited class can override {@link #onPageChanged} to start the custom animations.
122  * <p>
123  * <h3>Finishing the screen</h3>
124  * <p>
125  * If the user finishes the onboarding screen after navigating all the pages,
126  * {@link #onFinishFragment} is called. The inherited class can override this method to show another
127  * fragment or activity, or just remove this fragment.
128  * <p>
129  * <h3>Theming</h3>
130  * <p>
131  * OnboardingFragment must have access to an appropriate theme. Specifically, the fragment must
132  * receive  {@link R.style#Theme_Leanback_Onboarding}, or a theme whose parent is set to that theme.
133  * Themes can be provided in one of three ways:
134  * <ul>
135  * <li>The simplest way is to set the theme for the host Activity to the Onboarding theme or a theme
136  * that derives from it.</li>
137  * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the
138  * existing Activity theme can have an entry added for the attribute
139  * {@link R.styleable#LeanbackOnboardingTheme_onboardingTheme}. If present, this theme will be used
140  * by OnboardingFragment as an overlay to the Activity's theme.</li>
141  * <li>Finally, custom subclasses of OnboardingFragment may provide a theme through the
142  * {@link #onProvideTheme} method. This can be useful if a subclass is used across multiple
143  * Activities.</li>
144  * </ul>
145  * <p>
146  * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by
147  * the Activity's theme. (Themes whose parent theme is already set to the onboarding theme do not
148  * need to set the onboardingTheme attribute; if set, it will be ignored.)
149  *
150  * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTheme
151  * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingHeaderStyle
152  * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTitleStyle
153  * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingDescriptionStyle
154  * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingNavigatorContainerStyle
155  * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingPageIndicatorStyle
156  * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle
157  * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingLogoStyle
158  * @deprecated use {@link OnboardingSupportFragment}
159  */
160 @Deprecated
161 abstract public class OnboardingFragment extends Fragment {
162     private static final String TAG = "OnboardingF";
163     private static final boolean DEBUG = false;
164 
165     private static final long LOGO_SPLASH_PAUSE_DURATION_MS = 1333;
166 
167     private static final long HEADER_ANIMATION_DURATION_MS = 417;
168     private static final long DESCRIPTION_START_DELAY_MS = 33;
169     private static final long HEADER_APPEAR_DELAY_MS = 500;
170     private static final int SLIDE_DISTANCE = 60;
171 
172     private static int sSlideDistance;
173 
174     private static final TimeInterpolator HEADER_APPEAR_INTERPOLATOR = new DecelerateInterpolator();
175     private static final TimeInterpolator HEADER_DISAPPEAR_INTERPOLATOR =
176             new AccelerateInterpolator();
177 
178     // Keys used to save and restore the states.
179     private static final String KEY_CURRENT_PAGE_INDEX = "leanback.onboarding.current_page_index";
180     private static final String KEY_LOGO_ANIMATION_FINISHED =
181             "leanback.onboarding.logo_animation_finished";
182     private static final String KEY_ENTER_ANIMATION_FINISHED =
183             "leanback.onboarding.enter_animation_finished";
184 
185     private ContextThemeWrapper mThemeWrapper;
186 
187     PagingIndicator mPageIndicator;
188     View mStartButton;
189     private ImageView mLogoView;
190     // Optional icon that can be displayed on top of the header section.
191     private ImageView mMainIconView;
192     private int mIconResourceId;
193 
194     TextView mTitleView;
195     TextView mDescriptionView;
196 
197     boolean mIsLtr;
198 
199     // No need to save/restore the logo resource ID, because the logo animation will not appear when
200     // the fragment is restored.
201     private int mLogoResourceId;
202     boolean mLogoAnimationFinished;
203     boolean mEnterAnimationFinished;
204     int mCurrentPageIndex;
205 
206     @ColorInt
207     private int mTitleViewTextColor = Color.TRANSPARENT;
208     private boolean mTitleViewTextColorSet;
209 
210     @ColorInt
211     private int mDescriptionViewTextColor = Color.TRANSPARENT;
212     private boolean mDescriptionViewTextColorSet;
213 
214     @ColorInt
215     private int mDotBackgroundColor = Color.TRANSPARENT;
216     private boolean mDotBackgroundColorSet;
217 
218     @ColorInt
219     private int mArrowColor = Color.TRANSPARENT;
220     private boolean mArrowColorSet;
221 
222     @ColorInt
223     private int mArrowBackgroundColor = Color.TRANSPARENT;
224     private boolean mArrowBackgroundColorSet;
225 
226     private CharSequence mStartButtonText;
227     private boolean mStartButtonTextSet;
228 
229 
230     private AnimatorSet mAnimator;
231 
232     private final OnClickListener mOnClickListener = new OnClickListener() {
233         @Override
234         public void onClick(View view) {
235             if (!mLogoAnimationFinished) {
236                 // Do not change page until the enter transition finishes.
237                 return;
238             }
239             if (mCurrentPageIndex == getPageCount() - 1) {
240                 onFinishFragment();
241             } else {
242                 moveToNextPage();
243             }
244         }
245     };
246 
247     private final OnKeyListener mOnKeyListener = new OnKeyListener() {
248         @Override
249         public boolean onKey(View v, int keyCode, KeyEvent event) {
250             if (!mLogoAnimationFinished) {
251                 // Ignore key event until the enter transition finishes.
252                 return keyCode != KeyEvent.KEYCODE_BACK;
253             }
254             if (event.getAction() == KeyEvent.ACTION_DOWN) {
255                 return false;
256             }
257             switch (keyCode) {
258                 case KeyEvent.KEYCODE_BACK:
259                     if (mCurrentPageIndex == 0) {
260                         return false;
261                     }
262                     moveToPreviousPage();
263                     return true;
264                 case KeyEvent.KEYCODE_DPAD_LEFT:
265                     if (mIsLtr) {
266                         moveToPreviousPage();
267                     } else {
268                         moveToNextPage();
269                     }
270                     return true;
271                 case KeyEvent.KEYCODE_DPAD_RIGHT:
272                     if (mIsLtr) {
273                         moveToNextPage();
274                     } else {
275                         moveToPreviousPage();
276                     }
277                     return true;
278             }
279             return false;
280         }
281     };
282 
283     /**
284      * Navigates to the previous page.
285      */
moveToPreviousPage()286     protected void moveToPreviousPage() {
287         if (!mLogoAnimationFinished) {
288             // Ignore if the logo enter transition is in progress.
289             return;
290         }
291         if (mCurrentPageIndex > 0) {
292             --mCurrentPageIndex;
293             onPageChangedInternal(mCurrentPageIndex + 1);
294         }
295     }
296 
297     /**
298      * Navigates to the next page.
299      */
moveToNextPage()300     protected void moveToNextPage() {
301         if (!mLogoAnimationFinished) {
302             // Ignore if the logo enter transition is in progress.
303             return;
304         }
305         if (mCurrentPageIndex < getPageCount() - 1) {
306             ++mCurrentPageIndex;
307             onPageChangedInternal(mCurrentPageIndex - 1);
308         }
309     }
310 
311     @Nullable
312     @Override
onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState)313     public View onCreateView(LayoutInflater inflater, final ViewGroup container,
314             Bundle savedInstanceState) {
315         resolveTheme();
316         LayoutInflater localInflater = getThemeInflater(inflater);
317         final ViewGroup view = (ViewGroup) localInflater.inflate(R.layout.lb_onboarding_fragment,
318                 container, false);
319         mIsLtr = getResources().getConfiguration().getLayoutDirection()
320                 == View.LAYOUT_DIRECTION_LTR;
321         mPageIndicator = (PagingIndicator) view.findViewById(R.id.page_indicator);
322         mPageIndicator.setOnClickListener(mOnClickListener);
323         mPageIndicator.setOnKeyListener(mOnKeyListener);
324         mStartButton = view.findViewById(R.id.button_start);
325         mStartButton.setOnClickListener(mOnClickListener);
326         mStartButton.setOnKeyListener(mOnKeyListener);
327         mMainIconView = (ImageView) view.findViewById(R.id.main_icon);
328         mLogoView = (ImageView) view.findViewById(R.id.logo);
329         mTitleView = (TextView) view.findViewById(R.id.title);
330         mDescriptionView = (TextView) view.findViewById(R.id.description);
331 
332         if (mTitleViewTextColorSet) {
333             mTitleView.setTextColor(mTitleViewTextColor);
334         }
335         if (mDescriptionViewTextColorSet) {
336             mDescriptionView.setTextColor(mDescriptionViewTextColor);
337         }
338         if (mDotBackgroundColorSet) {
339             mPageIndicator.setDotBackgroundColor(mDotBackgroundColor);
340         }
341         if (mArrowColorSet) {
342             mPageIndicator.setArrowColor(mArrowColor);
343         }
344         if (mArrowBackgroundColorSet) {
345             mPageIndicator.setDotBackgroundColor(mArrowBackgroundColor);
346         }
347         if (mStartButtonTextSet) {
348             ((Button) mStartButton).setText(mStartButtonText);
349         }
350         final Context context = FragmentUtil.getContext(OnboardingFragment.this);
351         if (sSlideDistance == 0) {
352             sSlideDistance = (int) (SLIDE_DISTANCE * context.getResources()
353                     .getDisplayMetrics().scaledDensity);
354         }
355         view.requestFocus();
356         return view;
357     }
358 
359     @Override
onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)360     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
361         super.onViewCreated(view, savedInstanceState);
362         if (savedInstanceState == null) {
363             mCurrentPageIndex = 0;
364             mLogoAnimationFinished = false;
365             mEnterAnimationFinished = false;
366             mPageIndicator.onPageSelected(0, false);
367             view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
368                 @Override
369                 public boolean onPreDraw() {
370                     getView().getViewTreeObserver().removeOnPreDrawListener(this);
371                     if (!startLogoAnimation()) {
372                         mLogoAnimationFinished = true;
373                         onLogoAnimationFinished();
374                     }
375                     return true;
376                 }
377             });
378         } else {
379             mCurrentPageIndex = savedInstanceState.getInt(KEY_CURRENT_PAGE_INDEX);
380             mLogoAnimationFinished = savedInstanceState.getBoolean(KEY_LOGO_ANIMATION_FINISHED);
381             mEnterAnimationFinished = savedInstanceState.getBoolean(KEY_ENTER_ANIMATION_FINISHED);
382             if (!mLogoAnimationFinished) {
383                 // logo animation wasn't started or was interrupted when the activity was destroyed;
384                 // restart it againl
385                 if (!startLogoAnimation()) {
386                     mLogoAnimationFinished = true;
387                     onLogoAnimationFinished();
388                 }
389             } else {
390                 onLogoAnimationFinished();
391             }
392         }
393     }
394 
395     @Override
onSaveInstanceState(Bundle outState)396     public void onSaveInstanceState(Bundle outState) {
397         super.onSaveInstanceState(outState);
398         outState.putInt(KEY_CURRENT_PAGE_INDEX, mCurrentPageIndex);
399         outState.putBoolean(KEY_LOGO_ANIMATION_FINISHED, mLogoAnimationFinished);
400         outState.putBoolean(KEY_ENTER_ANIMATION_FINISHED, mEnterAnimationFinished);
401     }
402 
403     /**
404      * Sets the text color for TitleView. If not set, the default textColor set in style
405      * referenced by attr {@link R.attr#onboardingTitleStyle} will be used.
406      * @param color the color to use as the text color for TitleView
407      */
setTitleViewTextColor(@olorInt int color)408     public void setTitleViewTextColor(@ColorInt int color) {
409         mTitleViewTextColor = color;
410         mTitleViewTextColorSet = true;
411         if (mTitleView != null) {
412             mTitleView.setTextColor(color);
413         }
414     }
415 
416     /**
417      * Returns the text color of TitleView if it's set through
418      * {@link #setTitleViewTextColor(int)}. If no color was set, transparent is returned.
419      */
420     @ColorInt
getTitleViewTextColor()421     public final int getTitleViewTextColor() {
422         return mTitleViewTextColor;
423     }
424 
425     /**
426      * Sets the text color for DescriptionView. If not set, the default textColor set in style
427      * referenced by attr {@link R.attr#onboardingDescriptionStyle} will be used.
428      * @param color the color to use as the text color for DescriptionView
429      */
setDescriptionViewTextColor(@olorInt int color)430     public void setDescriptionViewTextColor(@ColorInt int color) {
431         mDescriptionViewTextColor = color;
432         mDescriptionViewTextColorSet = true;
433         if (mDescriptionView != null) {
434             mDescriptionView.setTextColor(color);
435         }
436     }
437 
438     /**
439      * Returns the text color of DescriptionView if it's set through
440      * {@link #setDescriptionViewTextColor(int)}. If no color was set, transparent is returned.
441      */
442     @ColorInt
getDescriptionViewTextColor()443     public final int getDescriptionViewTextColor() {
444         return mDescriptionViewTextColor;
445     }
446     /**
447      * Sets the background color of the dots. If not set, the default color from attr
448      * {@link R.styleable#PagingIndicator_dotBgColor} in the theme will be used.
449      * @param color the color to use for dot backgrounds
450      */
setDotBackgroundColor(@olorInt int color)451     public void setDotBackgroundColor(@ColorInt int color) {
452         mDotBackgroundColor = color;
453         mDotBackgroundColorSet = true;
454         if (mPageIndicator != null) {
455             mPageIndicator.setDotBackgroundColor(color);
456         }
457     }
458 
459     /**
460      * Returns the background color of the dot if it's set through
461      * {@link #setDotBackgroundColor(int)}. If no color was set, transparent is returned.
462      */
463     @ColorInt
getDotBackgroundColor()464     public final int getDotBackgroundColor() {
465         return mDotBackgroundColor;
466     }
467 
468     /**
469      * Sets the color of the arrow. This color will supersede the color set in the theme attribute
470      * {@link R.styleable#PagingIndicator_arrowColor} if provided. If none of these two are set, the
471      * arrow will have its original bitmap color.
472      *
473      * @param color the color to use for arrow background
474      */
setArrowColor(@olorInt int color)475     public void setArrowColor(@ColorInt int color) {
476         mArrowColor = color;
477         mArrowColorSet = true;
478         if (mPageIndicator != null) {
479             mPageIndicator.setArrowColor(color);
480         }
481     }
482 
483     /**
484      * Returns the color of the arrow if it's set through
485      * {@link #setArrowColor(int)}. If no color was set, transparent is returned.
486      */
487     @ColorInt
getArrowColor()488     public final int getArrowColor() {
489         return mArrowColor;
490     }
491 
492     /**
493      * Sets the background color of the arrow. If not set, the default color from attr
494      * {@link R.styleable#PagingIndicator_arrowBgColor} in the theme will be used.
495      * @param color the color to use for arrow background
496      */
setArrowBackgroundColor(@olorInt int color)497     public void setArrowBackgroundColor(@ColorInt int color) {
498         mArrowBackgroundColor = color;
499         mArrowBackgroundColorSet = true;
500         if (mPageIndicator != null) {
501             mPageIndicator.setArrowBackgroundColor(color);
502         }
503     }
504 
505     /**
506      * Returns the background color of the arrow if it's set through
507      * {@link #setArrowBackgroundColor(int)}. If no color was set, transparent is returned.
508      */
509     @ColorInt
getArrowBackgroundColor()510     public final int getArrowBackgroundColor() {
511         return mArrowBackgroundColor;
512     }
513 
514     /**
515      * Returns the start button text if it's set through
516      * {@link #setStartButtonText(CharSequence)}}. If no string was set, null is returned.
517      */
getStartButtonText()518     public final CharSequence getStartButtonText() {
519         return mStartButtonText;
520     }
521 
522     /**
523      * Sets the text on the start button text. If not set, the default text set in
524      * {@link R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle} will be used.
525      *
526      * @param text the start button text
527      */
setStartButtonText(CharSequence text)528     public void setStartButtonText(CharSequence text) {
529         mStartButtonText = text;
530         mStartButtonTextSet = true;
531         if (mStartButton != null) {
532             ((Button) mStartButton).setText(mStartButtonText);
533         }
534     }
535 
536     /**
537      * Returns the theme used for styling the fragment. The default returns -1, indicating that the
538      * host Activity's theme should be used.
539      *
540      * @return The theme resource ID of the theme to use in this fragment, or -1 to use the host
541      *         Activity's theme.
542      */
onProvideTheme()543     public int onProvideTheme() {
544         return -1;
545     }
546 
resolveTheme()547     private void resolveTheme() {
548         final Context context = FragmentUtil.getContext(OnboardingFragment.this);
549         int theme = onProvideTheme();
550         if (theme == -1) {
551             // Look up the onboardingTheme in the activity's currently specified theme. If it
552             // exists, wrap the theme with its value.
553             int resId = R.attr.onboardingTheme;
554             TypedValue typedValue = new TypedValue();
555             boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
556             if (DEBUG) Log.v(TAG, "Found onboarding theme reference? " + found);
557             if (found) {
558                 mThemeWrapper = new ContextThemeWrapper(context, typedValue.resourceId);
559             }
560         } else {
561             mThemeWrapper = new ContextThemeWrapper(context, theme);
562         }
563     }
564 
getThemeInflater(LayoutInflater inflater)565     private LayoutInflater getThemeInflater(LayoutInflater inflater) {
566         return mThemeWrapper == null ? inflater : inflater.cloneInContext(mThemeWrapper);
567     }
568 
569     /**
570      * Sets the resource ID of the splash logo image. If the logo resource id set, the default logo
571      * splash animation will be played.
572      *
573      * @param id The resource ID of the logo image.
574      */
setLogoResourceId(int id)575     public final void setLogoResourceId(int id) {
576         mLogoResourceId = id;
577     }
578 
579     /**
580      * Returns the resource ID of the splash logo image.
581      *
582      * @return The resource ID of the splash logo image.
583      */
getLogoResourceId()584     public final int getLogoResourceId() {
585         return mLogoResourceId;
586     }
587 
588     /**
589      * Called to have the inherited class create its own logo animation.
590      * <p>
591      * This is called only if the logo image resource ID is not set by {@link #setLogoResourceId}.
592      * If this returns {@code null}, the logo animation is skipped.
593      *
594      * @return The {@link Animator} object which runs the logo animation.
595      */
596     @Nullable
onCreateLogoAnimation()597     protected Animator onCreateLogoAnimation() {
598         return null;
599     }
600 
startLogoAnimation()601     boolean startLogoAnimation() {
602         final Context context = FragmentUtil.getContext(OnboardingFragment.this);
603         if (context == null) {
604             return false;
605         }
606         Animator animator = null;
607         if (mLogoResourceId != 0) {
608             mLogoView.setVisibility(View.VISIBLE);
609             mLogoView.setImageResource(mLogoResourceId);
610             Animator inAnimator = AnimatorInflater.loadAnimator(context,
611                     R.animator.lb_onboarding_logo_enter);
612             Animator outAnimator = AnimatorInflater.loadAnimator(context,
613                     R.animator.lb_onboarding_logo_exit);
614             outAnimator.setStartDelay(LOGO_SPLASH_PAUSE_DURATION_MS);
615             AnimatorSet logoAnimator = new AnimatorSet();
616             logoAnimator.playSequentially(inAnimator, outAnimator);
617             logoAnimator.setTarget(mLogoView);
618             animator = logoAnimator;
619         } else {
620             animator = onCreateLogoAnimation();
621         }
622         if (animator != null) {
623             animator.addListener(new AnimatorListenerAdapter() {
624                 @Override
625                 public void onAnimationEnd(Animator animation) {
626                     if (context != null) {
627                         mLogoAnimationFinished = true;
628                         onLogoAnimationFinished();
629                     }
630                 }
631             });
632             animator.start();
633             return true;
634         }
635         return false;
636     }
637 
638     /**
639      * Called to have the inherited class create its enter animation. The start animation runs after
640      * logo animation ends.
641      *
642      * @return The {@link Animator} object which runs the page enter animation.
643      */
644     @Nullable
onCreateEnterAnimation()645     protected Animator onCreateEnterAnimation() {
646         return null;
647     }
648 
649 
650     /**
651      * Hides the logo view and makes other fragment views visible. Also initializes the texts for
652      * Title and Description views.
653      */
hideLogoView()654     void hideLogoView() {
655         mLogoView.setVisibility(View.GONE);
656 
657         if (mIconResourceId != 0) {
658             mMainIconView.setImageResource(mIconResourceId);
659             mMainIconView.setVisibility(View.VISIBLE);
660         }
661 
662         View container = getView();
663         // Create custom views.
664         LayoutInflater inflater = getThemeInflater(LayoutInflater.from(
665                 FragmentUtil.getContext(OnboardingFragment.this)));
666         ViewGroup backgroundContainer = (ViewGroup) container.findViewById(
667                 R.id.background_container);
668         View background = onCreateBackgroundView(inflater, backgroundContainer);
669         if (background != null) {
670             backgroundContainer.setVisibility(View.VISIBLE);
671             backgroundContainer.addView(background);
672         }
673         ViewGroup contentContainer = (ViewGroup) container.findViewById(R.id.content_container);
674         View content = onCreateContentView(inflater, contentContainer);
675         if (content != null) {
676             contentContainer.setVisibility(View.VISIBLE);
677             contentContainer.addView(content);
678         }
679         ViewGroup foregroundContainer = (ViewGroup) container.findViewById(
680                 R.id.foreground_container);
681         View foreground = onCreateForegroundView(inflater, foregroundContainer);
682         if (foreground != null) {
683             foregroundContainer.setVisibility(View.VISIBLE);
684             foregroundContainer.addView(foreground);
685         }
686         // Make views visible which were invisible while logo animation is running.
687         container.findViewById(R.id.page_container).setVisibility(View.VISIBLE);
688         container.findViewById(R.id.content_container).setVisibility(View.VISIBLE);
689         if (getPageCount() > 1) {
690             mPageIndicator.setPageCount(getPageCount());
691             mPageIndicator.onPageSelected(mCurrentPageIndex, false);
692         }
693         if (mCurrentPageIndex == getPageCount() - 1) {
694             mStartButton.setVisibility(View.VISIBLE);
695         } else {
696             mPageIndicator.setVisibility(View.VISIBLE);
697         }
698         // Header views.
699         mTitleView.setText(getPageTitle(mCurrentPageIndex));
700         mDescriptionView.setText(getPageDescription(mCurrentPageIndex));
701     }
702 
703     /**
704      * Called immediately after the logo animation is complete or no logo animation is specified.
705      * This method can also be called when the activity is recreated, i.e. when no logo animation
706      * are performed.
707      * By default, this method will hide the logo view and start the entrance animation for this
708      * fragment.
709      * Overriding subclasses can provide their own data loading logic as to when the entrance
710      * animation should be executed.
711      */
onLogoAnimationFinished()712     protected void onLogoAnimationFinished() {
713         startEnterAnimation(false);
714     }
715 
716     /**
717      * Called to start entrance transition. This can be called by subclasses when the logo animation
718      * and data loading is complete. If force flag is set to false, it will only start the animation
719      * if it's not already done yet. Otherwise, it will always start the enter animation. In both
720      * cases, the logo view will hide and the rest of fragment views become visible after this call.
721      *
722      * @param force {@code true} if enter animation has to be performed regardless of whether it's
723      *                          been done in the past, {@code false} otherwise
724      */
startEnterAnimation(boolean force)725     protected final void startEnterAnimation(boolean force) {
726         final Context context = FragmentUtil.getContext(OnboardingFragment.this);
727         if (context == null) {
728             return;
729         }
730         hideLogoView();
731         if (mEnterAnimationFinished && !force) {
732             return;
733         }
734         List<Animator> animators = new ArrayList<>();
735         Animator animator = AnimatorInflater.loadAnimator(context,
736                 R.animator.lb_onboarding_page_indicator_enter);
737         animator.setTarget(getPageCount() <= 1 ? mStartButton : mPageIndicator);
738         animators.add(animator);
739 
740         animator = onCreateTitleAnimator();
741         if (animator != null) {
742             // Header title.
743             animator.setTarget(mTitleView);
744             animators.add(animator);
745         }
746 
747         animator = onCreateDescriptionAnimator();
748         if (animator != null) {
749             // Header description.
750             animator.setTarget(mDescriptionView);
751             animators.add(animator);
752         }
753 
754         // Customized animation by the inherited class.
755         Animator customAnimator = onCreateEnterAnimation();
756         if (customAnimator != null) {
757             animators.add(customAnimator);
758         }
759 
760         // Return if we don't have any animations.
761         if (animators.isEmpty()) {
762             return;
763         }
764         mAnimator = new AnimatorSet();
765         mAnimator.playTogether(animators);
766         mAnimator.start();
767         mAnimator.addListener(new AnimatorListenerAdapter() {
768             @Override
769             public void onAnimationEnd(Animator animation) {
770                 mEnterAnimationFinished = true;
771             }
772         });
773         // Search focus and give the focus to the appropriate child which has become visible.
774         getView().requestFocus();
775     }
776 
777     /**
778      * Provides the entry animation for description view. This allows users to override the
779      * default fade and slide animation. Returning null will disable the animation.
780      */
onCreateDescriptionAnimator()781     protected Animator onCreateDescriptionAnimator() {
782         return AnimatorInflater.loadAnimator(FragmentUtil.getContext(OnboardingFragment.this),
783                 R.animator.lb_onboarding_description_enter);
784     }
785 
786     /**
787      * Provides the entry animation for title view. This allows users to override the
788      * default fade and slide animation. Returning null will disable the animation.
789      */
onCreateTitleAnimator()790     protected Animator onCreateTitleAnimator() {
791         return AnimatorInflater.loadAnimator(FragmentUtil.getContext(OnboardingFragment.this),
792                 R.animator.lb_onboarding_title_enter);
793     }
794 
795     /**
796      * Returns whether the logo enter animation is finished.
797      *
798      * @return {@code true} if the logo enter transition is finished, {@code false} otherwise
799      */
isLogoAnimationFinished()800     protected final boolean isLogoAnimationFinished() {
801         return mLogoAnimationFinished;
802     }
803 
804     /**
805      * Returns the page count.
806      *
807      * @return The page count.
808      */
getPageCount()809     abstract protected int getPageCount();
810 
811     /**
812      * Returns the title of the given page.
813      *
814      * @param pageIndex The page index.
815      *
816      * @return The title of the page.
817      */
getPageTitle(int pageIndex)818     abstract protected CharSequence getPageTitle(int pageIndex);
819 
820     /**
821      * Returns the description of the given page.
822      *
823      * @param pageIndex The page index.
824      *
825      * @return The description of the page.
826      */
getPageDescription(int pageIndex)827     abstract protected CharSequence getPageDescription(int pageIndex);
828 
829     /**
830      * Returns the index of the current page.
831      *
832      * @return The index of the current page.
833      */
getCurrentPageIndex()834     protected final int getCurrentPageIndex() {
835         return mCurrentPageIndex;
836     }
837 
838     /**
839      * Called to have the inherited class create background view. This is optional and the fragment
840      * which doesn't have the background view can return {@code null}. This is called inside
841      * {@link #onCreateView}.
842      *
843      * @param inflater The LayoutInflater object that can be used to inflate the views,
844      * @param container The parent view that the additional views are attached to.The fragment
845      *        should not add the view by itself.
846      *
847      * @return The background view for the onboarding screen, or {@code null}.
848      */
849     @Nullable
onCreateBackgroundView(LayoutInflater inflater, ViewGroup container)850     abstract protected View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container);
851 
852     /**
853      * Called to have the inherited class create content view. This is optional and the fragment
854      * which doesn't have the content view can return {@code null}. This is called inside
855      * {@link #onCreateView}.
856      *
857      * <p>The content view would be located at the center of the screen.
858      *
859      * @param inflater The LayoutInflater object that can be used to inflate the views,
860      * @param container The parent view that the additional views are attached to.The fragment
861      *        should not add the view by itself.
862      *
863      * @return The content view for the onboarding screen, or {@code null}.
864      */
865     @Nullable
onCreateContentView(LayoutInflater inflater, ViewGroup container)866     abstract protected View onCreateContentView(LayoutInflater inflater, ViewGroup container);
867 
868     /**
869      * Called to have the inherited class create foreground view. This is optional and the fragment
870      * which doesn't need the foreground view can return {@code null}. This is called inside
871      * {@link #onCreateView}.
872      *
873      * <p>This foreground view would have the highest z-order.
874      *
875      * @param inflater The LayoutInflater object that can be used to inflate the views,
876      * @param container The parent view that the additional views are attached to.The fragment
877      *        should not add the view by itself.
878      *
879      * @return The foreground view for the onboarding screen, or {@code null}.
880      */
881     @Nullable
onCreateForegroundView(LayoutInflater inflater, ViewGroup container)882     abstract protected View onCreateForegroundView(LayoutInflater inflater, ViewGroup container);
883 
884     /**
885      * Called when the onboarding flow finishes.
886      */
onFinishFragment()887     protected void onFinishFragment() { }
888 
889     /**
890      * Called when the page changes.
891      */
onPageChangedInternal(int previousPage)892     private void onPageChangedInternal(int previousPage) {
893         if (mAnimator != null) {
894             mAnimator.end();
895         }
896         mPageIndicator.onPageSelected(mCurrentPageIndex, true);
897 
898         List<Animator> animators = new ArrayList<>();
899         // Header animation
900         Animator fadeAnimator = null;
901         if (previousPage < getCurrentPageIndex()) {
902             // sliding to left
903             animators.add(createAnimator(mTitleView, false, Gravity.START, 0));
904             animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.START,
905                     DESCRIPTION_START_DELAY_MS));
906             animators.add(createAnimator(mTitleView, true, Gravity.END,
907                     HEADER_APPEAR_DELAY_MS));
908             animators.add(createAnimator(mDescriptionView, true, Gravity.END,
909                     HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS));
910         } else {
911             // sliding to right
912             animators.add(createAnimator(mTitleView, false, Gravity.END, 0));
913             animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.END,
914                     DESCRIPTION_START_DELAY_MS));
915             animators.add(createAnimator(mTitleView, true, Gravity.START,
916                     HEADER_APPEAR_DELAY_MS));
917             animators.add(createAnimator(mDescriptionView, true, Gravity.START,
918                     HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS));
919         }
920         final int currentPageIndex = getCurrentPageIndex();
921         fadeAnimator.addListener(new AnimatorListenerAdapter() {
922             @Override
923             public void onAnimationEnd(Animator animation) {
924                 mTitleView.setText(getPageTitle(currentPageIndex));
925                 mDescriptionView.setText(getPageDescription(currentPageIndex));
926             }
927         });
928 
929         final Context context = FragmentUtil.getContext(OnboardingFragment.this);
930         // Animator for switching between page indicator and button.
931         if (getCurrentPageIndex() == getPageCount() - 1) {
932             mStartButton.setVisibility(View.VISIBLE);
933             Animator navigatorFadeOutAnimator = AnimatorInflater.loadAnimator(context,
934                     R.animator.lb_onboarding_page_indicator_fade_out);
935             navigatorFadeOutAnimator.setTarget(mPageIndicator);
936             navigatorFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
937                 @Override
938                 public void onAnimationEnd(Animator animation) {
939                     mPageIndicator.setVisibility(View.GONE);
940                 }
941             });
942             animators.add(navigatorFadeOutAnimator);
943             Animator buttonFadeInAnimator = AnimatorInflater.loadAnimator(context,
944                     R.animator.lb_onboarding_start_button_fade_in);
945             buttonFadeInAnimator.setTarget(mStartButton);
946             animators.add(buttonFadeInAnimator);
947         } else if (previousPage == getPageCount() - 1) {
948             mPageIndicator.setVisibility(View.VISIBLE);
949             Animator navigatorFadeInAnimator = AnimatorInflater.loadAnimator(context,
950                     R.animator.lb_onboarding_page_indicator_fade_in);
951             navigatorFadeInAnimator.setTarget(mPageIndicator);
952             animators.add(navigatorFadeInAnimator);
953             Animator buttonFadeOutAnimator = AnimatorInflater.loadAnimator(context,
954                     R.animator.lb_onboarding_start_button_fade_out);
955             buttonFadeOutAnimator.setTarget(mStartButton);
956             buttonFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
957                 @Override
958                 public void onAnimationEnd(Animator animation) {
959                     mStartButton.setVisibility(View.GONE);
960                 }
961             });
962             animators.add(buttonFadeOutAnimator);
963         }
964         mAnimator = new AnimatorSet();
965         mAnimator.playTogether(animators);
966         mAnimator.start();
967         onPageChanged(mCurrentPageIndex, previousPage);
968     }
969 
970     /**
971      * Called when the page has been changed.
972      *
973      * @param newPage The new page.
974      * @param previousPage The previous page.
975      */
onPageChanged(int newPage, int previousPage)976     protected void onPageChanged(int newPage, int previousPage) { }
977 
createAnimator(View view, boolean fadeIn, int slideDirection, long startDelay)978     private Animator createAnimator(View view, boolean fadeIn, int slideDirection,
979             long startDelay) {
980         boolean isLtr = getView().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
981         boolean slideRight = (isLtr && slideDirection == Gravity.END)
982                 || (!isLtr && slideDirection == Gravity.START)
983                 || slideDirection == Gravity.RIGHT;
984         Animator fadeAnimator;
985         Animator slideAnimator;
986         if (fadeIn) {
987             fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 0.0f, 1.0f);
988             slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X,
989                     slideRight ? sSlideDistance : -sSlideDistance, 0);
990             fadeAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR);
991             slideAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR);
992         } else {
993             fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1.0f, 0.0f);
994             slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0,
995                     slideRight ? sSlideDistance : -sSlideDistance);
996             fadeAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR);
997             slideAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR);
998         }
999         fadeAnimator.setDuration(HEADER_ANIMATION_DURATION_MS);
1000         fadeAnimator.setTarget(view);
1001         slideAnimator.setDuration(HEADER_ANIMATION_DURATION_MS);
1002         slideAnimator.setTarget(view);
1003         AnimatorSet animator = new AnimatorSet();
1004         animator.playTogether(fadeAnimator, slideAnimator);
1005         if (startDelay > 0) {
1006             animator.setStartDelay(startDelay);
1007         }
1008         return animator;
1009     }
1010 
1011     /**
1012      * Sets the resource id for the main icon.
1013      */
setIconResouceId(int resourceId)1014     public final void setIconResouceId(int resourceId) {
1015         this.mIconResourceId = resourceId;
1016         if (mMainIconView != null) {
1017             mMainIconView.setImageResource(resourceId);
1018             mMainIconView.setVisibility(View.VISIBLE);
1019         }
1020     }
1021 
1022     /**
1023      * Returns the resource id of the main icon.
1024      */
getIconResourceId()1025     public final int getIconResourceId() {
1026         return mIconResourceId;
1027     }
1028 }
1029