public abstract class

OnboardingSupportFragment

extends Fragment

 java.lang.Object

androidx.fragment.app.Fragment

↳androidx.leanback.app.OnboardingSupportFragment

Gradle dependencies

compile group: 'androidx.leanback', name: 'leanback', version: '1.2.0-alpha04'

  • groupId: androidx.leanback
  • artifactId: leanback
  • version: 1.2.0-alpha04

Artifact androidx.leanback:leanback:1.2.0-alpha04 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.leanback:leanback com.android.support:leanback-v17

Androidx class mapping:

androidx.leanback.app.OnboardingSupportFragment android.support.v17.leanback.app.OnboardingSupportFragment

Overview

An OnboardingSupportFragment provides a common and simple way to build onboarding screen for applications.

Building the screen

The view structure of onboarding screen is composed of the common parts and custom parts. The common parts are composed of icon, title, description and page navigator and the custom parts are composed of background, contents and foreground.

To build the screen views, the inherited class should override:

Each of these methods can return null if the application doesn't want to provide it.

Page information

The onboarding screen may have several pages which explain the functionality of the application. The inherited class should provide the page information by overriding the methods:

Note that the information is used in OnboardingSupportFragment.onCreateView(LayoutInflater, ViewGroup, Bundle), so should be initialized before calling super.onCreateView.

Animation

Onboarding screen has three kinds of animations:

Logo Splash Animation

When onboarding screen appears, the logo splash animation is played by default. The animation fades in the logo image, pauses in a few seconds and fades it out.

In most cases, the logo animation needs to be customized because the logo images of applications are different from each other, or some applications may want to show their own animations.

The logo animation can be customized in two ways:

If the inherited class provides neither the logo image nor the animation, the logo animation will be omitted.

Page enter animation

After logo animation finishes, page enter animation starts, which causes the header section - title and description views to fade and slide in. Users can override the default fade + slide animation by overriding OnboardingSupportFragment.onCreateTitleAnimator() & OnboardingSupportFragment.onCreateDescriptionAnimator(). By default we don't animate the custom views but users can provide animation by overriding OnboardingSupportFragment.onCreateEnterAnimation().

Page change animation

When the page changes, the default animations of the title and description are played. The inherited class can override OnboardingSupportFragment.onPageChanged(int, int) to start the custom animations.

Finishing the screen

If the user finishes the onboarding screen after navigating all the pages, OnboardingSupportFragment.onFinishFragment() is called. The inherited class can override this method to show another fragment or activity, or just remove this fragment.

Theming

OnboardingSupportFragment must have access to an appropriate theme. Specifically, the fragment must receive , or a theme whose parent is set to that theme. Themes can be provided in one of three ways:

  • The simplest way is to set the theme for the host Activity to the Onboarding theme or a theme that derives from it.
  • If the Activity already has a theme and setting its parent theme is inconvenient, the existing Activity theme can have an entry added for the attribute . If present, this theme will be used by OnboardingSupportFragment as an overlay to the Activity's theme.
  • Finally, custom subclasses of OnboardingSupportFragment may provide a theme through the OnboardingSupportFragment.onProvideTheme() method. This can be useful if a subclass is used across multiple Activities.

If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by the Activity's theme. (Themes whose parent theme is already set to the onboarding theme do not need to set the onboardingTheme attribute; if set, it will be ignored.)

Summary

Fields
from FragmentmPreviousWho
Constructors
publicOnboardingSupportFragment()

Methods
public final intgetArrowBackgroundColor()

Returns the background color of the arrow if it's set through OnboardingSupportFragment.setArrowBackgroundColor(int).

public final intgetArrowColor()

Returns the color of the arrow if it's set through OnboardingSupportFragment.setArrowColor(int).

protected final intgetCurrentPageIndex()

Returns the index of the current page.

public final intgetDescriptionViewTextColor()

Returns the text color of DescriptionView if it's set through OnboardingSupportFragment.setDescriptionViewTextColor(int).

public final intgetDotBackgroundColor()

Returns the background color of the dot if it's set through OnboardingSupportFragment.setDotBackgroundColor(int).

public final intgetIconResourceId()

Returns the resource id of the main icon.

public final intgetLogoResourceId()

Returns the resource ID of the splash logo image.

protected abstract intgetPageCount()

Returns the page count.

protected abstract java.lang.CharSequencegetPageDescription(int pageIndex)

Returns the description of the given page.

protected abstract java.lang.CharSequencegetPageTitle(int pageIndex)

Returns the title of the given page.

public final java.lang.CharSequencegetStartButtonText()

Returns the start button text if it's set through OnboardingSupportFragment.setStartButtonText(CharSequence)}.

public final intgetTitleViewTextColor()

Returns the text color of TitleView if it's set through OnboardingSupportFragment.setTitleViewTextColor(int).

protected final booleanisLogoAnimationFinished()

Returns whether the logo enter animation is finished.

protected voidmoveToNextPage()

Navigates to the next page.

protected voidmoveToPreviousPage()

Navigates to the previous page.

protected abstract ViewonCreateBackgroundView(LayoutInflater inflater, ViewGroup container)

Called to have the inherited class create background view.

protected abstract ViewonCreateContentView(LayoutInflater inflater, ViewGroup container)

Called to have the inherited class create content view.

protected AnimatoronCreateDescriptionAnimator()

Provides the entry animation for description view.

protected AnimatoronCreateEnterAnimation()

Called to have the inherited class create its enter animation.

protected abstract ViewonCreateForegroundView(LayoutInflater inflater, ViewGroup container)

Called to have the inherited class create foreground view.

protected AnimatoronCreateLogoAnimation()

Called to have the inherited class create its own logo animation.

protected AnimatoronCreateTitleAnimator()

Provides the entry animation for title view.

public ViewonCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)

Called to have the fragment instantiate its user interface view.

protected voidonFinishFragment()

Called when the onboarding flow finishes.

protected voidonLogoAnimationFinished()

Called immediately after the logo animation is complete or no logo animation is specified.

protected voidonPageChanged(int newPage, int previousPage)

Called when the page has been changed.

public intonProvideTheme()

Returns the theme used for styling the fragment.

public voidonSaveInstanceState(Bundle outState)

Called to ask the fragment to save its current dynamic state, so it can later be reconstructed in a new instance if its process is restarted.

public voidonViewCreated(View view, Bundle savedInstanceState)

Called immediately after Fragment.onCreateView(LayoutInflater, ViewGroup, Bundle) has returned, but before any saved state has been restored in to the view.

public voidsetArrowBackgroundColor(int color)

Sets the background color of the arrow.

