Skip to content

Commit 2d9b5b4

Browse files
committed
MaterialDatePicker: Added a optional 'Clear' button that be used to remove the selected date with the option to calling a clear listener and dismissing the dialog when clicked. Supports both dialog and full screen mode
1 parent 80aa0cc commit 2d9b5b4

8 files changed

Lines changed: 141 additions & 8 deletions

File tree

lib/java/com/google/android/material/datepicker/DateSelector.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ public interface DateSelector<S> extends Parcelable {
6666
*/
6767
void setSelection(@NonNull S selection);
6868

69+
/**
70+
* Clears the current selection.
71+
*/
72+
void clearSelection();
73+
6974
/**
7075
* Allows this selection handler to respond to clicks within the {@link AdapterView}.
7176
*

lib/java/com/google/android/material/datepicker/MaterialCalendar.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,19 @@ void sendAccessibilityFocusEventToMonthDropdown() {
487487
}
488488
}
489489

490+
public void clearSelection() {
491+
if (dateSelector != null) {
492+
// Clear the selection then call 'selection changed' listeners, if any
493+
dateSelector.clearSelection();
494+
for (OnSelectionChangedListener<S> listener : onSelectionChangedListeners) {
495+
listener.onSelectionChanged(dateSelector.getSelection());
496+
}
497+
498+
// Update the gridview
499+
recyclerView.getAdapter().notifyDataSetChanged();
500+
}
501+
}
502+
490503
void toggleVisibleSelector() {
491504
if (calendarSelector == CalendarSelector.YEAR) {
492505
setSelector(CalendarSelector.DAY);

lib/java/com/google/android/material/datepicker/MaterialDatePicker.java

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import android.text.TextUtils;
4040
import android.view.LayoutInflater;
4141
import android.view.View;
42+
import android.view.View.OnClickListener;
4243
import android.view.ViewGroup;
4344
import android.view.Window;
4445
import android.widget.Button;
@@ -99,10 +100,13 @@ public class MaterialDatePicker<S> extends DialogFragment {
99100
private static final String INPUT_MODE_KEY = "INPUT_MODE_KEY";
100101
private static final String CALENDAR_FRAGMENT_TAG = "CALENDAR_FRAGMENT_TAG";
101102
private static final String TEXT_INPUT_FRAGMENT_TAG = "TEXT_INPUT_FRAGMENT_TAG";
103+
private static final String CLEARABLE_KEY = "CLEARABLE_KEY";
104+
private static final String DISMISS_ON_CLEAR_KEY = "DISMISS_ON_CLEAR_KEY";
102105

103106
static final Object CONFIRM_BUTTON_TAG = "CONFIRM_BUTTON_TAG";
104107
static final Object CANCEL_BUTTON_TAG = "CANCEL_BUTTON_TAG";
105108
static final Object TOGGLE_BUTTON_TAG = "TOGGLE_BUTTON_TAG";
109+
static final Object CLEAR_BUTTON_TAG = "CLEAR_BUTTON_TAG";
106110

107111
/** Date picker will start with calendar view. */
108112
public static final int INPUT_MODE_CALENDAR = 0;
@@ -143,6 +147,8 @@ public String getHeaderText() {
143147
new LinkedHashSet<>();
144148
private final LinkedHashSet<DialogInterface.OnCancelListener> onCancelListeners =
145149
new LinkedHashSet<>();
150+
private final LinkedHashSet<OnClickListener> onClearButtonClickListeners =
151+
new LinkedHashSet<>();
146152
private final LinkedHashSet<DialogInterface.OnDismissListener> onDismissListeners =
147153
new LinkedHashSet<>();
148154

@@ -169,6 +175,9 @@ public String getHeaderText() {
169175
private CheckableImageButton headerToggleButton;
170176
@Nullable private MaterialShapeDrawable background;
171177
private Button confirmButton;
178+
private Button clearButton;
179+
private boolean isClearable;
180+
private boolean dismissOnClear;
172181

173182
private boolean edgeToEdgeEnabled;
174183
@Nullable private CharSequence fullTitleText;
@@ -199,6 +208,8 @@ static <S> MaterialDatePicker<S> newInstance(@NonNull Builder<S> options) {
199208
options.negativeButtonContentDescriptionResId);
200209
args.putCharSequence(
201210
NEGATIVE_BUTTON_CONTENT_DESCRIPTION_KEY, options.negativeButtonContentDescription);
211+
args.putBoolean(CLEARABLE_KEY, options.isClearable);
212+
args.putBoolean(DISMISS_ON_CLEAR_KEY, options.dismissOnClear);
202213
materialDatePickerDialogFragment.setArguments(args);
203214
return materialDatePickerDialogFragment;
204215
}
@@ -232,6 +243,8 @@ public final void onSaveInstanceState(@NonNull Bundle bundle) {
232243
NEGATIVE_BUTTON_CONTENT_DESCRIPTION_RES_ID_KEY, negativeButtonContentDescriptionResId);
233244
bundle.putCharSequence(
234245
NEGATIVE_BUTTON_CONTENT_DESCRIPTION_KEY, negativeButtonContentDescription);
246+
bundle.putBoolean(CLEARABLE_KEY, isClearable);
247+
bundle.putBoolean(DISMISS_ON_CLEAR_KEY, dismissOnClear);
235248
}
236249

237250
@Override
@@ -261,6 +274,8 @@ public final void onCreate(@Nullable Bundle bundle) {
261274
fullTitleText =
262275
titleText != null ? titleText : requireContext().getResources().getText(titleTextResId);
263276
singleLineTitleText = getFirstLineBySeparator(fullTitleText);
277+
isClearable = activeBundle.getBoolean(CLEARABLE_KEY);
278+
dismissOnClear = activeBundle.getBoolean(DISMISS_ON_CLEAR_KEY);
264279
}
265280

266281
private int getThemeResId(Context context) {
@@ -331,11 +346,7 @@ public final View onCreateView(
331346
initHeaderToggle(context);
332347

333348
confirmButton = root.findViewById(R.id.confirm_button);
334-
if (getDateSelector().isSelectionComplete()) {
335-
confirmButton.setEnabled(true);
336-
} else {
337-
confirmButton.setEnabled(false);
338-
}
349+
confirmButton.setEnabled(getDateSelector().isSelectionComplete());
339350
confirmButton.setTag(CONFIRM_BUTTON_TAG);
340351
if (positiveButtonText != null) {
341352
confirmButton.setText(positiveButtonText);
@@ -364,6 +375,35 @@ public final View onCreateView(
364375
getContext().getResources().getText(negativeButtonContentDescriptionResId));
365376
}
366377
cancelButton.setOnClickListener(this::onNegativeButtonClick);
378+
379+
clearButton = root.findViewById(R.id.clear_button);
380+
clearButton.setTag(CLEAR_BUTTON_TAG);
381+
if (isClearable) {
382+
clearButton.setEnabled(getDateSelector().isSelectionComplete());
383+
clearButton.setOnClickListener(
384+
new View.OnClickListener() {
385+
@Override
386+
public void onClick(View v) {
387+
// Clear the selection
388+
calendar.clearSelection();
389+
390+
// Call 'cleared' listeners, if any
391+
for (View.OnClickListener listener : onClearButtonClickListeners) {
392+
listener.onClick(v);
393+
}
394+
395+
if (dismissOnClear) {
396+
dismiss();
397+
}
398+
}
399+
});
400+
401+
clearButton.setVisibility(View.VISIBLE);
402+
403+
} else {
404+
clearButton.setVisibility(View.GONE);
405+
}
406+
367407
return root;
368408
}
369409

@@ -531,12 +571,13 @@ private void startPickerFragment() {
531571
@Override
532572
public void onSelectionChanged(S selection) {
533573
updateHeader(getHeaderText());
534-
confirmButton.setEnabled(getDateSelector().isSelectionComplete());
574+
updateActionButtonEnabledState();
535575
}
536576

537577
@Override
538578
public void onIncompleteSelectionChanged() {
539579
confirmButton.setEnabled(false);
580+
clearButton.setEnabled(false);
540581
}
541582
});
542583

@@ -562,7 +603,7 @@ private void initHeaderToggle(Context context) {
562603
headerToggleButton.setOnClickListener(
563604
v -> {
564605
// Update confirm button in case in progress selection has been reset
565-
confirmButton.setEnabled(getDateSelector().isSelectionComplete());
606+
updateActionButtonEnabledState();
566607

567608
headerToggleButton.toggle();
568609
inputMode = (inputMode == INPUT_MODE_TEXT) ? INPUT_MODE_CALENDAR : INPUT_MODE_TEXT;
@@ -572,6 +613,11 @@ private void initHeaderToggle(Context context) {
572613
});
573614
}
574615

616+
private void updateActionButtonEnabledState() {
617+
confirmButton.setEnabled(getDateSelector().isSelectionComplete());
618+
clearButton.setEnabled(isClearable && getDateSelector().isSelectionComplete());
619+
}
620+
575621
private void updateToggleContentDescription(@NonNull CheckableImageButton toggle) {
576622
String contentDescription =
577623
inputMode == INPUT_MODE_TEXT
@@ -696,6 +742,27 @@ public void clearOnNegativeButtonClickListeners() {
696742
onNegativeButtonClickListeners.clear();
697743
}
698744

745+
/** The supplied listener is called when the user clicks the clear button. */
746+
public boolean addOnClearButtonClickListener(
747+
@NonNull OnClickListener onClearButtonClickListener) {
748+
return onClearButtonClickListeners.add(onClearButtonClickListener);
749+
}
750+
751+
/**
752+
* Removes a listener previously added via {@link MaterialDatePicker#addOnClearButtonClickListener}.
753+
*/
754+
public boolean removeOnClearButtonClickListener(
755+
@NonNull OnClickListener onClearButtonClickListener) {
756+
return onClearButtonClickListeners.remove(onClearButtonClickListener);
757+
}
758+
759+
/**
760+
* Removes all listeners added via {@link MaterialDatePicker#addOnClearButtonClickListener}.
761+
*/
762+
public void clearOnClearButtonClickListeners() {
763+
onClearButtonClickListeners.clear();
764+
}
765+
699766
/**
700767
* The supplied listener is called when the user cancels the picker via back button or a touch
701768
* outside the view. It is not called when the user clicks the cancel button. To add a listener
@@ -754,6 +821,8 @@ public static final class Builder<S> {
754821
CharSequence negativeButtonContentDescription = null;
755822
@Nullable S selection = null;
756823
@InputMode int inputMode = INPUT_MODE_CALENDAR;
824+
boolean isClearable = false;
825+
boolean dismissOnClear = false;
757826

758827
private Builder(DateSelector<S> dateSelector) {
759828
this.dateSelector = dateSelector;
@@ -975,6 +1044,20 @@ public Builder<S> setInputMode(@InputMode int inputMode) {
9751044
return this;
9761045
}
9771046

1047+
/** Sets the state of the clear button */
1048+
@NonNull
1049+
public Builder<S> setClearable(boolean clearable) {
1050+
this.isClearable = clearable;
1051+
return this;
1052+
}
1053+
1054+
/** Sets the state of the dialog dismiss state when clear button is clicked */
1055+
@NonNull
1056+
public Builder<S> setDismissOnClear(boolean dismiss) {
1057+
this.dismissOnClear = dismiss;
1058+
return this;
1059+
}
1060+
9781061
/** Creates a {@link MaterialDatePicker} with the provided options. */
9791062
@NonNull
9801063
public MaterialDatePicker<S> build() {

lib/java/com/google/android/material/datepicker/RangeDateSelector.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ public boolean isSelectionComplete() {
8383
&& isValidRange(selectedStartItem, selectedEndItem);
8484
}
8585

86+
@Override
87+
public void clearSelection() {
88+
selectedStartItem = null;
89+
selectedEndItem = null;
90+
}
91+
8692
@Override
8793
public void setSelection(@NonNull Pair<Long, Long> selection) {
8894
if (selection.first != null && selection.second != null) {

lib/java/com/google/android/material/datepicker/SingleDateSelector.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ public void select(long selection) {
5858
selectedItem = selection;
5959
}
6060

61-
private void clearSelection() {
61+
@Override
62+
public void clearSelection() {
6263
selectedItem = null;
6364
}
6465

lib/java/com/google/android/material/datepicker/res/layout/mtrl_picker_actions.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@
2424
android:gravity="end"
2525
android:orientation="horizontal">
2626

27+
<Button
28+
android:id="@+id/clear_button"
29+
style="?attr/buttonBarNeutralButtonStyle"
30+
android:layout_width="wrap_content"
31+
android:layout_height="wrap_content"
32+
android:layout_gravity="center_vertical"
33+
android:text="@string/mtrl_picker_clear"/>
34+
35+
<Space
36+
android:layout_width="0dp"
37+
android:layout_height="0dp"
38+
android:layout_weight="1"
39+
android:visibility="invisible" />
40+
2741
<Button
2842
android:id="@+id/cancel_button"
2943
style="?attr/buttonBarNegativeButtonStyle"

lib/java/com/google/android/material/datepicker/res/layout/mtrl_picker_header_fullscreen.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@
7676
android:gravity="start|bottom"
7777
app:firstBaselineToTopHeight="@dimen/mtrl_calendar_selection_baseline_to_top_fullscreen"
7878
app:lineHeight="@dimen/mtrl_calendar_header_selection_line_height"/>
79+
80+
<Button
81+
android:id="@+id/clear_button"
82+
style="?attr/materialCalendarHeaderConfirmButton"
83+
android:layout_width="wrap_content"
84+
android:layout_height="@dimen/mtrl_min_touch_target_size"
85+
android:layout_gravity="end|top"
86+
android:contentDescription="@string/mtrl_picker_clear"
87+
android:text="@string/mtrl_picker_clear"/>
88+
7989
</FrameLayout>
8090

8191
<LinearLayout

lib/java/com/google/android/material/datepicker/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<string name="mtrl_picker_date_header_selected" description="A single date [CHAR_LIMIT=60]">%1$s</string>
2626
<string name="mtrl_picker_confirm" description="Button text to indicate that the widget will save the user's selection [CHAR_LIMIT=16]">OK</string>
2727
<string name="mtrl_picker_cancel" description="Button text to indicate that the widget will ignore the user's selection [CHAR_LIMIT=16]">Cancel</string>
28+
<string name="mtrl_picker_clear" description="Button text to indicate that the user can clear the current selection [CHAR_LIMIT=16]">Clear</string>
2829
<string name="mtrl_picker_text_input_date_hint" description="Label for a single date selected by the user [CHAR_LIMIT=60]">Date</string>
2930
<string name="mtrl_picker_text_input_date_range_start_hint" description="Label for the start date in a range selected by the user [CHAR_LIMIT=60]">Start date</string>
3031
<string name="mtrl_picker_text_input_date_range_end_hint" description="Label for the end date in a range selected by the user [CHAR_LIMIT=60]">End date</string>

0 commit comments

Comments
 (0)