--- Note to moderators: Today (July 15), I've noticed that someone already faced this problem here. But I'm not sure if it's appropriate to close this as a duplicate, since i think I provided a much better explanation of the issue. I'm not sure if I should edit the other question and paste this content there, but I'm not comfortable changing someone else's question too much. ---
I have something weird here.
I don't think the problem depends on which SDK you build against. The device OS version is what matters.
Problem #1: inconsistency by default
DatePickerDialog
was changed (?) in Jelly Bean and now only provides a Done button. Previous versions included a Cancel button, and this may affect user experience (inconsistency, muscle memory from previous Android versions).
Replicate: Create a basic project. Put this in onCreate
:
DatePickerDialog picker = new DatePickerDialog(
this,
new OnDateSetListener() {
@Override
public void onDateSet(DatePicker v, int y, int m, int d) {
Log.d("Picker", "Set!");
}
},
2012, 6, 15);
picker.show();
Expected: A Cancel button to appear in the dialog.
Current: A Cancel button does not appear.
Screenshots: 4.0.3 (OK) and 4.1.1 (possibly wrong?).
Problem #2: wrong dismiss behavior
Dialog calls whichever listener it should call indeed, and then always calls OnDateSetListener
listener. Canceling still calls the set method, and setting it calls the method twice.
Replicate: Use #1 code, but add code below (you'll see this solves #1, but only visually/UI):
picker.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancel",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d("Picker", "Cancel!");
}
});
Expected:
- Pressing the BACK key or clicking outside the dialog should do nothing.
- Pressing "Cancel" should print Picker Cancel!.
- Pressing "Set" should print Picker Set!.
Current:
- Pressing the BACK key or clicking outside the dialog prints Picker Set!.
- Pressing "Cancel" prints Picker Cancel! and then Picker Set!.
- Pressing "Set" prints Picker Set! and then Picker Set!.
Log lines showing the behavior:
07-15 12:00:13.415: D/Picker(21000): Set!
07-15 12:00:24.860: D/Picker(21000): Cancel!
07-15 12:00:24.876: D/Picker(21000): Set!
07-15 12:00:33.696: D/Picker(21000): Set!
07-15 12:00:33.719: D/Picker(21000): Set!
Other notes and comments
- Wrapping it around a
DatePickerFragment
doesn't matter. I simplified the problem for you, but I've tested it.
After testing some of the sugestions posted here, I personally think this solution is the most simple. I pass "null" as my listener in the DatePickerDialog constructor, and then when I click the "OK" button I call my onDateSearchSetListener:
There is a very simple workaround, if your application does not use the action bar. Note by the way, that some apps rely on this functionality to work, because cancelling out of the date picker has a special meaning (e.g. it clears the date field to an empty string, which for some apps is a valid and meaningful type of input) and using boolean flags to prevent the date from being set twice on OK will not help you in this case.
Re. the actual fix, you do not have to create new buttons or your own dialog. The point is to be compatible with both, the older versions of Android, the buggy ones (4.) and any future ones, though the latter is impossible to be sure about, of course. Note that in Android 2., the onStop() for android.app.Dialog does nothing at all, and in 4.* it does mActionBar.setShowHideAnimationEnabled(false) which is important only if your app has an action bar. The onStop() in DatePickerDialog, which inherits from Dialog, only contributes mDatePicker.clearFocus() (as of the latest fix to Android sources 4.3), which does not seem essential.
Therefore replacing onStop() with a method that does nothing should in many instances fix your app and ensure that it will remain so for the foreseeable future. Thus simply extend DatePickerDialog class with your own and override onStop() whit a dummy method. You will also have to provide one or two constructors, as per your requirements. Note also that one should not be tempted to try to overdo this fix by e.g. attempting to do something with the activity bar directly, as this would limit your compatibility to the latest versions of Android only. Also note that it would be nice to be able to call the super for DatePicker's onStop() because the bug is only in the onStop() in DatePickerDialog itself, but not in DatePickerDialog's super class. However, this would require you to call super.super.onStop() from your custom class, which Java will not let you do, as it goes against the encapsulation philosophy :) Below is my little class that I used to verride DatePickerDialog. I hope this comment would be useful for somebody. Wojtek Jarosz
}
I'm using date pickers, time pickers and number pickers. The number pickers call onValueChanged whenever the user selects a number, before the picker is dismissed, so I already had structure like this to do something with the value only when the picker is dismissed:
I extended this to set custom onClickListeners for my buttons, with an argument to see which button was clicked. Now I can check which button was tapped before I set my final value:
And then I extended that to work with the date and time types for date and time pickers as well as the int type for number pickers.
I posted this because I thought it was simpler than some of the solutions above, but now that I've included all the code, I guess it's not much simpler! But it fit nicely into the structure I already had.
Update for Lollipop: Apparently this bug doesn't happen on all Android 4.1-4.4 devices, because I received a few reports from users whose date and time pickers weren't calling the onDateSet and onTimeSet callbacks. And the bug was officially fixed in Android 5.0. My approach only worked on devices where the bug is present, because my custom buttons didn't call the dialog's onClick handler, which is the only place that onDateSet and onTimeSet are called when the bug is not present. I updated my code above to call the dialog's onClick, so now it works whether or not the bug is present.
Note: Fixed as of Lollipop, source here. Automated class for use in clients (compatible with all Android versions) updated as well.
TL;DR: 1-2-3 dead easy steps for a global solution:
OnDateSetListener
in your activity (or change the class to suit your needs).Trigger the dialog with this code (in this sample, I use it inside a
Fragment
):And that's all it takes! The reason I still keep my answer as "accepted" is because I still prefer my solution since it has a very small footprint in client code, it addresses the fundamental issue (the listener being called in the framework class), works fine across config changes and it routes the code logic to the default implementation in previous Android versions not plagued by this bug (see class source).
Original answer (kept for historical and didactic reasons):
Bug source
OK, looks like it's indeed a bug and someone else already filled it. Issue 34833.
I've found that the problem is possibly in
DatePickerDialog.java
. Where it reads:I'd guess it could have been:
Now if someone can tell me how I can propose a patch/bug report to Android, I'd be glad to. Meanwhile, I suggested a possible fix (simple) as an attached version of
DatePickerDialog.java
in the Issue there.Concept to avoid the bug
Set the listener to
null
in the constructor and create your ownBUTTON_POSITIVE
button later on. That's it, details below.The problem happens because
DatePickerDialog.java
, as you can see in the source, calls a global variable (mCallBack
) that stores the listener that was passed in the constructor:So, the trick is to provide a
null
listener to be stored as the listener, and then roll your own set of buttons (below is the original code from #1, updated):Now it will work because of the possible correction that I posted above.
And since
DatePickerDialog.java
checks for anull
whenever it readsmCallback
(since the days of API 3/1.5 it seems --- can't check Honeycomb of course), it won't trigger the exception. Considering Lollipop fixed the issue, I'm not going to look into it: just use the default implementation (covered in the class I provided).At first I was afraid of not calling the
clearFocus()
, but I've tested here and the Log lines were clean. So that line I proposed may not even be necessary after all, but I don't know.Compatibility with previous API levels (edited)
As I pointed in the comment below, that was a concept, and you can download the class I'm using from my Google Drive account. The way I used, the default system implementation is used on versions not affected by the bug.
I took a few assumptions (button names etc.) that are suitable for my needs because I wanted to reduce boilerplate code in client classes to a minimum. Full usage example:
A simple solution would be using a boolean to skip second run
For TimePickerDialog the workaround can be as follows:
I delegate all events to wrapper KitKatSetTimeListener, and only fire back to original OnTimeSetListener in case BUTTON_POSITIVE is clicked.