Implemented stopwatch page
This commit is contained in:
parent
3e542f585e
commit
7dfda796f3
@ -53,10 +53,23 @@ public abstract class AsyncDatabaseTableUpdateHandler<
|
|||||||
}.execute();
|
}.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() {
|
protected final Context getContext() {
|
||||||
return mAppContext;
|
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 TM getTableManager(Context context);
|
||||||
|
|
||||||
protected abstract void onPostAsyncDelete(Integer result, T item);
|
protected abstract void onPostAsyncDelete(Integer result, T item);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.philliphsu.clock2;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
import android.support.design.widget.FloatingActionButton;
|
import android.support.design.widget.FloatingActionButton;
|
||||||
import android.support.design.widget.TabLayout;
|
import android.support.design.widget.TabLayout;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
@ -16,6 +17,7 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import com.philliphsu.clock2.alarms.AlarmsFragment;
|
import com.philliphsu.clock2.alarms.AlarmsFragment;
|
||||||
import com.philliphsu.clock2.settings.SettingsActivity;
|
import com.philliphsu.clock2.settings.SettingsActivity;
|
||||||
|
import com.philliphsu.clock2.stopwatch.StopwatchFragment;
|
||||||
import com.philliphsu.clock2.timers.TimersFragment;
|
import com.philliphsu.clock2.timers.TimersFragment;
|
||||||
|
|
||||||
import butterknife.Bind;
|
import butterknife.Bind;
|
||||||
@ -33,6 +35,13 @@ public class MainActivity extends BaseActivity {
|
|||||||
*/
|
*/
|
||||||
private SectionsPagerAdapter mSectionsPagerAdapter;
|
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)
|
@Bind(R.id.container)
|
||||||
ViewPager mViewPager;
|
ViewPager mViewPager;
|
||||||
|
|
||||||
@ -47,6 +56,76 @@ public class MainActivity extends BaseActivity {
|
|||||||
// primary sections of the activity.
|
// primary sections of the activity.
|
||||||
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
|
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
|
||||||
mViewPager.setAdapter(mSectionsPagerAdapter);
|
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 tabLayout = (TabLayout) findViewById(R.id.tabs);
|
||||||
tabLayout.setupWithViewPager(mViewPager);
|
tabLayout.setupWithViewPager(mViewPager);
|
||||||
@ -62,6 +141,12 @@ public class MainActivity extends BaseActivity {
|
|||||||
// // Fragment's onActivityResult() will NOT be called.
|
// // Fragment's onActivityResult() will NOT be called.
|
||||||
// mSectionsPagerAdapter.getCurrentFragment()
|
// mSectionsPagerAdapter.getCurrentFragment()
|
||||||
// .startActivityForResult(intent, AlarmsFragment.REQUEST_CREATE_ALARM);
|
// .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;
|
Fragment f;
|
||||||
if ((f = mSectionsPagerAdapter.getCurrentFragment()) instanceof RecyclerViewFragment) {
|
if ((f = mSectionsPagerAdapter.getCurrentFragment()) instanceof RecyclerViewFragment) {
|
||||||
((RecyclerViewFragment) f).onFabClick();
|
((RecyclerViewFragment) f).onFabClick();
|
||||||
@ -156,6 +241,8 @@ public class MainActivity extends BaseActivity {
|
|||||||
return AlarmsFragment.newInstance(1);
|
return AlarmsFragment.newInstance(1);
|
||||||
case 1:
|
case 1:
|
||||||
return new TimersFragment();
|
return new TimersFragment();
|
||||||
|
case 2:
|
||||||
|
return new StopwatchFragment();
|
||||||
default:
|
default:
|
||||||
return PlaceholderFragment.newInstance(position + 1);
|
return PlaceholderFragment.newInstance(position + 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,12 +64,6 @@ public abstract class RecyclerViewFragment<
|
|||||||
return new LinearLayoutManager(getActivity());
|
return new LinearLayoutManager(getActivity());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
getLoaderManager().initLoader(0, null, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
@ -79,6 +73,14 @@ public abstract class RecyclerViewFragment<
|
|||||||
return view;
|
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
|
@Override
|
||||||
public void onLoadFinished(Loader<C> loader, C data) {
|
public void onLoadFinished(Loader<C> loader, C data) {
|
||||||
mAdapter.swapCursor(data);
|
mAdapter.swapCursor(data);
|
||||||
|
|||||||
@ -64,6 +64,17 @@ public abstract class Timer extends ObjectWithId implements Parcelable {
|
|||||||
|
|
||||||
public long timeRemaining() {
|
public long timeRemaining() {
|
||||||
if (!hasStarted())
|
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 0;
|
||||||
return isRunning()
|
return isRunning()
|
||||||
? endTime - SystemClock.elapsedRealtime()
|
? endTime - SystemClock.elapsedRealtime()
|
||||||
|
|||||||
@ -68,7 +68,12 @@ public final class AlarmsTable {
|
|||||||
|
|
||||||
public static void onCreate(SQLiteDatabase db) {
|
public static void onCreate(SQLiteDatabase db) {
|
||||||
db.execSQL("CREATE TABLE " + TABLE_ALARMS + " ("
|
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_HOUR + " INTEGER NOT NULL, "
|
||||||
+ COLUMN_MINUTES + " INTEGER NOT NULL, "
|
+ COLUMN_MINUTES + " INTEGER NOT NULL, "
|
||||||
+ COLUMN_LABEL + " TEXT, "
|
+ COLUMN_LABEL + " TEXT, "
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import android.content.Context;
|
|||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
|
||||||
|
import com.philliphsu.clock2.stopwatch.LapsTable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Phillip Hsu on 7/30/2016.
|
* Created by Phillip Hsu on 7/30/2016.
|
||||||
*/
|
*/
|
||||||
@ -31,11 +33,13 @@ public class ClockAppDatabaseHelper extends SQLiteOpenHelper {
|
|||||||
public void onCreate(SQLiteDatabase db) {
|
public void onCreate(SQLiteDatabase db) {
|
||||||
AlarmsTable.onCreate(db);
|
AlarmsTable.onCreate(db);
|
||||||
TimersTable.onCreate(db);
|
TimersTable.onCreate(db);
|
||||||
|
LapsTable.onCreate(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||||
AlarmsTable.onUpgrade(db, oldVersion, newVersion);
|
AlarmsTable.onUpgrade(db, oldVersion, newVersion);
|
||||||
TimersTable.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
|
limit); // limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all rows in this table.
|
||||||
|
*/
|
||||||
|
public final void clear() {
|
||||||
|
mDbHelper.getWritableDatabase().delete(getTableName(), null/*all rows*/, null);
|
||||||
|
notifyContentChanged();
|
||||||
|
}
|
||||||
|
|
||||||
private void notifyContentChanged() {
|
private void notifyContentChanged() {
|
||||||
LocalBroadcastHelper.sendBroadcast(mAppContext, getOnContentChangeAction());
|
LocalBroadcastHelper.sendBroadcast(mAppContext, getOnContentChangeAction());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,7 +37,12 @@ public final class TimersTable {
|
|||||||
|
|
||||||
public static void onCreate(SQLiteDatabase db) {
|
public static void onCreate(SQLiteDatabase db) {
|
||||||
db.execSQL("CREATE TABLE " + TABLE_TIMERS + " ("
|
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_HOUR + " INTEGER NOT NULL, "
|
||||||
+ COLUMN_MINUTE + " INTEGER NOT NULL, "
|
+ COLUMN_MINUTE + " INTEGER NOT NULL, "
|
||||||
+ COLUMN_SECOND + " 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.OnListItemInteractionListener;
|
||||||
import com.philliphsu.clock2.R;
|
import com.philliphsu.clock2.R;
|
||||||
import com.philliphsu.clock2.Timer;
|
import com.philliphsu.clock2.Timer;
|
||||||
|
import com.philliphsu.clock2.util.ProgressBarUtils;
|
||||||
|
|
||||||
import butterknife.Bind;
|
import butterknife.Bind;
|
||||||
import butterknife.OnClick;
|
import butterknife.OnClick;
|
||||||
@ -85,6 +86,10 @@ public class TimerViewHolder extends BaseViewHolder<Timer> {
|
|||||||
// concurrent messages to its handler.
|
// concurrent messages to its handler.
|
||||||
mChronometer.stop();
|
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()) {
|
if (!timer.hasStarted()) {
|
||||||
// Set the initial text
|
// Set the initial text
|
||||||
mChronometer.setDuration(timer.duration());
|
mChronometer.setDuration(timer.duration());
|
||||||
@ -115,9 +120,8 @@ public class TimerViewHolder extends BaseViewHolder<Timer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void bindProgressBar(Timer timer) {
|
private void bindProgressBar(Timer timer) {
|
||||||
mProgressBar.setMax(MAX_PROGRESS);
|
|
||||||
final long timeRemaining = timer.timeRemaining();
|
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
|
// In case we're reusing an animator instance that could be running
|
||||||
if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
|
if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
|
||||||
@ -125,30 +129,13 @@ public class TimerViewHolder extends BaseViewHolder<Timer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!timer.isRunning()) {
|
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 {
|
} else {
|
||||||
mProgressAnimator = ObjectAnimator.ofInt(
|
mProgressAnimator = ProgressBarUtils.startNewAnimator(
|
||||||
// The object that has the property we wish to animate
|
mProgressBar, ratio, timeRemaining);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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:id="@+id/fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="end|bottom"
|
android:layout_gravity="bottom|end"
|
||||||
android:layout_margin="@dimen/fab_margin"
|
android:layout_margin="@dimen/fab_margin"
|
||||||
android:src="@android:drawable/ic_dialog_email"/>
|
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