Browse Source

feat: `onStateChange` prop (#771)

master
Henning Hall 1 year ago
committed by GitHub
parent
commit
2ee3bb2bbd
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
15 changed files with 141 additions and 11 deletions
  1. +12
    -0
      README.md
  2. +13
    -0
      android/src/main/java/com/henninghall/date_picker/Emitter.java
  3. +9
    -0
      android/src/main/java/com/henninghall/date_picker/PickerView.java
  4. +15
    -6
      android/src/main/java/com/henninghall/date_picker/pickers/AndroidNative.java
  5. +12
    -0
      android/src/main/java/com/henninghall/date_picker/pickers/IosClone.java
  6. +1
    -0
      android/src/main/java/com/henninghall/date_picker/pickers/Picker.java
  7. +6
    -0
      android/src/main/java/com/henninghall/date_picker/ui/SpinnerState.java
  8. +5
    -0
      android/src/main/java/com/henninghall/date_picker/ui/SpinnerStateListener.java
  9. +6
    -1
      android/src/main/java/com/henninghall/date_picker/ui/UIManager.java
  10. +2
    -0
      android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListener.java
  11. +16
    -0
      android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListenerImpl.java
  12. +5
    -0
      android/src/main/java/com/henninghall/date_picker/wheelFunctions/AddOnChangeListener.java
  13. +17
    -4
      android/src/newarch/java/com/henninghall/date_picker/DatePickerModule.java
  14. +11
    -0
      index.d.ts
  15. +11
    -0
      src/DatePickerAndroid.js

+ 12
- 0
README.md View File

@ -154,6 +154,7 @@ export default () => {
| `confirmText` | Modal only: Confirm button text. | | `confirmText` | Modal only: Confirm button text. |
| `cancelText` | Modal only: Cancel button text. | | `cancelText` | Modal only: Cancel button text. |
| `theme` | Modal only: The theme of the modal. `"light"`, `"dark"`, `"auto"`. Defaults to `"auto"`. | | `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 ## Additional android styling
@ -226,6 +227,17 @@ If you have enabled
There are no breaking changes in v4, so just bump the version number in your package json. There are no breaking changes in v4, so just bump the version number in your package json.
### How can we disable confirm button until the wheel has stopped spinning?
Use the `onStateChange` prop to track the state of the spinning wheel.
```jsx
const [state, setState] = useState("idle")
...
<DatePicker onStateChange={setState} />
<ConfirmButton disabled={state === "spinning"} />
```
## Two different Android variants ## Two different Android variants
On Android there are two design variants to choose from: On Android there are two design variants to choose from:

+ 13
- 0
android/src/main/java/com/henninghall/date_picker/Emitter.java View File

@ -6,6 +6,7 @@ import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.uimanager.events.RCTEventEmitter; import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.henninghall.date_picker.ui.SpinnerState;
import java.util.Calendar; import java.util.Calendar;
@ -19,6 +20,18 @@ public class Emitter {
return DatePickerPackage.context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class); 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) { public static void onDateChange(Calendar date, String displayValueString, String id, View view) {
WritableMap event = Arguments.createMap(); WritableMap event = Arguments.createMap();
String dateString = Utils.dateToIso(date); String dateString = Utils.dateToIso(date);

+ 9
- 0
android/src/main/java/com/henninghall/date_picker/PickerView.java View File

@ -6,6 +6,7 @@ import android.widget.LinearLayout;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import com.facebook.react.bridge.Dynamic; 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.DividerHeightProp;
import com.henninghall.date_picker.props.Is24hourSourceProp; import com.henninghall.date_picker.props.Is24hourSourceProp;
import com.henninghall.date_picker.props.MaximumDateProp; 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.LocaleProp;
import com.henninghall.date_picker.props.ModeProp; import com.henninghall.date_picker.props.ModeProp;
import com.henninghall.date_picker.props.TextColorProp; 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.UIManager;
import com.henninghall.date_picker.ui.Accessibility; import com.henninghall.date_picker.ui.Accessibility;
@ -107,6 +109,10 @@ public class PickerView extends RelativeLayout {
uiManager.scroll(wheelIndex, scrollTimes); uiManager.scroll(wheelIndex, scrollTimes);
} }
public void addSpinnerStateListener(SpinnerStateListener listener){
uiManager.addStateListener(listener);
}
public String getDate() { public String getDate() {
return state.derived.getLastDate(); return state.derived.getLastDate();
} }
@ -132,4 +138,7 @@ public class PickerView extends RelativeLayout {
} }
public Variant getVariant() {
return state.getVariant();
}
} }

+ 15
- 6
android/src/main/java/com/henninghall/date_picker/pickers/AndroidNative.java View File

@ -17,16 +17,16 @@ import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import static android.widget.NumberPicker.OnScrollListener.SCROLL_STATE_FLING;
import static android.widget.NumberPicker.OnScrollListener.SCROLL_STATE_IDLE; import static android.widget.NumberPicker.OnScrollListener.SCROLL_STATE_IDLE;
public class AndroidNative extends NumberPicker implements Picker { public class AndroidNative extends NumberPicker implements Picker {
private Picker.OnValueChangeListener onValueChangedListener; private Picker.OnValueChangeListener onValueChangedListener;
private int state = SCROLL_STATE_IDLE;
private int internalSpinState = SCROLL_STATE_IDLE;
private OnValueChangeListenerInScrolling listenerInScrolling; private OnValueChangeListenerInScrolling listenerInScrolling;
private boolean isAnimating; private boolean isAnimating;
private final Handler handler = new Handler(); private final Handler handler = new Handler();
private boolean spinning;
public AndroidNative(Context context) { public AndroidNative(Context context) {
super(context); super(context);
@ -102,7 +102,7 @@ public class AndroidNative extends NumberPicker implements Picker {
@Override @Override
public boolean isSpinning() { public boolean isSpinning() {
return state == SCROLL_STATE_FLING || isAnimating;
return spinning || isAnimating;
} }
@Override @Override
@ -116,10 +116,13 @@ public class AndroidNative extends NumberPicker implements Picker {
int timeBetweenScrollsMs = 100; int timeBetweenScrollsMs = 100;
int willStopScrollingInMs = timeBetweenScrollsMs * moves; int willStopScrollingInMs = timeBetweenScrollsMs * moves;
isAnimating = true; isAnimating = true;
onValueChangedListener.onSpinnerStateChange();
handler.postDelayed(new Runnable() { handler.postDelayed(new Runnable() {
@Override @Override
public void run() { public void run() {
isAnimating = false; isAnimating = false;
onValueChangedListener.onSpinnerStateChange();
} }
}, willStopScrollingInMs); }, willStopScrollingInMs);
@ -184,7 +187,7 @@ public class AndroidNative extends NumberPicker implements Picker {
// onValueChange is triggered also during scrolling. Since we don't want // onValueChange is triggered also during scrolling. Since we don't want
// to send event during scrolling we make sure wheel is still. This particular // to send event during scrolling we make sure wheel is still. This particular
// case happens when wheel is tapped, not scrolled. // case happens when wheel is tapped, not scrolled.
if(state == SCROLL_STATE_IDLE) {
if(internalSpinState == SCROLL_STATE_IDLE) {
sendEventIn500ms(); sendEventIn500ms();
} }
} }
@ -194,13 +197,17 @@ public class AndroidNative extends NumberPicker implements Picker {
@Override @Override
public void onScrollStateChange(NumberPicker numberPicker, int nextState) { public void onScrollStateChange(NumberPicker numberPicker, int nextState) {
sendEventIfStopped(nextState); sendEventIfStopped(nextState);
state = nextState;
internalSpinState = nextState;
if (nextState != SCROLL_STATE_IDLE){
spinning = true;
onValueChangedListener.onSpinnerStateChange();
}
} }
}); });
} }
private void sendEventIfStopped(int nextState){ 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) { if (stoppedScrolling) {
sendEventIn500ms(); sendEventIn500ms();
} }
@ -210,7 +217,9 @@ public class AndroidNative extends NumberPicker implements Picker {
handler.postDelayed(new Runnable() { handler.postDelayed(new Runnable() {
@Override @Override
public void run() { public void run() {
spinning = false;
onValueChangedListener.onValueChange(); onValueChangedListener.onValueChange();
onValueChangedListener.onSpinnerStateChange();
} }
// the delay make sure the wheel has stopped before sending the value change event // the delay make sure the wheel has stopped before sending the value change event
}, 500); }, 500);

+ 12
- 0
android/src/main/java/com/henninghall/date_picker/pickers/IosClone.java View File

@ -16,6 +16,7 @@ import com.henninghall.date_picker.ui.Accessibility;
public class IosClone extends NumberPickerView implements Picker { public class IosClone extends NumberPickerView implements Picker {
private Picker.OnValueChangeListenerInScrolling mOnValueChangeListenerInScrolling; private Picker.OnValueChangeListenerInScrolling mOnValueChangeListenerInScrolling;
private Picker.OnValueChangeListener onValueChangedListener;
public IosClone(Context context) { public IosClone(Context context) {
super(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 @Override
@ -71,6 +81,8 @@ public class IosClone extends NumberPickerView implements Picker {
@Override @Override
public void setOnValueChangedListener(final Picker.OnValueChangeListener listener) { public void setOnValueChangedListener(final Picker.OnValueChangeListener listener) {
this.onValueChangedListener = listener;
super.setOnValueChangedListener(new NumberPickerView.OnValueChangeListener() { super.setOnValueChangedListener(new NumberPickerView.OnValueChangeListener() {
@Override @Override
public void onValueChange(NumberPickerView picker, int oldVal, int newVal) { public void onValueChange(NumberPickerView picker, int oldVal, int newVal) {

+ 1
- 0
android/src/main/java/com/henninghall/date_picker/pickers/Picker.java View File

@ -32,5 +32,6 @@ public interface Picker {
interface OnValueChangeListener { interface OnValueChangeListener {
void onValueChange(); void onValueChange();
void onSpinnerStateChange();
} }
} }

+ 6
- 0
android/src/main/java/com/henninghall/date_picker/ui/SpinnerState.java View File

@ -0,0 +1,6 @@
package com.henninghall.date_picker.ui;
public enum SpinnerState {
idle,
spinning
}

+ 5
- 0
android/src/main/java/com/henninghall/date_picker/ui/SpinnerStateListener.java View File

@ -0,0 +1,5 @@
package com.henninghall.date_picker.ui;
public interface SpinnerStateListener {
void onChange(SpinnerState event);
}

+ 6
- 1
android/src/main/java/com/henninghall/date_picker/ui/UIManager.java View File

@ -21,6 +21,7 @@ public class UIManager {
private Wheels wheels; private Wheels wheels;
private FadingOverlay fadingOverlay; private FadingOverlay fadingOverlay;
private WheelScroller wheelScroller = new WheelScroller(); private WheelScroller wheelScroller = new WheelScroller();
private WheelChangeListenerImpl onWheelChangeListener;
public UIManager(State state, View rootView){ public UIManager(State state, View rootView){
this.state = state; this.state = state;
@ -78,10 +79,14 @@ public class UIManager {
} }
private void addOnChangeListener(){ private void addOnChangeListener(){
WheelChangeListener onWheelChangeListener = new WheelChangeListenerImpl(wheels, state, this, rootView);
onWheelChangeListener = new WheelChangeListenerImpl(wheels, state, this, rootView);
wheels.applyOnAll(new AddOnChangeListener(onWheelChangeListener)); wheels.applyOnAll(new AddOnChangeListener(onWheelChangeListener));
} }
public void addStateListener(SpinnerStateListener listener){
onWheelChangeListener.addStateListener(listener);
}
public void updateDividerHeight() { public void updateDividerHeight() {
wheels.updateDividerHeight(); wheels.updateDividerHeight();
} }

+ 2
- 0
android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListener.java View File

@ -5,6 +5,8 @@ import com.henninghall.date_picker.wheels.Wheel;
public interface WheelChangeListener { public interface WheelChangeListener {
void onChange(Wheel picker); void onChange(Wheel picker);
void onStateChange(Wheel picker);
} }

+ 16
- 0
android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListenerImpl.java View File

@ -9,6 +9,8 @@ import com.henninghall.date_picker.wheels.Wheel;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.Calendar;
import java.util.HashSet;
import java.util.Set;
import java.util.TimeZone; import java.util.TimeZone;
public class WheelChangeListenerImpl implements WheelChangeListener { public class WheelChangeListenerImpl implements WheelChangeListener {
@ -17,6 +19,8 @@ public class WheelChangeListenerImpl implements WheelChangeListener {
private final State state; private final State state;
private final UIManager uiManager; private final UIManager uiManager;
private final View rootView; private final View rootView;
private SpinnerState lastEmittedSpinnerState;
private Set<SpinnerStateListener> listeners = new HashSet<>();
WheelChangeListenerImpl(Wheels wheels, State state, UIManager uiManager, View rootView) { WheelChangeListenerImpl(Wheels wheels, State state, UIManager uiManager, View rootView) {
this.wheels = wheels; this.wheels = wheels;
@ -65,6 +69,15 @@ public class WheelChangeListenerImpl implements WheelChangeListener {
Emitter.onDateChange(selectedDate, displayData, state.getId(), rootView); 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. // Example: Jan 1 returns true, April 31 returns false.
private boolean dateExists(){ private boolean dateExists(){
SimpleDateFormat dateFormat = getDateFormat(); SimpleDateFormat dateFormat = getDateFormat();
@ -111,4 +124,7 @@ public class WheelChangeListenerImpl implements WheelChangeListener {
return null; return null;
} }
public void addStateListener(SpinnerStateListener listener) {
listeners.add(listener);
}
} }

+ 5
- 0
android/src/main/java/com/henninghall/date_picker/wheelFunctions/AddOnChangeListener.java View File

@ -21,6 +21,11 @@ public class AddOnChangeListener implements WheelFunction {
public void onValueChange() { public void onValueChange() {
onChangeListener.onChange(wheel); onChangeListener.onChange(wheel);
} }
@Override
public void onSpinnerStateChange() {
onChangeListener.onStateChange(wheel);
}
}); });
} }
} }

+ 17
- 4
android/src/newarch/java/com/henninghall/date_picker/DatePickerModule.java View File

@ -1,9 +1,7 @@
package com.henninghall.date_picker; package com.henninghall.date_picker;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.telecom.Call;
import android.view.View; import android.view.View;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
@ -12,12 +10,13 @@ import androidx.annotation.NonNull;
import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator; 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; import net.time4j.android.ApplicationStarter;
@ -129,9 +128,23 @@ public class DatePickerModule extends NativeRNDatePickerSpec {
} }
} }
picker.update(); 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; return picker;
} }
private void setEnabledConfirmButton(boolean enabled) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled);
}
private View withTopMargin(PickerView view) { private View withTopMargin(PickerView view) {
LinearLayout linearLayout = new LinearLayout(DatePickerPackage.context); LinearLayout linearLayout = new LinearLayout(DatePickerPackage.context);
linearLayout.setLayoutParams(new LinearLayout.LayoutParams( linearLayout.setLayoutParams(new LinearLayout.LayoutParams(

+ 11
- 0
index.d.ts View File

@ -50,6 +50,17 @@ export interface DatePickerProps extends ViewProps {
*/ */
onDateChange?: (date: Date) => void 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. * Timezone offset in minutes.
* *

+ 11
- 0
src/DatePickerAndroid.js View File

@ -40,11 +40,16 @@ class DatePickerAndroid extends React.PureComponent {
componentDidMount = () => { componentDidMount = () => {
this.eventEmitter = new NativeEventEmitter(NativeModule) this.eventEmitter = new NativeEventEmitter(NativeModule)
this.eventEmitter.addListener('dateChange', this._onChange) this.eventEmitter.addListener('dateChange', this._onChange)
this.eventEmitter.addListener(
'spinnerStateChange',
this._onSpinnerStateChanged
)
this.eventEmitter.addListener('onConfirm', this._onConfirm) this.eventEmitter.addListener('onConfirm', this._onConfirm)
this.eventEmitter.addListener('onCancel', this._onCancel) this.eventEmitter.addListener('onCancel', this._onCancel)
} }
componentWillUnmount = () => { componentWillUnmount = () => {
this.eventEmitter.removeAllListeners('spinnerStateChange')
this.eventEmitter.removeAllListeners('dateChange') this.eventEmitter.removeAllListeners('dateChange')
this.eventEmitter.removeAllListeners('onConfirm') this.eventEmitter.removeAllListeners('onConfirm')
this.eventEmitter.removeAllListeners('onCancel') this.eventEmitter.removeAllListeners('onCancel')
@ -80,6 +85,12 @@ class DatePickerAndroid extends React.PureComponent {
this.props.onDateStringChange(dateString) 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 = () => _maximumDate = () =>
this.props.maximumDate && this.props.maximumDate &&

Loading…
Cancel
Save