public voidsetArrowColor(int color)

Sets the color of the arrow.

public voidsetDescriptionViewTextColor(int color)

Sets the text color for DescriptionView.

public voidsetDotBackgroundColor(int color)

Sets the background color of the dots.

public final voidsetIconResouceId(int resourceId)

Sets the resource id for the main icon.

public final voidsetLogoResourceId(int id)

Sets the resource ID of the splash logo image.

public voidsetStartButtonText(java.lang.CharSequence text)

Sets the text on the start button text.

public voidsetTitleViewTextColor(int color)

Sets the text color for TitleView.

protected final voidstartEnterAnimation(boolean force)

Called to start entrance transition.

from Fragmentdump, equals, getActivity, getAllowEnterTransitionOverlap, getAllowReturnTransitionOverlap, getArguments, getChildFragmentManager, getContext, getDefaultViewModelCreationExtras, getDefaultViewModelProviderFactory, getEnterTransition, getExitTransition, getFragmentManager, getHost, getId, getLayoutInflater, getLayoutInflater, getLifecycle, getLoaderManager, getParentFragment, getParentFragmentManager, getReenterTransition, getResources, getRetainInstance, getReturnTransition, getSavedStateRegistry, getSharedElementEnterTransition, getSharedElementReturnTransition, getString, getString, getTag, getTargetFragment, getTargetRequestCode, getText, getUserVisibleHint, getView, getViewLifecycleOwner, getViewLifecycleOwnerLiveData, getViewModelStore, hashCode, hasOptionsMenu, instantiate, instantiate, isAdded, isDetached, isHidden, isInLayout, isMenuVisible, isRemoving, isResumed, isStateSaved, isVisible, onActivityCreated, onActivityResult, onAttach, onAttach, onAttachFragment, onConfigurationChanged, onContextItemSelected, onCreate, onCreateAnimation, onCreateAnimator, onCreateContextMenu, onCreateOptionsMenu, onDestroy, onDestroyOptionsMenu, onDestroyView, onDetach, onGetLayoutInflater, onHiddenChanged, onInflate, onInflate, onLowMemory, onMultiWindowModeChanged, onOptionsItemSelected, onOptionsMenuClosed, onPause, onPictureInPictureModeChanged, onPrepareOptionsMenu, onPrimaryNavigationFragmentChanged, onRequestPermissionsResult, onResume, onStart, onStop, onViewStateRestored, postponeEnterTransition, postponeEnterTransition, registerForActivityResult, registerForActivityResult, registerForContextMenu, requestPermissions, requireActivity, requireArguments, requireContext, requireFragmentManager, requireHost, requireParentFragment, requireView, setAllowEnterTransitionOverlap, setAllowReturnTransitionOverlap, setArguments, setEnterSharedElementCallback, setEnterTransition, setExitSharedElementCallback, setExitTransition, setHasOptionsMenu, setInitialSavedState, setMenuVisibility, setReenterTransition, setRetainInstance, setReturnTransition, setSharedElementEnterTransition, setSharedElementReturnTransition, setTargetFragment, setUserVisibleHint, shouldShowRequestPermissionRationale, startActivity, startActivity, startActivityForResult, startActivityForResult, startIntentSenderForResult, startPostponedEnterTransition, toString, unregisterForContextMenu
from java.lang.Objectclone, finalize, getClass, notify, notifyAll, wait, wait, wait

Constructors

public OnboardingSupportFragment()

Methods

protected void moveToPreviousPage()

Navigates to the previous page.

protected void moveToNextPage()

Navigates to the next page.

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)

Called to have the fragment instantiate its user interface view. This is optional, and non-graphical fragments can return null. This will be called between Fragment.onCreate(Bundle) and Fragment.onViewCreated(View, Bundle).

A default View can be returned by calling Fragment.Fragment(int) in your constructor. Otherwise, this method returns null.

It is recommended to only inflate the layout in this method and move logic that operates on the returned View to Fragment.onViewCreated(View, Bundle).

If you return a View from here, you will later be called in Fragment.onDestroyView() when the view is being released.

Parameters:

inflater: The LayoutInflater object that can be used to inflate any views in the fragment,
container: If non-null, this is the parent view that the fragment's UI should be attached to. The fragment should not add the view itself, but this can be used to generate the LayoutParams of the view.
savedInstanceState: If non-null, this fragment is being re-constructed from a previous saved state as given here.

Returns:

Return the View for the fragment's UI, or null.

public void onViewCreated(View view, Bundle savedInstanceState)

Called immediately after Fragment.onCreateView(LayoutInflater, ViewGroup, Bundle) has returned, but before any saved state has been restored in to the view. This gives subclasses a chance to initialize themselves once they know their view hierarchy has been completely created. The fragment's view hierarchy is not however attached to its parent at this point.

Parameters:

view: The View returned by Fragment.onCreateView(LayoutInflater, ViewGroup, Bundle).
savedInstanceState: If non-null, this fragment is being re-constructed from a previous saved state as given here.

public void onSaveInstanceState(Bundle outState)

Called to ask the fragment to save its current dynamic state, so it can later be reconstructed in a new instance if its process is restarted. If a new instance of the fragment later needs to be created, the data you place in the Bundle here will be available in the Bundle given to Fragment.onCreate(Bundle), Fragment.onCreateView(LayoutInflater, ViewGroup, Bundle), and Fragment.onViewCreated(View, Bundle).

This corresponds to and most of the discussion there applies here as well. Note however: this method may be called at any time before Fragment.onDestroy(). There are many situations where a fragment may be mostly torn down (such as when placed on the back stack with no UI showing), but its state will not be saved until its owning activity actually needs to save its state.

Parameters:

outState: Bundle in which to place your saved state.

public void setTitleViewTextColor(int color)

Sets the text color for TitleView. If not set, the default textColor set in style referenced by attr will be used.

Parameters:

color: the color to use as the text color for TitleView

public final int getTitleViewTextColor()

Returns the text color of TitleView if it's set through OnboardingSupportFragment.setTitleViewTextColor(int). If no color was set, transparent is returned.

public void setDescriptionViewTextColor(int color)

Sets the text color for DescriptionView. If not set, the default textColor set in style referenced by attr will be used.

Parameters:

color: the color to use as the text color for DescriptionView

public final int getDescriptionViewTextColor()

Returns the text color of DescriptionView if it's set through OnboardingSupportFragment.setDescriptionViewTextColor(int). If no color was set, transparent is returned.

public void setDotBackgroundColor(int color)

Sets the background color of the dots. If not set, the default color from attr in the theme will be used.

Parameters:

