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         } 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
    415     public void onRightButtonClick(@NonNull Button right) {
    416         if (mCurrentView != mCreateTimerView) {
    417             animateToView(mCreateTimerView, null, true);
    418         }
    419     }
    420 
    421     @Override
    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      */
    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
    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      */
    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      */
    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      */
    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      */
    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 
    677     private boolean hasTimers() {
    678         return mAdapter.getCount() > 0;
    679     }
    680 
    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 
    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 
    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
    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
    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
    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
    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
    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
    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 }