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.app.Activity; 26import android.app.Fragment; 27import android.os.Bundle; 28import android.support.annotation.Nullable; 29import android.support.v17.leanback.R; 30import android.support.v17.leanback.widget.PagingIndicator; 31import android.util.Log; 32import android.util.TypedValue; 33import android.view.ContextThemeWrapper; 34import android.view.Gravity; 35import android.view.KeyEvent; 36import android.view.LayoutInflater; 37import android.view.View; 38import android.view.View.OnClickListener; 39import android.view.View.OnKeyListener; 40import android.view.ViewGroup; 41import android.view.ViewTreeObserver.OnPreDrawListener; 42import android.view.animation.AccelerateInterpolator; 43import android.view.animation.DecelerateInterpolator; 44import android.widget.ImageView; 45import android.widget.TextView; 46 47import java.util.ArrayList; 48import java.util.List; 49 50/** 51 * An OnboardingFragment provides a common and simple way to build onboarding screen for 52 * applications. 53 * <p> 54 * <h3>Building the screen</h3> 55 * The view structure of onboarding screen is composed of the common parts and custom parts. The 56 * common parts are composed of title, description and page navigator and the custom parts are 57 * composed of background, contents and foreground. 58 * <p> 59 * To build the screen views, the inherited class should override: 60 * <ul> 61 * <li>{@link #onCreateBackgroundView} to provide the background view. Background view has the same 62 * size as the screen and the lowest z-order.</li> 63 * <li>{@link #onCreateContentView} to provide the contents view. The content view is located in 64 * the content area at the center of the screen.</li> 65 * <li>{@link #onCreateForegroundView} to provide the foreground view. Foreground view has the same 66 * size as the screen and the highest z-order</li> 67 * </ul> 68 * <p> 69 * Each of these methods can return {@code null} if the application doesn't want to provide it. 70 * <p> 71 * <h3>Page information</h3> 72 * The onboarding screen may have several pages which explain the functionality of the application. 73 * The inherited class should provide the page information by overriding the methods: 74 * <p> 75 * <ul> 76 * <li>{@link #getPageCount} to provide the number of pages.</li> 77 * <li>{@link #getPageTitle} to provide the title of the page.</li> 78 * <li>{@link #getPageDescription} to provide the description of the page.</li> 79 * </ul> 80 * <p> 81 * Note that the information is used in {@link #onCreateView}, so should be initialized before 82 * calling {@code super.onCreateView}. 83 * <p> 84 * <h3>Animation</h3> 85 * Onboarding screen has three kinds of animations: 86 * <p> 87 * <h4>Logo Splash Animation</a></h4> 88 * When onboarding screen appears, the logo splash animation is played by default. The animation 89 * fades in the logo image, pauses in a few seconds and fades it out. 90 * <p> 91 * In most cases, the logo animation needs to be customized because the logo images of applications 92 * are different from each other, or some applications may want to show their own animations. 93 * <p> 94 * The logo animation can be customized in two ways: 95 * <ul> 96 * <li>The simplest way is to provide the logo image by calling {@link #setLogoResourceId} to show 97 * the default logo animation. This method should be called in {@link Fragment#onCreateView}.</li> 98 * <li>If the logo animation is complex, then override {@link #onCreateLogoAnimation} and return the 99 * {@link Animator} object to run.</li> 100 * </ul> 101 * <p> 102 * If the inherited class provides neither the logo image nor the animation, the logo animation will 103 * be omitted. 104 * <h4>Page enter animation</h4> 105 * After logo animation finishes, page enter animation starts. The application can provide the 106 * animations of custom views by overriding {@link #onCreateEnterAnimation}. 107 * <h4>Page change animation</h4> 108 * When the page changes, the default animations of the title and description are played. The 109 * inherited class can override {@link #onPageChanged} to start the custom animations. 110 * <p> 111 * <h3>Finishing the screen</h3> 112 * <p> 113 * If the user finishes the onboarding screen after navigating all the pages, 114 * {@link #onFinishFragment} is called. The inherited class can override this method to show another 115 * fragment or activity, or just remove this fragment. 116 * <p> 117 * <h3>Theming</h3> 118 * <p> 119 * OnboardingFragment must have access to an appropriate theme. Specifically, the fragment must 120 * receive {@link R.style#Theme_Leanback_Onboarding}, or a theme whose parent is set to that theme. 121 * Themes can be provided in one of three ways: 122 * <ul> 123 * <li>The simplest way is to set the theme for the host Activity to the Onboarding theme or a theme 124 * that derives from it.</li> 125 * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the 126 * existing Activity theme can have an entry added for the attribute 127 * {@link R.styleable#LeanbackOnboardingTheme_onboardingTheme}. If present, this theme will be used 128 * by OnboardingFragment as an overlay to the Activity's theme.</li> 129 * <li>Finally, custom subclasses of OnboardingFragment may provide a theme through the 130 * {@link #onProvideTheme} method. This can be useful if a subclass is used across multiple 131 * Activities.</li> 132 * </ul> 133 * <p> 134 * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by 135 * the Activity's theme. (Themes whose parent theme is already set to the onboarding theme do not 136 * need to set the onboardingTheme attribute; if set, it will be ignored.) 137 * 138 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTheme 139 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingHeaderStyle 140 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingTitleStyle 141 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingDescriptionStyle 142 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingNavigatorContainerStyle 143 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingPageIndicatorStyle 144 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle 145 * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingLogoStyle 146 */ 147abstract public class OnboardingFragment extends Fragment { 148 private static final String TAG = "OnboardingFragment"; 149 private static final boolean DEBUG = false; 150 151 private static final long LOGO_SPLASH_PAUSE_DURATION_MS = 1333; 152 private static final long START_DELAY_TITLE_MS = 33; 153 private static final long START_DELAY_DESCRIPTION_MS = 33; 154 155 private static final long HEADER_ANIMATION_DURATION_MS = 417; 156 private static final long DESCRIPTION_START_DELAY_MS = 33; 157 private static final long HEADER_APPEAR_DELAY_MS = 500; 158 private static final int SLIDE_DISTANCE = 60; 159 160 private static int sSlideDistance; 161 162 private static final TimeInterpolator HEADER_APPEAR_INTERPOLATOR = new DecelerateInterpolator(); 163 private static final TimeInterpolator HEADER_DISAPPEAR_INTERPOLATOR 164 = new AccelerateInterpolator(); 165 166 // Keys used to save and restore the states. 167 private static final String KEY_CURRENT_PAGE_INDEX = "leanback.onboarding.current_page_index"; 168 169 private ContextThemeWrapper mThemeWrapper; 170 171 private PagingIndicator mPageIndicator; 172 private View mStartButton; 173 private ImageView mLogoView; 174 private TextView mTitleView; 175 private TextView mDescriptionView; 176 177 private boolean mIsLtr; 178 179 // No need to save/restore the logo resource ID, because the logo animation will not appear when 180 // the fragment is restored. 181 private int mLogoResourceId; 182 private boolean mEnterTransitionFinished; 183 private int mCurrentPageIndex; 184 185 private AnimatorSet mAnimator; 186 187 private final OnClickListener mOnClickListener = new OnClickListener() { 188 @Override 189 public void onClick(View view) { 190 if (!mEnterTransitionFinished) { 191 // Do not change page until the enter transition finishes. 192 return; 193 } 194 if (mCurrentPageIndex == getPageCount() - 1) { 195 onFinishFragment(); 196 } else { 197 moveToNextPage(); 198 } 199 } 200 }; 201 202 private final OnKeyListener mOnKeyListener = new OnKeyListener() { 203 @Override 204 public boolean onKey(View v, int keyCode, KeyEvent event) { 205 if (!mEnterTransitionFinished) { 206 // Ignore key event until the enter transition finishes. 207 return keyCode != KeyEvent.KEYCODE_BACK; 208 } 209 if (event.getAction() == KeyEvent.ACTION_DOWN) { 210 return false; 211 } 212 switch (keyCode) { 213 case KeyEvent.KEYCODE_BACK: 214 if (mCurrentPageIndex == 0) { 215 return false; 216 } 217 moveToPreviousPage(); 218 return true; 219 case KeyEvent.KEYCODE_DPAD_LEFT: 220 if (mIsLtr) { 221 moveToPreviousPage(); 222 } else { 223 moveToNextPage(); 224 } 225 return true; 226 case KeyEvent.KEYCODE_DPAD_RIGHT: 227 if (mIsLtr) { 228 moveToNextPage(); 229 } else { 230 moveToPreviousPage(); 231 } 232 return true; 233 } 234 return false; 235 } 236 }; 237 238 private void moveToPreviousPage() { 239 if (mCurrentPageIndex > 0) { 240 --mCurrentPageIndex; 241 onPageChangedInternal(mCurrentPageIndex + 1); 242 } 243 } 244 private void moveToNextPage() { 245 if (mCurrentPageIndex < getPageCount() - 1) { 246 ++mCurrentPageIndex; 247 onPageChangedInternal(mCurrentPageIndex - 1); 248 } 249 } 250 251 @Nullable 252 @Override 253 public View onCreateView(LayoutInflater inflater, final ViewGroup container, 254 Bundle savedInstanceState) { 255 resolveTheme(); 256 LayoutInflater localInflater = getThemeInflater(inflater); 257 final ViewGroup view = (ViewGroup) localInflater.inflate(R.layout.lb_onboarding_fragment, 258 container, false); 259 mIsLtr = getResources().getConfiguration().getLayoutDirection() 260 == View.LAYOUT_DIRECTION_LTR; 261 mPageIndicator = (PagingIndicator) view.findViewById(R.id.page_indicator); 262 mPageIndicator.setOnClickListener(mOnClickListener); 263 mPageIndicator.setOnKeyListener(mOnKeyListener); 264 mStartButton = view.findViewById(R.id.button_start); 265 mStartButton.setOnClickListener(mOnClickListener); 266 mStartButton.setOnKeyListener(mOnKeyListener); 267 mLogoView = (ImageView) view.findViewById(R.id.logo); 268 mTitleView = (TextView) view.findViewById(R.id.title); 269 mDescriptionView = (TextView) view.findViewById(R.id.description); 270 if (sSlideDistance == 0) { 271 sSlideDistance = (int) (SLIDE_DISTANCE * getActivity().getResources() 272 .getDisplayMetrics().scaledDensity); 273 } 274 if (savedInstanceState == null) { 275 mCurrentPageIndex = 0; 276 mEnterTransitionFinished = false; 277 mPageIndicator.onPageSelected(0, false); 278 view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { 279 @Override 280 public boolean onPreDraw() { 281 view.getViewTreeObserver().removeOnPreDrawListener(this); 282 if (!startLogoAnimation()) { 283 startEnterAnimation(); 284 } 285 return true; 286 } 287 }); 288 } else { 289 mEnterTransitionFinished = true; 290 mCurrentPageIndex = savedInstanceState.getInt(KEY_CURRENT_PAGE_INDEX); 291 initializeViews(view); 292 } 293 view.requestFocus(); 294 return view; 295 } 296 297 @Override 298 public void onSaveInstanceState(Bundle outState) { 299 super.onSaveInstanceState(outState); 300 outState.putInt(KEY_CURRENT_PAGE_INDEX, mCurrentPageIndex); 301 } 302 303 /** 304 * Returns the theme used for styling the fragment. The default returns -1, indicating that the 305 * host Activity's theme should be used. 306 * 307 * @return The theme resource ID of the theme to use in this fragment, or -1 to use the host 308 * Activity's theme. 309 */ 310 public int onProvideTheme() { 311 return -1; 312 } 313 314 private void resolveTheme() { 315 Activity activity = getActivity(); 316 int theme = onProvideTheme(); 317 if (theme == -1) { 318 // Look up the onboardingTheme in the activity's currently specified theme. If it 319 // exists, wrap the theme with its value. 320 int resId = R.attr.onboardingTheme; 321 TypedValue typedValue = new TypedValue(); 322 boolean found = activity.getTheme().resolveAttribute(resId, typedValue, true); 323 if (DEBUG) Log.v(TAG, "Found onboarding theme reference? " + found); 324 if (found) { 325 mThemeWrapper = new ContextThemeWrapper(activity, typedValue.resourceId); 326 } 327 } else { 328 mThemeWrapper = new ContextThemeWrapper(activity, theme); 329 } 330 } 331 332 private LayoutInflater getThemeInflater(LayoutInflater inflater) { 333 return mThemeWrapper == null ? inflater : inflater.cloneInContext(mThemeWrapper); 334 } 335 336 /** 337 * Sets the resource ID of the splash logo image. If the logo resource id set, the default logo 338 * splash animation will be played. 339 * 340 * @param id The resource ID of the logo image. 341 */ 342 public final void setLogoResourceId(int id) { 343 mLogoResourceId = id; 344 } 345 346 /** 347 * Returns the resource ID of the splash logo image. 348 * 349 * @return The resource ID of the splash logo image. 350 */ 351 public final int getLogoResourceId() { 352 return mLogoResourceId; 353 } 354 355 /** 356 * Called to have the inherited class create its own logo animation. 357 * <p> 358 * This is called only if the logo image resource ID is not set by {@link #setLogoResourceId}. 359 * If this returns {@code null}, the logo animation is skipped. 360 * 361 * @return The {@link Animator} object which runs the logo animation. 362 */ 363 @Nullable 364 protected Animator onCreateLogoAnimation() { 365 return null; 366 } 367 368 private boolean startLogoAnimation() { 369 Animator animator = null; 370 if (mLogoResourceId != 0) { 371 mLogoView.setVisibility(View.VISIBLE); 372 mLogoView.setImageResource(mLogoResourceId); 373 Animator inAnimator = AnimatorInflater.loadAnimator(getActivity(), 374 R.animator.lb_onboarding_logo_enter); 375 Animator outAnimator = AnimatorInflater.loadAnimator(getActivity(), 376 R.animator.lb_onboarding_logo_exit); 377 outAnimator.setStartDelay(LOGO_SPLASH_PAUSE_DURATION_MS); 378 AnimatorSet logoAnimator = new AnimatorSet(); 379 logoAnimator.playSequentially(inAnimator, outAnimator); 380 logoAnimator.setTarget(mLogoView); 381 animator = logoAnimator; 382 } else { 383 animator = onCreateLogoAnimation(); 384 } 385 if (animator != null) { 386 animator.addListener(new AnimatorListenerAdapter() { 387 @Override 388 public void onAnimationEnd(Animator animation) { 389 if (getActivity() != null) { 390 startEnterAnimation(); 391 } 392 } 393 }); 394 animator.start(); 395 return true; 396 } 397 return false; 398 } 399 400 /** 401 * Called to have the inherited class create its enter animation. The start animation runs after 402 * logo animation ends. 403 * 404 * @return The {@link Animator} object which runs the page enter animation. 405 */ 406 @Nullable 407 protected Animator onCreateEnterAnimation() { 408 return null; 409 } 410 411 private void initializeViews(View container) { 412 mLogoView.setVisibility(View.GONE); 413 // Create custom views. 414 LayoutInflater inflater = getThemeInflater(LayoutInflater.from(getActivity())); 415 ViewGroup backgroundContainer = (ViewGroup) container.findViewById( 416 R.id.background_container); 417 View background = onCreateBackgroundView(inflater, backgroundContainer); 418 if (background != null) { 419 backgroundContainer.setVisibility(View.VISIBLE); 420 backgroundContainer.addView(background); 421 } 422 ViewGroup contentContainer = (ViewGroup) container.findViewById(R.id.content_container); 423 View content = onCreateContentView(inflater, contentContainer); 424 if (content != null) { 425 contentContainer.setVisibility(View.VISIBLE); 426 contentContainer.addView(content); 427 } 428 ViewGroup foregroundContainer = (ViewGroup) container.findViewById( 429 R.id.foreground_container); 430 View foreground = onCreateForegroundView(inflater, foregroundContainer); 431 if (foreground != null) { 432 foregroundContainer.setVisibility(View.VISIBLE); 433 foregroundContainer.addView(foreground); 434 } 435 // Make views visible which were invisible while logo animation is running. 436 container.findViewById(R.id.page_container).setVisibility(View.VISIBLE); 437 container.findViewById(R.id.content_container).setVisibility(View.VISIBLE); 438 if (getPageCount() > 1) { 439 mPageIndicator.setPageCount(getPageCount()); 440 mPageIndicator.onPageSelected(mCurrentPageIndex, false); 441 } 442 if (mCurrentPageIndex == getPageCount() - 1) { 443 mStartButton.setVisibility(View.VISIBLE); 444 } else { 445 mPageIndicator.setVisibility(View.VISIBLE); 446 } 447 // Header views. 448 mTitleView.setText(getPageTitle(mCurrentPageIndex)); 449 mDescriptionView.setText(getPageDescription(mCurrentPageIndex)); 450 } 451 452 private void startEnterAnimation() { 453 mEnterTransitionFinished = true; 454 initializeViews(getView()); 455 List<Animator> animators = new ArrayList<>(); 456 Animator animator = AnimatorInflater.loadAnimator(getActivity(), 457 R.animator.lb_onboarding_page_indicator_enter); 458 animator.setTarget(getPageCount() <= 1 ? mStartButton : mPageIndicator); 459 animators.add(animator); 460 // Header title 461 View view = getActivity().findViewById(R.id.title); 462 view.setAlpha(0); 463 animator = AnimatorInflater.loadAnimator(getActivity(), 464 R.animator.lb_onboarding_title_enter); 465 animator.setStartDelay(START_DELAY_TITLE_MS); 466 animator.setTarget(view); 467 animators.add(animator); 468 // Header description 469 view = getActivity().findViewById(R.id.description); 470 view.setAlpha(0); 471 animator = AnimatorInflater.loadAnimator(getActivity(), 472 R.animator.lb_onboarding_description_enter); 473 animator.setStartDelay(START_DELAY_DESCRIPTION_MS); 474 animator.setTarget(view); 475 animators.add(animator); 476 // Customized animation by the inherited class. 477 Animator customAnimator = onCreateEnterAnimation(); 478 if (customAnimator != null) { 479 animators.add(customAnimator); 480 } 481 mAnimator = new AnimatorSet(); 482 mAnimator.playTogether(animators); 483 mAnimator.start(); 484 // Search focus and give the focus to the appropriate child which has become visible. 485 getView().requestFocus(); 486 } 487 488 /** 489 * Returns the page count. 490 * 491 * @return The page count. 492 */ 493 abstract protected int getPageCount(); 494 495 /** 496 * Returns the title of the given page. 497 * 498 * @param pageIndex The page index. 499 * 500 * @return The title of the page. 501 */ 502 abstract protected CharSequence getPageTitle(int pageIndex); 503 504 /** 505 * Returns the description of the given page. 506 * 507 * @param pageIndex The page index. 508 * 509 * @return The description of the page. 510 */ 511 abstract protected CharSequence getPageDescription(int pageIndex); 512 513 /** 514 * Returns the index of the current page. 515 * 516 * @return The index of the current page. 517 */ 518 protected final int getCurrentPageIndex() { 519 return mCurrentPageIndex; 520 } 521 522 /** 523 * Called to have the inherited class create background view. This is optional and the fragment 524 * which doesn't have the background view can return {@code null}. This is called inside 525 * {@link #onCreateView}. 526 * 527 * @param inflater The LayoutInflater object that can be used to inflate the views, 528 * @param container The parent view that the additional views are attached to.The fragment 529 * should not add the view by itself. 530 * 531 * @return The background view for the onboarding screen, or {@code null}. 532 */ 533 @Nullable 534 abstract protected View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container); 535 536 /** 537 * Called to have the inherited class create content view. This is optional and the fragment 538 * which doesn't have the content view can return {@code null}. This is called inside 539 * {@link #onCreateView}. 540 * 541 * <p>The content view would be located at the center of the screen. 542 * 543 * @param inflater The LayoutInflater object that can be used to inflate the views, 544 * @param container The parent view that the additional views are attached to.The fragment 545 * should not add the view by itself. 546 * 547 * @return The content view for the onboarding screen, or {@code null}. 548 */ 549 @Nullable 550 abstract protected View onCreateContentView(LayoutInflater inflater, ViewGroup container); 551 552 /** 553 * Called to have the inherited class create foreground view. This is optional and the fragment 554 * which doesn't need the foreground view can return {@code null}. This is called inside 555 * {@link #onCreateView}. 556 * 557 * <p>This foreground view would have the highest z-order. 558 * 559 * @param inflater The LayoutInflater object that can be used to inflate the views, 560 * @param container The parent view that the additional views are attached to.The fragment 561 * should not add the view by itself. 562 * 563 * @return The foreground view for the onboarding screen, or {@code null}. 564 */ 565 @Nullable 566 abstract protected View onCreateForegroundView(LayoutInflater inflater, ViewGroup container); 567 568 /** 569 * Called when the onboarding flow finishes. 570 */ 571 protected void onFinishFragment() { } 572 573 /** 574 * Called when the page changes. 575 */ 576 private void onPageChangedInternal(int previousPage) { 577 if (mAnimator != null) { 578 mAnimator.end(); 579 } 580 mPageIndicator.onPageSelected(mCurrentPageIndex, true); 581 582 List<Animator> animators = new ArrayList<>(); 583 // Header animation 584 Animator fadeAnimator = null; 585 if (previousPage < getCurrentPageIndex()) { 586 // sliding to left 587 animators.add(createAnimator(mTitleView, false, Gravity.START, 0)); 588 animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.START, 589 DESCRIPTION_START_DELAY_MS)); 590 animators.add(createAnimator(mTitleView, true, Gravity.END, 591 HEADER_APPEAR_DELAY_MS)); 592 animators.add(createAnimator(mDescriptionView, true, Gravity.END, 593 HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS)); 594 } else { 595 // sliding to right 596 animators.add(createAnimator(mTitleView, false, Gravity.END, 0)); 597 animators.add(fadeAnimator = createAnimator(mDescriptionView, false, Gravity.END, 598 DESCRIPTION_START_DELAY_MS)); 599 animators.add(createAnimator(mTitleView, true, Gravity.START, 600 HEADER_APPEAR_DELAY_MS)); 601 animators.add(createAnimator(mDescriptionView, true, Gravity.START, 602 HEADER_APPEAR_DELAY_MS + DESCRIPTION_START_DELAY_MS)); 603 } 604 final int currentPageIndex = getCurrentPageIndex(); 605 fadeAnimator.addListener(new AnimatorListenerAdapter() { 606 @Override 607 public void onAnimationEnd(Animator animation) { 608 mTitleView.setText(getPageTitle(currentPageIndex)); 609 mDescriptionView.setText(getPageDescription(currentPageIndex)); 610 } 611 }); 612 613 // Animator for switching between page indicator and button. 614 if (getCurrentPageIndex() == getPageCount() - 1) { 615 mStartButton.setVisibility(View.VISIBLE); 616 Animator navigatorFadeOutAnimator = AnimatorInflater.loadAnimator(getActivity(), 617 R.animator.lb_onboarding_page_indicator_fade_out); 618 navigatorFadeOutAnimator.setTarget(mPageIndicator); 619 navigatorFadeOutAnimator.addListener(new AnimatorListenerAdapter() { 620 @Override 621 public void onAnimationEnd(Animator animation) { 622 mPageIndicator.setVisibility(View.GONE); 623 } 624 }); 625 animators.add(navigatorFadeOutAnimator); 626 Animator buttonFadeInAnimator = AnimatorInflater.loadAnimator(getActivity(), 627 R.animator.lb_onboarding_start_button_fade_in); 628 buttonFadeInAnimator.setTarget(mStartButton); 629 animators.add(buttonFadeInAnimator); 630 } else if (previousPage == getPageCount() - 1) { 631 mPageIndicator.setVisibility(View.VISIBLE); 632 Animator navigatorFadeInAnimator = AnimatorInflater.loadAnimator(getActivity(), 633 R.animator.lb_onboarding_page_indicator_fade_in); 634 navigatorFadeInAnimator.setTarget(mPageIndicator); 635 animators.add(navigatorFadeInAnimator); 636 Animator buttonFadeOutAnimator = AnimatorInflater.loadAnimator(getActivity(), 637 R.animator.lb_onboarding_start_button_fade_out); 638 buttonFadeOutAnimator.setTarget(mStartButton); 639 buttonFadeOutAnimator.addListener(new AnimatorListenerAdapter() { 640 @Override 641 public void onAnimationEnd(Animator animation) { 642 mStartButton.setVisibility(View.GONE); 643 } 644 }); 645 animators.add(buttonFadeOutAnimator); 646 } 647 mAnimator = new AnimatorSet(); 648 mAnimator.playTogether(animators); 649 mAnimator.start(); 650 onPageChanged(mCurrentPageIndex, previousPage); 651 } 652 653 /** 654 * Called when the page has been changed. 655 * 656 * @param newPage The new page. 657 * @param previousPage The previous page. 658 */ 659 protected void onPageChanged(int newPage, int previousPage) { } 660 661 private Animator createAnimator(View view, boolean fadeIn, int slideDirection, 662 long startDelay) { 663 boolean isLtr = getView().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; 664 boolean slideRight = (isLtr && slideDirection == Gravity.END) 665 || (!isLtr && slideDirection == Gravity.START) 666 || slideDirection == Gravity.RIGHT; 667 Animator fadeAnimator; 668 Animator slideAnimator; 669 if (fadeIn) { 670 fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 0.0f, 1.0f); 671 slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 672 slideRight ? sSlideDistance : -sSlideDistance, 0); 673 fadeAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR); 674 slideAnimator.setInterpolator(HEADER_APPEAR_INTERPOLATOR); 675 } else { 676 fadeAnimator = ObjectAnimator.ofFloat(view, View.ALPHA, 1.0f, 0.0f); 677 slideAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0, 678 slideRight ? sSlideDistance : -sSlideDistance); 679 fadeAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR); 680 slideAnimator.setInterpolator(HEADER_DISAPPEAR_INTERPOLATOR); 681 } 682 fadeAnimator.setDuration(HEADER_ANIMATION_DURATION_MS); 683 fadeAnimator.setTarget(view); 684 slideAnimator.setDuration(HEADER_ANIMATION_DURATION_MS); 685 slideAnimator.setTarget(view); 686 AnimatorSet animator = new AnimatorSet(); 687 animator.playTogether(fadeAnimator, slideAnimator); 688 if (startDelay > 0) { 689 animator.setStartDelay(startDelay); 690 } 691 return animator; 692 } 693} 694