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 androidx.annotation.NonNull;
28 import androidx.annotation.VisibleForTesting;
29 import androidx.viewpager.widget.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      */
createTimerSetupIntent(Context context)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. */
TimerFragment()100     public TimerFragment() {
101         super(TIMERS);
102     }
103 
104     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onStart()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
onResume()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
onStop()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
onDestroyView()221     public void onDestroyView() {
222         super.onDestroyView();
223 
224         DataModel.getDataModel().removeTimerListener(mAdapter);
225         DataModel.getDataModel().removeTimerListener(mTimerWatcher);
226     }
227 
228     @Override
onSaveInstanceState(Bundle outState)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 
updateFab(@onNull ImageView fab, boolean animate)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
onUpdateFab(@onNull ImageView fab)292     public void onUpdateFab(@NonNull ImageView fab) {
293         updateFab(fab, false);
294     }
295 
296     @Override
onMorphFab(@onNull ImageView fab)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
onUpdateFabButtons(@onNull Button left, @NonNull Button right)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
onFabClick(@onNull ImageView fab)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
onLeftButtonClick(@onNull Button left)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         } else if (mCurrentView == mCreateTimerView) {
405             // Clicking the "cancel" button on the timer creation page returns to the timers list.
406             mCreateTimerView.reset();
407 
408             animateToView(mTimersView, null, false);
409 
410             left.announceForAccessibility(getActivity().getString(R.string.timer_canceled));
411         }
412     }
413 
414     @Override
onRightButtonClick(@onNull Button right)415     public void onRightButtonClick(@NonNull Button right) {
416         if (mCurrentView != mCreateTimerView) {
417             animateToView(mCreateTimerView, null, true);
418         }
419     }
420 
421     @Override
onKeyDown(int keyCode, KeyEvent event)422     public boolean onKeyDown(int keyCode, KeyEvent event) {
423         if (mCurrentView == mCreateTimerView) {
424             return mCreateTimerView.onKeyDown(keyCode, event);
425         }
426         return super.onKeyDown(keyCode, event);
427     }
428 
429     /**
430      * Updates the state of the page indicators so they reflect the selected page in the context of
431      * all pages.
432      */
updatePageIndicators()433     private void updatePageIndicators() {
434         final int page = mViewPager.getCurrentItem();
435         final int pageIndicatorCount = mPageIndicators.length;
436         final int pageCount = mAdapter.getCount();
437 
438         final int[] states = computePageIndicatorStates(page, pageIndicatorCount, pageCount);
439         for (int i = 0; i < states.length; i++) {
440             final int state = states[i];
441             final ImageView pageIndicator = mPageIndicators[i];
442             if (state == 0) {
443                 pageIndicator.setVisibility(GONE);
444             } else {
445                 pageIndicator.setVisibility(VISIBLE);
446                 pageIndicator.setImageResource(state);
447             }
448         }
449     }
450 
451     /**
452      * @param page the selected page; value between 0 and {@code pageCount}
453      * @param pageIndicatorCount the number of indicators displaying the {@code page} location
454      * @param pageCount the number of pages that exist
455      * @return an array of length {@code pageIndicatorCount} specifying which image to display for
456      *      each page indicator or 0 if the page indicator should be hidden
457      */
458     @VisibleForTesting
computePageIndicatorStates(int page, int pageIndicatorCount, int pageCount)459     static int[] computePageIndicatorStates(int page, int pageIndicatorCount, int pageCount) {
460         // Compute the number of page indicators that will be visible.
461         final int rangeSize = Math.min(pageIndicatorCount, pageCount);
462 
463         // Compute the inclusive range of pages to indicate centered around the selected page.
464         int rangeStart = page - (rangeSize / 2);
465         int rangeEnd = rangeStart + rangeSize - 1;
466 
467         // Clamp the range of pages if they extend beyond the last page.
468         if (rangeEnd >= pageCount) {
469             rangeEnd = pageCount - 1;
470             rangeStart = rangeEnd - rangeSize + 1;
471         }
472 
473         // Clamp the range of pages if they extend beyond the first page.
474         if (rangeStart < 0) {
475             rangeStart = 0;
476             rangeEnd = rangeSize - 1;
477         }
478 
479         // Build the result with all page indicators initially hidden.
480         final int[] states = new int[pageIndicatorCount];
481         Arrays.fill(states, 0);
482 
483         // If 0 or 1 total pages exist, all page indicators must remain hidden.
484         if (rangeSize < 2) {
485             return states;
486         }
487 
488         // Initialize the visible page indicators to be dark.
489         Arrays.fill(states, 0, rangeSize, R.drawable.ic_swipe_circle_dark);
490 
491         // If more pages exist before the first page indicator, make it a fade-in gradient.
492         if (rangeStart > 0) {
493             states[0] = R.drawable.ic_swipe_circle_top;
494         }
495 
496         // If more pages exist after the last page indicator, make it a fade-out gradient.
497         if (rangeEnd < pageCount - 1) {
498             states[rangeSize - 1] = R.drawable.ic_swipe_circle_bottom;
499         }
500 
501         // Set the indicator of the selected page to be light.
502         states[page - rangeStart] = R.drawable.ic_swipe_circle_light;
503 
504         return states;
505     }
506 
507     /**
508      * Display the view that creates a new timer.
509      */
showCreateTimerView(int updateTypes)510     private void showCreateTimerView(int updateTypes) {
511         // Stop animating the timers.
512         stopUpdatingTime();
513 
514         // Show the creation view; hide the timer view.
515         mTimersView.setVisibility(GONE);
516         mCreateTimerView.setVisibility(VISIBLE);
517 
518         // Record the fact that the create view is visible.
519         mCurrentView = mCreateTimerView;
520 
521         // Update the fab and buttons.
522         updateFab(updateTypes);
523     }
524 
525     /**
526      * Display the view that lists all existing timers.
527      */
showTimersView(int updateTypes)528     private void showTimersView(int updateTypes) {
529         // Clear any defunct timer creation state; the next timer creation starts fresh.
530         mTimerSetupState = null;
531 
532         // Show the timer view; hide the creation view.
533         mTimersView.setVisibility(VISIBLE);
534         mCreateTimerView.setVisibility(GONE);
535 
536         // Record the fact that the create view is visible.
537         mCurrentView = mTimersView;
538 
539         // Update the fab and buttons.
540         updateFab(updateTypes);
541 
542         // Start animating the timers.
543         startUpdatingTime();
544     }
545 
546     /**
547      * @param timerToRemove the timer to be removed during the animation
548      */
animateTimerRemove(final Timer timerToRemove)549     private void animateTimerRemove(final Timer timerToRemove) {
550         final long duration = UiDataModel.getUiDataModel().getShortAnimationDuration();
551 
552         final Animator fadeOut = ObjectAnimator.ofFloat(mViewPager, ALPHA, 1, 0);
553         fadeOut.setDuration(duration);
554         fadeOut.setInterpolator(new DecelerateInterpolator());
555         fadeOut.addListener(new AnimatorListenerAdapter() {
556             @Override
557             public void onAnimationEnd(Animator animation) {
558                 DataModel.getDataModel().removeTimer(timerToRemove);
559                 Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock);
560             }
561         });
562 
563         final Animator fadeIn = ObjectAnimator.ofFloat(mViewPager, ALPHA, 0, 1);
564         fadeIn.setDuration(duration);
565         fadeIn.setInterpolator(new AccelerateInterpolator());
566 
567         final AnimatorSet animatorSet = new AnimatorSet();
568         animatorSet.play(fadeOut).before(fadeIn);
569         animatorSet.start();
570     }
571 
572     /**
573      * @param toView one of {@link #mTimersView} or {@link #mCreateTimerView}
574      * @param timerToRemove the timer to be removed during the animation; {@code null} if no timer
575      *      should be removed
576      * @param animateDown {@code true} if the views should animate upwards, otherwise downwards
577      */
animateToView(final View toView, final Timer timerToRemove, final boolean animateDown)578     private void animateToView(final View toView, final Timer timerToRemove,
579             final boolean animateDown) {
580         if (mCurrentView == toView) {
581             return;
582         }
583 
584         final boolean toTimers = toView == mTimersView;
585         if (toTimers) {
586             mTimersView.setVisibility(VISIBLE);
587         } else {
588             mCreateTimerView.setVisibility(VISIBLE);
589         }
590         // Avoid double-taps by enabling/disabling the set of buttons active on the new view.
591         updateFab(BUTTONS_DISABLE);
592 
593         final long animationDuration = UiDataModel.getUiDataModel().getLongAnimationDuration();
594 
595         final ViewTreeObserver viewTreeObserver = toView.getViewTreeObserver();
596         viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
597             @Override
598             public boolean onPreDraw() {
599                 if (viewTreeObserver.isAlive()) {
600                     viewTreeObserver.removeOnPreDrawListener(this);
601                 }
602 
603                 final View view = mTimersView.findViewById(R.id.timer_time);
604                 final float distanceY = view != null ? view.getHeight() + view.getY() : 0;
605                 final float translationDistance = animateDown ? distanceY : -distanceY;
606 
607                 toView.setTranslationY(-translationDistance);
608                 mCurrentView.setTranslationY(0f);
609                 toView.setAlpha(0f);
610                 mCurrentView.setAlpha(1f);
611 
612                 final Animator translateCurrent = ObjectAnimator.ofFloat(mCurrentView,
613                         TRANSLATION_Y, translationDistance);
614                 final Animator translateNew = ObjectAnimator.ofFloat(toView, TRANSLATION_Y, 0f);
615                 final AnimatorSet translationAnimatorSet = new AnimatorSet();
616                 translationAnimatorSet.playTogether(translateCurrent, translateNew);
617                 translationAnimatorSet.setDuration(animationDuration);
618                 translationAnimatorSet.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
619 
620                 final Animator fadeOutAnimator = ObjectAnimator.ofFloat(mCurrentView, ALPHA, 0f);
621                 fadeOutAnimator.setDuration(animationDuration / 2);
622                 fadeOutAnimator.addListener(new AnimatorListenerAdapter() {
623                     @Override
624                     public void onAnimationStart(Animator animation) {
625                         super.onAnimationStart(animation);
626 
627                         // The fade-out animation and fab-shrinking animation should run together.
628                         updateFab(FAB_AND_BUTTONS_SHRINK);
629                     }
630 
631                     @Override
632                     public void onAnimationEnd(Animator animation) {
633                         super.onAnimationEnd(animation);
634                         if (toTimers) {
635                             showTimersView(FAB_AND_BUTTONS_EXPAND);
636 
637                             // Reset the state of the create view.
638                             mCreateTimerView.reset();
639                         } else {
640                             showCreateTimerView(FAB_AND_BUTTONS_EXPAND);
641                         }
642 
643                         if (timerToRemove != null) {
644                             DataModel.getDataModel().removeTimer(timerToRemove);
645                             Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock);
646                         }
647 
648                         // Update the fab and button states now that the correct view is visible and
649                         // before the animation to expand the fab and buttons starts.
650                         updateFab(FAB_AND_BUTTONS_IMMEDIATE);
651                     }
652                 });
653 
654                 final Animator fadeInAnimator = ObjectAnimator.ofFloat(toView, ALPHA, 1f);
655                 fadeInAnimator.setDuration(animationDuration / 2);
656                 fadeInAnimator.setStartDelay(animationDuration / 2);
657 
658                 final AnimatorSet animatorSet = new AnimatorSet();
659                 animatorSet.playTogether(fadeOutAnimator, fadeInAnimator, translationAnimatorSet);
660                 animatorSet.addListener(new AnimatorListenerAdapter() {
661                     @Override
662                     public void onAnimationEnd(Animator animation) {
663                         super.onAnimationEnd(animation);
664                         mTimersView.setTranslationY(0f);
665                         mCreateTimerView.setTranslationY(0f);
666                         mTimersView.setAlpha(1f);
667                         mCreateTimerView.setAlpha(1f);
668                     }
669                 });
670                 animatorSet.start();
671 
672                 return true;
673             }
674         });
675     }
676 
hasTimers()677     private boolean hasTimers() {
678         return mAdapter.getCount() > 0;
679     }
680 
getTimer()681     private Timer getTimer() {
682         if (mViewPager == null) {
683             return null;
684         }
685 
686         return mAdapter.getCount() == 0 ? null : mAdapter.getTimer(mViewPager.getCurrentItem());
687     }
688 
startUpdatingTime()689     private void startUpdatingTime() {
690         // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
691         stopUpdatingTime();
692         mViewPager.post(mTimeUpdateRunnable);
693     }
694 
stopUpdatingTime()695     private void stopUpdatingTime() {
696         mViewPager.removeCallbacks(mTimeUpdateRunnable);
697     }
698 
699     /**
700      * Periodically refreshes the state of each timer.
701      */
702     private class TimeUpdateRunnable implements Runnable {
703         @Override
run()704         public void run() {
705             final long startTime = SystemClock.elapsedRealtime();
706             // If no timers require continuous updates, avoid scheduling the next update.
707             if (!mAdapter.updateTime()) {
708                 return;
709             }
710             final long endTime = SystemClock.elapsedRealtime();
711 
712             // Try to maintain a consistent period of time between redraws.
713             final long delay = Math.max(0, startTime + 20 - endTime);
714             mTimersView.postDelayed(this, delay);
715         }
716     }
717 
718     /**
719      * Update the page indicators and fab in response to a new timer becoming visible.
720      */
721     private class TimerPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
722         @Override
onPageSelected(int position)723         public void onPageSelected(int position) {
724             updatePageIndicators();
725             updateFab(FAB_AND_BUTTONS_IMMEDIATE);
726 
727             // Showing a new timer page may introduce a timer requiring continuous updates.
728             startUpdatingTime();
729         }
730 
731         @Override
onPageScrollStateChanged(int state)732         public void onPageScrollStateChanged(int state) {
733             // Teasing a neighboring timer may introduce a timer requiring continuous updates.
734             if (state == ViewPager.SCROLL_STATE_DRAGGING) {
735                 startUpdatingTime();
736             }
737         }
738     }
739 
740     /**
741      * Update the page indicators in response to timers being added or removed.
742      * Update the fab in response to the visible timer changing.
743      */
744     private class TimerWatcher implements TimerListener {
745         @Override
timerAdded(Timer timer)746         public void timerAdded(Timer timer) {
747             updatePageIndicators();
748             // If the timer is being created via this fragment avoid adjusting the fab.
749             // Timer setup view is about to be animated away in response to this timer creation.
750             // Changes to the fab immediately preceding that animation are jarring.
751             if (!mCreatingTimer) {
752                 updateFab(FAB_AND_BUTTONS_IMMEDIATE);
753             }
754         }
755 
756         @Override
timerUpdated(Timer before, Timer after)757         public void timerUpdated(Timer before, Timer after) {
758             // If the timer started, animate the timers.
759             if (before.isReset() && !after.isReset()) {
760                 startUpdatingTime();
761             }
762 
763             // Fetch the index of the change.
764             final int index = DataModel.getDataModel().getTimers().indexOf(after);
765 
766             // If the timer just expired but is not displayed, display it now.
767             if (!before.isExpired() && after.isExpired() && index != mViewPager.getCurrentItem()) {
768                 mViewPager.setCurrentItem(index, true);
769 
770             } else if (mCurrentView == mTimersView && index == mViewPager.getCurrentItem()) {
771                 // Morph the fab from its old state to new state if necessary.
772                 if (before.getState() != after.getState()
773                         && !(before.isPaused() && after.isReset())) {
774                     updateFab(FAB_MORPH);
775                 }
776             }
777         }
778 
779         @Override
timerRemoved(Timer timer)780         public void timerRemoved(Timer timer) {
781             updatePageIndicators();
782             updateFab(FAB_AND_BUTTONS_IMMEDIATE);
783 
784             if (mCurrentView == mTimersView && mAdapter.getCount() == 0) {
785                 animateToView(mCreateTimerView, null, false);
786             }
787         }
788     }
789 }