From c768559acce9e41cf698fc1249502dfca62cf2a9 Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Wed, 13 Jul 2016 03:18:40 -0700 Subject: [PATCH] Created dialog for numpad time picker --- app/build.gradle | 9 +- .../com/philliphsu/clock2/MainActivity.java | 15 +- .../clock2/editalarm/EditAlarmActivity.java | 9 +- .../clock2/editalarm/GridLayoutNumpad.java | 222 ++++++++++ .../clock2/editalarm/NumpadTimePicker.java | 416 ++++++++++++++++++ .../editalarm/NumpadTimePickerDialog.java | 146 ++++++ .../clock2/editalarm/OnTimeSetListener.java | 18 + .../res/layout/content_grid_layout_numpad.xml | 74 ++++ .../res/layout/content_numpad_time_picker.xml | 20 + .../res/layout/dialog_time_picker_numpad.xml | 65 +++ app/src/main/res/values/styles.xml | 7 + 11 files changed, 995 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/philliphsu/clock2/editalarm/GridLayoutNumpad.java create mode 100644 app/src/main/java/com/philliphsu/clock2/editalarm/NumpadTimePicker.java create mode 100644 app/src/main/java/com/philliphsu/clock2/editalarm/NumpadTimePickerDialog.java create mode 100644 app/src/main/java/com/philliphsu/clock2/editalarm/OnTimeSetListener.java create mode 100644 app/src/main/res/layout/content_grid_layout_numpad.xml create mode 100644 app/src/main/res/layout/content_numpad_time_picker.xml create mode 100644 app/src/main/res/layout/dialog_time_picker_numpad.xml diff --git a/app/build.gradle b/app/build.gradle index 791a8d4..3c77533 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,9 +28,10 @@ dependencies { testCompile 'org.robolectric:robolectric:3.0' // TODO: delete, not in use provided 'com.google.auto.value:auto-value:1.2' apt 'com.google.auto.value:auto-value:1.2' - compile 'com.android.support:appcompat-v7:23.2.1' - compile 'com.android.support:design:23.2.1' - compile 'com.android.support:support-v4:23.2.1' - compile 'com.android.support:recyclerview-v7:23.2.1' + compile 'com.android.support:appcompat-v7:23.4.0' + compile 'com.android.support:design:23.4.0' + compile 'com.android.support:support-v4:23.4.0' + compile 'com.android.support:recyclerview-v7:23.4.0' + compile 'com.android.support:gridlayout-v7:23.4.0' compile 'com.jakewharton:butterknife:7.0.1' } diff --git a/app/src/main/java/com/philliphsu/clock2/MainActivity.java b/app/src/main/java/com/philliphsu/clock2/MainActivity.java index 48c732d..44c895b 100644 --- a/app/src/main/java/com/philliphsu/clock2/MainActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/MainActivity.java @@ -8,6 +8,8 @@ import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; +import android.text.format.DateFormat; +import android.util.Log; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; @@ -15,7 +17,8 @@ import android.view.ViewGroup; import android.widget.TextView; import com.philliphsu.clock2.alarms.AlarmsFragment; -import com.philliphsu.clock2.editalarm.EditAlarmActivity; +import com.philliphsu.clock2.editalarm.NumpadTimePickerDialog; +import com.philliphsu.clock2.editalarm.OnTimeSetListener; import com.philliphsu.clock2.settings.SettingsActivity; import butterknife.Bind; @@ -51,9 +54,18 @@ public class MainActivity extends BaseActivity { TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); tabLayout.setupWithViewPager(mViewPager); + // TODO: @OnCLick instead. mFab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { + NumpadTimePickerDialog tpd = NumpadTimePickerDialog.newInstance(new OnTimeSetListener() { + @Override + public void onTimeSet(ViewGroup viewGroup, int hourOfDay, int minute) { + Log.i(TAG, "Time set: " + String.format("%02d:%02d", hourOfDay, minute)); + } + }, 0, 0, DateFormat.is24HourFormat(MainActivity.this)); + tpd.show(getFragmentManager(), "tag"); + /* Intent intent = new Intent(MainActivity.this, EditAlarmActivity.class); // Call Fragment#startActivityForResult() instead of Activity#startActivityForResult() // because we want the result to be handled in the Fragment, not in this Activity. @@ -61,6 +73,7 @@ public class MainActivity extends BaseActivity { // Fragment's onActivityResult() will NOT be called. mSectionsPagerAdapter.getCurrentFragment() .startActivityForResult(intent, AlarmsFragment.REQUEST_CREATE_ALARM); + */ } }); } diff --git a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java index 04f780f..ac323ca 100644 --- a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java @@ -19,6 +19,7 @@ import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; @@ -62,7 +63,8 @@ public class EditAlarmActivity extends BaseActivity implements AlarmNumpad.KeyLi EditAlarmContract.View, // TODO: Remove @Override from the methods AlarmUtilsHelper, SharedPreferencesHelper, - LoaderManager.LoaderCallbacks { + LoaderManager.LoaderCallbacks, + OnTimeSetListener { private static final String TAG = "EditAlarmActivity"; public static final String EXTRA_ALARM_ID = "com.philliphsu.clock2.editalarm.extra.ALARM_ID"; public static final String EXTRA_MODIFIED_ALARM = "com.philliphsu.clock2.editalarm.extra.MODIFIED_ALARM"; @@ -88,6 +90,11 @@ public class EditAlarmActivity extends BaseActivity implements AlarmNumpad.KeyLi @Bind(R.id.vibrate) CheckBox mVibrate; @Bind(R.id.numpad) AlarmNumpad mNumpad; + @Override + public void onTimeSet(ViewGroup viewGroup, int hourOfDay, int minute) { + Log.i(TAG, "Time set: " + String.format("%02d:%02d", hourOfDay, minute)); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/app/src/main/java/com/philliphsu/clock2/editalarm/GridLayoutNumpad.java b/app/src/main/java/com/philliphsu/clock2/editalarm/GridLayoutNumpad.java new file mode 100644 index 0000000..7e318d1 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/GridLayoutNumpad.java @@ -0,0 +1,222 @@ +package com.philliphsu.clock2.editalarm; + +import android.content.Context; +import android.support.annotation.CallSuper; +import android.support.annotation.LayoutRes; +import android.support.v7.widget.GridLayout; +import android.util.AttributeSet; +import android.view.View; +import android.widget.Button; + +import com.philliphsu.clock2.R; + +import java.util.Arrays; + +import butterknife.Bind; +import butterknife.ButterKnife; + +/** + * Created by Phillip Hsu on 7/12/2016. + * + * Successor to the Numpad class that was based on TableLayout. + * Unlike Numpad, this class only manages the logic for number button clicks + * and not the backspace button. However, we do provide an API for removing + * digits from the input. + */ +public abstract class GridLayoutNumpad extends GridLayout implements View.OnClickListener { + // TODO: change to private? + protected static final int UNMODIFIED = -1; + private static final int COLUMNS = 3; + + private int[] mInput; + private int mCount = 0; + private OnInputChangeListener mOnInputChangeListener; + + @Bind({ R.id.zero, R.id.one, R.id.two, R.id.three, R.id.four, + R.id.five, R.id.six, R.id.seven, R.id.eight, R.id.nine }) + Button[] mButtons; + + /** + * Informs clients how to output the digits inputted into this numpad. + */ + public interface OnInputChangeListener { + /** + * @param newStr the new value of the input formatted as a + * String after a digit insertion + */ + void onDigitInserted(String newStr); + /** + * @param newStr the new value of the input formatted as a + * String after a digit deletion + */ + void onDigitDeleted(String newStr); + void onDigitsCleared(); + } + + public GridLayoutNumpad(Context context) { + super(context); + init(); + } + + public GridLayoutNumpad(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + /** + * @return the number of digits we can input + */ + protected abstract int capacity(); + + /** + * @return the layout resource that defines the children for this numpad + */ + @LayoutRes + protected abstract int contentLayout(); + + public final void setOnInputChangeListener(OnInputChangeListener onInputChangeListener) { + mOnInputChangeListener = onInputChangeListener; + } + + protected final void enable(int lowerLimitInclusive, int upperLimitExclusive) { + if (lowerLimitInclusive < 0 || upperLimitExclusive > mButtons.length) + throw new IndexOutOfBoundsException("Upper limit out of range"); + + for (int i = 0; i < mButtons.length; i++) + mButtons[i].setEnabled(i >= lowerLimitInclusive && i < upperLimitExclusive); + } + + protected final int valueAt(int index) { + return mInput[index]; + } + + /** + * @return the number of digits inputted + */ + protected final int count() { + return mCount; + } + + /** + * @return the integer represented by the inputted digits + */ + protected final int getInput() { + return Integer.parseInt(getInputString()); + } + + private String getInputString() { + String currentInput = ""; + for (int i : mInput) + if (i != UNMODIFIED) + currentInput += i; + return currentInput; + } + + public void delete() { + /* + if (mCount - 1 >= 0) { + mInput[--mCount] = UNMODIFIED; + } + onDigitDeleted(getInputString()); + */ + delete(mCount); + } + + // TODO: Why do we need this? + public void delete(int at) { + if (at - 1 >= 0) { + mInput[at - 1] = UNMODIFIED; + mCount--; + onDigitDeleted(getInputString()); + } + } + + public final void clear() { + Arrays.fill(mInput, UNMODIFIED); + mCount = 0; + onDigitsCleared(); + } + + /** + * Forwards the provided String to the assigned + * {@link OnInputChangeListener OnInputChangeListener} + * after a digit insertion. By default, the String + * forwarded is just the String value of the inserted digit. + * @see #onClick(View) + * @param newDigit the formatted String that should be displayed + */ + @CallSuper + protected void onDigitInserted(String newDigit) { + if (mOnInputChangeListener != null) { + mOnInputChangeListener.onDigitInserted(newDigit); + } + } + + /** + * Forwards the provided String to the assigned + * {@link OnInputChangeListener OnInputChangeListener} + * after a digit deletion. By default, the String + * forwarded is {@link #getInputString()}. + * @param newStr the formatted String that should be displayed + */ + @CallSuper + protected void onDigitDeleted(String newStr) { + if (mOnInputChangeListener != null) { + mOnInputChangeListener.onDigitDeleted(newStr); + } + } + + /** + * Forwards a {@code onDigitsCleared()} event to the assigned + * {@link OnInputChangeListener OnInputChangeListener}. + */ + @CallSuper + protected void onDigitsCleared() { + if (mOnInputChangeListener != null) { + mOnInputChangeListener.onDigitsCleared(); + } + } + + /** + * Inserts as many of the digits in the given sequence + * into the input as possible. At the end, if any digits + * were inserted, this calls {@link #onDigitInserted(String)} + * with the String value of those digits. + */ + protected final void insertDigits(int... digits) { + String newDigits = ""; + for (int d : digits) { + if (mCount == mInput.length) + break; + mInput[mCount++] = d; + newDigits += d; + } + if (!newDigits.isEmpty()) { + // By only calling this once after making + // the insertions, we skip all of the + // intermediate callbacks. + onDigitInserted(newDigits); + } + } + + // TODO: Why not @OnClick instead? + @Override + public final void onClick(View v) { + if (mCount < mInput.length) { + String textNum = ((Button) v).getText().toString(); + insertDigits(Integer.parseInt(textNum)); + } + } + + private void init() { + setAlignmentMode(ALIGN_BOUNDS); + setColumnCount(COLUMNS); + View.inflate(getContext(), contentLayout(), this); + ButterKnife.bind(this); + for (Button b : mButtons) + b.setOnClickListener(this); + // If capacity() < 0, we let the system throw the exception. + mInput = new int[capacity()]; + Arrays.fill(mInput, UNMODIFIED); + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/editalarm/NumpadTimePicker.java b/app/src/main/java/com/philliphsu/clock2/editalarm/NumpadTimePicker.java new file mode 100644 index 0000000..94cde07 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/NumpadTimePicker.java @@ -0,0 +1,416 @@ +package com.philliphsu.clock2.editalarm; + +import android.content.Context; +import android.support.annotation.IntDef; +import android.text.format.DateFormat; +import android.util.AttributeSet; +import android.widget.Button; + +import com.philliphsu.clock2.R; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.text.DateFormatSymbols; +import java.util.Calendar; + +import butterknife.Bind; +import butterknife.OnClick; + +/** + * Created by Phillip Hsu on 7/12/2016. + */ +public class NumpadTimePicker extends GridLayoutNumpad { + // Time can be represented with maximum of 4 digits + private static final int MAX_DIGITS = 4; + + // Formatted time string has a maximum of 8 characters + // in the 12-hour clock, e.g 12:59 AM. Although the 24-hour + // clock should be capped at 5 characters, the difference + // is not significant enough to deal with the separate cases. + private static final int MAX_CHARS = 8; + + // Constant for converting text digits to numeric digits in base-10. + private static final int BASE_10 = 10; + + // AmPmStates + private static final int UNSPECIFIED = -1; + private static final int AM = 0; + private static final int PM = 1; + private static final int HRS_24 = 2; + + @IntDef({ UNSPECIFIED, AM, PM, HRS_24 }) // Specifies the accepted constants + @Retention(RetentionPolicy.SOURCE) // Usages do not need to be recorded in .class files + private @interface AmPmState {} + + @AmPmState + private int mAmPmState = UNSPECIFIED; + private final StringBuilder mFormattedInput = new StringBuilder(MAX_CHARS); + + @Bind({ R.id.leftAlt, R.id.rightAlt }) + Button[] mAltButtons; + + public NumpadTimePicker(Context context) { + super(context); + init(); + } + + public NumpadTimePicker(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + @Override + protected int capacity() { + return MAX_DIGITS; + } + + @Override + protected int contentLayout() { + return R.layout.content_numpad_time_picker; + } + + @Override + protected void onDigitInserted(String newDigit) { + // Append the new digit(s) to the formatter + updateFormattedInputOnDigitInserted(newDigit); + super.onDigitInserted(mFormattedInput.toString()); + updateNumpadStates(); + } + + @Override + protected void onDigitDeleted(String newStr) { + updateFormattedInputOnDigitDeleted(); + super.onDigitDeleted(mFormattedInput.toString()); + updateNumpadStates(); + } + + @Override + protected void onDigitsCleared() { + mFormattedInput.delete(0, mFormattedInput.length()); + updateNumpadStates(); + mAmPmState = UNSPECIFIED; + super.onDigitsCleared(); + } + + @Override + public void delete() { + int len = mFormattedInput.length(); + if (!is24HourFormat() && mAmPmState != UNSPECIFIED) { + mAmPmState = UNSPECIFIED; + // Delete starting from index of space to end + mFormattedInput.delete(mFormattedInput.indexOf(" "), len); + // No digit was actually deleted, but we have to notify the + // listener to update its output. + /*TOneverDO: remove super*/super.onDigitDeleted( + mFormattedInput.toString()); + updateNumpadStates(); + } else { + super.delete(); + } + } + + /** Returns the hour of day (0-23) regardless of clock system */ + public int getHours() { + if (!checkTimeValid()) + throw new IllegalStateException("Cannot call getHours() until legal time inputted"); + int hours = count() < 4 ? valueAt(0) : valueAt(0) * 10 + valueAt(1); + if (hours == 12) { + switch (mAmPmState) { + case AM: + return 0; + case PM: + case HRS_24: + return 12; + default: + break; + } + } + + // AM/PM clock needs value offset + return hours + (mAmPmState == PM ? 12 : 0); + } + + public int getMinutes() { + if (!checkTimeValid()) + throw new IllegalStateException("Cannot call getMinutes() until legal time inputted"); + return count() < 4 ? valueAt(1) * 10 + valueAt(2) : valueAt(2) * 10 + valueAt(3); + } + + /** + * Checks if the input stored so far qualifies as a valid time. + * For this to return {@code true}, the hours, minutes AND AM/PM + * state must be set. + */ + public boolean checkTimeValid() { + if (mAmPmState == UNSPECIFIED || mAmPmState == HRS_24 && count() < 3) + return false; + // AM or PM can only be set if the time was already valid previously, so we don't need + // to check for them. + return true; + } + + public void setTime(int hours, int minutes) { + if (hours < 0 || hours > 23) + throw new IllegalArgumentException("Illegal hours: " + hours); + if (minutes < 0 || minutes > 59) + throw new IllegalArgumentException("Illegal minutes: " + minutes); + + // Internal representation of the time has been checked for legality. + // Now we need to format it depending on the user's clock system. + // If 12-hour clock, can't set mAmPmState yet or else this interferes + // with the button state update mechanism. Instead, cache the state + // the hour would resolve to in a local variable and set it after + // all digits are inputted. + int amPmState; + if (!is24HourFormat()) { + // Convert 24-hour times into 12-hour compatible times. + if (hours == 0) { + hours = 12; + amPmState = AM; + } else if (hours == 12) { + amPmState = PM; + } else if (hours > 12) { + hours -= 12; + amPmState = PM; + } else { + amPmState = AM; + } + } else { + amPmState = HRS_24; + } + + // Convert the hour and minutes into text form, so that + // we can read each digit individually. + // Only if on 24-hour clock, zero-pad single digit hours. + // Zero cannot be the first digit of any time in the 12-hour clock. + String textDigits = is24HourFormat() + ? String.format("%02d", hours) + : String.valueOf(hours); + textDigits += String.format("%02d", minutes); + + int[] digits = new int[textDigits.length()]; + for (int i = 0; i < textDigits.length(); i++) { + digits[i] = Character.digit(textDigits.charAt(i), BASE_10); + } + + insertDigits(digits); + + mAmPmState = amPmState; + if (mAmPmState != HRS_24) { + onAltButtonClick(mAmPmState == AM ? mAltButtons[0] : mAltButtons[1]); + } + } + + public String getTime() { + return mFormattedInput.toString(); + } + + private void init() { + if (is24HourFormat()) { + mAltButtons[0].setText(R.string.left_alt_24hr); + mAltButtons[1].setText(R.string.right_alt_24hr); + } else { + String[] amPmTexts = new DateFormatSymbols().getAmPmStrings(); + mAltButtons[0].setText(amPmTexts[Calendar.AM]); + mAltButtons[1].setText(amPmTexts[Calendar.PM]); + } + updateNumpadStates(); + } + + @OnClick({ R.id.leftAlt, R.id.rightAlt }) + void onClick(Button altBtn) { + onAltButtonClick(altBtn); + } + + private void onAltButtonClick(Button altBtn) { + if (mAltButtons[0] != altBtn && mAltButtons[1] != altBtn) + throw new IllegalArgumentException("Not called with one of the alt buttons"); + + // Manually insert special characters for 12-hour clock + if (!is24HourFormat()) { + if (count() <= 2) { + // The colon is inserted for you + insertDigits(0, 0); + } + // text is AM or PM, so include space before + mFormattedInput.append(' ').append(altBtn.getText()); + mAmPmState = mAltButtons[0] == altBtn ? AM : PM; + // Digits will be shown for you on insert, but not AM/PM + /*TOneverDO: remove super*/super.onDigitInserted( + mFormattedInput.toString()); + } else { + CharSequence text = altBtn.getText(); + int[] digits = new int[text.length() - 1]; + // charAt(0) is the colon, so skip i = 0. + // We are only interested in storing the digits. + for (int i = 1; i < text.length(); i++) { + digits[i] = Character.digit(text.charAt(i), BASE_10); + } + // Colon is added for you + insertDigits(digits); + mAmPmState = HRS_24; + } + + updateNumpadStates(); + } + + private boolean is24HourFormat() { + return DateFormat.is24HourFormat(getContext()); + } + + private void updateFormattedInputOnDigitInserted(String newDigits) { + mFormattedInput.append(newDigits); + // Add colon if necessary, depending on how many digits entered so far + if (count() == 3) { + // Insert a colon + int digits = getInput(); + if (digits >= 60 && digits < 100 || digits >= 160 && digits < 200) { + // From 060-099 (really only to 095, but might as well go up to 100) + // From 160-199 (really only to 195, but might as well go up to 200), + // time does not exist if colon goes at pos. 1 + mFormattedInput.insert(2, ':'); + // These times only apply to the 24-hour clock, and if we're here, + // the time is not legal yet. So we can't set mAmPmState here for + // either clock. + // The 12-hour clock can only have mAmPmState set when AM/PM are clicked. + } else { + // A valid time exists if colon is at pos. 1 + mFormattedInput.insert(1, ':'); + // We can set mAmPmState here (and not in the above case) because + // the time here is legal in 24-hour clock + if (is24HourFormat()) { + mAmPmState = HRS_24; + } + } + } else if (count() == MAX_DIGITS) { + int colonAt = mFormattedInput.indexOf(":"); + // Since we now batch updating the formatted input whenever + // digits are inserted, the colon may legitimately not be + // present in the formatted input when this is initialized. + if (colonAt != -1) { + // Colon needs to move, so remove the colon previously added + mFormattedInput.deleteCharAt(colonAt); + } + mFormattedInput.insert(2, ':'); + + // Time is legal in 24-hour clock + if (is24HourFormat()) { + mAmPmState = HRS_24; + } + } + } + + private void updateFormattedInputOnDigitDeleted() { + int len = mFormattedInput.length(); + mFormattedInput.delete(len - 1, len); + if (count() == 3) { + // Move the colon from its 4-digit position to its 3-digit position, + // unless doing so gives an invalid time. + // e.g. 17:55 becomes 1:75, which is invalid. + // All 3-digit times in the 12-hour clock at this point should be + // valid. The limits <=155 and (>=200 && <=235) are really only + // imposed on the 24-hour clock, and were chosen because 4-digit times + // in the 24-hour clock can only go up to 15:5[0-9] or be within the range + // [20:00, 23:59] if they are to remain valid when they become three digits. + // The is24HourFormat() check is therefore unnecessary. + int value = getInput(); + if (value <= 155 || value >= 200 && value <= 235) { + mFormattedInput.deleteCharAt(mFormattedInput.indexOf(":")); + mFormattedInput.insert(1, ":"); + } + } else if (count() == 2) { + // Remove the colon + mFormattedInput.deleteCharAt(mFormattedInput.indexOf(":")); + } + } + + private void updateNumpadStates() { + updateAltButtonStates(); + //updateBackspaceState(); // Backspace is not part of the numpad + updateNumberKeysStates(); + } + + private void updateAltButtonStates() { + if (count() == 0) { + // No input, no access! + mAltButtons[0].setEnabled(false); + mAltButtons[1].setEnabled(false); + } else if (count() == 1) { + // Any of 0-9 inputted, always have access in either clock. + mAltButtons[0].setEnabled(true); + mAltButtons[1].setEnabled(true); + } else if (count() == 2) { + // Any 2 digits that make a valid hour for either clock are eligible for access + int time = getInput(); + boolean validTwoDigitHour = is24HourFormat() ? time <= 23 : time >= 10 && time <= 12; + mAltButtons[0].setEnabled(validTwoDigitHour); + mAltButtons[1].setEnabled(validTwoDigitHour); + } else if (count() == 3) { + if (is24HourFormat()) { + // For the 24-hour clock, no access at all because + // two more digits (00 or 30) cannot be added to 3 digits. + mAltButtons[0].setEnabled(false); + mAltButtons[1].setEnabled(false); + } else { + // True for any 3 digits, if AM/PM not already entered + boolean enabled = mAmPmState == UNSPECIFIED; + mAltButtons[0].setEnabled(enabled); + mAltButtons[1].setEnabled(enabled); + } + } else if (count() == MAX_DIGITS) { + // If all 4 digits are filled in, the 24-hour clock has absolutely + // no need for the alt buttons. However, The 12-hour clock has + // complete need of them, if not already used. + boolean enabled = !is24HourFormat() && mAmPmState == UNSPECIFIED; + mAltButtons[0].setEnabled(enabled); + mAltButtons[1].setEnabled(enabled); + } + } + + private void updateNumberKeysStates() { + int cap = 10; // number of buttons + boolean is24hours = is24HourFormat(); + + if (count() == 0) { + enable(is24hours ? 0 : 1, cap); + return; + } else if (count() == MAX_DIGITS) { + enable(0, 0); + return; + } + + int time = getInput(); + if (is24hours) { + if (count() == 1) { + enable(0, time < 2 ? cap : 6); + } else if (count() == 2) { + enable(0, time % 10 >= 0 && time % 10 <= 5 ? cap : 6); + } else if (count() == 3) { + if (time >= 236) { + enable(0, 0); + } else { + enable(0, time % 10 >= 0 && time % 10 <= 5 ? cap : 0); + } + } + } else { + if (count() == 1) { + if (time == 0) { + throw new IllegalStateException("12-hr format, zeroth digit = 0?"); + } else { + enable(0, 6); + } + } else if (count() == 2 || count() == 3) { + if (time >= 126) { + enable(0, 0); + } else { + if (time >= 100 && time <= 125 && mAmPmState != UNSPECIFIED) { + // Could legally input fourth digit, if not for the am/pm state already set + enable(0, 0); + } else { + enable(0, time % 10 >= 0 && time % 10 <= 5 ? cap : 0); + } + } + } + } + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/editalarm/NumpadTimePickerDialog.java b/app/src/main/java/com/philliphsu/clock2/editalarm/NumpadTimePickerDialog.java new file mode 100644 index 0000000..1901848 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/NumpadTimePickerDialog.java @@ -0,0 +1,146 @@ +package com.philliphsu.clock2.editalarm; + +import android.app.DialogFragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; + +import com.philliphsu.clock2.R; + +import butterknife.Bind; +import butterknife.ButterKnife; +import butterknife.OnClick; +import butterknife.OnLongClick; + +/** + * Created by Phillip Hsu on 7/12/2016. + * + * Note this extends the framework's DialogFragment, NOT the support version's! + */ +public class NumpadTimePickerDialog extends DialogFragment + implements NumpadTimePicker.OnInputChangeListener { + + private static final String KEY_HOUR_OF_DAY = "hour_of_day"; + private static final String KEY_MINUTE = "minute"; + private static final String KEY_IS_24_HOUR_VIEW = "is_24_hour_view"; + + private OnTimeSetListener mCallback; + + private int mInitialHourOfDay; + private int mInitialMinute; + private boolean mIs24HourMode; + + @Bind(R.id.backspace) ImageButton mBackspace; + @Bind(R.id.input) EditText mInputField; + @Bind(R.id.cancel) Button mCancelButton; + @Bind(R.id.ok) Button mOkButton; + @Bind(R.id.number_grid) NumpadTimePicker mNumpad; + + public NumpadTimePickerDialog() { + // Empty constructor required for dialog fragment. + } + + // TODO: We don't need to pass in an initial hour and minute for a new instance. + public static NumpadTimePickerDialog newInstance(OnTimeSetListener callback, + int hourOfDay, int minute, boolean is24HourMode) { + NumpadTimePickerDialog ret = new NumpadTimePickerDialog(); + ret.initialize(callback, hourOfDay, minute, is24HourMode); + return ret; + } + + public void initialize(OnTimeSetListener callback, + int hourOfDay, int minute, boolean is24HourMode) { + mCallback = callback; + mInitialHourOfDay = hourOfDay; + mInitialMinute = minute; + mIs24HourMode = is24HourMode; + } + + public void setOnTimeSetListener(OnTimeSetListener callback) { + mCallback = callback; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null && savedInstanceState.containsKey(KEY_HOUR_OF_DAY) + && savedInstanceState.containsKey(KEY_MINUTE) + && savedInstanceState.containsKey(KEY_IS_24_HOUR_VIEW)) { + mInitialHourOfDay = savedInstanceState.getInt(KEY_HOUR_OF_DAY); + mInitialMinute = savedInstanceState.getInt(KEY_MINUTE); + mIs24HourMode = savedInstanceState.getBoolean(KEY_IS_24_HOUR_VIEW); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE); + View view = inflater.inflate(R.layout.dialog_time_picker_numpad, container, false); + ButterKnife.bind(this, view); + mNumpad.setOnInputChangeListener(this); + + //mNumpad.setTime(mInitialHourOfDay, mInitialMinute); + // TODO: Write numpad method set24HourMode() and use mIs24HourMode + + return view; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + if (mNumpad != null) { + outState.putInt(KEY_HOUR_OF_DAY, mNumpad.getHours()); + outState.putInt(KEY_MINUTE, mNumpad.getMinutes()); + outState.putBoolean(KEY_IS_24_HOUR_VIEW, mIs24HourMode); + //outState.putBoolean(KEY_DARK_THEME, mThemeDark); + } + } + + @Override + public void onDigitInserted(String newStr) { + updateInputText(newStr); + } + + @Override + public void onDigitDeleted(String newStr) { + updateInputText(newStr); + } + + @Override + public void onDigitsCleared() { + updateInputText(""); + } + + @OnClick(R.id.cancel) + void myCancel() { + dismiss(); + } + + @OnClick(R.id.ok) + void ok() { + if (!mNumpad.checkTimeValid()) + return; + mCallback.onTimeSet(mNumpad, mNumpad.getHours(), mNumpad.getMinutes()); + dismiss(); + } + + @OnClick(R.id.backspace) + void backspace() { + mNumpad.delete(); + } + + @OnLongClick(R.id.backspace) + boolean longBackspace() { + mNumpad.clear(); + return true; + } + + private void updateInputText(String inputText) { + mInputField.setText(inputText); + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/editalarm/OnTimeSetListener.java b/app/src/main/java/com/philliphsu/clock2/editalarm/OnTimeSetListener.java new file mode 100644 index 0000000..5b475ec --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/OnTimeSetListener.java @@ -0,0 +1,18 @@ +package com.philliphsu.clock2.editalarm; + +import android.view.ViewGroup; + +/** + * Created by Phillip Hsu on 7/12/2016. + * + * The callback interface used to indicate the user is done filling in + * the time (they clicked on the 'Set' button). + */ +public interface OnTimeSetListener { + /** + * @param viewGroup The view associated with this listener. + * @param hourOfDay The hour that was set. + * @param minute The minute that was set. + */ + void onTimeSet(ViewGroup viewGroup, int hourOfDay, int minute); +} diff --git a/app/src/main/res/layout/content_grid_layout_numpad.xml b/app/src/main/res/layout/content_grid_layout_numpad.xml new file mode 100644 index 0000000..e80f365 --- /dev/null +++ b/app/src/main/res/layout/content_grid_layout_numpad.xml @@ -0,0 +1,74 @@ + + +