com.stasbar.knowyourself.stopwatch.StopwatchFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.stasbar.knowyourself.stopwatch.StopwatchFragment.java

Source

/*
 * Copyright (C) 2015 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.stasbar.knowyourself.stopwatch;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.graphics.ColorUtils;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.ImageView;

import com.stasbar.knowyourself.LogUtils;
import com.stasbar.knowyourself.R;
import com.stasbar.knowyourself.data.Stopwatch;
import com.stasbar.knowyourself.Utils;
import com.stasbar.knowyourself.data.ActivityItem;
import com.stasbar.knowyourself.data.DataModel;
import com.stasbar.knowyourself.data.Lap;
import com.stasbar.knowyourself.data.StopwatchListener;
import com.stasbar.knowyourself.timer.CountingTimerView;
import com.stasbar.knowyourself.uidata.UiDataModel;

import static android.graphics.drawable.GradientDrawable.Orientation.TOP_BOTTOM;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;

/**
 * Fragment that shows the stopwatch and recorded laps.
 */
public final class StopwatchFragment extends Fragment {
    /** Milliseconds between redraws. */
    private static final int REDRAW_PERIOD = 25;
    private static final String ACTIVITY_ITEM = "activity_item";

    /** Scheduled to update the stopwatch time and current lap time while stopwatch is running. */
    private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable();

    /** Updates the user interface in response to stopwatch changes. */
    private final StopwatchListener mStopwatchWatcher = new StopwatchWatcher();

    /** Draws the reference lap while the stopwatch is running. */
    private StopwatchCircleView mTime;

    /** Displays the current stopwatch time. */
    private CountingTimerView mTimeText;

    /** Number of laps the stopwatch is tracking. */
    private int mLapCount;

    private ActivityItem activityItem;

    /** The public no-arg constructor required by all fragments. */
    public StopwatchFragment() {
    }

    public static StopwatchFragment newInstance(ActivityItem activityItem) {
        StopwatchFragment fragment = new StopwatchFragment();
        Bundle bundle = new Bundle();
        bundle.putParcelable(ACTIVITY_ITEM, activityItem);
        fragment.setArguments(bundle);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            activityItem = getArguments().getParcelable(ACTIVITY_ITEM);
        } else {
            throw new IllegalArgumentException("Arguments can not be null");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {

        final View v = inflater.inflate(R.layout.stopwatch_fragment, container, false);
        mTime = (StopwatchCircleView) v.findViewById(R.id.stopwatch_time);

        // Timer text serves as a virtual start/stop button.
        mTimeText = (CountingTimerView) v.findViewById(R.id.stopwatch_time_text);
        mTimeText.setShowBoundingCircle(mTime != null);
        mTimeText.setVirtualButtonEnabled(true);
        mTimeText.registerVirtualButtonAction(new ToggleStopwatchRunnable());

        DataModel.getDataModel().addStopwatchListener(mStopwatchWatcher);

        return v;
    }

    @Override
    public void onStart() {
        super.onStart();
        // Synchronize the user interface with the data model.
        updateUI();
    }

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

        // Stop all updates while the fragment is not visible.
        stopUpdatingTime();
        mTimeText.blinkTimeStr(false);

        // Release the wake lock if it is currently held.
        releaseWakeLock();
    }

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