color: the color to use for dot backgrounds

public final int getDotBackgroundColor()

Returns the background color of the dot if it's set through OnboardingSupportFragment.setDotBackgroundColor(int). If no color was set, transparent is returned.

public void setArrowColor(int color)

Sets the color of the arrow. This color will supersede the color set in the theme attribute if provided. If none of these two are set, the arrow will have its original bitmap color.

Parameters:

color: the color to use for arrow background

public final int getArrowColor()

Returns the color of the arrow if it's set through OnboardingSupportFragment.setArrowColor(int). If no color was set, transparent is returned.

public void setArrowBackgroundColor(int color)

Sets the background color of the arrow. If not set, the default color from attr in the theme will be used.

Parameters:

color: the color to use for arrow background

public final int getArrowBackgroundColor()

Returns the background color of the arrow if it's set through OnboardingSupportFragment.setArrowBackgroundColor(int). If no color was set, transparent is returned.

public final java.lang.CharSequence getStartButtonText()

Returns the start button text if it's set through OnboardingSupportFragment.setStartButtonText(CharSequence)}. If no string was set, null is returned.

public void setStartButtonText(java.lang.CharSequence text)

Sets the text on the start button text. If not set, the default text set in will be used.

Parameters:

text: the start button text

public int onProvideTheme()

Returns the theme used for styling the fragment. The default returns -1, indicating that the host Activity's theme should be used.

Returns:

The theme resource ID of the theme to use in this fragment, or -1 to use the host Activity's theme.

public final void setLogoResourceId(int id)

Sets the resource ID of the splash logo image. If the logo resource id set, the default logo splash animation will be played.

Parameters:

id: The resource ID of the logo image.

public final int getLogoResourceId()

Returns the resource ID of the splash logo image.

Returns:

The resource ID of the splash logo image.

protected Animator onCreateLogoAnimation()

Called to have the inherited class create its own logo animation.

This is called only if the logo image resource ID is not set by OnboardingSupportFragment.setLogoResourceId(int). If this returns null, the logo animation is skipped.

Returns:

The Animator object which runs the logo animation.

protected Animator onCreateEnterAnimation()

Called to have the inherited class create its enter animation. The start animation runs after logo animation ends.

Returns:

The Animator object which runs the page enter animation.

protected void onLogoAnimationFinished()

Called immediately after the logo animation is complete or no logo animation is specified. This method can also be called when the activity is recreated, i.e. when no logo animation are performed. By default, this method will hide the logo view and start the entrance animation for this fragment. Overriding subclasses can provide their own data loading logic as to when the entrance animation should be executed.

protected final void startEnterAnimation(boolean force)

Called to start entrance transition. This can be called by subclasses when the logo animation and data loading is complete. If force flag is set to false, it will only start the animation if it's not already done yet. Otherwise, it will always start the enter animation. In both cases, the logo view will hide and the rest of fragment views become visible after this call.

Parameters:

force: true if enter animation has to be performed regardless of whether it's been done in the past, false otherwise

protected Animator onCreateDescriptionAnimator()

Provides the entry animation for description view. This allows users to override the default fade and slide animation. Returning null will disable the animation.

protected Animator onCreateTitleAnimator()

Provides the entry animation for title view. This allows users to override the default fade and slide animation. Returning null will disable the animation.

protected final boolean isLogoAnimationFinished()

Returns whether the logo enter animation is finished.

Returns:

true if the logo enter transition is finished, false otherwise

protected abstract int getPageCount()

Returns the page count.

Returns:

The page count.

protected abstract java.lang.CharSequence getPageTitle(int pageIndex)

Returns the title of the given page.

Parameters:

pageIndex: The page index.

Returns:

The title of the page.

protected abstract java.lang.CharSequence getPageDescription(int pageIndex)

Returns the description of the given page.

Parameters:

pageIndex: The page index.

Returns:

The description of the page.

protected final int getCurrentPageIndex()

Returns the index of the current page.

Returns:

The index of the current page.

protected abstract View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container)

Called to have the inherited class create background view. This is optional and the fragment which doesn't have the background view can return null. This is called inside OnboardingSupportFragment.onCreateView(LayoutInflater, ViewGroup, Bundle).

Parameters:

inflater: The LayoutInflater object that can be used to inflate the views,
container: The parent view that the additional views are attached to.The fragment should not add the view by itself.

Returns:

The background view for the onboarding screen, or null.

protected abstract View onCreateContentView(LayoutInflater inflater, ViewGroup container)

Called to have the inherited class create content view. This is optional and the fragment which doesn't have the content view can return null. This is called inside OnboardingSupportFragment.onCreateView(LayoutInflater, ViewGroup, Bundle).

The content view would be located at the center of the screen.

Parameters:

inflater: The LayoutInflater object that can be used to inflate the views,
container: The parent view that the additional views are attached to.The fragment should not add the view by itself.

Returns:

The content view for the onboarding screen, or null.

protected abstract View onCreateForegroundView(LayoutInflater inflater, ViewGroup container)

Called to have the inherited class create foreground view. This is optional and the fragment which doesn't need the foreground view can return null. This is called inside OnboardingSupportFragment.onCreateView(LayoutInflater, ViewGroup, Bundle).

This foreground view would have the highest z-order.

Parameters:

inflater: The LayoutInflater object that can be used to inflate the views,
container: The parent view that the additional views are attached to.The fragment should not add the view by itself.

Returns:

The foreground view for the onboarding screen, or null.

protected void onFinishFragment()

Called when the onboarding flow finishes.

protected void onPageChanged(int newPage, int previousPage)

Called when the page has been changed.

Parameters:

newPage: The new page.
previousPage: The previous page.

public final void setIconResouceId(int resourceId)

Sets the resource id for the main icon.

public final int getIconResourceId()

Returns the resource id of the main icon.

Source

/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.leanback.app;

import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnPreDrawListener;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.leanback.R;
import androidx.leanback.widget.PagingIndicator;

import java.util.ArrayList;
import java.util.List;

