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