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. |
| `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
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
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.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);

+ 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 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();
}
}

+ 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.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);

+ 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 {
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) {

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

@ -32,5 +32,6 @@ public interface Picker {
interface OnValueChangeListener {
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 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();
}

+ 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 {
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.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<SpinnerStateListener> 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);
}
}

+ 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() {
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;
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(

+ 11
- 0
index.d.ts View File

@ -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.
*

+ 11
- 0
src/DatePickerAndroid.js View File

@ -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 &&

Loading…
Cancel
Save