1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v17.leanback.app;
18
19import android.animation.Animator;
20import android.animation.AnimatorInflater;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.AnimatorSet;
23import android.animation.ObjectAnimator;
24import android.animation.TimeInterpolator;
25import android.app.Activity;
26import android.app.Fragment;
27import android.os.Bundle;
28import android.support.annotation.Nullable;
29import android.support.v17.leanback.R;
30import android.support.v17.leanback.widget.PagingIndicator;
31import android.util.Log;
32import android.util.TypedValue;
33import android.view.ContextThemeWrapper;
34import android.view.Gravity;
35import android.view.KeyEvent;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.View.OnClickListener;
39import android.view.View.OnKeyListener;
40import android.view.ViewGroup;
41import android.view.ViewTreeObserver.OnPreDrawListener;
42import android.view.animation.AccelerateInterpolator;
43import android.view.animation.DecelerateInterpolator;
44import android.widget.ImageView;
45import android.widget.TextView;
46
47import java.util.ArrayList;
48import java.util.List;
49
50/**
51 * An OnboardingFragment provides a common and simple way to build onboarding screen for
52 * applications.
53 * <p>
54 * <h3>Building the screen</h3>
55 * The view structure of onboarding screen is composed of the common parts and custom parts. The
56 * common parts are composed of title, description and page navigator and the custom parts are
57 * composed of background, contents and foreground.
58 * <p>
59 * To build the screen views, the inherited class should override:
60 * <ul>
61 * <li>{@link #onCreateBackgroundView} to provide the background view. Background view has the same
62 * size as the screen and the lowest z-order.</li>
63 * <li>{@link #onCreateContentView} to provide the contents view. The content view is located in
64 * the content area at the center of the screen.</li>
65 * <li>{@link #onCreateForegroundView} to provide the foreground view. Foreground view has the same
66 * size as the screen and the highest z-order</li>
67 * </ul>
68 * <p>
69 * Each of these methods can return {@code null} if the application doesn't want to provide it.
70 * <p>
71 * <h3>Page information</h3>
72 * The onboarding screen may have several pages which explain the functionality of the application.
73 * The inherited class should provide the page information by overriding the methods:
74 * <p>
75 * <ul>
76 * <li>{@link #getPageCount} to provide the number of pages.</li>
77 * <li>{@link #getPageTitle} to provide the title of the page.</li>
78 * <li>{@link #getPageDescription} to provide the description of the page.</li>
79 * </ul>
80 * <p>
81 * Note that the information is used in {@link #onCreateView}, so should be initialized before
82 * calling {@code super.onCreateView}.
83 * <p>
84 * <h3>Animation</h3>
85 * Onboarding screen has three kinds of animations:
86 * <p>
87 * <h4>Logo Splash Animation</a></h4>
88 * When onboarding screen appears, the logo splash animation is played by default. The animation
89 * fades in the logo image, pauses in a few seconds and fades it out.
90 * <p>
91 * In most cases, the logo animation needs to be customized because the logo images of applications
92 * are different from each other, or some applications may want to show their own animations.
93 * <p>
94 * The logo animation can be customized in two ways:
95 * <ul>
96 * <li>The simplest way is to provide the logo image by calling {@link #setLogoResourceId} to show
97 * the default logo animation. This method should be called in {@link Fragment#onCreateView}.</li>
98 * <li>If the logo animation is complex, then override {@link #onCreateLogoAnimation} and return the
99 * {@link Animator} object to run.</li>
100 * </ul>
101 * <p>
102 * If the inherited class provides neither the logo image nor the animation, the logo animation will
103 * be omitted.
104 * <h4>Page enter animation</h4>
105 * After logo animation finishes, page enter animation starts. The application can provide the
106 * animations of custom views by overriding {@link #onCreateEnterAnimation}.
107 * <h4>Page change animation</h4>
108 * When the page changes, the default animations of the title and description are played. The
109 * inherited class can override {@link #onPageChanged} to start the custom animations.
110 * <p>
111 * <h3>Finishing the screen</h3>
112 * <p>
113 * If the user finishes the onboarding screen after navigating all the pages,
114 * {@link #onFinishFragment} is called. The inherited class can override this method to show another
115 * fragment or activity, or just remove this fragment.
116 * <p>
117 * <h3>Theming</h3>
118 * <p>
119 * OnboardingFragment must have access to an appropriate theme. Specifically, the fragment must
120 * receive  {@link R.style#Theme_Leanback_Onboarding}, or a theme whose parent is set to that theme.
121 * Themes can be provided in one of three ways:
122 * <ul>
123 * <li>The simplest way is to set the theme for the host Activity to the Onboarding theme or a theme
124 * that derives from it.</li>
125 * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the
126 * existing Activity theme can have an entry added for the attribute
127 * {@link R.styleable#LeanbackOnboardingTheme_onboardingTheme}. If present, this theme will be used
128 * by OnboardingFragment as an overlay to the Activity's theme.</li>
129 * <li>Finally, custom subclasses of OnboardingFragment may provide a theme through the
130 * {@link #onProvideTheme} method. This can be useful if a subclass is used across multiple
131 * Activities.</li>
132 * </ul>
133 * <p>
134 * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by
135 * the Activity's theme. (Themes whose parent theme is already set to the onboarding theme do not
136 * need to set the onboardingTheme attribute; if set, it will be ignored.)
137 *
138 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTheme
139 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingHeaderStyle
140 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTitleStyle
141 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingDescriptionStyle
142 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingNavigatorContainerStyle
143 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingPageIndicatorStyle
144 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle
145 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingLogoStyle
146 */
147abstract public class OnboardingFragment extends Fragment {
148    private static final String TAG = "OnboardingFragment";
149    private static final boolean DEBUG = false;
150
151    private static final long LOGO_SPLASH_PAUSE_DURATION_MS = 1333;
152    private static final long START_DELAY_TITLE_MS = 33;
153    private static final long START_DELAY_DESCRIPTION_MS = 33;
154
155    private static final long HEADER_ANIMATION_DURATION_MS = 417;
156    private static final long DESCRIPTION_START_DELAY_MS = 33;
157    private static final long HEADER_APPEAR_DELAY_MS = 500;
158    private static final int SLIDE_DISTANCE = 60;
159
160    private static int sSlideDistance;
161
162    private static final TimeInterpolator HEADER_APPEAR_INTERPOLATOR = new DecelerateInterpolator();
163    private static final TimeInterpolator HEADER_DISAPPEAR_INTERPOLATOR
164            = new AccelerateInterpolator();
165
166    // Keys used to save and restore the states.
167    private static final String KEY_CURRENT_PAGE_INDEX = "leanback.onboarding.current_page_index";
168
169    private ContextThemeWrapper mThemeWrapper;
170
171    private PagingIndicator mPageIndicator;
172    private View mStartButton;
173    private ImageView mLogoView;
174    private TextView mTitleView;
175    private TextView mDescriptionView;
176
177    private boolean mIsLtr;
178
179    // No need to save/restore the logo resource ID, because the logo animation will not appear when
180    // the fragment is restored.
181    private int mLogoResourceId;
182    private boolean mEnterTransitionFinished;
183    private int mCurrentPageIndex;
184
185    private AnimatorSet mAnimator;
186
187    private final OnClickListener mOnClickListener = new OnClickListener() {
188        @Override
189        public void onClick(View view) {
190            if (!mEnterTransitionFinished) {
191                // Do not change page until the enter transition finishes.
192                return;
193            }
194            if (mCurrentPageIndex == getPageCount() - 1) {
195                onFinishFragment();
196            } else {
197                moveToNextPage();
198            }
199        }
200    };
201
202    private final OnKeyListener mOnKeyListener = new OnKeyListener() {
203        @Override
204        public boolean onKey(View v, int keyCode, KeyEvent event) {
205            if (!mEnterTransitionFinished) {
206                // Ignore key event until the enter transition finishes.
207                return keyCode != KeyEvent.KEYCODE_BACK;
208            }
209            if (event.getAction() == KeyEvent.ACTION_DOWN) {
210                return false;
211            }
212            switch (keyCode) {
213                case KeyEvent.KEYCODE_BACK:
214                    if (mCurrentPageIndex == 0) {
215                        return false;
216                    }
217                    moveToPreviousPage();
218                    return true;
219                case KeyEvent.KEYCODE_DPAD_LEFT:
220                    if (mIsLtr) {
221                        moveToPreviousPage();
222                    } else {
223                        moveToNextPage();
224                    }
225                    return true;
226                case KeyEvent.KEYCODE_DPAD_RIGHT:
227                    if (mIsLtr) {
228                        moveToNextPage();
229                    } else {
230                        moveToPreviousPage();
231                    }
232                    return true;
233            }
234            return false;
235        }
236    };
237
238    private void moveToPreviousPage() {
239        if (mCurrentPageIndex > 0) {
240            --mCurrentPageIndex;
241            onPageChangedInternal(mCurrentPageIndex + 1);
242        }
243    }
244    private void moveToNextPage() {
245        if (mCurrentPageIndex < getPageCount() - 1) {
246            ++mCurrentPageIndex;
247            onPageChangedInternal(mCurrentPageIndex - 1);
248        }
249    }
250
251    @Nullable
252    @Override
253    public View onCreateView(LayoutInflater inflater, final ViewGroup container,
254            Bundle savedInstanceState) {
255        resolveTheme();
256        LayoutInflater localInflater = getThemeInflater(inflater);
257        final ViewGroup view = (ViewGroup) localInflater.inflate(R.layout.lb_onboarding_fragment,
258                container, false);
259        mIsLtr = getResources().getConfiguration().getLayoutDirection()
260                == View.LAYOUT_DIRECTION_LTR;
261        mPageIndicator = (PagingIndicator) view.findViewById(R.id.page_indicator);
262        mPageIndicator.setOnClickListener(mOnClickListener);
263        mPageIndicator.setOnKeyListener(mOnKeyListener);
264        mStartButton = view.findViewById(R.id.button_start);
265        mStartButton.setOnClickListener(mOnClickListener);
266        mStartButton.setOnKeyListener(mOnKeyListener);
267        mLogoView = (ImageView) view.findViewById(R.id.logo);
268        mTitleView = (TextView) view.findViewById(R.id.title);
269        mDescriptionView = (TextView) view.findViewById(R.id.description);
270        if (sSlideDistance == 0) {
271            sSlideDistance = (int) (SLIDE_DISTANCE * getActivity().getResources()
272                    .getDisplayMetrics().scaledDensity);
273        }
274        if (savedInstanceState == null) {
275            mCurrentPageIndex = 0;
276            mEnterTransitionFinished = false;
277            mPageIndicator.onPageSelected(0, false);
278            view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
279                @Override
280                public boolean onPreDraw() {
281                    view.getViewTreeObserver().removeOnPreDrawListener(this);
282                    if (!startLogoAnimation()) {
283                        startEnterAnimation();
284                    }
285                    return true;
286                }
287            });
288        } else {
289            mEnterTransitionFinished = true;
290            mCurrentPageIndex = savedInstanceState.getInt(KEY_CURRENT_PAGE_INDEX);
291            initializeViews(view);
292        }
293        view.requestFocus();
294        return view;
295    }
296
297    @Override
298    public void onSaveInstanceState(Bundle outState) {
299        super.onSaveInstanceState(outState);
300        outState.putInt(KEY_CURRENT_PAGE_INDEX, mCurrentPageIndex);
301    }
302
303    /**
304     * Returns the theme used for styling the fragment. The default returns -1, indicating that the
305     * host Activity's theme should be used.
306     *
307     * @return The theme resource ID of the theme to use in this fragment, or -1 to use the host
308     *         Activity's theme.
309     */
310    public int onProvideTheme() {
311        return -1;
312    }
313
314    private void resolveTheme() {
315        Activity activity = getActivity();
316        int theme = onProvideTheme();
317        if (theme == -1) {
318            // Look up the onboardingTheme in the activity's currently specified theme. If it
319            // exists, wrap the theme with its value.
320            int resId = R.attr.onboardingTheme;
321            TypedValue typedValue = new TypedValue();
322            boolean found = activity.getTheme().resolveAttribute(resId, typedValue, true);
323            if (DEBUG) Log.v(TAG, "Found onboarding theme reference? " + found);
324            if (found) {
325                mThemeWrapper = new ContextThemeWrapper(activity, typedValue.resourceId);
326            }
327        } else {
328            mThemeWrapper = new ContextThemeWrapper(activity, theme);
329        }
330    }
331
332    private LayoutInflater getThemeInflater(LayoutInflater inflater) {
333        return mThemeWrapper == null ? inflater : inflater.cloneInContext(mThemeWrapper);
334    }
335
336    /**
337     * Sets the resource ID of the splash logo image. If the logo resource id set, the default logo
338     * splash animation will be played.
339     *
340     * @param id The resource ID of the logo image.
341     */
342    public final void setLogoResourceId(int id) {
343        mLogoResourceId = id;
344    }
345
346    /**
347     * Returns the resource ID of the splash logo image.
348     *
349     * @return The resource ID of the splash logo image.
350     */
351    public final int getLogoResourceId() {
352        return mLogoResourceId;
353    }
354
355    /**
356     * Called to have the inherited class create its own logo animation.
357     * <p>
358     * This is called only if the logo image resource ID is not set by {@link #setLogoResourceId}.
359     * If this returns {@code null}, the logo animation is skipped.
360     *
361     * @return The {@link Animator} object which runs the logo animation.
362     */
363    @Nullable
364    protected Animator onCreateLogoAnimation() {
365        return null;
366    }
367
368    private boolean startLogoAnimation() {
369        Animator animator = null;
370        if (mLogoResourceId != 0) {
371            mLogoView.setVisibility(View.VISIBLE);
372            mLogoView.setImageResource(mLogoResourceId);
373            Animator inAnimator = AnimatorInflater.loadAnimator(getActivity(),
374                    R.animator.lb_onboarding_logo_enter);
375            Animator outAnimator = AnimatorInflater.loadAnimator(getActivity(),
376                    R.animator.lb_onboarding_logo_exit);
377            outAnimator.setStartDelay(LOGO_SPLASH_PAUSE_DURATION_MS);
378            AnimatorSet logoAnimator = new AnimatorSet();
379            logoAnimator.playSequentially(inAnimator, outAnimator);
380            logoAnimator.setTarget(mLogoView);
381            animator = logoAnimator;
382        } else {
383            animator = onCreateLogoAnimation();
384        }
385        if (animator != null) {
386            animator.addListener(new AnimatorListenerAdapter() {
387                @Override
388                public void onAnimationEnd(Animator animation) {
389                    if (getActivity() != null) {
390                        startEnterAnimation();
391                    }
392                }
393            });
394            animator.start();
395            return true;
396        }
397        return false;
398    }
399
400    /**
401     * Called to have the inherited class create its enter animation. The start animation runs after
402     * logo animation ends.
403     *
404     * @return The {@link Animator} object which runs the page enter animation.
405     */
406    @Nullable
407    protected Animator onCreateEnterAnimation() {
408        return null;
409    }
410
411    private void initializeViews(View container) {
412        mLogoView.setVisibility(View.GONE);
413        // Create custom views.
414        LayoutInflater inflater = getThemeInflater(LayoutInflater.from(getActivity()));
415        ViewGroup backgroundContainer = (ViewGroup) container.findViewById(
416                R.id.background_container);
417        View background = onCreateBackgroundView(inflater, backgroundContainer);
418        if (background != null) {
419            backgroundContainer.setVisibility(View.VISIBLE);
420            backgroundContainer.addView(background);
421        }
422        ViewGroup contentContainer = (ViewGroup) container.findViewById(R.id.content_container);
423        View content = onCreateContentView(inflater, contentContainer);
424        if (content != null) {
425            contentContainer.setVisibility(View.VISIBLE);
426            contentContainer.addView(content);
427        }
428        ViewGroup foregroundContainer = (ViewGroup) container.findViewById(
429                R.id.foreground_container);
430        View foreground = onCreateForegroundView(inflater, foregroundContainer);
431        if (foreground != null) {
432            foregroundContainer.setVisibility(View.VISIBLE);
433            foregroundContainer.addView(foreground);
434        }
435        // Make views visible which were invisible while logo animation is running.
436        container.findViewById(R.id.page_container).setVisibility(View.VISIBLE);
437        container.findViewById(R.id.content_container).setVisibility(View.VISIBLE);
438        if (getPageCount() > 1) {
439            mPageIndicator.setPageCount(getPageCount());
440            mPageIndicator.onPageSelected(mCurrentPageIndex, false);
441        }
442        if (mCurrentPageIndex == getPageCount() - 1) {
443            mStartButton.setVisibility(View.VISIBLE);
444        } else {
445            mPageIndicator.setVisibility(View.VISIBLE);
446        }
447        // Header views.
448        mTitleView.setText(getPageTitle(mCurrentPageIndex));
449        mDescriptionView.setText(getPageDescription(mCurrentPageIndex));
450    }
451
452    private void startEnterAnimation() {
453        mEnterTransitionFinished = true;
454        initializeViews(getView());
455        List<Animator> animators = new ArrayList<>();
456        Animator animator = AnimatorInflater.loadAnimator(getActivity(),
457                R.animator.lb_onboarding_page_indicator_enter);
458        animator.setTarget(getPageCount() <= 1 ? mStartButton : mPageIndicator);
459        animators.add(animator);
460        // Header title
461        View view = getActivity().findViewById(R.id.title);
462        view.setAlpha(0);
463        animator = AnimatorInflater.loadAnimator(getActivity(),
464                R.animator.lb_onboarding_title_enter);
465        animator.setStartDelay(START_DELAY_TITLE_MS);
466        animator.setTarget(view);
467        animators.add(animator);
468        // Header description
469        view = getActivity().findViewById(R.id.description);
470        view.setAlpha(0);
471        animator = AnimatorInflater.loadAnimator(getActivity(),
472                R.animator.lb_onboarding_description_enter);
473        animator.setStartDelay(START_DELAY_DESCRIPTION_MS);
474        animator.setTarget(view);
475        animators.add(animator);
476        // Customized animation by the inherited class.
477        Animator customAnimator = onCreateEnterAnimation();
478        if (customAnimator != null) {
479            animators.add(customAnimator);
480        }
481        mAnimator = new AnimatorSet();
482        mAnimator.playTogether(animators);
483        mAnimator.start();
484        // Search focus and give the focus to the appropriate child which has become visible.
485        getView().requestFocus();
486    }
487
488    /**
489     * Returns the page count.
490     *
491     * @return The page count.
492     */
493    abstract protected int getPageCount();
494
495    /**
496     * Returns the title of the given page.
497     *
498     * @param pageIndex The page index.
499     *
500     * @return The title of the page.
501     */
502    abstract protected CharSequence getPageTitle(int pageIndex);
503
504    /**
505     * Returns the description of the given page.
506     *
507     * @param pageIndex The page index.
508     *
509     * @return The description of the page.
510     */
511    abstract protected CharSequence getPageDescription(int pageIndex);
512
513    /**
514     * Returns the index of the current page.
515     *
516     * @return The index of the current page.
517     */
518    protected final int getCurrentPageIndex() {
519        return mCurrentPageIndex;
520    }
521
522    /**
523     * Called to have the inherited class create background view. This is optional and the fragment
524     * which doesn't have the background view can return {@code null}. This is called inside
525     * {@link #onCreateView}.
526     *
527     * @param inflater The LayoutInflater object that can be used to inflate the views,
528     * @param container The parent view that the additional views are attached to.The fragment
529     *        should not add the view by itself.
530     *
531     * @return The background view for the onboarding screen, or {@code null}.
532     */
533    @Nullable
534    abstract protected View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container);
535
536    /**
537     * Called to have the inherited class create content view. This is optional and the fragment
538     * which doesn't have the content view can return {@code null}. This is called inside
539     * {@link #onCreateView}.
540     *
541     * <p>The content view would be located at the center of the screen.
542     *
543     * @param inflater The LayoutInflater object that can be used to inflate the views,
544     * @param container The parent view that the additional views are attached to.The fragment
545     *        should not add the view by itself.
546     *
547     * @return The content view for the onboarding screen, or {@code null}.
548     */
549    @Nullable
550    abstract protected View onCreateContentView(LayoutInflater inflater, ViewGroup container);
551
552    /**
553     * Called to have the inherited class create foreground view. This is optional and the fragment
554     * which doesn't need the foreground view can return {@code null}. This is called inside
555     * {@link #onCreateView}.
556     *
557     * <p>This foreground view would have the highest z-order.
558     *
559     * @param inflater The LayoutInflater object that can be used to inflate the views,
560     * @param container The parent view that the additional views are attached to.The fragment
561     *        should not add the view by itself.
562     *
563     * @return The foreground view for the onboarding screen, or {@code null}.
564     */
565    @Nullable
566    abstract protected View onCreateForegroundView(LayoutInflater inflater, ViewGroup container);
567
568    /**
569     * Called when the onboarding flow finishes.
570     */
571    protected void onFinishFragment() { }
572
573    /**
574     * Called when the page changes.
575     */
576    private void onPageChangedInternal(int previousPage) {
577        if (mAnimator != null) {
578            mAnimator.end();
579        }
580        mPageIndicator.onPageSelected(mCurrentPageIndex, true);
581
582        List<Animator> animators = new ArrayList<>();
583        // Header animation
584        Animator fadeAnimator = null;
585        if (previousPage < getCurrentPageIndex()) {
586            // sliding to left
587            animators.add(createAnimator(mTitleView, false, Gravity.START, 0));
588            animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.START,
589                    DESCRIPTION_START_DELAY_MS));
590            animators.add(createAnimator(mTitleView, true, Gravity.END,
591                    HEADER_APPEAR_DELAY_MS));
592            animators.add(createAnimator(mDescriptionView, true, Gravity.END,
593                    HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS));
594        } else {
595            // sliding to right
596            animators.add(createAnimator(mTitleView, false, Gravity.END, 0));
597            animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.END,
598                    DESCRIPTION_START_DELAY_MS));
599            animators.add(createAnimator(mTitleView, true, Gravity.START,
600                    HEADER_APPEAR_DELAY_MS));
601            animators.add(createAnimator(mDescriptionView, true, Gravity.START,
602                    HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS));
603        }
604        final int currentPageIndex = getCurrentPageIndex();
605        fadeAnimator.addListener(new AnimatorListenerAdapter() {
606            @Override
607            public void onAnimationEnd(Animator animation) {
608                mTitleView.setText(getPageTitle(currentPageIndex));
609                mDescriptionView.setText(getPageDescription(currentPageIndex));
610            }
611        });
612
613        // Animator for switching between page indicator and button.
614        if (getCurrentPageIndex() == getPageCount() - 1) {
615            mStartButton.setVisibility(View.VISIBLE);
616            Animator navigatorFadeOutAnimator = AnimatorInflater.loadAnimator(getActivity(),
617                    R.animator.lb_onboarding_page_indicator_fade_out);
618            navigatorFadeOutAnimator.setTarget(mPageIndicator);
619            navigatorFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
620                @Override
621                public void onAnimationEnd(Animator animation) {
622                    mPageIndicator.setVisibility(View.GONE);
623                }
624            });
625            animators.add(navigatorFadeOutAnimator);
626            Animator buttonFadeInAnimator = AnimatorInflater.loadAnimator(getActivity(),
627                    R.animator.lb_onboarding_start_button_fade_in);
628            buttonFadeInAnimator.setTarget(mStartButton);
629            animators.add(buttonFadeInAnimator);
630        } else if (previousPage == getPageCount() - 1) {
631            mPageIndicator.setVisibility(View.VISIBLE);
632            Animator navigatorFadeInAnimator = AnimatorInflater.loadAnimator(getActivity(),
633                    R.animator.lb_onboarding_page_indicator_fade_in);
634            navigatorFadeInAnimator.setTarget(mPageIndicator);
635            animators.add(navigatorFadeInAnimator);
636            Animator buttonFadeOutAnimator = AnimatorInflater.loadAnimator(getActivity(),
637                    R.animator.lb_onboarding_start_button_fade_out);
638            buttonFadeOutAnimator.setTarget(mStartButton);
639            buttonFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
640                @Override
641                public void onAnimationEnd(Animator animation) {
642                    mStartButton.setVisibility(View.GONE);
643                }
644            });
645            animators.add(buttonFadeOutAnimator);
646        }
647        mAnimator = new AnimatorSet();
648        mAnimator.playTogether(animators);
649        mAnimator.start();
650        onPageChanged(mCurrentPageIndex, previousPage);
651    }
652
653    /**
654     * Called when the page has been changed.
655     *
656     * @param newPage The new page.
657     * @param previousPage The previous page.
658     */
659    protected void onPageChanged(int newPage, int previousPage) { }
660
661    private Animator createAnimator(View view, boolean fadeIn, int slideDirection,
662            long startDelay) {
663        boolean isLtr = getView().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
664        boolean slideRight = (isLtr && slideDirection == Gravity.END)
665                || (!isLtr && slideDirection == Gravity.START)
666                || slideDirection == Gravity.RIGHT;
667        Animator fadeAnimator;
668        Animator slideAnimator;
669        if (fadeIn) {
670            fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 0.0f, 1.0f);
671            slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X,
672                    slideRight ? sSlideDistance : -sSlideDistance, 0);
673            fadeAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR);
674            slideAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR);
675        } else {
676            fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1.0f, 0.0f);
677            slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0,
678                    slideRight ? sSlideDistance : -sSlideDistance);
679            fadeAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR);
680            slideAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR);
681        }
682        fadeAnimator.setDuration(HEADER_ANIMATION_DURATION_MS);
683        fadeAnimator.setTarget(view);
684        slideAnimator.setDuration(HEADER_ANIMATION_DURATION_MS);
685        slideAnimator.setTarget(view);
686        AnimatorSet animator = new AnimatorSet();
687        animator.playTogether(fadeAnimator, slideAnimator);
688        if (startDelay > 0) {
689            animator.setStartDelay(startDelay);
690        }
691        return animator;
692    }
693}
694