diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/Utils.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/Utils.java index bc7dc91c..603dc193 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/Utils.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/Utils.java @@ -20,7 +20,9 @@ import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.annotation.SuppressLint; +import android.app.UiModeManager; import android.content.Context; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; @@ -183,4 +185,14 @@ private static boolean resolveBoolean(Context context, @AttrRes int attr, boolea a.recycle(); } } + + /** + * Indicates if app is running on Android TV device + * @param context The context to use as reference for the test + * @return true if device is Android TV, false if other device type + */ + public static boolean isTv(Context context) { + UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); + return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION; + } } diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerController.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerController.java index 3ac4291a..28098d48 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerController.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerController.java @@ -54,4 +54,10 @@ public interface DatePickerController { boolean isOutOfRange(int year, int month, int day); void tryVibrate(); + + void focusYear(); + + void focusMonthDays(); + + void focusDialogButtons(); } diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerDialog.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerDialog.java index 3314913e..9a1cc448 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerDialog.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerDialog.java @@ -983,4 +983,24 @@ public void notifyOnDateListener() { mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH)); } } + + @Override + public void focusYear() { + setCurrentView(YEAR_VIEW); + } + + @Override + public void focusMonthDays() { + setCurrentView(MONTH_AND_DAY_VIEW); + } + + @Override + public void focusDialogButtons() { + if (getView() != null) { + Button okButton = (Button) getView().findViewById(R.id.ok); + if (okButton != null) { + okButton.requestFocus(); + } + } + } } diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DayPickerView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DayPickerView.java index 303488f3..ed48349e 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DayPickerView.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DayPickerView.java @@ -158,6 +158,9 @@ protected void setUpListView() { setFadingEdgeLength(0); // Make the scrolling behavior nicer setFriction(ViewConfiguration.getScrollFriction() * mFriction); + // Turn off focus so that MonthView can get key events, for Android TV + setFocusable(false); + setFocusableInTouchMode(false); } /** diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/MonthView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/MonthView.java index ae184b88..6129143e 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/MonthView.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/MonthView.java @@ -33,6 +33,7 @@ import android.support.v4.widget.ExploreByTouchHelper; import android.text.format.DateFormat; import android.util.AttributeSet; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityEvent; @@ -40,6 +41,7 @@ import com.wdullaer.materialdatetimepicker.R; import com.wdullaer.materialdatetimepicker.TypefaceHelper; +import com.wdullaer.materialdatetimepicker.Utils; import com.wdullaer.materialdatetimepicker.date.MonthAdapter.CalendarDay; import java.security.InvalidParameterException; @@ -239,6 +241,11 @@ public MonthView(Context context, AttributeSet attr, DatePickerController contro ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); mLockAccessibilityDelegate = true; + if (Utils.isTv(getContext())) { + setFocusable(true); + setFocusableInTouchMode(true); + } + // Sets up any standard paints that will be used initView(); } @@ -286,6 +293,168 @@ public boolean onTouchEvent(@NonNull MotionEvent event) { return true; } + /** + * Key listener for D-pad navigation between days, particularly used for Android TV + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (Utils.isTv(getContext())) { + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER + || keyCode == KeyEvent.KEYCODE_ENTER) { + // move focus to ok button + mController.focusDialogButtons(); + return true; + } + Calendar currentDate = Calendar.getInstance(); + currentDate.set(mYear, mMonth, mSelectedDay); + Calendar newDate = Calendar.getInstance(); + newDate.set(mYear, mMonth, mSelectedDay); + int dayDrawOffset = findDayOffset(); + if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { + newDate.add(Calendar.DAY_OF_MONTH, -7); + } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + newDate.add(Calendar.DAY_OF_MONTH, 7); + } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + int firstLeftSideDay = dayDrawOffset == 0 ? 1 : 7 - dayDrawOffset + 1; // +1 to correct that days start at 1, not 0 + int dayColumn = (mSelectedDay % 7) - firstLeftSideDay; + int MAX_ITERATIONS = 10; + int count = 0; + while (dayColumn < 0) { + dayColumn += 7; + if (++count > MAX_ITERATIONS) { + break; + } + } + if (dayColumn == 0) { + // move focus left off month days, i.e. to year picker + mController.focusYear(); + return true; + } + boolean dayIsLeftMostSelectable = true; + for (int i = 1 ; i <= dayColumn ; i++) { + if (mSelectedDay - i < 1) { + break; + } + if (!mController.isOutOfRange(mYear, mMonth, mSelectedDay - i)) { + dayIsLeftMostSelectable = false; + break; + } + } + if (dayIsLeftMostSelectable) { + // move focus left off month days, i.e. to year picker + mController.focusYear(); + return true; + } + // else + newDate.add(Calendar.DAY_OF_MONTH, -1); + } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + int firstLeftSideDay = dayDrawOffset == 0 ? 1 : 7 - dayDrawOffset + 1; // +1 to correct that days start at 1, not 0 + int dayColumn = (mSelectedDay % 7) - firstLeftSideDay; + int MAX_ITERATIONS = 10; + int count = 0; + while (dayColumn < 0) { + dayColumn += 7; + if (++count > MAX_ITERATIONS) { + break; + } + } + if (dayColumn == 6) { + // move focus right off month days, i.e. to ok button + mController.focusDialogButtons(); + return true; + } + boolean dayIsRightMostSelectable = true; + Calendar monthCalendar = Calendar.getInstance(); + monthCalendar.set(mYear, mMonth, 1); + int numDaysInMonth = monthCalendar.getActualMaximum(Calendar.DAY_OF_MONTH); + for (int i = 1 ; i <= (6 - dayColumn) ; i++) { + if (mSelectedDay + i > numDaysInMonth) { + break; + } + if (!mController.isOutOfRange(mYear, mMonth, mSelectedDay + i)) { + dayIsRightMostSelectable = false; + break; + } + } + if (dayIsRightMostSelectable) { + // move focus right off month days, i.e. to ok button + mController.focusDialogButtons(); + return true; + } + // else + newDate.add(Calendar.DAY_OF_MONTH, 1); + } + if (newDate.get(Calendar.DAY_OF_MONTH) != mSelectedDay) { + if (mOnDayClickListener != null) { + boolean isDateAllowed = !mController.isOutOfRange( + newDate.get(Calendar.YEAR), + newDate.get(Calendar.MONTH), + newDate.get(Calendar.DAY_OF_MONTH)); + if (isDateAllowed) { + mOnDayClickListener.onDayClick(this, new CalendarDay( + newDate.get(Calendar.YEAR), + newDate.get(Calendar.MONTH), + newDate.get(Calendar.DAY_OF_MONTH))); + return true; + } + int MAX_MONTHS_TO_SEARCH = 12; + if (newDate.before(currentDate)) { + // first (this) month + int year = currentDate.get(Calendar.YEAR); + int month = currentDate.get(Calendar.MONTH); + newDate.set(year, month, 1); + for (int i = (mSelectedDay - 1) ; i >= 1 ; i--) { + if (!mController.isOutOfRange(year, month, i)) { + mOnDayClickListener.onDayClick(this, new CalendarDay(year, month, i)); + return true; + } + } + // try previous months up to search limit + for (int j = 0 ; j < MAX_MONTHS_TO_SEARCH ; j++) { + newDate.add(Calendar.MONTH, -1); + year = newDate.get(Calendar.YEAR); + month = newDate.get(Calendar.MONTH); + for (int i = newDate.getActualMaximum(Calendar.DAY_OF_MONTH) ; i >= 1 ; i--) { + if (!mController.isOutOfRange(year, month, i)) { + mOnDayClickListener.onDayClick(this, new CalendarDay(year, month, i)); + return true; + } + } + } + // give up + } else if (newDate.after(currentDate)) { + // first (this) month + int year = currentDate.get(Calendar.YEAR); + int month = currentDate.get(Calendar.MONTH); + newDate.set(year, month, 1); + int numDaysInMonth = newDate.getActualMaximum(Calendar.DAY_OF_MONTH); + for (int i = (mSelectedDay + 1) ; i <= numDaysInMonth ; i++) { + if (!mController.isOutOfRange(year, month, i)) { + mOnDayClickListener.onDayClick(this, new CalendarDay(year, month, i)); + return true; + } + } + // try next months up to search limit + for (int j = 0 ; j < MAX_MONTHS_TO_SEARCH ; j++) { + newDate.add(Calendar.MONTH, 1); + year = newDate.get(Calendar.YEAR); + month = newDate.get(Calendar.MONTH); + numDaysInMonth = newDate.getActualMaximum(Calendar.DAY_OF_MONTH); + for (int i = 1 ; i <= numDaysInMonth ; i++) { + if (!mController.isOutOfRange(year, month, i)) { + mOnDayClickListener.onDayClick(this, new CalendarDay(year, month, i)); + return true; + } + } + } + // give up + } + } + } + } + return false; + } + /** * Sets up the text and style properties for painting. Override this if you * want to use a different paint. diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/YearPickerView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/YearPickerView.java index 563b513e..ac1f2a42 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/YearPickerView.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/YearPickerView.java @@ -19,6 +19,7 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.drawable.StateListDrawable; +import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; @@ -29,6 +30,7 @@ import android.widget.TextView; import com.wdullaer.materialdatetimepicker.R; +import com.wdullaer.materialdatetimepicker.Utils; import com.wdullaer.materialdatetimepicker.date.DatePickerDialog.OnDateChangedListener; import java.util.ArrayList; @@ -66,6 +68,9 @@ public YearPickerView(Context context, DatePickerController controller) { setSelector(new StateListDrawable()); setDividerHeight(0); onDateChanged(); + if (Utils.isTv(getContext())) { + setItemsCanFocus(true); + } } private void init(Context context) { @@ -107,7 +112,7 @@ public YearAdapter(Context context, int resource, List objects) { } @Override - public View getView(int position, View convertView, ViewGroup parent) { + public View getView(final int position, View convertView, ViewGroup parent) { TextViewWithCircularIndicator v = (TextViewWithCircularIndicator) super.getView(position, convertView, parent); v.setAccentColor(mController.getAccentColor(), mController.isThemeDark()); @@ -118,6 +123,55 @@ public View getView(int position, View convertView, ViewGroup parent) { if (selected) { mSelectedView = v; } + if (Utils.isTv(getContext())) { + v.setFocusable(true); + v.setFocusableInTouchMode(true); + if (selected) { + mSelectedView.post(new Runnable() { + @Override + public void run() { + mSelectedView.requestFocus(); + } + }); + } + v.setOnFocusChangeListener(new OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + TextViewWithCircularIndicator focusedView = (TextViewWithCircularIndicator) v; + if (focusedView != mSelectedView) { + if (mSelectedView != null) { + mSelectedView.drawIndicator(false); + mSelectedView.requestLayout(); + } + focusedView.drawIndicator(true); + focusedView.requestLayout(); + mSelectedView = focusedView; + } + mAdapter.notifyDataSetChanged(); + } + } + }); + v.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + performItemClick(v, position, 0); + } + }); + v.setOnKeyListener(new OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + mController.focusMonthDays(); + return true; + } else if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + mController.focusDialogButtons(); + return true; + } + return false; + } + }); + } return v; } } diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/RadialPickerLayout.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/RadialPickerLayout.java index bf6d714c..cfbcd641 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/RadialPickerLayout.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/RadialPickerLayout.java @@ -28,6 +28,7 @@ import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; @@ -39,6 +40,7 @@ import android.widget.FrameLayout; import com.wdullaer.materialdatetimepicker.R; +import com.wdullaer.materialdatetimepicker.Utils; import java.util.Calendar; import java.util.Locale; @@ -61,6 +63,7 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { private static final int HOUR_INDEX = TimePickerDialog.HOUR_INDEX; private static final int MINUTE_INDEX = TimePickerDialog.MINUTE_INDEX; private static final int SECOND_INDEX = TimePickerDialog.SECOND_INDEX; + private static final int AM_PM_INDEX = TimePickerDialog.AM_PM_INDEX; private static final int AM = TimePickerDialog.AM; private static final int PM = TimePickerDialog.PM; @@ -99,7 +102,8 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { public interface OnValueSelectedListener { void onValueSelected(Timepoint newTime); void enablePicker(); - void advancePicker(int index); + void advancePicker(int index, boolean force); + void retreatPicker(int index, boolean force); } public RadialPickerLayout(Context context, AttributeSet attrs) { @@ -619,7 +623,7 @@ private int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal, * Get the item (hours, minutes or seconds) that is currently showing. */ public int getCurrentItemShowing() { - if (mCurrentItemShowing != HOUR_INDEX && mCurrentItemShowing != MINUTE_INDEX && mCurrentItemShowing != SECOND_INDEX) { + if (mCurrentItemShowing != HOUR_INDEX && mCurrentItemShowing != MINUTE_INDEX && mCurrentItemShowing != SECOND_INDEX && mCurrentItemShowing != AM_PM_INDEX) { Log.e(TAG, "Current item showing was unfortunately set to " + mCurrentItemShowing); return -1; } @@ -631,7 +635,7 @@ public int getCurrentItemShowing() { * @param animate True to animate the transition, false to show with no animation. */ public void setCurrentItemShowing(int index, boolean animate) { - if (index != HOUR_INDEX && index != MINUTE_INDEX && index != SECOND_INDEX) { + if (index != HOUR_INDEX && index != MINUTE_INDEX && index != SECOND_INDEX && index != AM_PM_INDEX) { Log.e(TAG, "TimePicker does not support view at index "+index); return; } @@ -639,6 +643,10 @@ public void setCurrentItemShowing(int index, boolean animate) { int lastIndex = getCurrentItemShowing(); mCurrentItemShowing = index; + if (index == AM_PM_INDEX) { + return; + } + if (animate && (index != lastIndex)) { ObjectAnimator[] anims = new ObjectAnimator[4]; if (index == MINUTE_INDEX && lastIndex == HOUR_INDEX) { @@ -676,9 +684,11 @@ public void setCurrentItemShowing(int index, boolean animate) { if (mTransition != null && mTransition.isRunning()) { mTransition.end(); } - mTransition = new AnimatorSet(); - mTransition.playTogether(anims); - mTransition.start(); + if (anims[0] != null && anims[1] != null && anims[2] != null && anims[3] != null) { + mTransition = new AnimatorSet(); + mTransition.playTogether(anims); + mTransition.start(); + } } else { int hourAlpha = (index == HOUR_INDEX) ? 1 : 0; int minuteAlpha = (index == MINUTE_INDEX) ? 1 : 0; @@ -693,6 +703,188 @@ public void setCurrentItemShowing(int index, boolean animate) { } + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mInputEnabled && Utils.isTv(getContext())) { + int currentlyShowingValue = getCurrentItemShowing(); + Timepoint timepoint = null; + int itemToSet = -1; + boolean flipAmPm = false; + if (keyCode == KeyEvent.KEYCODE_DPAD_UP) { + if (currentlyShowingValue == HOUR_INDEX) { + for (int i = 1 ; i < 24 ; i++) { + timepoint = new Timepoint( + (mCurrentTime.getHour() + i) % (mIs24HourMode ? 24 : 12), + mCurrentTime.getMinute(), + mCurrentTime.getSecond() + ); + if (!mController.isOutOfRange(timepoint, HOUR_INDEX)) { + flipAmPm = !mIs24HourMode + && (mCurrentTime.getHour() % 12) == 11 + && i <= 12; + break; + } + } + if (mController.isOutOfRange(timepoint, HOUR_INDEX)) { + return false; + } + itemToSet = HOUR_INDEX; + } else if (currentlyShowingValue == MINUTE_INDEX) { + int hour = mCurrentTime.getHour(); + if (mCurrentTime.getMinute() == 59) { + flipAmPm = !mIs24HourMode && mCurrentTime.getHour() == 11; + hour = (mCurrentTime.getHour() + 1) % (mIs24HourMode ? 24 : 12); + } + timepoint = new Timepoint( + hour, + (mCurrentTime.getMinute() + 1) % 60, + mCurrentTime.getSecond() + ); + if (mController.isOutOfRange(timepoint, HOUR_INDEX) + || mController.isOutOfRange(timepoint, MINUTE_INDEX)) { + return false; + } + itemToSet = MINUTE_INDEX; + } else if (currentlyShowingValue == SECOND_INDEX) { + int hour = mCurrentTime.getHour(); + int minute = mCurrentTime.getMinute(); + if (mCurrentTime.getSecond() == 59) { + if (mCurrentTime.getMinute() == 59) { + flipAmPm = !mIs24HourMode && mCurrentTime.getHour() == 11; + hour = (mCurrentTime.getHour() + 1) % (mIs24HourMode ? 24 : 12); + } + minute = (mCurrentTime.getMinute() + 1) % 60; + } + timepoint = new Timepoint( + hour, + minute, + (mCurrentTime.getSecond() + 1) % 60 + ); + if (mController.isOutOfRange(timepoint, HOUR_INDEX) + || mController.isOutOfRange(timepoint, MINUTE_INDEX) + || mController.isOutOfRange(timepoint, SECOND_INDEX)) { + return false; + } + itemToSet = SECOND_INDEX; + } else if (currentlyShowingValue == AM_PM_INDEX) { + timepoint = new Timepoint(mCurrentTime); + flipAmPm = true; + itemToSet = AM_PM_INDEX; + } + } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + if (currentlyShowingValue == HOUR_INDEX) { + for (int i = 1 ; i < 24 ; i++) { + int hour = (mCurrentTime.getHour() - i) % (mIs24HourMode ? 24 : 12); + flipAmPm = (hour % 12) < 0 || (hour % 12) == 11; + if (hour < 0) { + hour += (mIs24HourMode ? 24 : 12); + } + hour = hour % (mIs24HourMode ? 24 : 12); + timepoint = new Timepoint( + hour, + mCurrentTime.getMinute(), + mCurrentTime.getSecond() + ); + if (!mController.isOutOfRange(timepoint, HOUR_INDEX)) { + break; + } + } + if (mController.isOutOfRange(timepoint, HOUR_INDEX)) { + return false; + } + itemToSet = HOUR_INDEX; + } else if (currentlyShowingValue == MINUTE_INDEX) { + int hour = mCurrentTime.getHour(); + int minute; + if ((mCurrentTime.getMinute() % 60) == 0) { + minute = 59; + if ((mCurrentTime.getHour() % 12) == 0) { + hour = (mIs24HourMode ? 24 : 12) - 1; + flipAmPm = true; + } else { + hour = (mCurrentTime.getHour() - 1) % (mIs24HourMode ? 24 : 12); + } + } else { + minute = (mCurrentTime.getMinute() - 1) % 60; + } + timepoint = new Timepoint( + hour, + minute, + mCurrentTime.getSecond() + ); + if (mController.isOutOfRange(timepoint, HOUR_INDEX) + || mController.isOutOfRange(timepoint, MINUTE_INDEX)) { + return false; + } + itemToSet = MINUTE_INDEX; + } else if (currentlyShowingValue == SECOND_INDEX) { + int hour = mCurrentTime.getHour(); + int minute = mCurrentTime.getMinute(); + int second; + if (mCurrentTime.getSecond() == 0) { + second = 59; + if ((mCurrentTime.getMinute() % 60) == 0) { + minute = 59; + if ((mCurrentTime.getHour() % 12) == 0) { + hour = (mIs24HourMode ? 24 : 12) - 1; + flipAmPm = true; + } else { + hour = (mCurrentTime.getHour() - 1) % (mIs24HourMode ? 24 : 12); + } + } else { + minute = (mCurrentTime.getMinute() - 1) % 60; + } + } else { + second = (mCurrentTime.getSecond() - 1) % 60; + } + timepoint = new Timepoint( + hour, + minute, + second + ); + if (mController.isOutOfRange(timepoint, HOUR_INDEX) + || mController.isOutOfRange(timepoint, MINUTE_INDEX) + || mController.isOutOfRange(timepoint, SECOND_INDEX)) { + return false; + } + itemToSet = SECOND_INDEX; + } else if (currentlyShowingValue == AM_PM_INDEX) { + timepoint = new Timepoint(mCurrentTime); + flipAmPm = true; + itemToSet = AM_PM_INDEX; + } + } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + mListener.retreatPicker(getCurrentItemShowing(), true); + return true; + } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + mListener.advancePicker(getCurrentItemShowing(), true); + return true; + } + if (timepoint != null && itemToSet != -1) { + if (!mIs24HourMode) { + if (flipAmPm) { + if (mCurrentTime.isAM()) { + timepoint.setPM(); + } else { + timepoint.setAM(); + } + } else { + if (mCurrentTime.isAM()) { + timepoint.setAM(); + } else { + timepoint.setPM(); + } + } + setAmOrPm(timepoint.isAM() ? AM : PM); + } + setItem(itemToSet, timepoint); + mListener.onValueSelected(timepoint); + return true; + } + } + return false; + } + @Override public boolean onTouch(View v, MotionEvent event) { final float eventX = event.getX(); @@ -853,7 +1045,7 @@ public void run() { reselectSelector(value, false, getCurrentItemShowing()); mCurrentTime = value; mListener.onValueSelected(value); - mListener.advancePicker(getCurrentItemShowing()); + mListener.advancePicker(getCurrentItemShowing(), false); } } mDoingMove = false; diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimePickerDialog.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimePickerDialog.java index 7b36bc59..7bebe8f2 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimePickerDialog.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimePickerDialog.java @@ -86,6 +86,7 @@ public class TimePickerDialog extends DialogFragment implements public static final int HOUR_INDEX = 0; public static final int MINUTE_INDEX = 1; public static final int SECOND_INDEX = 2; + public static final int AM_PM_INDEX = 3; public static final int AM = 0; public static final int PM = 1; @@ -571,6 +572,36 @@ public void onClick(View v) { else mCancelButton.setText(mCancelResid); mCancelButton.setVisibility(isCancelable() ? View.VISIBLE : View.GONE); + if (Utils.isTv(mTimePicker.getContext())) { + mTimePicker.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + setButtonsFocusable(false); + setCurrentItemShowing(mTimePicker.getCurrentItemShowing(), false, false, false); + } + } + }); + mCancelButton.setOnKeyListener(new OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_DPAD_UP) { + mTimePicker.requestFocus(); + } + return false; + } + }); + mOkButton.setOnKeyListener(new OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_DPAD_UP) { + mTimePicker.requestFocus(); + } + return false; + } + }); + } + // Enable or disable the AM/PM view. mAmPmHitspace = view.findViewById(R.id.ampm_hitspace); if (mIs24HourMode) { @@ -704,6 +735,13 @@ public void onClick(View v) { mTimePicker.setBackgroundColor(mThemeDark? lightGray : circleBackground); view.findViewById(R.id.time_picker_dialog).setBackgroundColor(mThemeDark ? darkBackgroundColor : backgroundColor); + + /* + if (Utils.isTv(getContext())) { + mTimePicker.requestFocus(); + } + */ + return view; } @@ -801,17 +839,79 @@ public void onValueSelected(Timepoint newValue) { } @Override - public void advancePicker(int index) { - if(!mAllowAutoAdvance) return; - if(index == HOUR_INDEX && mEnableMinutes) { - setCurrentItemShowing(MINUTE_INDEX, true, true, false); + public void advancePicker(int index, boolean force) { + if(!force && !mAllowAutoAdvance) return; + if(index == HOUR_INDEX) { + if (mEnableMinutes) { + setCurrentItemShowing(MINUTE_INDEX, true, true, false); - String announcement = mSelectHours + ". " + mTimePicker.getMinutes(); - Utils.tryAccessibilityAnnounce(mTimePicker, announcement); - } else if(index == MINUTE_INDEX && mEnableSeconds) { - setCurrentItemShowing(SECOND_INDEX, true, true, false); + String announcement = mSelectHours + ":" + mTimePicker.getMinutes() + "." + mTimePicker.getSeconds(); + Utils.tryAccessibilityAnnounce(mTimePicker, announcement); + } else { + setButtonsFocusable(true); + } + } else if(index == MINUTE_INDEX ) { + if (mEnableSeconds) { + setCurrentItemShowing(SECOND_INDEX, true, true, false); - String announcement = mSelectMinutes+". " + mTimePicker.getSeconds(); + String announcement = mSelectHours + ":" + mTimePicker.getMinutes() + "." + mTimePicker.getSeconds(); + Utils.tryAccessibilityAnnounce(mTimePicker, announcement); + } else if (Utils.isTv(getActivity().getApplicationContext())) { + if (!mIs24HourMode) { + // TODO : check this + setCurrentItemShowing(AM_PM_INDEX, true, false, false); + } else { + setButtonsFocusable(true); + } + } + } else if (index == SECOND_INDEX) { + if (!mIs24HourMode) { + // TODO : check this + setCurrentItemShowing(AM_PM_INDEX, true, false, false); + } else if (Utils.isTv(getActivity().getApplicationContext())) { + setButtonsFocusable(true); + } + } else if (index == AM_PM_INDEX) { + setButtonsFocusable(true); + } + } + + @Override + public void retreatPicker(int index, boolean force) { + if(!force && !mAllowAutoAdvance) return; + if (index == AM_PM_INDEX) { + if (mEnableSeconds) { + setCurrentItemShowing(SECOND_INDEX, true, true, false); + + String announcement = mSelectHours + ":" + mTimePicker.getMinutes() + "." + mTimePicker.getSeconds(); + Utils.tryAccessibilityAnnounce(mTimePicker, announcement); + } else if (mEnableMinutes) { + setCurrentItemShowing(MINUTE_INDEX, true, true, false); + + String announcement = mSelectHours + ":" + mTimePicker.getMinutes() + "." + mTimePicker.getSeconds(); + Utils.tryAccessibilityAnnounce(mTimePicker, announcement); + } else { + setCurrentItemShowing(HOUR_INDEX, true, true, false); + + String announcement = mSelectHours + ":" + mTimePicker.getMinutes() + "." + mTimePicker.getSeconds(); + Utils.tryAccessibilityAnnounce(mTimePicker, announcement); + } + } else if(index == SECOND_INDEX) { + if (mEnableMinutes) { + setCurrentItemShowing(MINUTE_INDEX, true, true, false); + + String announcement = mSelectHours + ":" + mTimePicker.getMinutes() + "." + mTimePicker.getSeconds(); + Utils.tryAccessibilityAnnounce(mTimePicker, announcement); + } else { + setCurrentItemShowing(HOUR_INDEX, true, true, false); + + String announcement = mSelectHours + ":" + mTimePicker.getMinutes() + "." + mTimePicker.getSeconds(); + Utils.tryAccessibilityAnnounce(mTimePicker, announcement); + } + } else if(index == MINUTE_INDEX) { + setCurrentItemShowing(HOUR_INDEX, true, true, false); + + String announcement = mSelectHours + ":" + mTimePicker.getMinutes() + "." + mTimePicker.getSeconds(); Utils.tryAccessibilityAnnounce(mTimePicker, announcement); } } @@ -1001,6 +1101,9 @@ private void setCurrentItemShowing(int index, boolean animateCircle, boolean del } labelToAnimate = mMinuteView; break; + case AM_PM_INDEX: + labelToAnimate = mAmPmTextView; + break; default: int seconds = mTimePicker.getSeconds(); mTimePicker.setContentDescription(mSecondPickerDescription + ": " + seconds); @@ -1013,9 +1116,11 @@ private void setCurrentItemShowing(int index, boolean animateCircle, boolean del int hourColor = (index == HOUR_INDEX) ? mSelectedColor : mUnselectedColor; int minuteColor = (index == MINUTE_INDEX) ? mSelectedColor : mUnselectedColor; int secondColor = (index == SECOND_INDEX) ? mSelectedColor : mUnselectedColor; + int amPmColor = (index == AM_PM_INDEX) ? mSelectedColor : mUnselectedColor; mHourView.setTextColor(hourColor); mMinuteView.setTextColor(minuteColor); mSecondView.setTextColor(secondColor); + mAmPmTextView.setTextColor(amPmColor); ObjectAnimator pulseAnimator = Utils.getPulseAnimator(labelToAnimate, 0.85f, 1.1f); if (delayLabelAnimate) { @@ -1650,4 +1755,17 @@ public void notifyOnDateListener() { mCallback.onTimeSet(mTimePicker, mTimePicker.getHours(), mTimePicker.getMinutes(), mTimePicker.getSeconds()); } } + + private void setButtonsFocusable(boolean focusable) { + mOkButton.setFocusable(focusable); + mOkButton.setFocusableInTouchMode(focusable); + mCancelButton.setFocusable(focusable); + mCancelButton.setFocusableInTouchMode(focusable); + mOkButton.post(new Runnable() { + @Override + public void run() { + mOkButton.requestFocus(); + } + }); + } } diff --git a/library/src/main/res/layout-land/mdtp_time_picker_dialog.xml b/library/src/main/res/layout-land/mdtp_time_picker_dialog.xml index c93865b3..246b1777 100644 --- a/library/src/main/res/layout-land/mdtp_time_picker_dialog.xml +++ b/library/src/main/res/layout-land/mdtp_time_picker_dialog.xml @@ -20,7 +20,7 @@ android:layout_width="wrap_content" android:orientation="horizontal" android:background="@color/mdtp_background_color" - android:focusable="true" > + android:focusable="false" >