com.androidinspain.deskclock.timer.TimerFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.androidinspain.deskclock.timer.TimerFragment.java

Source

/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.androidinspain.deskclock.timer;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.v4.view.ViewPager;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.Button;
import android.widget.ImageView;

import com.androidinspain.deskclock.AnimatorUtils;
import com.androidinspain.deskclock.DeskClock;
import com.androidinspain.deskclock.DeskClockFragment;
import com.androidinspain.deskclock.Utils;
import com.androidinspain.deskclock.data.DataModel;
import com.androidinspain.deskclock.data.Timer;
import com.androidinspain.deskclock.data.TimerListener;
import com.androidinspain.deskclock.data.TimerStringFormatter;
import com.androidinspain.deskclock.events.Events;
import com.androidinspain.deskclock.uidata.UiDataModel;

import java.io.Serializable;
import java.util.Arrays;

import static android.view.View.ALPHA;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.TRANSLATION_Y;
import static android.view.View.VISIBLE;

/**
 * Displays a vertical list of timers in all states.
 */
public final class TimerFragment extends DeskClockFragment {

    private static final String EXTRA_TIMER_SETUP = "com.androidinspain.deskclock.action.TIMER_SETUP";

    private static final String KEY_TIMER_SETUP_STATE = "timer_setup_input";

    /** Notified when the user swipes vertically to change the visible timer. */
    private final TimerPageChangeListener mTimerPageChangeListener = new TimerPageChangeListener();

    /** Scheduled to update the timers while at least one is running. */
    private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable();

    /** Updates the {@link #mPageIndicators} in response to timers being added or removed. */
    private final TimerListener mTimerWatcher = new TimerWatcher();

    private TimerSetupView mCreateTimerView;
    private ViewPager mViewPager;
    private TimerPagerAdapter mAdapter;
    private View mTimersView;
    private View mCurrentView;
    private ImageView[] mPageIndicators;

    private Serializable mTimerSetupState;

    /** {@code true} while this fragment is creating a new timer; {@code false} otherwise. */
    private boolean mCreatingTimer;

    /**
     * @return an Intent that selects the timers tab with the setup screen for a new timer in place.
     */
    public static Intent createTimerSetupIntent(Context context) {
        return new Intent(context, DeskClock.class).putExtra(EXTRA_TIMER_SETUP, true);
    }

    /** The public no-arg constructor required by all fragments. */
    public TimerFragment() {
        super(UiDataModel.Tab.TIMERS);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final View view = inflater.inflate(com.androidinspain.deskclock.R.layout.timer_fragment, container, false);

        mAdapter = new TimerPagerAdapter(getFragmentManager());
        mViewPager = (ViewPager) view.findViewById(com.androidinspain.deskclock.R.id.vertical_view_pager);
        mViewPager.setAdapter(mAdapter);
        mViewPager.addOnPageChangeListener(mTimerPageChangeListener);

        mTimersView = view.findViewById(com.androidinspain.deskclock.R.id.timer_view);
        mCreateTimerView = (TimerSetupView) view.findViewById(com.androidinspain.deskclock.R.id.timer_setup);
        mCreateTimerView.setFabContainer(this);
        mPageIndicators = new ImageView[] {
                (ImageView) view.findViewById(com.androidinspain.deskclock.R.id.page_indicator0),
                (ImageView) view.findViewById(com.androidinspain.deskclock.R.id.page_indicator1),
                (ImageView) view.findViewById(com.androidinspain.deskclock.R.id.page_indicator2),
                (ImageView) view.findViewById(com.androidinspain.deskclock.R.id.page_indicator3) };

        DataModel.getDataModel().addTimerListener(mAdapter);
        DataModel.getDataModel().addTimerListener(mTimerWatcher);

        // If timer setup state is present, retrieve it to be later honored.
        if (savedInstanceState != null) {
            mTimerSetupState = savedInstanceState.getSerializable(KEY_TIMER_SETUP_STATE);
        }

        return view;
    }

    @Override
    public void onStart() {
        super.onStart();

        // Initialize the page indicators.
        updatePageIndicators();

        boolean createTimer = false;
        int showTimerId = -1;

        // Examine the intent of the parent activity to determine which view to display.
        final Intent intent = getActivity().getIntent();
        if (intent != null) {
            // These extras are single-use; remove them after honoring them.
            createTimer = intent.getBooleanExtra(EXTRA_TIMER_SETUP, false);
            intent.removeExtra(EXTRA_TIMER_SETUP);

            showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1);
            intent.removeExtra(TimerService.EXTRA_TIMER_ID);
        }

