• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
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 
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
onRightButtonClick(@onNull Button right)416     public void onRightButtonClick(@NonNull Button right) {
417         if (mCurrentView != mCreateTimerView) {
418             animateToView(mCreateTimerView, null, true);
419         }
420     }
421 
422     @Override
onKeyDown(int keyCode, KeyEvent event)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      */
updatePageIndicators()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
computePageIndicatorStates(int page, int pageIndicatorCount, int pageCount)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      */
showCreateTimerView(int updateTypes)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      */
showTimersView(int updateTypes)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      */
animateTimerRemove(final Timer timerToRemove)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      */
animateToView(final View toView, final Timer timerToRemove, final boolean animateDown)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 
hasTimers()678     private boolean hasTimers() {
679         return mAdapter.getCount() > 0;
680     }
681 
getTimer()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 
startUpdatingTime()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 
stopUpdatingTime()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
run()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
onPageSelected(int position)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
onPageScrollStateChanged(int state)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
timerAdded(Timer timer)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
timerUpdated(Timer before, Timer after)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
timerRemoved(Timer timer)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 }