From 67f74041c5463cc7e1a3804fed8015c3ccac3224 Mon Sep 17 00:00:00 2001 From: huymobile Date: Sat, 3 Oct 2026 02:22:58 +0700 Subject: [PATCH] fix(android): keep hour when changing minutes in spinner with minuteInterval 20 or 30 The platform TimePicker spinner moves the hour by one whenever the minute spinner goes from its max to its min value (or back), assuming the wheel wrapped around. With minuteInterval 20 or 30 the minute spinner has too few values to wrap, so every such change shifted the hour. Restore the hour in that case. Co-Authored-By: Claude Opus 5.5 --- ...nuteIntervalSnappableTimePickerDialog.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/MinuteIntervalSnappableTimePickerDialog.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/MinuteIntervalSnappableTimePickerDialog.java index f977e112..e43da53d 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/MinuteIntervalSnappableTimePickerDialog.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/MinuteIntervalSnappableTimePickerDialog.java @@ -23,6 +23,9 @@ class MinuteIntervalSnappableTimePickerDialog extends TimePickerDialog { private Handler handler = new Handler(); private Runnable runnable; private Context mContext; + private NumberPicker mMinuteSpinner; + private int mLastHour = -1; + private int mLastMinute = -1; public MinuteIntervalSnappableTimePickerDialog( Context context, @@ -188,8 +191,31 @@ private void moveCursorToEnd() { handler.postDelayed(runnable, 500); } + /** + * The platform spinner treats a minute change from its max to its min value (and vice versa) + * as the minute wheel wrapping around, and moves the hour by one. When the minute spinner has + * too few values to wrap (e.g. minuteInterval 20 or 30), such a change is not a wrap, + * so the hour is restored. + * + * @return true if the hour was restored (another onTimeChanged will follow) + */ + private boolean restoreHourIfChangedByMinuteSpinner(final TimePicker view, final int hourOfDay, final int minute) { + boolean minuteSpinnerCanWrap = mMinuteSpinner == null || mMinuteSpinner.getWrapSelectorWheel(); + if (!minuteSpinnerCanWrap && mLastHour != -1 && hourOfDay != mLastHour && minute != mLastMinute) { + // will fire another onTimeChanged + view.setCurrentHour(mLastHour); + return true; + } + mLastHour = hourOfDay; + mLastMinute = minute; + return false; + } + @Override public void onTimeChanged(final TimePicker view, final int hourOfDay, final int minute) { + if (isSpinner() && restoreHourIfChangedByMinuteSpinner(view, hourOfDay, minute)) { + return; + } final int realMinutes = getRealMinutes(minute); // *always* remove pending 'validation' callbacks, otherwise a valid value might be rewritten handler.removeCallbacks(runnable); @@ -264,6 +290,8 @@ private void setupPickerDialog() { setSpinnerDisplayedValues(); int selectedIndex = snapRealMinutesToInterval(realMinuteBackup) / mTimePickerInterval; mTimePicker.setCurrentMinute(selectedIndex); + mLastHour = mTimePicker.getCurrentHour(); + mLastMinute = mTimePicker.getCurrentMinute(); } else { int snappedRealMinute = snapRealMinutesToInterval(realMinuteBackup); mTimePicker.setCurrentMinute(snappedRealMinute); @@ -274,6 +302,7 @@ private void setupPickerDialog() { private void setSpinnerDisplayedValues() { int minutePickerId = mContext.getResources().getIdentifier("minute", "id", "android"); NumberPicker minutePicker = this.findViewById(minutePickerId); + mMinuteSpinner = minutePicker; minutePicker.setMinValue(0); minutePicker.setMaxValue((60 / mTimePickerInterval) - 1);