/* * 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.Nullable; import android.support.v17.leanback.R; import android.support.v17.leanback.widget.PagingIndicator; import android.support.v4.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 OnboardingSupportFragment provides a common and simple way to build onboarding screen for * applications. *

*

Building the screen

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

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

*

* Each of these methods can return {@code null} if the application doesn't want to provide it. *

*

Page information

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

*

*

* Note that the information is used in {@link #onCreateView}, so should be initialized before * calling {@code super.onCreateView}. *

*

Animation

* Onboarding screen has three kinds of animations: *

*

Logo Splash Animation

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

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

* The logo animation can be customized in two ways: *

*

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

Page enter animation

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

Page change animation

* 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. *

*

Finishing the screen

*

* 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. *

*

Theming

*

* OnboardingSupportFragment must have access to an appropriate theme. Specifically, the fragment must * receive {@link R.style#Theme_Leanback_Onboarding}, or a theme whose parent is set to that theme. * Themes can be provided in one of three ways: *

*

* 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 OnboardingSupportFragment extends Fragment { private static final String TAG = "OnboardingF"; private static final boolean DEBUG = false; private static final long LOGO_SPLASH_PAUSE_DURATION_MS = 1333; private static final long HEADER_ANIMATION_DURATION_MS = 417; private static final long DESCRIPTION_START_DELAY_MS = 33; private static final long HEADER_APPEAR_DELAY_MS = 500; private static final int SLIDE_DISTANCE = 60; private static int sSlideDistance; private static final TimeInterpolator HEADER_APPEAR_INTERPOLATOR = new DecelerateInterpolator(); private static final TimeInterpolator HEADER_DISAPPEAR_INTERPOLATOR = new AccelerateInterpolator(); // Keys used to save and restore the states. private static final String KEY_CURRENT_PAGE_INDEX = "leanback.onboarding.current_page_index"; private static final String KEY_LOGO_ANIMATION_FINISHED = "leanback.onboarding.logo_animation_finished"; private static final String KEY_ENTER_ANIMATION_FINISHED = "leanback.onboarding.enter_animation_finished"; private ContextThemeWrapper mThemeWrapper; PagingIndicator mPageIndicator; View mStartButton; private ImageView mLogoView; // Optional icon that can be displayed on top of the header section. private ImageView mMainIconView; private int mIconResourceId; TextView mTitleView; TextView mDescriptionView; boolean mIsLtr; // No need to save/restore the logo resource ID, because the logo animation will not appear when // the fragment is restored. private int mLogoResourceId; boolean mLogoAnimationFinished; boolean mEnterAnimationFinished; int mCurrentPageIndex; @ColorInt private int mTitleViewTextColor = Color.TRANSPARENT; private boolean mTitleViewTextColorSet; @ColorInt private int mDescriptionViewTextColor = Color.TRANSPARENT; private boolean mDescriptionViewTextColorSet; @ColorInt private int mDotBackgroundColor = Color.TRANSPARENT; private boolean mDotBackgroundColorSet; @ColorInt private int mArrowColor = Color.TRANSPARENT; private boolean mArrowColorSet; @ColorInt private int mArrowBackgroundColor = Color.TRANSPARENT; private boolean mArrowBackgroundColorSet; private CharSequence mStartButtonText; private boolean mStartButtonTextSet; private AnimatorSet mAnimator; private final OnClickListener mOnClickListener = new OnClickListener() { @Override public void onClick(View view) { if (!mLogoAnimationFinished) { // Do not change page until the enter transition finishes. return; } if (mCurrentPageIndex == getPageCount() - 1) { onFinishFragment(); } else { moveToNextPage(); } } }; private final OnKeyListener mOnKeyListener = new OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (!mLogoAnimationFinished) { // Ignore key event until the enter transition finishes. return keyCode != KeyEvent.KEYCODE_BACK; } if (event.getAction() == KeyEvent.ACTION_DOWN) { return false; } switch (keyCode) { case KeyEvent.KEYCODE_BACK: if (mCurrentPageIndex == 0) { return false; } moveToPreviousPage(); return true; case KeyEvent.KEYCODE_DPAD_LEFT: if (mIsLtr) { moveToPreviousPage(); } else { moveToNextPage(); } return true; case KeyEvent.KEYCODE_DPAD_RIGHT: if (mIsLtr) { moveToNextPage(); } else { moveToPreviousPage(); } return true; } return false; } }; /** * Navigates to the previous page. */ protected void moveToPreviousPage() { if (!mLogoAnimationFinished) { // Ignore if the logo enter transition is in progress. return; } if (mCurrentPageIndex > 0) { --mCurrentPageIndex; onPageChangedInternal(mCurrentPageIndex + 1); } } /** * Navigates to the next page. */ protected void moveToNextPage() { if (!mLogoAnimationFinished) { // Ignore if the logo enter transition is in progress. return; } if (mCurrentPageIndex < getPageCount() - 1) { ++mCurrentPageIndex; onPageChangedInternal(mCurrentPageIndex - 1); } } @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 = getContext(); if (sSlideDistance == 0) { sSlideDistance = (int) (SLIDE_DISTANCE * context.getResources() .getDisplayMetrics().scaledDensity); } view.requestFocus(); return view; } @Override public void onViewCreated(View view, 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 = getContext(); int theme = onProvideTheme(); if (theme == -1) { // Look up the onboardingTheme in the activity's currently specified theme. If it // exists, wrap the theme with its value. int resId = R.attr.onboardingTheme; TypedValue typedValue = new TypedValue(); boolean found = context.getTheme().resolveAttribute(resId, typedValue, true); if (DEBUG) Log.v(TAG, "Found onboarding theme reference? " + found); if (found) { mThemeWrapper = new ContextThemeWrapper(context, typedValue.resourceId); } } else { mThemeWrapper = new ContextThemeWrapper(context, theme); } } private LayoutInflater getThemeInflater(LayoutInflater inflater) { return mThemeWrapper == null ? inflater : inflater.cloneInContext(mThemeWrapper); } /** * Sets the resource ID of the splash logo image. If the logo resource id set, the default logo * splash animation will be played. * * @param id The resource ID of the logo image. */ public final void setLogoResourceId(int id) { mLogoResourceId = id; } /** * Returns the resource ID of the splash logo image. * * @return The resource ID of the splash logo image. */ public final int getLogoResourceId() { return mLogoResourceId; } /** * Called to have the inherited class create its own logo animation. *

* This is called only if the logo image resource ID is not set by {@link #setLogoResourceId}. * If this returns {@code null}, the logo animation is skipped. * * @return The {@link Animator} object which runs the logo animation. */ @Nullable protected Animator onCreateLogoAnimation() { return null; } boolean startLogoAnimation() { final Context context = getContext(); if (context == null) { return false; } Animator animator = null; if (mLogoResourceId != 0) { mLogoView.setVisibility(View.VISIBLE); mLogoView.setImageResource(mLogoResourceId); Animator inAnimator = AnimatorInflater.loadAnimator(context, R.animator.lb_onboarding_logo_enter); Animator outAnimator = AnimatorInflater.loadAnimator(context, R.animator.lb_onboarding_logo_exit); outAnimator.setStartDelay(LOGO_SPLASH_PAUSE_DURATION_MS); AnimatorSet logoAnimator = new AnimatorSet(); logoAnimator.playSequentially(inAnimator, outAnimator); logoAnimator.setTarget(mLogoView); animator = logoAnimator; } else { animator = onCreateLogoAnimation(); } if (animator != null) { animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (context != null) { mLogoAnimationFinished = true; onLogoAnimationFinished(); } } }); animator.start(); return true; } return false; } /** * Called to have the inherited class create its enter animation. The start animation runs after * logo animation ends. * * @return The {@link Animator} object which runs the page enter animation. */ @Nullable protected Animator onCreateEnterAnimation() { return null; } /** * Hides the logo view and makes other fragment views visible. Also initializes the texts for * Title and Description views. */ void hideLogoView() { mLogoView.setVisibility(View.GONE); if (mIconResourceId != 0) { mMainIconView.setImageResource(mIconResourceId); mMainIconView.setVisibility(View.VISIBLE); } View container = getView(); // Create custom views. LayoutInflater inflater = getThemeInflater(LayoutInflater.from( getContext())); ViewGroup backgroundContainer = (ViewGroup) container.findViewById( R.id.background_container); View background = onCreateBackgroundView(inflater, backgroundContainer); if (background != null) { backgroundContainer.setVisibility(View.VISIBLE); backgroundContainer.addView(background); } ViewGroup contentContainer = (ViewGroup) container.findViewById(R.id.content_container); View content = onCreateContentView(inflater, contentContainer); if (content != null) { contentContainer.setVisibility(View.VISIBLE); contentContainer.addView(content); } ViewGroup foregroundContainer = (ViewGroup) container.findViewById( R.id.foreground_container); View foreground = onCreateForegroundView(inflater, foregroundContainer); if (foreground != null) { foregroundContainer.setVisibility(View.VISIBLE); foregroundContainer.addView(foreground); } // Make views visible which were invisible while logo animation is running. container.findViewById(R.id.page_container).setVisibility(View.VISIBLE); container.findViewById(R.id.content_container).setVisibility(View.VISIBLE); if (getPageCount() > 1) { mPageIndicator.setPageCount(getPageCount()); mPageIndicator.onPageSelected(mCurrentPageIndex, false); } if (mCurrentPageIndex == getPageCount() - 1) { mStartButton.setVisibility(View.VISIBLE); } else { mPageIndicator.setVisibility(View.VISIBLE); } // Header views. mTitleView.setText(getPageTitle(mCurrentPageIndex)); mDescriptionView.setText(getPageDescription(mCurrentPageIndex)); } /** * Called immediately after the logo animation is complete or no logo animation is specified. * This method can also be called when the activity is recreated, i.e. when no logo animation * are performed. * By default, this method will hide the logo view and start the entrance animation for this * fragment. * Overriding subclasses can provide their own data loading logic as to when the entrance * animation should be executed. */ protected void onLogoAnimationFinished() { startEnterAnimation(false); } /** * Called to start entrance transition. This can be called by subclasses when the logo animation * and data loading is complete. If force flag is set to false, it will only start the animation * if it's not already done yet. Otherwise, it will always start the enter animation. In both * cases, the logo view will hide and the rest of fragment views become visible after this call. * * @param force {@code true} if enter animation has to be performed regardless of whether it's * been done in the past, {@code false} otherwise */ protected final void startEnterAnimation(boolean force) { final Context context = getContext(); if (context == null) { return; } hideLogoView(); if (mEnterAnimationFinished && !force) { return; } List 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(getContext(), R.animator.lb_onboarding_description_enter); } /** * Provides the entry animation for title view. This allows users to override the * default fade and slide animation. Returning null will disable the animation. */ protected Animator onCreateTitleAnimator() { return AnimatorInflater.loadAnimator(getContext(), R.animator.lb_onboarding_title_enter); } /** * Returns whether the logo enter animation is finished. * * @return {@code true} if the logo enter transition is finished, {@code false} otherwise */ protected final boolean isLogoAnimationFinished() { return mLogoAnimationFinished; } /** * Returns the page count. * * @return The page count. */ 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}. * *

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}. * *

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 animators = new ArrayList<>(); // Header animation Animator fadeAnimator = null; if (previousPage < getCurrentPageIndex()) { // sliding to left animators.add(createAnimator(mTitleView, false, Gravity.START, 0)); animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.START, DESCRIPTION_START_DELAY_MS)); animators.add(createAnimator(mTitleView, true, Gravity.END, HEADER_APPEAR_DELAY_MS)); animators.add(createAnimator(mDescriptionView, true, Gravity.END, HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS)); } else { // sliding to right animators.add(createAnimator(mTitleView, false, Gravity.END, 0)); animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.END, DESCRIPTION_START_DELAY_MS)); animators.add(createAnimator(mTitleView, true, Gravity.START, HEADER_APPEAR_DELAY_MS)); animators.add(createAnimator(mDescriptionView, true, Gravity.START, HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS)); } final int currentPageIndex = getCurrentPageIndex(); fadeAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mTitleView.setText(getPageTitle(currentPageIndex)); mDescriptionView.setText(getPageDescription(currentPageIndex)); } }); final Context context = getContext(); // Animator for switching between page indicator and button. if (getCurrentPageIndex() == getPageCount() - 1) { mStartButton.setVisibility(View.VISIBLE); Animator navigatorFadeOutAnimator = AnimatorInflater.loadAnimator(context, R.animator.lb_onboarding_page_indicator_fade_out); navigatorFadeOutAnimator.setTarget(mPageIndicator); navigatorFadeOutAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mPageIndicator.setVisibility(View.GONE); } }); animators.add(navigatorFadeOutAnimator); Animator buttonFadeInAnimator = AnimatorInflater.loadAnimator(context, R.animator.lb_onboarding_start_button_fade_in); buttonFadeInAnimator.setTarget(mStartButton); animators.add(buttonFadeInAnimator); } else if (previousPage == getPageCount() - 1) { mPageIndicator.setVisibility(View.VISIBLE); Animator navigatorFadeInAnimator = AnimatorInflater.loadAnimator(context, R.animator.lb_onboarding_page_indicator_fade_in); navigatorFadeInAnimator.setTarget(mPageIndicator); animators.add(navigatorFadeInAnimator); Animator buttonFadeOutAnimator = AnimatorInflater.loadAnimator(context, R.animator.lb_onboarding_start_button_fade_out); buttonFadeOutAnimator.setTarget(mStartButton); buttonFadeOutAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mStartButton.setVisibility(View.GONE); } }); animators.add(buttonFadeOutAnimator); } mAnimator = new AnimatorSet(); mAnimator.playTogether(animators); mAnimator.start(); onPageChanged(mCurrentPageIndex, previousPage); } /** * Called when the page has been changed. * * @param newPage The new page. * @param previousPage The previous page. */ protected void onPageChanged(int newPage, int previousPage) { } private Animator createAnimator(View view, boolean fadeIn, int slideDirection, long startDelay) { boolean isLtr = getView().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; boolean slideRight = (isLtr && slideDirection == Gravity.END) || (!isLtr && slideDirection == Gravity.START) || slideDirection == Gravity.RIGHT; Animator fadeAnimator; Animator slideAnimator; if (fadeIn) { fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 0.0f, 1.0f); slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, slideRight ? sSlideDistance : -sSlideDistance, 0); fadeAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR); slideAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR); } else { fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1.0f, 0.0f); slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0, slideRight ? sSlideDistance : -sSlideDistance); fadeAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR); slideAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR); } fadeAnimator.setDuration(HEADER_ANIMATION_DURATION_MS); fadeAnimator.setTarget(view); slideAnimator.setDuration(HEADER_ANIMATION_DURATION_MS); slideAnimator.setTarget(view); AnimatorSet animator = new AnimatorSet(); animator.playTogether(fadeAnimator, slideAnimator); if (startDelay > 0) { animator.setStartDelay(startDelay); } return animator; } /** * Sets the resource id for the main icon. */ public final void setIconResouceId(int resourceId) { this.mIconResourceId = resourceId; if (mMainIconView != null) { mMainIconView.setImageResource(resourceId); mMainIconView.setVisibility(View.VISIBLE); } } /** * Returns the resource id of the main icon. */ public final int getIconResourceId() { return mIconResourceId; } }