Implemented stopwatch page
This commit is contained in:
parent
3e542f585e
commit
7dfda796f3
@ -53,10 +53,23 @@ public abstract class AsyncDatabaseTableUpdateHandler<
|
||||
}.execute();
|
||||
}
|
||||
|
||||
public final void asyncClear() {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
mTableManager.clear();
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
protected final Context getContext() {
|
||||
return mAppContext;
|
||||
}
|
||||
|
||||
// TODO: Consider giving a base impl that returns our mTableManager field.
|
||||
// Subclasses will check if this base impl is null before creating and returning
|
||||
// a new instance of the TableManager.
|
||||
protected abstract TM getTableManager(Context context);
|
||||
|
||||
protected abstract void onPostAsyncDelete(Integer result, T item);
|
||||
|
||||
@ -2,6 +2,7 @@ package com.philliphsu.clock2;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
@ -16,6 +17,7 @@ import android.widget.TextView;
|
||||
|
||||
import com.philliphsu.clock2.alarms.AlarmsFragment;
|
||||
import com.philliphsu.clock2.settings.SettingsActivity;
|
||||
import com.philliphsu.clock2.stopwatch.StopwatchFragment;
|
||||
import com.philliphsu.clock2.timers.TimersFragment;
|
||||
|
||||
import butterknife.Bind;
|
||||
@ -33,6 +35,13 @@ public class MainActivity extends BaseActivity {
|
||||
*/
|
||||
private SectionsPagerAdapter mSectionsPagerAdapter;
|
||||
|
||||
// For delaying fab.show() on SCROLL_STATE_SETTLING
|
||||
private final Handler mHandler = new Handler();
|
||||
|
||||
private boolean mScrollStateDragging;
|
||||
private int mPageDragging = -1; // TOneverDO: initial value >= 0
|
||||
private boolean mDraggingPastEndBoundaries;
|
||||
|
||||
@Bind(R.id.container)
|
||||
ViewPager mViewPager;
|
||||
|
||||
@ -47,6 +56,76 @@ public class MainActivity extends BaseActivity {
|
||||
// primary sections of the activity.
|
||||
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
|
||||
mViewPager.setAdapter(mSectionsPagerAdapter);
|
||||
mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
if (position == mSectionsPagerAdapter.getCount() - 1) {
|
||||
mFab.hide();
|
||||
} else {
|
||||
mFab.show();
|
||||
}
|
||||
}
|
||||
// @Override
|
||||
// public void onPageScrollStateChanged(int state) {
|
||||
// // TODO: This was not sufficient to prevent the user from quickly
|
||||
// // hitting the fab for the previous page.
|
||||
// switch (state) {
|
||||
// case ViewPager.SCROLL_STATE_DRAGGING:
|
||||
// if (mDraggingPastEndBoundaries) {
|
||||
// return;
|
||||
// }
|
||||
// mScrollStateDragging = true;
|
||||
// mPageDragging = mViewPager.getCurrentItem();
|
||||
// mFab.hide();
|
||||
// break;
|
||||
// case ViewPager.SCROLL_STATE_SETTLING:
|
||||
// if (!mScrollStateDragging) {
|
||||
// mFab.hide();
|
||||
// }
|
||||
// mScrollStateDragging = false;
|
||||
// // getCurrentItem() has changed to the target page we're settling on.
|
||||
// // 200ms is the same as show/hide animation duration
|
||||
// int targetPage = mViewPager.getCurrentItem();
|
||||
// if (targetPage != 2) { // TODO: Use page constant
|
||||
// int delay = mPageDragging == targetPage ? 0 : 200;
|
||||
// mHandler.postDelayed(new Runnable() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// mFab.show();
|
||||
// }
|
||||
// }, delay);
|
||||
// }
|
||||
// mPageDragging = -1;
|
||||
// break;
|
||||
// case ViewPager.SCROLL_STATE_IDLE:
|
||||
// // Nothing
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
});
|
||||
// mViewPager.setPageTransformer(false, new ViewPager.PageTransformer() {
|
||||
// @Override
|
||||
// public void transformPage(View page, float position) {
|
||||
// Log.d(TAG, "position: " + position);
|
||||
// // position represents a page's offset from the front-and-center position of 0 (the page
|
||||
// // that is in full view). Consider pages A, B, C, D.
|
||||
// // If we are now on page A (position 0), then pages B, C, and D are respectively
|
||||
// // in positions 1, 2, 3.
|
||||
// // If we move to the right to page B (now in position 0), then pages A, C, D are
|
||||
// // respectively in positions -1, 1, 2.
|
||||
// int currentPage = mViewPager.getCurrentItem();
|
||||
// // TODO: Use page constants
|
||||
// // Page 0 can't move one full page position to the right (i.e. there is no page to
|
||||
// // the left of page 0 that can adopt the front-and-center position of 0 while page 0
|
||||
// // moves to adopt position 1)
|
||||
// mDraggingPastEndBoundaries = currentPage == 0 && position >= 0f
|
||||
// // The last page can't move one full page position to the left (i.e. there
|
||||
// // is no page to the right of the last page that can adopt the front-and-center
|
||||
// // position of 0 while the last page moves to adopt position -1)
|
||||
// || currentPage == mSectionsPagerAdapter.getCount() - 1 && position <= 0f;
|
||||
// Log.d(TAG, "Draggin past end bounds: " + mDraggingPastEndBoundaries);
|
||||
// }
|
||||
// });
|
||||
|
||||
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
|
||||
tabLayout.setupWithViewPager(mViewPager);
|
||||
@ -62,6 +141,12 @@ public class MainActivity extends BaseActivity {
|
||||
// // Fragment's onActivityResult() will NOT be called.
|
||||
// mSectionsPagerAdapter.getCurrentFragment()
|
||||
// .startActivityForResult(intent, AlarmsFragment.REQUEST_CREATE_ALARM);
|
||||
|
||||
// TODO: If the user switches between pages and is quick enough to hit the
|
||||
// fab before the target page comes fully into view, the onFabClick()
|
||||
// implementation of the previous page will be called. Perhaps do something
|
||||
// with the current page from the ViewPager instead. E.g. monolithically handle
|
||||
// each Fragment's fab click reaction in this activity.
|
||||
Fragment f;
|
||||
if ((f = mSectionsPagerAdapter.getCurrentFragment()) instanceof RecyclerViewFragment) {
|
||||
((RecyclerViewFragment) f).onFabClick();
|
||||
@ -156,6 +241,8 @@ public class MainActivity extends BaseActivity {
|
||||
return AlarmsFragment.newInstance(1);
|
||||
case 1:
|
||||
return new TimersFragment();
|
||||
case 2:
|
||||
return new StopwatchFragment();
|
||||
default:
|
||||
return PlaceholderFragment.newInstance(position + 1);
|
||||
}
|
||||
|
||||
@ -64,12 +64,6 @@ public abstract class RecyclerViewFragment<
|
||||
return new LinearLayoutManager(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
@ -79,6 +73,14 @@ public abstract class RecyclerViewFragment<
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
// http://stackoverflow.com/a/14632434/5055032
|
||||
// A Loader's lifecycle is bound to its Activity, not its Fragment.
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<C> loader, C data) {
|
||||
mAdapter.swapCursor(data);
|
||||
|
||||
@ -64,6 +64,17 @@ public abstract class Timer extends ObjectWithId implements Parcelable {
|
||||
|
||||
public long timeRemaining() {
|
||||
if (!hasStarted())
|
||||
// TODO: Consider returning duration instead? So we can simplify
|
||||
// bindChronometer() in TimerVH to:
|
||||
// if (isRunning())
|
||||
// ...
|
||||
// else
|
||||
// chronom.setDuration(timeRemaining())
|
||||
// ---
|
||||
// Actually, I think we can also simplify it even further to just:
|
||||
// chronom.setDuration(timeRemaining())
|
||||
// if (isRunning)
|
||||
// chronom.start();
|
||||
return 0;
|
||||
return isRunning()
|
||||
? endTime - SystemClock.elapsedRealtime()
|
||||
|
||||
@ -68,7 +68,12 @@ public final class AlarmsTable {
|
||||
|
||||
public static void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL("CREATE TABLE " + TABLE_ALARMS + " ("
|
||||
+ COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
// https://sqlite.org/autoinc.html
|
||||
// If the AUTOINCREMENT keyword appears after INTEGER PRIMARY KEY, that changes the
|
||||
// automatic ROWID assignment algorithm to prevent the reuse of ROWIDs over the
|
||||
// lifetime of the database. In other words, the purpose of AUTOINCREMENT is to
|
||||
// prevent the reuse of ROWIDs from previously deleted rows.
|
||||
+ COLUMN_ID + " INTEGER PRIMARY KEY, "
|
||||
+ COLUMN_HOUR + " INTEGER NOT NULL, "
|
||||
+ COLUMN_MINUTES + " INTEGER NOT NULL, "
|
||||
+ COLUMN_LABEL + " TEXT, "
|
||||
|
||||
@ -4,6 +4,8 @@ import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import com.philliphsu.clock2.stopwatch.LapsTable;
|
||||
|
||||
/**
|
||||
* Created by Phillip Hsu on 7/30/2016.
|
||||
*/
|
||||
@ -31,11 +33,13 @@ public class ClockAppDatabaseHelper extends SQLiteOpenHelper {
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
AlarmsTable.onCreate(db);
|
||||
TimersTable.onCreate(db);
|
||||
LapsTable.onCreate(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
AlarmsTable.onUpgrade(db, oldVersion, newVersion);
|
||||
TimersTable.onUpgrade(db, oldVersion, newVersion);
|
||||
LapsTable.onUpgrade(db, oldVersion, newVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,6 +117,14 @@ public abstract class DatabaseTableManager<T extends ObjectWithId> {
|
||||
limit); // limit
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all rows in this table.
|
||||
*/
|
||||
public final void clear() {
|
||||
mDbHelper.getWritableDatabase().delete(getTableName(), null/*all rows*/, null);
|
||||
notifyContentChanged();
|
||||
}
|
||||
|
||||
private void notifyContentChanged() {
|
||||
LocalBroadcastHelper.sendBroadcast(mAppContext, getOnContentChangeAction());
|
||||
}
|
||||
|
||||
@ -37,7 +37,12 @@ public final class TimersTable {
|
||||
|
||||
public static void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL("CREATE TABLE " + TABLE_TIMERS + " ("
|
||||
+ COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
// https://sqlite.org/autoinc.html
|
||||
// If the AUTOINCREMENT keyword appears after INTEGER PRIMARY KEY, that changes the
|
||||
// automatic ROWID assignment algorithm to prevent the reuse of ROWIDs over the
|
||||
// lifetime of the database. In other words, the purpose of AUTOINCREMENT is to
|
||||
// prevent the reuse of ROWIDs from previously deleted rows.
|
||||
+ COLUMN_ID + " INTEGER PRIMARY KEY, "
|
||||
+ COLUMN_HOUR + " INTEGER NOT NULL, "
|
||||
+ COLUMN_MINUTE + " INTEGER NOT NULL, "
|
||||
+ COLUMN_SECOND + " INTEGER NOT NULL, "
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
package com.philliphsu.clock2.stopwatch;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.philliphsu.clock2.AsyncDatabaseTableUpdateHandler;
|
||||
import com.philliphsu.clock2.alarms.ScrollHandler;
|
||||
|
||||
/**
|
||||
* Created by Phillip Hsu on 8/9/2016.
|
||||
*/
|
||||
public class AsyncLapsTableUpdateHandler extends AsyncDatabaseTableUpdateHandler<Lap, LapsTableManager> {
|
||||
|
||||
public AsyncLapsTableUpdateHandler(Context context, ScrollHandler scrollHandler) {
|
||||
super(context, scrollHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LapsTableManager getTableManager(Context context) {
|
||||
return new LapsTableManager(context);
|
||||
}
|
||||
|
||||
// ===================== DO NOT IMPLEMENT =========================
|
||||
|
||||
@Override
|
||||
protected void onPostAsyncDelete(Integer result, Lap item) {
|
||||
// Leave blank.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostAsyncInsert(Long result, Lap item) {
|
||||
// Leave blank.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostAsyncUpdate(Long result, Lap item) {
|
||||
// Leave blank.
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,352 @@
|
||||
package com.philliphsu.clock2.stopwatch;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.SystemClock;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.Formatter;
|
||||
import java.util.IllegalFormatException;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Created by Phillip Hsu on 8/9/2016.
|
||||
*
|
||||
* A modified version of the framework's Chronometer widget that shows
|
||||
* up to hundredths of a second.
|
||||
*/
|
||||
public class ChronometerWithMillis extends TextView {
|
||||
private static final String TAG = "ChronometerWithMillis";
|
||||
|
||||
/**
|
||||
* A callback that notifies when the chronometer has incremented on its own.
|
||||
*/
|
||||
public interface OnChronometerTickListener {
|
||||
|
||||
/**
|
||||
* Notification that the chronometer has changed.
|
||||
*/
|
||||
void onChronometerTick(ChronometerWithMillis chronometer);
|
||||
|
||||
}
|
||||
|
||||
private long mBase;
|
||||
private long mNow; // the currently displayed time
|
||||
private boolean mVisible;
|
||||
private boolean mStarted;
|
||||
private boolean mRunning;
|
||||
private boolean mLogged;
|
||||
private String mFormat;
|
||||
private Formatter mFormatter;
|
||||
private Locale mFormatterLocale;
|
||||
private Object[] mFormatterArgs = new Object[1];
|
||||
private StringBuilder mFormatBuilder;
|
||||
private OnChronometerTickListener mOnChronometerTickListener;
|
||||
private StringBuilder mRecycle = new StringBuilder(8);
|
||||
|
||||
private static final int TICK_WHAT = 2;
|
||||
|
||||
/**
|
||||
* Initialize this Chronometer object.
|
||||
* Sets the base to the current time.
|
||||
*/
|
||||
public ChronometerWithMillis(Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize with standard view layout information.
|
||||
* Sets the base to the current time.
|
||||
*/
|
||||
public ChronometerWithMillis(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize with standard view layout information and style.
|
||||
* Sets the base to the current time.
|
||||
*/
|
||||
public ChronometerWithMillis(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
// final TypedArray a = context.obtainStyledAttributes(
|
||||
// attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, 0);
|
||||
// setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format));
|
||||
// a.recycle();
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
public ChronometerWithMillis(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
|
||||
// final TypedArray a = context.obtainStyledAttributes(
|
||||
// attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes);
|
||||
// setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format));
|
||||
// a.recycle();
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
mBase = SystemClock.elapsedRealtime();
|
||||
updateText(mBase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time that the count-up timer is in reference to.
|
||||
*
|
||||
* @param base Use the {@link SystemClock#elapsedRealtime} time base.
|
||||
*/
|
||||
// @android.view.RemotableViewMethod
|
||||
public void setBase(long base) {
|
||||
mBase = base;
|
||||
dispatchChronometerTick();
|
||||
updateText(SystemClock.elapsedRealtime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the base time as set through {@link #setBase}.
|
||||
*/
|
||||
public long getBase() {
|
||||
return mBase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to {@link #setBase(long) setBase(SystemClock.elapsedRealtime() - elapsed)}.
|
||||
*/
|
||||
public void setElapsed(long elapsed) {
|
||||
setBase(SystemClock.elapsedRealtime() - elapsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the format string used for display. The Chronometer will display
|
||||
* this string, with the first "%s" replaced by the current timer value in
|
||||
* "MM:SS" or "H:MM:SS" form.
|
||||
*
|
||||
* If the format string is null, or if you never call setFormat(), the
|
||||
* Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS"
|
||||
* form.
|
||||
*
|
||||
* @param format the format string.
|
||||
*/
|
||||
// @android.view.RemotableViewMethod
|
||||
public void setFormat(String format) {
|
||||
mFormat = format;
|
||||
if (format != null && mFormatBuilder == null) {
|
||||
mFormatBuilder = new StringBuilder(format.length() * 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current format string as set through {@link #setFormat}.
|
||||
*/
|
||||
public String getFormat() {
|
||||
return mFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener to be called when the chronometer changes.
|
||||
*
|
||||
* @param listener The listener.
|
||||
*/
|
||||
public void setOnChronometerTickListener(OnChronometerTickListener listener) {
|
||||
mOnChronometerTickListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The listener (may be null) that is listening for chronometer change
|
||||
* events.
|
||||
*/
|
||||
public OnChronometerTickListener getOnChronometerTickListener() {
|
||||
return mOnChronometerTickListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start counting up. This does not affect the base as set from {@link #setBase}, just
|
||||
* the view display.
|
||||
*
|
||||
* Chronometer works by regularly scheduling messages to the handler, even when the
|
||||
* Widget is not visible. To make sure resource leaks do not occur, the user should
|
||||
* make sure that each start() call has a reciprocal call to {@link #stop}.
|
||||
*/
|
||||
public void start() {
|
||||
mStarted = true;
|
||||
updateRunning();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop counting up. This does not affect the base as set from {@link #setBase}, just
|
||||
* the view display.
|
||||
*
|
||||
* This stops the messages to the handler, effectively releasing resources that would
|
||||
* be held as the chronometer is running, via {@link #start}.
|
||||
*/
|
||||
public void stop() {
|
||||
mStarted = false;
|
||||
updateRunning();
|
||||
}
|
||||
|
||||
/**
|
||||
* The same as calling {@link #start} or {@link #stop}.
|
||||
* @hide pending API council approval
|
||||
*/
|
||||
// @android.view.RemotableViewMethod
|
||||
public void setStarted(boolean started) {
|
||||
mStarted = started;
|
||||
updateRunning();
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return mRunning;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
mVisible = false;
|
||||
updateRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onWindowVisibilityChanged(int visibility) {
|
||||
Log.d(TAG, "onWindowVisibilityChanged()");
|
||||
super.onWindowVisibilityChanged(visibility);
|
||||
mVisible = visibility == VISIBLE;
|
||||
updateRunning();
|
||||
}
|
||||
|
||||
private synchronized void updateText(long now) {
|
||||
mNow = now;
|
||||
long millis = now - mBase;
|
||||
String text = DateUtils.formatElapsedTime(mRecycle, millis / 1000/*needs to be in seconds*/);
|
||||
|
||||
Locale loc = Locale.getDefault();
|
||||
if (mFormat != null) {
|
||||
if (mFormatter == null || !loc.equals(mFormatterLocale)) {
|
||||
mFormatterLocale = loc;
|
||||
mFormatter = new Formatter(mFormatBuilder, loc);
|
||||
}
|
||||
mFormatBuilder.setLength(0);
|
||||
mFormatterArgs[0] = text;
|
||||
try {
|
||||
mFormatter.format(mFormat, mFormatterArgs);
|
||||
text = mFormatBuilder.toString();
|
||||
} catch (IllegalFormatException ex) {
|
||||
if (!mLogged) {
|
||||
Log.w(TAG, "Illegal format string: " + mFormat);
|
||||
mLogged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
long centiseconds = (millis % 1000) / 10;
|
||||
String centisecondsText = String.format(loc,
|
||||
// TODO: Different locales use different decimal marks.
|
||||
// The two most common are . and ,
|
||||
// Consider removing the . and just let the size span
|
||||
// represent this as fractional seconds?
|
||||
// ...or figure out how to get the correct mark for the
|
||||
// current locale.
|
||||
// It looks like Google's Clock app strictly uses .
|
||||
".%02d", // The . before % is not a format specifier
|
||||
centiseconds);
|
||||
setText(text.concat(centisecondsText));
|
||||
}
|
||||
|
||||
private void updateRunning() {
|
||||
boolean running = mVisible && mStarted;
|
||||
if (running != mRunning) {
|
||||
if (running) {
|
||||
Log.d(TAG, "Running");
|
||||
updateText(SystemClock.elapsedRealtime());
|
||||
dispatchChronometerTick();
|
||||
mHandler.sendMessageDelayed(Message.obtain(mHandler, TICK_WHAT), 10);
|
||||
} else {
|
||||
Log.d(TAG, "Not running anymore");
|
||||
mHandler.removeMessages(TICK_WHAT);
|
||||
}
|
||||
mRunning = running;
|
||||
}
|
||||
}
|
||||
|
||||
private Handler mHandler = new Handler() {
|
||||
public void handleMessage(Message m) {
|
||||
if (mRunning) {
|
||||
updateText(SystemClock.elapsedRealtime());
|
||||
dispatchChronometerTick();
|
||||
sendMessageDelayed(Message.obtain(this, TICK_WHAT), 10);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void dispatchChronometerTick() {
|
||||
if (mOnChronometerTickListener != null) {
|
||||
mOnChronometerTickListener.onChronometerTick(this);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int MIN_IN_SEC = 60;
|
||||
private static final int HOUR_IN_SEC = MIN_IN_SEC*60;
|
||||
// private static String formatDuration(long ms) {
|
||||
// final Resources res = Resources.getSystem();
|
||||
// final StringBuilder text = new StringBuilder();
|
||||
//
|
||||
// int duration = (int) (ms / DateUtils.SECOND_IN_MILLIS);
|
||||
// if (duration < 0) {
|
||||
// duration = -duration;
|
||||
// }
|
||||
//
|
||||
// int h = 0;
|
||||
// int m = 0;
|
||||
//
|
||||
// if (duration >= HOUR_IN_SEC) {
|
||||
// h = duration / HOUR_IN_SEC;
|
||||
// duration -= h * HOUR_IN_SEC;
|
||||
// }
|
||||
// if (duration >= MIN_IN_SEC) {
|
||||
// m = duration / MIN_IN_SEC;
|
||||
// duration -= m * MIN_IN_SEC;
|
||||
// }
|
||||
// int s = duration;
|
||||
//
|
||||
// try {
|
||||
// if (h > 0) {
|
||||
// text.append(res.getQuantityString(
|
||||
// com.android.internal.R.plurals.duration_hours, h, h));
|
||||
// }
|
||||
// if (m > 0) {
|
||||
// if (text.length() > 0) {
|
||||
// text.append(' ');
|
||||
// }
|
||||
// text.append(res.getQuantityString(
|
||||
// com.android.internal.R.plurals.duration_minutes, m, m));
|
||||
// }
|
||||
//
|
||||
// if (text.length() > 0) {
|
||||
// text.append(' ');
|
||||
// }
|
||||
// text.append(res.getQuantityString(
|
||||
// com.android.internal.R.plurals.duration_seconds, s, s));
|
||||
// } catch (Resources.NotFoundException e) {
|
||||
// // Ignore; plurals throws an exception for an untranslated quantity for a given locale.
|
||||
// return null;
|
||||
// }
|
||||
// return text.toString();
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public CharSequence getContentDescription() {
|
||||
// return formatDuration(mNow - mBase);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public CharSequence getAccessibilityClassName() {
|
||||
// return ChronometerWithMillis.class.getName();
|
||||
// }
|
||||
}
|
||||
128
app/src/main/java/com/philliphsu/clock2/stopwatch/Lap.java
Normal file
128
app/src/main/java/com/philliphsu/clock2/stopwatch/Lap.java
Normal file
@ -0,0 +1,128 @@
|
||||
package com.philliphsu.clock2.stopwatch;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.philliphsu.clock2.model.ObjectWithId;
|
||||
|
||||
/**
|
||||
* Created by Phillip Hsu on 8/8/2016.
|
||||
*/
|
||||
public class Lap extends ObjectWithId {
|
||||
|
||||
private long t1;
|
||||
private long t2;
|
||||
private long pauseTime;
|
||||
private String totalTimeText;
|
||||
|
||||
public Lap() {
|
||||
// TOneverDO: nanos because chronometer expects times in the elapsedRealtime base
|
||||
t1 = SystemClock.elapsedRealtime();
|
||||
}
|
||||
|
||||
public long t1() {
|
||||
return t1;
|
||||
}
|
||||
|
||||
public long t2() {
|
||||
return t2;
|
||||
}
|
||||
|
||||
public long pauseTime() {
|
||||
return pauseTime;
|
||||
}
|
||||
|
||||
public String totalTimeText() {
|
||||
return totalTimeText;
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
if (isEnded())
|
||||
throw new IllegalStateException("Cannot pause a Lap that has already ended");
|
||||
if (!isRunning())
|
||||
throw new IllegalStateException("Cannot pause a Lap that is already paused");
|
||||
pauseTime = SystemClock.elapsedRealtime();
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
if (isEnded())
|
||||
throw new IllegalStateException("Cannot resume a Lap that has already ended");
|
||||
if (isRunning())
|
||||
throw new IllegalStateException("Cannot resume a Lap that is not paused");
|
||||
t1 += SystemClock.elapsedRealtime() - pauseTime;
|
||||
pauseTime = 0;
|
||||
}
|
||||
|
||||
public void end(String totalTime) {
|
||||
if (isEnded())
|
||||
throw new IllegalStateException("Cannot end a Lap that has already ended");
|
||||
t2 = SystemClock.elapsedRealtime();
|
||||
totalTimeText = totalTime;
|
||||
pauseTime = 0;
|
||||
}
|
||||
|
||||
public long elapsed() {
|
||||
if (isRunning())
|
||||
return SystemClock.elapsedRealtime() - t1;
|
||||
else if (isEnded())
|
||||
return t2 - t1;
|
||||
else return pauseTime - t1;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return !isEnded() && pauseTime == 0;
|
||||
}
|
||||
|
||||
public boolean isEnded() {
|
||||
return t2 > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Lap lap = (Lap) o;
|
||||
|
||||
if (t1 != lap.t1) return false;
|
||||
if (t2 != lap.t2) return false;
|
||||
if (pauseTime != lap.pauseTime) return false;
|
||||
return totalTimeText != null ? totalTimeText.equals(lap.totalTimeText) : lap.totalTimeText == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = (int) (t1 ^ (t1 >>> 32));
|
||||
result = 31 * result + (int) (t2 ^ (t2 >>> 32));
|
||||
result = 31 * result + (int) (pauseTime ^ (pauseTime >>> 32));
|
||||
result = 31 * result + (totalTimeText != null ? totalTimeText.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Lap{" +
|
||||
"t1=" + t1 +
|
||||
", t2=" + t2 +
|
||||
", pauseTime=" + pauseTime +
|
||||
", totalTimeText='" + totalTimeText + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
// ================ TO ONLY BE CALLED BY LapsTableManager ================
|
||||
|
||||
public void setT1(long t1) {
|
||||
this.t1 = t1;
|
||||
}
|
||||
|
||||
public void setT2(long t2) {
|
||||
this.t2 = t2;
|
||||
}
|
||||
|
||||
public void setPauseTime(long pauseTime) {
|
||||
this.pauseTime = pauseTime;
|
||||
}
|
||||
|
||||
public void setTotalTimeText(String totalTimeText) {
|
||||
this.totalTimeText = totalTimeText;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.philliphsu.clock2.stopwatch;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.philliphsu.clock2.model.BaseItemCursor;
|
||||
|
||||
/**
|
||||
* Created by Phillip Hsu on 8/8/2016.
|
||||
*/
|
||||
public class LapCursor extends BaseItemCursor<Lap> {
|
||||
|
||||
public LapCursor(Cursor cursor) {
|
||||
super(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Lap getItem() {
|
||||
Lap lap = new Lap();
|
||||
lap.setId(getLong(getColumnIndexOrThrow(LapsTable.COLUMN_ID)));
|
||||
lap.setT1(getLong(getColumnIndexOrThrow(LapsTable.COLUMN_T1)));
|
||||
lap.setT2(getLong(getColumnIndexOrThrow(LapsTable.COLUMN_T2)));
|
||||
lap.setPauseTime(getLong(getColumnIndexOrThrow(LapsTable.COLUMN_PAUSE_TIME)));
|
||||
lap.setTotalTimeText(getString(getColumnIndexOrThrow(LapsTable.COLUMN_TOTAL_TIME_TEXT)));
|
||||
return lap;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package com.philliphsu.clock2.stopwatch;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.philliphsu.clock2.BaseViewHolder;
|
||||
import com.philliphsu.clock2.R;
|
||||
|
||||
import butterknife.Bind;
|
||||
|
||||
/**
|
||||
* Created by Phillip Hsu on 8/8/2016.
|
||||
*/
|
||||
public class LapViewHolder extends BaseViewHolder<Lap> {
|
||||
|
||||
@Bind(R.id.lap_number) TextView mLapNumber;
|
||||
@Bind(R.id.elapsed_time) ChronometerWithMillis mElapsedTime;
|
||||
@Bind(R.id.total_time) TextView mTotalTime;
|
||||
|
||||
public LapViewHolder(ViewGroup parent) {
|
||||
super(parent, R.layout.item_lap, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(Lap lap) {
|
||||
super.onBind(lap);
|
||||
if (getItemViewType() == LapsAdapter.VIEW_TYPE_FIRST_LAP) {
|
||||
itemView.setVisibility(View.GONE);
|
||||
} else {
|
||||
mLapNumber.setText(String.valueOf(lap.getId()));
|
||||
bindElapsedTime(lap);
|
||||
bindTotalTime(lap);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindElapsedTime(Lap lap) {
|
||||
// In case we're reusing a chronometer instance that could be running:
|
||||
// If the lap is not running, this just guarantees the chronometer
|
||||
// won't tick, regardless of whether it was running.
|
||||
// If the lap is running, we don't care whether the chronometer is
|
||||
// also running, because we call start() right after. Stopping it just
|
||||
// guarantees that, if it was running, we don't deliver another set of
|
||||
// concurrent messages to its handler.
|
||||
mElapsedTime.stop();
|
||||
// We're going to forget about the + sign in front of the text. I think
|
||||
// the 'Elapsed' header column is sufficient to convey what this timer means.
|
||||
// (Don't want to figure out a solution)
|
||||
mElapsedTime.setElapsed(lap.elapsed());
|
||||
if (lap.isRunning()) {
|
||||
mElapsedTime.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void bindTotalTime(Lap lap) {
|
||||
if (lap.isEnded()) {
|
||||
mTotalTime.setVisibility(View.VISIBLE);
|
||||
mTotalTime.setText(lap.totalTimeText());
|
||||
} else {
|
||||
mTotalTime.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.philliphsu.clock2.stopwatch;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.philliphsu.clock2.BaseCursorAdapter;
|
||||
import com.philliphsu.clock2.OnListItemInteractionListener;
|
||||
|
||||
/**
|
||||
* Created by Phillip Hsu on 8/9/2016.
|
||||
*/
|
||||
public class LapsAdapter extends BaseCursorAdapter<Lap, LapViewHolder, LapCursor> {
|
||||
public static final int VIEW_TYPE_FIRST_LAP = 1; // TOneverDO: 0, that's the default view type
|
||||
|
||||
public LapsAdapter() {
|
||||
super(null/*OnListItemInteractionListener*/);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LapViewHolder onCreateViewHolder(ViewGroup parent, OnListItemInteractionListener<Lap> listener, int viewType) {
|
||||
// TODO: Consider defining a view type for the lone first lap. We should persist this first lap
|
||||
// when is is created, but this view type will tell us it should not be visible until there
|
||||
// are at least two laps.
|
||||
// Or could we return null? Probably not because that will get passed into onBindVH, unless
|
||||
// you check for null before accessing it.
|
||||
// RecyclerView.ViewHolder has an internal field that holds viewType for us,
|
||||
// and can be retrieved by the instance via getItemViewType().
|
||||
return new LapViewHolder(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return getItemCount() == 1 ? VIEW_TYPE_FIRST_LAP : super.getItemViewType(position);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.philliphsu.clock2.stopwatch;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.philliphsu.clock2.model.SQLiteCursorLoader;
|
||||
|
||||
/**
|
||||
* Created by Phillip Hsu on 8/9/2016.
|
||||
*/
|
||||
public class LapsCursorLoader extends SQLiteCursorLoader<Lap, LapCursor> {
|
||||
public static final String ACTION_CHANGE_CONTENT
|
||||
// TODO: Correct package prefix
|
||||
= "com.philliphsu.clock2.model.LapsCursorLoader.action.CHANGE_CONTENT";
|
||||
|
||||
public LapsCursorLoader(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LapCursor loadCursor() {
|
||||
return new LapsTableManager(getContext()).queryItems();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getOnContentChangeAction() {
|
||||
return ACTION_CHANGE_CONTENT;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.philliphsu.clock2.stopwatch;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
/**
|
||||
* Created by Phillip Hsu on 8/8/2016.
|
||||
*/
|
||||
public final class LapsTable {
|
||||
private LapsTable() {}
|
||||
|
||||
public static final String TABLE_LAPS = "laps";
|
||||
|
||||
public static final String COLUMN_ID = "_id";
|
||||
public static final String COLUMN_T1 = "elapsed";
|
||||
public static final String COLUMN_T2 = "total";
|
||||
public static final String COLUMN_PAUSE_TIME = "pause_time";
|
||||
public static final String COLUMN_TOTAL_TIME_TEXT = "total_time_text";
|
||||
|
||||
public static final String SORT_ORDER = COLUMN_ID + " DESC";
|
||||
|
||||
public static void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL("CREATE TABLE " + TABLE_LAPS + " ("
|
||||
// https://sqlite.org/autoinc.html
|
||||
// If the AUTOINCREMENT keyword appears after INTEGER PRIMARY KEY, that changes the
|
||||
// automatic ROWID assignment algorithm to prevent the reuse of ROWIDs over the
|
||||
// lifetime of the database. In other words, the purpose of AUTOINCREMENT is to
|
||||
// prevent the reuse of ROWIDs from previously deleted rows.
|
||||
// If we ever clear the laps of a stopwatch timing, we want the next timing's laps
|
||||
// to begin again from id = 1. Using AUTOINCREMENT prevents that from happening;
|
||||
// the laps will begin from the last ID used plus one.
|
||||
+ COLUMN_ID + " INTEGER PRIMARY KEY, "
|
||||
+ COLUMN_T1 + " INTEGER NOT NULL, "
|
||||
+ COLUMN_T2 + " INTEGER NOT NULL, "
|
||||
+ COLUMN_PAUSE_TIME + " INTEGER NOT NULL, "
|
||||
+ COLUMN_TOTAL_TIME_TEXT + " TEXT);");
|
||||
}
|
||||
|
||||
public static void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_LAPS);
|
||||
onCreate(db);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
package com.philliphsu.clock2.stopwatch;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.philliphsu.clock2.model.DatabaseTableManager;
|
||||
|
||||
/**
|
||||
* Created by Phillip Hsu on 8/8/2016.
|
||||
*/
|
||||
public class LapsTableManager extends DatabaseTableManager<Lap> {
|
||||
|
||||
public LapsTableManager(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getQuerySortOrder() {
|
||||
return LapsTable.SORT_ORDER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LapCursor queryItem(long id) {
|
||||
return wrapInLapCursor(super.queryItem(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LapCursor queryItems() {
|
||||
return wrapInLapCursor(super.queryItems());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LapCursor queryItems(String where, String limit) {
|
||||
return wrapInLapCursor(super.queryItems(where, limit));
|
||||
}
|
||||
|
||||
// public LapCursor queryTwoMostRecentLaps(long currentLapId, long previousLapId) {
|
||||
// String where = LapsTable.COLUMN_ID + " = " + currentLapId
|
||||
// + " OR " + LapsTable.COLUMN_ID + " = " + previousLapId;
|
||||
// LapCursor cursor = queryItems(where, "2");
|
||||
// cursor.moveToFirst();
|
||||
// return cursor;
|
||||
// }
|
||||
|
||||
@Override
|
||||
protected String getTableName() {
|
||||
return LapsTable.TABLE_LAPS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ContentValues toContentValues(Lap lap) {
|
||||
ContentValues cv = new ContentValues();
|
||||
// cv.put(LapsTable.COLUMN_ID, lap.getId()); // NEVER SET THE ID, BECAUSE THE DATABASE SETS AN AUTOINCREMENTING PRIMARY KEY FOR YOU!
|
||||
cv.put(LapsTable.COLUMN_T1, lap.t1());
|
||||
cv.put(LapsTable.COLUMN_T2, lap.t2());
|
||||
cv.put(LapsTable.COLUMN_PAUSE_TIME, lap.pauseTime());
|
||||
cv.put(LapsTable.COLUMN_TOTAL_TIME_TEXT, lap.totalTimeText());
|
||||
return cv;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getOnContentChangeAction() {
|
||||
return LapsCursorLoader.ACTION_CHANGE_CONTENT;
|
||||
}
|
||||
|
||||
private LapCursor wrapInLapCursor(Cursor c) {
|
||||
return new LapCursor(c);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,300 @@
|
||||
package com.philliphsu.clock2.stopwatch;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.SystemClock;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import com.philliphsu.clock2.R;
|
||||
import com.philliphsu.clock2.RecyclerViewFragment;
|
||||
import com.philliphsu.clock2.util.ProgressBarUtils;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.OnClick;
|
||||
|
||||
/**
|
||||
* Created by Phillip Hsu on 8/8/2016.
|
||||
*/
|
||||
public class StopwatchFragment extends RecyclerViewFragment<
|
||||
Lap,
|
||||
LapViewHolder,
|
||||
LapCursor,
|
||||
LapsAdapter> {
|
||||
private static final String TAG = "StopwatchFragment";
|
||||
private static final String KEY_START_TIME = "start_time";
|
||||
private static final String KEY_PAUSE_TIME = "pause_time";
|
||||
private static final String KEY_CHRONOMETER_RUNNING = "chronometer_running";
|
||||
|
||||
private long mStartTime;
|
||||
private long mPauseTime;
|
||||
private Lap mCurrentLap;
|
||||
private Lap mPreviousLap;
|
||||
|
||||
private AsyncLapsTableUpdateHandler mUpdateHandler;
|
||||
private ObjectAnimator mProgressAnimator;
|
||||
private SharedPreferences mPrefs;
|
||||
|
||||
@Bind(R.id.chronometer) ChronometerWithMillis mChronometer;
|
||||
@Bind(R.id.new_lap) ImageButton mNewLapButton;
|
||||
@Bind(R.id.fab) FloatingActionButton mFab;
|
||||
@Bind(R.id.stop) ImageButton mStopButton;
|
||||
@Bind(R.id.progress_bar) ProgressBar mProgressBar;
|
||||
|
||||
/**
|
||||
* This is called only when a new instance of this Fragment is being created,
|
||||
* especially if the user is navigating to this tab for the first time in
|
||||
* this app session.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mUpdateHandler = new AsyncLapsTableUpdateHandler(getActivity(), null/*we shouldn't need a scroll handler*/);
|
||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
mStartTime = mPrefs.getLong(KEY_START_TIME, 0);
|
||||
mPauseTime = mPrefs.getLong(KEY_PAUSE_TIME, 0);
|
||||
Log.d(TAG, "mStartTime = " + mStartTime
|
||||
+ ", mPauseTime = " + mPauseTime);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
// TODO: Apply size span on chronom
|
||||
View view = super.onCreateView(inflater, container, savedInstanceState);
|
||||
if (mStartTime > 0) {
|
||||
long base = mStartTime;
|
||||
if (mPauseTime > 0) {
|
||||
base += SystemClock.elapsedRealtime() - mPauseTime;
|
||||
// We're not done pausing yet, so don't reset mPauseTime.
|
||||
}
|
||||
mChronometer.setBase(base);
|
||||
}
|
||||
if (mPrefs.getBoolean(KEY_CHRONOMETER_RUNNING, false)) {
|
||||
mChronometer.start();
|
||||
}
|
||||
updateButtonControls();
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user navigates away, this is the furthest point in the lifecycle
|
||||
* this Fragment gets to. Here, the view hierarchy returned from onCreateView()
|
||||
* is destroyed--the Fragment itself is NOT destroyed. If the user navigates back
|
||||
* to this tab, this Fragment goes through its lifecycle beginning from onCreateView().
|
||||
*
|
||||
* TODO: Verify that members are not reset.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
Log.d(TAG, "onDestroyView()");
|
||||
Log.d(TAG, "mStartTime = " + mStartTime
|
||||
+ ", mPauseTime = " + mPauseTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<LapCursor> onCreateLoader(int id, Bundle args) {
|
||||
return new LapsCursorLoader(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<LapCursor> loader, LapCursor data) {
|
||||
super.onLoadFinished(loader, data);
|
||||
// TODO: Will manipulating the cursor's position here affect the current
|
||||
// position in the adapter? Should we make a defensive copy and manipulate
|
||||
// that copy instead?
|
||||
if (data.moveToFirst()) {
|
||||
mCurrentLap = data.getItem();
|
||||
Log.d(TAG, "Current lap ID = " + mCurrentLap.getId());
|
||||
}
|
||||
if (data.moveToNext()) {
|
||||
mPreviousLap = data.getItem();
|
||||
Log.d(TAG, "Previous lap ID = " + mPreviousLap.getId());
|
||||
}
|
||||
if (mChronometer.isRunning() && mCurrentLap != null && mPreviousLap != null) {
|
||||
// We really only want to start a new animator when the NEWLY RETRIEVED current
|
||||
// and previous laps are different (i.e. different laps, NOT merely different instances)
|
||||
// from the CURRENT current and previous laps, as referenced by mCurrentLap and mPreviousLap.
|
||||
// However, both equals() and == are insufficient. Our cursor's getItem() will always
|
||||
// create new instances of Lap representing the underlying data, so an '== test' will
|
||||
// always fail to convey our intention. Also, equals() would fail especially when the
|
||||
// physical lap is paused/resumed, because the two instances in comparison
|
||||
// (the retrieved and current) would obviously
|
||||
// have different values for, e.g., t1 and pauseTime.
|
||||
//
|
||||
// Therefore, we'll just always end the previous animator and start a new one.
|
||||
// TODO: We may as well move the contents of this method here, since we're the only caller.
|
||||
startNewProgressBarAnimator();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected LapsAdapter getAdapter() {
|
||||
if (super.getAdapter() != null)
|
||||
return super.getAdapter();
|
||||
return new LapsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int contentLayout() {
|
||||
return R.layout.fragment_stopwatch;
|
||||
}
|
||||
|
||||
@OnClick(R.id.new_lap)
|
||||
void addNewLap() {
|
||||
if (!mChronometer.isRunning()) {
|
||||
Log.d(TAG, "Cannot add new lap");
|
||||
return;
|
||||
}
|
||||
if (mCurrentLap != null) {
|
||||
mCurrentLap.end(mChronometer.getText().toString());
|
||||
}
|
||||
mPreviousLap = mCurrentLap;
|
||||
mCurrentLap = new Lap();
|
||||
if (mPreviousLap != null) {
|
||||
// if (getAdapter().getItemCount() == 0) {
|
||||
// mUpdateHandler.asyncInsert(mPreviousLap);
|
||||
// } else {
|
||||
mUpdateHandler.asyncUpdate(mPreviousLap.getId(), mPreviousLap);
|
||||
// }
|
||||
}
|
||||
mUpdateHandler.asyncInsert(mCurrentLap);
|
||||
// This would end up being called twice: here, and in onLoadFinished(), because the
|
||||
// table updates will prompt us to requery.
|
||||
// startNewProgressBarAnimator();
|
||||
}
|
||||
|
||||
@OnClick(R.id.fab)
|
||||
void startPause() {
|
||||
if (mChronometer.isRunning()) {
|
||||
mPauseTime = SystemClock.elapsedRealtime();
|
||||
mChronometer.stop();
|
||||
mCurrentLap.pause();
|
||||
mUpdateHandler.asyncUpdate(mCurrentLap.getId(), mCurrentLap);
|
||||
// No issues controlling the animator here, because onLoadFinished() can't
|
||||
// call through to startNewProgressBarAnimator(), because by that point
|
||||
// the chronometer won't be running.
|
||||
if (mProgressAnimator != null) {
|
||||
// We may as well call cancel(), since our resume() call would be
|
||||
// rendered meaningless.
|
||||
// mProgressAnimator.pause();
|
||||
mProgressAnimator.cancel();
|
||||
}
|
||||
} else {
|
||||
if (mStartTime == 0) {
|
||||
// TODO: I'm strongly considering inserting the very first lap alone.
|
||||
// We'll need to tell the adapter to just hide the corresponding VH
|
||||
// until a second lap is added.
|
||||
// addNewLap() won't call through unless chronometer is running, which
|
||||
// we can't start until we compute mStartTime
|
||||
mCurrentLap = new Lap();
|
||||
mUpdateHandler.asyncInsert(mCurrentLap);
|
||||
}
|
||||
mStartTime += SystemClock.elapsedRealtime() - mPauseTime;
|
||||
mPauseTime = 0;
|
||||
mChronometer.setBase(mStartTime);
|
||||
mChronometer.start();
|
||||
if (!mCurrentLap.isRunning()) {
|
||||
mCurrentLap.resume();
|
||||
mUpdateHandler.asyncUpdate(mCurrentLap.getId(), mCurrentLap);
|
||||
}
|
||||
// This animator instance will end up having end() called on it. When
|
||||
// the table update prompts us to requery, onLoadFinished will be called as a result.
|
||||
// There, it calls startNewProgressAnimator() to end this animation and starts an
|
||||
// entirely new animator instance.
|
||||
// if (mProgressAnimator != null) {
|
||||
// mProgressAnimator.resume();
|
||||
// }
|
||||
}
|
||||
updateButtonControls();
|
||||
savePrefs();
|
||||
}
|
||||
|
||||
@OnClick(R.id.stop)
|
||||
void stop() {
|
||||
mChronometer.stop();
|
||||
mChronometer.setBase(SystemClock.elapsedRealtime());
|
||||
mStartTime = 0;
|
||||
mPauseTime = 0;
|
||||
mCurrentLap = null;
|
||||
mPreviousLap = null;
|
||||
mUpdateHandler.asyncClear(); // Clear laps
|
||||
// No issues controlling the animator here, because onLoadFinished() can't
|
||||
// call through to startNewProgressBarAnimator(), because by that point
|
||||
// the chronometer won't be running.
|
||||
if (mProgressAnimator != null) {
|
||||
mProgressAnimator.end();
|
||||
}
|
||||
mProgressAnimator = null;
|
||||
updateButtonControls();
|
||||
savePrefs();
|
||||
}
|
||||
|
||||
private void updateButtonControls() {
|
||||
boolean started = mStartTime > 0;
|
||||
int vis = started ? View.VISIBLE : View.INVISIBLE;
|
||||
mNewLapButton.setVisibility(vis);
|
||||
mStopButton.setVisibility(vis);
|
||||
// TODO: pause and start icon, resp.
|
||||
mFab.setImageResource(mChronometer.isRunning() ? 0 : 0);
|
||||
}
|
||||
|
||||
private void startNewProgressBarAnimator() {
|
||||
if (mProgressAnimator != null) {
|
||||
mProgressAnimator.end();
|
||||
}
|
||||
long timeRemaining = mPreviousLap.elapsed() - mCurrentLap.elapsed();
|
||||
if (timeRemaining <= 0)
|
||||
return;
|
||||
// The cast is necessary, or else we'd have integer division between two longs and we'd
|
||||
// always get zero since the numerator will always be less than the denominator.
|
||||
double ratioTimeRemaining = timeRemaining / (double) mPreviousLap.elapsed();
|
||||
mProgressAnimator = ProgressBarUtils.startNewAnimator(
|
||||
mProgressBar, ratioTimeRemaining, timeRemaining);
|
||||
}
|
||||
|
||||
private void savePrefs() {
|
||||
mPrefs.edit().putLong(KEY_START_TIME, mStartTime)
|
||||
.putLong(KEY_PAUSE_TIME, mPauseTime)
|
||||
.putBoolean(KEY_CHRONOMETER_RUNNING, mChronometer.isRunning())
|
||||
.apply();
|
||||
}
|
||||
|
||||
// ======================= DO NOT IMPLEMENT ============================
|
||||
|
||||
@Override
|
||||
public void onFabClick() {
|
||||
// DO NOT THROW AN UNSUPPORTED OPERATION EXCEPTION.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onScrolledToStableId(long id, int position) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemClick(Lap item, int position) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemDeleted(Lap item) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemUpdate(Lap item, int position) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,7 @@ import com.philliphsu.clock2.BaseViewHolder;
|
||||
import com.philliphsu.clock2.OnListItemInteractionListener;
|
||||
import com.philliphsu.clock2.R;
|
||||
import com.philliphsu.clock2.Timer;
|
||||
import com.philliphsu.clock2.util.ProgressBarUtils;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.OnClick;
|
||||
@ -85,6 +86,10 @@ public class TimerViewHolder extends BaseViewHolder<Timer> {
|
||||
// concurrent messages to its handler.
|
||||
mChronometer.stop();
|
||||
|
||||
// TODO: I think we can simplify all this to just:
|
||||
// mChronometer.setDuration(timer.timeRemaining())
|
||||
// if we make the modification to the method as
|
||||
// described in the Timer class.
|
||||
if (!timer.hasStarted()) {
|
||||
// Set the initial text
|
||||
mChronometer.setDuration(timer.duration());
|
||||
@ -115,9 +120,8 @@ public class TimerViewHolder extends BaseViewHolder<Timer> {
|
||||
}
|
||||
|
||||
private void bindProgressBar(Timer timer) {
|
||||
mProgressBar.setMax(MAX_PROGRESS);
|
||||
final long timeRemaining = timer.timeRemaining();
|
||||
final int progress = (int) (MAX_PROGRESS * (double) timeRemaining / timer.duration());
|
||||
double ratio = (double) timeRemaining / timer.duration();
|
||||
|
||||
// In case we're reusing an animator instance that could be running
|
||||
if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
|
||||
@ -125,30 +129,13 @@ public class TimerViewHolder extends BaseViewHolder<Timer> {
|
||||
}
|
||||
|
||||
if (!timer.isRunning()) {
|
||||
mProgressBar.setProgress(progress);
|
||||
// If our scale were 1, then casting ratio to an int will ALWAYS
|
||||
// truncate down to zero.
|
||||
mProgressBar.setMax(100);
|
||||
mProgressBar.setProgress((int) (100 * ratio));
|
||||
} else {
|
||||
mProgressAnimator = ObjectAnimator.ofInt(
|
||||
// The object that has the property we wish to animate
|
||||
mProgressBar,
|
||||
// The name of the property of the object that identifies which setter method
|
||||
// the animation will call to update its values. Here, a property name of
|
||||
// "progress" will result in a call to the function setProgress() in ProgressBar.
|
||||
// The docs for ObjectAnimator#setPropertyName() says that for best performance,
|
||||
// the setter method should take a float or int parameter, and its return type
|
||||
// should be void (both of which setProgress() satisfies).
|
||||
"progress",
|
||||
// The set of values to animate between. A single value implies that that value
|
||||
// is the one being animated to. Two values imply starting and ending values.
|
||||
// More than two values imply a starting value, values to animate through along
|
||||
// the way, and an ending value (these values will be distributed evenly across
|
||||
// the duration of the animation).
|
||||
progress, 0);
|
||||
mProgressAnimator.setDuration(timeRemaining);
|
||||
// The algorithm that calculates intermediate values between keyframes. We use linear
|
||||
// interpolation so that the animation runs at constant speed.
|
||||
mProgressAnimator.setInterpolator(null/*results in linear interpolation*/);
|
||||
// This MUST be run on the UI thread.
|
||||
mProgressAnimator.start();
|
||||
mProgressAnimator = ProgressBarUtils.startNewAnimator(
|
||||
mProgressBar, ratio, timeRemaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
package com.philliphsu.clock2.util;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
/**
|
||||
* Created by Phillip Hsu on 8/10/2016.
|
||||
*/
|
||||
public class ProgressBarUtils {
|
||||
private static final int MAX_PROGRESS = 100000;
|
||||
|
||||
/**
|
||||
* Constructs and starts a new countdown ObjectAnimator with the given properties.
|
||||
* @param ratio The initial ratio to show on the progress bar. This will be scaled
|
||||
* by {@link #MAX_PROGRESS}. A ratio of 1 means the bar is full.
|
||||
* @return the created ObjectAnimator for holding a reference to
|
||||
*/
|
||||
public static ObjectAnimator startNewAnimator(final ProgressBar bar, final double ratio, final long duration) {
|
||||
bar.setMax(MAX_PROGRESS);
|
||||
final int progress = (int) (MAX_PROGRESS * ratio);
|
||||
ObjectAnimator animator = ObjectAnimator.ofInt(
|
||||
// The object that has the property we wish to animate
|
||||
bar,
|
||||
// The name of the property of the object that identifies which setter method
|
||||
// the animation will call to update its values. Here, a property name of
|
||||
// "progress" will result in a call to the function setProgress() in ProgressBar.
|
||||
// The docs for ObjectAnimator#setPropertyName() says that for best performance,
|
||||
// the setter method should take a float or int parameter, and its return type
|
||||
// should be void (both of which setProgress() satisfies).
|
||||
"progress",
|
||||
// The set of values to animate between. A single value implies that that value
|
||||
// is the one being animated to. Two values imply starting and ending values.
|
||||
// More than two values imply a starting value, values to animate through along
|
||||
// the way, and an ending value (these values will be distributed evenly across
|
||||
// the duration of the animation).
|
||||
// TODO: Consider leaving the set of values up to the client. Currently, we
|
||||
// have hardcoded this animator to be a "countdown" progress bar. This is
|
||||
// sufficient for our current needs.
|
||||
progress, 0);
|
||||
animator.setDuration(duration);
|
||||
// The algorithm that calculates intermediate values between keyframes. We use linear
|
||||
// interpolation so that the animation runs at constant speed.
|
||||
animator.setInterpolator(null/*results in linear interpolation*/);
|
||||
// This MUST be run on the UI thread.
|
||||
animator.start();
|
||||
return animator;
|
||||
}
|
||||
}
|
||||
69
app/src/main/res/layout-v21/fragment_stopwatch.xml
Normal file
69
app/src/main/res/layout-v21/fragment_stopwatch.xml
Normal file
@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/top_panel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/colorPrimary"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.philliphsu.clock2.stopwatch.ChronometerWithMillis
|
||||
android:id="@+id/chronometer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:textSize="45sp"
|
||||
style="@style/TextAppearance.AppCompat.Inverse"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/new_lap"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:layout_below="@id/chronometer"
|
||||
android:layout_alignParentStart="true"
|
||||
android:src="@drawable/ic_half_day_1_black_24dp"/>
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/chronometer"
|
||||
android:layout_centerInParent="true"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/stop"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:layout_below="@id/chronometer"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:src="@drawable/ic_half_day_1_black_24dp"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- RecyclerView -->
|
||||
<include layout="@layout/fragment_recycler_view"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_anchor="@id/top_panel"
|
||||
app:layout_anchorGravity="bottom"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"/>
|
||||
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
@ -42,7 +42,7 @@
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
android:src="@android:drawable/ic_dialog_email"/>
|
||||
|
||||
|
||||
74
app/src/main/res/layout/fragment_stopwatch.xml
Normal file
74
app/src/main/res/layout/fragment_stopwatch.xml
Normal file
@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/top_panel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/colorPrimary"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.philliphsu.clock2.stopwatch.ChronometerWithMillis
|
||||
android:id="@+id/chronometer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:textSize="45sp"
|
||||
style="@style/TextAppearance.AppCompat.Inverse"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/new_lap"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:src="@drawable/ic_half_day_1_black_24dp"/>
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/stop"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:src="@drawable/ic_half_day_1_black_24dp"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- RecyclerView -->
|
||||
<include layout="@layout/fragment_recycler_view"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_anchor="@id/top_panel"
|
||||
app:layout_anchorGravity="bottom"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"/>
|
||||
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
30
app/src/main/res/layout/item_lap.xml
Normal file
30
app/src/main/res/layout/item_lap.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/lap_number"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
style="@style/TextAppearance.AppCompat"/>
|
||||
|
||||
<com.philliphsu.clock2.stopwatch.ChronometerWithMillis
|
||||
android:id="@+id/elapsed_time"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total_time"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
style="@style/TextAppearance.AppCompat"/>
|
||||
|
||||
</LinearLayout>
|
||||
Loading…
Reference in New Issue
Block a user