1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package androidx.leanback.app; 18 19import android.animation.Animator; 20import android.animation.AnimatorInflater; 21import android.animation.AnimatorListenerAdapter; 22import android.animation.AnimatorSet; 23import android.animation.ObjectAnimator; 24import android.animation.TimeInterpolator; 25import android.content.Context; 26import android.graphics.Color; 27import android.os.Bundle; 28import android.util.Log; 29import android.util.TypedValue; 30import android.view.ContextThemeWrapper; 31import android.view.Gravity; 32import android.view.KeyEvent; 33import android.view.LayoutInflater; 34import android.view.View; 35import android.view.View.OnClickListener; 36import android.view.View.OnKeyListener; 37import android.view.ViewGroup; 38import android.view.ViewTreeObserver.OnPreDrawListener; 39import android.view.animation.AccelerateInterpolator; 40import android.view.animation.DecelerateInterpolator; 41import android.widget.Button; 42import android.widget.ImageView; 43import android.widget.TextView; 44 45import androidx.annotation.ColorInt; 46import androidx.annotation.NonNull; 47import androidx.annotation.Nullable; 48import androidx.fragment.app.Fragment; 49import androidx.leanback.R; 50import androidx.leanback.widget.PagingIndicator; 51 52import java.util.ArrayList; 53import java.util.List; 54 55/** 56 * An OnboardingSupportFragment provides a common and simple way to build onboarding screen for 57 * applications. 58 * <p> 59 * <h3>Building the screen</h3> 60 * The view structure of onboarding screen is composed of the common parts and custom parts. The 61 * common parts are composed of icon, title, description and page navigator and the custom parts 62 * are composed of background, contents and foreground. 63 * <p> 64 * To build the screen views, the inherited class should override: 65 * <ul> 66 * <li>{@link #onCreateBackgroundView} to provide the background view. Background view has the same 67 * size as the screen and the lowest z-order.</li> 68 * <li>{@link #onCreateContentView} to provide the contents view. The content view is located in 69 * the content area at the center of the screen.</li> 70 * <li>{@link #onCreateForegroundView} to provide the foreground view. Foreground view has the same 71 * size as the screen and the highest z-order</li> 72 * </ul> 73 * <p> 74 * Each of these methods can return {@code null} if the application doesn't want to provide it. 75 * <p> 76 * <h3>Page information</h3> 77 * The onboarding screen may have several pages which explain the functionality of the application. 78 * The inherited class should provide the page information by overriding the methods: 79 * <p> 80 * <ul> 81 * <li>{@link #getPageCount} to provide the number of pages.</li> 82 * <li>{@link #getPageTitle} to provide the title of the page.</li> 83 * <li>{@link #getPageDescription} to provide the description of the page.</li> 84 * </ul> 85 * <p> 86 * Note that the information is used in {@link #onCreateView}, so should be initialized before 87 * calling {@code super.onCreateView}. 88 * <p> 89 * <h3>Animation</h3> 90 * Onboarding screen has three kinds of animations: 91 * <p> 92 * <h4>Logo Splash Animation</a></h4> 93 * When onboarding screen appears, the logo splash animation is played by default. The animation 94 * fades in the logo image, pauses in a few seconds and fades it out. 95 * <p> 96 * In most cases, the logo animation needs to be customized because the logo images of applications 97 * are different from each other, or some applications may want to show their own animations. 98 * <p> 99 * The logo animation can be customized in two ways: 100 * <ul> 101 * <li>The simplest way is to provide the logo image by calling {@link #setLogoResourceId} to show 102 * the default logo animation. This method should be called in {@link Fragment#onCreateView}.</li> 103 * <li>If the logo animation is complex, then override {@link #onCreateLogoAnimation} and return the 104 * {@link Animator} object to run.</li> 105 * </ul> 106 * <p> 107 * If the inherited class provides neither the logo image nor the animation, the logo animation will 108 * be omitted. 109 * <h4>Page enter animation</h4> 110 * After logo animation finishes, page enter animation starts, which causes the header section - 111 * title and description views to fade and slide in. Users can override the default 112 * fade + slide animation by overriding {@link #onCreateTitleAnimator()} & 113 * {@link #onCreateDescriptionAnimator()}. By default we don't animate the custom views but users 114 * can provide animation by overriding {@link #onCreateEnterAnimation}. 115 * 116 * <h4>Page change animation</h4> 117 * When the page changes, the default animations of the title and description are played. The 118 * inherited class can override {@link #onPageChanged} to start the custom animations. 119 * <p> 120 * <h3>Finishing the screen</h3> 121 * <p> 122 * If the user finishes the onboarding screen after navigating all the pages, 123 * {@link #onFinishFragment} is called. The inherited class can override this method to show another 124 * fragment or activity, or just remove this fragment. 125 * <p> 126 * <h3>Theming</h3> 127 * <p> 128 * OnboardingSupportFragment must have access to an appropriate theme. Specifically, the fragment must 129 * receive {@link R.style#Theme_Leanback_Onboarding}, or a theme whose parent is set to that theme. 130 * Themes can be provided in one of three ways: 131 * <ul> 132 * <li>The simplest way is to set the theme for the host Activity to the Onboarding theme or a theme 133 * that derives from it.</li> 134 * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the 135 * existing Activity theme can have an entry added for the attribute 136 * {@link R.styleable#LeanbackOnboardingTheme_onboardingTheme}. If present, this theme will be used 137 * by OnboardingSupportFragment as an overlay to the Activity's theme.</li> 138 * <li>Finally, custom subclasses of OnboardingSupportFragment may provide a theme through the 139 * {@link #onProvideTheme} method. This can be useful if a subclass is used across multiple 140 * Activities.</li> 141 * </ul> 142 * <p> 143 * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by 144 * the Activity's theme. (Themes whose parent theme is already set to the onboarding theme do not 145 * need to set the onboardingTheme attribute; if set, it will be ignored.) 146 * 147 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTheme 148 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingHeaderStyle 149 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTitleStyle 150 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingDescriptionStyle 151 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingNavigatorContainerStyle 152 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingPageIndicatorStyle 153 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle 154 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingLogoStyle 155 */ 156abstract public class OnboardingSupportFragment extends Fragment { 157 private static final String TAG = "OnboardingF"; 158 private static final boolean DEBUG = false; 159 160 private static final long LOGO_SPLASH_PAUSE_DURATION_MS = 1333; 161 162 private static final long HEADER_ANIMATION_DURATION_MS = 417; 163 private static final long DESCRIPTION_START_DELAY_MS = 33; 164 private static final long HEADER_APPEAR_DELAY_MS = 500; 165 private static final int SLIDE_DISTANCE = 60; 166 167 private static int sSlideDistance; 168 169 private static final TimeInterpolator HEADER_APPEAR_INTERPOLATOR = new DecelerateInterpolator(); 170 private static final TimeInterpolator HEADER_DISAPPEAR_INTERPOLATOR = 171 new AccelerateInterpolator(); 172 173 // Keys used to save and restore the states. 174 private static final String KEY_CURRENT_PAGE_INDEX = "leanback.onboarding.current_page_index"; 175 private static final String KEY_LOGO_ANIMATION_FINISHED = 176 "leanback.onboarding.logo_animation_finished"; 177 private static final String KEY_ENTER_ANIMATION_FINISHED = 178 "leanback.onboarding.enter_animation_finished"; 179 180 private ContextThemeWrapper mThemeWrapper; 181 182 PagingIndicator mPageIndicator; 183 View mStartButton; 184 private ImageView mLogoView; 185 // Optional icon that can be displayed on top of the header section. 186 private ImageView mMainIconView; 187 private int mIconResourceId; 188 189 TextView mTitleView; 190 TextView mDescriptionView; 191 192 boolean mIsLtr; 193 194 // No need to save/restore the logo resource ID, because the logo animation will not appear when 195 // the fragment is restored. 196 private int mLogoResourceId; 197 boolean mLogoAnimationFinished; 198 boolean mEnterAnimationFinished; 199 int mCurrentPageIndex; 200 201 @ColorInt 202 private int mTitleViewTextColor = Color.TRANSPARENT; 203 private boolean mTitleViewTextColorSet; 204 205 @ColorInt 206 private int mDescriptionViewTextColor = Color.TRANSPARENT; 207 private boolean mDescriptionViewTextColorSet; 208 209 @ColorInt 210 private int mDotBackgroundColor = Color.TRANSPARENT; 211 private boolean mDotBackgroundColorSet; 212 213 @ColorInt 214 private int mArrowColor = Color.TRANSPARENT; 215 private boolean mArrowColorSet; 216 217 @ColorInt 218 private int mArrowBackgroundColor = Color.TRANSPARENT; 219 private boolean mArrowBackgroundColorSet; 220 221 private CharSequence mStartButtonText; 222 private boolean mStartButtonTextSet; 223 224 225 private AnimatorSet mAnimator; 226 227 private final OnClickListener mOnClickListener = new OnClickListener() { 228 @Override 229 public void onClick(View view) { 230 if (!mLogoAnimationFinished) { 231 // Do not change page until the enter transition finishes. 232 return; 233 } 234 if (mCurrentPageIndex == getPageCount() - 1) { 235 onFinishFragment(); 236 } else { 237 moveToNextPage(); 238 } 239 } 240 }; 241 242 private final OnKeyListener mOnKeyListener = new OnKeyListener() { 243 @Override 244 public boolean onKey(View v, int keyCode, KeyEvent event) { 245 if (!mLogoAnimationFinished) { 246 // Ignore key event until the enter transition finishes. 247 return keyCode != KeyEvent.KEYCODE_BACK; 248 } 249 if (event.getAction() == KeyEvent.ACTION_DOWN) { 250 return false; 251 } 252 switch (keyCode) { 253 case KeyEvent.KEYCODE_BACK: 254 if (mCurrentPageIndex == 0) { 255 return false; 256 } 257 moveToPreviousPage(); 258 return true; 259 case KeyEvent.KEYCODE_DPAD_LEFT: 260 if (mIsLtr) { 261 moveToPreviousPage(); 262 } else { 263 moveToNextPage(); 264 } 265 return true; 266 case KeyEvent.KEYCODE_DPAD_RIGHT: 267 if (mIsLtr) { 268 moveToNextPage(); 269 } else { 270 moveToPreviousPage(); 271 } 272 return true; 273 } 274 return false; 275 } 276 }; 277 278 /** 279 * Navigates to the previous page. 280 */ 281 protected void moveToPreviousPage() { 282 if (!mLogoAnimationFinished) { 283 // Ignore if the logo enter transition is in progress. 284 return; 285 } 286 if (mCurrentPageIndex > 0) { 287 --mCurrentPageIndex; 288 onPageChangedInternal(mCurrentPageIndex + 1); 289 } 290 } 291 292 /** 293 * Navigates to the next page. 294 */ 295 protected void moveToNextPage() { 296 if (!mLogoAnimationFinished) { 297 // Ignore if the logo enter transition is in progress. 298 return; 299 } 300 if (mCurrentPageIndex < getPageCount() - 1) { 301 ++mCurrentPageIndex; 302 onPageChangedInternal(mCurrentPageIndex - 1); 303 } 304 } 305 306 @Nullable 307 @Override 308 public View onCreateView(LayoutInflater inflater, final ViewGroup container, 309 Bundle savedInstanceState) { 310 resolveTheme(); 311 LayoutInflater localInflater = getThemeInflater(inflater); 312 final ViewGroup view = (ViewGroup) localInflater.inflate(R.layout.lb_onboarding_fragment, 313 container, false); 314 mIsLtr = getResources().getConfiguration().getLayoutDirection() 315 == View.LAYOUT_DIRECTION_LTR; 316 mPageIndicator = (PagingIndicator) view.findViewById(R.id.page_indicator); 317 mPageIndicator.setOnClickListener(mOnClickListener); 318 mPageIndicator.setOnKeyListener(mOnKeyListener); 319 mStartButton = view.findViewById(R.id.button_start); 320 mStartButton.setOnClickListener(mOnClickListener); 321 mStartButton.setOnKeyListener(mOnKeyListener); 322 mMainIconView = (ImageView) view.findViewById(R.id.main_icon); 323 mLogoView = (ImageView) view.findViewById(R.id.logo); 324 mTitleView = (TextView) view.findViewById(R.id.title); 325 mDescriptionView = (TextView) view.findViewById(R.id.description); 326 327 if (mTitleViewTextColorSet) { 328 mTitleView.setTextColor(mTitleViewTextColor); 329 } 330 if (mDescriptionViewTextColorSet) { 331 mDescriptionView.setTextColor(mDescriptionViewTextColor); 332 } 333 if (mDotBackgroundColorSet) { 334 mPageIndicator.setDotBackgroundColor(mDotBackgroundColor); 335 } 336 if (mArrowColorSet) { 337 mPageIndicator.setArrowColor(mArrowColor); 338 } 339 if (mArrowBackgroundColorSet) { 340 mPageIndicator.setDotBackgroundColor(mArrowBackgroundColor); 341 } 342 if (mStartButtonTextSet) { 343 ((Button) mStartButton).setText(mStartButtonText); 344 } 345 final Context context = getContext(); 346 if (sSlideDistance == 0) { 347 sSlideDistance = (int) (SLIDE_DISTANCE * context.getResources() 348 .getDisplayMetrics().scaledDensity); 349 } 350 view.requestFocus(); 351 return view; 352 } 353 354 @Override 355 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 356 super.onViewCreated(view, savedInstanceState); 357 if (savedInstanceState == null) { 358 mCurrentPageIndex = 0; 359 mLogoAnimationFinished = false; 360 mEnterAnimationFinished = false; 361 mPageIndicator.onPageSelected(0, false); 362 view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { 363 @Override 364 public boolean onPreDraw() { 365 getView().getViewTreeObserver().removeOnPreDrawListener(this); 366 if (!startLogoAnimation()) { 367 mLogoAnimationFinished = true; 368 onLogoAnimationFinished(); 369 } 370 return true; 371 } 372 }); 373 } else { 374 mCurrentPageIndex = savedInstanceState.getInt(KEY_CURRENT_PAGE_INDEX); 375 mLogoAnimationFinished = savedInstanceState.getBoolean(KEY_LOGO_ANIMATION_FINISHED); 376 mEnterAnimationFinished = savedInstanceState.getBoolean(KEY_ENTER_ANIMATION_FINISHED); 377 if (!mLogoAnimationFinished) { 378 // logo animation wasn't started or was interrupted when the activity was destroyed; 379 // restart it againl 380 if (!startLogoAnimation()) { 381 mLogoAnimationFinished = true; 382 onLogoAnimationFinished(); 383 } 384 } else { 385 onLogoAnimationFinished(); 386 } 387 } 388 } 389 390 @Override 391 public void onSaveInstanceState(Bundle outState) { 392 super.onSaveInstanceState(outState); 393 outState.putInt(KEY_CURRENT_PAGE_INDEX, mCurrentPageIndex); 394 outState.putBoolean(KEY_LOGO_ANIMATION_FINISHED, mLogoAnimationFinished); 395 outState.putBoolean(KEY_ENTER_ANIMATION_FINISHED, mEnterAnimationFinished); 396 } 397 398 /** 399 * Sets the text color for TitleView. If not set, the default textColor set in style 400 * referenced by attr {@link R.attr#onboardingTitleStyle} will be used. 401 * @param color the color to use as the text color for TitleView 402 */ 403 public void setTitleViewTextColor(@ColorInt int color) { 404 mTitleViewTextColor = color; 405 mTitleViewTextColorSet = true; 406 if (mTitleView != null) { 407 mTitleView.setTextColor(color); 408 } 409 } 410 411 /** 412 * Returns the text color of TitleView if it's set through 413 * {@link #setTitleViewTextColor(int)}. If no color was set, transparent is returned. 414 */ 415 @ColorInt 416 public final int getTitleViewTextColor() { 417 return mTitleViewTextColor; 418 } 419 420 /** 421 * Sets the text color for DescriptionView. If not set, the default textColor set in style 422 * referenced by attr {@link R.attr#onboardingDescriptionStyle} will be used. 423 * @param color the color to use as the text color for DescriptionView 424 */ 425 public void setDescriptionViewTextColor(@ColorInt int color) { 426 mDescriptionViewTextColor = color; 427 mDescriptionViewTextColorSet = true; 428 if (mDescriptionView != null) { 429 mDescriptionView.setTextColor(color); 430 } 431 } 432 433 /** 434 * Returns the text color of DescriptionView if it's set through 435 * {@link #setDescriptionViewTextColor(int)}. If no color was set, transparent is returned. 436 */ 437 @ColorInt 438 public final int getDescriptionViewTextColor() { 439 return mDescriptionViewTextColor; 440 } 441 /** 442 * Sets the background color of the dots. If not set, the default color from attr 443 * {@link R.styleable#PagingIndicator_dotBgColor} in the theme will be used. 444 * @param color the color to use for dot backgrounds 445 */ 446 public void setDotBackgroundColor(@ColorInt int color) { 447 mDotBackgroundColor = color; 448 mDotBackgroundColorSet = true; 449 if (mPageIndicator != null) { 450 mPageIndicator.setDotBackgroundColor(color); 451 } 452 } 453 454 /** 455 * Returns the background color of the dot if it's set through 456 * {@link #setDotBackgroundColor(int)}. If no color was set, transparent is returned. 457 */ 458 @ColorInt 459 public final int getDotBackgroundColor() { 460 return mDotBackgroundColor; 461 } 462 463 /** 464 * Sets the color of the arrow. This color will supersede the color set in the theme attribute 465 * {@link R.styleable#PagingIndicator_arrowColor} if provided. If none of these two are set, the 466 * arrow will have its original bitmap color. 467 * 468 * @param color the color to use for arrow background 469 */ 470 public void setArrowColor(@ColorInt int color) { 471 mArrowColor = color; 472 mArrowColorSet = true; 473 if (mPageIndicator != null) { 474 mPageIndicator.setArrowColor(color); 475 } 476 } 477 478 /** 479 * Returns the color of the arrow if it's set through 480 * {@link #setArrowColor(int)}. If no color was set, transparent is returned. 481 */ 482 @ColorInt 483 public final int getArrowColor() { 484 return mArrowColor; 485 } 486 487 /** 488 * Sets the background color of the arrow. If not set, the default color from attr 489 * {@link R.styleable#PagingIndicator_arrowBgColor} in the theme will be used. 490 * @param color the color to use for arrow background 491 */ 492 public void setArrowBackgroundColor(@ColorInt int color) { 493 mArrowBackgroundColor = color; 494 mArrowBackgroundColorSet = true; 495 if (mPageIndicator != null) { 496 mPageIndicator.setArrowBackgroundColor(color); 497 } 498 } 499 500 /** 501 * Returns the background color of the arrow if it's set through 502 * {@link #setArrowBackgroundColor(int)}. If no color was set, transparent is returned. 503 */ 504 @ColorInt 505 public final int getArrowBackgroundColor() { 506 return mArrowBackgroundColor; 507 } 508 509 /** 510 * Returns the start button text if it's set through 511 * {@link #setStartButtonText(CharSequence)}}. If no string was set, null is returned. 512 */ 513 public final CharSequence getStartButtonText() { 514 return mStartButtonText; 515 } 516 517 /** 518 * Sets the text on the start button text. If not set, the default text set in 519 * {@link R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle} will be used. 520 * 521 * @param text the start button text 522 */ 523 public void setStartButtonText(CharSequence text) { 524 mStartButtonText = text; 525 mStartButtonTextSet = true; 526 if (mStartButton != null) { 527 ((Button) mStartButton).setText(mStartButtonText); 528 } 529 } 530 531 /** 532 * Returns the theme used for styling the fragment. The default returns -1, indicating that the 533 * host Activity's theme should be used. 534 * 535 * @return The theme resource ID of the theme to use in this fragment, or -1 to use the host 536 * Activity's theme. 537 */ 538 public int onProvideTheme() { 539 return -1; 540 } 541 542 private void resolveTheme() { 543 final Context context = getContext(); 544 int theme = onProvideTheme(); 545 if (theme == -1) { 546 // Look up the onboardingTheme in the activity's currently specified theme. If it 547 // exists, wrap the theme with its value. 548 int resId = R.attr.onboardingTheme; 549 TypedValue typedValue = new TypedValue(); 550 boolean found = context.getTheme().resolveAttribute(resId, typedValue, true); 551 if (DEBUG) Log.v(TAG, "Found onboarding theme reference? " + found); 552 if (found) { 553 mThemeWrapper = new ContextThemeWrapper(context, typedValue.resourceId); 554 } 555 } else { 556 mThemeWrapper = new ContextThemeWrapper(context, theme); 557 } 558 } 559 560 private LayoutInflater getThemeInflater(LayoutInflater inflater) { 561 return mThemeWrapper == null ? inflater : inflater.cloneInContext(mThemeWrapper); 562 } 563 564 /** 565 * Sets the resource ID of the splash logo image. If the logo resource id set, the default logo 566 * splash animation will be played. 567 * 568 * @param id The resource ID of the logo image. 569 */ 570 public final void setLogoResourceId(int id) { 571 mLogoResourceId = id; 572 } 573 574 /** 575 * Returns the resource ID of the splash logo image. 576 * 577 * @return The resource ID of the splash logo image. 578 */ 579 public final int getLogoResourceId() { 580 return mLogoResourceId; 581 } 582 583 /** 584 * Called to have the inherited class create its own logo animation. 585 * <p> 586 * This is called only if the logo image resource ID is not set by {@link #setLogoResourceId}. 587 * If this returns {@code null}, the logo animation is skipped. 588 * 589 * @return The {@link Animator} object which runs the logo animation. 590 */ 591 @Nullable 592 protected Animator onCreateLogoAnimation() { 593 return null; 594 } 595 596 boolean startLogoAnimation() { 597 final Context context = getContext(); 598 if (context == null) { 599 return false; 600 } 601 Animator animator = null; 602 if (mLogoResourceId != 0) { 603 mLogoView.setVisibility(View.VISIBLE); 604 mLogoView.setImageResource(mLogoResourceId); 605 Animator inAnimator = AnimatorInflater.loadAnimator(context, 606 R.animator.lb_onboarding_logo_enter); 607 Animator outAnimator = AnimatorInflater.loadAnimator(context, 608 R.animator.lb_onboarding_logo_exit); 609 outAnimator.setStartDelay(LOGO_SPLASH_PAUSE_DURATION_MS); 610 AnimatorSet logoAnimator = new AnimatorSet(); 611 logoAnimator.playSequentially(inAnimator, outAnimator); 612 logoAnimator.setTarget(mLogoView); 613 animator = logoAnimator; 614 } else { 615 animator = onCreateLogoAnimation(); 616 } 617 if (animator != null) { 618 animator.addListener(new AnimatorListenerAdapter() { 619 @Override 620 public void onAnimationEnd(Animator animation) { 621 if (context != null) { 622 mLogoAnimationFinished = true; 623 onLogoAnimationFinished(); 624 } 625 } 626 }); 627 animator.start(); 628 return true; 629 } 630 return false; 631 } 632 633 /** 634 * Called to have the inherited class create its enter animation. The start animation runs after 635 * logo animation ends. 636 * 637 * @return The {@link Animator} object which runs the page enter animation. 638 */ 639 @Nullable 640 protected Animator onCreateEnterAnimation() { 641 return null; 642 } 643 644 645 /** 646 * Hides the logo view and makes other fragment views visible. Also initializes the texts for 647 * Title and Description views. 648 */ 649 void hideLogoView() { 650 mLogoView.setVisibility(View.GONE); 651 652 if (mIconResourceId != 0) { 653 mMainIconView.setImageResource(mIconResourceId); 654 mMainIconView.setVisibility(View.VISIBLE); 655 } 656 657 View container = getView(); 658 // Create custom views. 659 LayoutInflater inflater = getThemeInflater(LayoutInflater.from( 660 getContext())); 661 ViewGroup backgroundContainer = (ViewGroup) container.findViewById( 662 R.id.background_container); 663 View background = onCreateBackgroundView(inflater, backgroundContainer); 664 if (background != null) { 665 backgroundContainer.setVisibility(View.VISIBLE); 666 backgroundContainer.addView(background); 667 } 668 ViewGroup contentContainer = (ViewGroup) container.findViewById(R.id.content_container); 669 View content = onCreateContentView(inflater, contentContainer); 670 if (content != null) { 671 contentContainer.setVisibility(View.VISIBLE); 672 contentContainer.addView(content); 673 } 674 ViewGroup foregroundContainer = (ViewGroup) container.findViewById( 675 R.id.foreground_container); 676 View foreground = onCreateForegroundView(inflater, foregroundContainer); 677 if (foreground != null) { 678 foregroundContainer.setVisibility(View.VISIBLE); 679 foregroundContainer.addView(foreground); 680 } 681 // Make views visible which were invisible while logo animation is running. 682 container.findViewById(R.id.page_container).setVisibility(View.VISIBLE); 683 container.findViewById(R.id.content_container).setVisibility(View.VISIBLE); 684 if (getPageCount() > 1) { 685 mPageIndicator.setPageCount(getPageCount()); 686 mPageIndicator.onPageSelected(mCurrentPageIndex, false); 687 } 688 if (mCurrentPageIndex == getPageCount() - 1) { 689 mStartButton.setVisibility(View.VISIBLE); 690 } else { 691 mPageIndicator.setVisibility(View.VISIBLE); 692 } 693 // Header views. 694 mTitleView.setText(getPageTitle(mCurrentPageIndex)); 695 mDescriptionView.setText(getPageDescription(mCurrentPageIndex)); 696 } 697 698 /** 699 * Called immediately after the logo animation is complete or no logo animation is specified. 700 * This method can also be called when the activity is recreated, i.e. when no logo animation 701 * are performed. 702 * By default, this method will hide the logo view and start the entrance animation for this 703 * fragment. 704 * Overriding subclasses can provide their own data loading logic as to when the entrance 705 * animation should be executed. 706 */ 707 protected void onLogoAnimationFinished() { 708 startEnterAnimation(false); 709 } 710 711 /** 712 * Called to start entrance transition. This can be called by subclasses when the logo animation 713 * and data loading is complete. If force flag is set to false, it will only start the animation 714 * if it's not already done yet. Otherwise, it will always start the enter animation. In both 715 * cases, the logo view will hide and the rest of fragment views become visible after this call. 716 * 717 * @param force {@code true} if enter animation has to be performed regardless of whether it's 718 * been done in the past, {@code false} otherwise 719 */ 720 protected final void startEnterAnimation(boolean force) { 721 final Context context = getContext(); 722 if (context == null) { 723 return; 724 } 725 hideLogoView(); 726 if (mEnterAnimationFinished && !force) { 727 return; 728 } 729 List<Animator> animators = new ArrayList<>(); 730 Animator animator = AnimatorInflater.loadAnimator(context, 731 R.animator.lb_onboarding_page_indicator_enter); 732 animator.setTarget(getPageCount() <= 1 ? mStartButton : mPageIndicator); 733 animators.add(animator); 734 735 animator = onCreateTitleAnimator(); 736 if (animator != null) { 737 // Header title. 738 animator.setTarget(mTitleView); 739 animators.add(animator); 740 } 741 742 animator = onCreateDescriptionAnimator(); 743 if (animator != null) { 744 // Header description. 745 animator.setTarget(mDescriptionView); 746 animators.add(animator); 747 } 748 749 // Customized animation by the inherited class. 750 Animator customAnimator = onCreateEnterAnimation(); 751 if (customAnimator != null) { 752 animators.add(customAnimator); 753 } 754 755 // Return if we don't have any animations. 756 if (animators.isEmpty()) { 757 return; 758 } 759 mAnimator = new AnimatorSet(); 760 mAnimator.playTogether(animators); 761 mAnimator.start(); 762 mAnimator.addListener(new AnimatorListenerAdapter() { 763 @Override 764 public void onAnimationEnd(Animator animation) { 765 mEnterAnimationFinished = true; 766 } 767 }); 768 // Search focus and give the focus to the appropriate child which has become visible. 769 getView().requestFocus(); 770 } 771 772 /** 773 * Provides the entry animation for description view. This allows users to override the 774 * default fade and slide animation. Returning null will disable the animation. 775 */ 776 protected Animator onCreateDescriptionAnimator() { 777 return AnimatorInflater.loadAnimator(getContext(), 778 R.animator.lb_onboarding_description_enter); 779 } 780 781 /** 782 * Provides the entry animation for title view. This allows users to override the 783 * default fade and slide animation. Returning null will disable the animation. 784 */ 785 protected Animator onCreateTitleAnimator() { 786 return AnimatorInflater.loadAnimator(getContext(), 787 R.animator.lb_onboarding_title_enter); 788 } 789 790 /** 791 * Returns whether the logo enter animation is finished. 792 * 793 * @return {@code true} if the logo enter transition is finished, {@code false} otherwise 794 */ 795 protected final boolean isLogoAnimationFinished() { 796 return mLogoAnimationFinished; 797 } 798 799 /** 800 * Returns the page count. 801 * 802 * @return The page count. 803 */ 804 abstract protected int getPageCount(); 805 806 /** 807 * Returns the title of the given page. 808 * 809 * @param pageIndex The page index. 810 * 811 * @return The title of the page. 812 */ 813 abstract protected CharSequence getPageTitle(int pageIndex); 814 815 /** 816 * Returns the description of the given page. 817 * 818 * @param pageIndex The page index. 819 * 820 * @return The description of the page. 821 */ 822 abstract protected CharSequence getPageDescription(int pageIndex); 823 824 /** 825 * Returns the index of the current page. 826 * 827 * @return The index of the current page. 828 */ 829 protected final int getCurrentPageIndex() { 830 return mCurrentPageIndex; 831 } 832 833 /** 834 * Called to have the inherited class create background view. This is optional and the fragment 835 * which doesn't have the background view can return {@code null}. This is called inside 836 * {@link #onCreateView}. 837 * 838 * @param inflater The LayoutInflater object that can be used to inflate the views, 839 * @param container The parent view that the additional views are attached to.The fragment 840 * should not add the view by itself. 841 * 842 * @return The background view for the onboarding screen, or {@code null}. 843 */ 844 @Nullable 845 abstract protected View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container); 846 847 /** 848 * Called to have the inherited class create content view. This is optional and the fragment 849 * which doesn't have the content view can return {@code null}. This is called inside 850 * {@link #onCreateView}. 851 * 852 * <p>The content view would be located at the center of the screen. 853 * 854 * @param inflater The LayoutInflater object that can be used to inflate the views, 855 * @param container The parent view that the additional views are attached to.The fragment 856 * should not add the view by itself. 857 * 858 * @return The content view for the onboarding screen, or {@code null}. 859 */ 860 @Nullable 861 abstract protected View onCreateContentView(LayoutInflater inflater, ViewGroup container); 862 863 /** 864 * Called to have the inherited class create foreground view. This is optional and the fragment 865 * which doesn't need the foreground view can return {@code null}. This is called inside 866 * {@link #onCreateView}. 867 * 868 * <p>This foreground view would have the highest z-order. 869 * 870 * @param inflater The LayoutInflater object that can be used to inflate the views, 871 * @param container The parent view that the additional views are attached to.The fragment 872 * should not add the view by itself. 873 * 874 * @return The foreground view for the onboarding screen, or {@code null}. 875 */ 876 @Nullable 877 abstract protected View onCreateForegroundView(LayoutInflater inflater, ViewGroup container); 878 879 /** 880 * Called when the onboarding flow finishes. 881 */ 882 protected void onFinishFragment() { } 883 884 /** 885 * Called when the page changes. 886 */ 887 private void onPageChangedInternal(int previousPage) { 888 if (mAnimator != null) { 889 mAnimator.end(); 890 } 891 mPageIndicator.onPageSelected(mCurrentPageIndex, true); 892 893 List<Animator> animators = new ArrayList<>(); 894 // Header animation 895 Animator fadeAnimator = null; 896 if (previousPage < getCurrentPageIndex()) { 897 // sliding to left 898 animators.add(createAnimator(mTitleView, false, Gravity.START, 0)); 899 animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.START, 900 DESCRIPTION_START_DELAY_MS)); 901 animators.add(createAnimator(mTitleView, true, Gravity.END, 902 HEADER_APPEAR_DELAY_MS)); 903 animators.add(createAnimator(mDescriptionView, true, Gravity.END, 904 HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS)); 905 } else { 906 // sliding to right 907 animators.add(createAnimator(mTitleView, false, Gravity.END, 0)); 908 animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.END, 909 DESCRIPTION_START_DELAY_MS)); 910 animators.add(createAnimator(mTitleView, true, Gravity.START, 911 HEADER_APPEAR_DELAY_MS)); 912 animators.add(createAnimator(mDescriptionView, true, Gravity.START, 913 HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS)); 914 } 915 final int currentPageIndex = getCurrentPageIndex(); 916 fadeAnimator.addListener(new AnimatorListenerAdapter() { 917 @Override 918 public void onAnimationEnd(Animator animation) { 919 mTitleView.setText(getPageTitle(currentPageIndex)); 920 mDescriptionView.setText(getPageDescription(currentPageIndex)); 921 } 922 }); 923 924 final Context context = getContext(); 925 // Animator for switching between page indicator and button. 926 if (getCurrentPageIndex() == getPageCount() - 1) { 927 mStartButton.setVisibility(View.VISIBLE); 928 Animator navigatorFadeOutAnimator = AnimatorInflater.loadAnimator(context, 929 R.animator.lb_onboarding_page_indicator_fade_out); 930 navigatorFadeOutAnimator.setTarget(mPageIndicator); 931 navigatorFadeOutAnimator.addListener(new AnimatorListenerAdapter() { 932 @Override 933 public void onAnimationEnd(Animator animation) { 934 mPageIndicator.setVisibility(View.GONE); 935 } 936 }); 937 animators.add(navigatorFadeOutAnimator); 938 Animator buttonFadeInAnimator = AnimatorInflater.loadAnimator(context, 939 R.animator.lb_onboarding_start_button_fade_in); 940 buttonFadeInAnimator.setTarget(mStartButton); 941 animators.add(buttonFadeInAnimator); 942 } else if (previousPage == getPageCount() - 1) { 943 mPageIndicator.setVisibility(View.VISIBLE); 944 Animator navigatorFadeInAnimator = AnimatorInflater.loadAnimator(context, 945 R.animator.lb_onboarding_page_indicator_fade_in); 946 navigatorFadeInAnimator.setTarget(mPageIndicator); 947 animators.add(navigatorFadeInAnimator); 948 Animator buttonFadeOutAnimator = AnimatorInflater.loadAnimator(context, 949 R.animator.lb_onboarding_start_button_fade_out); 950 buttonFadeOutAnimator.setTarget(mStartButton); 951 buttonFadeOutAnimator.addListener(new AnimatorListenerAdapter() { 952 @Override 953 public void onAnimationEnd(Animator animation) { 954 mStartButton.setVisibility(View.GONE); 955 } 956 }); 957 animators.add(buttonFadeOutAnimator); 958 } 959 mAnimator = new AnimatorSet(); 960 mAnimator.playTogether(animators); 961 mAnimator.start(); 962 onPageChanged(mCurrentPageIndex, previousPage); 963 } 964 965 /** 966 * Called when the page has been changed. 967 * 968 * @param newPage The new page. 969 * @param previousPage The previous page. 970 */ 971 protected void onPageChanged(int newPage, int previousPage) { } 972 973 private Animator createAnimator(View view, boolean fadeIn, int slideDirection, 974 long startDelay) { 975 boolean isLtr = getView().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; 976 boolean slideRight = (isLtr && slideDirection == Gravity.END) 977 || (!isLtr && slideDirection == Gravity.START) 978 || slideDirection == Gravity.RIGHT; 979 Animator fadeAnimator; 980 Animator slideAnimator; 981 if (fadeIn) { 982 fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 0.0f, 1.0f); 983 slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 984 slideRight ? sSlideDistance : -sSlideDistance, 0); 985 fadeAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR); 986 slideAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR); 987 } else { 988 fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1.0f, 0.0f); 989 slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0, 990 slideRight ? sSlideDistance : -sSlideDistance); 991 fadeAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR); 992 slideAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR); 993 } 994 fadeAnimator.setDuration(HEADER_ANIMATION_DURATION_MS); 995 fadeAnimator.setTarget(view); 996 slideAnimator.setDuration(HEADER_ANIMATION_DURATION_MS); 997 slideAnimator.setTarget(view); 998 AnimatorSet animator = new AnimatorSet(); 999 animator.playTogether(fadeAnimator, slideAnimator); 1000 if (startDelay > 0) { 1001 animator.setStartDelay(startDelay); 1002 } 1003 return animator; 1004 } 1005 1006 /** 1007 * Sets the resource id for the main icon. 1008 */ 1009 public final void setIconResouceId(int resourceId) { 1010 this.mIconResourceId = resourceId; 1011 if (mMainIconView != null) { 1012 mMainIconView.setImageResource(resourceId); 1013 mMainIconView.setVisibility(View.VISIBLE); 1014 } 1015 } 1016 1017 /** 1018 * Returns the resource id of the main icon. 1019 */ 1020 public final int getIconResourceId() { 1021 return mIconResourceId; 1022 } 1023} 1024