• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.support.v17.leanback.app;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorInflater;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.AnimatorSet;
23 import android.animation.ObjectAnimator;
24 import android.animation.TimeInterpolator;
25 import android.app.Activity;
26 import android.app.Fragment;
27 import android.os.Bundle;
28 import android.support.annotation.Nullable;
29 import android.support.v17.leanback.R;
30 import android.support.v17.leanback.widget.PagingIndicator;
31 import android.util.Log;
32 import android.util.TypedValue;
33 import android.view.ContextThemeWrapper;
34 import android.view.Gravity;
35 import android.view.KeyEvent;
36 import android.view.LayoutInflater;
37 import android.view.View;
38 import android.view.View.OnClickListener;
39 import android.view.View.OnKeyListener;
40 import android.view.ViewGroup;
41 import android.view.ViewTreeObserver.OnPreDrawListener;
42 import android.view.animation.AccelerateInterpolator;
43 import android.view.animation.DecelerateInterpolator;
44 import android.widget.ImageView;
45 import android.widget.TextView;
46 
47 import java.util.ArrayList;
48 import 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  */
147 abstract 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 
moveToPreviousPage()238     private void moveToPreviousPage() {
239         if (mCurrentPageIndex > 0) {
240             --mCurrentPageIndex;
241             onPageChangedInternal(mCurrentPageIndex + 1);
242         }
243     }
moveToNextPage()244     private void moveToNextPage() {
245         if (mCurrentPageIndex < getPageCount() - 1) {
246             ++mCurrentPageIndex;
247             onPageChangedInternal(mCurrentPageIndex - 1);
248         }
249     }
250 
251     @Nullable
252     @Override
onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState)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
onSaveInstanceState(Bundle outState)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      */
onProvideTheme()310     public int onProvideTheme() {
311         return -1;
312     }
313 
resolveTheme()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 
getThemeInflater(LayoutInflater inflater)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      */
setLogoResourceId(int id)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      */
getLogoResourceId()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
onCreateLogoAnimation()364     protected Animator onCreateLogoAnimation() {
365         return null;
366     }
367 
startLogoAnimation()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
onCreateEnterAnimation()407     protected Animator onCreateEnterAnimation() {
408         return null;
409     }
410 
initializeViews(View container)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 
startEnterAnimation()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      */
getPageCount()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      */
getPageTitle(int pageIndex)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      */
getPageDescription(int pageIndex)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      */
getCurrentPageIndex()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
onCreateBackgroundView(LayoutInflater inflater, ViewGroup container)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
onCreateContentView(LayoutInflater inflater, ViewGroup container)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
onCreateForegroundView(LayoutInflater inflater, ViewGroup container)566     abstract protected View onCreateForegroundView(LayoutInflater inflater, ViewGroup container);
567 
568     /**
569      * Called when the onboarding flow finishes.
570      */
onFinishFragment()571     protected void onFinishFragment() { }
572 
573     /**
574      * Called when the page changes.
575      */
onPageChangedInternal(int previousPage)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      */
onPageChanged(int newPage, int previousPage)659     protected void onPageChanged(int newPage, int previousPage) { }
660 
createAnimator(View view, boolean fadeIn, int slideDirection, long startDelay)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