        DataModel.getDataModel().removeStopwatchListener(mStopwatchWatcher);
    }

    public void onFabClick(@NonNull ImageView fab) {
        toggleStopwatchState();
    }

    public void onUpdateFab(@NonNull ImageView fab) {
        if (getStopwatch().isRunning()) {
            fab.setImageResource(R.drawable.ic_pause_white_24dp);
            fab.setContentDescription(fab.getResources().getString(R.string.sw_pause_button));
        } else {
            fab.setImageResource(R.drawable.ic_start_white_24dp);
            fab.setContentDescription(fab.getResources().getString(R.string.sw_start_button));
        }
        fab.setVisibility(VISIBLE);
    }

    public void onUpdateFabButtons(@NonNull ImageButton left, @NonNull ImageButton right) {
        right.setImageResource(R.drawable.ic_share);
        right.setContentDescription(right.getResources().getString(R.string.sw_share_button));

        switch (getStopwatch().getState()) {
        case RESET:
            left.setEnabled(false);
            left.setVisibility(INVISIBLE);
            right.setVisibility(INVISIBLE);
            break;
        case RUNNING:
            final boolean canRecordLaps = canRecordMoreLaps();
            left.setImageResource(R.drawable.ic_lap);
            left.setContentDescription(left.getResources().getString(R.string.sw_lap_button));
            left.setEnabled(canRecordLaps);
            left.setVisibility(canRecordLaps ? VISIBLE : INVISIBLE);
            right.setVisibility(INVISIBLE);
            break;
        case PAUSED:
            left.setEnabled(true);
            left.setImageResource(R.drawable.ic_reset);
            left.setContentDescription(left.getResources().getString(R.string.sw_reset_button));
            left.setVisibility(VISIBLE);
            right.setVisibility(VISIBLE);
            break;
        }
    }

    public void onMorphFabButtons(@NonNull ImageButton left, @NonNull ImageButton right) {
        right.setImageResource(R.drawable.ic_share);
        right.setContentDescription(right.getResources().getString(R.string.sw_share_button));

        switch (getStopwatch().getState()) {
        case RESET:
            left.setEnabled(false);
            left.setVisibility(INVISIBLE);
            right.setVisibility(INVISIBLE);
            break;
        case RUNNING: {
            final boolean canRecordLaps = canRecordMoreLaps();
            left.setContentDescription(left.getResources().getString(R.string.sw_lap_button));
            left.setEnabled(canRecordLaps);
            left.setVisibility(canRecordLaps ? VISIBLE : INVISIBLE);
            right.setVisibility(INVISIBLE);
            final Drawable icon = left.getDrawable();
            if (icon instanceof Animatable) {
                ((Animatable) icon).start();
            }
            break;
        }
        case PAUSED: {
            left.setEnabled(true);
            updateResetIcon(left);
            left.setContentDescription(left.getResources().getString(R.string.sw_reset_button));
            left.setVisibility(VISIBLE);
            right.setVisibility(VISIBLE);
            final Drawable icon = left.getDrawable();
            if (icon instanceof Animatable) {
                ((Animatable) icon).start();
            }
            break;
        }
        }
    }

    private void updateResetIcon(ImageButton button) {
        if (Utils.isLMR1OrLater()) {
            button.setImageResource(R.drawable.ic_lap_reset_animation);
        } else {
            button.setImageResource(R.drawable.ic_reset);
        }
    }

    /**
     * Start the stopwatch.
     */
    private void doStart() {
        DataModel.getDataModel().startStopwatch();
    }

    /**
     * Pause the stopwatch.
     */
    private void doPause() {
        DataModel.getDataModel().pauseStopwatch();
    }

    /**
     * Reset the stopwatch.
     */
    private void doReset() {
        DataModel.getDataModel().resetStopwatch();
    }

    private void adjustWakeLock() {
        final boolean appInForeground = DataModel.getDataModel().isApplicationInForeground();
        if (getStopwatch().isRunning() && appInForeground) {
            getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        } else {
            releaseWakeLock();
        }
    }

    private void releaseWakeLock() {
        getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    }

    /**
     * Either pause or start the stopwatch based on its current state.
     */
    private void toggleStopwatchState() {
        if (getStopwatch().isRunning()) {
            doPause();
        } else {
            doStart();
        }
    }

    private Stopwatch getStopwatch() {
        return DataModel.getDataModel().getStopwatch();
    }

    private boolean canRecordMoreLaps() {
        return DataModel.getDataModel().canAddMoreLaps();
    }

    /**
     * Post the first runnable to update times within the UI. It will reschedule itself as needed.
     */
    private void startUpdatingTime() {
        // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
        stopUpdatingTime();
        mTimeText.post(mTimeUpdateRunnable);
    }

    /**
     * Remove the runnable that updates times within the UI.
     */
    private void stopUpdatingTime() {
        mTimeText.removeCallbacks(mTimeUpdateRunnable);
    }

    /**
     * Update all time displays based on a single snapshot of the stopwatch progress. This includes
     * the stopwatch time drawn in the circle, the current lap time and the total elapsed time in
     * the list of laps.
     */
    private void updateTime() {
        // Compute the total time of the stopwatch.
        final Stopwatch stopwatch = getStopwatch();
        final long totalTime = stopwatch.getTotalTime();

        // Update the total time display.
        mTimeText.setTime(totalTime, true);

    }

    /**
     * Synchronize the UI state with the model data.
     */
    private void updateUI() {
        LogUtils.d("updateUI");
        adjustWakeLock();

        // Draw the latest stopwatch and current lap times.
        updateTime();

        if (mTime != null) {
            mTime.update();
        }

        // Start updates if the stopwatch is running.
        final Stopwatch stopwatch = getStopwatch();
        if (stopwatch.isRunning()) {
            startUpdatingTime();
        }

        // Blink text iff the stopwatch is paused.
        mTimeText.blinkTimeStr(stopwatch.isPaused());

    }

    /**
     * This runnable periodically updates times throughout the UI. It stops these updates when the
     * stopwatch is no longer running.
     */
    private final class TimeUpdateRunnable implements Runnable {
        @Override
        public void run() {
            final long startTime = SystemClock.elapsedRealtime();

            updateTime();

            if (getStopwatch().isRunning()) {
                // Try to maintain a consistent period of time between redraws.
                final long endTime = SystemClock.elapsedRealtime();
                final long delay = Math.max(0, startTime + REDRAW_PERIOD - endTime);

                mTimeText.postDelayed(this, delay);
            }
        }
    }

    /**
     * Tapping the stopwatch text also toggles the stopwatch state, just like the fab.
     */
    private final class ToggleStopwatchRunnable implements Runnable {
        @Override
        public void run() {
            toggleStopwatchState();
        }
    }

    /**
     * Update the user interface in response to a stopwatch change.
     */
    private class StopwatchWatcher implements StopwatchListener {
        @Override
        public void stopwatchUpdated(Stopwatch before, Stopwatch after) {
            LogUtils.d("stopwatchUpdated");
            if (DataModel.getDataModel().isApplicationInForeground()) {

                updateUI();
            }
        }

        @Override
        public void lapAdded(Lap lap) {
        }
    }

    /**
     * Draws a tinting gradient over the bottom of the stopwatch laps list. This reduces the
     * contrast between floating buttons and the laps list content.
     */
    private static final class GradientItemDecoration extends RecyclerView.ItemDecoration {

        //  0% -  25% of gradient length -> opacity changes from 0% to 50%
        // 25% -  90% of gradient length -> opacity changes from 50% to 100%
        // 90% - 100% of gradient length -> opacity remains at 100%
        private static final int[] ALPHAS = { 0x00, // 0%
                0x1A, // 10%
                0x33, // 20%
                0x4D, // 30%
                0x66, // 40%
                0x80, // 50%
                0x89, // 53.8%
                0x93, // 57.6%
                0x9D, // 61.5%
                0xA7, // 65.3%
                0xB1, // 69.2%
                0xBA, // 73.0%
                0xC4, // 76.9%
                0xCE, // 80.7%
                0xD8, // 84.6%
                0xE2, // 88.4%
                0xEB, // 92.3%
                0xF5, // 96.1%
                0xFF, // 100%
                0xFF, // 100%
                0xFF, // 100%
        };

        /**
         * A reusable array of control point colors that define the gradient. It is based on the
         * background color of the window and thus recomputed each time that color is changed.
         */
        private final int[] mGradientColors = new int[ALPHAS.length];

        /** The drawable that produces the tinting gradient effect of this decoration. */
        private final GradientDrawable mGradient = new GradientDrawable();

        /** The height of the gradient; sized relative to the fab height. */
        private final int mGradientHeight;

        public GradientItemDecoration(Context context) {
            mGradient.setOrientation(TOP_BOTTOM);
            updateGradientColors(UiDataModel.getUiDataModel().getWindowBackgroundColor());

            final Resources resources = context.getResources();
            final float fabHeight = resources.getDimensionPixelSize(R.dimen.fab_height);
            mGradientHeight = Math.round(fabHeight * 1.2f);
        }

        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);

            final int w = parent.getWidth();
            final int h = parent.getHeight();

            mGradient.setBounds(0, h - mGradientHeight, w, h);
            mGradient.draw(c);
        }

        /**
         * Given a {@code baseColor}, compute a gradient of tinted colors that define the fade
         * effect to apply to the bottom of the lap list.
         *
         * @param baseColor a base color to which the gradient tint should be applied
         */
        public void updateGradientColors(@ColorInt int baseColor) {
            // Compute the tinted colors that form the gradient.
            for (int i = 0; i < mGradientColors.length; i++) {
                mGradientColors[i] = ColorUtils.setAlphaComponent(baseColor, ALPHAS[i]);
            }

            // Set the gradient colors into the drawable.
            mGradient.setColors(mGradientColors);
        }
    }
}