Home | History | Annotate | Download | only in app
      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      */
    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      */
    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
    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
    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
    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      */
    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
    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      */
    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
    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      */
    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
    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      */
    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
    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      */
    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
    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      */
    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      */
    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      */
    543     public int onProvideTheme() {
    544         return -1;
    545     }
    546 
    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 
    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      */
    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      */
    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
    597     protected Animator onCreateLogoAnimation() {
    598         return null;
    599     }
    600 
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    800     protected final boolean isLogoAnimationFinished() {
    801         return mLogoAnimationFinished;
    802     }
    803 
    804     /**
    805      * Returns the page count.
    806      *
    807      * @return The page count.
    808      */
    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      */
    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      */
    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      */
    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
    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
    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
    882     abstract protected View onCreateForegroundView(LayoutInflater inflater, ViewGroup container);
    883 
    884     /**
    885      * Called when the onboarding flow finishes.
    886      */
    887     protected void onFinishFragment() { }
    888 
    889     /**
    890      * Called when the page changes.
    891      */
    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      */
    976     protected void onPageChanged(int newPage, int previousPage) { }
    977 
    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      */
   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      */
   1025     public final int getIconResourceId() {
   1026         return mIconResourceId;
   1027     }
   1028 }
   1029