android - Jelly Bean DatePickerDialog: ¿hay alguna manera de cancelar?
android-4.2-jelly-bean android-datepicker (19)
--- Nota para los moderadores: Hoy (15 de julio), noté que alguien ya enfrentaba este problema here . Pero no estoy seguro de si es apropiado cerrar esto como un duplicado, ya que creo que proporcioné una explicación mucho mejor del problema. No estoy seguro de si debería editar la otra pregunta y pegar este contenido allí, pero no me siento cómodo al cambiar demasiado la pregunta de otra persona. ---
Tengo algo raro aquí.
No creo que el problema dependa de en qué SDK compile. La versión del sistema operativo del dispositivo es lo que importa.
Problema n. ° 1: inconsistencia por defecto
DatePickerDialog
se modificó (?) En Jelly Bean y ahora solo proporciona un botón Hecho . Las versiones anteriores incluían un botón Cancelar , y esto puede afectar la experiencia del usuario (incoherencia, memoria muscular de versiones anteriores de Android).
Replicar: crea un proyecto básico. Pon esto en 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();
Esperado: un botón Cancelar para aparecer en el cuadro de diálogo.
Actual: un botón Cancelar no aparece.
Capturas de pantalla: 4.0.3 (OK) y 4.1.1 (¿posiblemente incorrecto?).
Problema n. ° 2: comportamiento de descarte equivocado
El cuadro de diálogo llama a cualquier oyente al que debe llamar, y luego siempre llama OnDateSetListener
oyente OnDateSetListener
. La cancelación de llamadas inmóviles llama al método set y su configuración llama al método dos veces.
Replicar: use el código n. ° 1, pero añada el código a continuación (verá que esto resuelve el n. ° 1, pero solo visualmente / UI):
picker.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancel",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d("Picker", "Cancel!");
}
});
Esperado:
- Presionar la tecla ATRÁS o hacer clic fuera del diálogo no debería hacer nada .
- Al presionar "Cancelar", se debe imprimir Picker Cancel! .
- Presionando "Set" debería imprimir Picker Set! .
Corriente:
- Al presionar la tecla ATRÁS o hacer clic fuera del cuadro de diálogo, se imprime el juego de selección. .
- Al presionar "Cancelar" se imprime el selector ¡Cancelar! y luego Picker Set! .
- Al presionar "Establecer", se imprime el selector. y luego Picker Set! .
Líneas de registro que muestran el comportamiento:
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!
Otras notas y comentarios
- Envolverlo alrededor de un
DatePickerFragment
no importa. Simplifiqué el problema para ti, pero lo he probado.
Aquí está mi clase de solución para DatePickerDialog en el botón de cancelación, así como abandonarlo por el botón Atrás. Copie y use en estilo de DatePickerDialog (Debido a que el oyente es con estado, debemos crear una nueva instancia cuando se use, de lo contrario se requiere más código para que funcione)
Utilizar:
new FixedDatePickerDialog(this,
new FixedOnDateSetListener() {
@Override
public void onDateSet(DatePicker view, int year,
int monthOfYear, int dayOfMonth) {
if (isOkSelected()) {
// when DONE button is clicked
}
}
}, year, month, day).show();
Clase:
public class FixedDatePickerDialog extends DatePickerDialog {
private final FixedOnDateSetListener fixedCallback;
public FixedDatePickerDialog(Context context,
FixedOnDateSetListener callBack, int year, int monthOfYear,
int dayOfMonth) {
super(context, callBack, year, monthOfYear, dayOfMonth);
fixedCallback = callBack;
this.setButton(DialogInterface.BUTTON_NEGATIVE,
context.getString(R.string.cancel), this);
this.setButton(DialogInterface.BUTTON_POSITIVE,
context.getString(R.string.done), this);
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == BUTTON_POSITIVE) {
fixedCallback.setOkSelected(true);
} else {
fixedCallback.setOkSelected(false);
}
super.onClick(dialog, which);
}
public abstract static class FixedOnDateSetListener implements
OnDateSetListener {
private boolean okSelected = false;
@Override
abstract public void onDateSet(DatePicker view, int year,
int monthOfYear, int dayOfMonth);
public void setOkSelected(boolean okSelected) {
this.okSelected = okSelected;
}
public boolean isOkSelected() {
return okSelected;
}
}
}
De acuerdo con la brillante answer Ankur Chaudhary sobre el problema TimePickerDialog
similar, si onDateSet
en onDateSet
si la vista es isShown()
o no, se resolverá todo el problema con el mínimo esfuerzo, sin necesidad de extender el selector o buscar algún horrible banderas que rodean el código o incluso buscan la versión del sistema operativo, solo haga lo siguiente:
public void onDateSet(DatePicker view, int year, int month, int day) {
if (view.isShown()) {
// read the date here :)
}
}
y, por supuesto, lo mismo se puede hacer para onTimeSet
según la respuesta de Ankur
El de TimePicker basado en la solución de David Cesarino, "TL; DR: 1-2-3 pasos fáciles y lentos para una solución global"
TimePickerDialog no proporciona la funcionalidad como DatePickerDialog.getDatePicker. Por lo tanto, se debe proporcionar el oyente OnTimeSetListener . Para mantener la similitud con la solución de solución DatePicker, he mantenido el viejo concepto de mListener. Puede cambiarlo si lo necesita.
Llamar y escuchar es lo mismo que la solución original. Solo incluye
import android.app.TimePickerDialog;
import android.app.TimePickerDialog.OnTimeSetListener;
extender la clase de padres,
... implements OnDateSetListener, OnTimeSetListener
Implementar
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
...
}
Ejemplo de llamada
Calendar cal = Calendar.getInstance();
int hour = cal.get(Calendar.HOUR_OF_DAY);
int minute = cal.get(Calendar.MINUTE);
Bundle b = new Bundle();
b.putInt(TimePickerDialogFragment.HOUR, hour);
b.putInt(TimePickerDialogFragment.MINUTE, minute);
DialogFragment picker = new TimePickerDialogFragment();
picker.setArguments(b);
picker.show(getSupportFragmentManager(), "frag_time_picker");
(Actualizado para manejar cancelar)
public class TimePickerDialogFragment extends DialogFragment {
public static final String HOUR = "Hour";
public static final String MINUTE = "Minute";
private boolean isCancelled = false; //Added to handle cancel
private TimePickerDialog.OnTimeSetListener mListener;
//Added to handle parent listener
private TimePickerDialog.OnTimeSetListener mTimeSetListener = new TimePickerDialog.OnTimeSetListener() {
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
if (!isCancelled)
{
mListener.onTimeSet(view,hourOfDay,minute);
}
}
};
//
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
this.mListener = (TimePickerDialog.OnTimeSetListener) activity;
}
@Override
public void onDetach() {
this.mListener = null;
super.onDetach();
}
@TargetApi(11)
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle b = getArguments();
int h = b.getInt(HOUR);
int m = b.getInt(MINUTE);
final TimePickerDialog picker = new TimePickerDialog(getActivity(), getConstructorListener(), h, m,DateFormat.is24HourFormat(getActivity()));
//final TimePicker timePicker = new TimePicker(getBaseContext());
if (hasJellyBeanAndAbove()) {
picker.setButton(DialogInterface.BUTTON_POSITIVE,
getActivity().getString(android.R.string.ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
isCancelled = false; //Cancel flag, used in mTimeSetListener
}
});
picker.setButton(DialogInterface.BUTTON_NEGATIVE,
getActivity().getString(android.R.string.cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
isCancelled = true; //Cancel flag, used in mTimeSetListener
}
});
}
return picker;
}
private boolean hasJellyBeanAndAbove() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
}
private TimePickerDialog.OnTimeSetListener getConstructorListener() {
return hasJellyBeanAndAbove() ? mTimeSetListener : mListener; //instead of null, mTimeSetListener is returned.
}
}
En caso de que alguien quiera una solución rápida, aquí está el código que utilicé:
public void showCustomDatePicker () {
final DatePicker mDatePicker = (DatePicker) getLayoutInflater().
inflate(R.layout.date_picker_view, null);
//Set an initial date for the picker
final Calendar c = Calendar.getInstance();
int year = c.get(Calendar.YEAR);
int month = c.get(Calendar.MONTH);
int day = c.get(Calendar.DAY_OF_MONTH);
//Set the date now
mDatePicker.updateDate(year, month, day);
//create the dialog
AlertDialog.Builder mBuilder = new Builder(this);
//set the title
mBuilder.setTitle(getString(R.string.date_picker_title))
//set our date picker
.setView(mDatePicker)
//set the buttons
.setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//whatever method you choose to handle the date changes
//the important thing to know is how to retrieve the data from the picker
handleOnDateSet(mDatePicker.getYear(),
mDatePicker.getMonth(),
mDatePicker.getDayOfMonth());
}
})
.setNegativeButton(android.R.string.cancel, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
//create the dialog and show it.
.create().show();
}
Donde layout.date_picker_view es un recurso de diseño simple con un DatePicker ya que es solo un elemento:
<!xml version="1.0" encoding="utf-8">
<DatePicker xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/date_picker"
android:layout_width="fill_parent"
android:spinnersShown="true"
android:calendarViewShown="false"
android:layout_height="fill_parent"/>
Aquí está el tutorial completo en caso de que esté interesado.
Estoy usando selectores de fecha, recolectores de tiempo y selectores de números. Los selectores de números llaman a ValueChanged cada vez que el usuario selecciona un número, antes de que se retire el selector, por lo que ya tenía una estructura como esta para hacer algo con el valor solo cuando se retira el selector:
public int interimValue;
public int finalValue;
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
this.interimValue = newVal;
}
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
this.finalValue = this.interimValue;
}
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:
public int interimValue;
public int finalValue;
public boolean saveButtonClicked;
public void setup() {
picker.setButton(DialogInterface.BUTTON_POSITIVE, getString(R.string.BUTTON_SAVE), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
picker.onClick(dialog, which); // added for Android 5.0
onButtonClicked(true);
}
});
picker.setButton(DialogInterface.BUTTON_NEGATIVE, getString(R.string.BUTTON_CANCEL), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
picker.onClick(dialog, which); // added for Android 5.0
onButtonClicked(false);
}
});
}
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
this.interimValue = newVal;
}
public void onButtonClicked(boolean save) {
this.saveButtonClicked = save;
}
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (this.saveButtonClicked) {
// save
this.finalValue = this.interimValue;
} else {
// cancel
}
}
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.
Hasta que se solucione el error, sugiero no utilizar DatePickerDialog o TimePickerDialog. Utilice AlertDialog personalizado con el widget TimePicker / DatePicker;
Cambiar TimePickerDialog con;
final TimePicker timePicker = new TimePicker(this);
timePicker.setIs24HourView(true);
timePicker.setCurrentHour(20);
timePicker.setCurrentMinute(15);
new AlertDialog.Builder(this)
.setTitle("Test")
.setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d("Picker", timePicker.getCurrentHour() + ":"
+ timePicker.getCurrentMinute());
}
})
.setNegativeButton(android.R.string.cancel,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
Log.d("Picker", "Cancelled!");
}
}).setView(timePicker).show();
Cambiar DatePickerDialog con;
final DatePicker datePicker = new DatePicker(this);
datePicker.init(2012, 10, 5, null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
datePicker.setCalendarViewShown(false);
}
new AlertDialog.Builder(this)
.setTitle("Test")
.setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d("Picker", datePicker.getYear() + " "
+ (datePicker.getMonth() + 1) + " "
+ datePicker.getDayOfMonth());
}
})
.setNegativeButton(android.R.string.cancel,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
Log.d("Picker", "Cancelled!");
}
}).setView(datePicker).show();
Hay una solución muy simple, si su aplicación no utiliza la barra de acciones. Tenga en cuenta que algunas aplicaciones confían en que esta funcionalidad funcione, porque cancelar el selector de fecha tiene un significado especial (por ejemplo, borra el campo de fecha en una cadena vacía, que para algunas aplicaciones es un tipo de entrada válido y significativo) ) y el uso de indicadores booleanos para evitar que la fecha se establezca dos veces en Aceptar no lo ayudará en este caso.
Re. la solución actual, no tiene que crear botones nuevos o su propio diálogo. El punto es ser compatible tanto con las versiones anteriores de Android, las defectuosas (4. ) y las futuras, aunque es imposible estar seguro de esto último, por supuesto. Tenga en cuenta que en Android 2. , el onStop () para android.app.Dialog no hace nada, y en 4. * hace mActionBar.setShowHideAnimationEnabled (false), que es importante solo si su aplicación tiene una barra de acciones. El onStop () en DatePickerDialog, que hereda de Dialog, solo contribuye con mDatePicker.clearFocus () (a partir de la última corrección de las fuentes de Android 4.3), lo que no parece esencial.
Por lo tanto, la sustitución de onStop () por un método que no hace nada debería, en muchos casos, arreglar tu aplicación y asegurarte de que seguirá siéndolo en el futuro previsible. Por lo tanto, simplemente extienda la clase DatePickerDialog con la suya y anule onStop () con un método ficticio. También deberá proporcionar uno o dos constructores, según sus requisitos. Tenga en cuenta también que uno no debería sentirse tentado de intentar sobrepasar esta corrección, por ejemplo, intentando hacer algo con la barra de actividades directamente, ya que esto limitaría su compatibilidad con las últimas versiones de Android solamente. También tenga en cuenta que sería bueno poder llamar al súper para el OnStop () de DatePicker porque el error está solo en el onStop () en DatePickerDialog, pero no en la superclase de DatePickerDialog. Sin embargo, esto requeriría que llamaras a super.super.onStop () desde tu clase personalizada, lo que Java no te permitirá hacer, ya que va en contra de la filosofía de encapsulación :) A continuación está mi pequeña clase que solía verrregular DatePickerDialog. Espero que este comentario sea útil para alguien. Wojtek Jarosz
public class myDatePickerDialog extends DatePickerDialog {
public myDatePickerDialog(Context context, OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) {
super(context, callBack, year, monthOfYear, dayOfMonth);
}
@Override
protected void onStop() {
// Replacing tryNotifyDateSet() with nothing - this is a workaround for Android bug https://android-review.googlesource.com/#/c/61270/A
// Would also like to clear focus, but we cannot get at the private members, so we do nothing. It seems to do no harm...
// mDatePicker.clearFocus();
// Now we would like to call super on onStop(), but actually what we would mean is super.super, because
// it is super.onStop() that we are trying NOT to run, because it is buggy. However, doing such a thing
// in Java is not allowed, as it goes against the philosophy of encapsulation (the Creators never thought
// that we might have to patch parent classes from the bottom up :)
// However, we do not lose much by doing nothing at all, because in Android 2.* onStop() in androd.app.Dialog //actually
// does nothing and in 4.* it does:
// if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(false);
// which is not essential for us here because we use no action bar... QED
// So we do nothing and we intend to keep this workaround forever because of users with older devices, who might
// run Android 4.1 - 4.3 for some time to come, even if the bug is fixed in later versions of Android.
}
}
La forma en que manejé esta situación fue usando una bandera y anulando los métodos onCancel y onDismiss.
onCancel se llama solo cuando el usuario toca fuera del cuadro de diálogo o el botón Atrás. onDismiss siempre se llama
Establecer un indicador en el método onCancel puede ayudar a filtrar en el método onDismiss la intención del usuario: cancelar acción o realizar acción. Debajo de un código que muestra la idea.
public class DatePickerDialogFragment extends DialogFragment implements DatePickerDialog.OnDateSetListener {
private boolean cancelDialog = false;
private int year;
private int month;
private int day;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
DatePickerDialog dpd = new DatePickerDialog(getActivity(), this, year, month, day);
return dpd;
}
public void setDatePickerDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
@Override
public void onCancel(DialogInterface dialog) {
super.onCancel(dialog);
cancelDialog = true;
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (!cancelDialog) {
#put the code you want to execute if the user clicks the done button
}
}
@Override
public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
setDatePickerDate(year, monthOfYear, dayOfMonth);
}
}
Mi solución simple Cuando desee que vuelva a disparar simplemente ejecute "resetFired" (por ejemplo, al abrir el cuadro de diálogo nuevamente).
private class FixedDatePickerDialogListener implements DatePickerDialog.OnDateSetListener{
private boolean fired;
public void resetFired(){
fired = false;
}
public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
if (fired) {
Log.i("DatePicker", "Double fire occurred.");
return;//ignore and return.
}
//put your code here to handle onDateSet
fired = true;//first time fired
}
}
Puede anular onCancel () y usar setOnDismissListener () para detectar acciones negativas del usuario. Y con DatePickerDialog.BUTTON_POSITIVE, usted sabe que el usuario desea establecer una nueva fecha.
DatePickerDialog mDPD = new DatePickerDialog(
getActivity(), mOnDateSetListener, mYear, mMonth, mDay);
mDPD.setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
// do something onCancek
setDate = false;
}
});
mDPD.setOnDismissListener(new OnDismissListener() {
@Override
public void onDismiss(DialogInterface arg0) {
// do something onDismiss
setDate = false;
}
});
mDPD.setButton(DatePickerDialog.BUTTON_POSITIVE, "Finish", new DatePickerDialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// user set new date
setDate = true;
}
});
luego verifica setDate:
public void onDateSet(DatePicker view, int year, int month, int day) {
if(setDate){
//do something with new date
}
}
Una solución simple sería usar un booleano para omitir la segunda ejecución
boolean isShow = false; // define global variable
// when showing time picker
TimePickerDialog timeDlg = new TimePickerDialog( this, new OnTimeSetListener()
{
@Override
public void onTimeSet( TimePicker view, int hourOfDay, int minute )
{
if ( isShow )
{
isShow = false;
// your code
}
}
}, 8, 30, false );
timeDlg.setButton( TimePickerDialog.BUTTON_NEGATIVE, "Cancel", new DialogInterface.OnClickListener()
{
@Override
public void onClick( DialogInterface dialog, int which )
{
isShow = false;
}
} );
timeDlg.setButton( TimePickerDialog.BUTTON_POSITIVE, "Set", new DialogInterface.OnClickListener()
{
@Override
public void onClick( DialogInterface dialog, int which )
{
isShow = true;
}
} );
timeDlg.show();
Voy a agregar mi propio riff sobre la solución que David Cesarino publicó, en caso de que no estés usando Fragments, y quieres una manera fácil de solucionarlo en todas las versiones (2.1 a 4.1):
public class FixedDatePickerDialog extends DatePickerDialog {
//I use a Calendar object to initialize it, but you can revert to Y,M,D easily
public FixedDatePickerDialog(Calendar dateToShow, Context context, OnDateSetListener callBack) {
super(context, null, dateToShow.get(YEAR), dateToShow.get(MONTH), dateToShow.get(DAY_OF_MONTH));
initializePicker(callBack);
}
public FixedDatePickerDialog(Calendar dateToShow, Context context, int theme,
OnDateSetListener callBack) {
super(context, theme, null, dateToShow.get(YEAR), dateToShow.get(MONTH), dateToShow.get(DAY_OF_MONTH));
initializePicker(callBack);
}
private void initializePicker(final OnDateSetListener callback) {
try {
//If you''re only using Honeycomb+ then you can just call getDatePicker() instead of using reflection
Field pickerField = DatePickerDialog.class.getDeclaredField("mDatePicker");
pickerField.setAccessible(true);
final DatePicker picker = (DatePicker) pickerField.get(this);
this.setCancelable(true);
this.setButton(DialogInterface.BUTTON_NEGATIVE, getContext().getText(android.R.string.cancel), (OnClickListener) null);
this.setButton(DialogInterface.BUTTON_POSITIVE, getContext().getText(android.R.string.ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
picker.clearFocus(); //Focus must be cleared so the value change listener is called
callback.onDateSet(picker, picker.getYear(), picker.getMonth(), picker.getDayOfMonth());
}
});
} catch (Exception e) { /* Reflection probably failed*/ }
}
}
Nota: Corregido como de Lollipop , fuente aquí . También se actualizó la clase automatizada para uso en clientes (compatible con todas las versiones de Android).
TL; DR: 1-2-3 pasos fáciles para una solución global:
- Descarga esta clase.
- Implemente
OnDateSetListener
en su actividad (o cambie la clase según sus necesidades). Dispare el diálogo con este código (en esta muestra, lo uso dentro de un
Fragment
):Bundle b = new Bundle(); b.putInt(DatePickerDialogFragment.YEAR, 2012); b.putInt(DatePickerDialogFragment.MONTH, 6); b.putInt(DatePickerDialogFragment.DATE, 17); DialogFragment picker = new DatePickerDialogFragment(); picker.setArguments(b); picker.show(getActivity().getSupportFragmentManager(), "frag_date_picker");
¡Y eso es todo! La razón por la que sigo manteniendo mi respuesta como "aceptada" es porque aún prefiero mi solución, ya que tiene una huella muy pequeña en el código del cliente, aborda el problema fundamental (el oyente llamado en la clase framework) funciona bien en los cambios de configuración y enruta la lógica del código a la implementación predeterminada en versiones anteriores de Android que no están plagadas por este error (ver fuente de clase).
Respuesta original (guardada por razones históricas y didácticas):
Fuente de error
OK, parece que de hecho es un error y alguien más ya lo completó. Número 34833 .
He descubierto que el problema posiblemente esté en DatePickerDialog.java
. Donde dice:
private void tryNotifyDateSet() {
if (mCallBack != null) {
mDatePicker.clearFocus();
mCallBack.onDateSet(mDatePicker, mDatePicker.getYear(),
mDatePicker.getMonth(), mDatePicker.getDayOfMonth());
}
}
@Override
protected void onStop() {
tryNotifyDateSet();
super.onStop();
}
Supongo que podría haber sido:
@Override
protected void onStop() {
// instead of the full tryNotifyDateSet() call:
if (mCallBack != null) mDatePicker.clearFocus();
super.onStop();
}
Ahora, si alguien puede decirme cómo puedo proponer un informe de parche / error a Android, me alegraría. Mientras tanto, sugerí una posible solución (simple) como una versión adjunta de DatePickerDialog.java
en el Issue allí.
Concepto para evitar el error
Configure el oyente como null
en el constructor y luego cree su propio botón BUTTON_POSITIVE
. Eso es todo, detalles a continuación.
El problema ocurre porque DatePickerDialog.java
, como se puede ver en la fuente, llama a una variable global ( mCallBack
) que almacena el oyente que se pasó en el constructor:
/**
* @param context The context the dialog is to run in.
* @param callBack How the parent is notified that the date is set.
* @param year The initial year of the dialog.
* @param monthOfYear The initial month of the dialog.
* @param dayOfMonth The initial day of the dialog.
*/
public DatePickerDialog(Context context,
OnDateSetListener callBack,
int year,
int monthOfYear,
int dayOfMonth) {
this(context, 0, callBack, year, monthOfYear, dayOfMonth);
}
/**
* @param context The context the dialog is to run in.
* @param theme the theme to apply to this dialog
* @param callBack How the parent is notified that the date is set.
* @param year The initial year of the dialog.
* @param monthOfYear The initial month of the dialog.
* @param dayOfMonth The initial day of the dialog.
*/
public DatePickerDialog(Context context,
int theme,
OnDateSetListener callBack,
int year,
int monthOfYear,
int dayOfMonth) {
super(context, theme);
mCallBack = callBack;
// ... rest of the constructor.
}
Por lo tanto, el truco consiste en proporcionar un oyente null
para que se almacene como el oyente, y luego enrollar su propio conjunto de botones (a continuación se muestra el código original del # 1, actualizado):
DatePickerDialog picker = new DatePickerDialog(
this,
null, // instead of a listener
2012, 6, 15);
picker.setCancelable(true);
picker.setCanceledOnTouchOutside(true);
picker.setButton(DialogInterface.BUTTON_POSITIVE, "OK",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d("Picker", "Correct behavior!");
}
});
picker.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancel",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d("Picker", "Cancel!");
}
});
picker.show();
Ahora funcionará debido a la posible corrección que publiqué anteriormente.
Y dado que DatePickerDialog.java
busca un null
cada vez que lee mCallback
( ya que los días de la API 3 / 1.5 parecen --- no se puede verificar Honeycomb, por supuesto), no activará la excepción. Teniendo en cuenta que Lollipop solucionó el problema, no voy a analizarlo: simplemente use la implementación predeterminada (cubierta en la clase que proporcioné).
Al principio tenía miedo de no llamar a clearFocus()
, pero lo he probado aquí y las líneas de registro estaban limpias. Entonces esa línea que propuse puede que ni siquiera sea necesaria después de todo, pero no sé.
Compatibilidad con niveles de API previos (editados)
Como señalé en el comentario a continuación, ese era un concepto, y puedes descargar la clase que estoy usando desde mi cuenta de Google Drive . La forma en que usé, la implementación del sistema por defecto se usa en versiones que no se ven afectadas por el error.
Tomé algunas suposiciones (nombres de botones, etc.) que son adecuadas para mis necesidades porque quería reducir al mínimo el código repetitivo en las clases de clientes. Ejemplo de uso completo:
class YourActivity extends SherlockFragmentActivity implements OnDateSetListener
// ...
Bundle b = new Bundle();
b.putInt(DatePickerDialogFragment.YEAR, 2012);
b.putInt(DatePickerDialogFragment.MONTH, 6);
b.putInt(DatePickerDialogFragment.DATE, 17);
DialogFragment picker = new DatePickerDialogFragment();
picker.setArguments(b);
picker.show(getActivity().getSupportFragmentManager(), "fragment_date_picker");
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:
datePickerDialog = new DatePickerDialog(getContext(), null, dateSearch.get(Calendar.YEAR), dateSearch.get(Calendar.MONTH), dateSearch.get(Calendar.DAY_OF_MONTH));
datePickerDialog.setCancelable(false);
datePickerDialog.setButton(DialogInterface.BUTTON_POSITIVE, getString(R.string.dialog_ok), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d("Debug", "Correct");
onDateSearchSetListener.onDateSet(datePickerDialog.getDatePicker(), datePickerDialog.getDatePicker().getYear(), datePickerDialog.getDatePicker().getMonth(), datePickerDialog.getDatePicker().getDayOfMonth());
}
});
datePickerDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getString(R.string.dialog_cancel), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d("Debug", "Cancel");
dialog.dismiss();
}
});
For TimePickerDialog the workaround can be as follows:
TimePickerDialog createTimePickerDialog(Context context, int themeResId, TimePickerDialog.OnTimeSetListener orignalListener,
int hourOfDay, int minute, boolean is24HourView) {
class KitKatTimeSetListener implements TimePickerDialog.OnTimeSetListener {
private int hour;
private int minute;
private KitKatTimeSetListener() {
}
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
this.hour = hourOfDay;
this.minute = minute;
}
private int getHour() { return hour; }
private int getMinute() {return minute; }
};
KitKatTimeSetListener kitkatTimeSetListener = new KitKatTimeSetListener();
TimePickerDialog timePickerDialog = new TimePickerDialog(context, themeResId, kitkatTimeSetListener, hourOfDay, minute, is24HourView);
timePickerDialog.setButton(DialogInterface.BUTTON_POSITIVE, context.getString(android.R.string.ok), (dialog, which) -> {
timePickerDialog.onClick(timePickerDialog, DialogInterface.BUTTON_POSITIVE);
orignalListener.onTimeSet(new TimePicker(context), kitkatTimeSetListener.getHour(), kitkatTimeSetListener.getMinute());
dialog.cancel();
});
timePickerDialog.setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(android.R.string.cancel), (dialog, which) -> {
dialog.cancel();
});
return timePickerDialog;
}
I delegate all events to wrapper KitKatSetTimeListener, and only fire back to original OnTimeSetListener in case BUTTON_POSITIVE is clicked.
I know this post has been here for almost a year but I thought I should post my findings. You could still keep the listener(instead of setting it to mull) and still have this work as expected. The key is to implicitly set the "OK" or(and) the "cancel" buttons. I tested it and it works gratefully for me. The listener does not get fired twice.
Look at this example,
private void setTime(){
final Calendar c = Calendar.getInstance();
int hour = c.get(Calendar.HOUR_OF_DAY);
int minute = c.get(Calendar.MINUTE);
final TimePickerDialog timepicker = new TimePickerDialog(this.getActivity(),
timePickerListener,
hour,
minute,
DateFormat.is24HourFormat(getActivity()));
timepicker.setButton(DialogInterface.BUTTON_POSITIVE, "Print", new
android.content.DialogInterface.OnClickListener(){
@Override
public void onClick(DialogInterface dialog,int which) {
print = true;
timepicker.dismiss();
}
});
timepicker.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancel", new
android.content.DialogInterface.OnClickListener(){
@Override
public void onClick(DialogInterface dialog,int which){
print = false;
timepicker.dismiss();
}
});
timepicker.setCancelable(false);
timepicker.show();
}
I liked David Cesarino''s answer above, but wanted something that was a drop in replacement for the broken dialog and would work on any dialog that might be missing cancel / have incorrect cancel behavior. Here are derived classes for DatePickerDialog / TimePickerDialog that should work as drop in replacements. These are not custom views. It uses the system dialog, but just changes the cancel / back button behavior to work as expected.
This should work on API level 3 and higher. So, basically any version of Android (I tested it on jellybean and lollipop specifically).
DatePickerDialog:
package snappy_company_name_here;
import android.content.Context;
import android.content.DialogInterface;
import android.widget.DatePicker;
/**
* This is a modified version of DatePickerDialog that correctly handles cancellation behavior since it''s broken on jellybean and
* kitkat date pickers.
*
* Here is the bug: http://code.google.com/p/android/issues/detail?id=34833
* Here is an SO post with a bunch of details: http://.com/questions/11444238/jelly-bean-datepickerdialog-is-there-a-way-to-cancel
*
* @author stuckj, created on 5/5/15.
*/
public class DatePickerDialog extends android.app.DatePickerDialog implements DialogInterface.OnClickListener
{
final CallbackHelper callbackHelper;
// NOTE: Must be static since we''re using it in a super constructor call. Which is annoying, but necessary
private static class CallbackHelper implements OnDateSetListener
{
private final OnDateSetListener callBack;
private boolean dialogButtonPressHandled = false; // To prevent setting the date when the dialog is dismissed...
// NOTE: Must be static since we''re using it in a super constructor call. Which is annoying, but necessary
public CallbackHelper(final OnDateSetListener callBack)
{
this.callBack = callBack;
}
@Override
public void onDateSet(final DatePicker view, final int year, final int monthOfYear, final int dayOfMonth)
{
if (!dialogButtonPressHandled && (callBack != null))
{
callBack.onDateSet(view, year, monthOfYear, dayOfMonth);
}
}
}
/**
* Sets the positive and negative buttons to use the dialog callbacks we define.
*/
private void setButtons(final Context context)
{
setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(android.R.string.cancel), this);
setButton(DialogInterface.BUTTON_POSITIVE, context.getString(android.R.string.ok), this);
}
@Override
public void onClick(final DialogInterface dialog, final int which)
{
// ONLY call the super method in the positive case...
if (which == DialogInterface.BUTTON_POSITIVE)
{
super.onClick(dialog, which);
}
callbackHelper.dialogButtonPressHandled = true;
}
@Override
public void onBackPressed()
{
getButton(DialogInterface.BUTTON_NEGATIVE).performClick();
}
// Need this so we can both pass callbackHelper to the super class and save it off as a variable.
private DatePickerDialog(final Context context,
final OnDateSetListener callBack,
final int year,
final int monthOfYear,
final int dayOfMonth,
final CallbackHelper callbackHelper)
{
super(context, callbackHelper, year, monthOfYear, dayOfMonth);
this.callbackHelper = callbackHelper;
setButtons(context);
}
/**
* @param context The context the dialog is to run in.
* @param callBack How the parent is notified that the date is set.
* @param year The initial year of the dialog.
* @param monthOfYear The initial month of the dialog.
* @param dayOfMonth The initial day of the dialog.
*/
public DatePickerDialog(final Context context,
final OnDateSetListener callBack,
final int year,
final int monthOfYear,
final int dayOfMonth)
{
this(context, callBack, year, monthOfYear, dayOfMonth, new CallbackHelper(callBack));
}
// Need this so we can both pass callbackHelper to the super class and save it off as a variable.
private DatePickerDialog(final Context context, final int theme, final OnDateSetListener listener, final int year,
final int monthOfYear, final int dayOfMonth, final CallbackHelper callbackHelper)
{
super(context, theme, callbackHelper, year, monthOfYear, dayOfMonth);
this.callbackHelper = callbackHelper;
setButtons(context);
}
/**
* @param context The context the dialog is to run in.
* @param theme the theme to apply to this dialog
* @param listener How the parent is notified that the date is set.
* @param year The initial year of the dialog.
* @param monthOfYear The initial month of the dialog.
* @param dayOfMonth The initial day of the dialog.
*/
public DatePickerDialog(final Context context, final int theme, final OnDateSetListener listener, final int year,
final int monthOfYear, final int dayOfMonth)
{
this(context, theme, listener, year, monthOfYear, dayOfMonth, new CallbackHelper(listener));
}
}
TimePickerDialog:
package snappy_company_name_here;
import android.content.Context;
import android.content.DialogInterface;
import android.widget.TimePicker;
/**
* This is a modified version of TimePickerDialog that correctly handles cancellation behavior since it''s broken on jellybean and
* kitkat date pickers.
*
* Here is the bug: http://code.google.com/p/android/issues/detail?id=34833
* Here is an SO post with a bunch of details: http://.com/questions/11444238/jelly-bean-datepickerdialog-is-there-a-way-to-cancel
*
* @author stuckj, created on 5/5/15.
*/
public class TimePickerDialog extends android.app.TimePickerDialog implements DialogInterface.OnClickListener
{
final CallbackHelper callbackHelper;
// NOTE: Must be static since we''re using it in a super constructor call. Which is annoying, but necessary
private static class CallbackHelper implements OnTimeSetListener
{
private final OnTimeSetListener callBack;
private boolean dialogButtonPressHandled = false; // To prevent setting the date when the dialog is dismissed...
// NOTE: Must be static since we''re using it in a super constructor call. Which is annoying, but necessary
public CallbackHelper(final OnTimeSetListener callBack)
{
this.callBack = callBack;
}
@Override
public void onTimeSet(final TimePicker view, final int hourOfDay, final int minute)
{
if (!dialogButtonPressHandled && (callBack != null))
{
callBack.onTimeSet(view, hourOfDay, minute);
}
}
}
/**
* Sets the positive and negative buttons to use the dialog callbacks we define.
*/
private void setButtons(final Context context)
{
setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(android.R.string.cancel), this);
setButton(DialogInterface.BUTTON_POSITIVE, context.getString(android.R.string.ok), this);
}
@Override
public void onClick(final DialogInterface dialog, final int which)
{
// ONLY call the super method in the positive case...
if (which == DialogInterface.BUTTON_POSITIVE)
{
super.onClick(dialog, which);
}
callbackHelper.dialogButtonPressHandled = true;
}
@Override
public void onBackPressed()
{
getButton(DialogInterface.BUTTON_NEGATIVE).performClick();
}
// Need this so we can both pass callbackHelper to the super class and save it off as a variable.
private TimePickerDialog(final Context context,
final OnTimeSetListener callBack,
final int hourOfDay, final int minute, final boolean is24HourView, final CallbackHelper callbackHelper)
{
super(context, callbackHelper, hourOfDay, minute, is24HourView);
this.callbackHelper = callbackHelper;
setButtons(context);
}
/**
* @param context Parent.
* @param callBack How parent is notified.
* @param hourOfDay The initial hour.
* @param minute The initial minute.
* @param is24HourView Whether this is a 24 hour view, or AM/PM.
*/
public TimePickerDialog(final Context context,
final OnTimeSetListener callBack,
final int hourOfDay, final int minute, final boolean is24HourView)
{
this(context, callBack, hourOfDay, minute, is24HourView, new CallbackHelper(callBack));
}
// Need this so we can both pass callbackHelper to the super class and save it off as a variable.
private TimePickerDialog(final Context context, final int theme, final OnTimeSetListener callBack, final int hourOfDay,
final int minute, final boolean is24HourView, final CallbackHelper callbackHelper)
{
super(context, theme, callbackHelper, hourOfDay, minute, is24HourView);
this.callbackHelper = callbackHelper;
setButtons(context);
}
/**
* @param context Parent.
* @param theme the theme to apply to this dialog
* @param callBack How parent is notified.
* @param hourOfDay The initial hour.
* @param minute The initial minute.
* @param is24HourView Whether this is a 24 hour view, or AM/PM.
*/
public TimePickerDialog(final Context context, final int theme, final OnTimeSetListener callBack, final int hourOfDay,
final int minute, final boolean is24HourView)
{
this(context, theme, callBack, hourOfDay, minute, is24HourView, new CallbackHelper(callBack));
}
}
My working version with ClearButton using Lambda Expressions:
public class DatePickerFragment extends DialogFragment {
private OnDateSelectListener dateSelectListener;
private OnDateClearListener dateClearListener;
public void setDateSelectListener(OnDateSelectListener dateSelectListener) {
this.dateSelectListener = dateSelectListener;
}
public void setDateClearListener(OnDateClearListener dateClearListener) {
this.dateClearListener = dateClearListener;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Use the current date as the default date in the picker
final Calendar c = Calendar.getInstance();
int year = c.get(Calendar.YEAR);
int month = c.get(Calendar.MONTH);
int day = c.get(Calendar.DAY_OF_MONTH);
// Create a new instance of DatePickerDialog and return it
DatePickerDialog dialog = new DatePickerDialog(getActivity(), null, year, month, day);
dialog.setCancelable(true);
dialog.setCanceledOnTouchOutside(true);
dialog.setTitle("Select Date");
dialog.setButton(BUTTON_POSITIVE, ("Done"), (dialog1, which) -> {
DatePicker dp = dialog.getDatePicker();
dialog.dismiss();
dateSelectListener.onDateSelect(dp.getYear(), dp.getMonth(), dp.getDayOfMonth());
});
dialog.setButton(BUTTON_NEUTRAL, ("Clear"), (dialog1, which) -> {
dialog.dismiss();
dateClearListener.onDateClear();
});
dialog.setButton(BUTTON_NEGATIVE, ("Cancel"), (dialog1, which) -> {
if (which == DialogInterface.BUTTON_NEGATIVE) {
dialog.cancel();
}
});
dialog.getDatePicker().setCalendarViewShown(false);
return dialog;
}
public interface OnDateClearListener {
void onDateClear();
}
public interface OnDateSelectListener {
void onDateSelect(int year, int monthOfYear, int dayOfMonth);
}
}
Pruebe los conceptos a continuación.
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();
el método onDateSet () llama dos veces (si está comprobando emulator.it llama dos veces. Si está utilizando un dispositivo real, se llamará correctamente una sola vez. Si está utilizando un emulador, utilice el contador. Si está trabajando en un dispositivo real, entonces ignorar la variable contraria. Para el dispositivo real, funciona para mí.)
cuando el usuario hace clic en el botón en DatePickerDialog.
para esto debes mantener un valor de contador y nada funciona cuando el método llama por primera vez y realizar la operación cuando el método llame por segunda vez.
Consulte los fragmentos de código a continuación
static int counter=0; //Counter will be declared globally.
DatePickerDialog picker = new DatePickerDialog(
this,
new OnDateSetListener() {
@Override
public void onDateSet(DatePicker v, int y, int m, int d) {
counter++;
if(counter==1) return;
counter=0;
//Do the operations here
}
},
2012, 6, 15);
picker.show();
Para cancelar datepicker dilalog está funcionando para mí. Para emulator no funciona
DialogInterface.OnClickListener dialogOnClickListener=new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which) {
// TODO Auto-generated method stub
if(which==Dialog.BUTTON_NEGATIVE)
{
Log.i(tagName, "dialog negative button clicked");
dialog.dismiss();
}
}
};
mDatePickerDialog.setButton(Dialog.BUTTON_NEGATIVE, "Cancel", dialogOnClickListener);
Está funcionando para mí para un dispositivo real. Pero para el emulador no funciona correctamente. Creo que es un error del emulador de Android.