diff --git a/README.md b/README.md index d4017ff..b659d61 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ export default () => { | `confirmText` | Modal only: Confirm button text. | | `cancelText` | Modal only: Cancel button text. | | `theme` | Modal only: The theme of the modal. `"light"`, `"dark"`, `"auto"`. Defaults to `"auto"`. | +| `onStateChange` | Spinner state change handler. Triggered on changes between "idle" and "spinning" state (Android inline only) | ## Additional android styling @@ -226,6 +227,17 @@ If you have enabled + +``` + ## Two different Android variants On Android there are two design variants to choose from: diff --git a/android/src/main/java/com/henninghall/date_picker/Emitter.java b/android/src/main/java/com/henninghall/date_picker/Emitter.java index 845f200..c78fff5 100644 --- a/android/src/main/java/com/henninghall/date_picker/Emitter.java +++ b/android/src/main/java/com/henninghall/date_picker/Emitter.java @@ -6,6 +6,7 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.uimanager.events.RCTEventEmitter; +import com.henninghall.date_picker.ui.SpinnerState; import java.util.Calendar; @@ -19,6 +20,18 @@ public class Emitter { return DatePickerPackage.context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); } + public static void onSpinnerStateChange(SpinnerState spinnerState, String id, View view) { + WritableMap event = Arguments.createMap(); + event.putString("spinnerState", spinnerState.toString()); + event.putString("id", id); + if(BuildConfig.IS_NEW_ARCHITECTURE_ENABLED){ + deviceEventEmitter().emit("spinnerStateChange", event); + } + else { + eventEmitter().receiveEvent(view.getId(),"spinnerStateChange",event); + } + } + public static void onDateChange(Calendar date, String displayValueString, String id, View view) { WritableMap event = Arguments.createMap(); String dateString = Utils.dateToIso(date); diff --git a/android/src/main/java/com/henninghall/date_picker/PickerView.java b/android/src/main/java/com/henninghall/date_picker/PickerView.java index 8af607a..50ea154 100644 --- a/android/src/main/java/com/henninghall/date_picker/PickerView.java +++ b/android/src/main/java/com/henninghall/date_picker/PickerView.java @@ -6,6 +6,7 @@ import android.widget.LinearLayout; import android.widget.RelativeLayout; import com.facebook.react.bridge.Dynamic; +import com.henninghall.date_picker.models.Variant; import com.henninghall.date_picker.props.DividerHeightProp; import com.henninghall.date_picker.props.Is24hourSourceProp; import com.henninghall.date_picker.props.MaximumDateProp; @@ -19,6 +20,7 @@ import com.henninghall.date_picker.props.HeightProp; import com.henninghall.date_picker.props.LocaleProp; import com.henninghall.date_picker.props.ModeProp; import com.henninghall.date_picker.props.TextColorProp; +import com.henninghall.date_picker.ui.SpinnerStateListener; import com.henninghall.date_picker.ui.UIManager; import com.henninghall.date_picker.ui.Accessibility; @@ -107,6 +109,10 @@ public class PickerView extends RelativeLayout { uiManager.scroll(wheelIndex, scrollTimes); } + public void addSpinnerStateListener(SpinnerStateListener listener){ + uiManager.addStateListener(listener); + } + public String getDate() { return state.derived.getLastDate(); } @@ -132,4 +138,7 @@ public class PickerView extends RelativeLayout { } + public Variant getVariant() { + return state.getVariant(); + } } diff --git a/android/src/main/java/com/henninghall/date_picker/pickers/AndroidNative.java b/android/src/main/java/com/henninghall/date_picker/pickers/AndroidNative.java index 91cc70c..a3550a3 100644 --- a/android/src/main/java/com/henninghall/date_picker/pickers/AndroidNative.java +++ b/android/src/main/java/com/henninghall/date_picker/pickers/AndroidNative.java @@ -17,16 +17,16 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import static android.widget.NumberPicker.OnScrollListener.SCROLL_STATE_FLING; import static android.widget.NumberPicker.OnScrollListener.SCROLL_STATE_IDLE; public class AndroidNative extends NumberPicker implements Picker { private Picker.OnValueChangeListener onValueChangedListener; - private int state = SCROLL_STATE_IDLE; + private int internalSpinState = SCROLL_STATE_IDLE; private OnValueChangeListenerInScrolling listenerInScrolling; private boolean isAnimating; private final Handler handler = new Handler(); + private boolean spinning; public AndroidNative(Context context) { super(context); @@ -102,7 +102,7 @@ public class AndroidNative extends NumberPicker implements Picker { @Override public boolean isSpinning() { - return state == SCROLL_STATE_FLING || isAnimating; + return spinning || isAnimating; } @Override @@ -116,10 +116,13 @@ public class AndroidNative extends NumberPicker implements Picker { int timeBetweenScrollsMs = 100; int willStopScrollingInMs = timeBetweenScrollsMs * moves; isAnimating = true; + onValueChangedListener.onSpinnerStateChange(); + handler.postDelayed(new Runnable() { @Override public void run() { isAnimating = false; + onValueChangedListener.onSpinnerStateChange(); } }, willStopScrollingInMs); @@ -184,7 +187,7 @@ public class AndroidNative extends NumberPicker implements Picker { // onValueChange is triggered also during scrolling. Since we don't want // to send event during scrolling we make sure wheel is still. This particular // case happens when wheel is tapped, not scrolled. - if(state == SCROLL_STATE_IDLE) { + if(internalSpinState == SCROLL_STATE_IDLE) { sendEventIn500ms(); } } @@ -194,13 +197,17 @@ public class AndroidNative extends NumberPicker implements Picker { @Override public void onScrollStateChange(NumberPicker numberPicker, int nextState) { sendEventIfStopped(nextState); - state = nextState; + internalSpinState = nextState; + if (nextState != SCROLL_STATE_IDLE){ + spinning = true; + onValueChangedListener.onSpinnerStateChange(); + } } }); } private void sendEventIfStopped(int nextState){ - boolean stoppedScrolling = state != SCROLL_STATE_IDLE && nextState == SCROLL_STATE_IDLE; + boolean stoppedScrolling = internalSpinState != SCROLL_STATE_IDLE && nextState == SCROLL_STATE_IDLE; if (stoppedScrolling) { sendEventIn500ms(); } @@ -210,7 +217,9 @@ public class AndroidNative extends NumberPicker implements Picker { handler.postDelayed(new Runnable() { @Override public void run() { + spinning = false; onValueChangedListener.onValueChange(); + onValueChangedListener.onSpinnerStateChange(); } // the delay make sure the wheel has stopped before sending the value change event }, 500); diff --git a/android/src/main/java/com/henninghall/date_picker/pickers/IosClone.java b/android/src/main/java/com/henninghall/date_picker/pickers/IosClone.java index 7601e00..aae68ae 100644 --- a/android/src/main/java/com/henninghall/date_picker/pickers/IosClone.java +++ b/android/src/main/java/com/henninghall/date_picker/pickers/IosClone.java @@ -16,6 +16,7 @@ import com.henninghall.date_picker.ui.Accessibility; public class IosClone extends NumberPickerView implements Picker { private Picker.OnValueChangeListenerInScrolling mOnValueChangeListenerInScrolling; + private Picker.OnValueChangeListener onValueChangedListener; public IosClone(Context context) { super(context); @@ -54,6 +55,15 @@ public class IosClone extends NumberPickerView implements Picker { } } }); + + super.setOnScrollListener(new OnScrollListener() { + @Override + public void onScrollStateChange(NumberPickerView view, int scrollState) { + if (onValueChangedListener != null) { + onValueChangedListener.onSpinnerStateChange(); + } + } + }); } @Override @@ -71,6 +81,8 @@ public class IosClone extends NumberPickerView implements Picker { @Override public void setOnValueChangedListener(final Picker.OnValueChangeListener listener) { + this.onValueChangedListener = listener; + super.setOnValueChangedListener(new NumberPickerView.OnValueChangeListener() { @Override public void onValueChange(NumberPickerView picker, int oldVal, int newVal) { diff --git a/android/src/main/java/com/henninghall/date_picker/pickers/Picker.java b/android/src/main/java/com/henninghall/date_picker/pickers/Picker.java index b438dcf..26089d6 100644 --- a/android/src/main/java/com/henninghall/date_picker/pickers/Picker.java +++ b/android/src/main/java/com/henninghall/date_picker/pickers/Picker.java @@ -32,5 +32,6 @@ public interface Picker { interface OnValueChangeListener { void onValueChange(); + void onSpinnerStateChange(); } } diff --git a/android/src/main/java/com/henninghall/date_picker/ui/SpinnerState.java b/android/src/main/java/com/henninghall/date_picker/ui/SpinnerState.java new file mode 100644 index 0000000..3e44d79 --- /dev/null +++ b/android/src/main/java/com/henninghall/date_picker/ui/SpinnerState.java @@ -0,0 +1,6 @@ +package com.henninghall.date_picker.ui; + +public enum SpinnerState { + idle, + spinning +} diff --git a/android/src/main/java/com/henninghall/date_picker/ui/SpinnerStateListener.java b/android/src/main/java/com/henninghall/date_picker/ui/SpinnerStateListener.java new file mode 100644 index 0000000..46e323d --- /dev/null +++ b/android/src/main/java/com/henninghall/date_picker/ui/SpinnerStateListener.java @@ -0,0 +1,5 @@ +package com.henninghall.date_picker.ui; + +public interface SpinnerStateListener { + void onChange(SpinnerState event); +} diff --git a/android/src/main/java/com/henninghall/date_picker/ui/UIManager.java b/android/src/main/java/com/henninghall/date_picker/ui/UIManager.java index 01a6ea5..930594f 100644 --- a/android/src/main/java/com/henninghall/date_picker/ui/UIManager.java +++ b/android/src/main/java/com/henninghall/date_picker/ui/UIManager.java @@ -21,6 +21,7 @@ public class UIManager { private Wheels wheels; private FadingOverlay fadingOverlay; private WheelScroller wheelScroller = new WheelScroller(); + private WheelChangeListenerImpl onWheelChangeListener; public UIManager(State state, View rootView){ this.state = state; @@ -78,10 +79,14 @@ public class UIManager { } private void addOnChangeListener(){ - WheelChangeListener onWheelChangeListener = new WheelChangeListenerImpl(wheels, state, this, rootView); + onWheelChangeListener = new WheelChangeListenerImpl(wheels, state, this, rootView); wheels.applyOnAll(new AddOnChangeListener(onWheelChangeListener)); } + public void addStateListener(SpinnerStateListener listener){ + onWheelChangeListener.addStateListener(listener); + } + public void updateDividerHeight() { wheels.updateDividerHeight(); } diff --git a/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListener.java b/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListener.java index c796347..0a6e914 100644 --- a/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListener.java +++ b/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListener.java @@ -5,6 +5,8 @@ import com.henninghall.date_picker.wheels.Wheel; public interface WheelChangeListener { void onChange(Wheel picker); + + void onStateChange(Wheel picker); } diff --git a/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListenerImpl.java b/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListenerImpl.java index 9b9c3ed..f6a7d72 100644 --- a/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListenerImpl.java +++ b/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListenerImpl.java @@ -9,6 +9,8 @@ import com.henninghall.date_picker.wheels.Wheel; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; +import java.util.HashSet; +import java.util.Set; import java.util.TimeZone; public class WheelChangeListenerImpl implements WheelChangeListener { @@ -17,6 +19,8 @@ public class WheelChangeListenerImpl implements WheelChangeListener { private final State state; private final UIManager uiManager; private final View rootView; + private SpinnerState lastEmittedSpinnerState; + private Set listeners = new HashSet<>(); WheelChangeListenerImpl(Wheels wheels, State state, UIManager uiManager, View rootView) { this.wheels = wheels; @@ -65,6 +69,15 @@ public class WheelChangeListenerImpl implements WheelChangeListener { Emitter.onDateChange(selectedDate, displayData, state.getId(), rootView); } + @Override + public void onStateChange(Wheel picker) { + SpinnerState event = wheels.hasSpinningWheel() ? SpinnerState.spinning : SpinnerState.idle; + if(event.equals(lastEmittedSpinnerState)) return; + lastEmittedSpinnerState = event; + Emitter.onSpinnerStateChange(event, state.getId(), rootView); + for (SpinnerStateListener l: listeners) l.onChange(event); + } + // Example: Jan 1 returns true, April 31 returns false. private boolean dateExists(){ SimpleDateFormat dateFormat = getDateFormat(); @@ -111,4 +124,7 @@ public class WheelChangeListenerImpl implements WheelChangeListener { return null; } + public void addStateListener(SpinnerStateListener listener) { + listeners.add(listener); + } } diff --git a/android/src/main/java/com/henninghall/date_picker/wheelFunctions/AddOnChangeListener.java b/android/src/main/java/com/henninghall/date_picker/wheelFunctions/AddOnChangeListener.java index af9e441..7706062 100644 --- a/android/src/main/java/com/henninghall/date_picker/wheelFunctions/AddOnChangeListener.java +++ b/android/src/main/java/com/henninghall/date_picker/wheelFunctions/AddOnChangeListener.java @@ -21,6 +21,11 @@ public class AddOnChangeListener implements WheelFunction { public void onValueChange() { onChangeListener.onChange(wheel); } + + @Override + public void onSpinnerStateChange() { + onChangeListener.onStateChange(wheel); + } }); } } diff --git a/android/src/newarch/java/com/henninghall/date_picker/DatePickerModule.java b/android/src/newarch/java/com/henninghall/date_picker/DatePickerModule.java index 36ea214..7f243c8 100644 --- a/android/src/newarch/java/com/henninghall/date_picker/DatePickerModule.java +++ b/android/src/newarch/java/com/henninghall/date_picker/DatePickerModule.java @@ -1,9 +1,7 @@ package com.henninghall.date_picker; - import android.app.AlertDialog; import android.content.DialogInterface; -import android.telecom.Call; import android.view.View; import android.widget.LinearLayout; import android.widget.RelativeLayout; @@ -12,12 +10,13 @@ import androidx.annotation.NonNull; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.Dynamic; -import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.henninghall.date_picker.models.Variant; +import com.henninghall.date_picker.ui.SpinnerState; +import com.henninghall.date_picker.ui.SpinnerStateListener; import net.time4j.android.ApplicationStarter; @@ -129,9 +128,23 @@ public class DatePickerModule extends NativeRNDatePickerSpec { } } picker.update(); + + boolean canDisableButtonsWithoutCrash = picker.getVariant() == Variant.nativeAndroid; + if(canDisableButtonsWithoutCrash){ + picker.addSpinnerStateListener(new SpinnerStateListener() { + @Override + public void onChange(SpinnerState state) { + setEnabledConfirmButton(state == SpinnerState.idle); + } + }); + } return picker; } + private void setEnabledConfirmButton(boolean enabled) { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled); + } + private View withTopMargin(PickerView view) { LinearLayout linearLayout = new LinearLayout(DatePickerPackage.context); linearLayout.setLayoutParams(new LinearLayout.LayoutParams( diff --git a/index.d.ts b/index.d.ts index ba1de42..45c7337 100644 --- a/index.d.ts +++ b/index.d.ts @@ -50,6 +50,17 @@ export interface DatePickerProps extends ViewProps { */ onDateChange?: (date: Date) => void + /** + * Spinner state change handler. + * + * This is called when the user start to spin the picker wheel and the spinner stops. + * It can be used to disable a confirm button until a spinner comes to a total stop + * to ensure the expected date is being selected. + * + * Android only. + */ + onStateChange?: (state: 'spinning' | 'idle') => void + /** * Timezone offset in minutes. * diff --git a/src/DatePickerAndroid.js b/src/DatePickerAndroid.js index 128f36a..8daa643 100644 --- a/src/DatePickerAndroid.js +++ b/src/DatePickerAndroid.js @@ -40,11 +40,16 @@ class DatePickerAndroid extends React.PureComponent { componentDidMount = () => { this.eventEmitter = new NativeEventEmitter(NativeModule) this.eventEmitter.addListener('dateChange', this._onChange) + this.eventEmitter.addListener( + 'spinnerStateChange', + this._onSpinnerStateChanged + ) this.eventEmitter.addListener('onConfirm', this._onConfirm) this.eventEmitter.addListener('onCancel', this._onCancel) } componentWillUnmount = () => { + this.eventEmitter.removeAllListeners('spinnerStateChange') this.eventEmitter.removeAllListeners('dateChange') this.eventEmitter.removeAllListeners('onConfirm') this.eventEmitter.removeAllListeners('onCancel') @@ -80,6 +85,12 @@ class DatePickerAndroid extends React.PureComponent { this.props.onDateStringChange(dateString) } } + _onSpinnerStateChanged = (e) => { + const { spinnerState, id } = e.nativeEvent ?? e + const newArch = id !== null + if (newArch && id !== this.id) return + this.props.onStateChange && this.props.onStateChange(spinnerState) + } _maximumDate = () => this.props.maximumDate &&