Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add minute interval on Android #177

Merged
merged 35 commits into from
Jun 30, 2020
Merged
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9b71ba9
minuteInterval types
evgeniy-ferapontov Nov 5, 2019
b42a395
minuteInterval android prop pass-through
evgeniy-ferapontov Nov 5, 2019
923e974
implemented custom picker and minuteInterval interface
evgeniy-ferapontov Nov 5, 2019
1564419
ignore intellij files
evgeniy-ferapontov Nov 5, 2019
3565d28
propper onTimeChanged
evgeniy-ferapontov Nov 5, 2019
6cebd37
refactored android picker
evgeniy-ferapontov Nov 6, 2019
8a49862
updated readme
evgeniy-ferapontov Nov 11, 2019
068ee38
added dependencies repos, upgraded gradle version
evgeniy-ferapontov Dec 12, 2019
605db40
fixed incorrectly displayed minutes when interval is set
evgeniy-ferapontov Dec 12, 2019
9239bc2
indentation and tests
evgeniy-ferapontov Dec 12, 2019
38e3d04
updated readme
evgeniy-ferapontov Dec 12, 2019
dc72274
added docs + correct handling in textinput mode
evgeniy-ferapontov Dec 13, 2019
a1e2acd
fixed get resources in RNDismissableTimePickerDialog
BenderBRodrigez May 14, 2020
0261535
separated CustomTimePickerDialog
BenderBRodrigez May 18, 2020
7d8882f
fix detox prettier errors
luancurti May 21, 2020
9212f15
make minute interval work with timepicker default
luancurti May 21, 2020
6af788a
fix add text color again o ts types
luancurti May 21, 2020
8aaa1de
remove note force timepicker mode spinner
luancurti May 21, 2020
4dbdcd6
fix detox tests
luancurti May 21, 2020
d798fff
remove minute interval options mutation
luancurti Jun 6, 2020
a4791a1
merge minute interval android/ios type
luancurti Jun 6, 2020
b8b6d0f
run attached window custom code only minute interval is set
luancurti Jun 6, 2020
e14ef50
refactor onClick and onAttachedWindow
luancurti Jun 6, 2020
59d0d4f
wait datepicker to be visible with timeout
luancurti Jun 11, 2020
de3ae1a
remove/undo some small changes
vonovak Jun 17, 2020
a136eac
reactor minuteinterval
vonovak Jun 25, 2020
850a288
fix time picker on older androids
vonovak Jun 26, 2020
e9e9603
e2e tests wip
vonovak Jun 28, 2020
23708f4
add e2e tests
vonovak Jun 28, 2020
b60cf3e
rename TimePickerDialog
vonovak Jun 28, 2020
b294f39
add forgotten folder
vonovak Jun 28, 2020
36030f7
make more space
vonovak Jun 28, 2020
53f9e56
fix minuteInterval assignment
vonovak Jun 28, 2020
3f1a980
minor polish
vonovak Jun 28, 2020
c6def15
fix isValidMinuteInterval
vonovak Jun 28, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -29,6 +29,9 @@ DerivedData
*.xcuserstate
project.xcworkspace

#Detox
#
artifacts

# Android/IntelliJ
#
@@ -37,6 +40,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/
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ React Native date & time picker component for iOS and Android
- [`locale` (`optional`, `iOS only`)](#locale-optional-ios-only)
- [`is24Hour` (`optional`, `Android only`)](#is24hour-optional-android-only)
- [`neutralButtonLabel` (`optional`, `Android only`)](#neutralbuttonlabel-optional-android-only)
- [`minuteInterval` (`optional`, `iOS only`)](#minuteinterval-optional-ios-only)
- [`minuteInterval` (`optional`)](#minuteinterval-optional-ios-only)
- [`style` (`optional`, `iOS only`)](#style-optional-ios-only)
- [Migration from the older components](#migration-from-the-older-components)
- [DatePickerIOS](#datepickerios)
@@ -312,7 +312,7 @@ Pressing button can be observed in onChange handler as `event.type === 'neutralB
<RNDateTimePicker neutralButtonLabel="clear" />
```

#### `minuteInterval` (`optional`, `iOS only`)
#### `minuteInterval` (`optional`)

The interval at which minutes can be selected.
Possible values are: `1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30`
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
package com.reactcommunity.rndatetimepicker;

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.Handler;
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 (mDisplay == RNTimePickerDisplay.SPINNER) {
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)!");
final EditText textInput = (EditText) view.findFocus();

// 'correction' callback
runnable = new Runnable() {
@Override
public void run() {
if (pickerIsInTextInputMode()) {
// set valid minutes && move caret to the end of input
view.setCurrentHour(hourOfDay);
view.setCurrentMinute(correctedMinutes);
textInput.setSelection(textInput.getText().length());
} else {
view.setCurrentHour(hourOfDay);
// we need to set minutes to 0 for this to work on older android devices
view.setCurrentMinute(0);
view.setCurrentMinute(correctedMinutes);
}
}
};

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) {
if (mTimePicker != null && which == BUTTON_POSITIVE && timePickerHasCustomMinuteInterval()) {
final int hours = mTimePicker.getCurrentHour();

final int realMinutes = getRealMinutes();
int validMinutes = isSpinner() ? realMinutes : snapRealMinutesToInterval(realMinutes);

if (mTimeSetListener != null) {
mTimeSetListener.onTimeSet(mTimePicker, hours, validMinutes);
}
} 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();

if (timePickerHasCustomMinuteInterval()) {
setupPickerDialog();
}
}

private void setupPickerDialog() {
int timePickerId = mContext.getResources().getIdentifier("timePicker", "id", "android");
mTimePicker = this.findViewById(timePickerId);

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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Loading