        // Choose the view to display in this fragment.
        if (showTimerId != -1) {
            // A specific timer must be shown; show the list of timers.
            showTimersView(FAB_AND_BUTTONS_IMMEDIATE);
        } else if (!hasTimers() || createTimer || mTimerSetupState != null) {
            // No timers exist, a timer is being created, or the last view was timer setup;
            // show the timer setup view.
            showCreateTimerView(FAB_AND_BUTTONS_IMMEDIATE);

            if (mTimerSetupState != null) {
                mCreateTimerView.setState(mTimerSetupState);
                mTimerSetupState = null;
            }
        } else {
            // Otherwise, default to showing the list of timers.
            showTimersView(FAB_AND_BUTTONS_IMMEDIATE);
        }

        // If the intent did not specify a timer to show, show the last timer that expired.
        if (showTimerId == -1) {
            final Timer timer = DataModel.getDataModel().getMostRecentExpiredTimer();
            showTimerId = timer == null ? -1 : timer.getId();
        }

        // If a specific timer should be displayed, display the corresponding timer tab.
        if (showTimerId != -1) {
            final Timer timer = DataModel.getDataModel().getTimer(showTimerId);
            if (timer != null) {
                final int index = DataModel.getDataModel().getTimers().indexOf(timer);
                mViewPager.setCurrentItem(index);
            }
        }
    }

    @Override
    public void onResume() {
        super.onResume();

        // We may have received a new intent while paused.
        final Intent intent = getActivity().getIntent();
        if (intent != null && intent.hasExtra(TimerService.EXTRA_TIMER_ID)) {
            // This extra is single-use; remove after honoring it.
            final int showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1);
            intent.removeExtra(TimerService.EXTRA_TIMER_ID);

            final Timer timer = DataModel.getDataModel().getTimer(showTimerId);
            if (timer != null) {
                // A specific timer must be shown; show the list of timers.
                final int index = DataModel.getDataModel().getTimers().indexOf(timer);
                mViewPager.setCurrentItem(index);

                animateToView(mTimersView, null, false);
            }
        }
    }

    @Override
    public void onStop() {
        super.onStop();

        // Stop updating the timers when this fragment is no longer visible.
        stopUpdatingTime();
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();

        DataModel.getDataModel().removeTimerListener(mAdapter);
        DataModel.getDataModel().removeTimerListener(mTimerWatcher);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        // If the timer creation view is visible, store the input for later restoration.
        if (mCurrentView == mCreateTimerView) {
            mTimerSetupState = mCreateTimerView.getState();
            outState.putSerializable(KEY_TIMER_SETUP_STATE, mTimerSetupState);
        }
    }

    private void updateFab(@NonNull ImageView fab, boolean animate) {
        if (mCurrentView == mTimersView) {
            final Timer timer = getTimer();
            if (timer == null) {
                fab.setVisibility(INVISIBLE);
                return;
            }

            fab.setVisibility(VISIBLE);
            switch (timer.getState()) {
            case RUNNING:
                if (animate) {
                    fab.setImageResource(com.androidinspain.deskclock.R.drawable.ic_play_pause_animation);
                } else {
                    fab.setImageResource(com.androidinspain.deskclock.R.drawable.ic_play_pause);
                }
                fab.setContentDescription(
                        fab.getResources().getString(com.androidinspain.deskclock.R.string.timer_stop));
                break;
            case RESET:
                if (animate) {
                    fab.setImageResource(com.androidinspain.deskclock.R.drawable.ic_stop_play_animation);
                } else {
                    fab.setImageResource(com.androidinspain.deskclock.R.drawable.ic_pause_play);
                }
                fab.setContentDescription(
                        fab.getResources().getString(com.androidinspain.deskclock.R.string.timer_start));
                break;
            case PAUSED:
                if (animate) {
                    fab.setImageResource(com.androidinspain.deskclock.R.drawable.ic_pause_play_animation);
                } else {
                    fab.setImageResource(com.androidinspain.deskclock.R.drawable.ic_pause_play);
                }
                fab.setContentDescription(
                        fab.getResources().getString(com.androidinspain.deskclock.R.string.timer_start));
                break;
            case MISSED:
            case EXPIRED:
                fab.setImageResource(com.androidinspain.deskclock.R.drawable.ic_stop_white_24dp);
                fab.setContentDescription(
                        fab.getResources().getString(com.androidinspain.deskclock.R.string.timer_stop));
                break;
            }
        } else if (mCurrentView == mCreateTimerView) {
            if (mCreateTimerView.hasValidInput()) {
                fab.setImageResource(com.androidinspain.deskclock.R.drawable.ic_start_white_24dp);
                fab.setContentDescription(
                        fab.getResources().getString(com.androidinspain.deskclock.R.string.timer_start));
                fab.setVisibility(VISIBLE);
            } else {
                fab.setContentDescription(null);
                fab.setVisibility(INVISIBLE);
            }
        }
    }

    @Override
    public void onUpdateFab(@NonNull ImageView fab) {
        updateFab(fab, false);
    }

    @Override
    public void onMorphFab(@NonNull ImageView fab) {
        // Update the fab's drawable to match the current timer state.
        updateFab(fab, Utils.isNOrLater());
        // Animate the drawable.
        AnimatorUtils.startDrawableAnimation(fab);
    }

    @Override
    public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) {
        if (mCurrentView == mTimersView) {
            left.setClickable(true);
            left.setText(com.androidinspain.deskclock.R.string.timer_delete);
            left.setContentDescription(
                    left.getResources().getString(com.androidinspain.deskclock.R.string.timer_delete));
            left.setVisibility(VISIBLE);

            right.setClickable(true);
            right.setText(com.androidinspain.deskclock.R.string.timer_add_timer);
            right.setContentDescription(
                    right.getResources().getString(com.androidinspain.deskclock.R.string.timer_add_timer));
            right.setVisibility(VISIBLE);

        } else if (mCurrentView == mCreateTimerView) {
            left.setClickable(true);
            left.setText(com.androidinspain.deskclock.R.string.timer_cancel);
            left.setContentDescription(
                    left.getResources().getString(com.androidinspain.deskclock.R.string.timer_cancel));
            // If no timers yet exist, the user is forced to create the first one.
            left.setVisibility(hasTimers() ? VISIBLE : INVISIBLE);

            right.setVisibility(INVISIBLE);
        }
    }

    @Override
    public void onFabClick(@NonNull ImageView fab) {
        if (mCurrentView == mTimersView) {
            final Timer timer = getTimer();

            // If no timer is currently showing a fab action is meaningless.
            if (timer == null) {
                return;
            }

            final Context context = fab.getContext();
            final long currentTime = timer.getRemainingTime();

            switch (timer.getState()) {
            case RUNNING:
                DataModel.getDataModel().pauseTimer(timer);
                Events.sendTimerEvent(com.androidinspain.deskclock.R.string.action_stop,
                        com.androidinspain.deskclock.R.string.label_deskclock);
                if (currentTime > 0) {
                    mTimersView.announceForAccessibility(TimerStringFormatter.formatString(context,
                            com.androidinspain.deskclock.R.string.timer_accessibility_stopped, currentTime, true));
                }
                break;
            case PAUSED:
            case RESET:
                DataModel.getDataModel().startTimer(timer);
                Events.sendTimerEvent(com.androidinspain.deskclock.R.string.action_start,
                        com.androidinspain.deskclock.R.string.label_deskclock);
                if (currentTime > 0) {
                    mTimersView.announceForAccessibility(TimerStringFormatter.formatString(context,
                            com.androidinspain.deskclock.R.string.timer_accessibility_started, currentTime, true));
                }
                break;
            case MISSED:
            case EXPIRED:
                DataModel.getDataModel().resetOrDeleteTimer(timer,
                        com.androidinspain.deskclock.R.string.label_deskclock);
                break;
            }

        } else if (mCurrentView == mCreateTimerView) {
            mCreatingTimer = true;
            try {
                // Create the new timer.
                final long timerLength = mCreateTimerView.getTimeInMillis();
                final Timer timer = DataModel.getDataModel().addTimer(timerLength, "", false);
                Events.sendTimerEvent(com.androidinspain.deskclock.R.string.action_create,
                        com.androidinspain.deskclock.R.string.label_deskclock);

                // Start the new timer.
                DataModel.getDataModel().startTimer(timer);
                Events.sendTimerEvent(com.androidinspain.deskclock.R.string.action_start,
                        com.androidinspain.deskclock.R.string.label_deskclock);

                // Display the freshly created timer view.
                mViewPager.setCurrentItem(0);
            } finally {
                mCreatingTimer = false;
            }

            // Return to the list of timers.
            animateToView(mTimersView, null, true);
        }
    }

    @Override
    public void onLeftButtonClick(@NonNull Button left) {
        if (mCurrentView == mTimersView) {
            // Clicking the "delete" button.
            final Timer timer = getTimer();
            if (timer == null) {
                return;
            }

            if (mAdapter.getCount() > 1) {
                animateTimerRemove(timer);
            } else {
                animateToView(mCreateTimerView, timer, false);
            }

            left.announceForAccessibility(
                    getActivity().getString(com.androidinspain.deskclock.R.string.timer_deleted));

        } else if (mCurrentView == mCreateTimerView) {
            // Clicking the "cancel" button on the timer creation page returns to the timers list.
            mCreateTimerView.reset();

            animateToView(mTimersView, null, false);

            left.announceForAccessibility(
                    getActivity().getString(com.androidinspain.deskclock.R.string.timer_canceled));
        }
    }

    @Override
    public void onRightButtonClick(@NonNull Button right) {
        if (mCurrentView != mCreateTimerView) {
            animateToView(mCreateTimerView, null, true);
        }
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (mCurrentView == mCreateTimerView) {
            return mCreateTimerView.onKeyDown(keyCode, event);
        }
        return super.onKeyDown(keyCode, event);
    }

    /**
     * Updates the state of the page indicators so they reflect the selected page in the context of
     * all pages.
     */
    private void updatePageIndicators() {
        final int page = mViewPager.getCurrentItem();
        final int pageIndicatorCount = mPageIndicators.length;
        final int pageCount = mAdapter.getCount();

        final int[] states = computePageIndicatorStates(page, pageIndicatorCount, pageCount);
        for (int i = 0; i < states.length; i++) {
            final int state = states[i];
            final ImageView pageIndicator = mPageIndicators[i];
            if (state == 0) {
                pageIndicator.setVisibility(GONE);
            } else {
                pageIndicator.setVisibility(VISIBLE);
                pageIndicator.setImageResource(state);
            }
        }
    }

    /**
     * @param page the selected page; value between 0 and {@code pageCount}
     * @param pageIndicatorCount the number of indicators displaying the {@code page} location
     * @param pageCount the number of pages that exist
     * @return an array of length {@code pageIndicatorCount} specifying which image to display for
     *      each page indicator or 0 if the page indicator should be hidden
     */
    @VisibleForTesting
    static int[] computePageIndicatorStates(int page, int pageIndicatorCount, int pageCount) {
        // Compute the number of page indicators that will be visible.
        final int rangeSize = Math.min(pageIndicatorCount, pageCount);

        // Compute the inclusive range of pages to indicate centered around the selected page.
        int rangeStart = page - (rangeSize / 2);
        int rangeEnd = rangeStart + rangeSize - 1;

        // Clamp the range of pages if they extend beyond the last page.
        if (rangeEnd >= pageCount) {
            rangeEnd = pageCount - 1;
            rangeStart = rangeEnd - rangeSize + 1;
        }

        // Clamp the range of pages if they extend beyond the first page.
        if (rangeStart < 0) {
            rangeStart = 0;
            rangeEnd = rangeSize - 1;
        }

        // Build the result with all page indicators initially hidden.
        final int[] states = new int[pageIndicatorCount];
        Arrays.fill(states, 0);

        // If 0 or 1 total pages exist, all page indicators must remain hidden.
        if (rangeSize < 2) {
            return states;
        }

        // Initialize the visible page indicators to be dark.
        Arrays.fill(states, 0, rangeSize, com.androidinspain.deskclock.R.drawable.ic_swipe_circle_dark);

        // If more pages exist before the first page indicator, make it a fade-in gradient.
        if (rangeStart > 0) {
            states[0] = com.androidinspain.deskclock.R.drawable.ic_swipe_circle_top;
        }

        // If more pages exist after the last page indicator, make it a fade-out gradient.
        if (rangeEnd < pageCount - 1) {
            states[rangeSize - 1] = com.androidinspain.deskclock.R.drawable.ic_swipe_circle_bottom;
        }

        // Set the indicator of the selected page to be light.
        states[page - rangeStart] = com.androidinspain.deskclock.R.drawable.ic_swipe_circle_light;

        return states;
    }

    /**
     * Display the view that creates a new timer.
     */
    private void showCreateTimerView(int updateTypes) {
        // Stop animating the timers.
        stopUpdatingTime();

        // Show the creation view; hide the timer view.
        mTimersView.setVisibility(GONE);
        mCreateTimerView.setVisibility(VISIBLE);

        // Record the fact that the create view is visible.
        mCurrentView = mCreateTimerView;

        // Update the fab and buttons.
        updateFab(updateTypes);
    }

    /**
     * Display the view that lists all existing timers.
     */
    private void showTimersView(int updateTypes) {
        // Clear any defunct timer creation state; the next timer creation starts fresh.
        mTimerSetupState = null;

        // Show the timer view; hide the creation view.
        mTimersView.setVisibility(VISIBLE);
        mCreateTimerView.setVisibility(GONE);

        // Record the fact that the create view is visible.
        mCurrentView = mTimersView;

        // Update the fab and buttons.
        updateFab(updateTypes);

        // Start animating the timers.
        startUpdatingTime();
    }

    /**
     * @param timerToRemove the timer to be removed during the animation
     */
    private void animateTimerRemove(final Timer timerToRemove) {
        final long duration = UiDataModel.getUiDataModel().getShortAnimationDuration();

        final Animator fadeOut = ObjectAnimator.ofFloat(mViewPager, ALPHA, 1, 0);
        fadeOut.setDuration(duration);
        fadeOut.setInterpolator(new DecelerateInterpolator());
        fadeOut.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                DataModel.getDataModel().removeTimer(timerToRemove);
                Events.sendTimerEvent(com.androidinspain.deskclock.R.string.action_delete,
                        com.androidinspain.deskclock.R.string.label_deskclock);
            }
        });

        final Animator fadeIn = ObjectAnimator.ofFloat(mViewPager, ALPHA, 0, 1);
        fadeIn.setDuration(duration);
        fadeIn.setInterpolator(new AccelerateInterpolator());

        final AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.play(fadeOut).before(fadeIn);
        animatorSet.start();
    }

    /**
     * @param toView one of {@link #mTimersView} or {@link #mCreateTimerView}
     * @param timerToRemove the timer to be removed during the animation; {@code null} if no timer
     *      should be removed
     * @param animateDown {@code true} if the views should animate upwards, otherwise downwards
     */
    private void animateToView(final View toView, final Timer timerToRemove, final boolean animateDown) {
        if (mCurrentView == toView) {
            return;
        }

        final boolean toTimers = toView == mTimersView;
        if (toTimers) {
            mTimersView.setVisibility(VISIBLE);
        } else {
            mCreateTimerView.setVisibility(VISIBLE);
        }
        // Avoid double-taps by enabling/disabling the set of buttons active on the new view.
        updateFab(BUTTONS_DISABLE);

        final long animationDuration = UiDataModel.getUiDataModel().getLongAnimationDuration();

        final ViewTreeObserver viewTreeObserver = toView.getViewTreeObserver();
        viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                if (viewTreeObserver.isAlive()) {
                    viewTreeObserver.removeOnPreDrawListener(this);
                }

                final View view = mTimersView.findViewById(com.androidinspain.deskclock.R.id.timer_time);
                final float distanceY = view != null ? view.getHeight() + view.getY() : 0;
                final float translationDistance = animateDown ? distanceY : -distanceY;

                toView.setTranslationY(-translationDistance);
                mCurrentView.setTranslationY(0f);
                toView.setAlpha(0f);
                mCurrentView.setAlpha(1f);

                final Animator translateCurrent = ObjectAnimator.ofFloat(mCurrentView, TRANSLATION_Y,
                        translationDistance);
                final Animator translateNew = ObjectAnimator.ofFloat(toView, TRANSLATION_Y, 0f);
                final AnimatorSet translationAnimatorSet = new AnimatorSet();
                translationAnimatorSet.playTogether(translateCurrent, translateNew);
                translationAnimatorSet.setDuration(animationDuration);
                translationAnimatorSet.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);

                final Animator fadeOutAnimator = ObjectAnimator.ofFloat(mCurrentView, ALPHA, 0f);
                fadeOutAnimator.setDuration(animationDuration / 2);
                fadeOutAnimator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        super.onAnimationStart(animation);

                        // The fade-out animation and fab-shrinking animation should run together.
                        updateFab(FAB_AND_BUTTONS_SHRINK);
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        if (toTimers) {
                            showTimersView(FAB_AND_BUTTONS_EXPAND);

                            // Reset the state of the create view.
                            mCreateTimerView.reset();
                        } else {
                            showCreateTimerView(FAB_AND_BUTTONS_EXPAND);
                        }

                        if (timerToRemove != null) {
                            DataModel.getDataModel().removeTimer(timerToRemove);
                            Events.sendTimerEvent(com.androidinspain.deskclock.R.string.action_delete,
                                    com.androidinspain.deskclock.R.string.label_deskclock);
                        }

                        // Update the fab and button states now that the correct view is visible and
                        // before the animation to expand the fab and buttons starts.
                        updateFab(FAB_AND_BUTTONS_IMMEDIATE);
                    }
                });

                final Animator fadeInAnimator = ObjectAnimator.ofFloat(toView, ALPHA, 1f);
                fadeInAnimator.setDuration(animationDuration / 2);
                fadeInAnimator.setStartDelay(animationDuration / 2);

                final AnimatorSet animatorSet = new AnimatorSet();
                animatorSet.playTogether(fadeOutAnimator, fadeInAnimator, translationAnimatorSet);
                animatorSet.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        mTimersView.setTranslationY(0f);
                        mCreateTimerView.setTranslationY(0f);
                        mTimersView.setAlpha(1f);
                        mCreateTimerView.setAlpha(1f);
                    }
                });
                animatorSet.start();

                return true;
            }
        });
    }

    private boolean hasTimers() {
        return mAdapter.getCount() > 0;
    }

    private Timer getTimer() {
        if (mViewPager == null) {
            return null;
        }

        return mAdapter.getCount() == 0 ? null : mAdapter.getTimer(mViewPager.getCurrentItem());
    }

    private void startUpdatingTime() {
        // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
        stopUpdatingTime();
        mViewPager.post(mTimeUpdateRunnable);
    }

    private void stopUpdatingTime() {
        mViewPager.removeCallbacks(mTimeUpdateRunnable);
    }

    /**
     * Periodically refreshes the state of each timer.
     */
    private class TimeUpdateRunnable implements Runnable {
        @Override
        public void run() {
            final long startTime = SystemClock.elapsedRealtime();
            // If no timers require continuous updates, avoid scheduling the next update.
            if (!mAdapter.updateTime()) {
                return;
            }
            final long endTime = SystemClock.elapsedRealtime();

            // Try to maintain a consistent period of time between redraws.
            final long delay = Math.max(0, startTime + 20 - endTime);
            mTimersView.postDelayed(this, delay);
        }
    }

    /**
     * Update the page indicators and fab in response to a new timer becoming visible.
     */
    private class TimerPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
        @Override
        public void onPageSelected(int position) {
            updatePageIndicators();
            updateFab(FAB_AND_BUTTONS_IMMEDIATE);

            // Showing a new timer page may introduce a timer requiring continuous updates.
            startUpdatingTime();
        }

        @Override
        public void onPageScrollStateChanged(int state) {
            // Teasing a neighboring timer may introduce a timer requiring continuous updates.
            if (state == ViewPager.SCROLL_STATE_DRAGGING) {
                startUpdatingTime();
            }
        }
    }

    /**
     * Update the page indicators in response to timers being added or removed.
     * Update the fab in response to the visible timer changing.
     */
    private class TimerWatcher implements TimerListener {
        @Override
        public void timerAdded(Timer timer) {
            updatePageIndicators();
            // If the timer is being created via this fragment avoid adjusting the fab.
            // Timer setup view is about to be animated away in response to this timer creation.
            // Changes to the fab immediately preceding that animation are jarring.
            if (!mCreatingTimer) {
                updateFab(FAB_AND_BUTTONS_IMMEDIATE);
            }
        }

        @Override
        public void timerUpdated(Timer before, Timer after) {
            // If the timer started, animate the timers.
            if (before.isReset() && !after.isReset()) {
                startUpdatingTime();
            }

            // Fetch the index of the change.
            final int index = DataModel.getDataModel().getTimers().indexOf(after);

            // If the timer just expired but is not displayed, display it now.
            if (!before.isExpired() && after.isExpired() && index != mViewPager.getCurrentItem()) {
                mViewPager.setCurrentItem(index, true);

            } else if (mCurrentView == mTimersView && index == mViewPager.getCurrentItem()) {
                // Morph the fab from its old state to new state if necessary.
                if (before.getState() != after.getState() && !(before.isPaused() && after.isReset())) {
                    updateFab(FAB_MORPH);
                }
            }
        }

        @Override
        public void timerRemoved(Timer timer) {
            updatePageIndicators();
            updateFab(FAB_AND_BUTTONS_IMMEDIATE);

            if (mCurrentView == mTimersView && mAdapter.getCount() == 0) {
                animateToView(mCreateTimerView, null, false);
            }
        }
    }
}