From 9b71ba9a8ab2e05418bae57538bac239e473e4a7 Mon Sep 17 00:00:00 2001 From: Evgeniy Ferapontov Date: Tue, 5 Nov 2019 23:07:52 +0300 Subject: [PATCH 01/35] minuteInterval types --- src/index.d.ts | 16 +++++++++------- src/types.js | 10 +++++++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 6f6f9b3c..387d2168 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -4,6 +4,8 @@ import {NativeMethods, ViewProps} from 'react-native'; type IOSMode = 'date' | 'time' | 'datetime' | 'countdown'; type AndroidMode = 'date' | 'time'; type Display = 'spinner' | 'default' | 'clock' | 'calendar'; +type MinuteIntervalIOS = 1 | 2 | 3 | 4 | 5 | 6 | 10 | 12 | 15 | 20 | 30; +type MinuteIntervalAndroid = 1 | 5 | 10 | 15 | 20 | 30; export type Event = SyntheticEvent< Readonly<{ @@ -72,7 +74,7 @@ export type IOSNativeProps = Readonly< /** * The interval at which minutes can be selected. */ - minuteInterval?: 1 | 2 | 3 | 4 | 5 | 6 | 10 | 12 | 15 | 20 | 30; + minuteInterval?: MinuteIntervalIOS; /** * The date picker mode. @@ -87,11 +89,6 @@ export type IOSNativeProps = Readonly< * instance, to show times in Pacific Standard Time, pass -7 * 60. */ timeZoneOffsetInMinutes?: number; - - /** - * The date picker text color. - */ - textColor?: string; } >; @@ -108,8 +105,13 @@ export type AndroidNativeProps = Readonly< * The display options. */ display?: Display; + + /** + * The interval at which minutes can be selected. + */ + minuteInterval?: MinuteIntervalAndroid; + onChange?: (event: AndroidEvent, date?: Date) => void; - neutralButtonLabel?: string; } >; diff --git a/src/types.js b/src/types.js index fb1a6156..da9c243e 100644 --- a/src/types.js +++ b/src/types.js @@ -13,6 +13,8 @@ import {ANDROID_MODE, DISPLAY, DAY_OF_WEEK} from './constants'; type IOSMode = 'date' | 'time' | 'datetime' | 'countdown'; type AndroidMode = $Keys; type Display = $Keys; +type MinuteIntervalIOS = ?(1 | 2 | 3 | 4 | 5 | 6 | 10 | 12 | 15 | 20 | 30); +type MinuteIntervalAndroid = ?(1 | 5 | 10 | 15 | 20 | 30); export type Event = SyntheticEvent< $ReadOnly<{| @@ -86,7 +88,7 @@ export type IOSNativeProps = $ReadOnly<{| /** * The interval at which minutes can be selected. */ - minuteInterval?: ?(1 | 2 | 3 | 4 | 5 | 6 | 10 | 12 | 15 | 20 | 30), + minuteInterval?: MinuteIntervalIOS, /** * The date picker mode. @@ -122,6 +124,12 @@ export type AndroidNativeProps = $ReadOnly<{| * The display options. */ display: Display, + + /** + * The interval at which minutes can be selected. + */ + minuteInterval?: MinuteIntervalAndroid, + onChange: (event: AndroidEvent, date?: Date) => void, neutralButtonLabel?: string, |}>; From b42a3959b06ca118e2045864ec723a868fb7b4a0 Mon Sep 17 00:00:00 2001 From: Evgeniy Ferapontov Date: Tue, 5 Nov 2019 23:17:27 +0300 Subject: [PATCH 02/35] minuteInterval android prop pass-through --- src/constants.js | 1 + src/datetimepicker.android.js | 2 ++ src/timepicker.android.js | 9 ++++++++- src/types.js | 1 + 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/constants.js b/src/constants.js index 9f3281d3..a6c7ec6b 100644 --- a/src/constants.js +++ b/src/constants.js @@ -40,3 +40,4 @@ export const DISMISS_ACTION = 'dismissedAction'; export const NEUTRAL_BUTTON_LABEL = 'neutralButtonLabel'; export const NEUTRAL_BUTTON_ACTION = 'neutralButtonAction'; +export const MINUTE_INTERVAL_DEFAULT = 1; diff --git a/src/datetimepicker.android.js b/src/datetimepicker.android.js index 21b6ee1d..71c69353 100644 --- a/src/datetimepicker.android.js +++ b/src/datetimepicker.android.js @@ -39,6 +39,7 @@ export default function RNDateTimePicker(props: AndroidNativeProps) { minimumDate, maximumDate, neutralButtonLabel, + minuteInterval, } = props; let picker; @@ -47,6 +48,7 @@ export default function RNDateTimePicker(props: AndroidNativeProps) { picker = pickers[MODE_TIME].open({ value, display, + minuteInterval, is24Hour, neutralButtonLabel, }); diff --git a/src/timepicker.android.js b/src/timepicker.android.js index 385e656c..c4c8b7e2 100644 --- a/src/timepicker.android.js +++ b/src/timepicker.android.js @@ -7,7 +7,12 @@ * @format * @flow strict-local */ -import {DISPLAY_DEFAULT, TIME_SET_ACTION, DISMISS_ACTION} from './constants'; +import { + DISPLAY_DEFAULT, + TIME_SET_ACTION, + DISMISS_ACTION, + MINUTE_INTERVAL_DEFAULT, +} from './constants'; import {NativeModules} from 'react-native'; import {toMilliseconds} from './utils'; @@ -22,6 +27,7 @@ export default class TimePickerAndroid { * * `is24Hour` (boolean) - If `true`, the picker uses the 24-hour format. If `false`, * the picker shows an AM/PM chooser. If undefined, the default for the current locale * is used. + * * `minuteInterval` (enum(1 | 5 | 10 | 15 | 20 | 30)`) - set the time picker minutes' interval * * `mode` (`enum('clock', 'spinner', 'default')`) - set the time picker mode * - 'clock': Show a time picker in clock mode. * - 'spinner': Show a time picker in spinner mode. @@ -35,6 +41,7 @@ export default class TimePickerAndroid { static async open(options: TimePickerOptions): Promise { toMilliseconds(options, 'value'); options.display = options.display || DISPLAY_DEFAULT; + options.minuteInterval = options.minuteInterval || MINUTE_INTERVAL_DEFAULT; return NativeModules.RNTimePickerAndroid.open(options); } diff --git a/src/types.js b/src/types.js index da9c243e..3c5e13a1 100644 --- a/src/types.js +++ b/src/types.js @@ -141,6 +141,7 @@ export type DatePickerOptions = {| export type TimePickerOptions = {| ...TimeOptions, + minuteInterval?: MinuteIntervalAndroid, display?: Display, |}; From 923e974c5209171b68b65c2c03b5ba124ad0fe5b Mon Sep 17 00:00:00 2001 From: Evgeniy Ferapontov Date: Wed, 6 Nov 2019 00:53:13 +0300 Subject: [PATCH 03/35] implemented custom picker and minuteInterval interface --- .../rndatetimepicker/RNConstants.java | 6 + .../RNDismissableTimePickerDialog.java | 129 ++++++++++++++++-- .../rndatetimepicker/RNMinuteIntervals.java | 15 ++ .../RNTimePickerDialogFragment.java | 55 +++++--- .../RNTimePickerDialogModule.java | 2 + 5 files changed, 175 insertions(+), 32 deletions(-) create mode 100644 android/src/main/java/com/reactcommunity/rndatetimepicker/RNMinuteIntervals.java diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java index b14cd462..ee090fcc 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNConstants.java @@ -5,6 +5,7 @@ public final class RNConstants { public static final String ARG_VALUE = "value"; public static final String ARG_MINDATE = "minimumDate"; public static final String ARG_MAXDATE = "maximumDate"; + public static final String ARG_INTERVAL = "minuteInterval"; public static final String ARG_IS24HOUR = "is24Hour"; public static final String ARG_DISPLAY = "display"; public static final String ARG_NEUTRAL_BUTTON_LABEL = "neutralButtonLabel"; @@ -17,4 +18,9 @@ public final class RNConstants { * Minimum date supported by {@link DatePicker}, 01 Jan 1900 */ public static final long DEFAULT_MIN_DATE = -2208988800001l; + + /** + * Minimum and default time picker minute interval + */ + public static final int DEFAULT_TIME_PICKER_INTERVAL = 1; } diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDismissableTimePickerDialog.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDismissableTimePickerDialog.java index f77deb0f..4267f962 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDismissableTimePickerDialog.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDismissableTimePickerDialog.java @@ -6,6 +6,11 @@ */ package com.reactcommunity.rndatetimepicker; +import static com.reactcommunity.rndatetimepicker.ReflectionHelper.findField; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + import android.app.TimePickerDialog; import android.content.Context; import android.content.res.TypedArray; @@ -14,34 +19,131 @@ import android.widget.TimePicker; import androidx.annotation.Nullable; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; - -import static com.reactcommunity.rndatetimepicker.ReflectionHelper.findField; - /** *

- * Certain versions of Android (Jellybean-KitKat) have a bug where when dismissed, the - * {@link TimePickerDialog} still calls the OnTimeSetListener. This class works around that issue - * by *not* calling super.onStop on KitKat on lower, as that would erroneously call the - * OnTimeSetListener when the dialog is dismissed, or call it twice when "OK" is pressed. + * Certain versions of Android (Jellybean-KitKat) have a bug where when dismissed, the + * {@link TimePickerDialog} still calls the OnTimeSetListener. This class works around that issue + * by *not* calling super.onStop on KitKat on lower, as that would erroneously call the + * OnTimeSetListener when the dialog is dismissed, or call it twice when "OK" is pressed. *

* *

- * See: Issue 34833 + * See: Issue 34833 *

*/ -public class RNDismissableTimePickerDialog extends TimePickerDialog { + +class CustomTimePickerDialog extends TimePickerDialog { + + private int TIME_PICKER_INTERVAL = RNConstants.DEFAULT_TIME_PICKER_INTERVAL; + + private TimePicker mTimePicker; + private final OnTimeSetListener mTimeSetListener; + + public CustomTimePickerDialog( + Context context, + OnTimeSetListener listener, + int hourOfDay, + int minute, + int minuteInterval, + boolean is24HourView) { + super(context, listener, hourOfDay, minute / minuteInterval, is24HourView); + TIME_PICKER_INTERVAL = minuteInterval; + mTimeSetListener = listener; + } + + public CustomTimePickerDialog( + Context context, + int theme, + OnTimeSetListener listener, + int hourOfDay, + int minute, + int minuteInterval, + boolean is24HourView) { + super(context, theme, listener, hourOfDay, minute / minuteInterval, is24HourView); + TIME_PICKER_INTERVAL = minuteInterval; + mTimeSetListener = listener; + } + + @Override + public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { + if (minute % TIME_PICKER_INTERVAL != 0) { + float stepsInMinutes = minute / TIME_PICKER_INTERVAL; + int correctedMinutes = Math.round(stepsInMinutes * TIME_PICKER_INTERVAL); + + view.setCurrentMinute(correctedMinutes); + return; + } + + super.onTimeChanged(view, hourOfDay, minute); + } + + @Override + public void updateTime(int hourOfDay, int minuteOfHour) { + mTimePicker.setCurrentHour(hourOfDay); + mTimePicker.setCurrentMinute(minuteOfHour / TIME_PICKER_INTERVAL); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case BUTTON_POSITIVE: + if (mTimeSetListener != null) { + mTimeSetListener.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(), + mTimePicker.getCurrentMinute() * TIME_PICKER_INTERVAL); + } + break; + case BUTTON_NEGATIVE: + cancel(); + break; + } + } + + /** + * Apply visual style in 'spinner' mode + */ + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + try { + Class pickerInternalClass = Class.forName("com.android.internal.R$id"); + Field timePickerField = pickerInternalClass.getField("timePicker"); + mTimePicker = findViewById(timePickerField.getInt(null)); + + Field minuteField = pickerInternalClass.getField("minute"); + NumberPicker minuteSpinner = mTimePicker.findViewById(minuteField.getInt(null)); + + Field radialPickerField = pickerInternalClass.getField("radial_picker"); + View radialPicker = mTimePicker.findViewById(radialPickerField.getInt(null)); + + if (minuteSpinner != null) { + minuteSpinner.setMinValue(0); + minuteSpinner.setMaxValue((60 / TIME_PICKER_INTERVAL) - 1); + List displayedValues = new ArrayList<>(); + for (int i = 0; i < 60; i += TIME_PICKER_INTERVAL) { + displayedValues.add(String.format("%02d", i)); + } + minuteSpinner.setDisplayedValues(displayedValues.toArray(new String[displayedValues.size()])); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } +} + + +public class RNDismissableTimePickerDialog extends CustomTimePickerDialog { public RNDismissableTimePickerDialog( Context context, @Nullable TimePickerDialog.OnTimeSetListener callback, int hourOfDay, int minute, + int minuteInterval, boolean is24HourView, RNTimePickerDisplay display) { - super(context, callback, hourOfDay, minute, is24HourView); fixSpinner(context, hourOfDay, minute, is24HourView, display); + super(context, callback, hourOfDay, minute, minuteInterval, is24HourView); } public RNDismissableTimePickerDialog( @@ -50,10 +152,11 @@ public RNDismissableTimePickerDialog( @Nullable TimePickerDialog.OnTimeSetListener callback, int hourOfDay, int minute, + int minuteInterval, boolean is24HourView, RNTimePickerDisplay display) { - super(context, theme, callback, hourOfDay, minute, is24HourView); fixSpinner(context, hourOfDay, minute, is24HourView, display); + super(context, theme, callback, hourOfDay, minute, minuteInterval, is24HourView); } @Override diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMinuteIntervals.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMinuteIntervals.java new file mode 100644 index 00000000..364a6ed4 --- /dev/null +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMinuteIntervals.java @@ -0,0 +1,15 @@ +package com.reactcommunity.rndatetimepicker; + +import java.util.List; +import java.util.Arrays; + +/** + * Time picker minutes' intervals. + */ +public final class RNMinuteIntervals { + private final static List mMinuteIntervals = Arrays.asList(1, 5, 10, 15, 20, 30); + + public static boolean isValid(Integer interval){ + return mMinuteIntervals.contains(interval); + } +} \ No newline at end of file diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogFragment.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogFragment.java index d35a396a..28613978 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogFragment.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogFragment.java @@ -64,26 +64,42 @@ static TimePickerDialog getDialog( is24hour = args.getBoolean(RNConstants.ARG_IS24HOUR, DateFormat.is24HourFormat(activityContext)); } + int minuteInterval = RNConstants.DEFAULT_TIME_PICKER_INTERVAL; + if (args != null && RNMinuteIntervals.isValid(args.getInt(RNConstants.ARG_INTERVAL, -1))) { + minuteInterval = args.getInt(RNConstants.ARG_INTERVAL); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - switch (display) { - case CLOCK: - case SPINNER: - String resourceName = display == RNTimePickerDisplay.CLOCK - ? "ClockTimePickerDialog" - : "SpinnerTimePickerDialog"; - return new RNDismissableTimePickerDialog( - activityContext, - activityContext.getResources().getIdentifier( - resourceName, - "style", - activityContext.getPackageName() - ), - onTimeSetListener, - hour, - minute, - is24hour, - display - ); + if (display == RNTimePickerDisplay.CLOCK) { + return new RNDismissableTimePickerDialog( + activityContext, + activityContext.getResources().getIdentifier( + "ClockTimePickerDialog", + "style", + activityContext.getPackageName() + ), + onTimeSetListener, + hour, + minute, + minuteInterval, + is24hour, + display + ); + } else if (display == RNTimePickerDisplay.SPINNER) { + return new RNDismissableTimePickerDialog( + activityContext, + activityContext.getResources().getIdentifier( + "SpinnerTimePickerDialog", + "style", + activityContext.getPackageName() + ), + onTimeSetListener, + hour, + minute, + minuteInterval, + is24hour, + display + ); } } return new RNDismissableTimePickerDialog( @@ -91,6 +107,7 @@ static TimePickerDialog getDialog( onTimeSetListener, hour, minute, + minuteInterval, is24hour, display ); diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogModule.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogModule.java index 41ea573a..813fc199 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogModule.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogModule.java @@ -133,6 +133,8 @@ private Bundle createFragmentArguments(ReadableMap options) { } if (options.hasKey(RNConstants.ARG_NEUTRAL_BUTTON_LABEL) && !options.isNull(RNConstants.ARG_NEUTRAL_BUTTON_LABEL)) { args.putString(RNConstants.ARG_NEUTRAL_BUTTON_LABEL, options.getString(RNConstants.ARG_NEUTRAL_BUTTON_LABEL)); + if (options.hasKey(RNConstants.ARG_INTERVAL) && !options.isNull(RNConstants.ARG_INTERVAL)) { + args.putInt(RNConstants.ARG_INTERVAL, options.getInt(RNConstants.ARG_INTERVAL)); } return args; } From 1564419ca9171ed86714b5292bef8398d774b928 Mon Sep 17 00:00:00 2001 From: Evgeniy Ferapontov Date: Wed, 6 Nov 2019 00:54:50 +0300 Subject: [PATCH 04/35] ignore intellij files --- .gitignore | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitignore b/.gitignore index 777dec08..8b48d3c9 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,14 @@ build/ .gradle local.properties *.iml +/example/android/app/.settings +/example/android/app/.project +/example/android/app/.classpath +/example/android/.settings +/example/android/.project +/android/.settings +/android/.project +/android/.classpath # BUCK buck-out/ From 3565d28c1897f05533df2db9df35942e63bb3d35 Mon Sep 17 00:00:00 2001 From: Evgeniy Ferapontov Date: Wed, 6 Nov 2019 02:16:10 +0300 Subject: [PATCH 05/35] propper onTimeChanged --- .../RNDismissableTimePickerDialog.java | 64 ++++++++++++------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDismissableTimePickerDialog.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDismissableTimePickerDialog.java index 4267f962..6f9fb705 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDismissableTimePickerDialog.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDismissableTimePickerDialog.java @@ -18,6 +18,11 @@ import android.util.AttributeSet; import android.widget.TimePicker; import androidx.annotation.Nullable; +import android.util.Log; +import android.widget.NumberPicker; + +import java.util.ArrayList; +import java.util.List; /** *

@@ -33,10 +38,12 @@ */ class CustomTimePickerDialog extends TimePickerDialog { + private static final String LOG_TAG = CustomTimePickerDialog.class.getSimpleName(); - private int TIME_PICKER_INTERVAL = RNConstants.DEFAULT_TIME_PICKER_INTERVAL; - + private int TIME_PICKER_INTERVAL; private TimePicker mTimePicker; + private RNTimePickerDisplay mDisplay; + private final OnTimeSetListener mTimeSetListener; public CustomTimePickerDialog( @@ -45,10 +52,13 @@ public CustomTimePickerDialog( int hourOfDay, int minute, int minuteInterval, - boolean is24HourView) { + boolean is24HourView, + RNTimePickerDisplay display + ) { super(context, listener, hourOfDay, minute / minuteInterval, is24HourView); TIME_PICKER_INTERVAL = minuteInterval; mTimeSetListener = listener; + mDisplay = display; } public CustomTimePickerDialog( @@ -58,15 +68,21 @@ public CustomTimePickerDialog( int hourOfDay, int minute, int minuteInterval, - boolean is24HourView) { + boolean is24HourView, + RNTimePickerDisplay display + ) { super(context, theme, listener, hourOfDay, minute / minuteInterval, is24HourView); TIME_PICKER_INTERVAL = minuteInterval; mTimeSetListener = listener; + mDisplay = display; } @Override public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { - if (minute % TIME_PICKER_INTERVAL != 0) { + boolean isRadialClock = mDisplay != RNTimePickerDisplay.SPINNER; + Log.d(LOG_TAG, "isRadialClock?:" + isRadialClock); + + if (isRadialClock && minute % TIME_PICKER_INTERVAL != 0) { float stepsInMinutes = minute / TIME_PICKER_INTERVAL; int correctedMinutes = Math.round(stepsInMinutes * TIME_PICKER_INTERVAL); @@ -104,27 +120,27 @@ public void onClick(DialogInterface dialog, int which) { @Override public void onAttachedToWindow() { super.onAttachedToWindow(); + try { Class pickerInternalClass = Class.forName("com.android.internal.R$id"); - Field timePickerField = pickerInternalClass.getField("timePicker"); - mTimePicker = findViewById(timePickerField.getInt(null)); + Log.d(LOG_TAG, "pickerInternalClass:" + pickerInternalClass); + mTimePicker = findViewById(pickerInternalClass.getField("timePicker").getInt(null)); - Field minuteField = pickerInternalClass.getField("minute"); - NumberPicker minuteSpinner = mTimePicker.findViewById(minuteField.getInt(null)); + if (mDisplay != RNTimePickerDisplay.SPINNER) { + return; + } - Field radialPickerField = pickerInternalClass.getField("radial_picker"); - View radialPicker = mTimePicker.findViewById(radialPickerField.getInt(null)); + NumberPicker minuteSpinner = mTimePicker.findViewById(pickerInternalClass.getField("minute").getInt(null)); - if (minuteSpinner != null) { - minuteSpinner.setMinValue(0); - minuteSpinner.setMaxValue((60 / TIME_PICKER_INTERVAL) - 1); - List displayedValues = new ArrayList<>(); - for (int i = 0; i < 60; i += TIME_PICKER_INTERVAL) { - displayedValues.add(String.format("%02d", i)); - } - minuteSpinner.setDisplayedValues(displayedValues.toArray(new String[displayedValues.size()])); + minuteSpinner.setMinValue(0); + minuteSpinner.setMaxValue((60 / TIME_PICKER_INTERVAL) - 1); + + List displayedValues = new ArrayList<>(); + for (int i = 0; i < 60; i += TIME_PICKER_INTERVAL) { + displayedValues.add(String.format("%02d", i)); } + minuteSpinner.setDisplayedValues(displayedValues.toArray(new String[displayedValues.size()])); } catch (Exception e) { e.printStackTrace(); } @@ -141,9 +157,10 @@ public RNDismissableTimePickerDialog( int minute, int minuteInterval, boolean is24HourView, - RNTimePickerDisplay display) { + RNTimePickerDisplay display + ) { fixSpinner(context, hourOfDay, minute, is24HourView, display); - super(context, callback, hourOfDay, minute, minuteInterval, is24HourView); + super(context, callback, hourOfDay, minute, minuteInterval, is24HourView, display); } public RNDismissableTimePickerDialog( @@ -154,9 +171,10 @@ public RNDismissableTimePickerDialog( int minute, int minuteInterval, boolean is24HourView, - RNTimePickerDisplay display) { + RNTimePickerDisplay display + ) { fixSpinner(context, hourOfDay, minute, is24HourView, display); - super(context, theme, callback, hourOfDay, minute, minuteInterval, is24HourView); + super(context, theme, callback, hourOfDay, minute, minuteInterval, is24HourView, display); } @Override From 6cebd3772e1282d41856c2cf08ca1a42909f9966 Mon Sep 17 00:00:00 2001 From: Evgeniy Ferapontov Date: Wed, 6 Nov 2019 20:03:28 +0300 Subject: [PATCH 06/35] refactored android picker * fixed incorrect time value setting * force set spinner mode if setting interval more than 5 min because of incompatibility * get rid of reflection api --- .../RNDismissableTimePickerDialog.java | 152 ++++++++++-------- .../rndatetimepicker/RNMinuteIntervals.java | 14 +- .../RNTimePickerDialogFragment.java | 36 +++-- .../RNTimePickerDialogModule.java | 8 +- .../rndatetimepicker/RNTimePickerDisplay.java | 3 +- example/App.js | 34 +++- src/timepicker.android.js | 9 +- src/types.js | 1 + 8 files changed, 167 insertions(+), 90 deletions(-) diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDismissableTimePickerDialog.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDismissableTimePickerDialog.java index 6f9fb705..800f65bf 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDismissableTimePickerDialog.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNDismissableTimePickerDialog.java @@ -1,8 +1,9 @@ /** * Copyright (c) Facebook, Inc. and its affiliates. - * + *

* This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. + *

*/ package com.reactcommunity.rndatetimepicker; @@ -13,16 +14,20 @@ import android.app.TimePickerDialog; import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Resources; import android.content.res.TypedArray; import android.os.Build; import android.util.AttributeSet; -import android.widget.TimePicker; -import androidx.annotation.Nullable; import android.util.Log; import android.widget.NumberPicker; +import android.widget.TimePicker; + +import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.Locale; /** *

@@ -40,63 +45,76 @@ class CustomTimePickerDialog extends TimePickerDialog { private static final String LOG_TAG = CustomTimePickerDialog.class.getSimpleName(); - private int TIME_PICKER_INTERVAL; private TimePicker mTimePicker; + private int mTimePickerInterval; private RNTimePickerDisplay mDisplay; - private final OnTimeSetListener mTimeSetListener; public CustomTimePickerDialog( - Context context, - OnTimeSetListener listener, - int hourOfDay, - int minute, - int minuteInterval, - boolean is24HourView, - RNTimePickerDisplay display + Context context, + OnTimeSetListener listener, + int hourOfDay, + int minute, + int minuteInterval, + boolean is24HourView, + RNTimePickerDisplay display ) { - super(context, listener, hourOfDay, minute / minuteInterval, is24HourView); - TIME_PICKER_INTERVAL = minuteInterval; + super(context, listener, hourOfDay, minute, is24HourView); + mTimePickerInterval = minuteInterval; mTimeSetListener = listener; mDisplay = display; } public CustomTimePickerDialog( - Context context, - int theme, - OnTimeSetListener listener, - int hourOfDay, - int minute, - int minuteInterval, - boolean is24HourView, - RNTimePickerDisplay display + Context context, + int theme, + OnTimeSetListener listener, + int hourOfDay, + int minute, + int minuteInterval, + boolean is24HourView, + RNTimePickerDisplay display ) { - super(context, theme, listener, hourOfDay, minute / minuteInterval, is24HourView); - TIME_PICKER_INTERVAL = minuteInterval; + super(context, theme, listener, hourOfDay, minute, is24HourView); + mTimePickerInterval = minuteInterval; mTimeSetListener = listener; mDisplay = display; } + private int getRealMinutes(int minute) { + if (mDisplay == RNTimePickerDisplay.SPINNER) { + return minute * mTimePickerInterval; + } + + return minute; + } + + private int snapMinutesToInterval(int realMinutes) { + float stepsInMinutes = (float) realMinutes / (float) mTimePickerInterval; + + if (mDisplay == RNTimePickerDisplay.SPINNER) { + return Math.round(stepsInMinutes); + } + + return Math.round(stepsInMinutes) * mTimePickerInterval; + } + @Override public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { - boolean isRadialClock = mDisplay != RNTimePickerDisplay.SPINNER; - Log.d(LOG_TAG, "isRadialClock?:" + isRadialClock); - - if (isRadialClock && minute % TIME_PICKER_INTERVAL != 0) { - float stepsInMinutes = minute / TIME_PICKER_INTERVAL; - int correctedMinutes = Math.round(stepsInMinutes * TIME_PICKER_INTERVAL); + int realMinutes = getRealMinutes(minute); - view.setCurrentMinute(correctedMinutes); + if (realMinutes % mTimePickerInterval != 0) { + view.setCurrentMinute(snapMinutesToInterval(realMinutes)); return; } - super.onTimeChanged(view, hourOfDay, minute); + super.onTimeChanged(view, hourOfDay, realMinutes); } @Override public void updateTime(int hourOfDay, int minuteOfHour) { mTimePicker.setCurrentHour(hourOfDay); - mTimePicker.setCurrentMinute(minuteOfHour / TIME_PICKER_INTERVAL); + mTimePicker.setCurrentMinute(snapMinutesToInterval(minuteOfHour)); } @Override @@ -104,8 +122,11 @@ public void onClick(DialogInterface dialog, int which) { switch (which) { case BUTTON_POSITIVE: if (mTimeSetListener != null) { - mTimeSetListener.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(), - mTimePicker.getCurrentMinute() * TIME_PICKER_INTERVAL); + mTimeSetListener.onTimeSet( + mTimePicker, + mTimePicker.getCurrentHour(), + getRealMinutes(mTimePicker.getCurrentMinute()) + ); } break; case BUTTON_NEGATIVE: @@ -116,31 +137,36 @@ public void onClick(DialogInterface dialog, int which) { /** * Apply visual style in 'spinner' mode + * Adjust minutes to correspond selected interval */ @Override public void onAttachedToWindow() { super.onAttachedToWindow(); try { - Class pickerInternalClass = Class.forName("com.android.internal.R$id"); - Log.d(LOG_TAG, "pickerInternalClass:" + pickerInternalClass); - mTimePicker = findViewById(pickerInternalClass.getField("timePicker").getInt(null)); + int timePickerId = Resources.getSystem() + .getIdentifier("timePicker", "id", "android"); - if (mDisplay != RNTimePickerDisplay.SPINNER) { - return; - } + mTimePicker = this.findViewById(timePickerId); - NumberPicker minuteSpinner = mTimePicker.findViewById(pickerInternalClass.getField("minute").getInt(null)); + if (mDisplay == RNTimePickerDisplay.SPINNER) { + int minutePickerId = Resources.getSystem() + .getIdentifier("minute", "id", "android"); + NumberPicker minutePicker = this.findViewById(minutePickerId); - minuteSpinner.setMinValue(0); - minuteSpinner.setMaxValue((60 / TIME_PICKER_INTERVAL) - 1); + minutePicker.setMinValue(0); + minutePicker.setMaxValue((60 / mTimePickerInterval) - 1); + + List displayedValues = new ArrayList<>(); + for (int i = 0; i < 60; i += mTimePickerInterval) { + displayedValues.add(String.format(Locale.US, "%02d", i)); + } - List displayedValues = new ArrayList<>(); - for (int i = 0; i < 60; i += TIME_PICKER_INTERVAL) { - displayedValues.add(String.format("%02d", i)); + minutePicker.setDisplayedValues(displayedValues.toArray(new String[displayedValues.size()])); } - minuteSpinner.setDisplayedValues(displayedValues.toArray(new String[displayedValues.size()])); + int currentMinute = mTimePicker.getCurrentMinute(); + mTimePicker.setCurrentMinute(snapMinutesToInterval(currentMinute)); } catch (Exception e) { e.printStackTrace(); } @@ -151,27 +177,27 @@ public void onAttachedToWindow() { public class RNDismissableTimePickerDialog extends CustomTimePickerDialog { public RNDismissableTimePickerDialog( - Context context, - @Nullable TimePickerDialog.OnTimeSetListener callback, - int hourOfDay, - int minute, - int minuteInterval, - boolean is24HourView, - RNTimePickerDisplay display + Context context, + @Nullable TimePickerDialog.OnTimeSetListener callback, + int hourOfDay, + int minute, + int minuteInterval, + boolean is24HourView, + RNTimePickerDisplay display ) { fixSpinner(context, hourOfDay, minute, is24HourView, display); super(context, callback, hourOfDay, minute, minuteInterval, is24HourView, display); } public RNDismissableTimePickerDialog( - Context context, - int theme, - @Nullable TimePickerDialog.OnTimeSetListener callback, - int hourOfDay, - int minute, - int minuteInterval, - boolean is24HourView, - RNTimePickerDisplay display + Context context, + int theme, + @Nullable TimePickerDialog.OnTimeSetListener callback, + int hourOfDay, + int minute, + int minuteInterval, + boolean is24HourView, + RNTimePickerDisplay display ) { fixSpinner(context, hourOfDay, minute, is24HourView, display); super(context, theme, callback, hourOfDay, minute, minuteInterval, is24HourView, display); diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMinuteIntervals.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMinuteIntervals.java index 364a6ed4..ed365380 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMinuteIntervals.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNMinuteIntervals.java @@ -1,15 +1,19 @@ package com.reactcommunity.rndatetimepicker; -import java.util.List; import java.util.Arrays; /** * Time picker minutes' intervals. + * NOTE: only compatible with {@link RNTimePickerDisplay.SPINNER} */ public final class RNMinuteIntervals { - private final static List mMinuteIntervals = Arrays.asList(1, 5, 10, 15, 20, 30); + private final static Integer[] MinuteIntervals = new Integer[]{1, 5, 10, 15, 20, 30}; - public static boolean isValid(Integer interval){ - return mMinuteIntervals.contains(interval); - } + public static boolean isValid(Integer interval) { + return Arrays.asList(MinuteIntervals).contains(interval); + } + + public static boolean isRadialPickerCompatible(Integer interval) { + return MinuteIntervals[0].equals(interval) || MinuteIntervals[1].equals(interval); + } } \ No newline at end of file diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogFragment.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogFragment.java index 28613978..67974e10 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogFragment.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogFragment.java @@ -1,8 +1,9 @@ /** * Copyright (c) Facebook, Inc. and its affiliates. - * + *

* This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. + *

*/ package com.reactcommunity.rndatetimepicker; @@ -16,9 +17,10 @@ import android.content.DialogInterface.OnClickListener; import android.os.Build; import android.os.Bundle; +import android.text.format.DateFormat; + import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; -import android.text.format.DateFormat; import java.util.Locale; @@ -55,20 +57,24 @@ static TimePickerDialog getDialog( final int minute = date.minute(); boolean is24hour = DateFormat.is24HourFormat(activityContext); + int minuteInterval = RNConstants.DEFAULT_TIME_PICKER_INTERVAL; + if (args != null && RNMinuteIntervals.isValid(args.getInt(RNConstants.ARG_INTERVAL, -1))) { + minuteInterval = args.getInt(RNConstants.ARG_INTERVAL); + } + RNTimePickerDisplay display = RNTimePickerDisplay.DEFAULT; if (args != null && args.getString(RNConstants.ARG_DISPLAY, null) != null) { - display = RNTimePickerDisplay.valueOf(args.getString(RNConstants.ARG_DISPLAY).toUpperCase(Locale.US)); + if (RNMinuteIntervals.isRadialPickerCompatible(minuteInterval)) { + display = RNTimePickerDisplay.valueOf(args.getString(RNConstants.ARG_DISPLAY).toUpperCase(Locale.US)); + } else { + display = RNTimePickerDisplay.SPINNER; + } } if (args != null) { is24hour = args.getBoolean(RNConstants.ARG_IS24HOUR, DateFormat.is24HourFormat(activityContext)); } - int minuteInterval = RNConstants.DEFAULT_TIME_PICKER_INTERVAL; - if (args != null && RNMinuteIntervals.isValid(args.getInt(RNConstants.ARG_INTERVAL, -1))) { - minuteInterval = args.getInt(RNConstants.ARG_INTERVAL); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (display == RNTimePickerDisplay.CLOCK) { return new RNDismissableTimePickerDialog( @@ -103,13 +109,13 @@ static TimePickerDialog getDialog( } } return new RNDismissableTimePickerDialog( - activityContext, - onTimeSetListener, - hour, - minute, - minuteInterval, - is24hour, - display + activityContext, + onTimeSetListener, + hour, + minute, + minuteInterval, + is24hour, + display ); } diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogModule.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogModule.java index 813fc199..84f1dcbf 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogModule.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDialogModule.java @@ -1,8 +1,9 @@ /** * Copyright (c) Facebook, Inc. and its affiliates. - * + *

* This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. + *

*/ package com.reactcommunity.rndatetimepicker; @@ -17,6 +18,7 @@ import android.content.DialogInterface.OnClickListener; import android.os.Bundle; import android.widget.TimePicker; + import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; @@ -87,8 +89,8 @@ public void open(@Nullable final ReadableMap options, Promise promise) { FragmentActivity activity = (FragmentActivity) getCurrentActivity(); if (activity == null) { promise.reject( - RNConstants.ERROR_NO_ACTIVITY, - "Tried to open a TimePicker dialog while not attached to an Activity"); + RNConstants.ERROR_NO_ACTIVITY, + "Tried to open a TimePicker dialog while not attached to an Activity"); return; } // We want to support both android.app.Activity and the pre-Honeycomb FragmentActivity diff --git a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDisplay.java b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDisplay.java index 32df4b88..eb9572c5 100644 --- a/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDisplay.java +++ b/android/src/main/java/com/reactcommunity/rndatetimepicker/RNTimePickerDisplay.java @@ -1,8 +1,9 @@ /** * Copyright (c) Facebook, Inc. and its affiliates. - * + *

* This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. + *

*/ package com.reactcommunity.rndatetimepicker; diff --git a/example/App.js b/example/App.js index 7abd59ad..da7cae45 100644 --- a/example/App.js +++ b/example/App.js @@ -34,6 +34,7 @@ export const App = () => { const [show, setShow] = useState(false); const [color, setColor] = useState(); const [display, setDisplay] = useState('default'); + const [interval, setMinInterval] = useState(undefined); // Windows-specific const [maxDate, setMinDate] = useState(new Date('2021')); @@ -80,6 +81,21 @@ export const App = () => { setDisplay('spinner'); }; + const showTimepickerClockModeWithInterval = () => { + showMode('time'); + setMinInterval(5); + setDisplay('clock'); + }; + + const showTimepickerSpinnerWithInterval = () => { + showMode('time'); + setMinInterval(5); + setDisplay('spinner'); + }; + const currentDateTime = moment + .utc(date) + .format(mode === 'time' ? 'HH:mm' : 'MM/DD/YYYY'); + const isDarkMode = useColorScheme() === 'dark'; const backgroundStyle = { @@ -150,10 +166,23 @@ export const App = () => { title="Show time picker spinner!" /> + +