1 /* 2 * Copyright (C) 2014 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 17 package com.android.deskclock.timer; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.os.Bundle; 26 import android.os.SystemClock; 27 import android.support.annotation.NonNull; 28 import android.support.annotation.VisibleForTesting; 29 import android.support.v4.view.ViewPager; 30 import android.view.KeyEvent; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.ViewTreeObserver; 35 import android.view.animation.AccelerateInterpolator; 36 import android.view.animation.DecelerateInterpolator; 37 import android.widget.Button; 38 import android.widget.ImageView; 39 40 import com.android.deskclock.AnimatorUtils; 41 import com.android.deskclock.DeskClock; 42 import com.android.deskclock.DeskClockFragment; 43 import com.android.deskclock.R; 44 import com.android.deskclock.Utils; 45 import com.android.deskclock.data.DataModel; 46 import com.android.deskclock.data.Timer; 47 import com.android.deskclock.data.TimerListener; 48 import com.android.deskclock.data.TimerStringFormatter; 49 import com.android.deskclock.events.Events; 50 import com.android.deskclock.uidata.UiDataModel; 51 52 import java.io.Serializable; 53 import java.util.Arrays; 54 55 import static android.view.View.ALPHA; 56 import static android.view.View.GONE; 57 import static android.view.View.INVISIBLE; 58 import static android.view.View.TRANSLATION_Y; 59 import static android.view.View.VISIBLE; 60 import static com.android.deskclock.uidata.UiDataModel.Tab.TIMERS; 61 62 /** 63 * Displays a vertical list of timers in all states. 64 */ 65 public final class TimerFragment extends DeskClockFragment { 66 67 private static final String EXTRA_TIMER_SETUP = "com.android.deskclock.action.TIMER_SETUP"; 68 69 private static final String KEY_TIMER_SETUP_STATE = "timer_setup_input"; 70 71 /** Notified when the user swipes vertically to change the visible timer. */ 72 private final TimerPageChangeListener mTimerPageChangeListener = new TimerPageChangeListener(); 73 74 /** Scheduled to update the timers while at least one is running. */ 75 private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable(); 76 77 /** Updates the {@link #mPageIndicators} in response to timers being added or removed. */ 78 private final TimerListener mTimerWatcher = new TimerWatcher(); 79 80 private TimerSetupView mCreateTimerView; 81 private ViewPager mViewPager; 82 private TimerPagerAdapter mAdapter; 83 private View mTimersView; 84 private View mCurrentView; 85 private ImageView[] mPageIndicators; 86 87 private Serializable mTimerSetupState; 88 89 /** {@code true} while this fragment is creating a new timer; {@code false} otherwise. */ 90 private boolean mCreatingTimer; 91 92 /** 93 * @return an Intent that selects the timers tab with the setup screen for a new timer in place. 94 */ 95 public static Intent createTimerSetupIntent(Context context) { 96 return new Intent(context, DeskClock.class).putExtra(EXTRA_TIMER_SETUP, true); 97 } 98 99 /** The public no-arg constructor required by all fragments. */ 100 public TimerFragment() { 101 super(TIMERS); 102 } 103 104 @Override 105 public View onCreateView(LayoutInflater inflater, ViewGroup container, 106 Bundle savedInstanceState) { 107 final View view = inflater.inflate(R.layout.timer_fragment, container, false); 108 109 mAdapter = new TimerPagerAdapter(getFragmentManager()); 110 mViewPager = (ViewPager) view.findViewById(R.id.vertical_view_pager); 111 mViewPager.setAdapter(mAdapter); 112 mViewPager.addOnPageChangeListener(mTimerPageChangeListener); 113 114 mTimersView = view.findViewById(R.id.timer_view); 115 mCreateTimerView = (TimerSetupView) view.findViewById(R.id.timer_setup); 116 mCreateTimerView.setFabContainer(this); 117 mPageIndicators = new ImageView[] { 118 (ImageView) view.findViewById(R.id.page_indicator0), 119 (ImageView) view.findViewById(R.id.page_indicator1), 120 (ImageView) view.findViewById(R.id.page_indicator2), 121 (ImageView) view.findViewById(R.id.page_indicator3) 122 }; 123 124 DataModel.getDataModel().addTimerListener(mAdapter); 125 DataModel.getDataModel().addTimerListener(mTimerWatcher); 126 127 // If timer setup state is present, retrieve it to be later honored. 128 if (savedInstanceState != null) { 129 mTimerSetupState = savedInstanceState.getSerializable(KEY_TIMER_SETUP_STATE); 130 } 131 132 return view; 133 } 134 135 @Override 136 public void onStart() { 137 super.onStart(); 138 139 // Initialize the page indicators. 140 updatePageIndicators(); 141 142 boolean createTimer = false; 143 int showTimerId = -1; 144 145 // Examine the intent of the parent activity to determine which view to display. 146 final Intent intent = getActivity().getIntent(); 147 if (intent != null) { 148 // These extras are single-use; remove them after honoring them. 149 createTimer = intent.getBooleanExtra(EXTRA_TIMER_SETUP, false); 150 intent.removeExtra(EXTRA_TIMER_SETUP); 151 152 showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1); 153 intent.removeExtra(TimerService.EXTRA_TIMER_ID); 154 } 155 156 // Choose the view to display in this fragment. 157 if (showTimerId != -1) { 158 // A specific timer must be shown; show the list of timers. 159 showTimersView(FAB_AND_BUTTONS_IMMEDIATE); 160 } else if (!hasTimers() || createTimer || mTimerSetupState != null) { 161 // No timers exist, a timer is being created, or the last view was timer setup; 162 // show the timer setup view. 163 showCreateTimerView(FAB_AND_BUTTONS_IMMEDIATE); 164 165 if (mTimerSetupState != null) { 166 mCreateTimerView.setState(mTimerSetupState); 167 mTimerSetupState = null; 168 } 169 } else { 170 // Otherwise, default to showing the list of timers. 171 showTimersView(FAB_AND_BUTTONS_IMMEDIATE); 172 } 173 174 // If the intent did not specify a timer to show, show the last timer that expired. 175 if (showTimerId == -1) { 176 final Timer timer = DataModel.getDataModel().getMostRecentExpiredTimer(); 177 showTimerId = timer == null ? -1 : timer.getId(); 178 } 179 180 // If a specific timer should be displayed, display the corresponding timer tab. 181 if (showTimerId != -1) { 182 final Timer timer = DataModel.getDataModel().getTimer(showTimerId); 183 if (timer != null) { 184 final int index = DataModel.getDataModel().getTimers().indexOf(timer); 185 mViewPager.setCurrentItem(index); 186 } 187 } 188 } 189 190 @Override 191 public void onResume() { 192 super.onResume(); 193 194 // We may have received a new intent while paused. 195 final Intent intent = getActivity().getIntent(); 196 if (intent != null && intent.hasExtra(TimerService.EXTRA_TIMER_ID)) { 197 // This extra is single-use; remove after honoring it. 198 final int showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1); 199 intent.removeExtra(TimerService.EXTRA_TIMER_ID); 200 201 final Timer timer = DataModel.getDataModel().getTimer(showTimerId); 202 if (timer != null) { 203 // A specific timer must be shown; show the list of timers. 204 final int index = DataModel.getDataModel().getTimers().indexOf(timer); 205 mViewPager.setCurrentItem(index); 206 207 animateToView(mTimersView, null, false); 208 } 209 } 210 } 211 212 @Override 213 public void onStop() { 214 super.onStop(); 215 216 // Stop updating the timers when this fragment is no longer visible. 217 stopUpdatingTime(); 218 } 219 220 @Override 221 public void onDestroyView() { 222 super.onDestroyView(); 223 224 DataModel.getDataModel().removeTimerListener(mAdapter); 225 DataModel.getDataModel().removeTimerListener(mTimerWatcher); 226 } 227 228 @Override 229 public void onSaveInstanceState(Bundle outState) { 230 super.onSaveInstanceState(outState); 231 232 // If the timer creation view is visible, store the input for later restoration. 233 if (mCurrentView == mCreateTimerView) { 234 mTimerSetupState = mCreateTimerView.getState(); 235 outState.putSerializable(KEY_TIMER_SETUP_STATE, mTimerSetupState); 236 } 237 } 238 239 private void updateFab(@NonNull ImageView fab, boolean animate) { 240 if (mCurrentView == mTimersView) { 241 final Timer timer = getTimer(); 242 if (timer == null) { 243 fab.setVisibility(INVISIBLE); 244 return; 245 } 246 247 fab.setVisibility(VISIBLE); 248 switch (timer.getState()) { 249 case RUNNING: 250 if (animate) { 251 fab.setImageResource(R.drawable.ic_play_pause_animation); 252 } else { 253 fab.setImageResource(R.drawable.ic_play_pause); 254 } 255 fab.setContentDescription(fab.getResources().getString(R.string.timer_stop)); 256 break; 257 case RESET: 258 if (animate) { 259 fab.setImageResource(R.drawable.ic_stop_play_animation); 260 } else { 261 fab.setImageResource(R.drawable.ic_pause_play); 262 } 263 fab.setContentDescription(fab.getResources().getString(R.string.timer_start)); 264 break; 265 case PAUSED: 266 if (animate) { 267 fab.setImageResource(R.drawable.ic_pause_play_animation); 268 } else { 269 fab.setImageResource(R.drawable.ic_pause_play); 270 } 271 fab.setContentDescription(fab.getResources().getString(R.string.timer_start)); 272 break; 273 case MISSED: 274 case EXPIRED: 275 fab.setImageResource(R.drawable.ic_stop_white_24dp); 276 fab.setContentDescription(fab.getResources().getString(R.string.timer_stop)); 277 break; 278 } 279 } else if (mCurrentView == mCreateTimerView) { 280 if (mCreateTimerView.hasValidInput()) { 281 fab.setImageResource(R.drawable.ic_start_white_24dp); 282 fab.setContentDescription(fab.getResources().getString(R.string.timer_start)); 283 fab.setVisibility(VISIBLE); 284 } else { 285 fab.setContentDescription(null); 286 fab.setVisibility(INVISIBLE); 287 } 288 } 289 } 290 291 @Override 292 public void onUpdateFab(@NonNull ImageView fab) { 293 updateFab(fab, false); 294 } 295 296 @Override 297 public void onMorphFab(@NonNull ImageView fab) { 298 // Update the fab's drawable to match the current timer state. 299 updateFab(fab, Utils.isNOrLater()); 300 // Animate the drawable. 301 AnimatorUtils.startDrawableAnimation(fab); 302 } 303 304 @Override 305 public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) { 306 if (mCurrentView == mTimersView) { 307 left.setClickable(true); 308 left.setText(R.string.timer_delete); 309 left.setContentDescription(left.getResources().getString(R.string.timer_delete)); 310 left.setVisibility(VISIBLE); 311 312 right.setClickable(true); 313 right.setText(R.string.timer_add_timer); 314 right.setContentDescription(right.getResources().getString(R.string.timer_add_timer)); 315 right.setVisibility(VISIBLE); 316 317 } else if (mCurrentView == mCreateTimerView) { 318 left.setClickable(true); 319 left.setText(R.string.timer_cancel); 320 left.setContentDescription(left.getResources().getString(R.string.timer_cancel)); 321 // If no timers yet exist, the user is forced to create the first one. 322 left.setVisibility(hasTimers() ? VISIBLE : INVISIBLE); 323 324 right.setVisibility(INVISIBLE); 325 } 326 } 327 328 @Override 329 public void onFabClick(@NonNull ImageView fab) { 330 if (mCurrentView == mTimersView) { 331 final Timer timer = getTimer(); 332 333 // If no timer is currently showing a fab action is meaningless. 334 if (timer == null) { 335 return; 336 } 337 338 final Context context = fab.getContext(); 339 final long currentTime = timer.getRemainingTime(); 340 341 switch (timer.getState()) { 342 case RUNNING: 343 DataModel.getDataModel().pauseTimer(timer); 344 Events.sendTimerEvent(R.string.action_stop, R.string.label_deskclock); 345 if (currentTime > 0) { 346 mTimersView.announceForAccessibility(TimerStringFormatter.formatString( 347 context, R.string.timer_accessibility_stopped, currentTime, true)); 348 } 349 break; 350 case PAUSED: 351 case RESET: 352 DataModel.getDataModel().startTimer(timer); 353 Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock); 354 if (currentTime > 0) { 355 mTimersView.announceForAccessibility(TimerStringFormatter.formatString( 356 context, R.string.timer_accessibility_started, currentTime, true)); 357 } 358 break; 359 case MISSED: 360 case EXPIRED: 361 DataModel.getDataModel().resetOrDeleteTimer(timer, R.string.label_deskclock); 362 break; 363 } 364 365 } else if (mCurrentView == mCreateTimerView) { 366 mCreatingTimer = true; 367 try { 368 // Create the new timer. 369 final long timerLength = mCreateTimerView.getTimeInMillis(); 370 final Timer timer = DataModel.getDataModel().addTimer(timerLength, "", false); 371 Events.sendTimerEvent(R.string.action_create, R.string.label_deskclock); 372 373 // Start the new timer. 374 DataModel.getDataModel().startTimer(timer); 375 Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock); 376 377 // Display the freshly created timer view. 378 mViewPager.setCurrentItem(0); 379 } finally { 380 mCreatingTimer = false; 381 } 382 383 // Return to the list of timers. 384 animateToView(mTimersView, null, true); 385 } 386 } 387 388 @Override 389 public void onLeftButtonClick(@NonNull Button left) { 390 if (mCurrentView == mTimersView) { 391 // Clicking the "delete" button. 392 final Timer timer = getTimer(); 393 if (timer == null) { 394 return; 395 } 396 397 if (mAdapter.getCount() > 1) { 398 animateTimerRemove(timer); 399 } else { 400 animateToView(mCreateTimerView, timer, false); 401 } 402 403 left.announceForAccessibility(getActivity().getString(R.string.timer_deleted)); 404 405 } else if (mCurrentView == mCreateTimerView) { 406 // Clicking the "cancel" button on the timer creation page returns to the timers list. 407 mCreateTimerView.reset(); 408 409 animateToView(mTimersView, null, false); 410 411 left.announceForAccessibility(getActivity().getString(R.string.timer_canceled)); 412 } 413 } 414 415 @Override 416 public void onRightButtonClick(@NonNull Button right) { 417 if (mCurrentView != mCreateTimerView) { 418 animateToView(mCreateTimerView, null, true); 419 } 420 } 421 422 @Override 423 public boolean onKeyDown(int keyCode, KeyEvent event) { 424 if (mCurrentView == mCreateTimerView) { 425 return mCreateTimerView.onKeyDown(keyCode, event); 426 } 427 return super.onKeyDown(keyCode, event); 428 } 429 430 /** 431 * Updates the state of the page indicators so they reflect the selected page in the context of 432 * all pages. 433 */ 434 private void updatePageIndicators() { 435 final int page = mViewPager.getCurrentItem(); 436 final int pageIndicatorCount = mPageIndicators.length; 437 final int pageCount = mAdapter.getCount(); 438 439 final int[] states = computePageIndicatorStates(page, pageIndicatorCount, pageCount); 440 for (int i = 0; i < states.length; i++) { 441 final int state = states[i]; 442 final ImageView pageIndicator = mPageIndicators[i]; 443 if (state == 0) { 444 pageIndicator.setVisibility(GONE); 445 } else { 446 pageIndicator.setVisibility(VISIBLE); 447 pageIndicator.setImageResource(state); 448 } 449 } 450 } 451 452 /** 453 * @param page the selected page; value between 0 and {@code pageCount} 454 * @param pageIndicatorCount the number of indicators displaying the {@code page} location 455 * @param pageCount the number of pages that exist 456 * @return an array of length {@code pageIndicatorCount} specifying which image to display for 457 * each page indicator or 0 if the page indicator should be hidden 458 */ 459 @VisibleForTesting 460 static int[] computePageIndicatorStates(int page, int pageIndicatorCount, int pageCount) { 461 // Compute the number of page indicators that will be visible. 462 final int rangeSize = Math.min(pageIndicatorCount, pageCount); 463 464 // Compute the inclusive range of pages to indicate centered around the selected page. 465 int rangeStart = page - (rangeSize / 2); 466 int rangeEnd = rangeStart + rangeSize - 1; 467 468 // Clamp the range of pages if they extend beyond the last page. 469 if (rangeEnd >= pageCount) { 470 rangeEnd = pageCount - 1; 471 rangeStart = rangeEnd - rangeSize + 1; 472 } 473 474 // Clamp the range of pages if they extend beyond the first page. 475 if (rangeStart < 0) { 476 rangeStart = 0; 477 rangeEnd = rangeSize - 1; 478 } 479 480 // Build the result with all page indicators initially hidden. 481 final int[] states = new int[pageIndicatorCount]; 482 Arrays.fill(states, 0); 483 484 // If 0 or 1 total pages exist, all page indicators must remain hidden. 485 if (rangeSize < 2) { 486 return states; 487 } 488 489 // Initialize the visible page indicators to be dark. 490 Arrays.fill(states, 0, rangeSize, R.drawable.ic_swipe_circle_dark); 491 492 // If more pages exist before the first page indicator, make it a fade-in gradient. 493 if (rangeStart > 0) { 494 states[0] = R.drawable.ic_swipe_circle_top; 495 } 496 497 // If more pages exist after the last page indicator, make it a fade-out gradient. 498 if (rangeEnd < pageCount - 1) { 499 states[rangeSize - 1] = R.drawable.ic_swipe_circle_bottom; 500 } 501 502 // Set the indicator of the selected page to be light. 503 states[page - rangeStart] = R.drawable.ic_swipe_circle_light; 504 505 return states; 506 } 507 508 /** 509 * Display the view that creates a new timer. 510 */ 511 private void showCreateTimerView(int updateTypes) { 512 // Stop animating the timers. 513 stopUpdatingTime(); 514 515 // Show the creation view; hide the timer view. 516 mTimersView.setVisibility(GONE); 517 mCreateTimerView.setVisibility(VISIBLE); 518 519 // Record the fact that the create view is visible. 520 mCurrentView = mCreateTimerView; 521 522 // Update the fab and buttons. 523 updateFab(updateTypes); 524 } 525 526 /** 527 * Display the view that lists all existing timers. 528 */ 529 private void showTimersView(int updateTypes) { 530 // Clear any defunct timer creation state; the next timer creation starts fresh. 531 mTimerSetupState = null; 532 533 // Show the timer view; hide the creation view. 534 mTimersView.setVisibility(VISIBLE); 535 mCreateTimerView.setVisibility(GONE); 536 537 // Record the fact that the create view is visible. 538 mCurrentView = mTimersView; 539 540 // Update the fab and buttons. 541 updateFab(updateTypes); 542 543 // Start animating the timers. 544 startUpdatingTime(); 545 } 546 547 /** 548 * @param timerToRemove the timer to be removed during the animation 549 */ 550 private void animateTimerRemove(final Timer timerToRemove) { 551 final long duration = UiDataModel.getUiDataModel().getShortAnimationDuration(); 552 553 final Animator fadeOut = ObjectAnimator.ofFloat(mViewPager, ALPHA, 1, 0); 554 fadeOut.setDuration(duration); 555 fadeOut.setInterpolator(new DecelerateInterpolator()); 556 fadeOut.addListener(new AnimatorListenerAdapter() { 557 @Override 558 public void onAnimationEnd(Animator animation) { 559 DataModel.getDataModel().removeTimer(timerToRemove); 560 Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock); 561 } 562 }); 563 564 final Animator fadeIn = ObjectAnimator.ofFloat(mViewPager, ALPHA, 0, 1); 565 fadeIn.setDuration(duration); 566 fadeIn.setInterpolator(new AccelerateInterpolator()); 567 568 final AnimatorSet animatorSet = new AnimatorSet(); 569 animatorSet.play(fadeOut).before(fadeIn); 570 animatorSet.start(); 571 } 572 573 /** 574 * @param toView one of {@link #mTimersView} or {@link #mCreateTimerView} 575 * @param timerToRemove the timer to be removed during the animation; {@code null} if no timer 576 * should be removed 577 * @param animateDown {@code true} if the views should animate upwards, otherwise downwards 578 */ 579 private void animateToView(final View toView, final Timer timerToRemove, 580 final boolean animateDown) { 581 if (mCurrentView == toView) { 582 return; 583 } 584 585 final boolean toTimers = toView == mTimersView; 586 if (toTimers) { 587 mTimersView.setVisibility(VISIBLE); 588 } else { 589 mCreateTimerView.setVisibility(VISIBLE); 590 } 591 // Avoid double-taps by enabling/disabling the set of buttons active on the new view. 592 updateFab(BUTTONS_DISABLE); 593 594 final long animationDuration = UiDataModel.getUiDataModel().getLongAnimationDuration(); 595 596 final ViewTreeObserver viewTreeObserver = toView.getViewTreeObserver(); 597 viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 598 @Override 599 public boolean onPreDraw() { 600 if (viewTreeObserver.isAlive()) { 601 viewTreeObserver.removeOnPreDrawListener(this); 602 } 603 604 final View view = mTimersView.findViewById(R.id.timer_time); 605 final float distanceY = view != null ? view.getHeight() + view.getY() : 0; 606 final float translationDistance = animateDown ? distanceY : -distanceY; 607 608 toView.setTranslationY(-translationDistance); 609 mCurrentView.setTranslationY(0f); 610 toView.setAlpha(0f); 611 mCurrentView.setAlpha(1f); 612 613 final Animator translateCurrent = ObjectAnimator.ofFloat(mCurrentView, 614 TRANSLATION_Y, translationDistance); 615 final Animator translateNew = ObjectAnimator.ofFloat(toView, TRANSLATION_Y, 0f); 616 final AnimatorSet translationAnimatorSet = new AnimatorSet(); 617 translationAnimatorSet.playTogether(translateCurrent, translateNew); 618 translationAnimatorSet.setDuration(animationDuration); 619 translationAnimatorSet.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN); 620 621 final Animator fadeOutAnimator = ObjectAnimator.ofFloat(mCurrentView, ALPHA, 0f); 622 fadeOutAnimator.setDuration(animationDuration / 2); 623 fadeOutAnimator.addListener(new AnimatorListenerAdapter() { 624 @Override 625 public void onAnimationStart(Animator animation) { 626 super.onAnimationStart(animation); 627 628 // The fade-out animation and fab-shrinking animation should run together. 629 updateFab(FAB_AND_BUTTONS_SHRINK); 630 } 631 632 @Override 633 public void onAnimationEnd(Animator animation) { 634 super.onAnimationEnd(animation); 635 if (toTimers) { 636 showTimersView(FAB_AND_BUTTONS_EXPAND); 637 638 // Reset the state of the create view. 639 mCreateTimerView.reset(); 640 } else { 641 showCreateTimerView(FAB_AND_BUTTONS_EXPAND); 642 } 643 644 if (timerToRemove != null) { 645 DataModel.getDataModel().removeTimer(timerToRemove); 646 Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock); 647 } 648 649 // Update the fab and button states now that the correct view is visible and 650 // before the animation to expand the fab and buttons starts. 651 updateFab(FAB_AND_BUTTONS_IMMEDIATE); 652 } 653 }); 654 655 final Animator fadeInAnimator = ObjectAnimator.ofFloat(toView, ALPHA, 1f); 656 fadeInAnimator.setDuration(animationDuration / 2); 657 fadeInAnimator.setStartDelay(animationDuration / 2); 658 659 final AnimatorSet animatorSet = new AnimatorSet(); 660 animatorSet.playTogether(fadeOutAnimator, fadeInAnimator, translationAnimatorSet); 661 animatorSet.addListener(new AnimatorListenerAdapter() { 662 @Override 663 public void onAnimationEnd(Animator animation) { 664 super.onAnimationEnd(animation); 665 mTimersView.setTranslationY(0f); 666 mCreateTimerView.setTranslationY(0f); 667 mTimersView.setAlpha(1f); 668 mCreateTimerView.setAlpha(1f); 669 } 670 }); 671 animatorSet.start(); 672 673 return true; 674 } 675 }); 676 } 677 678 private boolean hasTimers() { 679 return mAdapter.getCount() > 0; 680 } 681 682 private Timer getTimer() { 683 if (mViewPager == null) { 684 return null; 685 } 686 687 return mAdapter.getCount() == 0 ? null : mAdapter.getTimer(mViewPager.getCurrentItem()); 688 } 689 690 private void startUpdatingTime() { 691 // Ensure only one copy of the runnable is ever scheduled by first stopping updates. 692 stopUpdatingTime(); 693 mViewPager.post(mTimeUpdateRunnable); 694 } 695 696 private void stopUpdatingTime() { 697 mViewPager.removeCallbacks(mTimeUpdateRunnable); 698 } 699 700 /** 701 * Periodically refreshes the state of each timer. 702 */ 703 private class TimeUpdateRunnable implements Runnable { 704 @Override 705 public void run() { 706 final long startTime = SystemClock.elapsedRealtime(); 707 // If no timers require continuous updates, avoid scheduling the next update. 708 if (!mAdapter.updateTime()) { 709 return; 710 } 711 final long endTime = SystemClock.elapsedRealtime(); 712 713 // Try to maintain a consistent period of time between redraws. 714 final long delay = Math.max(0, startTime + 20 - endTime); 715 mTimersView.postDelayed(this, delay); 716 } 717 } 718 719 /** 720 * Update the page indicators and fab in response to a new timer becoming visible. 721 */ 722 private class TimerPageChangeListener extends ViewPager.SimpleOnPageChangeListener { 723 @Override 724 public void onPageSelected(int position) { 725 updatePageIndicators(); 726 updateFab(FAB_AND_BUTTONS_IMMEDIATE); 727 728 // Showing a new timer page may introduce a timer requiring continuous updates. 729 startUpdatingTime(); 730 } 731 732 @Override 733 public void onPageScrollStateChanged(int state) { 734 // Teasing a neighboring timer may introduce a timer requiring continuous updates. 735 if (state == ViewPager.SCROLL_STATE_DRAGGING) { 736 startUpdatingTime(); 737 } 738 } 739 } 740 741 /** 742 * Update the page indicators in response to timers being added or removed. 743 * Update the fab in response to the visible timer changing. 744 */ 745 private class TimerWatcher implements TimerListener { 746 @Override 747 public void timerAdded(Timer timer) { 748 updatePageIndicators(); 749 // If the timer is being created via this fragment avoid adjusting the fab. 750 // Timer setup view is about to be animated away in response to this timer creation. 751 // Changes to the fab immediately preceding that animation are jarring. 752 if (!mCreatingTimer) { 753 updateFab(FAB_AND_BUTTONS_IMMEDIATE); 754 } 755 } 756 757 @Override 758 public void timerUpdated(Timer before, Timer after) { 759 // If the timer started, animate the timers. 760 if (before.isReset() && !after.isReset()) { 761 startUpdatingTime(); 762 } 763 764 // Fetch the index of the change. 765 final int index = DataModel.getDataModel().getTimers().indexOf(after); 766 767 // If the timer just expired but is not displayed, display it now. 768 if (!before.isExpired() && after.isExpired() && index != mViewPager.getCurrentItem()) { 769 mViewPager.setCurrentItem(index, true); 770 771 } else if (mCurrentView == mTimersView && index == mViewPager.getCurrentItem()) { 772 // Morph the fab from its old state to new state if necessary. 773 if (before.getState() != after.getState() 774 && !(before.isPaused() && after.isReset())) { 775 updateFab(FAB_MORPH); 776 } 777 } 778 } 779 780 @Override 781 public void timerRemoved(Timer timer) { 782 updatePageIndicators(); 783 updateFab(FAB_AND_BUTTONS_IMMEDIATE); 784 785 if (mCurrentView == mTimersView && mAdapter.getCount() == 0) { 786 animateToView(mCreateTimerView, null, false); 787 } 788 } 789 } 790 }