Home | History | Annotate | Download | only in timer
      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 }