Skip to content

Commit

Permalink
feat: onStateChange prop (#771)
Browse files Browse the repository at this point in the history
  • Loading branch information
henninghall authored Mar 4, 2024
1 parent 3899e90 commit 2ee3bb2
Show file tree
Hide file tree
Showing 15 changed files with 141 additions and 11 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -226,6 +227,17 @@ If you have enabled <a href="https://facebook.github.io/react-native/docs/signed

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:
Expand Down
13 changes: 13 additions & 0 deletions android/src/main/java/com/henninghall/date_picker/Emitter.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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;

Expand All @@ -19,6 +20,18 @@ private static DeviceEventManagerModule.RCTDeviceEventEmitter deviceEventEmitter
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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;
Expand All @@ -19,6 +20,7 @@
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;

Expand Down Expand Up @@ -107,6 +109,10 @@ public void scroll(int wheelIndex, int scrollTimes) {
uiManager.scroll(wheelIndex, scrollTimes);
}

public void addSpinnerStateListener(SpinnerStateListener listener){
uiManager.addStateListener(listener);
}

public String getDate() {
return state.derived.getLastDate();
}
Expand All @@ -132,4 +138,7 @@ public void requestLayout() {
}


public Variant getVariant() {
return state.getVariant();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
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);
Expand Down Expand Up @@ -102,7 +102,7 @@ public void setItemPaddingHorizontal(int padding) {

@Override
public boolean isSpinning() {
return state == SCROLL_STATE_FLING || isAnimating;
return spinning || isAnimating;
}

@Override
Expand All @@ -116,10 +116,13 @@ public void smoothScrollToValue(final int value) {
int timeBetweenScrollsMs = 100;
int willStopScrollingInMs = timeBetweenScrollsMs * moves;
isAnimating = true;
onValueChangedListener.onSpinnerStateChange();

handler.postDelayed(new Runnable() {
@Override
public void run() {
isAnimating = false;
onValueChangedListener.onSpinnerStateChange();
}
}, willStopScrollingInMs);

Expand Down Expand Up @@ -184,7 +187,7 @@ public void onValueChange(NumberPicker numberPicker, int oldVal, int newVal) {
// 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();
}
}
Expand All @@ -194,13 +197,17 @@ public void onValueChange(NumberPicker numberPicker, int oldVal, int newVal) {
@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();
}
Expand All @@ -210,7 +217,9 @@ private void sendEventIn500ms(){
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

public class IosClone extends NumberPickerView implements Picker {
private Picker.OnValueChangeListenerInScrolling mOnValueChangeListenerInScrolling;
private Picker.OnValueChangeListener onValueChangedListener;

public IosClone(Context context) {
super(context);
Expand Down Expand Up @@ -54,6 +55,15 @@ public void onValueChangeInScrolling(NumberPickerView picker, int oldVal, int ne
}
}
});

super.setOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChange(NumberPickerView view, int scrollState) {
if (onValueChangedListener != null) {
onValueChangedListener.onSpinnerStateChange();
}
}
});
}

@Override
Expand All @@ -71,6 +81,8 @@ public void setOnValueChangeListenerInScrolling(final Picker.OnValueChangeListen

@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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ interface OnValueChangeListenerInScrolling {

interface OnValueChangeListener {
void onValueChange();
void onSpinnerStateChange();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.henninghall.date_picker.ui;

public enum SpinnerState {
idle,
spinning
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.henninghall.date_picker.ui;

public interface SpinnerStateListener {
void onChange(SpinnerState event);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -78,10 +79,14 @@ void animateToDate(Calendar date) {
}

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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
public interface WheelChangeListener {

void onChange(Wheel picker);

void onStateChange(Wheel picker);
}


Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
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 {
Expand All @@ -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;
Expand Down Expand Up @@ -65,6 +69,15 @@ public void onChange(Wheel picker) {
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();
Expand Down Expand Up @@ -111,4 +124,7 @@ private Calendar getClosestExistingDateInPast(){
return null;
}

public void addStateListener(SpinnerStateListener listener) {
listeners.add(listener);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ public void apply(final Wheel wheel) {
public void onValueChange() {
onChangeListener.onChange(wheel);
}

@Override
public void onSpinnerStateChange() {
onChangeListener.onStateChange(wheel);
}
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,12 +10,13 @@

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;

Expand Down Expand Up @@ -129,9 +128,23 @@ private PickerView createPicker(ReadableMap props){
}
}
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(
Expand Down
11 changes: 11 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Loading

0 comments on commit 2ee3bb2

Please sign in to comment.