| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.leanback.app; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorInflater; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.AnimatorSet; |
| import android.animation.ObjectAnimator; |
| import android.animation.TimeInterpolator; |
| import android.content.Context; |
| import android.graphics.Color; |
| import android.os.Bundle; |
| import android.util.Log; |
| import android.util.TypedValue; |
| import android.view.ContextThemeWrapper; |
| import android.view.Gravity; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.View.OnClickListener; |
| import android.view.View.OnKeyListener; |
| import android.view.ViewGroup; |
| import android.view.ViewTreeObserver.OnPreDrawListener; |
| import android.view.animation.AccelerateInterpolator; |
| import android.view.animation.DecelerateInterpolator; |
| import android.widget.Button; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| |
| import androidx.annotation.ColorInt; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.fragment.app.Fragment; |
| import androidx.leanback.R; |
| import androidx.leanback.widget.PagingIndicator; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * An OnboardingSupportFragment provides a common and simple way to build onboarding screen for |
| * applications. |
| * <p> |
| * <h3>Building the screen</h3> |
| * The view structure of onboarding screen is composed of the common parts and custom parts. The |
| * common parts are composed of icon, title, description and page navigator and the custom parts |
| * are composed of background, contents and foreground. |
| * <p> |
| * To build the screen views, the inherited class should override: |
| * <ul> |
| * <li>{@link #onCreateBackgroundView} to provide the background view. Background view has the same |
| * size as the screen and the lowest z-order.</li> |
| * <li>{@link #onCreateContentView} to provide the contents view. The content view is located in |
| * the content area at the center of the screen.</li> |
| * <li>{@link #onCreateForegroundView} to provide the foreground view. Foreground view has the same |
| * size as the screen and the highest z-order</li> |
| * </ul> |
| * <p> |
| * Each of these methods can return {@code null} if the application doesn't want to provide it. |
| * <p> |
| * <h3>Page information</h3> |
| * The onboarding screen may have several pages which explain the functionality of the application. |
| * The inherited class should provide the page information by overriding the methods: |
| * <p> |
| * <ul> |
| * <li>{@link #getPageCount} to provide the number of pages.</li> |
| * <li>{@link #getPageTitle} to provide the title of the page.</li> |
| * <li>{@link #getPageDescription} to provide the description of the page.</li> |
| * </ul> |
| * <p> |
| * Note that the information is used in {@link #onCreateView}, so should be initialized before |
| * calling {@code super.onCreateView}. |
| * <p> |
| * <h3>Animation</h3> |
| * Onboarding screen has three kinds of animations: |
| * <p> |
| * <h4>Logo Splash Animation</a></h4> |
| * When onboarding screen appears, the logo splash animation is played by default. The animation |
| * fades in the logo image, pauses in a few seconds and fades it out. |
| * <p> |
| * In most cases, the logo animation needs to be customized because the logo images of applications |
| * are different from each other, or some applications may want to show their own animations. |
| * <p> |
| * The logo animation can be customized in two ways: |
| * <ul> |
| * <li>The simplest way is to provide the logo image by calling {@link #setLogoResourceId} to show |
| * the default logo animation. This method should be called in {@link Fragment#onCreateView}.</li> |
| * <li>If the logo animation is complex, then override {@link #onCreateLogoAnimation} and return the |
| * {@link Animator} object to run.</li> |
| * </ul> |
| * <p> |
| * If the inherited class provides neither the logo image nor the animation, the logo animation will |
| * be omitted. |
| * <h4>Page enter animation</h4> |
| * After logo animation finishes, page enter animation starts, which causes the header section - |
| * title and description views to fade and slide in. Users can override the default |
| * fade + slide animation by overriding {@link #onCreateTitleAnimator()} & |
| * {@link #onCreateDescriptionAnimator()}. By default we don't animate the custom views but users |
| * can provide animation by overriding {@link #onCreateEnterAnimation}. |
| * |
| * <h4>Page change animation</h4> |
| * When the page changes, the default animations of the title and description are played. The |
| * inherited class can override {@link #onPageChanged} to start the custom animations. |
| * <p> |
| * <h3>Finishing the screen</h3> |
| * <p> |
| * If the user finishes the onboarding screen after navigating all the pages, |
| * {@link #onFinishFragment} is called. The inherited class can override this method to show another |
| * fragment or activity, or just remove this fragment. |
| * <p> |
| * <h3>Theming</h3> |
| * <p> |
| * OnboardingSupportFragment must have access to an appropriate theme. Specifically, the fragment must |
| * receive {@link R.style#Theme_Leanback_Onboarding}, or a theme whose parent is set to that theme. |
| * Themes can be provided in one of three ways: |
| * <ul> |
| * <li>The simplest way is to set the theme for the host Activity to the Onboarding theme or a theme |
| * that derives from it.</li> |
| * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the |
| * existing Activity theme can have an entry added for the attribute |
| * {@link R.styleable#LeanbackOnboardingTheme_onboardingTheme}. If present, this theme will be used |
| * by OnboardingSupportFragment as an overlay to the Activity's theme.</li> |
| * <li>Finally, custom subclasses of OnboardingSupportFragment may provide a theme through the |
| * {@link #onProvideTheme} method. This can be useful if a subclass is used across multiple |
| * Activities.</li> |
| * </ul> |
| * <p> |
| * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by |
| * the Activity's theme. (Themes whose parent theme is already set to the onboarding theme do not |
| * need to set the onboardingTheme attribute; if set, it will be ignored.) |
| * |
| * @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(@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 = getContext(); |
| int theme = onProvideTheme(); |
| if (theme == -1) { |
| // Look up the onboardingTheme in the activity's currently specified theme. If it |
| // exists, wrap the theme with its value. |
| int resId = R.attr.onboardingTheme; |
| TypedValue typedValue = new TypedValue(); |
| boolean found = context.getTheme().resolveAttribute(resId, typedValue, true); |
| if (DEBUG) Log.v(TAG, "Found onboarding theme reference? " + found); |
| if (found) { |
| mThemeWrapper = new ContextThemeWrapper(context, typedValue.resourceId); |
| } |
| } else { |
| mThemeWrapper = new ContextThemeWrapper(context, theme); |
| } |
| } |
| |
| private LayoutInflater getThemeInflater(LayoutInflater inflater) { |
| return mThemeWrapper == null ? inflater : inflater.cloneInContext(mThemeWrapper); |
| } |
| |
| /** |
| * Sets the resource ID of the splash logo image. If the logo resource id set, the default logo |
| * splash animation will be played. |
| * |
| * @param id The resource ID of the logo image. |
| */ |
| public final void setLogoResourceId(int id) { |
| mLogoResourceId = id; |
| } |
| |
| /** |
| * Returns the resource ID of the splash logo image. |
| * |
| * @return The resource ID of the splash logo image. |
| */ |
| public final int getLogoResourceId() { |
| return mLogoResourceId; |
| } |
| |
| /** |
| * Called to have the inherited class create its own logo animation. |
| * <p> |
| * This is called only if the logo image resource ID is not set by {@link #setLogoResourceId}. |
| * If this returns {@code null}, the logo animation is skipped. |
| * |
| * @return The {@link Animator} object which runs the logo animation. |
| */ |
| @Nullable |
| protected Animator onCreateLogoAnimation() { |
| return null; |
| } |
| |
| boolean startLogoAnimation() { |
| final Context context = getContext(); |
| if (context == null) { |
| return false; |
| } |
| Animator animator = null; |
| if (mLogoResourceId != 0) { |
| mLogoView.setVisibility(View.VISIBLE); |
| mLogoView.setImageResource(mLogoResourceId); |
| Animator inAnimator = AnimatorInflater.loadAnimator(context, |
| R.animator.lb_onboarding_logo_enter); |
| Animator outAnimator = AnimatorInflater.loadAnimator(context, |
| R.animator.lb_onboarding_logo_exit); |
| outAnimator.setStartDelay(LOGO_SPLASH_PAUSE_DURATION_MS); |
| AnimatorSet logoAnimator = new AnimatorSet(); |
| logoAnimator.playSequentially(inAnimator, outAnimator); |
| logoAnimator.setTarget(mLogoView); |
| animator = logoAnimator; |
| } else { |
| animator = onCreateLogoAnimation(); |
| } |
| if (animator != null) { |
| animator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| if (context != null) { |
| mLogoAnimationFinished = true; |
| onLogoAnimationFinished(); |
| } |
| } |
| }); |
| animator.start(); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Called to have the inherited class create its enter animation. The start animation runs after |
| * logo animation ends. |
| * |
| * @return The {@link Animator} object which runs the page enter animation. |
| */ |
| @Nullable |
| protected Animator onCreateEnterAnimation() { |
| return null; |
| } |
| |
| |
| /** |
| * Hides the logo view and makes other fragment views visible. Also initializes the texts for |
| * Title and Description views. |
| */ |
| void hideLogoView() { |
| mLogoView.setVisibility(View.GONE); |
| |
| if (mIconResourceId != 0) { |
| mMainIconView.setImageResource(mIconResourceId); |
| mMainIconView.setVisibility(View.VISIBLE); |
| } |
| |
| View container = getView(); |
| // Create custom views. |
| LayoutInflater inflater = getThemeInflater(LayoutInflater.from( |
| getContext())); |
| ViewGroup backgroundContainer = (ViewGroup) container.findViewById( |
| R.id.background_container); |
| View background = onCreateBackgroundView(inflater, backgroundContainer); |
| if (background != null) { |
| backgroundContainer.setVisibility(View.VISIBLE); |
| backgroundContainer.addView(background); |
| } |
| ViewGroup contentContainer = (ViewGroup) container.findViewById(R.id.content_container); |
| View content = onCreateContentView(inflater, contentContainer); |
| if (content != null) { |
| contentContainer.setVisibility(View.VISIBLE); |
| contentContainer.addView(content); |
| } |
| ViewGroup foregroundContainer = (ViewGroup) container.findViewById( |
| R.id.foreground_container); |
| View foreground = onCreateForegroundView(inflater, foregroundContainer); |
| if (foreground != null) { |
| foregroundContainer.setVisibility(View.VISIBLE); |
| foregroundContainer.addView(foreground); |
| } |
| // Make views visible which were invisible while logo animation is running. |
| container.findViewById(R.id.page_container).setVisibility(View.VISIBLE); |
| container.findViewById(R.id.content_container).setVisibility(View.VISIBLE); |
| if (getPageCount() > 1) { |
| mPageIndicator.setPageCount(getPageCount()); |
| mPageIndicator.onPageSelected(mCurrentPageIndex, false); |
| } |
| if (mCurrentPageIndex == getPageCount() - 1) { |
| mStartButton.setVisibility(View.VISIBLE); |
| } else { |
| mPageIndicator.setVisibility(View.VISIBLE); |
| } |
| // Header views. |
| mTitleView.setText(getPageTitle(mCurrentPageIndex)); |
| mDescriptionView.setText(getPageDescription(mCurrentPageIndex)); |
| } |
| |
| /** |
| * Called immediately after the logo animation is complete or no logo animation is specified. |
| * This method can also be called when the activity is recreated, i.e. when no logo animation |
| * are performed. |
| * By default, this method will hide the logo view and start the entrance animation for this |
| * fragment. |
| * Overriding subclasses can provide their own data loading logic as to when the entrance |
| * animation should be executed. |
| */ |
| protected void onLogoAnimationFinished() { |
| startEnterAnimation(false); |
| } |
| |
| /** |
| * Called to start entrance transition. This can be called by subclasses when the logo animation |
| * and data loading is complete. If force flag is set to false, it will only start the animation |
| * if it's not already done yet. Otherwise, it will always start the enter animation. In both |
| * cases, the logo view will hide and the rest of fragment views become visible after this call. |
| * |
| * @param force {@code true} if enter animation has to be performed regardless of whether it's |
| * been done in the past, {@code false} otherwise |
| */ |
| protected final void startEnterAnimation(boolean force) { |
| final Context context = getContext(); |
| if (context == null) { |
| return; |
| } |
| hideLogoView(); |
| if (mEnterAnimationFinished && !force) { |
| return; |
| } |
| List<Animator> animators = new ArrayList<>(); |
| Animator animator = AnimatorInflater.loadAnimator(context, |
| R.animator.lb_onboarding_page_indicator_enter); |
| animator.setTarget(getPageCount() <= 1 ? mStartButton : mPageIndicator); |
| animators.add(animator); |
| |
| animator = onCreateTitleAnimator(); |
| if (animator != null) { |
| // Header title. |
| animator.setTarget(mTitleView); |
| animators.add(animator); |
| } |
| |
| animator = onCreateDescriptionAnimator(); |
| if (animator != null) { |
| // Header description. |
| animator.setTarget(mDescriptionView); |
| animators.add(animator); |
| } |
| |
| // Customized animation by the inherited class. |
| Animator customAnimator = onCreateEnterAnimation(); |
| if (customAnimator != null) { |
| animators.add(customAnimator); |
| } |
| |
| // Return if we don't have any animations. |
| if (animators.isEmpty()) { |
| return; |
| } |
| mAnimator = new AnimatorSet(); |
| mAnimator.playTogether(animators); |
| mAnimator.start(); |
| mAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mEnterAnimationFinished = true; |
| } |
| }); |
| // Search focus and give the focus to the appropriate child which has become visible. |
| getView().requestFocus(); |
| } |
| |
| /** |
| * Provides the entry animation for description view. This allows users to override the |
| * default fade and slide animation. Returning null will disable the animation. |
| */ |
| 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}. |
| * |
| * <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 = 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; |
| } |
| } |