summaryrefslogtreecommitdiff
path: root/android/support/v17/leanback/app/OnboardingFragment.java
blob: b69d5a72d31e1ec1f295820c13fa4361a8fac108 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
// CHECKSTYLE:OFF Generated code
/* This file is auto-generated from OnboardingSupportFragment.java.  DO NOT MODIFY. */

/*
 * 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 android.support.v17.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.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v17.leanback.R;
import android.support.v17.leanback.widget.PagingIndicator;
import android.app.Fragment;
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 java.util.ArrayList;
import java.util.List;

/**
 * An OnboardingFragment 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>
 * OnboardingFragment 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 OnboardingFragment as an overlay to the Activity's theme.</li>
 * <li>Finally, custom subclasses of OnboardingFragment 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.)
 *
 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTheme
 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingHeaderStyle
 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTitleStyle
 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingDescriptionStyle
 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingNavigatorContainerStyle
 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingPageIndicatorStyle
 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle
 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingLogoStyle
 */
abstract public class OnboardingFragment 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);
        }
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, final ViewGroup container,
            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 = FragmentUtil.getContext(OnboardingFragment.this);
        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(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.
     */
    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(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 = FragmentUtil.getContext(OnboardingFragment.this);
        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 = FragmentUtil.getContext(OnboardingFragment.this);
        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(
                FragmentUtil.getContext(OnboardingFragment.this)));
        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 = FragmentUtil.getContext(OnboardingFragment.this);
        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.
     */
    protected Animator onCreateDescriptionAnimator() {
        return AnimatorInflater.loadAnimator(FragmentUtil.getContext(OnboardingFragment.this),
                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.
     */
    protected Animator onCreateTitleAnimator() {
        return AnimatorInflater.loadAnimator(FragmentUtil.getContext(OnboardingFragment.this),
                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.
     */
    abstract protected int getPageCount();

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

    /**
     * Returns the description of the given page.
     *
     * @param pageIndex The page index.
     *
     * @return The description of the page.
     */
    abstract protected 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
    abstract protected View onCreateBackgroundView(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 {@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
    abstract protected View onCreateContentView(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 {@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
    abstract protected View onCreateForegroundView(LayoutInflater inflater, 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 = FragmentUtil.getContext(OnboardingFragment.this);
        // 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;
    }
}