/**
 * An OnboardingSupportFragment provides a common and simple way to build onboarding screen for
 * applications.
 * <p>
 * <h3>Building the screen</h3>
 * The view structure of onboarding screen is composed of the common parts and custom parts. The
 * common parts are composed of icon, title, description and page navigator and the custom parts
 * are composed of background, contents and foreground.
 * <p>
 * To build the screen views, the inherited class should override:
 * <ul>
 * <li>{@link #onCreateBackgroundView} to provide the background view. Background view has the same
 * size as the screen and the lowest z-order.</li>
 * <li>{@link #onCreateContentView} to provide the contents view. The content view is located in
 * the content area at the center of the screen.</li>
 * <li>{@link #onCreateForegroundView} to provide the foreground view. Foreground view has the same
 * size as the screen and the highest z-order</li>
 * </ul>
 * <p>
 * Each of these methods can return {@code null} if the application doesn't want to provide it.
 * <p>
 * <h3>Page information</h3>
 * The onboarding screen may have several pages which explain the functionality of the application.
 * The inherited class should provide the page information by overriding the methods:
 * <p>
 * <ul>
 * <li>{@link #getPageCount} to provide the number of pages.</li>
 * <li>{@link #getPageTitle} to provide the title of the page.</li>
 * <li>{@link #getPageDescription} to provide the description of the page.</li>
 * </ul>
 * <p>
 * Note that the information is used in {@link #onCreateView}, so should be initialized before
 * calling {@code super.onCreateView}.
 * <p>
 * <h3>Animation</h3>
 * Onboarding screen has three kinds of animations:
 * <p>
 * <h4>Logo Splash Animation</a></h4>
 * When onboarding screen appears, the logo splash animation is played by default. The animation
 * fades in the logo image, pauses in a few seconds and fades it out.
 * <p>
 * In most cases, the logo animation needs to be customized because the logo images of applications
 * are different from each other, or some applications may want to show their own animations.
 * <p>
 * The logo animation can be customized in two ways:
 * <ul>
 * <li>The simplest way is to provide the logo image by calling {@link #setLogoResourceId} to show
 * the default logo animation. This method should be called in {@link Fragment#onCreateView}.</li>
 * <li>If the logo animation is complex, then override {@link #onCreateLogoAnimation} and return the
 * {@link Animator} object to run.</li>
 * </ul>
 * <p>
 * If the inherited class provides neither the logo image nor the animation, the logo animation will
 * be omitted.
 * <h4>Page enter animation</h4>
 * After logo animation finishes, page enter animation starts, which causes the header section -
 * title and description views to fade and slide in. Users can override the default
 * fade + slide animation by overriding {@link #onCreateTitleAnimator()} &
 * {@link #onCreateDescriptionAnimator()}. By default we don't animate the custom views but users
 * can provide animation by overriding {@link #onCreateEnterAnimation}.
 *
 * <h4>Page change animation</h4>
 * When the page changes, the default animations of the title and description are played. The
 * inherited class can override {@link #onPageChanged} to start the custom animations.
 * <p>
 * <h3>Finishing the screen</h3>
 * <p>
 * If the user finishes the onboarding screen after navigating all the pages,
 * {@link #onFinishFragment} is called. The inherited class can override this method to show another
 * fragment or activity, or just remove this fragment.
 * <p>
 * <h3>Theming</h3>
 * <p>
 * OnboardingSupportFragment must have access to an appropriate theme. Specifically, the fragment must
 * receive  {@link R.style#Theme_Leanback_Onboarding}, or a theme whose parent is set to that theme.
 * Themes can be provided in one of three ways:
 * <ul>
 * <li>The simplest way is to set the theme for the host Activity to the Onboarding theme or a theme
 * that derives from it.</li>
 * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the
 * existing Activity theme can have an entry added for the attribute
 * {@link R.styleable#LeanbackOnboardingTheme_onboardingTheme}. If present, this theme will be used
 * by OnboardingSupportFragment as an overlay to the Activity's theme.</li>
 * <li>Finally, custom subclasses of OnboardingSupportFragment may provide a theme through the
 * {@link #onProvideTheme} method. This can be useful if a subclass is used across multiple
 * Activities.</li>
 * </ul>
 * <p>
 * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by
 * the Activity's theme. (Themes whose parent theme is already set to the onboarding theme do not
 * need to set the onboardingTheme attribute; if set, it will be ignored.)
 *
 * {@link R.attr#onboardingTheme}
 * {@link R.attr#onboardingHeaderStyle}
 * {@link R.attr#onboardingTitleStyle}
 * {@link R.attr#onboardingDescriptionStyle}
 * {@link R.attr#onboardingNavigatorContainerStyle}
 * {@link R.attr#onboardingPageIndicatorStyle}
 * {@link R.attr#onboardingStartButtonStyle}
 * {@link R.attr#onboardingLogoStyle}
 */
abstract public class OnboardingSupportFragment extends Fragment {
    private static final String TAG = "OnboardingF";
    private static final boolean DEBUG = false;

    private static final long LOGO_SPLASH_PAUSE_DURATION_MS = 1333;

    private static final long HEADER_ANIMATION_DURATION_MS = 417;
    private static final long DESCRIPTION_START_DELAY_MS = 33;
    private static final long HEADER_APPEAR_DELAY_MS = 500;
    private static final int SLIDE_DISTANCE = 60;

    private static int sSlideDistance;

    private static final TimeInterpolator HEADER_APPEAR_INTERPOLATOR = new DecelerateInterpolator();
    private static final TimeInterpolator HEADER_DISAPPEAR_INTERPOLATOR =
            new AccelerateInterpolator();

    // Keys used to save and restore the states.
    private static final String KEY_CURRENT_PAGE_INDEX = "leanback.onboarding.current_page_index";
    private static final String KEY_LOGO_ANIMATION_FINISHED =
            "leanback.onboarding.logo_animation_finished";
    private static final String KEY_ENTER_ANIMATION_FINISHED =
            "leanback.onboarding.enter_animation_finished";

    private ContextThemeWrapper mThemeWrapper;

    PagingIndicator mPageIndicator;
    View mStartButton;
    private ImageView mLogoView;
    // Optional icon that can be displayed on top of the header section.
    private ImageView mMainIconView;
    private int mIconResourceId;

    TextView mTitleView;
    TextView mDescriptionView;

    boolean mIsLtr;

    // No need to save/restore the logo resource ID, because the logo animation will not appear when
    // the fragment is restored.
    private int mLogoResourceId;
    boolean mLogoAnimationFinished;
    boolean mEnterAnimationFinished;
    int mCurrentPageIndex;

    @ColorInt
    private int mTitleViewTextColor = Color.TRANSPARENT;
    private boolean mTitleViewTextColorSet;

    @ColorInt
    private int mDescriptionViewTextColor = Color.TRANSPARENT;
    private boolean mDescriptionViewTextColorSet;

    @ColorInt
    private int mDotBackgroundColor = Color.TRANSPARENT;
    private boolean mDotBackgroundColorSet;

    @ColorInt
    private int mArrowColor = Color.TRANSPARENT;
    private boolean mArrowColorSet;

    @ColorInt
    private int mArrowBackgroundColor = Color.TRANSPARENT;
    private boolean mArrowBackgroundColorSet;

