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