|
| 1 | +package com.reactcommunity.rndatetimepicker; |
| 2 | + |
| 3 | +import java.util.ArrayList; |
| 4 | +import java.util.List; |
| 5 | + |
| 6 | +import android.annotation.SuppressLint; |
| 7 | +import android.app.TimePickerDialog; |
| 8 | +import android.content.DialogInterface; |
| 9 | +import android.content.Context; |
| 10 | +import android.os.Handler; |
| 11 | +import android.widget.TimePicker; |
| 12 | +import android.view.View; |
| 13 | +import android.widget.EditText; |
| 14 | +import android.widget.NumberPicker; |
| 15 | + |
| 16 | +class MinuteIntervalSnappableTimePickerDialog extends TimePickerDialog { |
| 17 | + private TimePicker mTimePicker; |
| 18 | + private int mTimePickerInterval; |
| 19 | + private RNTimePickerDisplay mDisplay; |
| 20 | + private final OnTimeSetListener mTimeSetListener; |
| 21 | + private Handler handler = new Handler(); |
| 22 | + private Runnable runnable; |
| 23 | + private Context mContext; |
| 24 | + |
| 25 | + public MinuteIntervalSnappableTimePickerDialog( |
| 26 | + Context context, |
| 27 | + OnTimeSetListener listener, |
| 28 | + int hourOfDay, |
| 29 | + int minute, |
| 30 | + int minuteInterval, |
| 31 | + boolean is24HourView, |
| 32 | + RNTimePickerDisplay display |
| 33 | + ) { |
| 34 | + super(context, listener, hourOfDay, minute, is24HourView); |
| 35 | + mTimePickerInterval = minuteInterval; |
| 36 | + mTimeSetListener = listener; |
| 37 | + mDisplay = display; |
| 38 | + mContext = context; |
| 39 | + } |
| 40 | + |
| 41 | + public MinuteIntervalSnappableTimePickerDialog( |
| 42 | + Context context, |
| 43 | + int theme, |
| 44 | + OnTimeSetListener listener, |
| 45 | + int hourOfDay, |
| 46 | + int minute, |
| 47 | + int minuteInterval, |
| 48 | + boolean is24HourView, |
| 49 | + RNTimePickerDisplay display |
| 50 | + ) { |
| 51 | + super(context, theme, listener, hourOfDay, minute, is24HourView); |
| 52 | + mTimePickerInterval = minuteInterval; |
| 53 | + mTimeSetListener = listener; |
| 54 | + mDisplay = display; |
| 55 | + mContext = context; |
| 56 | + } |
| 57 | + |
| 58 | + public static boolean isValidMinuteInterval(int interval) { |
| 59 | + return interval >= 1 && interval <= 30 && 60 % interval == 0; |
| 60 | + } |
| 61 | + |
| 62 | + private boolean timePickerHasCustomMinuteInterval() { |
| 63 | + return mTimePickerInterval != RNConstants.DEFAULT_TIME_PICKER_INTERVAL; |
| 64 | + } |
| 65 | + |
| 66 | + private boolean isSpinner() { |
| 67 | + return mDisplay == RNTimePickerDisplay.SPINNER; |
| 68 | + } |
| 69 | + |
| 70 | + /** |
| 71 | + * Converts values returned from picker to actual minutes |
| 72 | + * |
| 73 | + * @param minutesOrSpinnerIndex the internal value of what the user had selected |
| 74 | + * @return returns 'real' minutes (0-59) |
| 75 | + */ |
| 76 | + private int getRealMinutes(int minutesOrSpinnerIndex) { |
| 77 | + if (mDisplay == RNTimePickerDisplay.SPINNER) { |
| 78 | + return minutesOrSpinnerIndex * mTimePickerInterval; |
| 79 | + } |
| 80 | + |
| 81 | + return minutesOrSpinnerIndex; |
| 82 | + } |
| 83 | + |
| 84 | + private int getRealMinutes() { |
| 85 | + int minute = mTimePicker.getCurrentMinute(); |
| 86 | + return getRealMinutes(minute); |
| 87 | + } |
| 88 | + |
| 89 | + /** |
| 90 | + * 'Snaps' real minutes or spinner value index to nearest valid value |
| 91 | + * in spinner mode you need to make sure to transform the picked value (which is an index) |
| 92 | + * to a real value before passing! |
| 93 | + * |
| 94 | + * @param realMinutes 'real' minutes (0-59) |
| 95 | + * @return nearest valid real minute |
| 96 | + */ |
| 97 | + private int snapRealMinutesToInterval(int realMinutes) { |
| 98 | + float stepsInMinutes = (float) realMinutes / (float) mTimePickerInterval; |
| 99 | + |
| 100 | + int rounded = Math.round(stepsInMinutes) * mTimePickerInterval; |
| 101 | + return rounded == 60 ? rounded - mTimePickerInterval : rounded; |
| 102 | + } |
| 103 | + |
| 104 | + private void assertNotSpinner(String s) { |
| 105 | + if (isSpinner()) { |
| 106 | + throw new RuntimeException(s); |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + /** |
| 111 | + * Determines if picked real minutes are ok with the minuteInterval |
| 112 | + * |
| 113 | + * @param realMinutes 'real' minutes (0-59) |
| 114 | + */ |
| 115 | + private boolean minutesNeedCorrection(int realMinutes) { |
| 116 | + assertNotSpinner("minutesNeedCorrection is not intended to be used with spinner, spinner won't allow picking invalid values"); |
| 117 | + |
| 118 | + return timePickerHasCustomMinuteInterval() && realMinutes != snapRealMinutesToInterval(realMinutes); |
| 119 | + } |
| 120 | + |
| 121 | + /** |
| 122 | + * Determines if the picker is in text input mode (keyboard icon in 'clock' mode) |
| 123 | + */ |
| 124 | + private boolean pickerIsInTextInputMode() { |
| 125 | + int textInputPickerId = mContext.getResources().getIdentifier("input_mode", "id", "android"); |
| 126 | + final View textInputPicker = this.findViewById(textInputPickerId); |
| 127 | + |
| 128 | + return textInputPicker != null && textInputPicker.hasFocus(); |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * Corrects minute values if they don't align with minuteInterval |
| 133 | + * <p> |
| 134 | + * in text input mode, correction will be postponed slightly to let the user finish the input |
| 135 | + * in clock mode we also delay it to give user visual cue about the correction |
| 136 | + * <p> |
| 137 | + * |
| 138 | + * @param view the picker's view |
| 139 | + * @param hourOfDay the picker's selected hours |
| 140 | + * @param correctedMinutes 'real' minutes (0-59) aligned to minute interval |
| 141 | + */ |
| 142 | + private void correctEnteredMinutes(final TimePicker view, final int hourOfDay, final int correctedMinutes) { |
| 143 | + assertNotSpinner("spinner never needs to be corrected because wrong values are not offered to user (both in scrolling and textInput mode)!"); |
| 144 | + final EditText textInput = (EditText) view.findFocus(); |
| 145 | + |
| 146 | + // 'correction' callback |
| 147 | + runnable = new Runnable() { |
| 148 | + @Override |
| 149 | + public void run() { |
| 150 | + if (pickerIsInTextInputMode()) { |
| 151 | + // set valid minutes && move caret to the end of input |
| 152 | + view.setCurrentHour(hourOfDay); |
| 153 | + view.setCurrentMinute(correctedMinutes); |
| 154 | + textInput.setSelection(textInput.getText().length()); |
| 155 | + } else { |
| 156 | + view.setCurrentHour(hourOfDay); |
| 157 | + // we need to set minutes to 0 for this to work on older android devices |
| 158 | + view.setCurrentMinute(0); |
| 159 | + view.setCurrentMinute(correctedMinutes); |
| 160 | + } |
| 161 | + } |
| 162 | + }; |
| 163 | + |
| 164 | + handler.postDelayed(runnable, 500); |
| 165 | + } |
| 166 | + |
| 167 | + @Override |
| 168 | + public void onTimeChanged(final TimePicker view, final int hourOfDay, final int minute) { |
| 169 | + final int realMinutes = getRealMinutes(minute); |
| 170 | + // *always* remove pending 'validation' callbacks, otherwise a valid value might be rewritten |
| 171 | + handler.removeCallbacks(runnable); |
| 172 | + |
| 173 | + if (!isSpinner() && minutesNeedCorrection(realMinutes)) { |
| 174 | + int correctedMinutes = snapRealMinutesToInterval(realMinutes); |
| 175 | + |
| 176 | + // will fire another onTimeChanged |
| 177 | + correctEnteredMinutes(view, hourOfDay, correctedMinutes); |
| 178 | + } else { |
| 179 | + super.onTimeChanged(view, hourOfDay, minute); |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + @Override |
| 184 | + public void onClick(DialogInterface dialog, int which) { |
| 185 | + if (mTimePicker != null && which == BUTTON_POSITIVE && timePickerHasCustomMinuteInterval()) { |
| 186 | + final int hours = mTimePicker.getCurrentHour(); |
| 187 | + |
| 188 | + final int realMinutes = getRealMinutes(); |
| 189 | + int validMinutes = isSpinner() ? realMinutes : snapRealMinutesToInterval(realMinutes); |
| 190 | + |
| 191 | + if (mTimeSetListener != null) { |
| 192 | + mTimeSetListener.onTimeSet(mTimePicker, hours, validMinutes); |
| 193 | + } |
| 194 | + } else { |
| 195 | + super.onClick(dialog, which); |
| 196 | + } |
| 197 | + } |
| 198 | + |
| 199 | + @Override |
| 200 | + public void updateTime(int hourOfDay, int minuteOfHour) { |
| 201 | + if (timePickerHasCustomMinuteInterval()) { |
| 202 | + if (isSpinner()) { |
| 203 | + final int realMinutes = getRealMinutes(); |
| 204 | + int selectedIndex = snapRealMinutesToInterval(realMinutes) / mTimePickerInterval; |
| 205 | + super.updateTime(hourOfDay, selectedIndex); |
| 206 | + } else { |
| 207 | + super.updateTime(hourOfDay, snapRealMinutesToInterval(minuteOfHour)); |
| 208 | + } |
| 209 | + } else { |
| 210 | + super.updateTime(hourOfDay, minuteOfHour); |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + /** |
| 215 | + * Apply visual style in 'spinner' mode |
| 216 | + * Adjust minutes to correspond selected interval |
| 217 | + */ |
| 218 | + @Override |
| 219 | + public void onAttachedToWindow() { |
| 220 | + super.onAttachedToWindow(); |
| 221 | + |
| 222 | + if (timePickerHasCustomMinuteInterval()) { |
| 223 | + setupPickerDialog(); |
| 224 | + } |
| 225 | + } |
| 226 | + |
| 227 | + private void setupPickerDialog() { |
| 228 | + int timePickerId = mContext.getResources().getIdentifier("timePicker", "id", "android"); |
| 229 | + mTimePicker = this.findViewById(timePickerId); |
| 230 | + |
| 231 | + int realMinuteBackup = mTimePicker.getCurrentMinute(); |
| 232 | + |
| 233 | + if (isSpinner()) { |
| 234 | + setSpinnerDisplayedValues(); |
| 235 | + int selectedIndex = snapRealMinutesToInterval(realMinuteBackup) / mTimePickerInterval; |
| 236 | + mTimePicker.setCurrentMinute(selectedIndex); |
| 237 | + } else { |
| 238 | + int snappedRealMinute = snapRealMinutesToInterval(realMinuteBackup); |
| 239 | + mTimePicker.setCurrentMinute(snappedRealMinute); |
| 240 | + } |
| 241 | + } |
| 242 | + |
| 243 | + @SuppressLint("DefaultLocale") |
| 244 | + private void setSpinnerDisplayedValues() { |
| 245 | + int minutePickerId = mContext.getResources().getIdentifier("minute", "id", "android"); |
| 246 | + NumberPicker minutePicker = this.findViewById(minutePickerId); |
| 247 | + |
| 248 | + minutePicker.setMinValue(0); |
| 249 | + minutePicker.setMaxValue((60 / mTimePickerInterval) - 1); |
| 250 | + |
| 251 | + List<String> displayedValues = new ArrayList<>(60 / mTimePickerInterval); |
| 252 | + for (int displayedMinute = 0; displayedMinute < 60; displayedMinute += mTimePickerInterval) { |
| 253 | + displayedValues.add(String.format("%02d", displayedMinute)); |
| 254 | + } |
| 255 | + |
| 256 | + minutePicker.setDisplayedValues(displayedValues.toArray(new String[0])); |
| 257 | + } |
| 258 | + |
| 259 | + @Override |
| 260 | + public void onDetachedFromWindow() { |
| 261 | + handler.removeCallbacks(runnable); |
| 262 | + super.onDetachedFromWindow(); |
| 263 | + } |
| 264 | +} |
0 commit comments