    private CharSequence mStartButtonText;
    private boolean mStartButtonTextSet;


    private AnimatorSet mAnimator;

    private final OnClickListener mOnClickListener = new OnClickListener() {
        @Override
        public void onClick(View view) {
            if (!mLogoAnimationFinished) {
                // Do not change page until the enter transition finishes.
                return;
            }
            if (mCurrentPageIndex == getPageCount() - 1) {
                onFinishFragment();
            } else {
                moveToNextPage();
            }
        }
    };

    private final OnKeyListener mOnKeyListener = new OnKeyListener() {
        @Override
        public boolean onKey(View v, int keyCode, KeyEvent event) {
            if (!mLogoAnimationFinished) {
                // Ignore key event until the enter transition finishes.
                return keyCode != KeyEvent.KEYCODE_BACK;
            }
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                return false;
            }
            switch (keyCode) {
                case KeyEvent.KEYCODE_BACK:
                    if (mCurrentPageIndex == 0) {
                        return false;
                    }
                    moveToPreviousPage();
                    return true;
                case KeyEvent.KEYCODE_DPAD_LEFT:
                    if (mIsLtr) {
                        moveToPreviousPage();
                    } else {
                        moveToNextPage();
                    }
                    return true;
                case KeyEvent.KEYCODE_DPAD_RIGHT:
                    if (mIsLtr) {
                        moveToNextPage();
                    } else {
                        moveToPreviousPage();
                    }
                    return true;
            }
            return false;
        }
    };

    /**
     * Navigates to the previous page.
     */
    protected void moveToPreviousPage() {
        if (!mLogoAnimationFinished) {
            // Ignore if the logo enter transition is in progress.
            return;
        }
        if (mCurrentPageIndex > 0) {
            --mCurrentPageIndex;
            onPageChangedInternal(mCurrentPageIndex + 1);
        }
    }

    /**
     * Navigates to the next page.
     */
    protected void moveToNextPage() {
        if (!mLogoAnimationFinished) {
            // Ignore if the logo enter transition is in progress.
            return;
        }
        if (mCurrentPageIndex < getPageCount() - 1) {
            ++mCurrentPageIndex;
            onPageChangedInternal(mCurrentPageIndex - 1);
        }
    }

    @Override
    @Nullable
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
            @Nullable Bundle savedInstanceState) {
        resolveTheme();
        LayoutInflater localInflater = getThemeInflater(inflater);
        final ViewGroup view = (ViewGroup) localInflater.inflate(R.layout.lb_onboarding_fragment,
                container, false);
        mIsLtr = getResources().getConfiguration().getLayoutDirection()
                == View.LAYOUT_DIRECTION_LTR;
        mPageIndicator = (PagingIndicator) view.findViewById(R.id.page_indicator);
        mPageIndicator.setOnClickListener(mOnClickListener);
        mPageIndicator.setOnKeyListener(mOnKeyListener);
        mStartButton = view.findViewById(R.id.button_start);
        mStartButton.setOnClickListener(mOnClickListener);
        mStartButton.setOnKeyListener(mOnKeyListener);
        mMainIconView = (ImageView) view.findViewById(R.id.main_icon);
        mLogoView = (ImageView) view.findViewById(R.id.logo);
        mTitleView = (TextView) view.findViewById(R.id.title);
        mDescriptionView = (TextView) view.findViewById(R.id.description);

        if (mTitleViewTextColorSet) {
            mTitleView.setTextColor(mTitleViewTextColor);
        }
        if (mDescriptionViewTextColorSet) {
            mDescriptionView.setTextColor(mDescriptionViewTextColor);
        }
        if (mDotBackgroundColorSet) {
            mPageIndicator.setDotBackgroundColor(mDotBackgroundColor);
        }
        if (mArrowColorSet) {
            mPageIndicator.setArrowColor(mArrowColor);
        }
        if (mArrowBackgroundColorSet) {
            mPageIndicator.setDotBackgroundColor(mArrowBackgroundColor);
        }
        if (mStartButtonTextSet) {
            ((Button) mStartButton).setText(mStartButtonText);
        }
        final Context context = getContext();
        if (sSlideDistance == 0) {
            sSlideDistance = (int) (SLIDE_DISTANCE * context.getResources()
                    .getDisplayMetrics().scaledDensity);
        }
        view.requestFocus();
        return view;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        if (savedInstanceState == null) {
            mCurrentPageIndex = 0;
            mLogoAnimationFinished = false;
            mEnterAnimationFinished = false;
            mPageIndicator.onPageSelected(0, false);
            view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
                @Override
                public boolean onPreDraw() {
                    getView().getViewTreeObserver().removeOnPreDrawListener(this);
                    if (!startLogoAnimation()) {
                        mLogoAnimationFinished = true;
                        onLogoAnimationFinished();
                    }
                    return true;
                }
            });
        } else {
            mCurrentPageIndex = savedInstanceState.getInt(KEY_CURRENT_PAGE_INDEX);
            mLogoAnimationFinished = savedInstanceState.getBoolean(KEY_LOGO_ANIMATION_FINISHED);
            mEnterAnimationFinished = savedInstanceState.getBoolean(KEY_ENTER_ANIMATION_FINISHED);
            if (!mLogoAnimationFinished) {
                // logo animation wasn't started or was interrupted when the activity was destroyed;
                // restart it againl
                if (!startLogoAnimation()) {
                    mLogoAnimationFinished = true;
                    onLogoAnimationFinished();
                }
            } else {
                onLogoAnimationFinished();
            }
        }
    }

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt(KEY_CURRENT_PAGE_INDEX, mCurrentPageIndex);
        outState.putBoolean(KEY_LOGO_ANIMATION_FINISHED, mLogoAnimationFinished);
        outState.putBoolean(KEY_ENTER_ANIMATION_FINISHED, mEnterAnimationFinished);
    }

    /**
     * Sets the text color for TitleView. If not set, the default textColor set in style
     * referenced by attr {@link R.attr#onboardingTitleStyle} will be used.
     * @param color the color to use as the text color for TitleView
     */
    public void setTitleViewTextColor(@ColorInt int color) {
        mTitleViewTextColor = color;
        mTitleViewTextColorSet = true;
        if (mTitleView != null) {
            mTitleView.setTextColor(color);
        }
    }

    /**
     * Returns the text color of TitleView if it's set through
     * {@link #setTitleViewTextColor(int)}. If no color was set, transparent is returned.
     */
    @ColorInt
    public final int getTitleViewTextColor() {
        return mTitleViewTextColor;
    }

    /**
     * Sets the text color for DescriptionView. If not set, the default textColor set in style
     * referenced by attr {@link R.attr#onboardingDescriptionStyle} will be used.
     * @param color the color to use as the text color for DescriptionView
     */
    public void setDescriptionViewTextColor(@ColorInt int color) {
        mDescriptionViewTextColor = color;
        mDescriptionViewTextColorSet = true;
        if (mDescriptionView != null) {
            mDescriptionView.setTextColor(color);
        }
    }

    /**
     * Returns the text color of DescriptionView if it's set through
     * {@link #setDescriptionViewTextColor(int)}. If no color was set, transparent is returned.
     */
    @ColorInt
    public final int getDescriptionViewTextColor() {
        return mDescriptionViewTextColor;
    }
    /**
     * Sets the background color of the dots. If not set, the default color from attr
     * {@link R.styleable#PagingIndicator_dotBgColor} in the theme will be used.
     * @param color the color to use for dot backgrounds
     */
    public void setDotBackgroundColor(@ColorInt int color) {
        mDotBackgroundColor = color;
        mDotBackgroundColorSet = true;
        if (mPageIndicator != null) {
            mPageIndicator.setDotBackgroundColor(color);
        }
    }

    /**
     * Returns the background color of the dot if it's set through
     * {@link #setDotBackgroundColor(int)}. If no color was set, transparent is returned.
     */
    @ColorInt
    public final int getDotBackgroundColor() {
        return mDotBackgroundColor;
    }

    /**
     * Sets the color of the arrow. This color will supersede the color set in the theme attribute
     * {@link R.styleable#PagingIndicator_arrowColor} if provided. If none of these two are set, the
     * arrow will have its original bitmap color.
     *
     * @param color the color to use for arrow background
     */
    public void setArrowColor(@ColorInt int color) {
        mArrowColor = color;
        mArrowColorSet = true;
        if (mPageIndicator != null) {
            mPageIndicator.setArrowColor(color);
        }
    }

    /**
     * Returns the color of the arrow if it's set through
     * {@link #setArrowColor(int)}. If no color was set, transparent is returned.
     */
    @ColorInt
    public final int getArrowColor() {
        return mArrowColor;
    }

    /**
     * Sets the background color of the arrow. If not set, the default color from attr
     * {@link R.styleable#PagingIndicator_arrowBgColor} in the theme will be used.
     * @param color the color to use for arrow background
     */
    public void setArrowBackgroundColor(@ColorInt int color) {
        mArrowBackgroundColor = color;
        mArrowBackgroundColorSet = true;
        if (mPageIndicator != null) {
            mPageIndicator.setArrowBackgroundColor(color);
        }
    }

    /**
     * Returns the background color of the arrow if it's set through
     * {@link #setArrowBackgroundColor(int)}. If no color was set, transparent is returned.
     */
    @ColorInt
    public final int getArrowBackgroundColor() {
        return mArrowBackgroundColor;
    }

    /**
     * Returns the start button text if it's set through
     * {@link #setStartButtonText(CharSequence)}}. If no string was set, null is returned.
     */
    @Nullable
    public final CharSequence getStartButtonText() {
        return mStartButtonText;
    }

    /**
     * Sets the text on the start button text. If not set, the default text set in
     * {@link R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle} will be used.
     *
     * @param text the start button text
     */
    public void setStartButtonText(@Nullable CharSequence text) {
        mStartButtonText = text;
        mStartButtonTextSet = true;
        if (mStartButton != null) {
            ((Button) mStartButton).setText(mStartButtonText);
        }
    }

    /**
     * Returns the theme used for styling the fragment. The default returns -1, indicating that the
     * host Activity's theme should be used.
     *
     * @return The theme resource ID of the theme to use in this fragment, or -1 to use the host
     *         Activity's theme.
     */
    public int onProvideTheme() {
        return -1;
    }

    private void resolveTheme() {
        final Context context = getContext();
        int theme = onProvideTheme();
        if (theme == -1) {
            // Look up the onboardingTheme in the activity's currently specified theme. If it
            // exists, wrap the theme with its value.
            int resId = R.attr.onboardingTheme;
            TypedValue typedValue = new TypedValue();
            boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
            if (DEBUG) Log.v(TAG, "Found onboarding theme reference? " + found);
            if (found) {
                mThemeWrapper = new ContextThemeWrapper(context, typedValue.resourceId);
            }
        } else {
            mThemeWrapper = new ContextThemeWrapper(context, theme);
        }
    }

    private LayoutInflater getThemeInflater(LayoutInflater inflater) {
        return mThemeWrapper == null ? inflater : inflater.cloneInContext(mThemeWrapper);
    }

    /**
     * Sets the resource ID of the splash logo image. If the logo resource id set, the default logo
     * splash animation will be played.
     *
     * @param id The resource ID of the logo image.
     */
    public final void setLogoResourceId(int id) {
        mLogoResourceId = id;
    }

    /**
     * Returns the resource ID of the splash logo image.
     *
     * @return The resource ID of the splash logo image.
     */
    public final int getLogoResourceId() {
        return mLogoResourceId;
    }

    /**
     * Called to have the inherited class create its own logo animation.
     * <p>
     * This is called only if the logo image resource ID is not set by {@link #setLogoResourceId}.
     * If this returns {@code null}, the logo animation is skipped.
     *
     * @return The {@link Animator} object which runs the logo animation.
     */
    @Nullable
    protected Animator onCreateLogoAnimation() {
        return null;
    }

    boolean startLogoAnimation() {
        final Context context = getContext();
        if (context == null) {
            return false;
        }
        Animator animator = null;
        if (mLogoResourceId != 0) {
            mLogoView.setVisibility(View.VISIBLE);
            mLogoView.setImageResource(mLogoResourceId);
            Animator inAnimator = AnimatorInflater.loadAnimator(context,
                    R.animator.lb_onboarding_logo_enter);
            Animator outAnimator = AnimatorInflater.loadAnimator(context,
                    R.animator.lb_onboarding_logo_exit);
            outAnimator.setStartDelay(LOGO_SPLASH_PAUSE_DURATION_MS);
            AnimatorSet logoAnimator = new AnimatorSet();
            logoAnimator.playSequentially(inAnimator, outAnimator);
            logoAnimator.setTarget(mLogoView);
            animator = logoAnimator;
        } else {
            animator = onCreateLogoAnimation();
        }
        if (animator != null) {
            animator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    if (context != null) {
                        mLogoAnimationFinished = true;
                        onLogoAnimationFinished();
                    }
                }
            });
            animator.start();
            return true;
        }
        return false;
    }

    /**
     * Called to have the inherited class create its enter animation. The start animation runs after
     * logo animation ends.
     *
     * @return The {@link Animator} object which runs the page enter animation.
     */
    @Nullable
    protected Animator onCreateEnterAnimation() {
        return null;
    }


    /**
     * Hides the logo view and makes other fragment views visible. Also initializes the texts for
     * Title and Description views.
     */
    void hideLogoView() {
        mLogoView.setVisibility(View.GONE);

        if (mIconResourceId != 0) {
            mMainIconView.setImageResource(mIconResourceId);
            mMainIconView.setVisibility(View.VISIBLE);
        }

        View container = getView();
        // Create custom views.
        LayoutInflater inflater = getThemeInflater(LayoutInflater.from(
                getContext()));
        ViewGroup backgroundContainer = (ViewGroup) container.findViewById(
                R.id.background_container);
        View background = onCreateBackgroundView(inflater, backgroundContainer);
        if (background != null) {
            backgroundContainer.setVisibility(View.VISIBLE);
            backgroundContainer.addView(background);
        }
        ViewGroup contentContainer = (ViewGroup) container.findViewById(R.id.content_container);
        View content = onCreateContentView(inflater, contentContainer);
        if (content != null) {
            contentContainer.setVisibility(View.VISIBLE);
            contentContainer.addView(content);
        }
        ViewGroup foregroundContainer = (ViewGroup) container.findViewById(
                R.id.foreground_container);
        View foreground = onCreateForegroundView(inflater, foregroundContainer);
        if (foreground != null) {
            foregroundContainer.setVisibility(View.VISIBLE);
            foregroundContainer.addView(foreground);
        }
        // Make views visible which were invisible while logo animation is running.
        container.findViewById(R.id.page_container).setVisibility(View.VISIBLE);
        container.findViewById(R.id.content_container).setVisibility(View.VISIBLE);
        if (getPageCount() > 1) {
            mPageIndicator.setPageCount(getPageCount());
            mPageIndicator.onPageSelected(mCurrentPageIndex, false);
        }
        if (mCurrentPageIndex == getPageCount() - 1) {
            mStartButton.setVisibility(View.VISIBLE);
        } else {
            mPageIndicator.setVisibility(View.VISIBLE);
        }
        // Header views.
        mTitleView.setText(getPageTitle(mCurrentPageIndex));
        mDescriptionView.setText(getPageDescription(mCurrentPageIndex));
    }

    /**
     * Called immediately after the logo animation is complete or no logo animation is specified.
     * This method can also be called when the activity is recreated, i.e. when no logo animation
     * are performed.
     * By default, this method will hide the logo view and start the entrance animation for this
     * fragment.
     * Overriding subclasses can provide their own data loading logic as to when the entrance
     * animation should be executed.
     */
    protected void onLogoAnimationFinished() {
        startEnterAnimation(false);
    }

    /**
     * Called to start entrance transition. This can be called by subclasses when the logo animation
     * and data loading is complete. If force flag is set to false, it will only start the animation
     * if it's not already done yet. Otherwise, it will always start the enter animation. In both
     * cases, the logo view will hide and the rest of fragment views become visible after this call.
     *
     * @param force {@code true} if enter animation has to be performed regardless of whether it's
     *                          been done in the past, {@code false} otherwise
     */
    protected final void startEnterAnimation(boolean force) {
        final Context context = getContext();
        if (context == null) {
            return;
        }
        hideLogoView();
        if (mEnterAnimationFinished && !force) {
            return;
        }
        List<Animator> animators = new ArrayList<>();
        Animator animator = AnimatorInflater.loadAnimator(context,
                R.animator.lb_onboarding_page_indicator_enter);
        animator.setTarget(getPageCount() <= 1 ? mStartButton : mPageIndicator);
        animators.add(animator);

        animator = onCreateTitleAnimator();
        if (animator != null) {
            // Header title.
            animator.setTarget(mTitleView);
            animators.add(animator);
        }

        animator = onCreateDescriptionAnimator();
        if (animator != null) {
            // Header description.
            animator.setTarget(mDescriptionView);
            animators.add(animator);
        }

        // Customized animation by the inherited class.
        Animator customAnimator = onCreateEnterAnimation();
        if (customAnimator != null) {
            animators.add(customAnimator);
        }

        // Return if we don't have any animations.
        if (animators.isEmpty()) {
            return;
        }
        mAnimator = new AnimatorSet();
        mAnimator.playTogether(animators);
        mAnimator.start();
        mAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mEnterAnimationFinished = true;
            }
        });
        // Search focus and give the focus to the appropriate child which has become visible.
        getView().requestFocus();
    }

    /**
     * Provides the entry animation for description view. This allows users to override the
     * default fade and slide animation. Returning null will disable the animation.
     */
    @NonNull
    protected Animator onCreateDescriptionAnimator() {
        return AnimatorInflater.loadAnimator(getContext(),
                R.animator.lb_onboarding_description_enter);
    }

    /**
     * Provides the entry animation for title view. This allows users to override the
     * default fade and slide animation. Returning null will disable the animation.
     */
    @NonNull
    protected Animator onCreateTitleAnimator() {
        return AnimatorInflater.loadAnimator(getContext(),
                R.animator.lb_onboarding_title_enter);
    }

    /**
     * Returns whether the logo enter animation is finished.
     *
     * @return {@code true} if the logo enter transition is finished, {@code false} otherwise
     */
    protected final boolean isLogoAnimationFinished() {
        return mLogoAnimationFinished;
    }

    /**
     * Returns the page count.
     *
     * @return The page count.
     */
    protected abstract int getPageCount();

    /**
     * Returns the title of the given page.
     *
     * @param pageIndex The page index.
     *
     * @return The title of the page.
     */
    @Nullable
    protected abstract CharSequence getPageTitle(int pageIndex);

    /**
     * Returns the description of the given page.
     *
     * @param pageIndex The page index.
     *
     * @return The description of the page.
     */
    @Nullable
    protected abstract CharSequence getPageDescription(int pageIndex);

    /**
     * Returns the index of the current page.
     *
     * @return The index of the current page.
     */
    protected final int getCurrentPageIndex() {
        return mCurrentPageIndex;
    }

    /**
     * Called to have the inherited class create background view. This is optional and the fragment
     * which doesn't have the background view can return {@code null}. This is called inside
     * {@link #onCreateView}.
     *
     * @param inflater The LayoutInflater object that can be used to inflate the views,
     * @param container The parent view that the additional views are attached to.The fragment
     *        should not add the view by itself.
     *
     * @return The background view for the onboarding screen, or {@code null}.
     */
    @Nullable
    protected abstract View onCreateBackgroundView(
            @NonNull LayoutInflater inflater,
            @NonNull ViewGroup container
    );

    /**
     * Called to have the inherited class create content view. This is optional and the fragment
     * which doesn't have the content view can return {@code null}. This is called inside
     * {@link #onCreateView}.
     *
     * <p>The content view would be located at the center of the screen.
     *
     * @param inflater The LayoutInflater object that can be used to inflate the views,
     * @param container The parent view that the additional views are attached to.The fragment
     *        should not add the view by itself.
     *
     * @return The content view for the onboarding screen, or {@code null}.
     */
    @Nullable
    protected abstract View onCreateContentView(
            @NonNull LayoutInflater inflater,
            @NonNull ViewGroup container
    );

    /**
     * Called to have the inherited class create foreground view. This is optional and the fragment
     * which doesn't need the foreground view can return {@code null}. This is called inside
     * {@link #onCreateView}.
     *
     * <p>This foreground view would have the highest z-order.
     *
     * @param inflater The LayoutInflater object that can be used to inflate the views,
     * @param container The parent view that the additional views are attached to.The fragment
     *        should not add the view by itself.
     *
     * @return The foreground view for the onboarding screen, or {@code null}.
     */
    @Nullable
    protected abstract View onCreateForegroundView(
            @NonNull LayoutInflater inflater,
            @NonNull ViewGroup container
    );

    /**
     * Called when the onboarding flow finishes.
     */
    protected void onFinishFragment() { }

    /**
     * Called when the page changes.
     */
    private void onPageChangedInternal(int previousPage) {
        if (mAnimator != null) {
            mAnimator.end();
        }
        mPageIndicator.onPageSelected(mCurrentPageIndex, true);

        List<Animator> animators = new ArrayList<>();
        // Header animation
        Animator fadeAnimator = null;
        if (previousPage < getCurrentPageIndex()) {
            // sliding to left
            animators.add(createAnimator(mTitleView, false, Gravity.START, 0));
            animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.START,
                    DESCRIPTION_START_DELAY_MS));
            animators.add(createAnimator(mTitleView, true, Gravity.END,
                    HEADER_APPEAR_DELAY_MS));
            animators.add(createAnimator(mDescriptionView, true, Gravity.END,
                    HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS));
        } else {
            // sliding to right
            animators.add(createAnimator(mTitleView, false, Gravity.END, 0));
            animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.END,
                    DESCRIPTION_START_DELAY_MS));
            animators.add(createAnimator(mTitleView, true, Gravity.START,
                    HEADER_APPEAR_DELAY_MS));
            animators.add(createAnimator(mDescriptionView, true, Gravity.START,
                    HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS));
        }
        final int currentPageIndex = getCurrentPageIndex();
        fadeAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mTitleView.setText(getPageTitle(currentPageIndex));
                mDescriptionView.setText(getPageDescription(currentPageIndex));
            }
        });

        final Context context = getContext();
        // Animator for switching between page indicator and button.
        if (getCurrentPageIndex() == getPageCount() - 1) {
            mStartButton.setVisibility(View.VISIBLE);
            Animator navigatorFadeOutAnimator = AnimatorInflater.loadAnimator(context,
                    R.animator.lb_onboarding_page_indicator_fade_out);
            navigatorFadeOutAnimator.setTarget(mPageIndicator);
            navigatorFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mPageIndicator.setVisibility(View.GONE);
                }
            });
            animators.add(navigatorFadeOutAnimator);
            Animator buttonFadeInAnimator = AnimatorInflater.loadAnimator(context,
                    R.animator.lb_onboarding_start_button_fade_in);
            buttonFadeInAnimator.setTarget(mStartButton);
            animators.add(buttonFadeInAnimator);
        } else if (previousPage == getPageCount() - 1) {
            mPageIndicator.setVisibility(View.VISIBLE);
            Animator navigatorFadeInAnimator = AnimatorInflater.loadAnimator(context,
                    R.animator.lb_onboarding_page_indicator_fade_in);
            navigatorFadeInAnimator.setTarget(mPageIndicator);
            animators.add(navigatorFadeInAnimator);
            Animator buttonFadeOutAnimator = AnimatorInflater.loadAnimator(context,
                    R.animator.lb_onboarding_start_button_fade_out);
            buttonFadeOutAnimator.setTarget(mStartButton);
            buttonFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mStartButton.setVisibility(View.GONE);
                }
            });
            animators.add(buttonFadeOutAnimator);
        }
        mAnimator = new AnimatorSet();
        mAnimator.playTogether(animators);
        mAnimator.start();
        onPageChanged(mCurrentPageIndex, previousPage);
    }

    /**
     * Called when the page has been changed.
     *
     * @param newPage The new page.
     * @param previousPage The previous page.
     */
    protected void onPageChanged(int newPage, int previousPage) { }

    private Animator createAnimator(View view, boolean fadeIn, int slideDirection,
            long startDelay) {
        boolean isLtr = getView().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
        boolean slideRight = (isLtr && slideDirection == Gravity.END)
                || (!isLtr && slideDirection == Gravity.START)
                || slideDirection == Gravity.RIGHT;
        Animator fadeAnimator;
        Animator slideAnimator;
        if (fadeIn) {
            fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 0.0f, 1.0f);
            slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X,
                    slideRight ? sSlideDistance : -sSlideDistance, 0);
            fadeAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR);
            slideAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR);
        } else {
            fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1.0f, 0.0f);
            slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0,
                    slideRight ? sSlideDistance : -sSlideDistance);
            fadeAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR);
            slideAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR);
        }
        fadeAnimator.setDuration(HEADER_ANIMATION_DURATION_MS);
        fadeAnimator.setTarget(view);
        slideAnimator.setDuration(HEADER_ANIMATION_DURATION_MS);
        slideAnimator.setTarget(view);
        AnimatorSet animator = new AnimatorSet();
        animator.playTogether(fadeAnimator, slideAnimator);
        if (startDelay > 0) {
            animator.setStartDelay(startDelay);
        }
        return animator;
    }

    /**
     * Sets the resource id for the main icon.
     */
    public final void setIconResouceId(int resourceId) {
        this.mIconResourceId = resourceId;
        if (mMainIconView != null) {
            mMainIconView.setImageResource(resourceId);
            mMainIconView.setVisibility(View.VISIBLE);
        }
    }

    /**
     * Returns the resource id of the main icon.
     */
    public final int getIconResourceId() {
        return mIconResourceId;
    }
}