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