diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..d8844f1e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: android +jdk: + - oraclejdk8 +android: + components: + - tools + - platform-tools + - build-tools-27.0.2 + - android-27 + - android-14 + - extra-android-m2repository + +script: ./gradlew check diff --git a/README.md b/README.md index 92afc6ab..f12d3fc6 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Join the chat at https://gitter.im/wdullaer/MaterialDateTimePicker](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/wdullaer/MaterialDateTimePicker?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ![Maven Central](https://img.shields.io/maven-central/v/com.wdullaer/materialdatetimepicker.svg) +![Build Status](https://travis-ci.org/wdullaer/MaterialDateTimePicker.svg?branch=master) Material DateTime Picker tries to offer you the date and time pickers as shown in [the Material Design spec](http://www.google.com/design/spec/components/pickers.html), with an @@ -39,9 +40,9 @@ Date Picker | Time Picker ## Setup The easiest way to add the Material DateTime Picker library to your project is by adding it as a dependency to your `build.gradle` -```java +```groovy dependencies { - compile 'com.wdullaer:materialdatetimepicker:3.1.3' + compile 'com.wdullaer:materialdatetimepicker:3.5.1' } ``` @@ -141,7 +142,7 @@ Shows a title at the top of the `TimePickerDialog` * `DatePickerDialog` `setTitle(String title)` Shows a title at the top of the `DatePickerDialog` instead of the day of the week -* `setOkText()` and `setCancelText()` +* `setOkText()` and `setCancelText()` Set a custom text for the dialog Ok and Cancel labels. Can take a resourceId of a String. Works in both the DatePickerDialog and TimePickerDialog * `setMinTime(Timepoint time)` @@ -150,11 +151,18 @@ Set the minimum valid time to be selected. Time values earlier in the day will b * `setMaxTime(Timepoint time)` Set the maximum valid time to be selected. Time values later in the day will be deactivated -* `setSelectableTimes(Timepoint[] times)` -You can pass in an array of `Timepoints`. These values are the only valid selections in the picker. `setMinTime(Timepoint time)` and `setMaxTime(Timepoint time)` will further trim this list down. +* `setSelectableTimes(Timepoint[] times)` +You can pass in an array of `Timepoints`. These values are the only valid selections in the picker. `setMinTime(Timepoint time)`, `setMaxTime(Timepoint time)` and `setDisabledTimes(Timepoint[] times)` will further trim this list down. Try to specify Timepoints only up to the resolution of your picker (i.e. do not add seconds if the resolution of the picker is minutes). + +* `setDisabledTimes(Timepoint[] times)` +You can pass in an array of `Timepoints`. These values will not be available for selection. These take precedence over `setSelectableTimes` and `setTimeInterval`. Be careful when using this without selectableTimes: rounding to a valid Timepoint is a very expensive operation if a lot of consecutive Timepoints are disabled. Try to specify Timepoints only up to the resolution of your picker (i.e. do not add seconds if the resolution of the picker is minutes). + +* `setTimeInterval(int hourInterval, int minuteInterval, int secondInterval)` +Set the interval for selectable times in the TimePickerDialog. This is a convenience wrapper around `setSelectableTimes`. The interval for all three time components can be set independently. If you are not using the seconds / minutes picker, set the respective item to 60 for better performance. -* `setTimeInterval(int hourInterval, int minuteInterval, int secondInterval)` -Set the interval for selectable times in the TimePickerDialog. This is a convenience wrapper around `setSelectableTimes` +* `setTimepointLimiter(TimepointLimiter limiter)` +Pass in a custom implementation of `TimeLimiter` +Disables `setSelectableTimes`, `setDisabledTimes`, `setTimeInterval`, `setMinTime` and `setMaxTime` * `setSelectableDays(Calendar[] days)` You can pass a `Calendar[]` to the `DatePickerDialog`. The values in this list are the only acceptable dates for the picker. It takes precedence over `setMinDate(Calendar day)` and `setMaxDate(Calendar day)` @@ -185,14 +193,24 @@ Set whether the dialogs should vibrate the device when a selection is made. This * `dismissOnPause(boolean dismissOnPause)` Set whether the picker dismisses itself when the parent Activity is paused or whether it recreates itself when the Activity is resumed. +* `setLocale(Locale locale)` +Allows the client to set a custom locale that will be used when generating various strings in the pickers. By default the current locale of the device will be used. Because the pickers will adapt to the Locale of the device by default you should only have to use this in very rare circumstances. + * `DatePickerDialog` `autoDismiss(boolean autoDismiss)` If set to `true` will dismiss the picker when the user selects a date. This defaults to `false`. * `TimepickerDialog` `enableSeconds(boolean enableSconds)` and `enableMinutes(boolean enableMinutes)` -Allows you to enable or disable a seconds and minutes picker ont he `TimepickerDialog`. Enabling the seconds picker, implies enabling the minutes picker. Disabling the minute picker will disable the seconds picker. The last applied setting will be used. By default `enableSeconds = false` and `enableMinutes = true`. +Allows you to enable or disable a seconds and minutes picker on the `TimepickerDialog`. Enabling the seconds picker, implies enabling the minutes picker. Disabling the minute picker will disable the seconds picker. The last applied setting will be used. By default `enableSeconds = false` and `enableMinutes = true`. -* `DatePickerDialog` `setTimeZone(Timezone timezone)` +* `DatePickerDialog` `setTimeZone(Timezone timezone)` *deprecated* Sets the `Timezone` used to represent time internally in the picker. Defaults to the current default Timezone of the device. +This method has been deprecated: you should use the `newInstance()` method which takes a Calendar set to the appropriate TimeZone. + +* `DatePickerDialog` `setDateRangeLimiter(DateRangeLimiter limiter)` +Provide a custom implementation of DateRangeLimiter, giving you full control over which days are available for selection. This disables all of the other options that limit date selection. + +* `getOnTimeSetListener()` and `getOnDateSetListener()` +Getters that allow the retrieval of a reference to the callbacks currently associated with the pickers ## FAQ @@ -200,7 +218,7 @@ Sets the `Timezone` used to represent time internally in the picker. Defaults to Not using the support library versions has been a well considered choice, based on the following considerations: * Less than 5% of the devices using the android market do not support native `Fragments`, a number which will decrease even further going forward. -* Even if you use `SupportFragments` in your application, you can still use the normal `FragmentManager` +* Even if you use `SupportFragments` in your application, you can still use the normal `FragmentManager`. Both can exist side by side. This means that in the current setup everyone can use the library: people using the support library and people not using the support library. @@ -208,31 +226,112 @@ Finally changing to `SupportDialogFragment` now will break the API for all the p If you do really need `SupportDialogFragment`, you can fork the library (It involves changing all of 2 lines of code, so it should be easy enough to keep it up to date with the upstream) or use this fork: https://github.com/infinum/MaterialDateTimePicker -```java +```groovy dependencies { - compile 'co.infinum:materialdatetimepicker-support:3.1.3' + compile 'co.infinum:materialdatetimepicker-support:3.5.1' } ``` ### Why does the `DatePickerDialog` return the selected month -1? In the java `Calendar` class months use 0 based indexing: January is month 0, December is month 11. This convention is widely used in the java world, for example the native Android DatePicker. +### How do I use a different version of the support library in my app? +This library depends on the android support library. Because the jvm allows only one version of a fully namespaced class to be loaded, you will run into issues if your app depends on a different version of the support library than the one used in this app. Gradle is generally quite good at resolving version conflicts (be default it will retain the latest version of a library), but should you run into problems (eg because you disabled conflict resolution), you can disable loading the support +library for MaterialDateTimePicker. + +Using the following snippet in your apps `build.gradle` file you can exclude this library's transitive support library dependency from being installed. + +```groovy +compile ('com.wdullaer:materialdatetimepicker:3.5.1') { + exclude group: 'com.android.support' +} +``` + +Your app will need to depend on at least the following pieces of the support library + +```groovy +compile 'com.android.support:support-v4:26.0.1' +compile 'com.android.support:support-v13:26.0.1' +compile 'com.android.support:design:26.0.1' +``` + +This will work fine as long as the support library version your app depends on is recent enough (supports `RecyclerView`) and google doesn't release a version in the future that contains breaking changes. (If/When this happens I will try hard to document this). See issue [#338](https://github.com/wdullaer/MaterialDateTimePicker/issues/338) for more information. + +### How do I turn this into a year and month picker? +This DatePickerDialog focusses on selecting dates, which means that it's central design element is the day picker. As this calendar like view is the center of the design it makes no sense to try and disable it. As such selecting just years and months, without a day, is not in scope for this library and will not be added. + ### How do I use my custom logic to enable/disable dates? -`DatePickerDialog` exposes some utility methods to enable / disable dates for common scenario's. If your needs are not covered by these, you can override the `isOutOfRange()` method by extending the `DatePickerDialog` class. +`DatePickerDialog` exposes some utility methods to enable / disable dates for common scenario's. If your needs are not covered by these, you can supply a custom implementation of the `DateRangeLimiter` interface. +Because the `DateRangeLimiter` is preserved when the `Dialog` pauzes, your implementation must also implement `Parcelable`. ```java -class MyDatePickerDialog extends DatePickerDialog { - @override +class MyDateRangeLimiter implements DateRangeLimiter { + public MyDateRangeLimiter(Parcel in) { + + } + + @Override + public int getMinYear() { + return 1900; + } + + @Override + public int getMaxYear() { + return 2100; + } + + @Override + public Calendar getStartDate() { + Calendar output = Calendar.newInstance(); + output.set(Calendar.YEAR, 1900); + output.set(Calendar.DAY_OF_MONTH, 1); + output.set(Calendar.MONTH, Calendar.JANUARY); + return output; + } + + @Override + public Calendar getEndDate() { + Calendar output = Calendar.newInstance(); + output.set(Calendar.YEAR, 2100); + output.set(Calendar.DAY_OF_MONTH, 1); + output.set(Calendar.MONTH, Calendar.JANUARY); + return output; + } + + @Override public boolean isOutOfRange(int year, int month, int day) { - // disable days that are odd - return day % 2 == 1; + return false; + } + + @Override + public Calendar setToNearestDate(Calendar day) { + return day; } + + @Override + public void writeToParcel(Parcel out) { + + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public MyDateRangeLimiter createFromParcel(Parcel in) { + return new MyDateRangeLimiter(in); + } + + public MyDateRangeLimiter[] newArray(int size) { + return new MyDateRangeLimiter[size]; + } + }; } ``` -> You need to override `isOutOfRange()` with this signature, not the one with the Calendar signature. - -When you override `isOutOfRange()` the built-in methods for setting the enabled / disabled dates will no longer work. It will need to be completely handled by your implementation. +When you provide a custom `DateRangeLimiter` the built-in methods for setting the enabled / disabled dates will no longer work. It will need to be completely handled by your implementation. ### Why are my callbacks lost when the device changes orientation? The simple solution is to dismiss the pickers when your activity is paused. diff --git a/README_ES.md b/README_ES.md new file mode 100644 index 00000000..cce63539 --- /dev/null +++ b/README_ES.md @@ -0,0 +1,381 @@ +# Material DateTime Picker - Seleccione una hora/fecha con estilo + +[![Únete al chat en https://gitter.im/wdullaer/MaterialDateTimePicker](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/wdullaer/MaterialDateTimePicker?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +![Maven Central](https://img.shields.io/maven-central/v/com.wdullaer/materialdatetimepicker.svg) +![estado de compilación](https://travis-ci.org/wdullaer/MaterialDateTimePicker.svg?branch=master) + + +Material DateTime Picker intenta ofrecerle los selectores de fecha y hora como se muestra en [Especificación de diseño de materiales](http://www.google.com/design/spec/components/pickers.html), con una API + fácil de usar. +La biblioteca utiliza [el código de los marcos de Android](https://android.googlesource.com/platform/frameworks/opt/datetimepicker/) La biblioteca utiliza [el código de los marcos de Android] como base y lo ajusta para que esté lo más idéntico posible al ejemplo de los diseños de materiales. + +Soporte para Android 4.0 y superior. + +Siéntase libre de crear un _fork_ o emitir solicitudes de _pull request_ en github. Los problemas se pueden informar en el rastreador de problemas github. + +**Diseño de la versión #2** + +Selector de fecha | Selector de tiempo +--- | --- +![Selector de fechas](https://raw.github.com/wdullaer/MaterialDateTimePicker/gh-pages/images/date_picker_v2.png) | ![Selector de tiempo](https://raw.github.com/wdullaer/MaterialDateTimePicker/gh-pages/images/time_picker_v2.png) + +**Diseño de la versión #1** + +Selector de fecha | Selector de tiempo +---- | ---- +![Selector de fechas](https://raw.github.com/wdullaer/MaterialDateTimePicker/gh-pages/images/date_picker.png) | ![Selector de tiempo](https://raw.github.com/wdullaer/MaterialDateTimePicker/gh-pages/images/time_picker.png) + + +## Tabla de contenido +1. [Ajustar](#setup) +2. [Usar selectores de fecha/hora de material](#using-material-datetime-pickers) +1. [Implementar oyentes](#implement-an-ontimesetlistenerondatesetlistener) +2. [Crear selectores](#create-a-timepickerdialogdatepickerdialog-using-the-supplied-factory) +3. [Elección de tema para los Pickers](#theme-the-pickers) +3. [Opciones adicionales](#additional-options) +4. [Preguntas más frecuentes](#faq) +5. [Mejoras potenciales](#potential-improvements) +6. [Licencia](#license) + + +## Ajustes +La forma más fácil de agregar a la Biblioteca la _Material DateTime Picker_ a su proyecto es agregarla como una dependencia `build.gradle` +```groovy +a sus dependencias{ + compile 'com.wdullaer:materialdatetimepicker:3.5.1' +} +``` + +También puede agregar la biblioteca como una biblioteca de Android a su proyecto. Todos los archivos de la biblioteca están en `library` + + +## Usar Material DateTime Picker selectores de fecha/hora +La biblioteca sigue la misma API que otros selectores en el marco de Android. +Para una implementación básica, necesitarás + +1. Implementar un `OnTimeSetListener`/`OnDateSetListener` Oyente en tiempo establecido/Oyente en la fecha establecida +2. Crear un `TimePickerDialog`/`DatePickerDialog` Diagrama de selector de tiempo/Diagrama selector de fecha usando la fabricaciones previas suministradas +3. Dale un tema a los selectores + +### Implementa la opción `OnTimeSetListener`/`OnDateSetListener` En oyente tiempo establecido/Oyente en la fecha establecida +Para recibir la fecha u hora configurada en el selector, deberá implementar las interfaces `OnTimeSetListener` En oyente tiempo establecido o + `OnDateSetListener` Oyente en la fecha establecida. Normalmente, esta será la `Activity` Actividad o `Fragment` Fragmentos que crearan los Selectores. Las devoluciones de llamada utilizan la misma API que los buscadores estándar de Android. +```java +@Override +public void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute, int second) { + String time = "You picked the following time: "+hourOfDay+"h"+minute+"m"+second; + timeTextView.setText(time); +} + +@Override +public void onDateSet(DatePickerDialog view, int year, int monthOfYear, int dayOfMonth) { + String date = "You picked the following date: "+dayOfMonth+"/"+(monthOfYear+1)+"/"+year; + dateTextView.setText(date); +} +``` + +### Crea un `TimePickerDialog`/`DatePickerDialog` diálogo de selector de tiempo/diálogo selector de fecha usando las fabricaciones suministradas +Deberá crear una nueva instancia de `TimePickerDialog` o `DatePickerDialog` diálogo de selector de tiempo/diálogo selector de fecha utilizando el método estático `newInstance()` Nueva instancia(), suministrando los valores predeterminados correctos y una devolución de llamada. Una vez que los diálogos están configurados, puede escribir `show()` mostrar(). +```java +Calendar now = Calendar.getInstance(); +DatePickerDialog dpd = DatePickerDialog.newInstance( + MainActivity.this, + now.get(Calendar.YEAR), + now.get(Calendar.MONTH), + now.get(Calendar.DAY_OF_MONTH) +); +dpd.show(getFragmentManager(), "Datepickerdialog"); +``` + +### Darle un tema a los selectores +La biblioteca contiene 2 versiones de diseño para cada selector. + +* Versión 1: este es el diseño original. Se basa en el diseño que Google utilizó en el kitkat y en la era inicial del diseño de materiales. +* Versión 2: este diseño se basa en las directrices que Google publicó al ejecutar Android marshmallow. Este es el diseño predeterminado y aún el más actual. + +Puede configurar la versión de diseño usando la fábrica +```java +dpd.setVersion(DatePickerDialog.Version.VERSION_2); +``` + +Los selectores serán temáticos de forma automática en función del tema actual en el que se crean, en función del `ColorAccent` Acentuar color actual. También puede tema los cuadros de diálogo a través del método `setAccentColor(int color)`. Alternativamente, puedes darle un tema a los selectores sobrescribiendo los recursos de color `mdtp_accent_color` y `mdtp_accent_color_dark` en el proyecto. +```xml +#009688 +#00796b +``` + +El orden exacto en que se seleccionan los colores es el siguiente: + +1. `setAccentColor(int color)` en código java +2. `android.R.attr.colorAccent` (si es Android 5.0+) +3. `R.attr.colorAccent` (p.ej. cuando se usa AppCompat) +4. `R.color.mdtp_accent_color` y `R.color.mdtp_accent_color_dark` si ninguno de los otros está configurado en su proyecto + +Los selectores también tienen un tema oscuro. Esto se puede especificar de forma global utilizando el atributo `mdtp_theme_dark` tema oscuro en su tema o las funciones `setThemeDark(boolean themeDark)` establecer el tema oscuro. La función llama a sobrescribir la configuración XML. +```xml +true +``` + + +## Opciones adicionales +* `TimePickerDialog` diálogo de selector de tiempo tema oscuro +El `TimePickerDialog` diálogo de selector de tiempo tiene un tema oscuro que se puede establecer tipeando +```java +tpd.setThemeDark(true); +``` + +* `DatePickerDialog` diálogo selector de fecha tema oscuro +El `DatePickerDialog` diálogo selector de fecha tiene un tema oscuro que se puede establecer tipeando +```java +dpd.setThemeDark(true); +``` + +* `setAccentColor(String color)` establecer el color (color de la cadena) y `setAccentColor(int color)` +Ajuste el color de acento que utilizará el cuadro de diálogo. La versión String analiza el color usando `Color.parseColor()`. La versión int requiere una cadena de bytes ColorInt. Establecerá explícitamente el color a totalmente opaco. + +* `setOkColor()` ajustar color() y `setCancelColor()`cancelar ajuste de color() +Ajuste el color del texto para el botón Aceptar o Cancelar. Se comporta de manera similar a `setAccentColor()` establecer color de acento + +* `TimePickerDialog` `setTitle(String title)` selector de tiempo diálogo, establecer título (título de la cadena) +Muestra un título en la parte superior del `TimePickerDialog` diálogo de selector de tiempo + +* `DatePickerDialog` `setTitle(String title)` +Muestra un título en la parte superior del `DatePickerDialog` en lugar del día de la semana + +* `setOkText()` y `setCancelText()` +Ajuste el texto personalizado para el diálogo en Aceptar y cancelar etiquetas. Puede tomar recursos de una Cadena. Funciona tanto en DatePickerDialog como en TimePickerDialog + +* `setMinTime(Timepoint time)` +Ajuste el tiempo mínimo válido para ser seleccionados. Los valores de tiempo más temprano en el día serán desactivados + +* `setMaxTime(Timepoint time)` +Ajuste el tiempo válido máximo para ser seleccionado. Los valores de tiempo más tarde en el día serán desactivados + +* `setSelectableTimes(Timepoint[] times)` +Puede pasar una serie de `Timepoints`. Estos valores son las únicas selecciones válidas en el selector. `setMinTime(Timepoint time)`, `setMaxTime(Timepoint time)` y `setDisabledTimes(Timepoint [] times)` recortarán aún más esta lista. Intente especificar puntos de tiempo solo hasta la resolución de su selector (es decir, no agregue segundos si la resolución del selector es de minutos). + +* `setDisabledTimes(Timepoint[] times)` +Puede pasar una serie de `Timepoints`. Estos valores no estarán disponibles para la selección. Estos tienen prioridad sobre `setSelectableTimes` y `setTimeInterval`. Tenga cuidado al usar esto sin tiempos seleccionables: el redondeo a un punto de tiempo válido es una operación muy costosa si se inhabilitan muchos puntos de tiempo consecutivos. Intente especificar Puntos de tiempo solo hasta la resolución de su selector (es decir, no agregue segundos si la resolución del selector es de minutos). + +* `setTimeInterval(int hourInterval, int minuteInterval, int secondInterval)` +Establezca el intervalo de tiempos seleccionables en TimePickerDialog. Este es un contenedor de conveniencia alrededor de `setSelectableTimes`. El intervalo para los tres componentes de tiempo se puede establecer de forma independiente. Si no está utilizando el selector de segundos/minutos, configure el elemento respectivo en 60 para un mejor rendimiento. + +* `setTimepointLimiter(TimepointLimiter limiter)` +Pase en una implementación personalizada de`TimeLimiter` +Desactive `setSelectableTimes`, `setDisabledTimes`, `setTimeInterval`, `setMinTime` y `setMaxTime` + +* `setSelectableDays(Calendar[] days)` +You can pass a `Calendar[]` to the `DatePickerDialog`. The values in this list are the only acceptable dates for the picker. It takes precedence over `setMinDate(Calendar day)` and `setMaxDate(Calendar day)` + +* `setDisabledDays(Calendar[] days)` +Los valores en este `Calendario []` están explícitamente deshabilitados (no seleccionables). Esta opción se puede usar junto con `setSelectableDays(Calendar [] days)`: en caso de que haya un conflicto `setDisabledDays(Calendar [] days)` tendrá prioridad sobre `setSelectableDays(Calendar [] days)` + +* `setHighlightedDays(Calendar[] days)` +Puede pasar un `Calendario []` de días para resaltar. Se presentarán en negrita. Puede modificar el color de los días resaltados sobrescribiendo `mdtp_date_picker_text_highlighted` +* `showYearPickerFirst(boolean yearPicker)` +Muestre primero el selector de año, en lugar del selector de mes y día. + +* `OnDismissListener` and `OnCancelListener` +Ambos selectores pueden pasar un `DialogInterface.OnDismissLisener` o` DialogInterface.OnCancelListener` que le permite ejecutar el código cuando se produce cualquiera de estos eventos. +```java +tpd.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + Log.d("TimePicker", "Dialog was cancelled"); + } +}); +``` + +* `vibrate(boolean vibrate)` vibrar +Establezca si los cuadros de diálogo deben hacer vibrar el dispositivo cuando se realiza una selección. Esta predeterminación es `true`. + +* `dismissOnPause(boolean dismissOnPause)` desactivado en pausa +Ajuste si el seleccionador se descarta cuando la Actividad principal está en pausa o si se recrea cuando se reanuda la Actividad. + +* `setLocale(Locale locale)` establecer local +Permite al cliente establecer una configuración regional personalizada que se utilizará al generar varias cadenas en los selectores. Por defecto, se usará la configuración regional actual del dispositivo. Debido a que los selectores se adaptarán a la configuración regional del dispositivo de forma predeterminada, solo debería tener que usar esto en circunstancias muy excepcionales. + +* `DatePickerDialog` Diagrama selector de fecha `autoDismiss(boolean autoDismiss)` descarte automático +Si se establece en `true` cierto, se cerrará el selector cuando el usuario seleccione una fecha. Esta predeterminación es `false` falso. + +* `TimepickerDialog` Diagrama de selector de tiempo `enableSeconds(boolean enableSconds)` habilitar segundos and `enableMinutes(boolean enableMinutes)` habilitar minutos +Le permite habilitar o deshabilitar un selector de segundos y minutos en el `TimepickerDialog`. Habilitar el selector de segundos implica habilitar el selector de minutos. Deshabilitar el selector de minutos desactivará el selector de segundos. Se usará la última configuración aplicada. Por defecto `enableSeconds = false` y `enableMinutes = true`. + +* `DatePickerDialog` Diagrama selector de fecha `setTimeZone(Timezone timezone)` establecer zona horaria *obsoleto* +Establece la `Timezone` utilizada para representar el tiempo internamente en el selector. El valor predeterminado es la zona horaria predeterminada actual del dispositivo. +Este método ha quedadoobsoleto: debe usar el método `newInstance()` que toma un calendario configurado en la TimeZone adecuada. + +* `DatePickerDialog` Diagrama selector de fecha `setDateRangeLimiter(DateRangeLimiter limiter)` ajuste el limitador de rango de fecha (limitador de límite de rango de fecha) +Proporcione una implementación personalizada de DateRangeLimiter, que le brinda control total sobre los días disponibles para la selección. Esto desactiva todas las demás opciones que limitan la selección de fecha. + +* `getOnTimeSetListener()` ponte a tiempo al oyente y `getOnDateSetListener()` ponerse al día con el oyente configurado +Recibidores que permiten la recuperación de una referencia a las devoluciones de llamada actualmente asociadas con los recolectores + +## Preguntas más frecuentes + +### ¿Por qué no usar `SupportDialogFragment` fragmento de diálogo de soporte? +No utilizar las versiones de la biblioteca de soporte ha sido una elección bien considerada, basada en las siguientes consideraciones: + +* Lmenos del 5% de los dispositivos que usan el mercado Android no admiten `Fragments` Fragmentos nativos, un número que disminuirá aún más en el futuro. +* ESi usas `SupportFragments` en tu aplicación, puedes usar el `FragmentManager` normal. Ambos pueden existir uno al lado del otro. + +Esto significa que en la configuración actual, todos pueden usar la biblioteca: personas que usan la biblioteca de soporte y personas que no usan la biblioteca de soporte. + +Finalmente, cambiar el `SupportDialogFragment` fragmento de diálogo de soporte ahora romperá la API para todas las personas que usan esta biblioteca. + +Si realmente necesita el `SupportDialogFragment` fragmento de diálogo de soportediálogo de selección de fechas devuelve el mes -1 seleccionado? +En la clase `Calendar` Calendario de Java, los meses usan indexación basada en 0: enero es el mes 0, diciembre es el mes 11. Esta convención es ampliamente utilizada en el mundo java, por ejemplo, el _native Android DatePicker_. + +### ¿Cómo uso una versión diferente de la biblioteca de soporte en mi aplicación? +Esta biblioteca depende de la biblioteca del soporte de Android. Debido a que jvm solo permite cargar una versión de una clase con espacio de nombres completo, se encontrará con problemas si su aplicación depende de una versión diferente de la biblioteca de soporte que la utilizada en esta aplicación. Gradle es generalmente bueno resolviendo conflictos de versiones (por defecto conservará la última versión de una biblioteca), pero si tiene problemas (por ejemplo, porque ha desactivado la resolución de conflictos), +puede desactivar la carga de la biblioteca de soporte para MaterialDateTimePicker material de selección de tiempo y fecha. + +Usando el siguiente fragmento en el archivo `build.gradle` de su aplicación, puede excluir la posibilidad de que se instale la biblioteca de soporte transitivo de esta biblioteca. + +```groovy +compile ('com.wdullaer:materialdatetimepicker:3.5.1') { + exclude group: 'com.android.support' +} +``` + +Su aplicación deberá depender al menos de las siguientes piezas de la biblioteca de soporte + +```groovy +compile 'com.android.support:support-v4:26.0.1' +compile 'com.android.support:support-v13:26.0.1' +compile 'com.android.support:design:26.0.1' +``` + +Esto funcionará bien, siempre y cuando la versión de la biblioteca de soporte de la que depende su aplicación sea lo suficientemente reciente (admite `RecyclerView` Vista al reciclador) y google no lance una versión en el futuro que contenga cambios de última hora. (Si/Cuando esto ocurra, intentaré documentarlo). Vea el documento [#338](https://github.com/wdullaer/MaterialDateTimePicker/issues/338) para más información. + +### ¿Cómo puedo convertir esto en un selector de año y mes? +Este DatePickerDialog Selector de fecha se enfoca en seleccionar fechas, lo que significa que su elemento de diseño central es el selector de días. Como esta vista de calendario es el centro del diseño, no tiene sentido intentar deshabilitarlo. Como tal, la selección de solo años y meses, sin un día, no está dentro del alcance de esta biblioteca y no se agregará. +### ¿Cómo uso mi lógica personalizada para habilitar/deshabilitar las fechas? +`DatePickerDialog` Limitador de rango de fechas expone algunos métodos de utilidad para habilitar/deshabilitar fechas para escenarios comunes. Si sus necesidades no están cubiertas por estas, puede suministrar una implementación personalizada de la interfaz `DateRangeLimiter` Limitador de rango de fechas. +Debido a que `DateRangeLimiter`Limitador de rango de fechas se conserva cuando el `Dialog` Dialogo hace una pausa, su implementación también debe implementar `Parcelable`. + +```java +class MyDateRangeLimiter implements DateRangeLimiter { + public MyDateRangeLimiter(Parcel in) { + + } + + @Override + public int getMinYear() { + return 1900; + } + + @Override + public int getMaxYear() { + return 2100; + } + + @Override + public Calendar getStartDate() { + Calendar output = Calendar.newInstance(); + output.set(Calendar.YEAR, 1900); + output.set(Calendar.DAY_OF_MONTH, 1); + output.set(Calendar.MONTH, Calendar.JANUARY); + return output; + } + + @Override + public Calendar getEndDate() { + Calendar output = Calendar.newInstance(); + output.set(Calendar.YEAR, 2100); + output.set(Calendar.DAY_OF_MONTH, 1); + output.set(Calendar.MONTH, Calendar.JANUARY); + return output; + } + + @Override + public boolean isOutOfRange(int year, int month, int day) { + return false; + } + + @Override + public Calendar setToNearestDate(Calendar day) { + return day; + } + + @Override + public void writeToParcel(Parcel out) { + + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public MyDateRangeLimiter createFromParcel(Parcel in) { + return new MyDateRangeLimiter(in); + } + + public MyDateRangeLimiter[] newArray(int size) { + return new MyDateRangeLimiter[size]; + } + }; +} +``` + +Cuando proporcione un `DateRangeLimiter` Limitador de rango de fechas personalizado, los métodos integrados para configurar las fechas activadas/desactivadas ya no funcionarán. Tendrá que ser completamente manejado por tu implementación. + +### ¿Por qué se pierden mis devoluciones de llamada cuando el dispositivo cambia de orientación? +La solución simple es desactivar a los selectores cuando su actividad está en pausa. + +```java +tpd.dismissOnPause(true); +``` + +Si desea retener a los selectores cuando ocurre un cambio de orientación, las cosas se vuelven un poco más complicadas. + +Por defecto, cuando se produce una orientación, Android destruye y recrea toda su `Activity` Actividad. Siempre que sea posible, esta biblioteca conservará su estado en un cambio de orientación. Las únicas excepciones notables son las diferentes devoluciones de llamada y oyentes. Estas interfaces a menudo se implementan en `Activities` Actividades o `Fragments` fragmentos. Tratar de retenerlos ingenuamente causaría pérdidas de memoria. Además de requerir explícitamente que las interfaces de devolución de llamada se implementen en una `Activity` Actividad, no hay una manera segura de retener las devoluciones de llamada de manera adecuada, que yo sepa. + +Esto significa que es su responsabilidad configurar a los oyentes en la `Activity`'s Actividad `onResume()` en espera de la devolución de llamadas. + +```java +@Override +public void onResume() { + super.onResume(); + + DatePickerDialog dpd = (DatePickerDialog) getFragmentManager().findFragmentByTag("Datepickerdialog"); + TimePickerDialog tpd = (TimePickerDialog) getFragmentManager().findFragmentByTag("TimepickerDialog"); + + if(tpd != null) tpd.setOnTimeSetListener(this); + if(dpd != null) dpd.setOnDateSetListener(this); +} +``` + + +## Mejoras potenciales +* Landscape timepicker puede usar alguna mejora +* Implementar el nuevo estilo de seleccionadores +* Limpieza de código: hay un poco de saliva y cinta adhesiva en los ajustes que he hecho. +* Documente todas las opciones en ambos selectores + + +## Licencia + Copyright (c) 2015 Wouter Dullaert + + Licencia bajo la Licencia Apache, Versión 2.0 (la "Licencia"); + no puede usar este archivo excepto en conformidad con la licencia. + Ypuede obtener una copia de la licencia en + + http://www.apache.org/licenses/LICENSE-2.0 + + A menos que lo exija la ley aplicable o se acuerde por escrito, el software + distribuido bajo la Licencia se distribuye "TAL CUAL", SIN GARANTÍAS + O CONDICIONES DE NINGÚN TIPO, ya sea expresa o implícita. + Consulte la Licencia para conocer el idioma específico que rige los permisos y + limitaciones bajo la Licencia. diff --git a/build.gradle b/build.gradle index f46d6535..38c58dd8 100644 --- a/build.gradle +++ b/build.gradle @@ -3,9 +3,13 @@ buildscript { repositories { jcenter() + maven { + url 'https://maven.google.com/' + name 'Google' + } } dependencies { - classpath 'com.android.tools.build:gradle:2.2.3' + classpath 'com.android.tools.build:gradle:3.1.0-alpha08' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -15,5 +19,9 @@ buildscript { allprojects { repositories { jcenter() + maven { + url "https://maven.google.com" + name 'Google' + } } } diff --git a/gradle.properties b/gradle.properties index a34431f0..9e3d3eaa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,14 +17,14 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=3.1.3 -VERSION_CODE=29 +VERSION_NAME=3.5.1 +VERSION_CODE=39 GROUP=com.wdullaer ANDROID_BUILD_MIN_SDK_VERSION=14 -ANDROID_BUILD_TARGET_SDK_VERSION=25 -ANDROID_BUILD_SDK_VERSION=25 -ANDROID_BUILD_TOOLS_VERSION=25.0.2 +ANDROID_BUILD_TARGET_SDK_VERSION=27 +ANDROID_BUILD_SDK_VERSION=27 +ANDROID_BUILD_TOOLS_VERSION=27.0.2 POM_DESCRIPTION=Material DateTimepicker POM_URL=https://github.com/wdullaer/MaterialDateTimePicker diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fb19cf7a..38d84593 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Dec 28 19:54:34 CET 2016 +#Thu Jan 18 12:36:42 CET 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/library/build.gradle b/library/build.gradle index 49178484..dcd12d47 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -9,6 +9,7 @@ android { targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_TARGET_SDK_VERSION) versionName project.VERSION_NAME versionCode Integer.parseInt(project.VERSION_CODE) + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -24,9 +25,17 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:support-v4:25.2.0' - compile 'com.android.support:support-v13:25.2.0' - compile 'com.android.support:design:25.2.0' + compile 'com.android.support:support-v4:27.0.2' + compile 'com.android.support:support-v13:27.0.2' + compile 'com.android.support:design:27.0.2' + + testCompile 'junit:junit:4.12' + testCompile 'com.pholser:junit-quickcheck-core:0.7' + testCompile 'com.pholser:junit-quickcheck-generators:0.7' + + androidTestCompile 'com.android.support:support-annotations:27.0.2' + androidTestCompile 'com.android.support.test:runner:1.0.1' + androidTestCompile 'com.android.support.test:rules:1.0.1' } apply from: 'gradle-mvn-push.gradle' diff --git a/library/src/androidTest/java/com/wdullaer/materialdatetimepicker/date/DefaultDateRangeLimiterTest.java b/library/src/androidTest/java/com/wdullaer/materialdatetimepicker/date/DefaultDateRangeLimiterTest.java new file mode 100644 index 00000000..079105e2 --- /dev/null +++ b/library/src/androidTest/java/com/wdullaer/materialdatetimepicker/date/DefaultDateRangeLimiterTest.java @@ -0,0 +1,152 @@ +package com.wdullaer.materialdatetimepicker.date; + +import android.os.Parcel; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Calendar; + +import static org.junit.Assert.*; + +/** + * Unit tests for DefaultDateRangeLimiter which need to run on an android device + * Mostly used to test Parcelable serialisation logic + * Created by wdullaer on 2/11/17. + */ +@RunWith(AndroidJUnit4.class) +public class DefaultDateRangeLimiterTest { + @Test + public void shouldCorrectlySaveAndRestoreAParcelWithAYearRange() { + int minYear = 1985; + int maxYear = 2015; + + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + limiter.setYearRange(minYear, maxYear); + + Parcel parcel = Parcel.obtain(); + limiter.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + DefaultDateRangeLimiter clonedLimiter = DefaultDateRangeLimiter.CREATOR.createFromParcel(parcel); + + assertEquals(clonedLimiter.getMinYear(), minYear); + assertEquals(clonedLimiter.getMaxYear(), maxYear); + } + + @Test + public void shouldCorrectlySaveAndRestoreAParcelWithAMinDate() { + Calendar minDate = Calendar.getInstance(); + minDate.set(Calendar.YEAR, 1980); + + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + limiter.setMinDate(minDate); + + Parcel parcel = Parcel.obtain(); + limiter.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + DefaultDateRangeLimiter clonedLimiter = DefaultDateRangeLimiter.CREATOR.createFromParcel(parcel); + + assertEquals(clonedLimiter.getMinDate(), limiter.getMinDate()); + } + + @Test + public void shouldCorrectlySaveAndRestoreAParcelWithAMaxDate() { + Calendar maxDate = Calendar.getInstance(); + + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + limiter.setMaxDate(maxDate); + + Parcel parcel = Parcel.obtain(); + limiter.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + DefaultDateRangeLimiter clonedLimiter = DefaultDateRangeLimiter.CREATOR.createFromParcel(parcel); + + assertEquals(clonedLimiter.getMaxDate(), limiter.getMaxDate()); + } + + @Test + public void shouldCorrectlySaveAndRestoreAParcelWithSelectableDays() { + Calendar day1 = Calendar.getInstance(); + day1.set(Calendar.YEAR, 1985); + Calendar day2 = Calendar.getInstance(); + Calendar[] selectableDays = { + day1, + day2 + }; + + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + limiter.setSelectableDays(selectableDays); + + Parcel parcel = Parcel.obtain(); + limiter.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + DefaultDateRangeLimiter clonedLimiter = DefaultDateRangeLimiter.CREATOR.createFromParcel(parcel); + + assertArrayEquals(clonedLimiter.getSelectableDays(), limiter.getSelectableDays()); + } + + @Test + public void shouldCorrectlySaveAndRestoreAParcelWithDisabledDays() { + Calendar day1 = Calendar.getInstance(); + day1.set(Calendar.YEAR, 1985); + Calendar day2 = Calendar.getInstance(); + Calendar[] disabledDays = { + day1, + day2 + }; + + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + limiter.setDisabledDays(disabledDays); + + Parcel parcel = Parcel.obtain(); + limiter.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + DefaultDateRangeLimiter clonedLimiter = DefaultDateRangeLimiter.CREATOR.createFromParcel(parcel); + + assertArrayEquals(clonedLimiter.getDisabledDays(), limiter.getDisabledDays()); + } + + @Test + public void shouldCorrectlySaveAndRestoreAParcel() { + int minYear = 1970; + int maxYear = 2020; + + Calendar minDate = Calendar.getInstance(); + minDate.set(Calendar.YEAR, 1985); + Calendar maxDate = Calendar.getInstance(); + maxDate.set(Calendar.YEAR, 2019); + + Calendar day1 = Calendar.getInstance(); + day1.set(Calendar.MONTH, Calendar.JANUARY); + Calendar day2 = Calendar.getInstance(); + day2.set(Calendar.MONTH, Calendar.AUGUST); + Calendar[] disabledDays = { + day1, + day2 + }; + + Calendar[] selectableDays = { + Calendar.getInstance() + }; + + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + limiter.setYearRange(minYear, maxYear); + limiter.setMinDate(minDate); + limiter.setMaxDate(maxDate); + limiter.setDisabledDays(disabledDays); + limiter.setSelectableDays(selectableDays); + + Parcel parcel = Parcel.obtain(); + limiter.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + DefaultDateRangeLimiter clonedLimiter = DefaultDateRangeLimiter.CREATOR.createFromParcel(parcel); + + assertEquals(clonedLimiter.getMinYear(), limiter.getMinYear()); + assertEquals(clonedLimiter.getMaxYear(), limiter.getMaxYear()); + assertEquals(clonedLimiter.getMinDate(), limiter.getMinDate()); + assertEquals(clonedLimiter.getMaxDate(), limiter.getMaxDate()); + assertArrayEquals(clonedLimiter.getDisabledDays(), limiter.getDisabledDays()); + assertArrayEquals(clonedLimiter.getSelectableDays(), limiter.getSelectableDays()); + } +} \ No newline at end of file diff --git a/library/src/androidTest/java/com/wdullaer/materialdatetimepicker/time/DefaultTimepointLimiterTest.java b/library/src/androidTest/java/com/wdullaer/materialdatetimepicker/time/DefaultTimepointLimiterTest.java new file mode 100644 index 00000000..6459f4f3 --- /dev/null +++ b/library/src/androidTest/java/com/wdullaer/materialdatetimepicker/time/DefaultTimepointLimiterTest.java @@ -0,0 +1,118 @@ +package com.wdullaer.materialdatetimepicker.time; + +import android.os.Parcel; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Unit tests for DefaultTimepointLimiter which need to run on an android device + * Mostly used to test Parcelable serialisation logic + * Created by wdullaer on 1/11/17. + */ +@RunWith(AndroidJUnit4.class) +public class DefaultTimepointLimiterTest { + @Test + public void shouldCorrectlySaveAndRestoreAParcelWithMinTime() { + Timepoint minTime = new Timepoint(1, 2, 3); + + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + limiter.setMinTime(minTime); + + Parcel limiterParcel = Parcel.obtain(); + limiter.writeToParcel(limiterParcel, 0); + limiterParcel.setDataPosition(0); + + DefaultTimepointLimiter clonedLimiter = DefaultTimepointLimiter.CREATOR.createFromParcel(limiterParcel); + + assertEquals(clonedLimiter.getMinTime(), minTime); + } + + @Test + public void shouldCorrectlySaveAndRestoreAParcelWithMaxTime() { + Timepoint maxTime = new Timepoint(1, 2, 3); + + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + limiter.setMaxTime(maxTime); + + Parcel limiterParcel = Parcel.obtain(); + limiter.writeToParcel(limiterParcel, 0); + limiterParcel.setDataPosition(0); + + DefaultTimepointLimiter clonedLimiter = DefaultTimepointLimiter.CREATOR.createFromParcel(limiterParcel); + + assertEquals(clonedLimiter.getMaxTime(), maxTime); + } + + @Test + public void shouldCorrectlySaveAndRestoreAParcelWithSelectableTimes() { + Timepoint[] disabledTimes = { + new Timepoint(1, 2, 3), + new Timepoint(10, 11, 12) + }; + + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + limiter.setDisabledTimes(disabledTimes); + + Parcel limiterParcel = Parcel.obtain(); + limiter.writeToParcel(limiterParcel, 0); + limiterParcel.setDataPosition(0); + + DefaultTimepointLimiter clonedLimiter = DefaultTimepointLimiter.CREATOR.createFromParcel(limiterParcel); + + assertArrayEquals(clonedLimiter.getDisabledTimes(), disabledTimes); + } + + @Test + public void shouldCorrectlySaveAndRestoreAParcelWithDisabledTimes() { + Timepoint[] selectableTimes = { + new Timepoint(1, 2, 3), + new Timepoint(10, 11, 12) + }; + + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + limiter.setSelectableTimes(selectableTimes); + + Parcel limiterParcel = Parcel.obtain(); + limiter.writeToParcel(limiterParcel, 0); + limiterParcel.setDataPosition(0); + + DefaultTimepointLimiter clonedLimiter = DefaultTimepointLimiter.CREATOR.createFromParcel(limiterParcel); + + assertArrayEquals(clonedLimiter.getSelectableTimes(), selectableTimes); + } + + @Test + public void shouldCorrectlySaveAndRestoreAParcel() { + Timepoint minTime = new Timepoint(1, 2, 3); + Timepoint maxTime = new Timepoint(12, 13, 14); + Timepoint[] disabledTimes = { + new Timepoint(2, 3, 4), + new Timepoint(3, 4, 5) + }; + Timepoint[] selectableTimes = { + new Timepoint(2, 3, 4), + new Timepoint(10, 11, 12) + }; + + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + limiter.setMinTime(minTime); + limiter.setMaxTime(maxTime); + limiter.setDisabledTimes(disabledTimes); + limiter.setSelectableTimes(selectableTimes); + + Parcel limiterParcel = Parcel.obtain(); + limiter.writeToParcel(limiterParcel, 0); + limiterParcel.setDataPosition(0); + + DefaultTimepointLimiter clonedLimiter = DefaultTimepointLimiter.CREATOR.createFromParcel(limiterParcel); + + assertEquals(clonedLimiter.getMinTime(), minTime); + assertEquals(clonedLimiter.getMaxTime(), maxTime); + assertArrayEquals(clonedLimiter.getDisabledTimes(), disabledTimes); + assertArrayEquals(clonedLimiter.getSelectableTimes(), selectableTimes); + } +} \ No newline at end of file diff --git a/library/src/androidTest/java/com/wdullaer/materialdatetimepicker/time/TimepointTest.java b/library/src/androidTest/java/com/wdullaer/materialdatetimepicker/time/TimepointTest.java new file mode 100644 index 00000000..c4fdc58d --- /dev/null +++ b/library/src/androidTest/java/com/wdullaer/materialdatetimepicker/time/TimepointTest.java @@ -0,0 +1,28 @@ +package com.wdullaer.materialdatetimepicker.time; + +import android.os.Parcel; +import android.support.test.runner.AndroidJUnit4; + +import static org.junit.Assert.*; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test for Timepoint which need to run on an actual device + * Created by wdullaer on 1/11/17. + */ +@RunWith(AndroidJUnit4.class) +public class TimepointTest { + @Test + public void shouldCorrectlySaveAndRestoreAParcel() { + Timepoint input = new Timepoint(1, 2, 3); + Parcel timepointParcel = Parcel.obtain(); + input.writeToParcel(timepointParcel, 0); + timepointParcel.setDataPosition(0); + + Timepoint output = Timepoint.CREATOR.createFromParcel(timepointParcel); + assertEquals(input.getHour(), output.getHour()); + assertEquals(input.getMinute(), output.getMinute()); + assertEquals(input.getSecond(), output.getSecond()); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/GravitySnapHelper.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/GravitySnapHelper.java new file mode 100644 index 00000000..01cee32a --- /dev/null +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/GravitySnapHelper.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2017 Wouter Dullaert + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific languag`e governing permissions and + * limitations under the License. + */ + +package com.wdullaer.materialdatetimepicker; + +import android.os.Build; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.LinearSnapHelper; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.OrientationHelper; +import android.support.v7.widget.RecyclerView; +import android.view.Gravity; +import android.view.View; + +/** + * Enables snapping better snapping in a RecyclerView + * Based on the code of Ruben Sousa + * Created by wdullaer on 3/04/17. + */ +public class GravitySnapHelper extends LinearSnapHelper { + + private OrientationHelper verticalHelper; + private OrientationHelper horizontalHelper; + private int gravity; + private boolean isRtlHorizontal; + private GravitySnapHelper.SnapListener listener; + private boolean snapping; + private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + if (newState == RecyclerView.SCROLL_STATE_SETTLING) { + snapping = false; + } + if (newState == RecyclerView.SCROLL_STATE_IDLE && snapping && listener != null) { + int position = getSnappedPosition(recyclerView); + if (position != RecyclerView.NO_POSITION) { + listener.onSnap(position); + } + snapping = false; + } + } + }; + + public GravitySnapHelper(int gravity) { + this(gravity, null); + } + + public GravitySnapHelper(int gravity, SnapListener snapListener) { + if (gravity != Gravity.START && gravity != Gravity.END + && gravity != Gravity.BOTTOM && gravity != Gravity.TOP) { + throw new IllegalArgumentException("Invalid gravity value. Use START " + + "| END | BOTTOM | TOP constants"); + } + this.gravity = gravity; + this.listener = snapListener; + } + + @Override + public void attachToRecyclerView(@Nullable RecyclerView recyclerView) + throws IllegalStateException { + if (recyclerView != null) { + if ((gravity == Gravity.START || gravity == Gravity.END) + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + isRtlHorizontal + = recyclerView.getContext().getResources().getConfiguration() + .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } + if (listener != null) { + recyclerView.addOnScrollListener(mScrollListener); + } + } + super.attachToRecyclerView(recyclerView); + } + + @Override + public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, + @NonNull View targetView) { + int[] out = new int[2]; + + if (layoutManager.canScrollHorizontally()) { + if (gravity == Gravity.START) { + out[0] = distanceToStart(targetView, getHorizontalHelper(layoutManager), false); + } else { // END + out[0] = distanceToEnd(targetView, getHorizontalHelper(layoutManager), false); + } + } else { + out[0] = 0; + } + + if (layoutManager.canScrollVertically()) { + if (gravity == Gravity.TOP) { + out[1] = distanceToStart(targetView, getVerticalHelper(layoutManager), false); + } else { // BOTTOM + out[1] = distanceToEnd(targetView, getVerticalHelper(layoutManager), false); + } + } else { + out[1] = 0; + } + + return out; + } + + @Override + public View findSnapView(RecyclerView.LayoutManager layoutManager) { + View snapView = null; + if (layoutManager instanceof LinearLayoutManager) { + switch (gravity) { + case Gravity.START: + snapView = findStartView(layoutManager, getHorizontalHelper(layoutManager)); + break; + case Gravity.END: + snapView = findEndView(layoutManager, getHorizontalHelper(layoutManager)); + break; + case Gravity.TOP: + snapView = findStartView(layoutManager, getVerticalHelper(layoutManager)); + break; + case Gravity.BOTTOM: + snapView = findEndView(layoutManager, getVerticalHelper(layoutManager)); + break; + } + } + snapping = snapView != null; + return snapView; + } + + private int distanceToStart(View targetView, OrientationHelper helper, boolean fromEnd) { + if (isRtlHorizontal && !fromEnd) { + return distanceToEnd(targetView, helper, true); + } + + return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding(); + } + + private int distanceToEnd(View targetView, OrientationHelper helper, boolean fromStart) { + if (isRtlHorizontal && !fromStart) { + return distanceToStart(targetView, helper, true); + } + + return helper.getDecoratedEnd(targetView) - helper.getEndAfterPadding(); + } + + /** + * Returns the first view that we should snap to. + * + * @param layoutManager the recyclerview's layout manager + * @param helper orientation helper to calculate view sizes + * @return the first view in the LayoutManager to snap to + */ + private View findStartView(RecyclerView.LayoutManager layoutManager, + OrientationHelper helper) { + + if (layoutManager instanceof LinearLayoutManager) { + int firstChild = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); + + if (firstChild == RecyclerView.NO_POSITION) { + return null; + } + + View child = layoutManager.findViewByPosition(firstChild); + + float visibleWidth; + + // We should return the child if it's visible width + // is greater than 0.5 of it's total width. + // In a RTL configuration, we need to check the start point and in LTR the end point + if (isRtlHorizontal) { + visibleWidth = (float) (helper.getTotalSpace() - helper.getDecoratedStart(child)) + / helper.getDecoratedMeasurement(child); + } else { + visibleWidth = (float) helper.getDecoratedEnd(child) + / helper.getDecoratedMeasurement(child); + } + + // If we're at the end of the list, we shouldn't snap + // to avoid having the last item not completely visible. + boolean endOfList = ((LinearLayoutManager) layoutManager) + .findLastCompletelyVisibleItemPosition() + == layoutManager.getItemCount() - 1; + + if (visibleWidth > 0.5f && !endOfList) { + return child; + } else if (endOfList) { + return null; + } else { + // If the child wasn't returned, we need to return + // the next view close to the start. + return layoutManager.findViewByPosition(firstChild + 1); + } + } + + return null; + } + + private View findEndView(RecyclerView.LayoutManager layoutManager, + OrientationHelper helper) { + + if (layoutManager instanceof LinearLayoutManager) { + int lastChild = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); + + if (lastChild == RecyclerView.NO_POSITION) { + return null; + } + + View child = layoutManager.findViewByPosition(lastChild); + + float visibleWidth; + + if (isRtlHorizontal) { + visibleWidth = (float) helper.getDecoratedEnd(child) + / helper.getDecoratedMeasurement(child); + } else { + visibleWidth = (float) (helper.getTotalSpace() - helper.getDecoratedStart(child)) + / helper.getDecoratedMeasurement(child); + } + + // If we're at the start of the list, we shouldn't snap + // to avoid having the first item not completely visible. + boolean startOfList = ((LinearLayoutManager) layoutManager) + .findFirstCompletelyVisibleItemPosition() == 0; + + if (visibleWidth > 0.5f && !startOfList) { + return child; + } else if (startOfList) { + return null; + } else { + // If the child wasn't returned, we need to return the previous view + return layoutManager.findViewByPosition(lastChild - 1); + } + } + return null; + } + + private int getSnappedPosition(RecyclerView recyclerView) { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + + if (layoutManager instanceof LinearLayoutManager) { + if (gravity == Gravity.START || gravity == Gravity.TOP) { + return ((LinearLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition(); + } else if (gravity == Gravity.END || gravity == Gravity.BOTTOM) { + return ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition(); + } + } + + return RecyclerView.NO_POSITION; + } + + private OrientationHelper getVerticalHelper(RecyclerView.LayoutManager layoutManager) { + if (verticalHelper == null) { + verticalHelper = OrientationHelper.createVerticalHelper(layoutManager); + } + return verticalHelper; + } + + private OrientationHelper getHorizontalHelper(RecyclerView.LayoutManager layoutManager) { + if (horizontalHelper == null) { + horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager); + } + return horizontalHelper; + } + + public interface SnapListener { + void onSnap(int position); + } + +} diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/Utils.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/Utils.java index bc7dc91c..1635ed90 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/Utils.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/Utils.java @@ -30,9 +30,12 @@ import android.util.TypedValue; import android.view.View; +import java.util.Calendar; + /** * Utility helper functions for time and date pickers. */ +@SuppressWarnings("WeakerAccess") public class Utils { //public static final int MONDAY_BEFORE_JULIAN_EPOCH = Time.EPOCH_JULIAN_DAY - 3; @@ -59,47 +62,6 @@ public static void tryAccessibilityAnnounce(View view, CharSequence text) { } } - /** - * Takes a number of weeks since the epoch and calculates the Julian day of - * the Monday for that week. - * - * This assumes that the week containing the {@link Time#EPOCH_JULIAN_DAY} - * is considered week 0. It returns the Julian day for the Monday - * {@code week} weeks after the Monday of the week containing the epoch. - * - * @param week Number of weeks since the epoch - * @return The julian day for the Monday of the given week since the epoch - */ - /** - public static int getJulianMondayFromWeeksSinceEpoch(int week) { - return MONDAY_BEFORE_JULIAN_EPOCH + week * 7; - } - */ - - /** - * Returns the week since {@link Time#EPOCH_JULIAN_DAY} (Jan 1, 1970) - * adjusted for first day of week. - * - * This takes a julian day and the week start day and calculates which - * week since {@link Time#EPOCH_JULIAN_DAY} that day occurs in, starting - * at 0. *Do not* use this to compute the ISO week number for the year. - * - * @param julianDay The julian day to calculate the week number for - * @param firstDayOfWeek Which week day is the first day of the week, - * see {@link Time#SUNDAY} - * @return Weeks since the epoch - */ - /** - public static int getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek) { - int diff = Time.THURSDAY - firstDayOfWeek; - if (diff < 0) { - diff += 7; - } - int refDay = Time.EPOCH_JULIAN_DAY - diff; - return (julianDay - refDay) / 7; - } - */ - /** * Render an animator to pulsate a view in place. * @param labelToAnimate the view to pulsate. @@ -183,4 +145,19 @@ private static boolean resolveBoolean(Context context, @AttrRes int attr, boolea a.recycle(); } } + + /** + * Trims off all time information, effectively setting it to midnight + * Makes it easier to compare at just the day level + * + * @param calendar The Calendar object to trim + * @return The trimmed Calendar object + */ + public static Calendar trimToMidnight(Calendar calendar) { + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar; + } } diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerController.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerController.java index 8e1846f5..161f7ba1 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerController.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerController.java @@ -17,6 +17,7 @@ package com.wdullaer.materialdatetimepicker.date; import java.util.Calendar; +import java.util.Locale; import java.util.TimeZone; /** @@ -30,6 +31,7 @@ public interface DatePickerController { void registerOnDateChangedListener(DatePickerDialog.OnDateChangedListener listener); + @SuppressWarnings("unused") void unregisterOnDateChangedListener(DatePickerDialog.OnDateChangedListener listener); MonthAdapter.CalendarDay getSelectedDay(); @@ -55,4 +57,10 @@ public interface DatePickerController { void tryVibrate(); TimeZone getTimeZone(); + + Locale getLocale(); + + DatePickerDialog.Version getVersion(); + + DatePickerDialog.ScrollOrientation getScrollOrientation(); } diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerDialog.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerDialog.java index 287a2797..c69b4067 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerDialog.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DatePickerDialog.java @@ -55,7 +55,6 @@ import java.util.HashSet; import java.util.Locale; import java.util.TimeZone; -import java.util.TreeSet; /** * Dialog allowing users to select a date. @@ -68,6 +67,11 @@ public enum Version { VERSION_2 } + public enum ScrollOrientation { + HORIZONTAL, + VERTICAL + } + private static final int UNINITIALIZED = -1; private static final int MONTH_AND_DAY_VIEW = 0; private static final int YEAR_VIEW = 1; @@ -77,15 +81,9 @@ public enum Version { private static final String KEY_SELECTED_DAY = "day"; private static final String KEY_LIST_POSITION = "list_position"; private static final String KEY_WEEK_START = "week_start"; - private static final String KEY_YEAR_START = "year_start"; - private static final String KEY_YEAR_END = "year_end"; private static final String KEY_CURRENT_VIEW = "current_view"; private static final String KEY_LIST_POSITION_OFFSET = "list_position_offset"; - private static final String KEY_MIN_DATE = "min_date"; - private static final String KEY_MAX_DATE = "max_date"; private static final String KEY_HIGHLIGHTED_DAYS = "highlighted_days"; - private static final String KEY_SELECTABLE_DAYS = "selectable_days"; - private static final String KEY_DISABLED_DAYS = "disabled_days"; private static final String KEY_THEME_DARK = "theme_dark"; private static final String KEY_THEME_DARK_CHANGED = "theme_dark_changed"; private static final String KEY_ACCENT = "accent"; @@ -102,10 +100,9 @@ public enum Version { private static final String KEY_CANCEL_COLOR = "cancel_color"; private static final String KEY_VERSION = "version"; private static final String KEY_TIMEZONE = "timezone"; - - - private static final int DEFAULT_START_YEAR = 1900; - private static final int DEFAULT_END_YEAR = 2100; + private static final String KEY_DATERANGELIMITER = "daterangelimiter"; + private static final String KEY_SCROLL_ORIENTATION = "scrollorientation"; + private static final String KEY_LOCALE = "locale"; private static final int ANIMATION_DURATION = 300; private static final int ANIMATION_DELAY = 500; @@ -115,7 +112,7 @@ public enum Version { private static SimpleDateFormat DAY_FORMAT = new SimpleDateFormat("dd", Locale.getDefault()); private static SimpleDateFormat VERSION_2_FORMAT; - private final Calendar mCalendar = trimToMidnight(Calendar.getInstance(getTimeZone())); + private Calendar mCalendar = Utils.trimToMidnight(Calendar.getInstance(getTimeZone())); private OnDateSetListener mCallBack; private HashSet mListeners = new HashSet<>(); private DialogInterface.OnCancelListener mOnCancelListener; @@ -134,14 +131,8 @@ public enum Version { private int mCurrentView = UNINITIALIZED; private int mWeekStart = mCalendar.getFirstDayOfWeek(); - private int mMinYear = DEFAULT_START_YEAR; - private int mMaxYear = DEFAULT_END_YEAR; private String mTitle; - private Calendar mMinDate; - private Calendar mMaxDate; private HashSet highlightedDays = new HashSet<>(); - private TreeSet selectableDays = new TreeSet<>(); - private HashSet disabledDays = new HashSet<>(); private boolean mThemeDark = false; private boolean mThemeDarkChanged = false; private int mAccentColor = -1; @@ -156,7 +147,11 @@ public enum Version { private String mCancelString; private int mCancelColor = -1; private Version mVersion; + private ScrollOrientation mScrollOrientation; private TimeZone mTimezone; + private Locale mLocale = Locale.getDefault(); + private DefaultDateRangeLimiter mDefaultLimiter = new DefaultDateRangeLimiter(); + private DateRangeLimiter mDateRangeLimiter = mDefaultLimiter; private HapticFeedbackController mHapticFeedbackController; @@ -174,11 +169,11 @@ public enum Version { public interface OnDateSetListener { /** - * @param view The view associated with this listener. - * @param year The year that was set. + * @param view The view associated with this listener. + * @param year The year that was set. * @param monthOfYear The month that was set (0-11) for compatibility - * with {@link java.util.Calendar}. - * @param dayOfMonth The day of the month that was set. + * with {@link java.util.Calendar}. + * @param dayOfMonth The day of the month that was set. */ void onDateSet(DatePickerDialog view, int year, int monthOfYear, int dayOfMonth); } @@ -186,7 +181,8 @@ public interface OnDateSetListener { /** * The callback used to notify other date picker components of a change in selected date. */ - interface OnDateChangedListener { + @SuppressWarnings("WeakerAccess") + protected interface OnDateChangedListener { void onDateChanged(); } @@ -197,28 +193,48 @@ public DatePickerDialog() { } /** - * @param callBack How the parent is notified that the date is set. - * @param year The initial year of the 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. + * @param dayOfMonth The initial day of the dialog. */ - public static DatePickerDialog newInstance(OnDateSetListener callBack, int year, - int monthOfYear, - int dayOfMonth) { + public static DatePickerDialog newInstance(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) { DatePickerDialog ret = new DatePickerDialog(); ret.initialize(callBack, year, monthOfYear, dayOfMonth); return ret; } - public void initialize(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) { + @SuppressWarnings("unused") + public static DatePickerDialog newInstance(OnDateSetListener callback) { + Calendar now = Calendar.getInstance(); + return DatePickerDialog.newInstance(callback, now); + } + + @SuppressWarnings("unused") + public static DatePickerDialog newInstance(OnDateSetListener callback, Calendar initialSelection) { + DatePickerDialog ret = new DatePickerDialog(); + ret.initialize(callback, initialSelection); + return ret; + } + + public void initialize(OnDateSetListener callBack, Calendar initialSelection) { mCallBack = callBack; - mCalendar.set(Calendar.YEAR, year); - mCalendar.set(Calendar.MONTH, monthOfYear); - mCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); + mCalendar = Utils.trimToMidnight((Calendar) initialSelection.clone()); + mScrollOrientation = null; + //noinspection deprecation + setTimeZone(mCalendar.getTimeZone()); mVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M ? Version.VERSION_1 : Version.VERSION_2; } + public void initialize(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) { + Calendar cal = Calendar.getInstance(getTimeZone()); + cal.set(Calendar.YEAR, year); + cal.set(Calendar.MONTH, monthOfYear); + cal.set(Calendar.DAY_OF_MONTH, dayOfMonth); + this.initialize(callBack, cal); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -233,9 +249,9 @@ public void onCreate(Bundle savedInstanceState) { mDefaultView = savedInstanceState.getInt(KEY_DEFAULT_VIEW); } if (Build.VERSION.SDK_INT < 18) { - VERSION_2_FORMAT = new SimpleDateFormat(activity.getResources().getString(R.string.mdtp_date_v2_daymonthyear), Locale.getDefault()); + VERSION_2_FORMAT = new SimpleDateFormat(activity.getResources().getString(R.string.mdtp_date_v2_daymonthyear), mLocale); } else { - VERSION_2_FORMAT = new SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "EEEMMMdd"), Locale.getDefault()); + VERSION_2_FORMAT = new SimpleDateFormat(DateFormat.getBestDateTimePattern(mLocale, "EEEMMMdd"), mLocale); } VERSION_2_FORMAT.setTimeZone(getTimeZone()); } @@ -247,8 +263,6 @@ public void onSaveInstanceState(@NonNull Bundle outState) { outState.putInt(KEY_SELECTED_MONTH, mCalendar.get(Calendar.MONTH)); outState.putInt(KEY_SELECTED_DAY, mCalendar.get(Calendar.DAY_OF_MONTH)); outState.putInt(KEY_WEEK_START, mWeekStart); - outState.putInt(KEY_YEAR_START, mMinYear); - outState.putInt(KEY_YEAR_END, mMaxYear); outState.putInt(KEY_CURRENT_VIEW, mCurrentView); int listPosition = -1; if (mCurrentView == MONTH_AND_DAY_VIEW) { @@ -258,11 +272,7 @@ public void onSaveInstanceState(@NonNull Bundle outState) { outState.putInt(KEY_LIST_POSITION_OFFSET, mYearPickerView.getFirstPositionOffset()); } outState.putInt(KEY_LIST_POSITION, listPosition); - outState.putSerializable(KEY_MIN_DATE, mMinDate); - outState.putSerializable(KEY_MAX_DATE, mMaxDate); outState.putSerializable(KEY_HIGHLIGHTED_DAYS, highlightedDays); - outState.putSerializable(KEY_SELECTABLE_DAYS, selectableDays); - outState.putSerializable(KEY_DISABLED_DAYS, disabledDays); outState.putBoolean(KEY_THEME_DARK, mThemeDark); outState.putBoolean(KEY_THEME_DARK_CHANGED, mThemeDarkChanged); outState.putInt(KEY_ACCENT, mAccentColor); @@ -278,27 +288,30 @@ public void onSaveInstanceState(@NonNull Bundle outState) { outState.putString(KEY_CANCEL_STRING, mCancelString); outState.putInt(KEY_CANCEL_COLOR, mCancelColor); outState.putSerializable(KEY_VERSION, mVersion); + outState.putSerializable(KEY_SCROLL_ORIENTATION, mScrollOrientation); outState.putSerializable(KEY_TIMEZONE, mTimezone); + outState.putParcelable(KEY_DATERANGELIMITER, mDateRangeLimiter); + outState.putSerializable(KEY_LOCALE, mLocale); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + Bundle savedInstanceState) { int listPosition = -1; int listPositionOffset = 0; int currentView = mDefaultView; + if (mScrollOrientation == null) { + mScrollOrientation = mVersion == Version.VERSION_1 + ? ScrollOrientation.VERTICAL + : ScrollOrientation.HORIZONTAL; + } if (savedInstanceState != null) { mWeekStart = savedInstanceState.getInt(KEY_WEEK_START); - mMinYear = savedInstanceState.getInt(KEY_YEAR_START); - mMaxYear = savedInstanceState.getInt(KEY_YEAR_END); currentView = savedInstanceState.getInt(KEY_CURRENT_VIEW); listPosition = savedInstanceState.getInt(KEY_LIST_POSITION); listPositionOffset = savedInstanceState.getInt(KEY_LIST_POSITION_OFFSET); - mMinDate = (Calendar)savedInstanceState.getSerializable(KEY_MIN_DATE); - mMaxDate = (Calendar)savedInstanceState.getSerializable(KEY_MAX_DATE); + //noinspection unchecked highlightedDays = (HashSet) savedInstanceState.getSerializable(KEY_HIGHLIGHTED_DAYS); - selectableDays = (TreeSet) savedInstanceState.getSerializable(KEY_SELECTABLE_DAYS); - disabledDays = (HashSet) savedInstanceState.getSerializable(KEY_DISABLED_DAYS); mThemeDark = savedInstanceState.getBoolean(KEY_THEME_DARK); mThemeDarkChanged = savedInstanceState.getBoolean(KEY_THEME_DARK_CHANGED); mAccentColor = savedInstanceState.getInt(KEY_ACCENT); @@ -313,20 +326,44 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, mCancelString = savedInstanceState.getString(KEY_CANCEL_STRING); mCancelColor = savedInstanceState.getInt(KEY_CANCEL_COLOR); mVersion = (Version) savedInstanceState.getSerializable(KEY_VERSION); + mScrollOrientation = (ScrollOrientation) savedInstanceState.getSerializable(KEY_SCROLL_ORIENTATION); mTimezone = (TimeZone) savedInstanceState.getSerializable(KEY_TIMEZONE); + mDateRangeLimiter = savedInstanceState.getParcelable(KEY_DATERANGELIMITER); + + /* + We need to update some variables when setting the locale, so use the setter rather + than a plain assignment + */ + setLocale((Locale) savedInstanceState.getSerializable(KEY_LOCALE)); + + /* + If the user supplied a custom limiter, we need to create a new default one to prevent + null pointer exceptions on the configuration methods + If the user did not supply a custom limiter we need to ensure both mDefaultLimiter + and mDateRangeLimiter are the same reference, so that the config methods actually + affect the behaviour of the picker (in the unlikely event the user reconfigures + the picker when it is shown) + */ + if (mDateRangeLimiter instanceof DefaultDateRangeLimiter) { + mDefaultLimiter = (DefaultDateRangeLimiter) mDateRangeLimiter; + } else { + mDefaultLimiter = new DefaultDateRangeLimiter(); + } } + mDefaultLimiter.setController(this); + int viewRes = mVersion == Version.VERSION_1 ? R.layout.mdtp_date_picker_dialog : R.layout.mdtp_date_picker_dialog_v2; View view = inflater.inflate(viewRes, container, false); // All options have been set at this point: round the initial selection if necessary - setToNearestDate(mCalendar); + mCalendar = mDateRangeLimiter.setToNearestDate(mCalendar); - mDatePickerHeaderView = (TextView) view.findViewById(R.id.mdtp_date_picker_header); - mMonthAndDayView = (LinearLayout) view.findViewById(R.id.mdtp_date_picker_month_and_day); + mDatePickerHeaderView = view.findViewById(R.id.mdtp_date_picker_header); + mMonthAndDayView = view.findViewById(R.id.mdtp_date_picker_month_and_day); mMonthAndDayView.setOnClickListener(this); - mSelectedMonthTextView = (TextView) view.findViewById(R.id.mdtp_date_picker_month); - mSelectedDayTextView = (TextView) view.findViewById(R.id.mdtp_date_picker_day); - mYearView = (TextView) view.findViewById(R.id.mdtp_date_picker_year); + mSelectedMonthTextView = view.findViewById(R.id.mdtp_date_picker_month); + mSelectedDayTextView = view.findViewById(R.id.mdtp_date_picker_day); + mYearView = view.findViewById(R.id.mdtp_date_picker_year); mYearView.setOnClickListener(this); final Activity activity = getActivity(); @@ -345,9 +382,11 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, mSelectYear = res.getString(R.string.mdtp_select_year); int bgColorResource = mThemeDark ? R.color.mdtp_date_picker_view_animator_dark_theme : R.color.mdtp_date_picker_view_animator; - view.setBackgroundColor(ContextCompat.getColor(activity, bgColorResource)); + int bgColor = ContextCompat.getColor(activity, bgColorResource); + view.setBackgroundColor(bgColor); - mAnimator = (AccessibleDateAnimator) view.findViewById(R.id.mdtp_animator); + mAnimator = view.findViewById(R.id.mdtp_animator); + mAnimator.setBackgroundColor(bgColor); mAnimator.addView(mDayPickerView); mAnimator.addView(mYearPickerView); mAnimator.setDateMillis(mCalendar.getTimeInMillis()); @@ -360,7 +399,8 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, animation2.setDuration(ANIMATION_DURATION); mAnimator.setOutAnimation(animation2); - Button okButton = (Button) view.findViewById(R.id.mdtp_ok); + String buttonTypeface = activity.getResources().getString(R.string.mdtp_button_typeface); + Button okButton = view.findViewById(R.id.mdtp_ok); okButton.setOnClickListener(new OnClickListener() { @Override @@ -370,20 +410,20 @@ public void onClick(View v) { dismiss(); } }); - okButton.setTypeface(TypefaceHelper.get(activity, "Roboto-Medium")); - if(mOkString != null) okButton.setText(mOkString); + okButton.setTypeface(TypefaceHelper.get(activity, buttonTypeface)); + if (mOkString != null) okButton.setText(mOkString); else okButton.setText(mOkResid); - Button cancelButton = (Button) view.findViewById(R.id.mdtp_cancel); + Button cancelButton = view.findViewById(R.id.mdtp_cancel); cancelButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { tryVibrate(); - if(getDialog() != null) getDialog().cancel(); + if (getDialog() != null) getDialog().cancel(); } }); - cancelButton.setTypeface(TypefaceHelper.get(activity,"Roboto-Medium")); - if(mCancelString != null) cancelButton.setText(mCancelString); + cancelButton.setTypeface(TypefaceHelper.get(activity, buttonTypeface)); + if (mCancelString != null) cancelButton.setText(mCancelString); else cancelButton.setText(mCancelResid); cancelButton.setVisibility(isCancelable() ? View.VISIBLE : View.GONE); @@ -391,7 +431,7 @@ public void onClick(View v) { if (mAccentColor == -1) { mAccentColor = Utils.getAccentColorFromThemeIfAvailable(getActivity()); } - if(mDatePickerHeaderView != null) mDatePickerHeaderView.setBackgroundColor(Utils.darkenColor(mAccentColor)); + if (mDatePickerHeaderView != null) mDatePickerHeaderView.setBackgroundColor(Utils.darkenColor(mAccentColor)); view.findViewById(R.id.mdtp_day_picker_selected_date_layout).setBackgroundColor(mAccentColor); // Buttons can have a different color @@ -400,7 +440,7 @@ public void onClick(View v) { if (mCancelColor != -1) cancelButton.setTextColor(mCancelColor); else cancelButton.setTextColor(mAccentColor); - if(getDialog() == null) { + if (getDialog() == null) { view.findViewById(R.id.mdtp_done_background).setVisibility(View.GONE); } @@ -447,19 +487,19 @@ public void onResume() { public void onPause() { super.onPause(); mHapticFeedbackController.stop(); - if(mDismissOnPause) dismiss(); + if (mDismissOnPause) dismiss(); } @Override public void onCancel(DialogInterface dialog) { super.onCancel(dialog); - if(mOnCancelListener != null) mOnCancelListener.onCancel(dialog); + if (mOnCancelListener != null) mOnCancelListener.onCancel(dialog); } @Override public void onDismiss(DialogInterface dialog) { super.onDismiss(dialog); - if(mOnDismissListener != null) mOnDismissListener.onDismiss(dialog); + if (mOnDismissListener != null) mOnDismissListener.onDismiss(dialog); } private void setCurrentView(final int viewIndex) { @@ -494,7 +534,7 @@ private void setCurrentView(final int viewIndex) { int flags = DateUtils.FORMAT_SHOW_DATE; String dayString = DateUtils.formatDateTime(getActivity(), millis, flags); - mAnimator.setContentDescription(mDayPickerDescription+": "+dayString); + mAnimator.setContentDescription(mDayPickerDescription + ": " + dayString); Utils.tryAccessibilityAnnounce(mAnimator, mSelectDay); break; case YEAR_VIEW: @@ -523,7 +563,7 @@ private void setCurrentView(final int viewIndex) { } CharSequence yearString = YEAR_FORMAT.format(millis); - mAnimator.setContentDescription(mYearPickerDescription+": "+yearString); + mAnimator.setContentDescription(mYearPickerDescription + ": " + yearString); Utils.tryAccessibilityAnnounce(mAnimator, mSelectYear); break; } @@ -535,10 +575,10 @@ private void updateDisplay(boolean announce) { if (mVersion == Version.VERSION_1) { if (mDatePickerHeaderView != null) { if (mTitle != null) - mDatePickerHeaderView.setText(mTitle.toUpperCase(Locale.getDefault())); + mDatePickerHeaderView.setText(mTitle.toUpperCase(mLocale)); else { mDatePickerHeaderView.setText(mCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, - Locale.getDefault()).toUpperCase(Locale.getDefault())); + mLocale).toUpperCase(mLocale)); } } mSelectedMonthTextView.setText(MONTH_FORMAT.format(mCalendar.getTime())); @@ -548,7 +588,7 @@ private void updateDisplay(boolean announce) { if (mVersion == Version.VERSION_2) { mSelectedDayTextView.setText(VERSION_2_FORMAT.format(mCalendar.getTime())); if (mTitle != null) - mDatePickerHeaderView.setText(mTitle.toUpperCase(Locale.getDefault())); + mDatePickerHeaderView.setText(mTitle.toUpperCase(mLocale)); else mDatePickerHeaderView.setVisibility(View.GONE); } @@ -569,6 +609,7 @@ private void updateDisplay(boolean announce) { /** * Set whether the device should vibrate when touching fields + * * @param vibrate true if the device should vibrate when touching a field */ public void vibrate(boolean vibrate) { @@ -577,6 +618,7 @@ public void vibrate(boolean vibrate) { /** * Set whether the picker should dismiss itself when being paused or whether it should try to survive an orientation change + * * @param dismissOnPause true if the dialog should dismiss itself when it's pausing */ public void dismissOnPause(boolean dismissOnPause) { @@ -585,6 +627,7 @@ public void dismissOnPause(boolean dismissOnPause) { /** * Set whether the picker should dismiss itself when a day is selected + * * @param autoDismiss true if the dialog should dismiss itself when a day is selected */ @SuppressWarnings("unused") @@ -594,6 +637,7 @@ public void autoDismiss(boolean autoDismiss) { /** * Set whether the dark theme should be used + * * @param themeDark true if the dark theme should be used, false if the default theme should be used */ public void setThemeDark(boolean themeDark) { @@ -603,6 +647,7 @@ public void setThemeDark(boolean themeDark) { /** * Returns true when the dark theme should be used + * * @return true if the dark theme should be used, false if the default theme should be used */ @Override @@ -612,6 +657,7 @@ public boolean isThemeDark() { /** * Set the accent color of this dialog + * * @param color the accent color you want */ @SuppressWarnings("unused") @@ -621,6 +667,7 @@ public void setAccentColor(String color) { /** * Set the accent color of this dialog + * * @param color the accent color you want */ public void setAccentColor(@ColorInt int color) { @@ -629,6 +676,7 @@ public void setAccentColor(@ColorInt int color) { /** * Set the text color of the OK button + * * @param color the color you want */ @SuppressWarnings("unused") @@ -638,6 +686,7 @@ public void setOkColor(String color) { /** * Set the text color of the OK button + * * @param color the color you want */ @SuppressWarnings("unused") @@ -647,6 +696,7 @@ public void setOkColor(@ColorInt int color) { /** * Set the text color of the Cancel button + * * @param color the color you want */ @SuppressWarnings("unused") @@ -656,6 +706,7 @@ public void setCancelColor(String color) { /** * Set the text color of the Cancel button + * * @param color the color you want */ @SuppressWarnings("unused") @@ -665,6 +716,7 @@ public void setCancelColor(@ColorInt int color) { /** * Get the accent color of this dialog + * * @return accent color */ @Override @@ -674,6 +726,7 @@ public int getAccentColor() { /** * Set whether the year picker of the month and day picker is shown first + * * @param yearPicker boolean */ public void showYearPickerFirst(boolean yearPicker) { @@ -694,12 +747,8 @@ public void setFirstDayOfWeek(int startOfWeek) { @SuppressWarnings("unused") public void setYearRange(int startYear, int endYear) { - if (endYear < startYear) { - throw new IllegalArgumentException("Year end must be larger than or equal to year start"); - } + mDefaultLimiter.setYearRange(startYear, endYear); - mMinYear = startYear; - mMaxYear = endYear; if (mDayPickerView != null) { mDayPickerView.onChange(); } @@ -708,11 +757,12 @@ public void setYearRange(int startYear, int endYear) { /** * Sets the minimal date supported by this DatePicker. Dates before (but not including) the * specified date will be disallowed from being selected. + * * @param calendar a Calendar object set to the year, month, day desired as the mindate. */ @SuppressWarnings("unused") public void setMinDate(Calendar calendar) { - mMinDate = trimToMidnight((Calendar) calendar.clone()); + mDefaultLimiter.setMinDate(calendar); if (mDayPickerView != null) { mDayPickerView.onChange(); @@ -724,17 +774,18 @@ public void setMinDate(Calendar calendar) { */ @SuppressWarnings("unused") public Calendar getMinDate() { - return mMinDate; + return mDefaultLimiter.getMinDate(); } /** * Sets the minimal date supported by this DatePicker. Dates after (but not including) the * specified date will be disallowed from being selected. + * * @param calendar a Calendar object set to the year, month, day desired as the maxdate. */ @SuppressWarnings("unused") public void setMaxDate(Calendar calendar) { - mMaxDate = trimToMidnight((Calendar) calendar.clone()); + mDefaultLimiter.setMaxDate(calendar); if (mDayPickerView != null) { mDayPickerView.onChange(); @@ -746,17 +797,19 @@ public void setMaxDate(Calendar calendar) { */ @SuppressWarnings("unused") public Calendar getMaxDate() { - return mMaxDate; + return mDefaultLimiter.getMaxDate(); } /** * Sets an array of dates which should be highlighted when the picker is drawn + * * @param highlightedDays an Array of Calendar objects containing the dates to be highlighted */ @SuppressWarnings("unused") public void setHighlightedDays(Calendar[] highlightedDays) { - for (Calendar highlightedDay : highlightedDays) trimToMidnight(highlightedDay); - this.highlightedDays.addAll(Arrays.asList(highlightedDays)); + for (Calendar highlightedDay : highlightedDays) { + this.highlightedDays.add(Utils.trimToMidnight((Calendar) highlightedDay.clone())); + } if (mDayPickerView != null) mDayPickerView.onChange(); } @@ -773,23 +826,23 @@ public Calendar[] getHighlightedDays() { @Override public boolean isHighlighted(int year, int month, int day) { - Calendar date = Calendar.getInstance(); + Calendar date = Calendar.getInstance(getTimeZone()); date.set(Calendar.YEAR, year); date.set(Calendar.MONTH, month); date.set(Calendar.DAY_OF_MONTH, day); - trimToMidnight(date); + Utils.trimToMidnight(date); return highlightedDays.contains(date); } /** * Sets a list of days which are the only valid selections. * Setting this value will take precedence over using setMinDate() and setMaxDate() + * * @param selectableDays an Array of Calendar Objects containing the selectable dates */ @SuppressWarnings("unused") public void setSelectableDays(Calendar[] selectableDays) { - for (Calendar selectableDay : selectableDays) trimToMidnight(selectableDay); - this.selectableDays.addAll(Arrays.asList(selectableDays)); + mDefaultLimiter.setSelectableDays(selectableDays); if (mDayPickerView != null) mDayPickerView.onChange(); } @@ -798,18 +851,18 @@ public void setSelectableDays(Calendar[] selectableDays) { */ @SuppressWarnings("unused") public Calendar[] getSelectableDays() { - return selectableDays.isEmpty() ? null : selectableDays.toArray(new Calendar[0]); + return mDefaultLimiter.getSelectableDays(); } /** * Sets a list of days that are not selectable in the picker * Setting this value will take precedence over using setMinDate() and setMaxDate(), but stacks with setSelectableDays() + * * @param disabledDays an Array of Calendar Objects containing the disabled dates */ @SuppressWarnings("unused") public void setDisabledDays(Calendar[] disabledDays) { - for (Calendar disabledDay : disabledDays) trimToMidnight(disabledDay); - this.disabledDays.addAll(Arrays.asList(disabledDays)); + mDefaultLimiter.setDisabledDays(disabledDays); if (mDayPickerView != null) mDayPickerView.onChange(); } @@ -818,14 +871,21 @@ public void setDisabledDays(Calendar[] disabledDays) { */ @SuppressWarnings("unused") public Calendar[] getDisabledDays() { - if (disabledDays.isEmpty()) return null; - Calendar[] output = disabledDays.toArray(new Calendar[0]); - Arrays.sort(output); - return output; + return mDefaultLimiter.getDisabledDays(); + } + + /** + * Provide a DateRangeLimiter for full control over which dates are enabled and disabled in the picker + * @param dateRangeLimiter An implementation of the DateRangeLimiter interface + */ + @SuppressWarnings("unused") + public void setDateRangeLimiter(DateRangeLimiter dateRangeLimiter) { + mDateRangeLimiter = dateRangeLimiter; } /** * Set a title to be displayed instead of the weekday + * * @param title String - The title to be displayed */ public void setTitle(String title) { @@ -834,6 +894,7 @@ public void setTitle(String title) { /** * Set the label for the Ok button (max 12 characters) + * * @param okString A literal String to be used as the Ok button label */ @SuppressWarnings("unused") @@ -843,6 +904,7 @@ public void setOkText(String okString) { /** * Set the label for the Ok button (max 12 characters) + * * @param okResid A resource ID to be used as the Ok button label */ @SuppressWarnings("unused") @@ -853,6 +915,7 @@ public void setOkText(@StringRes int okResid) { /** * Set the label for the Cancel button (max 12 characters) + * * @param cancelString A literal String to be used as the Cancel button label */ @SuppressWarnings("unused") @@ -862,6 +925,7 @@ public void setCancelText(String cancelString) { /** * Set the label for the Cancel button (max 12 characters) + * * @param cancelResid A resource ID to be used as the Cancel button label */ @SuppressWarnings("unused") @@ -872,17 +936,47 @@ public void setCancelText(@StringRes int cancelResid) { /** * Set which layout version the picker should use + * * @param version The version to use */ public void setVersion(Version version) { mVersion = version; } + /** + * Get the layout version the Dialog is using + * + * @return Version + */ + public Version getVersion() { + return mVersion; + } + + /** + * Set which way the user needs to swipe to switch months in the MonthView + * @param orientation The orientation to use + */ + public void setScrollOrientation(ScrollOrientation orientation) { + mScrollOrientation = orientation; + } + + /** + * Get which way the user needs to swipe to switch months in the MonthView + * @return SwipeOrientation + */ + public ScrollOrientation getScrollOrientation() { + return mScrollOrientation; + } + /** * Set which timezone the picker should use + * + * This has been deprecated in favor of setting the TimeZone using the constructor that + * takes a Calendar object * @param timeZone The timezone to use */ - @SuppressWarnings("unused") + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated public void setTimeZone(TimeZone timeZone) { mTimezone = timeZone; mCalendar.setTimeZone(timeZone); @@ -891,6 +985,27 @@ public void setTimeZone(TimeZone timeZone) { DAY_FORMAT.setTimeZone(timeZone); } + /** + * Set a custom locale to be used when generating various strings in the picker + * @param locale Locale + */ + public void setLocale(Locale locale) { + mLocale = locale; + mWeekStart = Calendar.getInstance(mTimezone, mLocale).getFirstDayOfWeek(); + YEAR_FORMAT = new SimpleDateFormat("yyyy", locale); + MONTH_FORMAT = new SimpleDateFormat("MMM", locale); + DAY_FORMAT = new SimpleDateFormat("dd", locale); + } + + /** + * Return the current locale (default or other) + * @return Locale + */ + @Override + public Locale getLocale() { + return mLocale; + } + @SuppressWarnings("unused") public void setOnDateSetListener(OnDateSetListener listener) { mCallBack = listener; @@ -906,17 +1021,26 @@ public void setOnDismissListener(DialogInterface.OnDismissListener onDismissList mOnDismissListener = onDismissListener; } + /** + * Get a reference to the callback + * @return OnDateSetListener the callback + */ + @SuppressWarnings("unused") + public OnDateSetListener getOnDateSetListener() { + return mCallBack; + } + // If the newly selected month / year does not contain the currently selected day number, // change the selected day number to the last day of the selected month or year. // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30 // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013 - private void adjustDayInMonthIfNeeded(Calendar calendar) { + private Calendar adjustDayInMonthIfNeeded(Calendar calendar) { int day = calendar.get(Calendar.DAY_OF_MONTH); int daysInMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH); if (day > daysInMonth) { calendar.set(Calendar.DAY_OF_MONTH, daysInMonth); } - setToNearestDate(calendar); + return mDateRangeLimiter.setToNearestDate(calendar); } @Override @@ -932,7 +1056,7 @@ public void onClick(View v) { @Override public void onYearSelected(int year) { mCalendar.set(Calendar.YEAR, year); - adjustDayInMonthIfNeeded(mCalendar); + mCalendar = adjustDayInMonthIfNeeded(mCalendar); updatePickers(); setCurrentView(MONTH_AND_DAY_VIEW); updateDisplay(true); @@ -952,7 +1076,7 @@ public void onDayOfMonthSelected(int year, int month, int day) { } private void updatePickers() { - for(OnDateChangedListener listener : mListeners) listener.onDateChanged(); + for (OnDateChangedListener listener : mListeners) listener.onDateChanged(); } @@ -963,142 +1087,28 @@ public MonthAdapter.CalendarDay getSelectedDay() { @Override public Calendar getStartDate() { - if (!selectableDays.isEmpty()) return selectableDays.first(); - if (mMinDate != null) return mMinDate; - Calendar output = Calendar.getInstance(getTimeZone()); - output.set(Calendar.YEAR, mMinYear); - output.set(Calendar.DAY_OF_MONTH, 1); - output.set(Calendar.MONTH, Calendar.JANUARY); - return output; + return mDateRangeLimiter.getStartDate(); } @Override public Calendar getEndDate() { - if (!selectableDays.isEmpty()) return selectableDays.last(); - if (mMaxDate != null) return mMaxDate; - Calendar output = Calendar.getInstance(getTimeZone()); - output.set(Calendar.YEAR, mMaxYear); - output.set(Calendar.DAY_OF_MONTH, 31); - output.set(Calendar.MONTH, Calendar.DECEMBER); - return output; + return mDateRangeLimiter.getEndDate(); } @Override public int getMinYear() { - if (!selectableDays.isEmpty()) return selectableDays.first().get(Calendar.YEAR); - // Ensure no years can be selected outside of the given minimum date - return mMinDate != null && mMinDate.get(Calendar.YEAR) > mMinYear ? mMinDate.get(Calendar.YEAR) : mMinYear; + return mDateRangeLimiter.getMinYear(); } @Override public int getMaxYear() { - if (!selectableDays.isEmpty()) return selectableDays.last().get(Calendar.YEAR); - // Ensure no years can be selected outside of the given maximum date - return mMaxDate != null && mMaxDate.get(Calendar.YEAR) < mMaxYear ? mMaxDate.get(Calendar.YEAR) : mMaxYear; + return mDateRangeLimiter.getMaxYear(); } - /** - * @return true if the specified year/month/day are within the selectable days or the range set by minDate and maxDate. - * If one or either have not been set, they are considered as Integer.MIN_VALUE and - * Integer.MAX_VALUE. - */ + @Override public boolean isOutOfRange(int year, int month, int day) { - Calendar date = Calendar.getInstance(); - date.set(Calendar.YEAR, year); - date.set(Calendar.MONTH, month); - date.set(Calendar.DAY_OF_MONTH, day); - return isOutOfRange(date); - } - - @SuppressWarnings("unused") - public boolean isOutOfRange(Calendar calendar) { - trimToMidnight(calendar); - return isDisabled(calendar) || !isSelectable(calendar); - } - - private boolean isDisabled(Calendar c) { - return disabledDays.contains(trimToMidnight(c)) || isBeforeMin(c) || isAfterMax(c); - } - - private boolean isSelectable(Calendar c) { - return selectableDays.isEmpty() || selectableDays.contains(trimToMidnight(c)); - } - - private boolean isBeforeMin(Calendar calendar) { - return mMinDate != null && calendar.before(mMinDate); - } - - private boolean isAfterMax(Calendar calendar) { - return mMaxDate != null && calendar.after(mMaxDate); - } - - private void setToNearestDate(Calendar calendar) { - if (!selectableDays.isEmpty()) { - Calendar newCalendar = null; - Calendar higher = selectableDays.ceiling(calendar); - Calendar lower = selectableDays.lower(calendar); - - if (higher == null && lower != null) newCalendar = lower; - else if (lower == null && higher != null) newCalendar = higher; - - if (newCalendar != null || higher == null) { - newCalendar = newCalendar == null ? calendar : newCalendar; - newCalendar.setTimeZone(getTimeZone()); - calendar.setTimeInMillis(newCalendar.getTimeInMillis()); - return; - } - - long highDistance = Math.abs(higher.getTimeInMillis() - calendar.getTimeInMillis()); - long lowDistance = Math.abs(calendar.getTimeInMillis() - lower.getTimeInMillis()); - - if (lowDistance < highDistance) calendar.setTimeInMillis(lower.getTimeInMillis()); - else calendar.setTimeInMillis(higher.getTimeInMillis()); - - return; - } - - if (!disabledDays.isEmpty()) { - Calendar forwardDate = (Calendar) calendar.clone(); - Calendar backwardDate = (Calendar) calendar.clone(); - while (isDisabled(forwardDate) && isDisabled(backwardDate)) { - forwardDate.add(Calendar.DAY_OF_MONTH, 1); - backwardDate.add(Calendar.DAY_OF_MONTH, -1); - } - if (!isDisabled(backwardDate)) { - calendar.setTimeInMillis(backwardDate.getTimeInMillis()); - return; - } - if (!isDisabled(forwardDate)) { - calendar.setTimeInMillis(forwardDate.getTimeInMillis()); - return; - } - } - - - if(isBeforeMin(calendar)) { - calendar.setTimeInMillis(mMinDate.getTimeInMillis()); - return; - } - - if(isAfterMax(calendar)) { - calendar.setTimeInMillis(mMaxDate.getTimeInMillis()); - return; - } - } - - /** - * Trims off all time information, effectively setting it to midnight - * Makes it easier to compare at just the day level - * @param calendar The Calendar object to trim - * @return The trimmed Calendar object - */ - private Calendar trimToMidnight(Calendar calendar) { - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - return calendar; + return mDateRangeLimiter.isOutOfRange(year, month, day); } @Override @@ -1118,7 +1128,7 @@ public void unregisterOnDateChangedListener(OnDateChangedListener listener) { @Override public void tryVibrate() { - if(mVibrate) mHapticFeedbackController.tryVibrate(); + if (mVibrate) mHapticFeedbackController.tryVibrate(); } @Override public TimeZone getTimeZone() { diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DateRangeLimiter.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DateRangeLimiter.java new file mode 100644 index 00000000..7b66f09e --- /dev/null +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DateRangeLimiter.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 Wouter Dullaert + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.wdullaer.materialdatetimepicker.date; + +import android.os.Parcelable; +import android.support.annotation.NonNull; + +import java.util.Calendar; + +@SuppressWarnings("WeakerAccess") +public interface DateRangeLimiter extends Parcelable { + int getMinYear(); + + int getMaxYear(); + + @NonNull Calendar getStartDate(); + + @NonNull Calendar getEndDate(); + + boolean isOutOfRange(int year, int month, int day); + + @NonNull Calendar setToNearestDate(@NonNull Calendar day); +} \ No newline at end of file diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DayPickerView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DayPickerView.java index 20bcf81d..4fbc912e 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DayPickerView.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DayPickerView.java @@ -22,16 +22,16 @@ import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.util.Log; +import android.view.Gravity; import android.view.View; -import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.AbsListView; -import android.widget.AbsListView.OnScrollListener; -import android.widget.ListView; +import com.wdullaer.materialdatetimepicker.GravitySnapHelper; import com.wdullaer.materialdatetimepicker.Utils; import com.wdullaer.materialdatetimepicker.date.DatePickerDialog.OnDateChangedListener; @@ -42,31 +42,21 @@ /** * This displays a list of months in a calendar format with selectable days. */ -public abstract class DayPickerView extends ListView implements OnScrollListener, - OnDateChangedListener { +public abstract class DayPickerView extends RecyclerView implements OnDateChangedListener { private static final String TAG = "MonthFragment"; // Affects when the month selection will change while scrolling up protected static final int SCROLL_HYST_WEEKS = 2; - // How long the GoTo fling animation should last - protected static final int GOTO_SCROLL_DURATION = 250; - // How long to wait after receiving an onScrollStateChanged notification - // before acting on it - protected static final int SCROLL_CHANGE_DELAY = 40; + // The number of days to display in each week public static final int DAYS_PER_WEEK = 7; - public static int LIST_TOP_OFFSET = -1; // so that the top line will be - // under the separator - // You can override these numbers to get a different appearance + protected int mNumWeeks = 6; protected boolean mShowWeekNumber = false; protected int mDaysPerWeek = 7; private static SimpleDateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy", Locale.getDefault()); - // These affect the scroll speed and feel - protected float mFriction = 1.0f; - protected Context mContext; protected Handler mHandler; @@ -85,12 +75,10 @@ public abstract class DayPickerView extends ListView implements OnScrollListener // used for tracking during a scroll protected long mPreviousScrollPosition; // used for tracking what state listview is in - protected int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE; - // used for tracking what state listview is in - protected int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE; + protected int mPreviousScrollState = RecyclerView.SCROLL_STATE_IDLE; private DatePickerController mController; - private boolean mPerformingScroll; + private LinearLayoutManager linearLayoutManager; public DayPickerView(Context context, AttributeSet attrs) { super(context, attrs); @@ -99,8 +87,8 @@ public DayPickerView(Context context, AttributeSet attrs) { public DayPickerView(Context context, DatePickerController controller) { super(context); - init(context); setController(controller); + init(context); } public void setController(DatePickerController controller) { @@ -108,30 +96,62 @@ public void setController(DatePickerController controller) { mController.registerOnDateChangedListener(this); mSelectedDay = new MonthAdapter.CalendarDay(mController.getTimeZone()); mTempDay = new MonthAdapter.CalendarDay(mController.getTimeZone()); + YEAR_FORMAT = new SimpleDateFormat("yyyy", controller.getLocale()); refreshAdapter(); onDateChanged(); } public void init(Context context) { + int scrollOrientation = mController.getScrollOrientation() == DatePickerDialog.ScrollOrientation.VERTICAL + ? LinearLayoutManager.VERTICAL + : LinearLayoutManager.HORIZONTAL; + linearLayoutManager = new LinearLayoutManager(context, scrollOrientation, false); + setLayoutManager(linearLayoutManager); mHandler = new Handler(); setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - setDrawSelectorOnTop(false); + setVerticalScrollBarEnabled(false); + setHorizontalScrollBarEnabled(false); mContext = context; - setUpListView(); + setUpRecyclerView(); + } + + public void setScrollOrientation(int orientation) { + linearLayoutManager.setOrientation(orientation); + } + + /** + * Sets all the required fields for the list view. Override this method to + * set a different list view behavior. + */ + protected void setUpRecyclerView() { + setVerticalScrollBarEnabled(false); + setFadingEdgeLength(0); + int gravity = mController.getScrollOrientation() == DatePickerDialog.ScrollOrientation.VERTICAL + ? Gravity.TOP + : Gravity.START; + GravitySnapHelper helper = new GravitySnapHelper(gravity); + helper.attachToRecyclerView(this); } public void onChange() { refreshAdapter(); } + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + final MonthAdapter.CalendarDay focusedDay = findAccessibilityFocus(); + restoreAccessibilityFocus(focusedDay); + } + /** * Creates a new adapter if necessary and sets up its parameters. Override * this method to provide a custom adapter. */ protected void refreshAdapter() { if (mAdapter == null) { - mAdapter = createMonthAdapter(getContext(), mController); + mAdapter = createMonthAdapter(mController); } else { mAdapter.setSelectedDay(mSelectedDay); } @@ -139,28 +159,7 @@ protected void refreshAdapter() { setAdapter(mAdapter); } - public abstract MonthAdapter createMonthAdapter(Context context, - DatePickerController controller); - - /* - * Sets all the required fields for the list view. Override this method to - * set a different list view behavior. - */ - protected void setUpListView() { - // Transparent background on scroll - setCacheColorHint(0); - // No dividers - setDivider(null); - // Items are clickable - setItemsCanFocus(true); - // The thumb gets in the way, so disable it - setFastScrollEnabled(false); - setVerticalScrollBarEnabled(false); - setOnScrollListener(this); - setFadingEdgeLength(0); - // Make the scrolling behavior nicer - setFriction(ViewConfiguration.getScrollFriction() * mFriction); - } + public abstract MonthAdapter createMonthAdapter(DatePickerController controller); /** * This moves to the specified time in the view. If the time is not already @@ -169,12 +168,12 @@ protected void setUpListView() { * the list will not be scrolled unless forceScroll is true. This time may * optionally be highlighted as selected as well. * - * @param day The day to move to - * @param animate Whether to scroll to the given time or just redraw at the - * new location + * @param day The day to move to + * @param animate Whether to scroll to the given time or just redraw at the + * new location * @param setSelected Whether to set the given time as selected * @param forceScroll Whether to recenter even if the time is already - * visible + * visible * @return Whether or not the view animated to the new location */ public boolean goTo(MonthAdapter.CalendarDay day, boolean animate, boolean setSelected, boolean forceScroll) { @@ -205,12 +204,7 @@ public boolean goTo(MonthAdapter.CalendarDay day, boolean animate, boolean setSe } while (top < 0); // Compute the first and last position visible - int selectedPosition; - if (child != null) { - selectedPosition = getPositionForView(child); - } else { - selectedPosition = 0; - } + int selectedPosition = child != null ? getChildAdapterPosition(child) : 0; if (setSelected) { mAdapter.setSelectedDay(mSelectedDay); @@ -223,10 +217,9 @@ public boolean goTo(MonthAdapter.CalendarDay day, boolean animate, boolean setSe // and if so scroll to the month that contains it if (position != selectedPosition || forceScroll) { setMonthDisplayed(mTempDay); - mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING; + mPreviousScrollState = RecyclerView.SCROLL_STATE_DRAGGING; if (animate) { - smoothScrollToPositionFromTop( - position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION); + smoothScrollToPosition(position); return true; } else { postSetSelection(position); @@ -243,28 +236,9 @@ public void postSetSelection(final int position) { @Override public void run() { - setSelection(position); + ((LinearLayoutManager) getLayoutManager()).scrollToPositionWithOffset(position, 0); } }); - onScrollStateChanged(this, OnScrollListener.SCROLL_STATE_IDLE); - } - - /** - * Updates the title and selected month if the view has moved to a new - * month. - */ - @Override - public void onScroll( - AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - MonthView child = (MonthView) view.getChildAt(0); - if (child == null) { - return; - } - - // Figure out where we are - long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom(); - mPreviousScrollPosition = currScroll; - mPreviousScrollState = mCurrentScrollState; } /** @@ -273,99 +247,37 @@ public void onScroll( */ protected void setMonthDisplayed(MonthAdapter.CalendarDay date) { mCurrentMonthDisplayed = date.month; - invalidateViews(); - } - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - // use a post to prevent re-entering onScrollStateChanged before it - // exits - mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); - } - - protected ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable(); - - protected class ScrollStateRunnable implements Runnable { - private int mNewState; - - /** - * Sets up the runnable with a short delay in case the scroll state - * immediately changes again. - * - * @param view The list view that changed state - * @param scrollState The new state it changed to - */ - public void doScrollStateChange(AbsListView view, int scrollState) { - mHandler.removeCallbacks(this); - mNewState = scrollState; - mHandler.postDelayed(this, SCROLL_CHANGE_DELAY); - } - - @Override - public void run() { - mCurrentScrollState = mNewState; - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, - "new scroll state: " + mNewState + " old state: " + mPreviousScrollState); - } - // Fix the position after a scroll or a fling ends - if (mNewState == OnScrollListener.SCROLL_STATE_IDLE - && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE - && mPreviousScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { - mPreviousScrollState = mNewState; - int i = 0; - View child = getChildAt(i); - while (child != null && child.getBottom() <= 0) { - child = getChildAt(++i); - } - if (child == null) { - // The view is no longer visible, just return - return; - } - int firstPosition = getFirstVisiblePosition(); - int lastPosition = getLastVisiblePosition(); - boolean scroll = firstPosition != 0 && lastPosition != getCount() - 1; - final int top = child.getTop(); - final int bottom = child.getBottom(); - final int midpoint = getHeight() / 2; - if (scroll && top < LIST_TOP_OFFSET) { - if (bottom > midpoint) { - smoothScrollBy(top, GOTO_SCROLL_DURATION); - } else { - smoothScrollBy(bottom, GOTO_SCROLL_DURATION); - } - } - } else { - mPreviousScrollState = mNewState; - } - } } /** - * Gets the position of the view that is most prominently displayed within the list view. + * Gets the position of the view that is most prominently displayed within the list. */ public int getMostVisiblePosition() { - final int firstPosition = getFirstVisiblePosition(); - final int height = getHeight(); - - int maxDisplayedHeight = 0; - int mostVisibleIndex = 0; - int i=0; - int bottom = 0; - while (bottom < height) { + return getChildAdapterPosition(getMostVisibleMonth()); + } + + public MonthView getMostVisibleMonth() { + boolean verticalScroll = ((LinearLayoutManager) getLayoutManager()).getOrientation() == LinearLayoutManager.VERTICAL; + final int maxSize = verticalScroll ? getHeight() : getWidth(); + int maxDisplayedSize = 0; + int i = 0; + int size = 0; + MonthView mostVisibleMonth = null; + + while (size < maxSize) { View child = getChildAt(i); if (child == null) { break; } - bottom = child.getBottom(); - int displayedHeight = Math.min(bottom, height) - Math.max(0, child.getTop()); - if (displayedHeight > maxDisplayedHeight) { - mostVisibleIndex = i; - maxDisplayedHeight = displayedHeight; + size = verticalScroll ? child.getBottom() : getRight(); + int displayedSize = Math.min(size, maxSize) - Math.max(0, child.getTop()); + if (displayedSize > maxDisplayedSize) { + mostVisibleMonth = (MonthView) child; + maxDisplayedSize = displayedSize; } i++; } - return firstPosition + mostVisibleIndex; + return mostVisibleMonth; } @Override @@ -377,7 +289,7 @@ public void onDateChanged() { * Attempts to return the date that has accessibility focus. * * @return The date that has accessibility focus, or {@code null} if no date - * has focus. + * has focus. */ private MonthAdapter.CalendarDay findAccessibilityFocus() { final int childCount = getChildCount(); @@ -423,29 +335,18 @@ private boolean restoreAccessibilityFocus(MonthAdapter.CalendarDay day) { return false; } - @Override - protected void layoutChildren() { - final MonthAdapter.CalendarDay focusedDay = findAccessibilityFocus(); - super.layoutChildren(); - if (mPerformingScroll) { - mPerformingScroll = false; - } else { - restoreAccessibilityFocus(focusedDay); - } - } - @Override public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); event.setItemCount(-1); - } + } - private static String getMonthAndYearString(MonthAdapter.CalendarDay day) { + private static String getMonthAndYearString(MonthAdapter.CalendarDay day, Locale locale) { Calendar cal = Calendar.getInstance(); cal.set(day.year, day.month, day.day); String sbuf = ""; - sbuf += cal.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault()); + sbuf += cal.getDisplayName(Calendar.MONTH, Calendar.LONG, locale); sbuf += " "; sbuf += YEAR_FORMAT.format(cal.getTime()); return sbuf; @@ -459,11 +360,10 @@ private static String getMonthAndYearString(MonthAdapter.CalendarDay day) { @SuppressWarnings("deprecation") public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); - if(Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= 21) { info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); - } - else { + } else { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); } @@ -479,13 +379,12 @@ public boolean performAccessibilityAction(int action, Bundle arguments) { action != AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { return super.performAccessibilityAction(action, arguments); } - // Figure out what month is showing. int firstVisiblePosition = getFirstVisiblePosition(); int minMonth = mController.getStartDate().get(Calendar.MONTH); int month = (firstVisiblePosition + minMonth) % MonthAdapter.MONTHS_IN_YEAR; int year = (firstVisiblePosition + minMonth) / MonthAdapter.MONTHS_IN_YEAR + mController.getMinYear(); - MonthAdapter.CalendarDay day = new MonthAdapter.CalendarDay(year, month, 1); + MonthAdapter.CalendarDay day = new MonthAdapter.CalendarDay(year, month, 1, mController.getTimeZone()); // Scroll either forward or backward one month. if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { @@ -510,9 +409,13 @@ public boolean performAccessibilityAction(int action, Bundle arguments) { } // Go to that month. - Utils.tryAccessibilityAnnounce(this, getMonthAndYearString(day)); + Utils.tryAccessibilityAnnounce(this, getMonthAndYearString(day, mController.getLocale())); goTo(day, true, false, true); - mPerformingScroll = true; return true; } + + private int getFirstVisiblePosition() { + return getChildAdapterPosition(getChildAt(0)); + } + } diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DefaultDateRangeLimiter.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DefaultDateRangeLimiter.java new file mode 100644 index 00000000..c6769d5f --- /dev/null +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/DefaultDateRangeLimiter.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2017 Wouter Dullaert + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.wdullaer.materialdatetimepicker.date; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.wdullaer.materialdatetimepicker.Utils; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashSet; +import java.util.TimeZone; +import java.util.TreeSet; + +class DefaultDateRangeLimiter implements DateRangeLimiter { + private static final int DEFAULT_START_YEAR = 1900; + private static final int DEFAULT_END_YEAR = 2100; + + private transient DatePickerController mController; + private int mMinYear = DEFAULT_START_YEAR; + private int mMaxYear = DEFAULT_END_YEAR; + private Calendar mMinDate; + private Calendar mMaxDate; + private TreeSet selectableDays = new TreeSet<>(); + private HashSet disabledDays = new HashSet<>(); + + DefaultDateRangeLimiter() {} + + @SuppressWarnings({"unchecked", "WeakerAccess"}) + public DefaultDateRangeLimiter(Parcel in) { + mMinYear = in.readInt(); + mMaxYear = in.readInt(); + mMinDate = (Calendar) in.readSerializable(); + mMaxDate = (Calendar) in.readSerializable(); + selectableDays = (TreeSet) in.readSerializable(); + disabledDays = (HashSet) in.readSerializable(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mMinYear); + out.writeInt(mMaxYear); + out.writeSerializable(mMinDate); + out.writeSerializable(mMaxDate); + out.writeSerializable(selectableDays); + out.writeSerializable(disabledDays); + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("WeakerAccess") + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public DefaultDateRangeLimiter createFromParcel(Parcel in) { + return new DefaultDateRangeLimiter(in); + } + + public DefaultDateRangeLimiter[] newArray(int size) { + return new DefaultDateRangeLimiter[size]; + } + }; + + void setSelectableDays(@NonNull Calendar[] days) { + for (Calendar selectableDay : days) { + this.selectableDays.add(Utils.trimToMidnight((Calendar) selectableDay.clone())); + } + } + + void setDisabledDays(@NonNull Calendar[] days) { + for (Calendar disabledDay : days) { + this.disabledDays.add(Utils.trimToMidnight((Calendar) disabledDay.clone())); + } + } + + void setMinDate(@NonNull Calendar calendar) { + mMinDate = Utils.trimToMidnight((Calendar) calendar.clone()); + } + + void setMaxDate(@NonNull Calendar calendar) { + mMaxDate = Utils.trimToMidnight((Calendar) calendar.clone()); + } + + void setController(@NonNull DatePickerController controller) { + mController = controller; + } + + void setYearRange(int startYear, int endYear) { + if (endYear < startYear) { + throw new IllegalArgumentException("Year end must be larger than or equal to year start"); + } + + mMinYear = startYear; + mMaxYear = endYear; + } + + @Nullable Calendar getMinDate() { + return mMinDate; + } + + @Nullable Calendar getMaxDate() { + return mMaxDate; + } + + @Nullable Calendar[] getSelectableDays() { + return selectableDays.isEmpty() ? null : selectableDays.toArray(new Calendar[0]); + } + + @Nullable Calendar[] getDisabledDays() { + return disabledDays.isEmpty() ? null : disabledDays.toArray(new Calendar[0]); + } + + @Override + public int getMinYear() { + if (!selectableDays.isEmpty()) return selectableDays.first().get(Calendar.YEAR); + // Ensure no years can be selected outside of the given minimum date + return mMinDate != null && mMinDate.get(Calendar.YEAR) > mMinYear ? mMinDate.get(Calendar.YEAR) : mMinYear; + } + + @Override + public int getMaxYear() { + if (!selectableDays.isEmpty()) return selectableDays.last().get(Calendar.YEAR); + // Ensure no years can be selected outside of the given maximum date + return mMaxDate != null && mMaxDate.get(Calendar.YEAR) < mMaxYear ? mMaxDate.get(Calendar.YEAR) : mMaxYear; + } + + @Override + public @NonNull Calendar getStartDate() { + if (!selectableDays.isEmpty()) return (Calendar) selectableDays.first().clone(); + if (mMinDate != null) return (Calendar) mMinDate.clone(); + TimeZone timeZone = mController == null ? TimeZone.getDefault() : mController.getTimeZone(); + Calendar output = Calendar.getInstance(timeZone); + output.set(Calendar.YEAR, mMinYear); + output.set(Calendar.DAY_OF_MONTH, 1); + output.set(Calendar.MONTH, Calendar.JANUARY); + return output; + } + + @Override + public @NonNull Calendar getEndDate() { + if (!selectableDays.isEmpty()) return (Calendar) selectableDays.last().clone(); + if (mMaxDate != null) return (Calendar) mMaxDate.clone(); + TimeZone timeZone = mController == null ? TimeZone.getDefault() : mController.getTimeZone(); + Calendar output = Calendar.getInstance(timeZone); + output.set(Calendar.YEAR, mMaxYear); + output.set(Calendar.DAY_OF_MONTH, 31); + output.set(Calendar.MONTH, Calendar.DECEMBER); + return output; + } + + /** + * @return true if the specified year/month/day are within the selectable days or the range set by minDate and maxDate. + * If one or either have not been set, they are considered as Integer.MIN_VALUE and + * Integer.MAX_VALUE. + */ + @Override + public boolean isOutOfRange(int year, int month, int day) { + TimeZone timezone = mController == null ? TimeZone.getDefault() : mController.getTimeZone(); + Calendar date = Calendar.getInstance(timezone); + date.set(Calendar.YEAR, year); + date.set(Calendar.MONTH, month); + date.set(Calendar.DAY_OF_MONTH, day); + return isOutOfRange(date); + } + + private boolean isOutOfRange(@NonNull Calendar calendar) { + Utils.trimToMidnight(calendar); + return isDisabled(calendar) || !isSelectable(calendar); + } + + private boolean isDisabled(@NonNull Calendar c) { + return disabledDays.contains(Utils.trimToMidnight(c)) || isBeforeMin(c) || isAfterMax(c); + } + + private boolean isSelectable(@NonNull Calendar c) { + return selectableDays.isEmpty() || selectableDays.contains(Utils.trimToMidnight(c)); + } + + private boolean isBeforeMin(@NonNull Calendar calendar) { + return mMinDate != null && calendar.before(mMinDate) || calendar.get(Calendar.YEAR) < mMinYear; + } + + private boolean isAfterMax(@NonNull Calendar calendar) { + return mMaxDate != null && calendar.after(mMaxDate) || calendar.get(Calendar.YEAR) > mMaxYear; + } + + @Override + public @NonNull Calendar setToNearestDate(@NonNull Calendar calendar) { + if (!selectableDays.isEmpty()) { + Calendar newCalendar = null; + Calendar higher = selectableDays.ceiling(calendar); + Calendar lower = selectableDays.lower(calendar); + + if (higher == null && lower != null) newCalendar = lower; + else if (lower == null && higher != null) newCalendar = higher; + + if (newCalendar != null || higher == null) { + newCalendar = newCalendar == null ? calendar : newCalendar; + TimeZone timeZone = mController == null ? TimeZone.getDefault() : mController.getTimeZone(); + newCalendar.setTimeZone(timeZone); + return (Calendar) newCalendar.clone(); + } + + long highDistance = Math.abs(higher.getTimeInMillis() - calendar.getTimeInMillis()); + long lowDistance = Math.abs(calendar.getTimeInMillis() - lower.getTimeInMillis()); + + if (lowDistance < highDistance) return (Calendar) lower.clone(); + else return (Calendar) higher.clone(); + } + + if (!disabledDays.isEmpty()) { + Calendar forwardDate = isBeforeMin(calendar) ? getStartDate() : (Calendar) calendar.clone(); + Calendar backwardDate = isAfterMax(calendar) ? getEndDate() : (Calendar) calendar.clone(); + while (isDisabled(forwardDate) && isDisabled(backwardDate)) { + forwardDate.add(Calendar.DAY_OF_MONTH, 1); + backwardDate.add(Calendar.DAY_OF_MONTH, -1); + } + if (!isDisabled(backwardDate)) { + return backwardDate; + } + if (!isDisabled(forwardDate)) { + return forwardDate; + } + } + + TimeZone timezone = mController == null ? TimeZone.getDefault() : mController.getTimeZone(); + if (isBeforeMin(calendar)) { + if (mMinDate != null) return (Calendar) mMinDate.clone(); + Calendar output = Calendar.getInstance(timezone); + output.set(Calendar.YEAR, mMinYear); + output.set(Calendar.MONTH, Calendar.JANUARY); + output.set(Calendar.DAY_OF_MONTH, 1); + return Utils.trimToMidnight(output); + } + + if (isAfterMax(calendar)) { + if (mMaxDate != null) return (Calendar) mMaxDate.clone(); + Calendar output = Calendar.getInstance(timezone); + output.set(Calendar.YEAR, mMaxYear); + output.set(Calendar.MONTH, Calendar.DECEMBER); + output.set(Calendar.DAY_OF_MONTH, 31); + return Utils.trimToMidnight(output); + } + + return calendar; + } +} \ No newline at end of file diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/MonthAdapter.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/MonthAdapter.java index 103c9f5b..a42957c2 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/MonthAdapter.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/MonthAdapter.java @@ -16,27 +16,23 @@ package com.wdullaer.materialdatetimepicker.date; -import android.annotation.SuppressLint; import android.content.Context; -import android.view.View; +import android.support.v7.widget.RecyclerView; import android.view.ViewGroup; import android.widget.AbsListView.LayoutParams; -import android.widget.BaseAdapter; +import com.wdullaer.materialdatetimepicker.date.MonthAdapter.MonthViewHolder; import com.wdullaer.materialdatetimepicker.date.MonthView.OnDayClickListener; import java.util.Calendar; -import java.util.HashMap; import java.util.TimeZone; /** * An adapter for a list of {@link MonthView} items. */ -public abstract class MonthAdapter extends BaseAdapter implements OnDayClickListener { +@SuppressWarnings("WeakerAccess") +public abstract class MonthAdapter extends RecyclerView.Adapter implements OnDayClickListener { - private static final String TAG = "SimpleMonthAdapter"; - - private final Context mContext; protected final DatePickerController mController; private CalendarDay mSelectedDay; @@ -71,10 +67,16 @@ public CalendarDay(Calendar calendar, TimeZone timeZone) { day = calendar.get(Calendar.DAY_OF_MONTH); } + @SuppressWarnings("unused") public CalendarDay(int year, int month, int day) { setDay(year, month, day); } + public CalendarDay(int year, int month, int day, TimeZone timezone) { + mTimeZone = timezone; + setDay(year, month, day); + } + public void set(CalendarDay date) { year = date.year; month = date.month; @@ -110,12 +112,11 @@ public int getDay() { } } - public MonthAdapter(Context context, - DatePickerController controller) { - mContext = context; + public MonthAdapter(DatePickerController controller) { mController = controller; init(); setSelectedDay(mController.getSelectedDay()); + setHasStableIds(true); } /** @@ -140,19 +141,20 @@ protected void init() { mSelectedDay = new CalendarDay(System.currentTimeMillis(), mController.getTimeZone()); } - @Override - public int getCount() { - Calendar endDate = mController.getEndDate(); - Calendar startDate = mController.getStartDate(); - int endMonth = endDate.get(Calendar.YEAR) * MONTHS_IN_YEAR + endDate.get(Calendar.MONTH); - int startMonth = startDate.get(Calendar.YEAR) * MONTHS_IN_YEAR + startDate.get(Calendar.MONTH); - return endMonth - startMonth + 1; - //return ((mController.getMaxYear() - mController.getMinYear()) + 1) * MONTHS_IN_YEAR; + @Override public MonthViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + + MonthView v = createMonthView(parent.getContext()); + // Set up the new view + LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + v.setLayoutParams(params); + v.setClickable(true); + v.setOnDayClickListener(this); + + return new MonthViewHolder(v); } - @Override - public Object getItem(int position) { - return null; + @Override public void onBindViewHolder(MonthViewHolder holder, int position) { + holder.bind(position, mController, mSelectedDay); } @Override @@ -160,63 +162,17 @@ public long getItemId(int position) { return position; } - @Override - public boolean hasStableIds() { - return true; - } - - @SuppressLint("NewApi") - @SuppressWarnings("unchecked") - @Override - public View getView(int position, View convertView, ViewGroup parent) { - MonthView v; - HashMap drawingParams = null; - if (convertView != null) { - v = (MonthView) convertView; - // We store the drawing parameters in the view so it can be recycled - drawingParams = (HashMap) v.getTag(); - } else { - v = createMonthView(mContext); - // Set up the new view - LayoutParams params = new LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - v.setLayoutParams(params); - v.setClickable(true); - v.setOnDayClickListener(this); - } - if (drawingParams == null) { - drawingParams = new HashMap<>(); - } - drawingParams.clear(); - - final int month = (position + mController.getStartDate().get(Calendar.MONTH)) % MONTHS_IN_YEAR; - final int year = (position + mController.getStartDate().get(Calendar.MONTH)) / MONTHS_IN_YEAR + mController.getMinYear(); - - int selectedDay = -1; - if (isSelectedDayInMonth(year, month)) { - selectedDay = mSelectedDay.day; - } - - // Invokes requestLayout() to ensure that the recycled view is set with the appropriate - // height/number of weeks before being displayed. - v.reuse(); - - drawingParams.put(MonthView.VIEW_PARAMS_SELECTED_DAY, selectedDay); - drawingParams.put(MonthView.VIEW_PARAMS_YEAR, year); - drawingParams.put(MonthView.VIEW_PARAMS_MONTH, month); - drawingParams.put(MonthView.VIEW_PARAMS_WEEK_START, mController.getFirstDayOfWeek()); - v.setMonthParams(drawingParams); - v.invalidate(); - return v; + @Override public int getItemCount() { + Calendar endDate = mController.getEndDate(); + Calendar startDate = mController.getStartDate(); + int endMonth = endDate.get(Calendar.YEAR) * MONTHS_IN_YEAR + endDate.get(Calendar.MONTH); + int startMonth = startDate.get(Calendar.YEAR) * MONTHS_IN_YEAR + startDate.get(Calendar.MONTH); + return endMonth - startMonth + 1; + //return ((mController.getMaxYear() - mController.getMinYear()) + 1) * MONTHS_IN_YEAR; } public abstract MonthView createMonthView(Context context); - private boolean isSelectedDayInMonth(int year, int month) { - return mSelectedDay.year == year && mSelectedDay.month == month; - } - - @Override public void onDayClick(MonthView view, CalendarDay day) { if (day != null) { @@ -234,4 +190,29 @@ protected void onDayTapped(CalendarDay day) { mController.onDayOfMonthSelected(day.year, day.month, day.day); setSelectedDay(day); } + + static class MonthViewHolder extends RecyclerView.ViewHolder { + + public MonthViewHolder(MonthView itemView) { + super(itemView); + + } + + void bind(int position, DatePickerController mController, CalendarDay selectedCalendarDay) { + final int month = (position + mController.getStartDate().get(Calendar.MONTH)) % MONTHS_IN_YEAR; + final int year = (position + mController.getStartDate().get(Calendar.MONTH)) / MONTHS_IN_YEAR + mController.getMinYear(); + + int selectedDay = -1; + if (isSelectedDayInMonth(selectedCalendarDay, year, month)) { + selectedDay = selectedCalendarDay.day; + } + + ((MonthView) itemView).setMonthParams(selectedDay, year, month, mController.getFirstDayOfWeek()); + this.itemView.invalidate(); + } + + private boolean isSelectedDayInMonth(CalendarDay selectedDay, int year, int month) { + return selectedDay.year == year && selectedDay.month == month; + } + } } diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/MonthView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/MonthView.java index 1d7b63f2..07de2063 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/MonthView.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/MonthView.java @@ -39,15 +39,11 @@ import android.view.accessibility.AccessibilityNodeInfo; import com.wdullaer.materialdatetimepicker.R; -import com.wdullaer.materialdatetimepicker.TypefaceHelper; import com.wdullaer.materialdatetimepicker.date.MonthAdapter.CalendarDay; -import com.wdullaer.materialdatetimepicker.supportdate.SupportMonthView; import java.security.InvalidParameterException; import java.text.SimpleDateFormat; import java.util.Calendar; -import java.util.Formatter; -import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -56,49 +52,6 @@ * within the specified month. */ public abstract class MonthView extends View { - private static final String TAG = "MonthView"; - - /** - * These params can be passed into the view to control how it appears. - * {@link #VIEW_PARAMS_WEEK} is the only required field, though the default - * values are unlikely to fit most layouts correctly. - */ - /** - * This sets the height of this week in pixels - */ - public static final String VIEW_PARAMS_HEIGHT = "height"; - /** - * This specifies the position (or weeks since the epoch) of this week. - */ - public static final String VIEW_PARAMS_MONTH = "month"; - /** - * This specifies the position (or weeks since the epoch) of this week. - */ - public static final String VIEW_PARAMS_YEAR = "year"; - /** - * This sets one of the days in this view as selected {@link Calendar#SUNDAY} - * through {@link Calendar#SATURDAY}. - */ - public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day"; - /** - * Which day the week should start on. {@link Calendar#SUNDAY} through - * {@link Calendar#SATURDAY}. - */ - public static final String VIEW_PARAMS_WEEK_START = "week_start"; - /** - * How many days to display at a time. Days will be displayed starting with - * {@link #mWeekStart}. - */ - public static final String VIEW_PARAMS_NUM_DAYS = "num_days"; - /** - * Which month is currently in focus, as defined by {@link Calendar#MONTH} - * [0-11]. - */ - public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month"; - /** - * If this month should display week numbers. false if 0, true otherwise. - */ - public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num"; protected static int DEFAULT_HEIGHT = 32; protected static int MIN_HEIGHT = 10; @@ -118,6 +71,8 @@ public abstract class MonthView extends View { protected static int MONTH_DAY_LABEL_TEXT_SIZE; protected static int MONTH_HEADER_SIZE; protected static int DAY_SELECTED_CIRCLE_SIZE; + protected static int DAY_HIGHLIGHT_CIRCLE_SIZE; + protected static int DAY_HIGHLIGHT_CIRCLE_MARGIN; // used for scaling to the device density protected static float mScale = 0; @@ -135,7 +90,6 @@ public abstract class MonthView extends View { protected Paint mSelectedCirclePaint; protected Paint mMonthDayLabelPaint; - private final Formatter mFormatter; private final StringBuilder mStringBuilder; // The Julian day of the first day displayed by this item @@ -189,6 +143,8 @@ public abstract class MonthView extends View { protected int mDisabledDayTextColor; protected int mMonthTitleColor; + private SimpleDateFormat weekDayLabelFormatter; + public MonthView(Context context) { this(context, null, null); } @@ -198,20 +154,19 @@ public MonthView(Context context, AttributeSet attr, DatePickerController contro mController = controller; Resources res = context.getResources(); - mDayLabelCalendar = Calendar.getInstance(mController.getTimeZone()); - mCalendar = Calendar.getInstance(mController.getTimeZone()); + mDayLabelCalendar = Calendar.getInstance(mController.getTimeZone(), mController.getLocale()); + mCalendar = Calendar.getInstance(mController.getTimeZone(), mController.getLocale()); mDayOfWeekTypeface = res.getString(R.string.mdtp_day_of_week_label_typeface); mMonthTitleTypeface = res.getString(R.string.mdtp_sans_serif); boolean darkTheme = mController != null && mController.isThemeDark(); - if(darkTheme) { + if (darkTheme) { mDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_normal_dark_theme); mMonthDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_month_day_dark_theme); mDisabledDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_disabled_dark_theme); mHighlightedDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_highlighted_dark_theme); - } - else { + } else { mDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_normal); mMonthDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_month_day); mDisabledDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_disabled); @@ -222,7 +177,6 @@ public MonthView(Context context, AttributeSet attr, DatePickerController contro mMonthTitleColor = ContextCompat.getColor(context, R.color.mdtp_white); mStringBuilder = new StringBuilder(50); - mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_day_number_size); MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_month_label_size); @@ -230,9 +184,18 @@ public MonthView(Context context, AttributeSet attr, DatePickerController contro MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.mdtp_month_list_item_header_height); DAY_SELECTED_CIRCLE_SIZE = res .getDimensionPixelSize(R.dimen.mdtp_day_number_select_circle_radius); - - mRowHeight = (res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height) - - getMonthHeaderSize()) / MAX_NUM_ROWS; + DAY_HIGHLIGHT_CIRCLE_SIZE = res + .getDimensionPixelSize(R.dimen.mdtp_day_highlight_circle_radius); + DAY_HIGHLIGHT_CIRCLE_MARGIN = res + .getDimensionPixelSize(R.dimen.mdtp_day_highlight_circle_margin); + + if (mController.getVersion() == DatePickerDialog.Version.VERSION_1) { + mRowHeight = (res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height) + - getMonthHeaderSize()) / MAX_NUM_ROWS; + } else { + mRowHeight = (res.getDimensionPixelSize(R.dimen.mdtp_date_picker_view_animator_height_v2) + - getMonthHeaderSize()) / MAX_NUM_ROWS; + } // Set up accessibility components. mTouchHelper = getMonthViewTouchHelper(); @@ -244,10 +207,6 @@ public MonthView(Context context, AttributeSet attr, DatePickerController contro initView(); } - public void setDatePickerController(DatePickerController controller) { - mController = controller; - } - protected MonthViewTouchHelper getMonthViewTouchHelper() { return new MonthViewTouchHelper(this); } @@ -268,10 +227,7 @@ public void setOnDayClickListener(OnDayClickListener listener) { @Override public boolean dispatchHoverEvent(@NonNull MotionEvent event) { // First right-of-refusal goes the touch exploration helper. - if (mTouchHelper.dispatchHoverEvent(event)) { - return true; - } - return super.dispatchHoverEvent(event); + return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event); } @Override @@ -313,7 +269,7 @@ protected void initView() { mMonthDayLabelPaint.setAntiAlias(true); mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE); mMonthDayLabelPaint.setColor(mMonthDayTextColor); - mMonthDayLabelPaint.setTypeface(TypefaceHelper.get(getContext(),"Roboto-Medium")); + mMonthTitlePaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.BOLD)); mMonthDayLabelPaint.setStyle(Style.FILL); mMonthDayLabelPaint.setTextAlign(Align.CENTER); mMonthDayLabelPaint.setFakeBoldText(true); @@ -339,36 +295,23 @@ protected void onDraw(Canvas canvas) { * Sets all the parameters for displaying this week. The only required * parameter is the week number. Other parameters have a default value and * will only update if a new value is included, except for focus month, - * which will always default to no focus month if no value is passed in. See - * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters. - * - * @param params A map of the new parameters, see - * {@link #VIEW_PARAMS_HEIGHT} + * which will always default to no focus month if no value is passed in. */ - public void setMonthParams(HashMap params) { - if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) { + public void setMonthParams(int selectedDay, int year, int month, int weekStart) { + if (month == -1 && year == -1) { throw new InvalidParameterException("You must specify month and year for this view"); } - setTag(params); - // We keep the current value for any params not present - if (params.containsKey(VIEW_PARAMS_HEIGHT)) { - mRowHeight = params.get(VIEW_PARAMS_HEIGHT); - if (mRowHeight < MIN_HEIGHT) { - mRowHeight = MIN_HEIGHT; - } - } - if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) { - mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY); - } + + mSelectedDay = selectedDay; // Allocate space for caching the day numbers and focus values - mMonth = params.get(VIEW_PARAMS_MONTH); - mYear = params.get(VIEW_PARAMS_YEAR); + mMonth = month; + mYear = year; // Figure out what day today is //final Time today = new Time(Time.getCurrentTimezone()); //today.setToNow(); - final Calendar today = Calendar.getInstance(mController.getTimeZone()); + final Calendar today = Calendar.getInstance(mController.getTimeZone(), mController.getLocale()); mHasToday = false; mToday = -1; @@ -377,8 +320,8 @@ public void setMonthParams(HashMap params) { mCalendar.set(Calendar.DAY_OF_MONTH, 1); mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK); - if (params.containsKey(VIEW_PARAMS_WEEK_START)) { - mWeekStart = params.get(VIEW_PARAMS_WEEK_START); + if (weekStart != -1) { + mWeekStart = weekStart; } else { mWeekStart = mCalendar.getFirstDayOfWeek(); } @@ -401,11 +344,6 @@ public void setSelectedDay(int day) { mSelectedDay = day; } - public void reuse() { - mNumRows = DEFAULT_NUM_ROWS; - requestLayout(); - } - private int calculateNumRows() { int offset = findDayOffset(); int dividend = (offset + mNumCells) / mNumDays; @@ -421,8 +359,7 @@ private boolean sameDay(int day, Calendar today) { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows - + getMonthHeaderSize() + 5); + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows + getMonthHeaderSize() + 5); } @Override @@ -450,10 +387,10 @@ protected int getMonthHeaderSize() { @NonNull private String getMonthAndYearString() { - Locale locale = Locale.getDefault(); + Locale locale = mController.getLocale(); String pattern = "MMMM yyyy"; - if(Build.VERSION.SDK_INT < 18) pattern = getContext().getResources().getString(R.string.mdtp_date_v1_monthyear); + if (Build.VERSION.SDK_INT < 18) pattern = getContext().getResources().getString(R.string.mdtp_date_v1_monthyear); else pattern = DateFormat.getBestDateTimePattern(locale, pattern); SimpleDateFormat formatter = new SimpleDateFormat(pattern, locale); @@ -495,14 +432,14 @@ protected void drawMonthNums(Canvas canvas) { final float dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2.0f); int j = findDayOffset(); for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) { - final int x = (int)((2 * j + 1) * dayWidthHalf + mEdgePadding); + final int x = (int) ((2 * j + 1) * dayWidthHalf + mEdgePadding); int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH; - final int startX = (int)(x - dayWidthHalf); - final int stopX = (int)(x + dayWidthHalf); - final int startY = (int)(y - yRelativeToDay); - final int stopY = (int)(startY + mRowHeight); + final int startX = (int) (x - dayWidthHalf); + final int stopX = (int) (x + dayWidthHalf); + final int startY = y - yRelativeToDay; + final int stopY = startY + mRowHeight; drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY); @@ -517,19 +454,19 @@ protected void drawMonthNums(Canvas canvas) { /** * This method should draw the month day. Implemented by sub-classes to allow customization. * - * @param canvas The canvas to draw on - * @param year The year of this month day + * @param canvas The canvas to draw on + * @param year The year of this month day * @param month The month of this month day - * @param day The day number of this month day - * @param x The default x position to draw the day number - * @param y The default y position to draw the day number - * @param startX The left boundary of the day number rect + * @param day The day number of this month day + * @param x The default x position to draw the day number + * @param y The default y position to draw the day number + * @param startX The left boundary of the day number rect * @param stopX The right boundary of the day number rect - * @param startY The top boundary of the day number rect + * @param startY The top boundary of the day number rect * @param stopY The bottom boundary of the day number rect */ public abstract void drawMonthDay(Canvas canvas, int year, int month, int day, - int x, int y, int startX, int stopX, int startY, int stopY); + int x, int y, int startX, int stopX, int startY, int stopY); protected int findDayOffset() { return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart) @@ -589,7 +526,7 @@ private void onDayClick(int day) { if (mOnDayClickListener != null) { - mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day)); + mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day, mController.getTimeZone())); } // This is a no-op if accessibility is turned off. @@ -597,9 +534,9 @@ private void onDayClick(int day) { } /** - * @param year - * @param month - * @param day + * @param year as an int + * @param month as an int + * @param day as an int * @return true if the given date should be highlighted */ protected boolean isHighlighted(int year, int month, int day) { @@ -608,30 +545,30 @@ protected boolean isHighlighted(int year, int month, int day) { /** * Return a 1 or 2 letter String for use as a weekday label + * * @param day The day for which to generate a label * @return The weekday label */ private String getWeekDayLabel(Calendar day) { - Locale locale = Locale.getDefault(); + Locale locale = mController.getLocale(); // Localised short version of the string is not available on API < 18 - if(Build.VERSION.SDK_INT < 18) { + if (Build.VERSION.SDK_INT < 18) { String dayName = new SimpleDateFormat("E", locale).format(day.getTime()); String dayLabel = dayName.toUpperCase(locale).substring(0, 1); // Chinese labels should be fetched right to left if (locale.equals(Locale.CHINA) || locale.equals(Locale.CHINESE) || locale.equals(Locale.SIMPLIFIED_CHINESE) || locale.equals(Locale.TRADITIONAL_CHINESE)) { int len = dayName.length(); - dayLabel = dayName.substring(len -1, len); + dayLabel = dayName.substring(len - 1, len); } // Most hebrew labels should select the second to last character if (locale.getLanguage().equals("he") || locale.getLanguage().equals("iw")) { - if(mDayLabelCalendar.get(Calendar.DAY_OF_WEEK) != Calendar.SATURDAY) { + if (mDayLabelCalendar.get(Calendar.DAY_OF_WEEK) != Calendar.SATURDAY) { int len = dayName.length(); dayLabel = dayName.substring(len - 2, len - 1); - } - else { + } else { // I know this is duplication, but it makes the code easier to grok by // having all hebrew code in the same block dayLabel = dayName.toUpperCase(locale).substring(0, 1); @@ -640,7 +577,7 @@ private String getWeekDayLabel(Calendar day) { // Catalan labels should be two digits in lowercase if (locale.getLanguage().equals("ca")) - dayLabel = dayName.toLowerCase().substring(0,2); + dayLabel = dayName.toLowerCase().substring(0, 2); // Correct single character label in Spanish is X if (locale.getLanguage().equals("es") && day.get(Calendar.DAY_OF_WEEK) == Calendar.WEDNESDAY) @@ -649,17 +586,20 @@ private String getWeekDayLabel(Calendar day) { return dayLabel; } // Getting the short label is a one liner on API >= 18 - return new SimpleDateFormat("EEEEE", locale).format(day.getTime()); + if (weekDayLabelFormatter == null) { + weekDayLabelFormatter = new SimpleDateFormat("EEEEE", locale); + } + return weekDayLabelFormatter.format(day.getTime()); } /** * @return The date that has accessibility focus, or {@code null} if no date - * has focus + * has focus */ public CalendarDay getAccessibilityFocus() { - final int day = mTouchHelper.getFocusedVirtualView(); + final int day = mTouchHelper.getAccessibilityFocusedVirtualViewId(); if (day >= 0) { - return new CalendarDay(mYear, mMonth, day); + return new CalendarDay(mYear, mMonth, day, mController.getTimeZone()); } return null; } @@ -677,7 +617,7 @@ public void clearAccessibilityFocus() { * * @param day The date which should receive focus * @return {@code false} if the date is not valid for this month view, or - * {@code true} if the date received focus + * {@code true} if the date received focus */ public boolean restoreAccessibilityFocus(CalendarDay day) { if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) { @@ -697,17 +637,17 @@ protected class MonthViewTouchHelper extends ExploreByTouchHelper { private final Rect mTempRect = new Rect(); private final Calendar mTempCalendar = Calendar.getInstance(mController.getTimeZone()); - public MonthViewTouchHelper(View host) { + MonthViewTouchHelper(View host) { super(host); } - public void setFocusedVirtualView(int virtualViewId) { + void setFocusedVirtualView(int virtualViewId) { getAccessibilityNodeProvider(MonthView.this).performAction( virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); } - public void clearFocusedVirtualView() { - final int focusedVirtualView = getFocusedVirtualView(); + void clearFocusedVirtualView() { + final int focusedVirtualView = getAccessibilityFocusedVirtualViewId(); if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) { getAccessibilityNodeProvider(MonthView.this).performAction( focusedVirtualView, @@ -733,13 +673,13 @@ protected void getVisibleVirtualViews(List virtualViewIds) { } @Override - protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { + protected void onPopulateEventForVirtualView(int virtualViewId, @NonNull AccessibilityEvent event) { event.setContentDescription(getItemDescription(virtualViewId)); } @Override protected void onPopulateNodeForVirtualView(int virtualViewId, - AccessibilityNodeInfoCompat node) { + @NonNull AccessibilityNodeInfoCompat node) { getItemBounds(virtualViewId, mTempRect); node.setContentDescription(getItemDescription(virtualViewId)); @@ -754,7 +694,7 @@ protected void onPopulateNodeForVirtualView(int virtualViewId, @Override protected boolean onPerformActionForVirtualView(int virtualViewId, int action, - Bundle arguments) { + Bundle arguments) { switch (action) { case AccessibilityNodeInfo.ACTION_CLICK: onDayClick(virtualViewId); @@ -767,10 +707,10 @@ protected boolean onPerformActionForVirtualView(int virtualViewId, int action, /** * Calculates the bounding rectangle of a given time object. * - * @param day The day to calculate bounds for + * @param day The day to calculate bounds for * @param rect The rectangle in which to store the bounds */ - protected void getItemBounds(int day, Rect rect) { + void getItemBounds(int day, Rect rect) { final int offsetX = mEdgePadding; final int offsetY = getMonthHeaderSize(); final int cellHeight = mRowHeight; @@ -792,7 +732,7 @@ protected void getItemBounds(int day, Rect rect) { * @param day The day to generate a description for * @return A description of the time object */ - protected CharSequence getItemDescription(int day) { + CharSequence getItemDescription(int day) { mTempCalendar.set(mYear, mMonth, day); final CharSequence date = DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis()); diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/SimpleDayPickerView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/SimpleDayPickerView.java index adcb545c..da033e35 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/SimpleDayPickerView.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/SimpleDayPickerView.java @@ -33,8 +33,8 @@ public SimpleDayPickerView(Context context, DatePickerController controller) { } @Override - public MonthAdapter createMonthAdapter(Context context, DatePickerController controller) { - return new SimpleMonthAdapter(context, controller); + public MonthAdapter createMonthAdapter(DatePickerController controller) { + return new SimpleMonthAdapter(controller); } } diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/SimpleMonthAdapter.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/SimpleMonthAdapter.java index 98472064..aaa4f628 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/SimpleMonthAdapter.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/SimpleMonthAdapter.java @@ -23,13 +23,12 @@ */ public class SimpleMonthAdapter extends MonthAdapter { - public SimpleMonthAdapter(Context context, DatePickerController controller) { - super(context, controller); + public SimpleMonthAdapter(DatePickerController controller) { + super(controller); } @Override public MonthView createMonthView(Context context) { - final MonthView monthView = new SimpleMonthView(context, null, mController); - return monthView; + return new SimpleMonthView(context, null, mController); } } diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/SimpleMonthView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/SimpleMonthView.java index 3533a411..cb886a8e 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/SimpleMonthView.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/SimpleMonthView.java @@ -29,24 +29,24 @@ public SimpleMonthView(Context context, AttributeSet attr, DatePickerController @Override public void drawMonthDay(Canvas canvas, int year, int month, int day, - int x, int y, int startX, int stopX, int startY, int stopY) { + int x, int y, int startX, int stopX, int startY, int stopY) { if (mSelectedDay == day) { - canvas.drawCircle(x , y - (MINI_DAY_NUMBER_TEXT_SIZE / 3), DAY_SELECTED_CIRCLE_SIZE, + canvas.drawCircle(x, y - (MINI_DAY_NUMBER_TEXT_SIZE / 3), DAY_SELECTED_CIRCLE_SIZE, mSelectedCirclePaint); } - if(isHighlighted(year, month, day)) { + if (isHighlighted(year, month, day) && mSelectedDay != day) { + canvas.drawCircle(x, y + MINI_DAY_NUMBER_TEXT_SIZE - DAY_HIGHLIGHT_CIRCLE_MARGIN, + DAY_HIGHLIGHT_CIRCLE_SIZE, mSelectedCirclePaint); mMonthNumPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); - } - else { + } else { mMonthNumPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)); } - // If we have a mindate or maxdate, gray out the day number if it's outside the range. + // gray out the day number if it's outside the range. if (mController.isOutOfRange(year, month, day)) { mMonthNumPaint.setColor(mDisabledDayTextColor); - } - else if (mSelectedDay == day) { + } else if (mSelectedDay == day) { mMonthNumPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); mMonthNumPaint.setColor(mSelectedDayTextColor); } else if (mHasToday && mToday == day) { @@ -55,6 +55,6 @@ else if (mSelectedDay == day) { mMonthNumPaint.setColor(isHighlighted(year, month, day) ? mHighlightedDayTextColor : mDayTextColor); } - canvas.drawText(String.format("%d", day), x, y, mMonthNumPaint); + canvas.drawText(String.format(mController.getLocale(), "%d", day), x, y, mMonthNumPaint); } } diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/YearPickerView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/YearPickerView.java index 942d8c06..257475cf 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/date/YearPickerView.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/date/YearPickerView.java @@ -52,7 +52,9 @@ public YearPickerView(Context context, DatePickerController controller) { LayoutParams.WRAP_CONTENT); setLayoutParams(frame); Resources res = context.getResources(); - mViewSize = res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height); + mViewSize = mController.getVersion() == DatePickerDialog.Version.VERSION_1 + ? res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height) + : res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height_v2); mChildSize = res.getDimensionPixelOffset(R.dimen.mdtp_year_label_height); setVerticalFadingEdgeEnabled(true); setFadingEdgeLength(mChildSize / 3); @@ -130,7 +132,7 @@ public View getView(int position, View convertView, ViewGroup parent) { } int year = mMinYear + position; boolean selected = mController.getSelectedDay().year == year; - v.setText(String.valueOf(year)); + v.setText(String.format(mController.getLocale(),"%d", year)); v.drawIndicator(selected); v.requestLayout(); if (selected) { diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportDatePickerController.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportDatePickerController.java deleted file mode 100644 index 2832cfdc..00000000 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportDatePickerController.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.wdullaer.materialdatetimepicker.supportdate; - -import com.wdullaer.materialdatetimepicker.date.MonthAdapter; - -import java.util.Calendar; -import java.util.TimeZone; - -/** - * Created by rmore on 06/03/2017. - */ - -public interface SupportDatePickerController { - - void onYearSelected(int year); - - void onDayOfMonthSelected(int year, int month, int day); - - void registerOnDateChangedListener(SupportDatePickerDialog.OnDateChangedListener listener); - - void unregisterOnDateChangedListener(SupportDatePickerDialog.OnDateChangedListener listener); - - SupportMonthAdapter.CalendarDay getSelectedDay(); - - boolean isThemeDark(); - - int getAccentColor(); - - boolean isHighlighted(int year, int month, int day); - - int getFirstDayOfWeek(); - - int getMinYear(); - - int getMaxYear(); - - Calendar getStartDate(); - - Calendar getEndDate(); - - boolean isOutOfRange(int year, int month, int day); - - void tryVibrate(); - - TimeZone getTimeZone(); -} diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportDatePickerDialog.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportDatePickerDialog.java deleted file mode 100644 index 25ac33c8..00000000 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportDatePickerDialog.java +++ /dev/null @@ -1,1145 +0,0 @@ -package com.wdullaer.materialdatetimepicker.supportdate; - -import android.animation.ObjectAnimator; -import android.app.Activity; -import android.app.Dialog; -import android.content.DialogInterface; -import android.content.res.AssetManager; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Color; -import android.graphics.Typeface; -import android.os.Build; -import android.os.Bundle; -import android.support.annotation.ColorInt; -import android.support.annotation.NonNull; -import android.support.annotation.StringRes; -import android.support.v4.app.DialogFragment; -import android.support.v4.content.ContextCompat; -import android.text.format.DateFormat; -import android.text.format.DateUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.view.animation.AlphaAnimation; -import android.view.animation.Animation; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.wdullaer.materialdatetimepicker.HapticFeedbackController; -import com.wdullaer.materialdatetimepicker.R; -import com.wdullaer.materialdatetimepicker.TypefaceHelper; -import com.wdullaer.materialdatetimepicker.Utils; -import com.wdullaer.materialdatetimepicker.date.AccessibleDateAnimator; -import com.wdullaer.materialdatetimepicker.date.MonthAdapter; - -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Calendar; -import java.util.HashSet; -import java.util.Locale; -import java.util.TimeZone; -import java.util.TreeSet; - -/** - * Created by rmore on 06/03/2017. - */ - -/** - * Dialog allowing users to select a date. - */ -public class SupportDatePickerDialog extends DialogFragment implements - View.OnClickListener, SupportDatePickerController { - - public enum Version { - VERSION_1, - VERSION_2 - } - - private static final int UNINITIALIZED = -1; - private static final int MONTH_AND_DAY_VIEW = 0; - private static final int YEAR_VIEW = 1; - - private static final String KEY_SELECTED_YEAR = "year"; - private static final String KEY_SELECTED_MONTH = "month"; - private static final String KEY_SELECTED_DAY = "day"; - private static final String KEY_LIST_POSITION = "list_position"; - private static final String KEY_WEEK_START = "week_start"; - private static final String KEY_YEAR_START = "year_start"; - private static final String KEY_YEAR_END = "year_end"; - private static final String KEY_CURRENT_VIEW = "current_view"; - private static final String KEY_LIST_POSITION_OFFSET = "list_position_offset"; - private static final String KEY_MIN_DATE = "min_date"; - private static final String KEY_MAX_DATE = "max_date"; - private static final String KEY_HIGHLIGHTED_DAYS = "highlighted_days"; - private static final String KEY_SELECTABLE_DAYS = "selectable_days"; - private static final String KEY_DISABLED_DAYS = "disabled_days"; - private static final String KEY_THEME_DARK = "theme_dark"; - private static final String KEY_THEME_DARK_CHANGED = "theme_dark_changed"; - private static final String KEY_ACCENT = "accent"; - private static final String KEY_VIBRATE = "vibrate"; - private static final String KEY_DISMISS = "dismiss"; - private static final String KEY_AUTO_DISMISS = "auto_dismiss"; - private static final String KEY_DEFAULT_VIEW = "default_view"; - private static final String KEY_TITLE = "title"; - private static final String KEY_OK_RESID = "ok_resid"; - private static final String KEY_OK_STRING = "ok_string"; - private static final String KEY_OK_COLOR = "ok_color"; - private static final String KEY_CANCEL_RESID = "cancel_resid"; - private static final String KEY_CANCEL_STRING = "cancel_string"; - private static final String KEY_CANCEL_COLOR = "cancel_color"; - private static final String KEY_VERSION = "version"; - private static final String KEY_TIMEZONE = "timezone"; - - - private static final int DEFAULT_START_YEAR = 1900; - private static final int DEFAULT_END_YEAR = 2100; - - private static final int ANIMATION_DURATION = 300; - private static final int ANIMATION_DELAY = 500; - - private static SimpleDateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy", Locale.getDefault()); - private static SimpleDateFormat MONTH_FORMAT = new SimpleDateFormat("MMM", Locale.getDefault()); - private static SimpleDateFormat DAY_FORMAT = new SimpleDateFormat("dd", Locale.getDefault()); - private static SimpleDateFormat VERSION_2_FORMAT; - - private final Calendar mCalendar = trimToMidnight(Calendar.getInstance(getTimeZone())); - private OnDateSetListener mCallBack; - private HashSet mListeners = new HashSet<>(); - private DialogInterface.OnCancelListener mOnCancelListener; - private DialogInterface.OnDismissListener mOnDismissListener; - - private AccessibleDateAnimator mAnimator; - - private TextView mDatePickerHeaderView; - private LinearLayout mMonthAndDayView; - private TextView mSelectedMonthTextView; - private TextView mSelectedDayTextView; - private TextView mYearView; - private SupportDayPickerView mDayPickerView; - private SupportYearPickerView mYearPickerView; - - private int mCurrentView = UNINITIALIZED; - - private int mWeekStart = mCalendar.getFirstDayOfWeek(); - private int mMinYear = DEFAULT_START_YEAR; - private int mMaxYear = DEFAULT_END_YEAR; - private String mTitle; - private Calendar mMinDate; - private Calendar mMaxDate; - private HashSet highlightedDays = new HashSet<>(); - private TreeSet selectableDays = new TreeSet<>(); - private HashSet disabledDays = new HashSet<>(); - private boolean mThemeDark = false; - private boolean mThemeDarkChanged = false; - private int mAccentColor = -1; - private boolean mVibrate = true; - private boolean mDismissOnPause = false; - private boolean mAutoDismiss = false; - private int mDefaultView = MONTH_AND_DAY_VIEW; - private int mOkResid = R.string.mdtp_ok; - private String mOkString; - private int mOkColor = -1; - private int mCancelResid = R.string.mdtp_cancel; - private String mCancelString; - private int mCancelColor = -1; - private Version mVersion; - private TimeZone mTimezone; - - private Typeface headerTypeface; - - private HapticFeedbackController mHapticFeedbackController; - - private boolean mDelayAnimation = true; - - // Accessibility strings. - private String mDayPickerDescription; - private String mSelectDay; - private String mYearPickerDescription; - private String mSelectYear; - - /** - * The callback used to indicate the user is done filling in the date. - */ - public interface OnDateSetListener { - - /** - * @param view The view associated with this listener. - * @param year The year that was set. - * @param monthOfYear The month that was set (0-11) for compatibility - * with {@link java.util.Calendar}. - * @param dayOfMonth The day of the month that was set. - */ - void onDateSet(SupportDatePickerDialog view, int year, int monthOfYear, int dayOfMonth); - } - - /** - * The callback used to notify other date picker components of a change in selected date. - */ - interface OnDateChangedListener { - - void onDateChanged(); - } - - - public SupportDatePickerDialog() { - // Empty constructor required for dialog fragment. - } - - /** - * @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 static SupportDatePickerDialog newInstance(OnDateSetListener callBack, int year, - int monthOfYear, - int dayOfMonth) { - SupportDatePickerDialog ret = new SupportDatePickerDialog(); - ret.initialize(callBack, year, monthOfYear, dayOfMonth); - return ret; - } - - public void initialize(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) { - mCallBack = callBack; - mCalendar.set(Calendar.YEAR, year); - mCalendar.set(Calendar.MONTH, monthOfYear); - mCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); - - mVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M ? Version.VERSION_1 : Version.VERSION_2; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Activity activity = getActivity(); - activity.getWindow().setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); - mCurrentView = UNINITIALIZED; - if (savedInstanceState != null) { - mCalendar.set(Calendar.YEAR, savedInstanceState.getInt(KEY_SELECTED_YEAR)); - mCalendar.set(Calendar.MONTH, savedInstanceState.getInt(KEY_SELECTED_MONTH)); - mCalendar.set(Calendar.DAY_OF_MONTH, savedInstanceState.getInt(KEY_SELECTED_DAY)); - mDefaultView = savedInstanceState.getInt(KEY_DEFAULT_VIEW); - } - if (Build.VERSION.SDK_INT < 18) { - VERSION_2_FORMAT = new SimpleDateFormat(activity.getResources().getString(R.string.mdtp_date_v2_daymonthyear), Locale.getDefault()); - } else { - VERSION_2_FORMAT = new SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "EEEMMMdd"), Locale.getDefault()); - } - VERSION_2_FORMAT.setTimeZone(getTimeZone()); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putInt(KEY_SELECTED_YEAR, mCalendar.get(Calendar.YEAR)); - outState.putInt(KEY_SELECTED_MONTH, mCalendar.get(Calendar.MONTH)); - outState.putInt(KEY_SELECTED_DAY, mCalendar.get(Calendar.DAY_OF_MONTH)); - outState.putInt(KEY_WEEK_START, mWeekStart); - outState.putInt(KEY_YEAR_START, mMinYear); - outState.putInt(KEY_YEAR_END, mMaxYear); - outState.putInt(KEY_CURRENT_VIEW, mCurrentView); - int listPosition = -1; - if (mCurrentView == MONTH_AND_DAY_VIEW) { - listPosition = mDayPickerView.getMostVisiblePosition(); - } else if (mCurrentView == YEAR_VIEW) { - listPosition = mYearPickerView.getFirstVisiblePosition(); - outState.putInt(KEY_LIST_POSITION_OFFSET, mYearPickerView.getFirstPositionOffset()); - } - outState.putInt(KEY_LIST_POSITION, listPosition); - outState.putSerializable(KEY_MIN_DATE, mMinDate); - outState.putSerializable(KEY_MAX_DATE, mMaxDate); - outState.putSerializable(KEY_HIGHLIGHTED_DAYS, highlightedDays); - outState.putSerializable(KEY_SELECTABLE_DAYS, selectableDays); - outState.putSerializable(KEY_DISABLED_DAYS, disabledDays); - outState.putBoolean(KEY_THEME_DARK, mThemeDark); - outState.putBoolean(KEY_THEME_DARK_CHANGED, mThemeDarkChanged); - outState.putInt(KEY_ACCENT, mAccentColor); - outState.putBoolean(KEY_VIBRATE, mVibrate); - outState.putBoolean(KEY_DISMISS, mDismissOnPause); - outState.putBoolean(KEY_AUTO_DISMISS, mAutoDismiss); - outState.putInt(KEY_DEFAULT_VIEW, mDefaultView); - outState.putString(KEY_TITLE, mTitle); - outState.putInt(KEY_OK_RESID, mOkResid); - outState.putString(KEY_OK_STRING, mOkString); - outState.putInt(KEY_OK_COLOR, mOkColor); - outState.putInt(KEY_CANCEL_RESID, mCancelResid); - outState.putString(KEY_CANCEL_STRING, mCancelString); - outState.putInt(KEY_CANCEL_COLOR, mCancelColor); - outState.putSerializable(KEY_VERSION, mVersion); - outState.putSerializable(KEY_TIMEZONE, mTimezone); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - int listPosition = -1; - int listPositionOffset = 0; - int currentView = mDefaultView; - if (savedInstanceState != null) { - mWeekStart = savedInstanceState.getInt(KEY_WEEK_START); - mMinYear = savedInstanceState.getInt(KEY_YEAR_START); - mMaxYear = savedInstanceState.getInt(KEY_YEAR_END); - currentView = savedInstanceState.getInt(KEY_CURRENT_VIEW); - listPosition = savedInstanceState.getInt(KEY_LIST_POSITION); - listPositionOffset = savedInstanceState.getInt(KEY_LIST_POSITION_OFFSET); - mMinDate = (Calendar)savedInstanceState.getSerializable(KEY_MIN_DATE); - mMaxDate = (Calendar)savedInstanceState.getSerializable(KEY_MAX_DATE); - highlightedDays = (HashSet) savedInstanceState.getSerializable(KEY_HIGHLIGHTED_DAYS); - selectableDays = (TreeSet) savedInstanceState.getSerializable(KEY_SELECTABLE_DAYS); - disabledDays = (HashSet) savedInstanceState.getSerializable(KEY_DISABLED_DAYS); - mThemeDark = savedInstanceState.getBoolean(KEY_THEME_DARK); - mThemeDarkChanged = savedInstanceState.getBoolean(KEY_THEME_DARK_CHANGED); - mAccentColor = savedInstanceState.getInt(KEY_ACCENT); - mVibrate = savedInstanceState.getBoolean(KEY_VIBRATE); - mDismissOnPause = savedInstanceState.getBoolean(KEY_DISMISS); - mAutoDismiss = savedInstanceState.getBoolean(KEY_AUTO_DISMISS); - mTitle = savedInstanceState.getString(KEY_TITLE); - mOkResid = savedInstanceState.getInt(KEY_OK_RESID); - mOkString = savedInstanceState.getString(KEY_OK_STRING); - mOkColor = savedInstanceState.getInt(KEY_OK_COLOR); - - mCancelResid = savedInstanceState.getInt(KEY_CANCEL_RESID); - mCancelString = savedInstanceState.getString(KEY_CANCEL_STRING); - mCancelColor = savedInstanceState.getInt(KEY_CANCEL_COLOR); - mVersion = (Version) savedInstanceState.getSerializable(KEY_VERSION); - mTimezone = (TimeZone) savedInstanceState.getSerializable(KEY_TIMEZONE); - } - - int viewRes = mVersion == Version.VERSION_1 ? R.layout.mdtp_date_picker_dialog : R.layout.mdtp_date_picker_dialog_v2; - View view = inflater.inflate(viewRes, container, false); - // All options have been set at this point: round the initial selection if necessary - setToNearestDate(mCalendar); - - mDatePickerHeaderView = (TextView) view.findViewById(R.id.mdtp_date_picker_header); - mMonthAndDayView = (LinearLayout) view.findViewById(R.id.mdtp_date_picker_month_and_day); - mMonthAndDayView.setOnClickListener(this); - mSelectedMonthTextView = (TextView) view.findViewById(R.id.mdtp_date_picker_month); - mSelectedDayTextView = (TextView) view.findViewById(R.id.mdtp_date_picker_day); - mYearView = (TextView) view.findViewById(R.id.mdtp_date_picker_year); - mYearView.setOnClickListener(this); - - final Activity activity = getActivity(); - mDayPickerView = new SupportSimpleDayPickerView(activity, this); - mYearPickerView = new SupportYearPickerView(activity, this); - - // if theme mode has not been set by java code, check if it is specified in Style.xml - if (!mThemeDarkChanged) { - mThemeDark = Utils.isDarkTheme(activity, mThemeDark); - } - - Resources res = getResources(); - mDayPickerDescription = res.getString(R.string.mdtp_day_picker_description); - mSelectDay = res.getString(R.string.mdtp_select_day); - mYearPickerDescription = res.getString(R.string.mdtp_year_picker_description); - mSelectYear = res.getString(R.string.mdtp_select_year); - - int bgColorResource = mThemeDark ? R.color.mdtp_date_picker_view_animator_dark_theme : R.color.mdtp_date_picker_view_animator; - view.setBackgroundColor(ContextCompat.getColor(activity, bgColorResource)); - - mAnimator = (AccessibleDateAnimator) view.findViewById(R.id.mdtp_animator); - mAnimator.addView(mDayPickerView); - mAnimator.addView(mYearPickerView); - mAnimator.setDateMillis(mCalendar.getTimeInMillis()); - // TODO: Replace with animation decided upon by the design team. - Animation animation = new AlphaAnimation(0.0f, 1.0f); - animation.setDuration(ANIMATION_DURATION); - mAnimator.setInAnimation(animation); - // TODO: Replace with animation decided upon by the design team. - Animation animation2 = new AlphaAnimation(1.0f, 0.0f); - animation2.setDuration(ANIMATION_DURATION); - mAnimator.setOutAnimation(animation2); - - Button okButton = (Button) view.findViewById(R.id.mdtp_ok); - okButton.setOnClickListener(new View.OnClickListener() { - - @Override - public void onClick(View v) { - tryVibrate(); - notifyOnDateListener(); - dismiss(); - } - }); - okButton.setTypeface(TypefaceHelper.get(activity, "Roboto-Medium")); - if(mOkString != null) okButton.setText(mOkString); - else okButton.setText(mOkResid); - - Button cancelButton = (Button) view.findViewById(R.id.mdtp_cancel); - cancelButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - tryVibrate(); - if(getDialog() != null) getDialog().cancel(); - } - }); - cancelButton.setTypeface(TypefaceHelper.get(activity,"Roboto-Medium")); - if(mCancelString != null) cancelButton.setText(mCancelString); - else cancelButton.setText(mCancelResid); - cancelButton.setVisibility(isCancelable() ? View.VISIBLE : View.GONE); - - // If an accent color has not been set manually, get it from the context - if (mAccentColor == -1) { - mAccentColor = Utils.getAccentColorFromThemeIfAvailable(getActivity()); - } - if(mDatePickerHeaderView != null) { - mDatePickerHeaderView.setBackgroundColor(Utils.darkenColor(mAccentColor)); - if(headerTypeface != null){ - mDatePickerHeaderView.setTypeface(headerTypeface); - } - } - view.findViewById(R.id.mdtp_day_picker_selected_date_layout).setBackgroundColor(mAccentColor); - - // Buttons can have a different color - if (mOkColor != -1) okButton.setTextColor(mOkColor); - else okButton.setTextColor(mAccentColor); - if (mCancelColor != -1) cancelButton.setTextColor(mCancelColor); - else cancelButton.setTextColor(mAccentColor); - - if(getDialog() == null) { - view.findViewById(R.id.mdtp_done_background).setVisibility(View.GONE); - } - - updateDisplay(false); - setCurrentView(currentView); - - if (listPosition != -1) { - if (currentView == MONTH_AND_DAY_VIEW) { - mDayPickerView.postSetSelection(listPosition); - } else if (currentView == YEAR_VIEW) { - mYearPickerView.postSetSelectionFromTop(listPosition, listPositionOffset); - } - } - - mHapticFeedbackController = new HapticFeedbackController(activity); - return view; - } - - public void setTypeFace(Typeface typeFace){ - headerTypeface = typeFace; - - } - - @Override - public void onConfigurationChanged(final Configuration newConfig) { - super.onConfigurationChanged(newConfig); - ViewGroup viewGroup = (ViewGroup) getView(); - if (viewGroup != null) { - viewGroup.removeAllViewsInLayout(); - View view = onCreateView(getActivity().getLayoutInflater(), viewGroup, null); - viewGroup.addView(view); - } - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Dialog dialog = super.onCreateDialog(savedInstanceState); - dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); - return dialog; - } - - @Override - public void onResume() { - super.onResume(); - mHapticFeedbackController.start(); - } - - @Override - public void onPause() { - super.onPause(); - mHapticFeedbackController.stop(); - if(mDismissOnPause) dismiss(); - } - - @Override - public void onCancel(DialogInterface dialog) { - super.onCancel(dialog); - if(mOnCancelListener != null) mOnCancelListener.onCancel(dialog); - } - - @Override - public void onDismiss(DialogInterface dialog) { - super.onDismiss(dialog); - if(mOnDismissListener != null) mOnDismissListener.onDismiss(dialog); - } - - private void setCurrentView(final int viewIndex) { - long millis = mCalendar.getTimeInMillis(); - - switch (viewIndex) { - case MONTH_AND_DAY_VIEW: - if (mVersion == Version.VERSION_1) { - ObjectAnimator pulseAnimator = Utils.getPulseAnimator(mMonthAndDayView, 0.9f, - 1.05f); - if (mDelayAnimation) { - pulseAnimator.setStartDelay(ANIMATION_DELAY); - mDelayAnimation = false; - } - mDayPickerView.onDateChanged(); - if (mCurrentView != viewIndex) { - mMonthAndDayView.setSelected(true); - mYearView.setSelected(false); - mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW); - mCurrentView = viewIndex; - } - pulseAnimator.start(); - } else { - mDayPickerView.onDateChanged(); - if (mCurrentView != viewIndex) { - mMonthAndDayView.setSelected(true); - mYearView.setSelected(false); - mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW); - mCurrentView = viewIndex; - } - } - - int flags = DateUtils.FORMAT_SHOW_DATE; - String dayString = DateUtils.formatDateTime(getActivity(), millis, flags); - mAnimator.setContentDescription(mDayPickerDescription+": "+dayString); - Utils.tryAccessibilityAnnounce(mAnimator, mSelectDay); - break; - case YEAR_VIEW: - if (mVersion == Version.VERSION_1) { - ObjectAnimator pulseAnimator = Utils.getPulseAnimator(mYearView, 0.85f, 1.1f); - if (mDelayAnimation) { - pulseAnimator.setStartDelay(ANIMATION_DELAY); - mDelayAnimation = false; - } - mYearPickerView.onDateChanged(); - if (mCurrentView != viewIndex) { - mMonthAndDayView.setSelected(false); - mYearView.setSelected(true); - mAnimator.setDisplayedChild(YEAR_VIEW); - mCurrentView = viewIndex; - } - pulseAnimator.start(); - } else { - mYearPickerView.onDateChanged(); - if (mCurrentView != viewIndex) { - mMonthAndDayView.setSelected(false); - mYearView.setSelected(true); - mAnimator.setDisplayedChild(YEAR_VIEW); - mCurrentView = viewIndex; - } - } - - CharSequence yearString = YEAR_FORMAT.format(millis); - mAnimator.setContentDescription(mYearPickerDescription+": "+yearString); - Utils.tryAccessibilityAnnounce(mAnimator, mSelectYear); - break; - } - } - - private void updateDisplay(boolean announce) { - mYearView.setText(YEAR_FORMAT.format(mCalendar.getTime())); - - if (mVersion == Version.VERSION_1) { - if (mDatePickerHeaderView != null) { - if(headerTypeface != null){ - mDatePickerHeaderView.setTypeface(headerTypeface); - } - if (mTitle != null) - mDatePickerHeaderView.setText(mTitle.toUpperCase(Locale.getDefault())); - else { - mDatePickerHeaderView.setText(mCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG, - Locale.getDefault()).toUpperCase(Locale.getDefault())); - } - } - mSelectedMonthTextView.setText(MONTH_FORMAT.format(mCalendar.getTime())); - mSelectedDayTextView.setText(DAY_FORMAT.format(mCalendar.getTime())); - } - - if (mVersion == Version.VERSION_2) { - mSelectedDayTextView.setText(VERSION_2_FORMAT.format(mCalendar.getTime())); - if(headerTypeface != null){ - mDatePickerHeaderView.setTypeface(headerTypeface); - } - if (mTitle != null) - mDatePickerHeaderView.setText(mTitle.toUpperCase(Locale.getDefault())); - else - mDatePickerHeaderView.setVisibility(View.GONE); - } - - // Accessibility. - long millis = mCalendar.getTimeInMillis(); - mAnimator.setDateMillis(millis); - int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR; - String monthAndDayText = DateUtils.formatDateTime(getActivity(), millis, flags); - mMonthAndDayView.setContentDescription(monthAndDayText); - - if (announce) { - flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR; - String fullDateText = DateUtils.formatDateTime(getActivity(), millis, flags); - Utils.tryAccessibilityAnnounce(mAnimator, fullDateText); - } - } - - /** - * Set whether the device should vibrate when touching fields - * @param vibrate true if the device should vibrate when touching a field - */ - public void vibrate(boolean vibrate) { - mVibrate = vibrate; - } - - /** - * Set whether the picker should dismiss itself when being paused or whether it should try to survive an orientation change - * @param dismissOnPause true if the dialog should dismiss itself when it's pausing - */ - public void dismissOnPause(boolean dismissOnPause) { - mDismissOnPause = dismissOnPause; - } - - /** - * Set whether the picker should dismiss itself when a day is selected - * @param autoDismiss true if the dialog should dismiss itself when a day is selected - */ - @SuppressWarnings("unused") - public void autoDismiss(boolean autoDismiss) { - mAutoDismiss = autoDismiss; - } - - /** - * Set whether the dark theme should be used - * @param themeDark true if the dark theme should be used, false if the default theme should be used - */ - public void setThemeDark(boolean themeDark) { - mThemeDark = themeDark; - mThemeDarkChanged = true; - } - - /** - * Returns true when the dark theme should be used - * @return true if the dark theme should be used, false if the default theme should be used - */ - @Override - public boolean isThemeDark() { - return mThemeDark; - } - - /** - * Set the accent color of this dialog - * @param color the accent color you want - */ - @SuppressWarnings("unused") - public void setAccentColor(String color) { - mAccentColor = Color.parseColor(color); - } - - /** - * Set the accent color of this dialog - * @param color the accent color you want - */ - public void setAccentColor(@ColorInt int color) { - mAccentColor = Color.argb(255, Color.red(color), Color.green(color), Color.blue(color)); - } - - /** - * Set the text color of the OK button - * @param color the color you want - */ - @SuppressWarnings("unused") - public void setOkColor(String color) { - mOkColor = Color.parseColor(color); - } - - /** - * Set the text color of the OK button - * @param color the color you want - */ - @SuppressWarnings("unused") - public void setOkColor(@ColorInt int color) { - mOkColor = Color.argb(255, Color.red(color), Color.green(color), Color.blue(color)); - } - - /** - * Set the text color of the Cancel button - * @param color the color you want - */ - @SuppressWarnings("unused") - public void setCancelColor(String color) { - mCancelColor = Color.parseColor(color); - } - - /** - * Set the text color of the Cancel button - * @param color the color you want - */ - @SuppressWarnings("unused") - public void setCancelColor(@ColorInt int color) { - mCancelColor = Color.argb(255, Color.red(color), Color.green(color), Color.blue(color)); - } - - /** - * Get the accent color of this dialog - * @return accent color - */ - @Override - public int getAccentColor() { - return mAccentColor; - } - - /** - * Set whether the year picker of the month and day picker is shown first - * @param yearPicker boolean - */ - public void showYearPickerFirst(boolean yearPicker) { - mDefaultView = yearPicker ? YEAR_VIEW : MONTH_AND_DAY_VIEW; - } - - @SuppressWarnings("unused") - public void setFirstDayOfWeek(int startOfWeek) { - if (startOfWeek < Calendar.SUNDAY || startOfWeek > Calendar.SATURDAY) { - throw new IllegalArgumentException("Value must be between Calendar.SUNDAY and " + - "Calendar.SATURDAY"); - } - mWeekStart = startOfWeek; - if (mDayPickerView != null) { - mDayPickerView.onChange(); - } - } - - @SuppressWarnings("unused") - public void setYearRange(int startYear, int endYear) { - if (endYear < startYear) { - throw new IllegalArgumentException("Year end must be larger than or equal to year start"); - } - - mMinYear = startYear; - mMaxYear = endYear; - if (mDayPickerView != null) { - mDayPickerView.onChange(); - } - } - - /** - * Sets the minimal date supported by this DatePicker. Dates before (but not including) the - * specified date will be disallowed from being selected. - * @param calendar a Calendar object set to the year, month, day desired as the mindate. - */ - @SuppressWarnings("unused") - public void setMinDate(Calendar calendar) { - mMinDate = trimToMidnight((Calendar) calendar.clone()); - - if (mDayPickerView != null) { - mDayPickerView.onChange(); - } - } - - /** - * @return The minimal date supported by this DatePicker. Null if it has not been set. - */ - @SuppressWarnings("unused") - public Calendar getMinDate() { - return mMinDate; - } - - /** - * Sets the minimal date supported by this DatePicker. Dates after (but not including) the - * specified date will be disallowed from being selected. - * @param calendar a Calendar object set to the year, month, day desired as the maxdate. - */ - @SuppressWarnings("unused") - public void setMaxDate(Calendar calendar) { - mMaxDate = trimToMidnight((Calendar) calendar.clone()); - - if (mDayPickerView != null) { - mDayPickerView.onChange(); - } - } - - /** - * @return The maximal date supported by this DatePicker. Null if it has not been set. - */ - @SuppressWarnings("unused") - public Calendar getMaxDate() { - return mMaxDate; - } - - /** - * Sets an array of dates which should be highlighted when the picker is drawn - * @param highlightedDays an Array of Calendar objects containing the dates to be highlighted - */ - @SuppressWarnings("unused") - public void setHighlightedDays(Calendar[] highlightedDays) { - for (Calendar highlightedDay : highlightedDays) trimToMidnight(highlightedDay); - this.highlightedDays.addAll(Arrays.asList(highlightedDays)); - if (mDayPickerView != null) mDayPickerView.onChange(); - } - - /** - * @return The list of dates, as Calendar Objects, which should be highlighted. null is no dates should be highlighted - */ - @SuppressWarnings("unused") - public Calendar[] getHighlightedDays() { - if (highlightedDays.isEmpty()) return null; - Calendar[] output = highlightedDays.toArray(new Calendar[0]); - Arrays.sort(output); - return output; - } - - @Override - public boolean isHighlighted(int year, int month, int day) { - Calendar date = Calendar.getInstance(); - date.set(Calendar.YEAR, year); - date.set(Calendar.MONTH, month); - date.set(Calendar.DAY_OF_MONTH, day); - trimToMidnight(date); - return highlightedDays.contains(date); - } - - /** - * Sets a list of days which are the only valid selections. - * Setting this value will take precedence over using setMinDate() and setMaxDate() - * @param selectableDays an Array of Calendar Objects containing the selectable dates - */ - @SuppressWarnings("unused") - public void setSelectableDays(Calendar[] selectableDays) { - for (Calendar selectableDay : selectableDays) trimToMidnight(selectableDay); - this.selectableDays.addAll(Arrays.asList(selectableDays)); - if (mDayPickerView != null) mDayPickerView.onChange(); - } - - /** - * @return an Array of Calendar objects containing the list with selectable items. null if no restriction is set - */ - @SuppressWarnings("unused") - public Calendar[] getSelectableDays() { - return selectableDays.isEmpty() ? null : selectableDays.toArray(new Calendar[0]); - } - - /** - * Sets a list of days that are not selectable in the picker - * Setting this value will take precedence over using setMinDate() and setMaxDate(), but stacks with setSelectableDays() - * @param disabledDays an Array of Calendar Objects containing the disabled dates - */ - @SuppressWarnings("unused") - public void setDisabledDays(Calendar[] disabledDays) { - for (Calendar disabledDay : disabledDays) trimToMidnight(disabledDay); - this.disabledDays.addAll(Arrays.asList(disabledDays)); - if (mDayPickerView != null) mDayPickerView.onChange(); - } - - /** - * @return an Array of Calendar objects containing the list of days that are not selectable. null if no restriction is set - */ - @SuppressWarnings("unused") - public Calendar[] getDisabledDays() { - if (disabledDays.isEmpty()) return null; - Calendar[] output = disabledDays.toArray(new Calendar[0]); - Arrays.sort(output); - return output; - } - - /** - * Set a title to be displayed instead of the weekday - * @param title String - The title to be displayed - */ - public void setTitle(String title) { - mTitle = title; - } - - /** - * Set the label for the Ok button (max 12 characters) - * @param okString A literal String to be used as the Ok button label - */ - @SuppressWarnings("unused") - public void setOkText(String okString) { - mOkString = okString; - } - - /** - * Set the label for the Ok button (max 12 characters) - * @param okResid A resource ID to be used as the Ok button label - */ - @SuppressWarnings("unused") - public void setOkText(@StringRes int okResid) { - mOkString = null; - mOkResid = okResid; - } - - /** - * Set the label for the Cancel button (max 12 characters) - * @param cancelString A literal String to be used as the Cancel button label - */ - @SuppressWarnings("unused") - public void setCancelText(String cancelString) { - mCancelString = cancelString; - } - - /** - * Set the label for the Cancel button (max 12 characters) - * @param cancelResid A resource ID to be used as the Cancel button label - */ - @SuppressWarnings("unused") - public void setCancelText(@StringRes int cancelResid) { - mCancelString = null; - mCancelResid = cancelResid; - } - - /** - * Set which layout version the picker should use - * @param version The version to use - */ - public void setVersion(Version version) { - mVersion = version; - } - - /** - * Set which timezone the picker should use - * @param timeZone The timezone to use - */ - @SuppressWarnings("unused") - public void setTimeZone(TimeZone timeZone) { - mTimezone = timeZone; - mCalendar.setTimeZone(timeZone); - YEAR_FORMAT.setTimeZone(timeZone); - MONTH_FORMAT.setTimeZone(timeZone); - DAY_FORMAT.setTimeZone(timeZone); - } - - @SuppressWarnings("unused") - public void setOnDateSetListener(OnDateSetListener listener) { - mCallBack = listener; - } - - @SuppressWarnings("unused") - public void setOnCancelListener(DialogInterface.OnCancelListener onCancelListener) { - mOnCancelListener = onCancelListener; - } - - @SuppressWarnings("unused") - public void setOnDismissListener(DialogInterface.OnDismissListener onDismissListener) { - mOnDismissListener = onDismissListener; - } - - // If the newly selected month / year does not contain the currently selected day number, - // change the selected day number to the last day of the selected month or year. - // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30 - // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013 - private void adjustDayInMonthIfNeeded(Calendar calendar) { - int day = calendar.get(Calendar.DAY_OF_MONTH); - int daysInMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH); - if (day > daysInMonth) { - calendar.set(Calendar.DAY_OF_MONTH, daysInMonth); - } - setToNearestDate(calendar); - } - - @Override - public void onClick(View v) { - tryVibrate(); - if (v.getId() == R.id.mdtp_date_picker_year) { - setCurrentView(YEAR_VIEW); - } else if (v.getId() == R.id.mdtp_date_picker_month_and_day) { - setCurrentView(MONTH_AND_DAY_VIEW); - } - } - - @Override - public void onYearSelected(int year) { - mCalendar.set(Calendar.YEAR, year); - adjustDayInMonthIfNeeded(mCalendar); - updatePickers(); - setCurrentView(MONTH_AND_DAY_VIEW); - updateDisplay(true); - } - - @Override - public void onDayOfMonthSelected(int year, int month, int day) { - mCalendar.set(Calendar.YEAR, year); - mCalendar.set(Calendar.MONTH, month); - mCalendar.set(Calendar.DAY_OF_MONTH, day); - updatePickers(); - updateDisplay(true); - if (mAutoDismiss) { - notifyOnDateListener(); - dismiss(); - } - } - - private void updatePickers() { - for(OnDateChangedListener listener : mListeners) listener.onDateChanged(); - } - - - @Override - public SupportMonthAdapter.CalendarDay getSelectedDay() { - return new SupportMonthAdapter.CalendarDay(mCalendar, getTimeZone()); - } - - @Override - public Calendar getStartDate() { - if (!selectableDays.isEmpty()) return selectableDays.first(); - if (mMinDate != null) return mMinDate; - Calendar output = Calendar.getInstance(getTimeZone()); - output.set(Calendar.YEAR, mMinYear); - output.set(Calendar.DAY_OF_MONTH, 1); - output.set(Calendar.MONTH, Calendar.JANUARY); - return output; - } - - @Override - public Calendar getEndDate() { - if (!selectableDays.isEmpty()) return selectableDays.last(); - if (mMaxDate != null) return mMaxDate; - Calendar output = Calendar.getInstance(getTimeZone()); - output.set(Calendar.YEAR, mMaxYear); - output.set(Calendar.DAY_OF_MONTH, 31); - output.set(Calendar.MONTH, Calendar.DECEMBER); - return output; - } - - @Override - public int getMinYear() { - if (!selectableDays.isEmpty()) return selectableDays.first().get(Calendar.YEAR); - // Ensure no years can be selected outside of the given minimum date - return mMinDate != null && mMinDate.get(Calendar.YEAR) > mMinYear ? mMinDate.get(Calendar.YEAR) : mMinYear; - } - - @Override - public int getMaxYear() { - if (!selectableDays.isEmpty()) return selectableDays.last().get(Calendar.YEAR); - // Ensure no years can be selected outside of the given maximum date - return mMaxDate != null && mMaxDate.get(Calendar.YEAR) < mMaxYear ? mMaxDate.get(Calendar.YEAR) : mMaxYear; - } - - /** - * @return true if the specified year/month/day are within the selectable days or the range set by minDate and maxDate. - * If one or either have not been set, they are considered as Integer.MIN_VALUE and - * Integer.MAX_VALUE. - */ - @Override - public boolean isOutOfRange(int year, int month, int day) { - Calendar date = Calendar.getInstance(); - date.set(Calendar.YEAR, year); - date.set(Calendar.MONTH, month); - date.set(Calendar.DAY_OF_MONTH, day); - return isOutOfRange(date); - } - - @SuppressWarnings("unused") - public boolean isOutOfRange(Calendar calendar) { - trimToMidnight(calendar); - return isDisabled(calendar) || !isSelectable(calendar); - } - - private boolean isDisabled(Calendar c) { - return disabledDays.contains(trimToMidnight(c)) || isBeforeMin(c) || isAfterMax(c); - } - - private boolean isSelectable(Calendar c) { - return selectableDays.isEmpty() || selectableDays.contains(trimToMidnight(c)); - } - - private boolean isBeforeMin(Calendar calendar) { - return mMinDate != null && calendar.before(mMinDate); - } - - private boolean isAfterMax(Calendar calendar) { - return mMaxDate != null && calendar.after(mMaxDate); - } - - private void setToNearestDate(Calendar calendar) { - if (!selectableDays.isEmpty()) { - Calendar newCalendar = null; - Calendar higher = selectableDays.ceiling(calendar); - Calendar lower = selectableDays.lower(calendar); - - if (higher == null && lower != null) newCalendar = lower; - else if (lower == null && higher != null) newCalendar = higher; - - if (newCalendar != null || higher == null) { - newCalendar = newCalendar == null ? calendar : newCalendar; - newCalendar.setTimeZone(getTimeZone()); - calendar.setTimeInMillis(newCalendar.getTimeInMillis()); - return; - } - - long highDistance = Math.abs(higher.getTimeInMillis() - calendar.getTimeInMillis()); - long lowDistance = Math.abs(calendar.getTimeInMillis() - lower.getTimeInMillis()); - - if (lowDistance < highDistance) calendar.setTimeInMillis(lower.getTimeInMillis()); - else calendar.setTimeInMillis(higher.getTimeInMillis()); - - return; - } - - if (!disabledDays.isEmpty()) { - Calendar forwardDate = (Calendar) calendar.clone(); - Calendar backwardDate = (Calendar) calendar.clone(); - while (isDisabled(forwardDate) && isDisabled(backwardDate)) { - forwardDate.add(Calendar.DAY_OF_MONTH, 1); - backwardDate.add(Calendar.DAY_OF_MONTH, -1); - } - if (!isDisabled(backwardDate)) { - calendar.setTimeInMillis(backwardDate.getTimeInMillis()); - return; - } - if (!isDisabled(forwardDate)) { - calendar.setTimeInMillis(forwardDate.getTimeInMillis()); - return; - } - } - - - if(isBeforeMin(calendar)) { - calendar.setTimeInMillis(mMinDate.getTimeInMillis()); - return; - } - - if(isAfterMax(calendar)) { - calendar.setTimeInMillis(mMaxDate.getTimeInMillis()); - return; - } - } - - /** - * Trims off all time information, effectively setting it to midnight - * Makes it easier to compare at just the day level - * @param calendar The Calendar object to trim - * @return The trimmed Calendar object - */ - private Calendar trimToMidnight(Calendar calendar) { - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - return calendar; - } - - @Override - public int getFirstDayOfWeek() { - return mWeekStart; - } - - @Override - public void registerOnDateChangedListener(OnDateChangedListener listener) { - mListeners.add(listener); - } - - @Override - public void unregisterOnDateChangedListener(OnDateChangedListener listener) { - mListeners.remove(listener); - } - - @Override - public void tryVibrate() { - if(mVibrate) mHapticFeedbackController.tryVibrate(); - } - - @Override public TimeZone getTimeZone() { - return mTimezone == null ? TimeZone.getDefault() : mTimezone; - } - - public void notifyOnDateListener() { - if (mCallBack != null) { - mCallBack.onDateSet(SupportDatePickerDialog.this, mCalendar.get(Calendar.YEAR), - mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH)); - } - } -} - diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportDayPickerView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportDayPickerView.java deleted file mode 100644 index 03e8bd5c..00000000 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportDayPickerView.java +++ /dev/null @@ -1,507 +0,0 @@ -package com.wdullaer.materialdatetimepicker.supportdate; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.support.annotation.NonNull; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.AbsListView; -import android.widget.ListView; - -import com.wdullaer.materialdatetimepicker.Utils; -import com.wdullaer.materialdatetimepicker.date.DatePickerController; -import com.wdullaer.materialdatetimepicker.date.DayPickerView; -import com.wdullaer.materialdatetimepicker.date.MonthAdapter; -import com.wdullaer.materialdatetimepicker.date.MonthView; - -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Locale; - -/** - * Created by rmore on 06/03/2017. - */ - -public abstract class SupportDayPickerView extends ListView implements AbsListView.OnScrollListener, - SupportDatePickerDialog.OnDateChangedListener { - - private static final String TAG = "MonthFragment"; - - // Affects when the month selection will change while scrolling up - protected static final int SCROLL_HYST_WEEKS = 2; - // How long the GoTo fling animation should last - protected static final int GOTO_SCROLL_DURATION = 250; - // How long to wait after receiving an onScrollStateChanged notification - // before acting on it - protected static final int SCROLL_CHANGE_DELAY = 40; - // The number of days to display in each week - public static final int DAYS_PER_WEEK = 7; - public static int LIST_TOP_OFFSET = -1; // so that the top line will be - // under the separator - // You can override these numbers to get a different appearance - protected int mNumWeeks = 6; - protected boolean mShowWeekNumber = false; - protected int mDaysPerWeek = 7; - private static SimpleDateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy", Locale.getDefault()); - - // These affect the scroll speed and feel - protected float mFriction = 1.0f; - - protected Context mContext; - protected Handler mHandler; - - // highlighted time - protected SupportMonthAdapter.CalendarDay mSelectedDay; - protected SupportMonthAdapter mAdapter; - - protected SupportMonthAdapter.CalendarDay mTempDay; - - // When the week starts; numbered like Time. (e.g. SUNDAY=0). - protected int mFirstDayOfWeek; - // The last name announced by accessibility - protected CharSequence mPrevMonthName; - // which month should be displayed/highlighted [0-11] - protected int mCurrentMonthDisplayed; - // used for tracking during a scroll - protected long mPreviousScrollPosition; - // used for tracking what state listview is in - protected int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE; - // used for tracking what state listview is in - protected int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE; - - private SupportDatePickerController mController; - private boolean mPerformingScroll; - - public SupportDayPickerView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context); - } - - public SupportDayPickerView(Context context, SupportDatePickerController controller) { - super(context); - init(context); - setController(controller); - } - - public void setController(SupportDatePickerController controller) { - mController = controller; - mController.registerOnDateChangedListener(this); - mSelectedDay = new SupportMonthAdapter.CalendarDay(mController.getTimeZone()); - mTempDay = new SupportMonthAdapter.CalendarDay(mController.getTimeZone()); - refreshAdapter(); - onDateChanged(); - } - - public void init(Context context) { - mHandler = new Handler(); - setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - setDrawSelectorOnTop(false); - - mContext = context; - setUpListView(); - } - - - - public void onChange() { - refreshAdapter(); - } - - /** - * Creates a new adapter if necessary and sets up its parameters. Override - * this method to provide a custom adapter. - */ - protected void refreshAdapter() { - if (mAdapter == null) { - mAdapter = createMonthAdapter(getContext(), mController); - } else { - mAdapter.setSelectedDay(mSelectedDay); - } - // refresh the view with the new parameters - setAdapter(mAdapter); - } - - public abstract SupportMonthAdapter createMonthAdapter(Context context, - SupportDatePickerController controller); - - /* - * Sets all the required fields for the list view. Override this method to - * set a different list view behavior. - */ - protected void setUpListView() { - // Transparent background on scroll - setCacheColorHint(0); - // No dividers - setDivider(null); - // Items are clickable - setItemsCanFocus(true); - // The thumb gets in the way, so disable it - setFastScrollEnabled(false); - setVerticalScrollBarEnabled(false); - setOnScrollListener(this); - setFadingEdgeLength(0); - // Make the scrolling behavior nicer - setFriction(ViewConfiguration.getScrollFriction() * mFriction); - } - - /** - * This moves to the specified time in the view. If the time is not already - * in range it will move the list so that the first of the month containing - * the time is at the top of the view. If the new time is already in view - * the list will not be scrolled unless forceScroll is true. This time may - * optionally be highlighted as selected as well. - * - * @param day The day to move to - * @param animate Whether to scroll to the given time or just redraw at the - * new location - * @param setSelected Whether to set the given time as selected - * @param forceScroll Whether to recenter even if the time is already - * visible - * @return Whether or not the view animated to the new location - */ - public boolean goTo(SupportMonthAdapter.CalendarDay day, boolean animate, boolean setSelected, boolean forceScroll) { - - // Set the selected day - if (setSelected) { - mSelectedDay.set(day); - } - - mTempDay.set(day); - int minMonth = mController.getStartDate().get(Calendar.MONTH); - final int position = (day.year - mController.getMinYear()) - * SupportMonthAdapter.MONTHS_IN_YEAR + day.month - minMonth; - - View child; - int i = 0; - int top = 0; - // Find a child that's completely in the view - do { - child = getChildAt(i++); - if (child == null) { - break; - } - top = child.getTop(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "child at " + (i - 1) + " has top " + top); - } - } while (top < 0); - - // Compute the first and last position visible - int selectedPosition; - if (child != null) { - selectedPosition = getPositionForView(child); - } else { - selectedPosition = 0; - } - - if (setSelected) { - mAdapter.setSelectedDay(mSelectedDay); - } - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "GoTo position " + position); - } - // Check if the selected day is now outside of our visible range - // and if so scroll to the month that contains it - if (position != selectedPosition || forceScroll) { - setMonthDisplayed(mTempDay); - mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING; - if (animate) { - smoothScrollToPositionFromTop( - position, LIST_TOP_OFFSET, GOTO_SCROLL_DURATION); - return true; - } else { - postSetSelection(position); - } - } else if (setSelected) { - setMonthDisplayed(mSelectedDay); - } - return false; - } - - public void postSetSelection(final int position) { - clearFocus(); - post(new Runnable() { - - @Override - public void run() { - setSelection(position); - } - }); - onScrollStateChanged(this, OnScrollListener.SCROLL_STATE_IDLE); - } - - /** - * Updates the title and selected month if the view has moved to a new - * month. - */ - @Override - public void onScroll( - AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - SupportMonthView child = (SupportMonthView) view.getChildAt(0); - if (child == null) { - return; - } - - // Figure out where we are - long currScroll = view.getFirstVisiblePosition() * child.getHeight() - child.getBottom(); - mPreviousScrollPosition = currScroll; - mPreviousScrollState = mCurrentScrollState; - } - - /** - * Sets the month displayed at the top of this view based on time. Override - * to add custom events when the title is changed. - */ - protected void setMonthDisplayed(SupportMonthAdapter.CalendarDay date) { - mCurrentMonthDisplayed = date.month; - invalidateViews(); - } - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - // use a post to prevent re-entering onScrollStateChanged before it - // exits - mScrollStateChangedRunnable.doScrollStateChange(view, scrollState); - } - - protected SupportDayPickerView.ScrollStateRunnable mScrollStateChangedRunnable = new SupportDayPickerView.ScrollStateRunnable(); - - protected class ScrollStateRunnable implements Runnable { - private int mNewState; - - /** - * Sets up the runnable with a short delay in case the scroll state - * immediately changes again. - * - * @param view The list view that changed state - * @param scrollState The new state it changed to - */ - public void doScrollStateChange(AbsListView view, int scrollState) { - mHandler.removeCallbacks(this); - mNewState = scrollState; - mHandler.postDelayed(this, SCROLL_CHANGE_DELAY); - } - - @Override - public void run() { - mCurrentScrollState = mNewState; - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, - "new scroll state: " + mNewState + " old state: " + mPreviousScrollState); - } - // Fix the position after a scroll or a fling ends - if (mNewState == OnScrollListener.SCROLL_STATE_IDLE - && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE - && mPreviousScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { - mPreviousScrollState = mNewState; - int i = 0; - View child = getChildAt(i); - while (child != null && child.getBottom() <= 0) { - child = getChildAt(++i); - } - if (child == null) { - // The view is no longer visible, just return - return; - } - int firstPosition = getFirstVisiblePosition(); - int lastPosition = getLastVisiblePosition(); - boolean scroll = firstPosition != 0 && lastPosition != getCount() - 1; - final int top = child.getTop(); - final int bottom = child.getBottom(); - final int midpoint = getHeight() / 2; - if (scroll && top < LIST_TOP_OFFSET) { - if (bottom > midpoint) { - smoothScrollBy(top, GOTO_SCROLL_DURATION); - } else { - smoothScrollBy(bottom, GOTO_SCROLL_DURATION); - } - } - } else { - mPreviousScrollState = mNewState; - } - } - } - - /** - * Gets the position of the view that is most prominently displayed within the list view. - */ - public int getMostVisiblePosition() { - final int firstPosition = getFirstVisiblePosition(); - final int height = getHeight(); - - int maxDisplayedHeight = 0; - int mostVisibleIndex = 0; - int i=0; - int bottom = 0; - while (bottom < height) { - View child = getChildAt(i); - if (child == null) { - break; - } - bottom = child.getBottom(); - int displayedHeight = Math.min(bottom, height) - Math.max(0, child.getTop()); - if (displayedHeight > maxDisplayedHeight) { - mostVisibleIndex = i; - maxDisplayedHeight = displayedHeight; - } - i++; - } - return firstPosition + mostVisibleIndex; - } - - @Override - public void onDateChanged() { - goTo(mController.getSelectedDay(), false, true, true); - } - - /** - * Attempts to return the date that has accessibility focus. - * - * @return The date that has accessibility focus, or {@code null} if no date - * has focus. - */ - private MonthAdapter.CalendarDay findAccessibilityFocus() { - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - if (child instanceof MonthView) { - final MonthAdapter.CalendarDay focus = ((MonthView) child).getAccessibilityFocus(); - if (focus != null) { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) { - // Clear focus to avoid ListView bug in Jelly Bean MR1. - ((MonthView) child).clearAccessibilityFocus(); - } - return focus; - } - } - } - - return null; - } - - /** - * Attempts to restore accessibility focus to a given date. No-op if - * {@code day} is {@code null}. - * - * @param day The date that should receive accessibility focus - * @return {@code true} if focus was restored - */ - private boolean restoreAccessibilityFocus(MonthAdapter.CalendarDay day) { - if (day == null) { - return false; - } - - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - if (child instanceof MonthView) { - if (((MonthView) child).restoreAccessibilityFocus(day)) { - return true; - } - } - } - - return false; - } - - @Override - protected void layoutChildren() { - final MonthAdapter.CalendarDay focusedDay = findAccessibilityFocus(); - super.layoutChildren(); - if (mPerformingScroll) { - mPerformingScroll = false; - } else { - restoreAccessibilityFocus(focusedDay); - } - } - - @Override - public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setItemCount(-1); - } - - private static String getMonthAndYearString(SupportMonthAdapter.CalendarDay day) { - Calendar cal = Calendar.getInstance(); - cal.set(day.year, day.month, day.day); - - String sbuf = ""; - sbuf += cal.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault()); - sbuf += " "; - sbuf += YEAR_FORMAT.format(cal.getTime()); - return sbuf; - } - - /** - * Necessary for accessibility, to ensure we support "scrolling" forward and backward - * in the month list. - */ - @Override - @SuppressWarnings("deprecation") - public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - if(Build.VERSION.SDK_INT >= 21) { - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); - info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); - } - else { - info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); - info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); - } - } - - /** - * When scroll forward/backward events are received, announce the newly scrolled-to month. - */ - @SuppressLint("NewApi") - @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { - if (action != AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && - action != AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { - return super.performAccessibilityAction(action, arguments); - } - - // Figure out what month is showing. - int firstVisiblePosition = getFirstVisiblePosition(); - int minMonth = mController.getStartDate().get(Calendar.MONTH); - int month = (firstVisiblePosition + minMonth) % SupportMonthAdapter.MONTHS_IN_YEAR; - int year = (firstVisiblePosition + minMonth) / SupportMonthAdapter.MONTHS_IN_YEAR + mController.getMinYear(); - SupportMonthAdapter.CalendarDay day = new SupportMonthAdapter.CalendarDay(year, month, 1); - - // Scroll either forward or backward one month. - if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { - day.month++; - if (day.month == 12) { - day.month = 0; - day.year++; - } - } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { - View firstVisibleView = getChildAt(0); - // If the view is fully visible, jump one month back. Otherwise, we'll just jump - // to the first day of first visible month. - if (firstVisibleView != null && firstVisibleView.getTop() >= -1) { - // There's an off-by-one somewhere, so the top of the first visible item will - // actually be -1 when it's at the exact top. - day.month--; - if (day.month == -1) { - day.month = 11; - day.year--; - } - } - } - - // Go to that month. - Utils.tryAccessibilityAnnounce(this, getMonthAndYearString(day)); - goTo(day, true, false, true); - mPerformingScroll = true; - return true; - } -} diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportMonthAdapter.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportMonthAdapter.java deleted file mode 100644 index 17d684d8..00000000 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportMonthAdapter.java +++ /dev/null @@ -1,224 +0,0 @@ -package com.wdullaer.materialdatetimepicker.supportdate; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.BaseAdapter; - -import com.wdullaer.materialdatetimepicker.date.DatePickerController; -import com.wdullaer.materialdatetimepicker.date.MonthAdapter; -import com.wdullaer.materialdatetimepicker.date.MonthView; - -import java.util.Calendar; -import java.util.HashMap; -import java.util.TimeZone; - -/** - * Created by rmore on 06/03/2017. - */ - -public abstract class SupportMonthAdapter extends BaseAdapter implements SupportMonthView.OnDayClickListener { - - private static final String TAG = "SimpleMonthAdapter"; - - private final Context mContext; - protected final SupportDatePickerController mController; - - private SupportMonthAdapter.CalendarDay mSelectedDay; - - protected static int WEEK_7_OVERHANG_HEIGHT = 7; - protected static final int MONTHS_IN_YEAR = 12; - - /** - * A convenience class to represent a specific date. - */ - public static class CalendarDay { - private Calendar calendar; - int year; - int month; - int day; - TimeZone mTimeZone; - - public CalendarDay(TimeZone timeZone) { - mTimeZone = timeZone; - setTime(System.currentTimeMillis()); - } - - public CalendarDay(long timeInMillis, TimeZone timeZone) { - mTimeZone = timeZone; - setTime(timeInMillis); - } - - public CalendarDay(Calendar calendar, TimeZone timeZone) { - mTimeZone = timeZone; - year = calendar.get(Calendar.YEAR); - month = calendar.get(Calendar.MONTH); - day = calendar.get(Calendar.DAY_OF_MONTH); - } - - public CalendarDay(int year, int month, int day) { - setDay(year, month, day); - } - - public void set(SupportMonthAdapter.CalendarDay date) { - year = date.year; - month = date.month; - day = date.day; - } - - public void setDay(int year, int month, int day) { - this.year = year; - this.month = month; - this.day = day; - } - - private void setTime(long timeInMillis) { - if (calendar == null) { - calendar = Calendar.getInstance(mTimeZone); - } - calendar.setTimeInMillis(timeInMillis); - month = calendar.get(Calendar.MONTH); - year = calendar.get(Calendar.YEAR); - day = calendar.get(Calendar.DAY_OF_MONTH); - } - - public int getYear() { - return year; - } - - public int getMonth() { - return month; - } - - public int getDay() { - return day; - } - } - - public SupportMonthAdapter(Context context, - SupportDatePickerController controller) { - mContext = context; - mController = controller; - init(); - setSelectedDay(mController.getSelectedDay()); - } - - /** - * Updates the selected day and related parameters. - * - * @param day The day to highlight - */ - public void setSelectedDay(SupportMonthAdapter.CalendarDay day) { - mSelectedDay = day; - notifyDataSetChanged(); - } - - @SuppressWarnings("unused") - public SupportMonthAdapter.CalendarDay getSelectedDay() { - return mSelectedDay; - } - - /** - * Set up the gesture detector and selected time - */ - protected void init() { - mSelectedDay = new SupportMonthAdapter.CalendarDay(System.currentTimeMillis(), mController.getTimeZone()); - } - - @Override - public int getCount() { - Calendar endDate = mController.getEndDate(); - Calendar startDate = mController.getStartDate(); - int endMonth = endDate.get(Calendar.YEAR) * MONTHS_IN_YEAR + endDate.get(Calendar.MONTH); - int startMonth = startDate.get(Calendar.YEAR) * MONTHS_IN_YEAR + startDate.get(Calendar.MONTH); - return endMonth - startMonth + 1; - //return ((mController.getMaxYear() - mController.getMinYear()) + 1) * MONTHS_IN_YEAR; - } - - @Override - public Object getItem(int position) { - return null; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public boolean hasStableIds() { - return true; - } - - @SuppressLint("NewApi") - @SuppressWarnings("unchecked") - @Override - public View getView(int position, View convertView, ViewGroup parent) { - SupportMonthView v; - HashMap drawingParams = null; - if (convertView != null) { - v = (SupportMonthView) convertView; - // We store the drawing parameters in the view so it can be recycled - drawingParams = (HashMap) v.getTag(); - } else { - v = createMonthView(mContext); - // Set up the new view - AbsListView.LayoutParams params = new AbsListView.LayoutParams( - AbsListView.LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.MATCH_PARENT); - v.setLayoutParams(params); - v.setClickable(true); - v.setOnDayClickListener(this); - } - if (drawingParams == null) { - drawingParams = new HashMap<>(); - } - drawingParams.clear(); - - final int month = (position + mController.getStartDate().get(Calendar.MONTH)) % MONTHS_IN_YEAR; - final int year = (position + mController.getStartDate().get(Calendar.MONTH)) / MONTHS_IN_YEAR + mController.getMinYear(); - - int selectedDay = -1; - if (isSelectedDayInMonth(year, month)) { - selectedDay = mSelectedDay.day; - } - - // Invokes requestLayout() to ensure that the recycled view is set with the appropriate - // height/number of weeks before being displayed. - v.reuse(); - - drawingParams.put(MonthView.VIEW_PARAMS_SELECTED_DAY, selectedDay); - drawingParams.put(MonthView.VIEW_PARAMS_YEAR, year); - drawingParams.put(MonthView.VIEW_PARAMS_MONTH, month); - drawingParams.put(MonthView.VIEW_PARAMS_WEEK_START, mController.getFirstDayOfWeek()); - v.setMonthParams(drawingParams); - v.invalidate(); - return v; - } - - public abstract SupportMonthView createMonthView(Context context); - - private boolean isSelectedDayInMonth(int year, int month) { - return mSelectedDay.year == year && mSelectedDay.month == month; - } - - - @Override - public void onDayClick(SupportMonthView view, SupportMonthAdapter.CalendarDay day) { - if (day != null) { - onDayTapped(day); - } - } - - /** - * Maintains the same hour/min/sec but moves the day to the tapped day. - * - * @param day The day that was tapped - */ - protected void onDayTapped(SupportMonthAdapter.CalendarDay day) { - mController.tryVibrate(); - mController.onDayOfMonthSelected(day.year, day.month, day.day); - setSelectedDay(day); - } -} diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportMonthView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportMonthView.java deleted file mode 100644 index f1d4f18a..00000000 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportMonthView.java +++ /dev/null @@ -1,798 +0,0 @@ -package com.wdullaer.materialdatetimepicker.supportdate; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.os.Build; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.content.ContextCompat; -import android.support.v4.view.ViewCompat; -import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; -import android.support.v4.widget.ExploreByTouchHelper; -import android.text.format.DateFormat; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; - -import com.wdullaer.materialdatetimepicker.R; -import com.wdullaer.materialdatetimepicker.TypefaceHelper; -import com.wdullaer.materialdatetimepicker.date.DatePickerController; -import com.wdullaer.materialdatetimepicker.date.MonthAdapter; -import com.wdullaer.materialdatetimepicker.date.MonthView; - -import java.security.InvalidParameterException; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Formatter; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; - -/** - * Created by rmore on 06/03/2017. - */ - -public abstract class SupportMonthView extends View { - private static final String TAG = "MonthView"; - - /** - * These params can be passed into the view to control how it appears. - * {@link #VIEW_PARAMS_WEEK} is the only required field, though the default - * values are unlikely to fit most layouts correctly. - */ - /** - * This sets the height of this week in pixels - */ - public static final String VIEW_PARAMS_HEIGHT = "height"; - /** - * This specifies the position (or weeks since the epoch) of this week. - */ - public static final String VIEW_PARAMS_MONTH = "month"; - /** - * This specifies the position (or weeks since the epoch) of this week. - */ - public static final String VIEW_PARAMS_YEAR = "year"; - /** - * This sets one of the days in this view as selected {@link Calendar#SUNDAY} - * through {@link Calendar#SATURDAY}. - */ - public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day"; - /** - * Which day the week should start on. {@link Calendar#SUNDAY} through - * {@link Calendar#SATURDAY}. - */ - public static final String VIEW_PARAMS_WEEK_START = "week_start"; - /** - * How many days to display at a time. Days will be displayed starting with - * {@link #mWeekStart}. - */ - public static final String VIEW_PARAMS_NUM_DAYS = "num_days"; - /** - * Which month is currently in focus, as defined by {@link Calendar#MONTH} - * [0-11]. - */ - public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month"; - /** - * If this month should display week numbers. false if 0, true otherwise. - */ - public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num"; - - protected static int DEFAULT_HEIGHT = 32; - protected static int MIN_HEIGHT = 10; - protected static final int DEFAULT_SELECTED_DAY = -1; - protected static final int DEFAULT_WEEK_START = Calendar.SUNDAY; - protected static final int DEFAULT_NUM_DAYS = 7; - protected static final int DEFAULT_SHOW_WK_NUM = 0; - protected static final int DEFAULT_FOCUS_MONTH = -1; - protected static final int DEFAULT_NUM_ROWS = 6; - protected static final int MAX_NUM_ROWS = 6; - - private static final int SELECTED_CIRCLE_ALPHA = 255; - - protected static int DAY_SEPARATOR_WIDTH = 1; - protected static int MINI_DAY_NUMBER_TEXT_SIZE; - protected static int MONTH_LABEL_TEXT_SIZE; - protected static int MONTH_DAY_LABEL_TEXT_SIZE; - protected static int MONTH_HEADER_SIZE; - protected static int DAY_SELECTED_CIRCLE_SIZE; - - // used for scaling to the device density - protected static float mScale = 0; - - protected SupportDatePickerController mController; - - // affects the padding on the sides of this view - protected int mEdgePadding = 0; - - private String mDayOfWeekTypeface; - private String mMonthTitleTypeface; - - protected Paint mMonthNumPaint; - protected Paint mMonthTitlePaint; - protected Paint mSelectedCirclePaint; - protected Paint mMonthDayLabelPaint; - - private final Formatter mFormatter; - private final StringBuilder mStringBuilder; - - // The Julian day of the first day displayed by this item - protected int mFirstJulianDay = -1; - // The month of the first day in this week - protected int mFirstMonth = -1; - // The month of the last day in this week - protected int mLastMonth = -1; - - protected int mMonth; - - protected int mYear; - // Quick reference to the width of this view, matches parent - protected int mWidth; - // The height this view should draw at in pixels, set by height param - protected int mRowHeight = DEFAULT_HEIGHT; - // If this view contains the today - protected boolean mHasToday = false; - // Which day is selected [0-6] or -1 if no day is selected - protected int mSelectedDay = -1; - // Which day is today [0-6] or -1 if no day is today - protected int mToday = DEFAULT_SELECTED_DAY; - // Which day of the week to start on [0-6] - protected int mWeekStart = DEFAULT_WEEK_START; - // How many days to display - protected int mNumDays = DEFAULT_NUM_DAYS; - // The number of days + a spot for week number if it is displayed - protected int mNumCells = mNumDays; - // The left edge of the selected day - protected int mSelectedLeft = -1; - // The right edge of the selected day - protected int mSelectedRight = -1; - - private final Calendar mCalendar; - protected final Calendar mDayLabelCalendar; - private final SupportMonthView.MonthViewTouchHelper mTouchHelper; - - protected int mNumRows = DEFAULT_NUM_ROWS; - - // Optional listener for handling day click actions - protected SupportMonthView.OnDayClickListener mOnDayClickListener; - - // Whether to prevent setting the accessibility delegate - private boolean mLockAccessibilityDelegate; - - protected int mDayTextColor; - protected int mSelectedDayTextColor; - protected int mMonthDayTextColor; - protected int mTodayNumberColor; - protected int mHighlightedDayTextColor; - protected int mDisabledDayTextColor; - protected int mMonthTitleColor; - - public SupportMonthView(Context context) { - this(context, null, null); - } - - public SupportMonthView(Context context, AttributeSet attr, SupportDatePickerController controller) { - super(context, attr); - mController = controller; - Resources res = context.getResources(); - - mDayLabelCalendar = Calendar.getInstance(mController.getTimeZone()); - mCalendar = Calendar.getInstance(mController.getTimeZone()); - - mDayOfWeekTypeface = res.getString(R.string.mdtp_day_of_week_label_typeface); - mMonthTitleTypeface = res.getString(R.string.mdtp_sans_serif); - - boolean darkTheme = mController != null && mController.isThemeDark(); - if(darkTheme) { - mDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_normal_dark_theme); - mMonthDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_month_day_dark_theme); - mDisabledDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_disabled_dark_theme); - mHighlightedDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_highlighted_dark_theme); - } - else { - mDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_normal); - mMonthDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_month_day); - mDisabledDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_disabled); - mHighlightedDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_highlighted); - } - mSelectedDayTextColor = ContextCompat.getColor(context, R.color.mdtp_white); - mTodayNumberColor = mController.getAccentColor(); - mMonthTitleColor = ContextCompat.getColor(context, R.color.mdtp_white); - - mStringBuilder = new StringBuilder(50); - mFormatter = new Formatter(mStringBuilder, Locale.getDefault()); - - MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_day_number_size); - MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_month_label_size); - MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_month_day_label_text_size); - MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.mdtp_month_list_item_header_height); - DAY_SELECTED_CIRCLE_SIZE = res - .getDimensionPixelSize(R.dimen.mdtp_day_number_select_circle_radius); - - mRowHeight = (res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height) - - getMonthHeaderSize()) / MAX_NUM_ROWS; - - // Set up accessibility components. - mTouchHelper = getMonthViewTouchHelper(); - ViewCompat.setAccessibilityDelegate(this, mTouchHelper); - ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); - mLockAccessibilityDelegate = true; - - // Sets up any standard paints that will be used - initView(); - } - - public void setDatePickerController(SupportDatePickerController controller) { - mController = controller; - } - - protected SupportMonthView.MonthViewTouchHelper getMonthViewTouchHelper() { - return new SupportMonthView.MonthViewTouchHelper(this); - } - - @Override - public void setAccessibilityDelegate(AccessibilityDelegate delegate) { - // Workaround for a JB MR1 issue where accessibility delegates on - // top-level ListView items are overwritten. - if (!mLockAccessibilityDelegate) { - super.setAccessibilityDelegate(delegate); - } - } - - public void setOnDayClickListener(SupportMonthView.OnDayClickListener listener) { - mOnDayClickListener = listener; - } - - @Override - public boolean dispatchHoverEvent(@NonNull MotionEvent event) { - // First right-of-refusal goes the touch exploration helper. - if (mTouchHelper.dispatchHoverEvent(event)) { - return true; - } - return super.dispatchHoverEvent(event); - } - - @Override - public boolean onTouchEvent(@NonNull MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_UP: - final int day = getDayFromLocation(event.getX(), event.getY()); - if (day >= 0) { - onDayClick(day); - } - break; - } - return true; - } - - /** - * Sets up the text and style properties for painting. Override this if you - * want to use a different paint. - */ - protected void initView() { - mMonthTitlePaint = new Paint(); - mMonthTitlePaint.setFakeBoldText(true); - mMonthTitlePaint.setAntiAlias(true); - mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE); - mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD)); - mMonthTitlePaint.setColor(mDayTextColor); - mMonthTitlePaint.setTextAlign(Paint.Align.CENTER); - mMonthTitlePaint.setStyle(Paint.Style.FILL); - - mSelectedCirclePaint = new Paint(); - mSelectedCirclePaint.setFakeBoldText(true); - mSelectedCirclePaint.setAntiAlias(true); - mSelectedCirclePaint.setColor(mTodayNumberColor); - mSelectedCirclePaint.setTextAlign(Paint.Align.CENTER); - mSelectedCirclePaint.setStyle(Paint.Style.FILL); - mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA); - - mMonthDayLabelPaint = new Paint(); - mMonthDayLabelPaint.setAntiAlias(true); - mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE); - mMonthDayLabelPaint.setColor(mMonthDayTextColor); - mMonthDayLabelPaint.setTypeface(TypefaceHelper.get(getContext(),"Roboto-Medium")); - mMonthDayLabelPaint.setStyle(Paint.Style.FILL); - mMonthDayLabelPaint.setTextAlign(Paint.Align.CENTER); - mMonthDayLabelPaint.setFakeBoldText(true); - - mMonthNumPaint = new Paint(); - mMonthNumPaint.setAntiAlias(true); - mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE); - mMonthNumPaint.setStyle(Paint.Style.FILL); - mMonthNumPaint.setTextAlign(Paint.Align.CENTER); - mMonthNumPaint.setFakeBoldText(false); - } - - @Override - protected void onDraw(Canvas canvas) { - drawMonthTitle(canvas); - drawMonthDayLabels(canvas); - drawMonthNums(canvas); - } - - private int mDayOfWeekStart = 0; - - /** - * Sets all the parameters for displaying this week. The only required - * parameter is the week number. Other parameters have a default value and - * will only update if a new value is included, except for focus month, - * which will always default to no focus month if no value is passed in. See - * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters. - * - * @param params A map of the new parameters, see - * {@link #VIEW_PARAMS_HEIGHT} - */ - public void setMonthParams(HashMap params) { - if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) { - throw new InvalidParameterException("You must specify month and year for this view"); - } - setTag(params); - // We keep the current value for any params not present - if (params.containsKey(VIEW_PARAMS_HEIGHT)) { - mRowHeight = params.get(VIEW_PARAMS_HEIGHT); - if (mRowHeight < MIN_HEIGHT) { - mRowHeight = MIN_HEIGHT; - } - } - if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) { - mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY); - } - - // Allocate space for caching the day numbers and focus values - mMonth = params.get(VIEW_PARAMS_MONTH); - mYear = params.get(VIEW_PARAMS_YEAR); - - // Figure out what day today is - //final Time today = new Time(Time.getCurrentTimezone()); - //today.setToNow(); - final Calendar today = Calendar.getInstance(mController.getTimeZone()); - mHasToday = false; - mToday = -1; - - mCalendar.set(Calendar.MONTH, mMonth); - mCalendar.set(Calendar.YEAR, mYear); - mCalendar.set(Calendar.DAY_OF_MONTH, 1); - mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK); - - if (params.containsKey(VIEW_PARAMS_WEEK_START)) { - mWeekStart = params.get(VIEW_PARAMS_WEEK_START); - } else { - mWeekStart = mCalendar.getFirstDayOfWeek(); - } - - mNumCells = mCalendar.getActualMaximum(Calendar.DAY_OF_MONTH); - for (int i = 0; i < mNumCells; i++) { - final int day = i + 1; - if (sameDay(day, today)) { - mHasToday = true; - mToday = day; - } - } - mNumRows = calculateNumRows(); - - // Invalidate cached accessibility information. - mTouchHelper.invalidateRoot(); - } - - public void setSelectedDay(int day) { - mSelectedDay = day; - } - - public void reuse() { - mNumRows = DEFAULT_NUM_ROWS; - requestLayout(); - } - - private int calculateNumRows() { - int offset = findDayOffset(); - int dividend = (offset + mNumCells) / mNumDays; - int remainder = (offset + mNumCells) % mNumDays; - return (dividend + (remainder > 0 ? 1 : 0)); - } - - private boolean sameDay(int day, Calendar today) { - return mYear == today.get(Calendar.YEAR) && - mMonth == today.get(Calendar.MONTH) && - day == today.get(Calendar.DAY_OF_MONTH); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows - + getMonthHeaderSize() + 5); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - mWidth = w; - - // Invalidate cached accessibility information. - mTouchHelper.invalidateRoot(); - } - - public int getMonth() { - return mMonth; - } - - public int getYear() { - return mYear; - } - - /** - * A wrapper to the MonthHeaderSize to allow override it in children - */ - protected int getMonthHeaderSize() { - return MONTH_HEADER_SIZE; - } - - @NonNull - private String getMonthAndYearString() { - Locale locale = Locale.getDefault(); - String pattern = "MMMM yyyy"; - - if(Build.VERSION.SDK_INT < 18) pattern = getContext().getResources().getString(R.string.mdtp_date_v1_monthyear); - else pattern = DateFormat.getBestDateTimePattern(locale, pattern); - - SimpleDateFormat formatter = new SimpleDateFormat(pattern, locale); - formatter.setTimeZone(mController.getTimeZone()); - formatter.applyLocalizedPattern(pattern); - mStringBuilder.setLength(0); - return formatter.format(mCalendar.getTime()); - } - - protected void drawMonthTitle(Canvas canvas) { - int x = (mWidth + 2 * mEdgePadding) / 2; - int y = (getMonthHeaderSize() - MONTH_DAY_LABEL_TEXT_SIZE) / 2; - canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint); - } - - protected void drawMonthDayLabels(Canvas canvas) { - int y = getMonthHeaderSize() - (MONTH_DAY_LABEL_TEXT_SIZE / 2); - int dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2); - - for (int i = 0; i < mNumDays; i++) { - int x = (2 * i + 1) * dayWidthHalf + mEdgePadding; - - int calendarDay = (i + mWeekStart) % mNumDays; - mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay); - String weekString = getWeekDayLabel(mDayLabelCalendar); - canvas.drawText(weekString, x, y, mMonthDayLabelPaint); - } - } - - /** - * Draws the week and month day numbers for this week. Override this method - * if you need different placement. - * - * @param canvas The canvas to draw on - */ - protected void drawMonthNums(Canvas canvas) { - int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH) - + getMonthHeaderSize(); - final float dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2.0f); - int j = findDayOffset(); - for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) { - final int x = (int)((2 * j + 1) * dayWidthHalf + mEdgePadding); - - int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH; - - final int startX = (int)(x - dayWidthHalf); - final int stopX = (int)(x + dayWidthHalf); - final int startY = (int)(y - yRelativeToDay); - final int stopY = (int)(startY + mRowHeight); - - drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY); - - j++; - if (j == mNumDays) { - j = 0; - y += mRowHeight; - } - } - } - - /** - * This method should draw the month day. Implemented by sub-classes to allow customization. - * - * @param canvas The canvas to draw on - * @param year The year of this month day - * @param month The month of this month day - * @param day The day number of this month day - * @param x The default x position to draw the day number - * @param y The default y position to draw the day number - * @param startX The left boundary of the day number rect - * @param stopX The right boundary of the day number rect - * @param startY The top boundary of the day number rect - * @param stopY The bottom boundary of the day number rect - */ - public abstract void drawMonthDay(Canvas canvas, int year, int month, int day, - int x, int y, int startX, int stopX, int startY, int stopY); - - protected int findDayOffset() { - return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart) - - mWeekStart; - } - - - /** - * Calculates the day that the given x position is in, accounting for week - * number. Returns the day or -1 if the position wasn't in a day. - * - * @param x The x position of the touch event - * @return The day number, or -1 if the position wasn't in a day - */ - public int getDayFromLocation(float x, float y) { - final int day = getInternalDayFromLocation(x, y); - if (day < 1 || day > mNumCells) { - return -1; - } - return day; - } - - /** - * Calculates the day that the given x position is in, accounting for week - * number. - * - * @param x The x position of the touch event - * @return The day number - */ - protected int getInternalDayFromLocation(float x, float y) { - int dayStart = mEdgePadding; - if (x < dayStart || x > mWidth - mEdgePadding) { - return -1; - } - // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels - int row = (int) (y - getMonthHeaderSize()) / mRowHeight; - int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mEdgePadding)); - - int day = column - findDayOffset() + 1; - day += row * mNumDays; - return day; - } - - /** - * Called when the user clicks on a day. Handles callbacks to the - * {@link MonthView.OnDayClickListener} if one is set. - *

- * If the day is out of the range set by minDate and/or maxDate, this is a no-op. - * - * @param day The day that was clicked - */ - private void onDayClick(int day) { - // If the min / max date are set, only process the click if it's a valid selection. - if (mController.isOutOfRange(mYear, mMonth, day)) { - return; - } - - - if (mOnDayClickListener != null) { - mOnDayClickListener.onDayClick(this, new SupportMonthAdapter.CalendarDay(mYear, mMonth, day)); - } - - // This is a no-op if accessibility is turned off. - mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED); - } - - /** - * @param year - * @param month - * @param day - * @return true if the given date should be highlighted - */ - protected boolean isHighlighted(int year, int month, int day) { - return mController.isHighlighted(year, month, day); - } - - /** - * Return a 1 or 2 letter String for use as a weekday label - * @param day The day for which to generate a label - * @return The weekday label - */ - private String getWeekDayLabel(Calendar day) { - Locale locale = Locale.getDefault(); - - // Localised short version of the string is not available on API < 18 - if(Build.VERSION.SDK_INT < 18) { - String dayName = new SimpleDateFormat("E", locale).format(day.getTime()); - String dayLabel = dayName.toUpperCase(locale).substring(0, 1); - - // Chinese labels should be fetched right to left - if (locale.equals(Locale.CHINA) || locale.equals(Locale.CHINESE) || locale.equals(Locale.SIMPLIFIED_CHINESE) || locale.equals(Locale.TRADITIONAL_CHINESE)) { - int len = dayName.length(); - dayLabel = dayName.substring(len -1, len); - } - - // Most hebrew labels should select the second to last character - if (locale.getLanguage().equals("he") || locale.getLanguage().equals("iw")) { - if(mDayLabelCalendar.get(Calendar.DAY_OF_WEEK) != Calendar.SATURDAY) { - int len = dayName.length(); - dayLabel = dayName.substring(len - 2, len - 1); - } - else { - // I know this is duplication, but it makes the code easier to grok by - // having all hebrew code in the same block - dayLabel = dayName.toUpperCase(locale).substring(0, 1); - } - } - - // Catalan labels should be two digits in lowercase - if (locale.getLanguage().equals("ca")) - dayLabel = dayName.toLowerCase().substring(0,2); - - // Correct single character label in Spanish is X - if (locale.getLanguage().equals("es") && day.get(Calendar.DAY_OF_WEEK) == Calendar.WEDNESDAY) - dayLabel = "X"; - - return dayLabel; - } - // Getting the short label is a one liner on API >= 18 - return new SimpleDateFormat("EEEEE", locale).format(day.getTime()); - } - - /** - * @return The date that has accessibility focus, or {@code null} if no date - * has focus - */ - public MonthAdapter.CalendarDay getAccessibilityFocus() { - final int day = mTouchHelper.getFocusedVirtualView(); - if (day >= 0) { - return new MonthAdapter.CalendarDay(mYear, mMonth, day); - } - return null; - } - - /** - * Clears accessibility focus within the view. No-op if the view does not - * contain accessibility focus. - */ - public void clearAccessibilityFocus() { - mTouchHelper.clearFocusedVirtualView(); - } - - /** - * Attempts to restore accessibility focus to the specified date. - * - * @param day The date which should receive focus - * @return {@code false} if the date is not valid for this month view, or - * {@code true} if the date received focus - */ - public boolean restoreAccessibilityFocus(SupportMonthAdapter.CalendarDay day) { - if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) { - return false; - } - mTouchHelper.setFocusedVirtualView(day.day); - return true; - } - - /** - * Provides a virtual view hierarchy for interfacing with an accessibility - * service. - */ - protected class MonthViewTouchHelper extends ExploreByTouchHelper { - private static final String DATE_FORMAT = "dd MMMM yyyy"; - - private final Rect mTempRect = new Rect(); - private final Calendar mTempCalendar = Calendar.getInstance(mController.getTimeZone()); - - public MonthViewTouchHelper(View host) { - super(host); - } - - public void setFocusedVirtualView(int virtualViewId) { - getAccessibilityNodeProvider(SupportMonthView.this).performAction( - virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); - } - - public void clearFocusedVirtualView() { - final int focusedVirtualView = getFocusedVirtualView(); - if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) { - getAccessibilityNodeProvider(SupportMonthView.this).performAction( - focusedVirtualView, - AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS, - null); - } - } - - @Override - protected int getVirtualViewAt(float x, float y) { - final int day = getDayFromLocation(x, y); - if (day >= 0) { - return day; - } - return ExploreByTouchHelper.INVALID_ID; - } - - @Override - protected void getVisibleVirtualViews(List virtualViewIds) { - for (int day = 1; day <= mNumCells; day++) { - virtualViewIds.add(day); - } - } - - @Override - protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { - event.setContentDescription(getItemDescription(virtualViewId)); - } - - @Override - protected void onPopulateNodeForVirtualView(int virtualViewId, - AccessibilityNodeInfoCompat node) { - getItemBounds(virtualViewId, mTempRect); - - node.setContentDescription(getItemDescription(virtualViewId)); - node.setBoundsInParent(mTempRect); - node.addAction(AccessibilityNodeInfo.ACTION_CLICK); - - if (virtualViewId == mSelectedDay) { - node.setSelected(true); - } - - } - - @Override - protected boolean onPerformActionForVirtualView(int virtualViewId, int action, - Bundle arguments) { - switch (action) { - case AccessibilityNodeInfo.ACTION_CLICK: - onDayClick(virtualViewId); - return true; - } - - return false; - } - - /** - * Calculates the bounding rectangle of a given time object. - * - * @param day The day to calculate bounds for - * @param rect The rectangle in which to store the bounds - */ - protected void getItemBounds(int day, Rect rect) { - final int offsetX = mEdgePadding; - final int offsetY = getMonthHeaderSize(); - final int cellHeight = mRowHeight; - final int cellWidth = ((mWidth - (2 * mEdgePadding)) / mNumDays); - final int index = ((day - 1) + findDayOffset()); - final int row = (index / mNumDays); - final int column = (index % mNumDays); - final int x = (offsetX + (column * cellWidth)); - final int y = (offsetY + (row * cellHeight)); - - rect.set(x, y, (x + cellWidth), (y + cellHeight)); - } - - /** - * Generates a description for a given time object. Since this - * description will be spoken, the components are ordered by descending - * specificity as DAY MONTH YEAR. - * - * @param day The day to generate a description for - * @return A description of the time object - */ - protected CharSequence getItemDescription(int day) { - mTempCalendar.set(mYear, mMonth, day); - final CharSequence date = DateFormat.format(DATE_FORMAT, - mTempCalendar.getTimeInMillis()); - - if (day == mSelectedDay) { - return getContext().getString(R.string.mdtp_item_is_selected, date); - } - - return date; - } - } - - /** - * Handles callbacks when the user clicks on a time object. - */ - public interface OnDayClickListener { - void onDayClick(SupportMonthView view, SupportMonthAdapter.CalendarDay day); - } -} - diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportSimpleDayPickerView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportSimpleDayPickerView.java deleted file mode 100644 index d4d170f4..00000000 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportSimpleDayPickerView.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.wdullaer.materialdatetimepicker.supportdate; - -import android.content.Context; -import android.util.AttributeSet; - -/** - * Created by rmore on 06/03/2017. - */ - -public class SupportSimpleDayPickerView extends SupportDayPickerView { - - public SupportSimpleDayPickerView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public SupportSimpleDayPickerView(Context context, SupportDatePickerController controller) { - super(context, controller); - } - - @Override - public SupportMonthAdapter createMonthAdapter(Context context, SupportDatePickerController controller) { - return new SupportSimpleMonthAdapter(context, controller); - } -} diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportSimpleMonthAdapter.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportSimpleMonthAdapter.java deleted file mode 100644 index 13855144..00000000 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportSimpleMonthAdapter.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.wdullaer.materialdatetimepicker.supportdate; - -import android.content.Context; - -/** - * Created by rmore on 06/03/2017. - */ - -public class SupportSimpleMonthAdapter extends SupportMonthAdapter { - - public SupportSimpleMonthAdapter(Context context, SupportDatePickerController controller) { - super(context, controller); - } - - @Override - public SupportMonthView createMonthView(Context context) { - final SupportMonthView monthView = new SupportSimpleMonthView(context, null, mController); - return monthView; - } -} diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportSimpleMonthView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportSimpleMonthView.java deleted file mode 100644 index cb14aa2e..00000000 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportSimpleMonthView.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.wdullaer.materialdatetimepicker.supportdate; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Typeface; -import android.util.AttributeSet; - -import com.wdullaer.materialdatetimepicker.date.DatePickerController; -import com.wdullaer.materialdatetimepicker.date.MonthView; - -/** - * Created by rmore on 06/03/2017. - */ - -public class SupportSimpleMonthView extends SupportMonthView { - - public SupportSimpleMonthView(Context context, AttributeSet attr, SupportDatePickerController controller) { - super(context, attr, controller); - } - - @Override - public void drawMonthDay(Canvas canvas, int year, int month, int day, - int x, int y, int startX, int stopX, int startY, int stopY) { - if (mSelectedDay == day) { - canvas.drawCircle(x , y - (MINI_DAY_NUMBER_TEXT_SIZE / 3), DAY_SELECTED_CIRCLE_SIZE, - mSelectedCirclePaint); - } - - if(isHighlighted(year, month, day)) { - mMonthNumPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); - } - else { - mMonthNumPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)); - } - - // If we have a mindate or maxdate, gray out the day number if it's outside the range. - if (mController.isOutOfRange(year, month, day)) { - mMonthNumPaint.setColor(mDisabledDayTextColor); - } - else if (mSelectedDay == day) { - mMonthNumPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); - mMonthNumPaint.setColor(mSelectedDayTextColor); - } else if (mHasToday && mToday == day) { - mMonthNumPaint.setColor(mTodayNumberColor); - } else { - mMonthNumPaint.setColor(isHighlighted(year, month, day) ? mHighlightedDayTextColor : mDayTextColor); - } - - canvas.drawText(String.format("%d", day), x, y, mMonthNumPaint); - } -} - diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportYearPickerView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportYearPickerView.java deleted file mode 100644 index e9b697d9..00000000 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/supportdate/SupportYearPickerView.java +++ /dev/null @@ -1,164 +0,0 @@ -package com.wdullaer.materialdatetimepicker.supportdate; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.drawable.StateListDrawable; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.ListView; -import android.widget.TextView; - -import com.wdullaer.materialdatetimepicker.R; -import com.wdullaer.materialdatetimepicker.date.TextViewWithCircularIndicator; - -/** - * Created by rmore on 06/03/2017. - */ - -public class SupportYearPickerView extends ListView implements AdapterView.OnItemClickListener, SupportDatePickerDialog.OnDateChangedListener { - private static final String TAG = "YearPickerView"; - - private final SupportDatePickerController mController; - private YearAdapter mAdapter; - private int mViewSize; - private int mChildSize; - private TextViewWithCircularIndicator mSelectedView; - - public SupportYearPickerView(Context context, SupportDatePickerController controller) { - super(context); - mController = controller; - mController.registerOnDateChangedListener(this); - ViewGroup.LayoutParams frame = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.WRAP_CONTENT); - setLayoutParams(frame); - Resources res = context.getResources(); - mViewSize = res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height); - mChildSize = res.getDimensionPixelOffset(R.dimen.mdtp_year_label_height); - setVerticalFadingEdgeEnabled(true); - setFadingEdgeLength(mChildSize / 3); - init(); - setOnItemClickListener(this); - setSelector(new StateListDrawable()); - setDividerHeight(0); - onDateChanged(); - } - - private void init() { - mAdapter = new YearAdapter(mController.getMinYear(), mController.getMaxYear()); - setAdapter(mAdapter); - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - mController.tryVibrate(); - TextViewWithCircularIndicator clickedView = (TextViewWithCircularIndicator) view; - if (clickedView != null) { - if (clickedView != mSelectedView) { - if (mSelectedView != null) { - mSelectedView.drawIndicator(false); - mSelectedView.requestLayout(); - } - clickedView.drawIndicator(true); - clickedView.requestLayout(); - mSelectedView = clickedView; - } - mController.onYearSelected(getYearFromTextView(clickedView)); - mAdapter.notifyDataSetChanged(); - } - } - - private static int getYearFromTextView(TextView view) { - return Integer.valueOf(view.getText().toString()); - } - - private final class YearAdapter extends BaseAdapter { - private final int mMinYear; - private final int mMaxYear; - - YearAdapter(int minYear, int maxYear) { - if (minYear > maxYear) { - throw new IllegalArgumentException("minYear > maxYear"); - } - mMinYear = minYear; - mMaxYear = maxYear; - } - - @Override - public int getCount() { - return mMaxYear - mMinYear + 1; - } - - @Override - public Object getItem(int position) { - return mMinYear + position; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - TextViewWithCircularIndicator v; - if (convertView != null) { - v = (TextViewWithCircularIndicator) convertView; - } else { - v = (TextViewWithCircularIndicator) LayoutInflater.from(parent.getContext()) - .inflate(R.layout.mdtp_year_label_text_view, parent, false); - v.setAccentColor(mController.getAccentColor(), mController.isThemeDark()); - } - int year = mMinYear + position; - boolean selected = mController.getSelectedDay().year == year; - v.setText(String.valueOf(year)); - v.drawIndicator(selected); - v.requestLayout(); - if (selected) { - mSelectedView = v; - } - return v; - } - } - - public void postSetSelectionCentered(final int position) { - postSetSelectionFromTop(position, mViewSize / 2 - mChildSize / 2); - } - - public void postSetSelectionFromTop(final int position, final int offset) { - post(new Runnable() { - - @Override - public void run() { - setSelectionFromTop(position, offset); - requestLayout(); - } - }); - } - - public int getFirstPositionOffset() { - final View firstChild = getChildAt(0); - if (firstChild == null) { - return 0; - } - return firstChild.getTop(); - } - - @Override - public void onDateChanged() { - mAdapter.notifyDataSetChanged(); - postSetSelectionCentered(mController.getSelectedDay().year - mController.getMinYear()); - } - - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { - event.setFromIndex(0); - event.setToIndex(0); - } - } -} diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/AmPmCirclesView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/AmPmCirclesView.java index 9682060d..0de684f6 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/AmPmCirclesView.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/AmPmCirclesView.java @@ -30,6 +30,7 @@ import com.wdullaer.materialdatetimepicker.Utils; import java.text.DateFormatSymbols; +import java.util.Locale; /** * Draw the two smaller AM and PM circles next to where the larger circle will be. @@ -73,7 +74,7 @@ public AmPmCirclesView(Context context) { mIsInitialized = false; } - public void initialize(Context context, TimePickerController controller, int amOrPm) { + public void initialize(Context context, Locale locale, TimePickerController controller, int amOrPm) { if (mIsInitialized) { Log.e(TAG, "AmPmCirclesView may only be initialized once."); return; @@ -107,7 +108,7 @@ public void initialize(Context context, TimePickerController controller, int amO Float.parseFloat(res.getString(R.string.mdtp_circle_radius_multiplier)); mAmPmCircleRadiusMultiplier = Float.parseFloat(res.getString(R.string.mdtp_ampm_circle_radius_multiplier)); - String[] amPmTexts = new DateFormatSymbols().getAmPmStrings(); + String[] amPmTexts = new DateFormatSymbols(locale).getAmPmStrings(); mAmText = amPmTexts[0]; mPmText = amPmTexts[1]; diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/DefaultTimepointLimiter.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/DefaultTimepointLimiter.java new file mode 100644 index 00000000..21d1f514 --- /dev/null +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/DefaultTimepointLimiter.java @@ -0,0 +1,306 @@ +package com.wdullaer.materialdatetimepicker.time; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.Arrays; +import java.util.TreeSet; + +import static com.wdullaer.materialdatetimepicker.time.TimePickerDialog.HOUR_INDEX; +import static com.wdullaer.materialdatetimepicker.time.TimePickerDialog.MINUTE_INDEX; + +/** + * An implementation of TimepointLimiter which implements the most common ways to restrict Timepoints + * in a TimePickerDialog + * Created by wdullaer on 20/06/17. + */ + +class DefaultTimepointLimiter implements TimepointLimiter { + private TreeSet mSelectableTimes = new TreeSet<>(); + private TreeSet mDisabledTimes = new TreeSet<>(); + private TreeSet exclusiveSelectableTimes = new TreeSet<>(); + private Timepoint mMinTime; + private Timepoint mMaxTime; + + DefaultTimepointLimiter() {} + + @SuppressWarnings("WeakerAccess") + public DefaultTimepointLimiter(Parcel in) { + mMinTime = in.readParcelable(Timepoint.class.getClassLoader()); + mMaxTime = in.readParcelable(Timepoint.class.getClassLoader()); + mSelectableTimes.addAll(Arrays.asList(in.createTypedArray(Timepoint.CREATOR))); + mDisabledTimes.addAll(Arrays.asList(in.createTypedArray(Timepoint.CREATOR))); + exclusiveSelectableTimes = getExclusiveSelectableTimes(mSelectableTimes, mDisabledTimes); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeParcelable(mMinTime, flags); + out.writeParcelable(mMaxTime, flags); + out.writeTypedArray(mSelectableTimes.toArray(new Timepoint[mSelectableTimes.size()]), flags); + out.writeTypedArray(mDisabledTimes.toArray(new Timepoint[mDisabledTimes.size()]), flags); + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("WeakerAccess") + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public DefaultTimepointLimiter createFromParcel(Parcel in) { + return new DefaultTimepointLimiter(in); + } + + public DefaultTimepointLimiter[] newArray(int size) { + return new DefaultTimepointLimiter[size]; + } + }; + + void setMinTime(@NonNull Timepoint minTime) { + if(mMaxTime != null && minTime.compareTo(mMaxTime) > 0) + throw new IllegalArgumentException("Minimum time must be smaller than the maximum time"); + mMinTime = minTime; + } + + void setMaxTime(@NonNull Timepoint maxTime) { + if(mMinTime != null && maxTime.compareTo(mMinTime) < 0) + throw new IllegalArgumentException("Maximum time must be greater than the minimum time"); + mMaxTime = maxTime; + } + + void setSelectableTimes(@NonNull Timepoint[] selectableTimes) { + mSelectableTimes.addAll(Arrays.asList(selectableTimes)); + exclusiveSelectableTimes = getExclusiveSelectableTimes(mSelectableTimes, mDisabledTimes); + } + + void setDisabledTimes(@NonNull Timepoint[] disabledTimes) { + mDisabledTimes.addAll(Arrays.asList(disabledTimes)); + exclusiveSelectableTimes = getExclusiveSelectableTimes(mSelectableTimes, mDisabledTimes); + } + + @Nullable Timepoint getMinTime() { + return mMinTime; + } + + @Nullable Timepoint getMaxTime() { + return mMaxTime; + } + + @NonNull Timepoint[] getSelectableTimes() { + return mSelectableTimes.toArray(new Timepoint[mSelectableTimes.size()]); + } + + @NonNull Timepoint[] getDisabledTimes() { + return mDisabledTimes.toArray(new Timepoint[mDisabledTimes.size()]); + } + + private TreeSet getExclusiveSelectableTimes(TreeSet selectable, TreeSet disabled) { + TreeSet output = new TreeSet<>(selectable); + output.removeAll(disabled); + return output; + } + + @Override + public boolean isOutOfRange(@Nullable Timepoint current, int index, @NonNull Timepoint.TYPE resolution) { + if (current == null) return false; + + if (index == HOUR_INDEX) { + if (mMinTime != null && mMinTime.getHour() > current.getHour()) return true; + + if (mMaxTime != null && mMaxTime.getHour()+1 <= current.getHour()) return true; + + if (!exclusiveSelectableTimes.isEmpty()) { + Timepoint ceil = exclusiveSelectableTimes.ceiling(current); + Timepoint floor = exclusiveSelectableTimes.floor(current); + return !(current.equals(ceil, Timepoint.TYPE.HOUR) || current.equals(floor, Timepoint.TYPE.HOUR)); + } + + if (!mDisabledTimes.isEmpty() && resolution == Timepoint.TYPE.HOUR) { + Timepoint ceil = mDisabledTimes.ceiling(current); + Timepoint floor = mDisabledTimes.floor(current); + return current.equals(ceil, Timepoint.TYPE.HOUR) || current.equals(floor, Timepoint.TYPE.HOUR); + } + + return false; + } + else if (index == MINUTE_INDEX) { + if (mMinTime != null) { + Timepoint roundedMin = new Timepoint(mMinTime.getHour(), mMinTime.getMinute()); + if (roundedMin.compareTo(current) > 0) return true; + } + + if (mMaxTime != null) { + Timepoint roundedMax = new Timepoint(mMaxTime.getHour(), mMaxTime.getMinute(), 59); + if (roundedMax.compareTo(current) < 0) return true; + } + + if (!exclusiveSelectableTimes.isEmpty()) { + Timepoint ceil = exclusiveSelectableTimes.ceiling(current); + Timepoint floor = exclusiveSelectableTimes.floor(current); + return !(current.equals(ceil, Timepoint.TYPE.MINUTE) || current.equals(floor, Timepoint.TYPE.MINUTE)); + } + + if (!mDisabledTimes.isEmpty() && resolution == Timepoint.TYPE.MINUTE) { + Timepoint ceil = mDisabledTimes.ceiling(current); + Timepoint floor = mDisabledTimes.floor(current); + boolean ceilExclude = current.equals(ceil, Timepoint.TYPE.MINUTE); + boolean floorExclude = current.equals(floor, Timepoint.TYPE.MINUTE); + return ceilExclude || floorExclude; + } + + return false; + } + else return isOutOfRange(current); + } + + public boolean isOutOfRange(@NonNull Timepoint current) { + if (mMinTime != null && mMinTime.compareTo(current) > 0) return true; + + if (mMaxTime != null && mMaxTime.compareTo(current) < 0) return true; + + if (!exclusiveSelectableTimes.isEmpty()) return !exclusiveSelectableTimes.contains(current); + + return mDisabledTimes.contains(current); + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean isAmDisabled() { + Timepoint midday = new Timepoint(12); + + if (mMinTime != null && mMinTime.compareTo(midday) >= 0) return true; + + if (!exclusiveSelectableTimes.isEmpty()) return exclusiveSelectableTimes.first().compareTo(midday) >= 0; + + return false; + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean isPmDisabled() { + Timepoint midday = new Timepoint(12); + + if (mMaxTime != null && mMaxTime.compareTo(midday) < 0) return true; + + if (!exclusiveSelectableTimes.isEmpty()) return exclusiveSelectableTimes.last().compareTo(midday) < 0; + + return false; + } + + @Override + public @NonNull Timepoint roundToNearest(@NonNull Timepoint time,@Nullable Timepoint.TYPE type, @NonNull Timepoint.TYPE resolution) { + if (mMinTime != null && mMinTime.compareTo(time) > 0) return mMinTime; + + if (mMaxTime != null && mMaxTime.compareTo(time) < 0) return mMaxTime; + + // type == SECOND: cannot change anything, return input + if (type == Timepoint.TYPE.SECOND) return time; + + if (!exclusiveSelectableTimes.isEmpty()) { + Timepoint floor = exclusiveSelectableTimes.floor(time); + Timepoint ceil = exclusiveSelectableTimes.ceiling(time); + + if (floor == null || ceil == null) { + Timepoint t = floor == null ? ceil : floor; + if (type == null) return t; + if (t.getHour() != time.getHour()) return time; + if (type == Timepoint.TYPE.MINUTE && t.getMinute() != time.getMinute()) return time; + return t; + } + + if (type == Timepoint.TYPE.HOUR) { + if (floor.getHour() != time.getHour() && ceil.getHour() == time.getHour()) return ceil; + if (floor.getHour() == time.getHour() && ceil.getHour() != time.getHour()) return floor; + if (floor.getHour() != time.getHour() && ceil.getHour() != time.getHour()) return time; + } + + if (type == Timepoint.TYPE.MINUTE) { + if (floor.getHour() != time.getHour() && ceil.getHour() != time.getHour()) return time; + if (floor.getHour() != time.getHour() && ceil.getHour() == time.getHour()) { + return ceil.getMinute() == time.getMinute() ? ceil : time; + } + if (floor.getHour() == time.getHour() && ceil.getHour() != time.getHour()) { + return floor.getMinute() == time.getMinute() ? floor : time; + } + if (floor.getMinute() != time.getMinute() && ceil.getMinute() == time.getMinute()) return ceil; + if (floor.getMinute() == time.getMinute() && ceil.getMinute() != time.getMinute()) return floor; + if (floor.getMinute() != time.getMinute() && ceil.getMinute() != time.getMinute()) return time; + } + + int floorDist = Math.abs(time.compareTo(floor)); + int ceilDist = Math.abs(time.compareTo(ceil)); + + return floorDist < ceilDist ? floor : ceil; + } + + if (!mDisabledTimes.isEmpty()) { + // if type matches resolution: cannot change anything, return input + if (type != null && type == resolution) return time; + + if (resolution == Timepoint.TYPE.SECOND) { + if (!mDisabledTimes.contains(time)) return time; + return searchValidTimePoint(time, type, resolution); + } + + if (resolution == Timepoint.TYPE.MINUTE) { + Timepoint ceil = mDisabledTimes.ceiling(time); + Timepoint floor = mDisabledTimes.floor(time); + boolean ceilDisabled = time.equals(ceil, Timepoint.TYPE.MINUTE); + boolean floorDisabled = time.equals(floor, Timepoint.TYPE.MINUTE); + + if (ceilDisabled || floorDisabled) return searchValidTimePoint(time, type, resolution); + return time; + } + + if (resolution == Timepoint.TYPE.HOUR) { + Timepoint ceil = mDisabledTimes.ceiling(time); + Timepoint floor = mDisabledTimes.floor(time); + boolean ceilDisabled = time.equals(ceil, Timepoint.TYPE.HOUR); + boolean floorDisabled = time.equals(floor, Timepoint.TYPE.HOUR); + + if (ceilDisabled || floorDisabled) return searchValidTimePoint(time, type, resolution); + return time; + } + } + + return time; + } + + private Timepoint searchValidTimePoint(@NonNull Timepoint time, @Nullable Timepoint.TYPE type, @NonNull Timepoint.TYPE resolution) { + Timepoint forward = new Timepoint(time); + Timepoint backward = new Timepoint(time); + int iteration = 0; + int resolutionMultiplier = 1; + if (resolution == Timepoint.TYPE.MINUTE) resolutionMultiplier = 60; + if (resolution == Timepoint.TYPE.SECOND) resolutionMultiplier = 3600; + + while (iteration < 24 * resolutionMultiplier) { + iteration++; + forward.add(resolution, 1); + backward.add(resolution, -1); + + if (type == null || forward.get(type) == time.get(type)) { + Timepoint forwardCeil = mDisabledTimes.ceiling(forward); + Timepoint forwardFloor = mDisabledTimes.floor(forward); + if (!forward.equals(forwardCeil, resolution) && !forward.equals(forwardFloor, resolution)) + return forward; + } + + if (type == null || backward.get(type) == time.get(type)) { + Timepoint backwardCeil = mDisabledTimes.ceiling(backward); + Timepoint backwardFloor = mDisabledTimes.floor(backward); + if (!backward.equals(backwardCeil, resolution) && !backward.equals(backwardFloor, resolution)) + return backward; + } + + if (type != null && backward.get(type) != time.get(type) && forward.get(type) != time.get(type)) + break; + } + // If this step is reached, the user has disabled all timepoints + return time; + } +} diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/RadialPickerLayout.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/RadialPickerLayout.java index 2c0ada9e..5bfb3de1 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/RadialPickerLayout.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/RadialPickerLayout.java @@ -19,6 +19,7 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; +import android.graphics.Typeface; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -95,6 +96,7 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener { private AnimatorSet mTransition; private Handler mHandler = new Handler(); + private Typeface mTypeface; public interface OnValueSelectedListener { void onValueSelected(Timepoint newTime); @@ -154,13 +156,18 @@ public void setOnValueSelectedListener(OnValueSelectedListener listener) { mListener = listener; } + public void setCustomFont(Typeface typeface) { + mTypeface = typeface; + } + /** * Initialize the Layout with starting values. * @param context A context needed to inflate resources + * @param locale A Locale to be used when generating strings * @param initialTime The initial selection of the Timepicker * @param is24HourMode Indicates whether we should render in 24hour mode or with AM/PM selectors */ - public void initialize(Context context, TimePickerController timePickerController, + public void initialize(Context context, Locale locale, TimePickerController timePickerController, Timepoint initialTime, boolean is24HourMode) { if (mTimeInitialized) { Log.e(TAG, "Time has already been initialized."); @@ -174,7 +181,7 @@ public void initialize(Context context, TimePickerController timePickerControlle mCircleView.initialize(context, mController); mCircleView.invalidate(); if (!mIs24HourMode && mController.getVersion() == TimePickerDialog.Version.VERSION_1) { - mAmPmCirclesView.initialize(context, mController, initialTime.isAM() ? AM : PM); + mAmPmCirclesView.initialize(context, locale, mController, initialTime.isAM() ? AM : PM); mAmPmCirclesView.invalidate(); } @@ -214,11 +221,25 @@ public boolean isValidSelection(int selection) { String[] secondsTexts = new String[12]; for (int i = 0; i < 12; i++) { hoursTexts[i] = is24HourMode? - String.format(Locale.getDefault(), "%02d", hours_24[i]) : String.format(Locale.getDefault(), "%d", hours[i]); - innerHoursTexts[i] = String.format(Locale.getDefault(), "%d", hours[i]); - minutesTexts[i] = String.format(Locale.getDefault(), "%02d", minutes[i]); - secondsTexts[i] = String.format(Locale.getDefault(), "%02d", seconds[i]); + String.format(locale, "%02d", hours_24[i]) : String.format(locale, "%d", hours[i]); + innerHoursTexts[i] = String.format(locale, "%d", hours[i]); + minutesTexts[i] = String.format(locale, "%02d", minutes[i]); + secondsTexts[i] = String.format(locale, "%02d", seconds[i]); + } + // The version 2 layout has the hours > 12 on the inner circle rather than the outer circle + // Inner circle and outer circle should be swapped (see #411) + if (mController.getVersion() == TimePickerDialog.Version.VERSION_2) { + String[] temp = hoursTexts; + hoursTexts = innerHoursTexts; + innerHoursTexts = temp; + } + + if (mTypeface != null) { + mHourRadialTextsView.setCustomFont(mTypeface); + mMinuteRadialTextsView.setCustomFont(mTypeface); + mSecondRadialTextsView.setCustomFont(mTypeface); } + mHourRadialTextsView.initialize(context, hoursTexts, (is24HourMode ? innerHoursTexts : null), mController, hourValidator, true); mHourRadialTextsView.setSelection(is24HourMode ? initialTime.getHour() : hours[initialTime.getHour() % 12]); @@ -264,7 +285,10 @@ private void setItem(int index, Timepoint time) { */ private boolean isHourInnerCircle(int hourOfDay) { // We'll have the 00 hours on the outside circle. - return mIs24HourMode && (hourOfDay <= 12 && hourOfDay != 0); + boolean isMorning = hourOfDay <= 12 && hourOfDay != 0; + // In the version 2 layout the circles are swapped + if (mController.getVersion() != TimePickerDialog.Version.VERSION_1) isMorning = !isMorning; + return mIs24HourMode && isMorning; } public int getHours() { @@ -435,18 +459,12 @@ private static int snapOnly30s(int degrees, int forceHigherOrLower) { private Timepoint roundToValidTime(Timepoint newSelection, int currentItemShowing) { switch(currentItemShowing) { case HOUR_INDEX: - newSelection = mController.roundToNearest(newSelection, Timepoint.TYPE.HOUR); - break; + return mController.roundToNearest(newSelection, null); case MINUTE_INDEX: - newSelection = mController.roundToNearest(newSelection, Timepoint.TYPE.MINUTE); - break; - case SECOND_INDEX: - newSelection = mController.roundToNearest(newSelection, Timepoint.TYPE.SECOND); - break; + return mController.roundToNearest(newSelection, Timepoint.TYPE.HOUR); default: - newSelection = mCurrentTime; + return mController.roundToNearest(newSelection, Timepoint.TYPE.MINUTE); } - return newSelection; } /** @@ -472,33 +490,33 @@ private void reselectSelector(Timepoint newSelection, boolean forceDrawDot, int mHourRadialTextsView.setSelection(hour); // If we rounded the minutes, reposition the minuteSelector too. if(newSelection.getMinute() != mCurrentTime.getMinute()) { - int minDegrees = newSelection.getMinute()*360/60; + int minDegrees = newSelection.getMinute() * (360 / 60); mMinuteRadialSelectorView.setSelection(minDegrees, isInnerCircle, forceDrawDot); mMinuteRadialTextsView.setSelection(newSelection.getMinute()); } // If we rounded the seconds, reposition the secondSelector too. if(newSelection.getSecond() != mCurrentTime.getSecond()) { - int secDegrees = newSelection.getSecond()*360/60; + int secDegrees = newSelection.getSecond() * (360 / 60); mSecondRadialSelectorView.setSelection(secDegrees, isInnerCircle, forceDrawDot); mSecondRadialTextsView.setSelection(newSelection.getSecond()); } break; case MINUTE_INDEX: // The selection might have changed, recalculate the degrees - degrees = newSelection.getMinute()*360/60; + degrees = newSelection.getMinute() * (360 / 60); mMinuteRadialSelectorView.setSelection(degrees, false, forceDrawDot); mMinuteRadialTextsView.setSelection(newSelection.getMinute()); // If we rounded the seconds, reposition the secondSelector too. if(newSelection.getSecond() != mCurrentTime.getSecond()) { - int secDegrees = newSelection.getSecond()*360/60; + int secDegrees = newSelection.getSecond()* (360 / 60); mSecondRadialSelectorView.setSelection(secDegrees, false, forceDrawDot); mSecondRadialTextsView.setSelection(newSelection.getSecond()); } break; case SECOND_INDEX: // The selection might have changed, recalculate the degrees - degrees = newSelection.getSecond()*360/60; + degrees = newSelection.getSecond() * (360 / 60); mSecondRadialSelectorView.setSelection(degrees, false, forceDrawDot); mSecondRadialTextsView.setSelection(newSelection.getSecond()); } @@ -545,6 +563,8 @@ private Timepoint getTimeFromDegrees(int degrees, boolean isInnerCircle, boolean stepSize = SECOND_VALUE_TO_DEGREES_STEP_SIZE; } + // TODO: simplify this logic. Just appending a swap of the values at the end for the v2 + // TODO: layout makes this code rather hard to read if (currentShowing == HOUR_INDEX) { if (mIs24HourMode) { if (degrees == 0 && isInnerCircle) { @@ -565,6 +585,12 @@ private Timepoint getTimeFromDegrees(int degrees, boolean isInnerCircle, boolean value += 12; } + if (currentShowing == HOUR_INDEX + && mController.getVersion() != TimePickerDialog.Version.VERSION_1 + && mIs24HourMode) { + value = (value + 12) % 24; + } + Timepoint newSelection; switch(currentShowing) { case HOUR_INDEX: @@ -807,26 +833,10 @@ public void run() { mHandler.removeCallbacksAndMessages(null); degrees = getDegreesFromCoords(eventX, eventY, true, isInnerCircle); if (degrees != -1) { - switch(getCurrentItemShowing()) { - case HOUR_INDEX: - value = mController.roundToNearest( - getTimeFromDegrees(degrees, isInnerCircle[0], false), - null - ); - break; - case MINUTE_INDEX: - value = mController.roundToNearest( - getTimeFromDegrees(degrees, isInnerCircle[0], false), - Timepoint.TYPE.HOUR - ); - break; - default: - value = mController.roundToNearest( - getTimeFromDegrees(degrees, isInnerCircle[0], false), - Timepoint.TYPE.MINUTE - ); - break; - } + value = roundToValidTime( + getTimeFromDegrees(degrees, isInnerCircle[0], false), + getCurrentItemShowing() + ); reselectSelector(value, true, getCurrentItemShowing()); if (value != null && (mLastValueSelected == null || !mLastValueSelected.equals(value))) { mController.tryVibrate(); diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/RadialTextsView.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/RadialTextsView.java index 497dc19c..f441bdc5 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/RadialTextsView.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/RadialTextsView.java @@ -98,9 +98,11 @@ public void initialize(Context context, String[] texts, String[] innerTexts, int textColorRes = controller.isThemeDark() ? R.color.mdtp_white : R.color.mdtp_numbers_text_color; mPaint.setColor(ContextCompat.getColor(context, textColorRes)); String typefaceFamily = res.getString(R.string.mdtp_radial_numbers_typeface); - mTypefaceLight = Typeface.create(typefaceFamily, Typeface.NORMAL); - String typefaceFamilyRegular = res.getString(R.string.mdtp_sans_serif); - mTypefaceRegular = Typeface.create(typefaceFamilyRegular, Typeface.NORMAL); + if (mTypefaceRegular == null) { + mTypefaceLight = Typeface.create(typefaceFamily, Typeface.NORMAL); + String typefaceFamilyRegular = res.getString(R.string.mdtp_sans_serif); + mTypefaceRegular = Typeface.create(typefaceFamilyRegular, Typeface.NORMAL); + } mPaint.setAntiAlias(true); mPaint.setTextAlign(Align.CENTER); @@ -139,12 +141,21 @@ public void initialize(Context context, String[] texts, String[] innerTexts, if (mHasInnerCircle) { mNumbersRadiusMultiplier = Float.parseFloat( res.getString(R.string.mdtp_numbers_radius_multiplier_outer)); - mTextSizeMultiplier = Float.parseFloat( - res.getString(R.string.mdtp_text_size_multiplier_outer)); mInnerNumbersRadiusMultiplier = Float.parseFloat( res.getString(R.string.mdtp_numbers_radius_multiplier_inner)); - mInnerTextSizeMultiplier = Float.parseFloat( - res.getString(R.string.mdtp_text_size_multiplier_inner)); + + // Version 2 layout draws outer circle bigger than inner + if (controller.getVersion() == TimePickerDialog.Version.VERSION_1) { + mTextSizeMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_text_size_multiplier_outer)); + mInnerTextSizeMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_text_size_multiplier_inner)); + } else { + mTextSizeMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_text_size_multiplier_outer_v2)); + mInnerTextSizeMultiplier = Float.parseFloat( + res.getString(R.string.mdtp_text_size_multiplier_inner_v2)); + } mInnerTextGridHeights = new float[7]; mInnerTextGridWidths = new float[7]; @@ -166,6 +177,11 @@ public void initialize(Context context, String[] texts, String[] innerTexts, mIsInitialized = true; } + public void setCustomFont(Typeface typeface) { + mTypefaceLight = typeface; + mTypefaceRegular = typeface; + } + /** * Set the value of the selected text. Depending on the theme this will be rendered differently * @param selection The text which is currently selected diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimePickerController.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimePickerController.java index 2a68c505..f148136b 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimePickerController.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimePickerController.java @@ -5,7 +5,7 @@ * * Created by wdullaer on 6/10/15. */ -public interface TimePickerController { +interface TimePickerController { /** * @return boolean - true if the dark theme should be used */ diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimePickerDialog.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimePickerDialog.java index f331f200..88553621 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimePickerDialog.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimePickerDialog.java @@ -25,10 +25,10 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; +import android.graphics.Typeface; import android.os.Build; import android.os.Bundle; import android.support.annotation.ColorInt; -import android.support.annotation.ColorRes; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -55,7 +55,7 @@ import java.text.DateFormatSymbols; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Calendar; import java.util.List; import java.util.Locale; @@ -82,9 +82,6 @@ public enum Version { private static final String KEY_ACCENT = "accent"; private static final String KEY_VIBRATE = "vibrate"; private static final String KEY_DISMISS = "dismiss"; - private static final String KEY_SELECTABLE_TIMES = "selectable_times"; - private static final String KEY_MIN_TIME = "min_time"; - private static final String KEY_MAX_TIME = "max_time"; private static final String KEY_ENABLE_SECONDS = "enable_seconds"; private static final String KEY_ENABLE_MINUTES = "enable_minutes"; private static final String KEY_OK_RESID = "ok_resid"; @@ -94,6 +91,8 @@ public enum Version { private static final String KEY_CANCEL_STRING = "cancel_string"; private static final String KEY_CANCEL_COLOR = "cancel_color"; private static final String KEY_VERSION = "version"; + private static final String KEY_TIMEPOINTLIMITER = "timepoint_limiter"; + private static final String KEY_LOCALE = "locale"; public static final int HOUR_INDEX = 0; public static final int MINUTE_INDEX = 1; @@ -123,8 +122,8 @@ public enum Version { private View mAmPmLayout; private RadialPickerLayout mTimePicker; - private int mSelectedColor; - private int mUnselectedColor; + private int mSelectedColor = 0; + private int mUnselectedColor = 0; private String mAmText; private String mPmText; @@ -136,11 +135,7 @@ public enum Version { private boolean mThemeDarkChanged; private boolean mVibrate; private int mAccentColor = -1; - private int mHeaderColor = -1; private boolean mDismissOnPause; - private Timepoint[] mSelectableTimes; - private Timepoint mMinTime; - private Timepoint mMaxTime; private boolean mEnableSeconds; private boolean mEnableMinutes; private int mOkResid; @@ -150,6 +145,9 @@ public enum Version { private String mCancelString; private int mCancelColor; private Version mVersion; + private DefaultTimepointLimiter mDefaultLimiter = new DefaultTimepointLimiter(); + private TimepointLimiter mLimiter = mDefaultLimiter; + private Locale mLocale = Locale.getDefault(); // For hardware IME input. private char mPlaceholderText; @@ -169,6 +167,13 @@ public enum Version { private String mSecondPickerDescription; private String mSelectSeconds; + //Custom Typeface + private Typeface headerTypeface; + private Typeface circleViewTypeface; + + //Custom Header color + private int mHeaderColor = 0; + /** * The callback interface used to indicate the user is done filling in * the time (they clicked on the 'Set' button). @@ -188,6 +193,7 @@ public TimePickerDialog() { // Empty constructor required for dialog fragment. } + @SuppressWarnings("SameParameterValue") public static TimePickerDialog newInstance(OnTimeSetListener callback, int hourOfDay, int minute, int second, boolean is24HourMode) { TimePickerDialog ret = new TimePickerDialog(); @@ -200,6 +206,12 @@ public static TimePickerDialog newInstance(OnTimeSetListener callback, return TimePickerDialog.newInstance(callback, hourOfDay, minute, 0, is24HourMode); } + @SuppressWarnings({"unused", "SameParameterValue"}) + public static TimePickerDialog newInstance(OnTimeSetListener callback, boolean is24HourMode) { + Calendar now = Calendar.getInstance(); + return TimePickerDialog.newInstance(callback, now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), is24HourMode); + } + public void initialize(OnTimeSetListener callback, int hourOfDay, int minute, int second, boolean is24HourMode) { mCallback = callback; @@ -220,6 +232,8 @@ public void initialize(OnTimeSetListener callback, mCancelResid = R.string.mdtp_cancel; mCancelColor = -1; mVersion = Build.VERSION.SDK_INT < Build.VERSION_CODES.M ? Version.VERSION_1 : Version.VERSION_2; + // Throw away the current TimePicker, which might contain old state if the dialog instance is reused + mTimePicker = null; } /** @@ -250,16 +264,13 @@ public void setAccentColor(String color) { mAccentColor = Color.parseColor(color); } - public void setHeaderColor(@ColorInt int color) { - mHeaderColor = Color.argb(255, Color.red(color), Color.green(color), Color.blue(color)); - } - /** * Set the accent color of this dialog * @param color the accent color you want */ public void setAccentColor(@ColorInt int color) { - mAccentColor = Color.argb(255, Color.red(color), Color.green(color), Color.blue(color)); + int col = Color.argb(255, Color.red(color), Color.green(color), Color.blue(color)); + mAccentColor = col; } /** @@ -271,6 +282,33 @@ public void setOkColor(String color) { mOkColor = Color.parseColor(color); } + public void setCustomHeaderTypeface(Typeface typeface) { + this.headerTypeface = typeface; + } + + public void setCustomCircleViewTypeface(Typeface typeface) { + this.circleViewTypeface = typeface; + } + + public void setHeaderColor(@ColorInt int color) { + int col = Color.argb(255, Color.red(color), Color.green(color), Color.blue(color)); + this.mHeaderColor = col; + } + + public void setHeaderColor(String color) { + this.mHeaderColor = Color.parseColor(color); + } + + public void setSelectedColor(@ColorInt int color) { + int col = Color.argb(255, Color.red(color), Color.green(color), Color.blue(color)); + this.mSelectedColor = col; + } + + public void setUnselectedColor(@ColorInt int color) { + int col = Color.argb(255, Color.red(color), Color.green(color), Color.blue(color)); + this.mUnselectedColor = col; + } + /** * Set the text color of the OK button * @param color the color you want @@ -355,9 +393,7 @@ public void setMinTime(int hour, int minute, int second) { } public void setMinTime(Timepoint minTime) { - if(mMaxTime != null && minTime.compareTo(mMaxTime) > 0) - throw new IllegalArgumentException("Minimum time must be smaller than the maximum time"); - mMinTime = minTime; + mDefaultLimiter.setMinTime(minTime); } @SuppressWarnings("unused") @@ -366,21 +402,38 @@ public void setMaxTime(int hour, int minute, int second) { } public void setMaxTime(Timepoint maxTime) { - if(mMinTime != null && maxTime.compareTo(mMinTime) < 0) - throw new IllegalArgumentException("Maximum time must be greater than the minimum time"); - mMaxTime = maxTime; + mDefaultLimiter.setMaxTime(maxTime); } - @SuppressWarnings("unused") + /** + * Pass in an array of Timepoints which are the only possible selections. + * Try to specify Timepoints only up to the resolution of your picker (i.e. do not add seconds + * if the resolution of the picker is minutes) + * @param selectableTimes Array of Timepoints which are the only valid selections in the picker + */ public void setSelectableTimes(Timepoint[] selectableTimes) { - mSelectableTimes = selectableTimes; - Arrays.sort(mSelectableTimes); + mDefaultLimiter.setSelectableTimes(selectableTimes); + } + + /** + * Pass in an array of Timepoints that cannot be selected. These take precedence over + * {@link TimePickerDialog#setSelectableTimes(Timepoint[])} + * Be careful when using this without selectableTimes: rounding to a valid Timepoint is a + * very expensive operation if a lot of consecutive Timepoints are disabled + * Try to specify Timepoints only up to the resolution of your picker (i.e. do not add seconds + * if the resolution of the picker is minutes) + * @param disabledTimes Array of Timepoints which are disabled in the resulting picker + */ + public void setDisabledTimes(Timepoint[] disabledTimes) { + mDefaultLimiter.setDisabledTimes(disabledTimes); } /** * Set the interval for selectable times in the TimePickerDialog - * This is a convenience wrapper around setSelectableTimes + * This is a convenience wrapper around {@link TimePickerDialog#setSelectableTimes(Timepoint[])} * The interval for all three time components can be set independently + * If you are not using the seconds / minutes picker, set the respective item to 60 for + * better performance. * @param hourInterval The interval between 2 selectable hours ([1,24]) * @param minuteInterval The interval between 2 selectable minutes ([1,60]) * @param secondInterval The interval between 2 selectable seconds ([1,60]) @@ -410,9 +463,12 @@ public void setTimeInterval(@IntRange(from=1, to=24) int hourInterval, * Set the interval for selectable times in the TimePickerDialog * This is a convenience wrapper around setSelectableTimes * The interval for all three time components can be set independently + * If you are not using the seconds / minutes picker, set the respective item to 60 for + * better performance. * @param hourInterval The interval between 2 selectable hours ([1,24]) * @param minuteInterval The interval between 2 selectable minutes ([1,60]) */ + @SuppressWarnings("SameParameterValue") public void setTimeInterval(@IntRange(from=1, to=24) int hourInterval, @IntRange(from=1, to=60) int minuteInterval) { setTimeInterval(hourInterval, minuteInterval, 1); @@ -422,6 +478,8 @@ public void setTimeInterval(@IntRange(from=1, to=24) int hourInterval, * Set the interval for selectable times in the TimePickerDialog * This is a convenience wrapper around setSelectableTimes * The interval for all three time components can be set independently + * If you are not using the seconds / minutes picker, set the respective item to 60 for + * better performance. * @param hourInterval The interval between 2 selectable hours ([1,24]) */ @SuppressWarnings("unused") @@ -442,16 +500,67 @@ public void setOnDismissListener(DialogInterface.OnDismissListener onDismissList mOnDismissListener = onDismissListener; } + /** + * Set the time that will be shown when the picker opens for the first time + * Overrides the value given in newInstance() + * + * @deprecated in favor of {@link #setInitialSelection(int, int, int)} + * @param hourOfDay the hour of the day + * @param minute the minute of the hour + * @param second the second of the minute + */ + @Deprecated public void setStartTime(int hourOfDay, int minute, int second) { mInitialTime = roundToNearest(new Timepoint(hourOfDay, minute, second)); mInKbMode = false; } - @SuppressWarnings("unused") + /** + * Set the time that will be shown when the picker opens for the first time + * Overrides the value given in newInstance + * + * @deprecated in favor of {@link #setInitialSelection(int, int)} + * @param hourOfDay the hour of the day + * @param minute the minute of the hour + */ + @SuppressWarnings({"unused", "deprecation"}) + @Deprecated public void setStartTime(int hourOfDay, int minute) { setStartTime(hourOfDay, minute, 0); } + /** + * Set the time that will be shown when the picker opens for the first time + * Overrides the value given in newInstance() + * @param hourOfDay the hour of the day + * @param minute the minute of the hour + * @param second the second of the minute + */ + public void setInitialSelection(int hourOfDay, int minute, int second) { + setInitialSelection(new Timepoint(hourOfDay, minute, second)); + } + + /** + * Set the time that will be shown when the picker opens for the first time + * Overrides the value given in newInstance + * @param hourOfDay the hour of the day + * @param minute the minute of the hour + */ + @SuppressWarnings("unused") + public void setInitialSelection(int hourOfDay, int minute) { + setInitialSelection(hourOfDay, minute, 0); + } + + /** + * Set the time that will be shown when the picker opens for the first time + * Overrides the value given in newInstance() + * @param time the Timepoint selected when the Dialog opens + */ + public void setInitialSelection(Timepoint time) { + mInitialTime = roundToNearest(time); + mInKbMode = false; + } + /** * Set the label for the Ok button (max 12 characters) * @param okString A literal String to be used as the Ok button label @@ -498,11 +607,39 @@ public void setVersion(Version version) { mVersion = version; } + /** + * Pass in a custom implementation of TimeLimiter + * Disables setSelectableTimes, setDisabledTimes, setTimeInterval, setMinTime and setMaxTime + * @param limiter A custom implementation of TimeLimiter + */ + @SuppressWarnings("unused") + public void setTimepointLimiter(TimepointLimiter limiter) { + mLimiter = limiter; + } + @Override public Version getVersion() { return mVersion; } + /** + * Get a reference to the OnTimeSetListener callback + * @return OnTimeSetListener the callback + */ + @SuppressWarnings("unused") + public OnTimeSetListener getOnTimeSetListener() { + return mCallback; + } + + /** + * Set the Locale which will be used to generate various strings throughout the picker + * @param locale Locale + */ + @SuppressWarnings("unused") + public void setLocale(Locale locale) { + mLocale = locale; + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -517,9 +654,6 @@ public void onCreate(Bundle savedInstanceState) { mAccentColor = savedInstanceState.getInt(KEY_ACCENT); mVibrate = savedInstanceState.getBoolean(KEY_VIBRATE); mDismissOnPause = savedInstanceState.getBoolean(KEY_DISMISS); - mSelectableTimes = (Timepoint[])savedInstanceState.getParcelableArray(KEY_SELECTABLE_TIMES); - mMinTime = savedInstanceState.getParcelable(KEY_MIN_TIME); - mMaxTime = savedInstanceState.getParcelable(KEY_MAX_TIME); mEnableSeconds = savedInstanceState.getBoolean(KEY_ENABLE_SECONDS); mEnableMinutes = savedInstanceState.getBoolean(KEY_ENABLE_MINUTES); mOkResid = savedInstanceState.getInt(KEY_OK_RESID); @@ -529,6 +663,20 @@ public void onCreate(Bundle savedInstanceState) { mCancelString = savedInstanceState.getString(KEY_CANCEL_STRING); mCancelColor = savedInstanceState.getInt(KEY_CANCEL_COLOR); mVersion = (Version) savedInstanceState.getSerializable(KEY_VERSION); + mLimiter = savedInstanceState.getParcelable(KEY_TIMEPOINTLIMITER); + mLocale = (Locale) savedInstanceState.getSerializable(KEY_LOCALE); + + /* + If the user supplied a custom limiter, we need to create a new default one to prevent + null pointer exceptions on the configuration methods + If the user did not supply a custom limiter we need to ensure both mDefaultLimiter + and mLimiter are the same reference, so that the config methods actually + affect the behaviour of the picker (in the unlikely event the user reconfigures + the picker when it is shown) + */ + mDefaultLimiter = mLimiter instanceof DefaultTimepointLimiter + ? (DefaultTimepointLimiter) mLimiter + : new DefaultTimepointLimiter(); } } @@ -559,27 +707,47 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, mSelectMinutes = res.getString(R.string.mdtp_select_minutes); mSecondPickerDescription = res.getString(R.string.mdtp_second_picker_description); mSelectSeconds = res.getString(R.string.mdtp_select_seconds); - mSelectedColor = ContextCompat.getColor(context, R.color.mdtp_white); - mUnselectedColor = ContextCompat.getColor(context, R.color.mdtp_accent_color_focused); - - mHourView = (TextView) view.findViewById(R.id.mdtp_hours); + if (mSelectedColor == 0) { + mSelectedColor = ContextCompat.getColor(context, R.color.mdtp_white); + } + if (mUnselectedColor == 0) { + mUnselectedColor = ContextCompat.getColor(context, R.color.mdtp_accent_color_focused); + } + TextView dotsMinutes = view.findViewById(R.id.mdtp_separator); + dotsMinutes.setTextColor(mUnselectedColor); + TextView dotsSeconds = view.findViewById(R.id.mdtp_separator_seconds); + dotsSeconds.setTextColor(mUnselectedColor); + TextView amText = view.findViewById(R.id.mdtp_am_label); + amText.setTextColor(mUnselectedColor); + TextView pmText = view.findViewById(R.id.mdtp_pm_label); + pmText.setTextColor(mUnselectedColor); + + mHourView = view.findViewById(R.id.mdtp_hours); mHourView.setOnKeyListener(keyboardListener); - mHourSpaceView = (TextView) view.findViewById(R.id.mdtp_hour_space); - mMinuteSpaceView = (TextView) view.findViewById(R.id.mdtp_minutes_space); - mMinuteView = (TextView) view.findViewById(R.id.mdtp_minutes); + mHourSpaceView = view.findViewById(R.id.mdtp_hour_space); + mMinuteSpaceView = view.findViewById(R.id.mdtp_minutes_space); + mMinuteView = view.findViewById(R.id.mdtp_minutes); mMinuteView.setOnKeyListener(keyboardListener); - mSecondSpaceView = (TextView) view.findViewById(R.id.mdtp_seconds_space); - mSecondView = (TextView) view.findViewById(R.id.mdtp_seconds); + mSecondSpaceView = view.findViewById(R.id.mdtp_seconds_space); + mSecondView = view.findViewById(R.id.mdtp_seconds); mSecondView.setOnKeyListener(keyboardListener); - mAmTextView = (TextView) view.findViewById(R.id.mdtp_am_label); + mAmTextView = view.findViewById(R.id.mdtp_am_label); mAmTextView.setOnKeyListener(keyboardListener); - mPmTextView = (TextView) view.findViewById(R.id.mdtp_pm_label); + mPmTextView = view.findViewById(R.id.mdtp_pm_label); mPmTextView.setOnKeyListener(keyboardListener); mAmPmLayout = view.findViewById(R.id.mdtp_ampm_layout); - String[] amPmTexts = new DateFormatSymbols().getAmPmStrings(); + String[] amPmTexts = new DateFormatSymbols(mLocale).getAmPmStrings(); mAmText = amPmTexts[0]; mPmText = amPmTexts[1]; + if (headerTypeface != null) { + mHourView.setTypeface(headerTypeface); + mMinuteView.setTypeface(headerTypeface); + mSecondView.setTypeface(headerTypeface); + mAmTextView.setTypeface(headerTypeface); + mPmTextView.setTypeface(headerTypeface); + } + mHapticFeedbackController = new HapticFeedbackController(getActivity()); if(mTimePicker != null) { @@ -588,10 +756,13 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, mInitialTime = roundToNearest(mInitialTime); - mTimePicker = (RadialPickerLayout) view.findViewById(R.id.mdtp_time_picker); + mTimePicker = view.findViewById(R.id.mdtp_time_picker); + if (circleViewTypeface != null) { + mTimePicker.setCustomFont(circleViewTypeface); + } mTimePicker.setOnValueSelectedListener(this); mTimePicker.setOnKeyListener(keyboardListener); - mTimePicker.initialize(getActivity(), this, mInitialTime, mIs24HourMode); + mTimePicker.initialize(getActivity(), mLocale, this, mInitialTime, mIs24HourMode); int currentItemShowing = HOUR_INDEX; if (savedInstanceState != null && @@ -623,7 +794,8 @@ public void onClick(View view) { } }); - mOkButton = (Button) view.findViewById(R.id.mdtp_ok); + String buttonTypeface = context.getResources().getString(R.string.mdtp_button_typeface); + mOkButton = view.findViewById(R.id.mdtp_ok); mOkButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -637,11 +809,11 @@ public void onClick(View v) { } }); mOkButton.setOnKeyListener(keyboardListener); - mOkButton.setTypeface(TypefaceHelper.get(context, "Roboto-Medium")); + mOkButton.setTypeface(TypefaceHelper.get(context, buttonTypeface)); if(mOkString != null) mOkButton.setText(mOkString); else mOkButton.setText(mOkResid); - mCancelButton = (Button) view.findViewById(R.id.mdtp_cancel); + mCancelButton = view.findViewById(R.id.mdtp_cancel); mCancelButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -649,7 +821,7 @@ public void onClick(View v) { if (getDialog() != null) getDialog().cancel(); } }); - mCancelButton.setTypeface(TypefaceHelper.get(context, "Roboto-Medium")); + mCancelButton.setTypeface(TypefaceHelper.get(context, buttonTypeface)); if(mCancelString != null) mCancelButton.setText(mCancelString); else mCancelButton.setText(mCancelResid); mCancelButton.setVisibility(isCancelable() ? View.VISIBLE : View.GONE); @@ -728,7 +900,7 @@ public void onClick(View v) { ); paramsSeparator.addRule(RelativeLayout.CENTER_HORIZONTAL); paramsSeparator.addRule(RelativeLayout.ABOVE, R.id.mdtp_center_view); - TextView separatorView = (TextView) view.findViewById(R.id.mdtp_separator); + TextView separatorView = view.findViewById(R.id.mdtp_separator); separatorView.setLayoutParams(paramsSeparator); } else if (!mEnableSeconds) { // Hour + Minutes + Am/Pm indicator @@ -738,7 +910,7 @@ public void onClick(View v) { ); paramsSeparator.addRule(RelativeLayout.CENTER_HORIZONTAL); paramsSeparator.addRule(RelativeLayout.ABOVE, R.id.mdtp_center_view); - TextView separatorView = (TextView) view.findViewById(R.id.mdtp_separator); + TextView separatorView = view.findViewById(R.id.mdtp_separator); separatorView.setLayoutParams(paramsSeparator); // Put the am/pm indicator below the separator RelativeLayout.LayoutParams paramsAmPm = new RelativeLayout.LayoutParams( @@ -755,7 +927,7 @@ public void onClick(View v) { ); paramsSeparator.addRule(RelativeLayout.CENTER_HORIZONTAL); paramsSeparator.addRule(RelativeLayout.ABOVE, R.id.mdtp_seconds_space); - TextView separatorView = (TextView) view.findViewById(R.id.mdtp_separator); + TextView separatorView = view.findViewById(R.id.mdtp_separator); separatorView.setLayoutParams(paramsSeparator); // Center the seconds RelativeLayout.LayoutParams paramsSeconds = new RelativeLayout.LayoutParams( @@ -777,7 +949,7 @@ public void onClick(View v) { ); paramsSeparator.addRule(RelativeLayout.CENTER_HORIZONTAL); paramsSeparator.addRule(RelativeLayout.ABOVE, R.id.mdtp_seconds_space); - TextView separatorView = (TextView) view.findViewById(R.id.mdtp_separator); + TextView separatorView = view.findViewById(R.id.mdtp_separator); separatorView.setLayoutParams(paramsSeparator); // Put the Am/Pm indicator below the seconds RelativeLayout.LayoutParams paramsAmPm = new RelativeLayout.LayoutParams( @@ -794,7 +966,7 @@ else if (mIs24HourMode && !mEnableSeconds && mEnableMinutes) { LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT ); paramsSeparator.addRule(RelativeLayout.CENTER_IN_PARENT); - TextView separatorView = (TextView) view.findViewById(R.id.mdtp_separator); + TextView separatorView = view.findViewById(R.id.mdtp_separator); separatorView.setLayoutParams(paramsSeparator); } else if (!mEnableMinutes && !mEnableSeconds) { // center the hour @@ -850,7 +1022,7 @@ else if (mIs24HourMode && !mEnableSeconds && mEnableMinutes) { mPlaceholderText = mDoublePlaceholderText.charAt(0); mAmKeyCode = mPmKeyCode = -1; generateLegalTimesTree(); - if (mInKbMode) { + if (mInKbMode && savedInstanceState != null) { mTypedTimes = savedInstanceState.getIntegerArrayList(KEY_TYPED_TIMES); tryStartingKbMode(-1); mHourView.invalidate(); @@ -859,16 +1031,23 @@ else if (mIs24HourMode && !mEnableSeconds && mEnableMinutes) { } // Set the title (if any) - TextView timePickerHeader = (TextView) view.findViewById(R.id.mdtp_time_picker_header); + TextView timePickerHeader = view.findViewById(R.id.mdtp_time_picker_header); if (!mTitle.isEmpty()) { timePickerHeader.setVisibility(TextView.VISIBLE); - timePickerHeader.setText(mTitle.toUpperCase(Locale.getDefault())); + timePickerHeader.setText(mTitle.toUpperCase(mLocale)); } // Set the theme at the end so that the initialize()s above don't counteract the theme. - timePickerHeader.setBackgroundColor(Utils.darkenColor(mHeaderColor)); - view.findViewById(R.id.mdtp_time_display_background).setBackgroundColor(mAccentColor); - view.findViewById(R.id.mdtp_time_display).setBackgroundColor(mAccentColor); + if (mHeaderColor == 0) { + timePickerHeader.setBackgroundColor(Utils.darkenColor(mAccentColor)); + view.findViewById(R.id.mdtp_time_display_background).setBackgroundColor(mAccentColor); + view.findViewById(R.id.mdtp_time_display).setBackgroundColor(mAccentColor); + } else { + timePickerHeader.setBackgroundColor(mHeaderColor); + view.findViewById(R.id.mdtp_time_display_background).setBackgroundColor(mHeaderColor); + view.findViewById(R.id.mdtp_time_display).setBackgroundColor(mHeaderColor); + } + // Button text can have a different color if (mOkColor != -1) mOkButton.setTextColor(mOkColor); @@ -980,9 +1159,6 @@ public void onSaveInstanceState(@NonNull Bundle outState) { outState.putInt(KEY_ACCENT, mAccentColor); outState.putBoolean(KEY_VIBRATE, mVibrate); outState.putBoolean(KEY_DISMISS, mDismissOnPause); - outState.putParcelableArray(KEY_SELECTABLE_TIMES, mSelectableTimes); - outState.putParcelable(KEY_MIN_TIME, mMinTime); - outState.putParcelable(KEY_MAX_TIME, mMaxTime); outState.putBoolean(KEY_ENABLE_SECONDS, mEnableSeconds); outState.putBoolean(KEY_ENABLE_MINUTES, mEnableMinutes); outState.putInt(KEY_OK_RESID, mOkResid); @@ -992,6 +1168,8 @@ public void onSaveInstanceState(@NonNull Bundle outState) { outState.putString(KEY_CANCEL_STRING, mCancelString); outState.putInt(KEY_CANCEL_COLOR, mCancelColor); outState.putSerializable(KEY_VERSION, mVersion); + outState.putParcelable(KEY_TIMEPOINTLIMITER, mLimiter); + outState.putSerializable(KEY_LOCALE, mLocale); } } @@ -1032,82 +1210,22 @@ public void enablePicker() { } public boolean isOutOfRange(Timepoint current) { - if(mMinTime != null && mMinTime.compareTo(current) > 0) return true; - - if(mMaxTime != null && mMaxTime.compareTo(current) < 0) return true; - - if(mSelectableTimes != null) return !Arrays.asList(mSelectableTimes).contains(current); - - return false; + return isOutOfRange(current, SECOND_INDEX); } @Override public boolean isOutOfRange(Timepoint current, int index) { - if(current == null) return false; - - if(index == HOUR_INDEX) { - if(mMinTime != null && mMinTime.getHour() > current.getHour()) return true; - - if(mMaxTime != null && mMaxTime.getHour()+1 <= current.getHour()) return true; - - if(mSelectableTimes != null) { - for(Timepoint t : mSelectableTimes) { - if(t.getHour() == current.getHour()) return false; - } - return true; - } - - return false; - } - else if(index == MINUTE_INDEX) { - if(mMinTime != null) { - Timepoint roundedMin = new Timepoint(mMinTime.getHour(), mMinTime.getMinute()); - if (roundedMin.compareTo(current) > 0) return true; - } - - if(mMaxTime != null) { - Timepoint roundedMax = new Timepoint(mMaxTime.getHour(), mMaxTime.getMinute(), 59); - if (roundedMax.compareTo(current) < 0) return true; - } - - if(mSelectableTimes != null) { - for(Timepoint t : mSelectableTimes) { - if(t.getHour() == current.getHour() && t.getMinute() == current.getMinute()) return false; - } - return true; - } - - return false; - } - else return isOutOfRange(current); + return mLimiter.isOutOfRange(current, index, getPickerResolution()); } @Override public boolean isAmDisabled() { - Timepoint midday = new Timepoint(12); - - if(mMinTime != null && mMinTime.compareTo(midday) > 0) return true; - - if(mSelectableTimes != null) { - for(Timepoint t : mSelectableTimes) if(t.compareTo(midday) < 0) return false; - return true; - } - - return false; + return mLimiter.isAmDisabled(); } @Override public boolean isPmDisabled() { - Timepoint midday = new Timepoint(12); - - if(mMaxTime != null && mMaxTime.compareTo(midday) < 0) return true; - - if(mSelectableTimes != null) { - for(Timepoint t : mSelectableTimes) if(t.compareTo(midday) >= 0) return false; - return true; - } - - return false; + return mLimiter.isPmDisabled(); } /** @@ -1121,34 +1239,17 @@ private Timepoint roundToNearest(@NonNull Timepoint time) { @Override public Timepoint roundToNearest(@NonNull Timepoint time, @Nullable Timepoint.TYPE type) { + return mLimiter.roundToNearest(time, type, getPickerResolution()); + } - if(mMinTime != null && mMinTime.compareTo(time) > 0) return mMinTime; - - if(mMaxTime != null && mMaxTime.compareTo(time) < 0) return mMaxTime; - if(mSelectableTimes != null) { - int currentDistance = Integer.MAX_VALUE; - Timepoint output = time; - for(Timepoint t : mSelectableTimes) { - Log.d("Timepoing", "" + type + " " + time + " compared to " + t.toString()); - // type == null: no restrictions - // type == HOUR: do not change the hour - if (type == Timepoint.TYPE.HOUR && t.getHour() != time.getHour()) continue; - // type == MINUTE: do not change hour or minute - if (type == Timepoint.TYPE.MINUTE && t.getHour() != time.getHour() && t.getMinute() != time.getMinute()) continue; - // type == SECOND: cannot change anything, return input - if (type == Timepoint.TYPE.SECOND) return time; - Log.d("Timepoing", "Comparing"); - int newDistance = Math.abs(t.compareTo(time)); - if (newDistance < currentDistance) { - currentDistance = newDistance; - output = t; - } - else break; - } - return output; - } - - return time; + /** + * Get the configured resolution of the current picker in terms of Timepoint components + * @return Timepoint.TYPE (hour, minute or second) + */ + @NonNull Timepoint.TYPE getPickerResolution() { + if (mEnableSeconds) return Timepoint.TYPE.SECOND; + if (mEnableMinutes) return Timepoint.TYPE.MINUTE; + return Timepoint.TYPE.HOUR; } private void setHour(int value, boolean announce) { @@ -1163,7 +1264,7 @@ private void setHour(int value, boolean announce) { } } - CharSequence text = String.format(format, value); + CharSequence text = String.format(mLocale, format, value); mHourView.setText(text); mHourSpaceView.setText(text); if (announce) { @@ -1175,7 +1276,7 @@ private void setMinute(int value) { if (value == 60) { value = 0; } - CharSequence text = String.format(Locale.getDefault(), "%02d", value); + CharSequence text = String.format(mLocale, "%02d", value); Utils.tryAccessibilityAnnounce(mTimePicker, text); mMinuteView.setText(text); mMinuteSpaceView.setText(text); @@ -1185,7 +1286,7 @@ private void setSecond(int value) { if(value == 60) { value = 0; } - CharSequence text = String.format(Locale.getDefault(), "%02d", value); + CharSequence text = String.format(mLocale, "%02d", value); Utils.tryAccessibilityAnnounce(mTimePicker, text); mSecondView.setText(text); mSecondSpaceView.setText(text); @@ -1279,7 +1380,7 @@ private boolean processKeyUp(int keyCode) { } else if (deleted == getAmOrPmKeyCode(PM)) { deletedKeyStr = mPmText; } else { - deletedKeyStr = String.format("%d", getValFromKeyCode(deleted)); + deletedKeyStr = String.format(mLocale, "%d", getValFromKeyCode(deleted)); } Utils.tryAccessibilityAnnounce(mTimePicker, String.format(mDeletedKeyFormat, deletedKeyStr)); @@ -1346,7 +1447,7 @@ private boolean addKeyIfLegal(int keyCode) { } int val = getValFromKeyCode(keyCode); - Utils.tryAccessibilityAnnounce(mTimePicker, String.format(Locale.getDefault(), "%d", val)); + Utils.tryAccessibilityAnnounce(mTimePicker, String.format(mLocale, "%d", val)); // Automatically fill in 0's if AM or PM was legally entered. if (isTypedTimeFullyLegal()) { if (!mIs24HourMode && mTypedTimes.size() <= (textSize - 1)) { @@ -1381,7 +1482,8 @@ private boolean isTypedTimeFullyLegal() { if (mIs24HourMode) { // For 24-hour mode, the time is legal if the hours and minutes are each legal. Note: // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode. - int[] values = getEnteredTime(null); + Boolean[] enteredZeros = {false, false, false}; + int[] values = getEnteredTime(enteredZeros); return (values[0] >= 0 && values[1] >= 0 && values[1] < 60 && values[2] >= 0 && values[2] < 60); } else { // For AM/PM mode, the time is legal if it contains an AM or PM, as those can only be @@ -1406,7 +1508,8 @@ private int deleteLastTypedKey() { private void finishKbMode(boolean updateDisplays) { mInKbMode = false; if (!mTypedTimes.isEmpty()) { - int values[] = getEnteredTime(null); + Boolean[] enteredZeros = {false, false, false}; + int values[] = getEnteredTime(enteredZeros); mTimePicker.setTime(new Timepoint(values[0], values[1], values[2])); if (!mIs24HourMode) { mTimePicker.setAmOrPm(values[3]); @@ -1501,7 +1604,8 @@ private static int getValFromKeyCode(int keyCode) { * @return A size-3 int array. The first value will be the hours, the second value will be the * minutes, and the third will be either TimePickerDialog.AM or TimePickerDialog.PM. */ - private int[] getEnteredTime(Boolean[] enteredZeros) { + @NonNull + private int[] getEnteredTime(@NonNull Boolean[] enteredZeros) { int amOrPm = -1; int startIndex = 1; if (!mIs24HourMode && isTypedTimeFullyLegal()) { @@ -1571,8 +1675,8 @@ private int getAmOrPmKeyCode(int amOrPm) { char amChar; char pmChar; for (int i = 0; i < Math.max(mAmText.length(), mPmText.length()); i++) { - amChar = mAmText.toLowerCase(Locale.getDefault()).charAt(i); - pmChar = mPmText.toLowerCase(Locale.getDefault()).charAt(i); + amChar = mAmText.toLowerCase(mLocale).charAt(i); + pmChar = mPmText.toLowerCase(mLocale).charAt(i); if (amChar != pmChar) { KeyEvent[] events = kcm.getEvents(new char[]{amChar, pmChar}); // There should be 4 events: a down and up for both AM and PM. @@ -1633,6 +1737,7 @@ private void generateLegalTimesTree() { firstDigit.addChild(secondDigit); return; } + //noinspection ConstantConditions if (!mEnableMinutes && !mIs24HourMode) { // We'll need to use the AM/PM node a lot. // Set up AM and PM to respond to "a" and "p". diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/Timepoint.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/Timepoint.java index 23f65483..536e3ffc 100644 --- a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/Timepoint.java +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/Timepoint.java @@ -4,6 +4,10 @@ import android.os.Parcelable; import android.support.annotation.IntRange; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import static com.wdullaer.materialdatetimepicker.time.Timepoint.TYPE.HOUR; +import static com.wdullaer.materialdatetimepicker.time.Timepoint.TYPE.MINUTE; /** * Simple utility class that represents a time in the day up to second precision @@ -13,6 +17,7 @@ * * Created by wdullaer on 13/10/15. */ +@SuppressWarnings("WeakerAccess") public class Timepoint implements Parcelable, Comparable { private int hour; private int minute; @@ -71,7 +76,7 @@ public boolean isAM() { } public boolean isPM() { - return hour >= 12 && hour < 24; + return !isAM(); } public void setAM() { @@ -82,23 +87,69 @@ public void setPM() { if(hour < 12) hour = (hour + 12) % 24; } + public void add(TYPE type, int value) { + if (type == MINUTE) value *= 60; + if (type == HOUR) value *= 3600; + value += toSeconds(); + + switch (type) { + case SECOND: + second = (value % 3600) % 60; + case MINUTE: + minute = (value % 3600) / 60; + case HOUR: + hour = (value / 3600) % 24; + } + } + + public int get(@NonNull TYPE type) { + switch (type) { + case SECOND: + return getSecond(); + case MINUTE: + return getMinute(); + case HOUR: + default: // Makes the compiler happy + return getHour(); + } + } + + public int toSeconds() { + return 3600 * hour + 60 * minute + second; + } + + @Override + public int hashCode() { + return toSeconds(); + } + @Override public boolean equals(Object o) { - try { - Timepoint other = (Timepoint) o; + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; - return other.getHour() == hour && - other.getMinute() == minute && - other.getSecond() == second; - } - catch(ClassCastException e) { - return false; + Timepoint timepoint = (Timepoint) o; + + return hashCode() == timepoint.hashCode(); + } + + public boolean equals(@Nullable Timepoint time, @NonNull TYPE resolution) { + if (time == null) return false; + boolean output = true; + switch (resolution) { + case SECOND: + output = output && time.getSecond() == getSecond(); + case MINUTE: + output = output && time.getMinute() == getMinute(); + case HOUR: + output = output && time.getHour() == getHour(); } + return output; } @Override public int compareTo(@NonNull Timepoint t) { - return (this.hour - t.hour)*3600 + (this.minute - t.minute)*60 + (this.second - t.second); + return hashCode() - t.hashCode(); } @Override diff --git a/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimepointLimiter.java b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimepointLimiter.java new file mode 100644 index 00000000..6c8938da --- /dev/null +++ b/library/src/main/java/com/wdullaer/materialdatetimepicker/time/TimepointLimiter.java @@ -0,0 +1,20 @@ +package com.wdullaer.materialdatetimepicker.time; + +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +@SuppressWarnings("WeakerAccess") +public interface TimepointLimiter extends Parcelable { + boolean isOutOfRange(@Nullable Timepoint point, int index, @NonNull Timepoint.TYPE resolution); + + boolean isAmDisabled(); + + boolean isPmDisabled(); + + @NonNull Timepoint roundToNearest( + @NonNull Timepoint time, + @Nullable Timepoint.TYPE type, + @NonNull Timepoint.TYPE resolution + ); +} \ No newline at end of file diff --git a/library/src/main/res/layout-land/mdtp_date_picker_dialog.xml b/library/src/main/res/layout-land/mdtp_date_picker_dialog.xml index 9264cbab..394d3b97 100644 --- a/library/src/main/res/layout-land/mdtp_date_picker_dialog.xml +++ b/library/src/main/res/layout-land/mdtp_date_picker_dialog.xml @@ -23,13 +23,15 @@ android:layout_width="match_parent" android:layout_height="@dimen/mdtp_date_picker_view_animator_height" android:gravity="center" - android:orientation="horizontal" > + android:orientation="horizontal" + android:background="@android:color/transparent"> + android:orientation="vertical" + android:background="@android:color/transparent"> @@ -40,4 +42,4 @@ - \ No newline at end of file + diff --git a/library/src/main/res/layout-land/mdtp_date_picker_dialog_v2.xml b/library/src/main/res/layout-land/mdtp_date_picker_dialog_v2.xml index 23a94f6e..99dbc2bc 100644 --- a/library/src/main/res/layout-land/mdtp_date_picker_dialog_v2.xml +++ b/library/src/main/res/layout-land/mdtp_date_picker_dialog_v2.xml @@ -24,7 +24,8 @@ + android:orientation="horizontal" + android:background="@android:color/transparent"> @@ -32,10 +33,11 @@ - + android:orientation="vertical" + android:background="@android:color/transparent"> + - \ No newline at end of file + diff --git a/library/src/main/res/layout-land/mdtp_time_header_label.xml b/library/src/main/res/layout-land/mdtp_time_header_label.xml index 43f65a68..18e1fe1f 100644 --- a/library/src/main/res/layout-land/mdtp_time_header_label.xml +++ b/library/src/main/res/layout-land/mdtp_time_header_label.xml @@ -36,8 +36,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/mdtp_time_placeholder" - android:layout_toLeftOf="@id/mdtp_separator" - android:layout_alignBaseline="@id/mdtp_separator" + android:layout_toLeftOf="@+id/mdtp_separator" + android:layout_alignBaseline="@+id/mdtp_separator" android:visibility="invisible" style="@style/mdtp_time_label" android:importantForAccessibility="no" /> @@ -46,9 +46,10 @@ android:layout_height="wrap_content" android:layout_alignRight="@+id/mdtp_hour_space" android:layout_alignLeft="@+id/mdtp_hour_space" - android:layout_alignBottom="@id/mdtp_separator" + android:layout_alignBottom="@+id/mdtp_separator" android:layout_marginLeft="@dimen/mdtp_extra_time_label_margin" android:layout_marginRight="@dimen/mdtp_extra_time_label_margin" + android:background="@android:color/transparent" android:clipChildren="false" > @@ -69,7 +71,8 @@ android:paddingLeft="@dimen/mdtp_separator_padding" android:paddingRight="@dimen/mdtp_separator_padding" android:layout_centerHorizontal="true" - android:layout_above="@id/mdtp_seconds_space" + android:layout_above="@+id/mdtp_seconds_space" + android:background="@android:color/transparent" android:textColor="@color/mdtp_accent_color_focused" style="@style/mdtp_time_label" android:importantForAccessibility="no" /> @@ -79,8 +82,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/mdtp_time_placeholder" - android:layout_toRightOf="@id/mdtp_separator" - android:layout_alignBaseline="@id/mdtp_separator" + android:layout_toRightOf="@+id/mdtp_separator" + android:layout_alignBaseline="@+id/mdtp_separator" android:visibility="invisible" android:textColor="@color/mdtp_accent_color_focused" style="@style/mdtp_time_label" @@ -88,11 +91,12 @@ + android:layout_marginRight="@dimen/mdtp_extra_time_label_margin" + android:background="@android:color/transparent"> @@ -112,7 +117,8 @@ android:visibility="gone" android:paddingLeft="@dimen/mdtp_separator_padding" android:paddingRight="@dimen/mdtp_separator_padding" - android:layout_toRightOf="@id/mdtp_minutes_space" + android:layout_toRightOf="@+id/mdtp_minutes_space" + android:background="@android:color/transparent" android:textColor="@color/mdtp_accent_color_focused" style="@style/mdtp_time_label" android:importantForAccessibility="no" /> @@ -124,19 +130,21 @@ android:text="@string/mdtp_time_placeholder" android:layout_centerInParent="true" android:visibility="invisible" + android:background="@android:color/transparent" android:textColor="@color/mdtp_accent_color_focused" - android:layout_alignBottom="@id/mdtp_center_view" + android:layout_alignBottom="@+id/mdtp_center_view" style="@style/mdtp_time_label_small" android:importantForAccessibility="no" /> + android:layout_marginRight="@dimen/mdtp_extra_time_label_margin" + android:background="@android:color/transparent"> @@ -154,7 +163,8 @@ android:layout_height="wrap_content" android:orientation="vertical" android:layout_centerHorizontal="true" - android:layout_below="@id/mdtp_seconds_space" + android:layout_below="@+id/mdtp_seconds_space" + android:background="@android:color/transparent" android:layout_marginTop="20dp"> @@ -174,6 +185,7 @@ android:layout_marginTop="6dp" android:paddingLeft="@dimen/mdtp_ampm_left_padding" android:paddingRight="@dimen/mdtp_ampm_left_padding" + android:background="@android:color/transparent" android:textColor="@color/mdtp_accent_color_focused" style="@style/mdtp_ampm_label" android:importantForAccessibility="no" /> diff --git a/library/src/main/res/layout-land/mdtp_time_picker_dialog.xml b/library/src/main/res/layout-land/mdtp_time_picker_dialog.xml index 662a2665..c7fdad62 100644 --- a/library/src/main/res/layout-land/mdtp_time_picker_dialog.xml +++ b/library/src/main/res/layout-land/mdtp_time_picker_dialog.xml @@ -32,7 +32,8 @@ + android:orientation="vertical" + android:background="@android:color/transparent"> - \ No newline at end of file + diff --git a/library/src/main/res/layout-land/mdtp_time_picker_dialog_v2.xml b/library/src/main/res/layout-land/mdtp_time_picker_dialog_v2.xml index 5548bf85..a9060c0e 100644 --- a/library/src/main/res/layout-land/mdtp_time_picker_dialog_v2.xml +++ b/library/src/main/res/layout-land/mdtp_time_picker_dialog_v2.xml @@ -36,7 +36,8 @@ + android:orientation="vertical" + android:background="@android:color/transparent"> - \ No newline at end of file + diff --git a/library/src/main/res/layout/mdtp_date_picker_dialog.xml b/library/src/main/res/layout/mdtp_date_picker_dialog.xml index 47515f87..d4aa58b0 100644 --- a/library/src/main/res/layout/mdtp_date_picker_dialog.xml +++ b/library/src/main/res/layout/mdtp_date_picker_dialog.xml @@ -24,7 +24,8 @@ + android:orientation="vertical" + android:background="@android:color/transparent"> @@ -33,4 +34,4 @@ - \ No newline at end of file + diff --git a/library/src/main/res/layout/mdtp_date_picker_dialog_v2.xml b/library/src/main/res/layout/mdtp_date_picker_dialog_v2.xml index 4190ff99..87b543c0 100644 --- a/library/src/main/res/layout/mdtp_date_picker_dialog_v2.xml +++ b/library/src/main/res/layout/mdtp_date_picker_dialog_v2.xml @@ -15,7 +15,7 @@ limitations under the License. --> - + - \ No newline at end of file + diff --git a/library/src/main/res/layout/mdtp_date_picker_selected_date.xml b/library/src/main/res/layout/mdtp_date_picker_selected_date.xml index 64ca747a..270c2d9a 100644 --- a/library/src/main/res/layout/mdtp_date_picker_selected_date.xml +++ b/library/src/main/res/layout/mdtp_date_picker_selected_date.xml @@ -32,6 +32,7 @@ android:layout_gravity="center" android:clickable="true" android:orientation="vertical" + android:background="@android:color/transparent" android:textColor="@color/mdtp_date_picker_selector" > @@ -68,8 +71,9 @@ android:layout_gravity="center" android:gravity="center_horizontal|top" android:includeFontPadding="false" + android:background="@android:color/transparent" android:textColor="@color/mdtp_date_picker_selector" android:textSize="@dimen/mdtp_selected_date_year_size" tools:text="2015" /> - \ No newline at end of file + diff --git a/library/src/main/res/layout/mdtp_date_picker_selected_date_v2.xml b/library/src/main/res/layout/mdtp_date_picker_selected_date_v2.xml index 369f3d39..ba22fffc 100644 --- a/library/src/main/res/layout/mdtp_date_picker_selected_date_v2.xml +++ b/library/src/main/res/layout/mdtp_date_picker_selected_date_v2.xml @@ -34,6 +34,7 @@ android:layout_marginBottom="4dp" android:gravity="center_vertical" android:includeFontPadding="false" + android:background="@android:color/transparent" android:textColor="@color/mdtp_date_picker_selector" android:textSize="@dimen/mdtp_datepicker_year_selection_text_size" tools:text="2015" /> @@ -45,6 +46,7 @@ android:layout_gravity="center" android:clickable="true" android:orientation="horizontal" + android:background="@android:color/transparent" android:textColor="@color/mdtp_date_picker_selector" > - \ No newline at end of file + diff --git a/library/src/main/res/layout/mdtp_date_picker_view_animator_v2.xml b/library/src/main/res/layout/mdtp_date_picker_view_animator_v2.xml new file mode 100644 index 00000000..45be69b0 --- /dev/null +++ b/library/src/main/res/layout/mdtp_date_picker_view_animator_v2.xml @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/library/src/main/res/layout/mdtp_time_header_label.xml b/library/src/main/res/layout/mdtp_time_header_label.xml index 8acdd19a..e23489ba 100644 --- a/library/src/main/res/layout/mdtp_time_header_label.xml +++ b/library/src/main/res/layout/mdtp_time_header_label.xml @@ -48,6 +48,7 @@ android:layout_marginLeft="@dimen/mdtp_extra_time_label_margin" android:layout_marginRight="@dimen/mdtp_extra_time_label_margin" android:layout_centerVertical="true" + android:background="@android:color/transparent" android:clipChildren="false" > @@ -69,6 +71,7 @@ android:paddingRight="@dimen/mdtp_separator_padding" android:layout_alignRight="@+id/mdtp_center_view" android:layout_centerVertical="true" + android:background="@android:color/transparent" android:textColor="@color/mdtp_accent_color_focused" style="@style/mdtp_time_label" android:importantForAccessibility="no" /> @@ -91,6 +94,7 @@ android:layout_alignLeft="@+id/mdtp_minutes_space" android:layout_marginLeft="@dimen/mdtp_extra_time_label_margin" android:layout_marginRight="@dimen/mdtp_extra_time_label_margin" + android:background="@android:color/transparent" android:layout_centerVertical="true" > @@ -112,6 +117,7 @@ android:paddingRight="@dimen/mdtp_separator_padding" android:layout_toRightOf="@+id/mdtp_minutes_space" android:layout_centerVertical="true" + android:background="@android:color/transparent" android:textColor="@color/mdtp_accent_color_focused" style="@style/mdtp_time_label" android:importantForAccessibility="no" /> @@ -121,9 +127,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/mdtp_time_placeholder" - android:layout_toRightOf="@id/mdtp_separator_seconds" + android:layout_toRightOf="@+id/mdtp_separator_seconds" android:layout_centerVertical="true" android:visibility="invisible" + android:background="@android:color/transparent" android:textColor="@color/mdtp_accent_color_focused" style="@style/mdtp_time_label_small" android:importantForAccessibility="no" /> @@ -136,7 +143,8 @@ android:gravity="center_horizontal" android:text="@string/mdtp_time_placeholder" android:layout_gravity="center" - android:layout_toRightOf="@id/mdtp_separator_seconds" + android:background="@android:color/transparent" + android:layout_toRightOf="@+id/mdtp_separator_seconds" android:layout_alignBaseline="@id/mdtp_separator" android:textColor="@color/mdtp_accent_color_focused" /> @@ -145,8 +153,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" - android:layout_alignBaseline="@id/mdtp_seconds" + android:layout_alignBaseline="@+id/mdtp_seconds" android:baselineAlignedChildIndex="1" + android:background="@android:color/transparent" android:layout_toRightOf="@id/mdtp_seconds"> @@ -166,6 +176,7 @@ android:layout_marginTop="6dp" android:paddingLeft="@dimen/mdtp_ampm_left_padding" android:paddingRight="@dimen/mdtp_ampm_left_padding" + android:background="@android:color/transparent" android:textColor="@color/mdtp_accent_color_focused" style="@style/mdtp_ampm_label" android:importantForAccessibility="no" /> diff --git a/library/src/main/res/values-h330dp/dimens.xml b/library/src/main/res/values-h330dp/dimens.xml deleted file mode 100644 index c2be45f9..00000000 --- a/library/src/main/res/values-h330dp/dimens.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 252dp - 300dip - diff --git a/library/src/main/res/values-land/dimens.xml b/library/src/main/res/values-land/dimens.xml index 79ca1906..8d69ccae 100644 --- a/library/src/main/res/values-land/dimens.xml +++ b/library/src/main/res/values-land/dimens.xml @@ -29,6 +29,7 @@ 170dp 308dp 220dp + 220dp 220dip 270dip diff --git a/library/src/main/res/values-sw600dp/dimens.xml b/library/src/main/res/values-sw600dp/dimens.xml index a63001f6..20d51407 100644 --- a/library/src/main/res/values-sw600dp/dimens.xml +++ b/library/src/main/res/values-sw600dp/dimens.xml @@ -21,6 +21,7 @@ 400dp 400dp + 400dp 360dp 45dp 75dp diff --git a/library/src/main/res/values/dimens.xml b/library/src/main/res/values/dimens.xml index 52fd0a47..7fe6c4cb 100644 --- a/library/src/main/res/values/dimens.xml +++ b/library/src/main/res/values/dimens.xml @@ -26,6 +26,8 @@ 0.12 0.11 0.08 + 0.08 + 0.11 54dp 30dp @@ -39,7 +41,7 @@ 48dip 48dip 24dip - 270dip + 300dip 300dip 270dp @@ -50,11 +52,14 @@ 155dp 140dp 252dp + 300dp 42dp 56dp 12sp 16dp + 2dp + 5dp 45dp 25dp 70dp diff --git a/library/src/main/res/values/strings.xml b/library/src/main/res/values/strings.xml index 1b6ebab0..6e9c9f68 100644 --- a/library/src/main/res/values/strings.xml +++ b/library/src/main/res/values/strings.xml @@ -85,6 +85,7 @@ sans-serif + Roboto-Medium am pm diff --git a/library/src/test/java/com/wdullaer/materialdatetimepicker/date/DatePickerDialogTest.java b/library/src/test/java/com/wdullaer/materialdatetimepicker/date/DatePickerDialogTest.java new file mode 100644 index 00000000..d69e4884 --- /dev/null +++ b/library/src/test/java/com/wdullaer/materialdatetimepicker/date/DatePickerDialogTest.java @@ -0,0 +1,91 @@ +package com.wdullaer.materialdatetimepicker.date; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Calendar; +import java.util.TimeZone; + +public class DatePickerDialogTest { + // isHighlighted + @Test + public void isHighlightedShouldReturnFalseIfNoHighlightedDaysAreSet() { + DatePickerDialog dpd = DatePickerDialog.newInstance(new DatePickerDialog.OnDateSetListener() { + @Override + public void onDateSet(DatePickerDialog view, int year, int monthOfYear, int dayOfMonth) { + + } + }); + Assert.assertFalse(dpd.isHighlighted(1990, 1, 1)); + } + + @Test + public void isHighlightedShouldReturnFalseIfHighlightedDoesNotContainSelection() { + DatePickerDialog dpd = DatePickerDialog.newInstance(new DatePickerDialog.OnDateSetListener() { + @Override + public void onDateSet(DatePickerDialog view, int year, int monthOfYear, int dayOfMonth) { + + } + }); + Calendar highlighted = Calendar.getInstance(); + highlighted.set(Calendar.YEAR, 1990); + highlighted.set(Calendar.MONTH, 1); + highlighted.set(Calendar.DAY_OF_MONTH, 1); + + Calendar[] highlightedDays = {highlighted}; + + dpd.setHighlightedDays(highlightedDays); + + Assert.assertFalse(dpd.isHighlighted(1990, 2, 1)); + } + + @Test + public void isHighlightedShouldReturnTrueIfHighlightedDoesContainSelection() { + DatePickerDialog dpd = DatePickerDialog.newInstance(new DatePickerDialog.OnDateSetListener() { + @Override + public void onDateSet(DatePickerDialog view, int year, int monthOfYear, int dayOfMonth) { + + } + }); + int year = 1990; + int month = 1; + int day = 1; + + Calendar highlighted = Calendar.getInstance(); + highlighted.set(Calendar.YEAR, year); + highlighted.set(Calendar.MONTH, month); + highlighted.set(Calendar.DAY_OF_MONTH, day); + + Calendar[] highlightedDays = {highlighted}; + + dpd.setHighlightedDays(highlightedDays); + + Assert.assertTrue(dpd.isHighlighted(year, month, day)); + } + + @Test + public void isHighlightedShouldBehaveCorrectlyInCustomTimezones() { + String timeZoneString = "Americas/Los_Angeles"; + Calendar initial = Calendar.getInstance(TimeZone.getTimeZone(timeZoneString)); + DatePickerDialog dpd = DatePickerDialog.newInstance(new DatePickerDialog.OnDateSetListener() { + @Override + public void onDateSet(DatePickerDialog view, int year, int monthOfYear, int dayOfMonth) { + + } + }, initial); + int year = 1990; + int month = 1; + int day = 1; + + Calendar highlighted = Calendar.getInstance(TimeZone.getTimeZone(timeZoneString)); + highlighted.set(Calendar.YEAR, year); + highlighted.set(Calendar.MONTH, month); + highlighted.set(Calendar.DAY_OF_MONTH, day); + + Calendar[] highlightedDays = {highlighted}; + + dpd.setHighlightedDays(highlightedDays); + + Assert.assertTrue(dpd.isHighlighted(year, month, day)); + } +} diff --git a/library/src/test/java/com/wdullaer/materialdatetimepicker/date/DefaultDateRangeLimiterPropertyTest.java b/library/src/test/java/com/wdullaer/materialdatetimepicker/date/DefaultDateRangeLimiterPropertyTest.java new file mode 100644 index 00000000..59e76bf3 --- /dev/null +++ b/library/src/test/java/com/wdullaer/materialdatetimepicker/date/DefaultDateRangeLimiterPropertyTest.java @@ -0,0 +1,206 @@ +package com.wdullaer.materialdatetimepicker.date; + +import com.pholser.junit.quickcheck.Property; +import com.pholser.junit.quickcheck.generator.InRange; +import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; +import com.wdullaer.materialdatetimepicker.Utils; + +import org.junit.Assert; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Property based tests for DefaultDateRangeLimiter (quickcheck FTW!) + * Created by wdullaer on 26/04/17. + */ + +@RunWith(JUnitQuickcheck.class) +public class DefaultDateRangeLimiterPropertyTest { + final private DatePickerController controller = new DatePickerController() { + @Override + public void onYearSelected(int year) {} + + @Override + public void onDayOfMonthSelected(int year, int month, int day) {} + + @Override + public void registerOnDateChangedListener(DatePickerDialog.OnDateChangedListener listener) {} + + @Override + public void unregisterOnDateChangedListener(DatePickerDialog.OnDateChangedListener listener) {} + + @Override + public MonthAdapter.CalendarDay getSelectedDay() { + return new MonthAdapter.CalendarDay(Calendar.getInstance(), TimeZone.getDefault()); + } + + @Override + public boolean isThemeDark() { + return false; + } + + @Override + public int getAccentColor() { + return 0; + } + + @Override + public boolean isHighlighted(int year, int month, int day) { + return false; + } + + @Override + public int getFirstDayOfWeek() { + return 0; + } + + @Override + public int getMinYear() { + return 0; + } + + @Override + public int getMaxYear() { + return 0; + } + + @Override + public Calendar getStartDate() { + return Calendar.getInstance(); + } + + @Override + public Calendar getEndDate() { + return Calendar.getInstance(); + } + + @Override + public boolean isOutOfRange(int year, int month, int day) { + return false; + } + + @Override + public void tryVibrate() {} + + @Override + public TimeZone getTimeZone() { + return TimeZone.getDefault(); + } + + @Override + public Locale getLocale() { + return Locale.getDefault(); + } + + @Override + public DatePickerDialog.Version getVersion() { + return DatePickerDialog.Version.VERSION_2; + } + + @Override + public DatePickerDialog.ScrollOrientation getScrollOrientation() { + return DatePickerDialog.ScrollOrientation.HORIZONTAL; + } + }; + + private static Calendar[] datesToCalendars(Date[] dates) { + Calendar[] output = new Calendar[dates.length]; + Calendar day = Calendar.getInstance(); + for (int i = 0; i < dates.length; i++) { + Calendar cal = (Calendar) day.clone(); + cal.setTime(dates[i]); + output[i] = cal; + } + return output; + } + + @Property + public void setToNearestShouldBeInSelectableDays( + @InRange(min = "01/01/1900", max = "12/31/2099", format = "MM/dd/yyyy") Date date, + @InRange(min = "01/01/1900", max = "12/31/2099", format = "MM/dd/yyyy") Date[] dates + ) { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + + Calendar day = Calendar.getInstance(); + day.setTime(date); + + Calendar[] selectables = datesToCalendars(dates); + + limiter.setSelectableDays(selectables); + + // selectableDays are manipulated a bit by the limiter + selectables = limiter.getSelectableDays(); + + // selectables == null when the input is empty + if (selectables == null) Assert.assertEquals( + day.getTimeInMillis(), + limiter.setToNearestDate(day).getTimeInMillis() + ); + else Assert.assertTrue(Arrays.asList(selectables).contains(limiter.setToNearestDate(day))); + } + + @Property + public void setToNearestShouldNeverBeInDisabledDays( + @InRange(min = "01/01/1900", max = "12/31/2099", format = "MM/dd/yyyy") Date date, + @InRange(min = "01/01/1900", max = "12/31/2099", format = "MM/dd/yyyy") Date[] dates + ) { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + + Calendar day = Calendar.getInstance(); + day.setTime(date); + + Calendar[] disableds = datesToCalendars(dates); + + limiter.setDisabledDays(disableds); + Assert.assertFalse(Arrays.asList(disableds).contains(limiter.setToNearestDate(day))); + } + + @Property + public void setToNearestShouldNeverBeBelowMinDate( + @InRange(min = "01/01/1900", max = "12/31/2099", format = "MM/dd/yyyy") Date date, + @InRange(min = "01/01/1900", max = "12/31/2099", format = "MM/dd/yyyy") Date minDate + ) { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + + Calendar day = Calendar.getInstance(); + day.setTime(date); + + Calendar minDay = Calendar.getInstance(); + minDay.setTime(minDate); + + limiter.setMinDate(minDay); + Assert.assertTrue(Utils.trimToMidnight(minDay).getTimeInMillis() <= limiter.setToNearestDate(day).getTimeInMillis()); + } + + @Property + public void setToNearestShouldNeverBeAboveMaxDate( + @InRange(min = "01/01/1800", max = "12/31/2099", format = "MM/dd/yyyy") Date date, + @InRange(min = "01/01/1800", max = "12/31/2099", format = "MM/dd/yyyy") Date maxDate + ) { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + + Calendar day = Calendar.getInstance(); + day.setTime(date); + + Calendar minDay = Calendar.getInstance(); + minDay.set(Calendar.YEAR, 1800); + minDay.set(Calendar.MONTH, Calendar.JANUARY); + minDay.set(Calendar.DAY_OF_MONTH, 1); + Utils.trimToMidnight(minDay); + + Calendar maxDay = Calendar.getInstance(); + maxDay.setTime(maxDate); + + limiter.setMinDate(minDay); + limiter.setMaxDate(maxDay); + Assert.assertTrue(Utils.trimToMidnight(maxDay).getTimeInMillis() >= limiter.setToNearestDate(day).getTimeInMillis()); + } + + // TODO write property based tests that enable all options as one + // TODO ensure generators cover more of the known edge cases in the inputs +} diff --git a/library/src/test/java/com/wdullaer/materialdatetimepicker/date/DefaultDateRangeLimiterTest.java b/library/src/test/java/com/wdullaer/materialdatetimepicker/date/DefaultDateRangeLimiterTest.java new file mode 100644 index 00000000..ef7bbd04 --- /dev/null +++ b/library/src/test/java/com/wdullaer/materialdatetimepicker/date/DefaultDateRangeLimiterTest.java @@ -0,0 +1,700 @@ +package com.wdullaer.materialdatetimepicker.date; + +import com.wdullaer.materialdatetimepicker.Utils; + +import org.junit.Test; +import org.junit.Assert; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Unit tests for the default DateRangeLimiter implementation + * Primarily used to assert that the rounding logic functions properly + * + * Created by wdullaer on 14/04/17. + */ +public class DefaultDateRangeLimiterTest { + final private DatePickerController controller = new DatePickerController() { + @Override + public void onYearSelected(int year) {} + + @Override + public void onDayOfMonthSelected(int year, int month, int day) {} + + @Override + public void registerOnDateChangedListener(DatePickerDialog.OnDateChangedListener listener) {} + + @Override + public void unregisterOnDateChangedListener(DatePickerDialog.OnDateChangedListener listener) {} + + @Override + public MonthAdapter.CalendarDay getSelectedDay() { + return new MonthAdapter.CalendarDay(Calendar.getInstance(), TimeZone.getDefault()); + } + + @Override + public boolean isThemeDark() { + return false; + } + + @Override + public int getAccentColor() { + return 0; + } + + @Override + public boolean isHighlighted(int year, int month, int day) { + return false; + } + + @Override + public int getFirstDayOfWeek() { + return 0; + } + + @Override + public int getMinYear() { + return 0; + } + + @Override + public int getMaxYear() { + return 0; + } + + @Override + public Calendar getStartDate() { + return Calendar.getInstance(); + } + + @Override + public Calendar getEndDate() { + return Calendar.getInstance(); + } + + @Override + public boolean isOutOfRange(int year, int month, int day) { + return false; + } + + @Override + public void tryVibrate() {} + + @Override + public TimeZone getTimeZone() { + return TimeZone.getDefault(); + } + + @Override + public Locale getLocale() { + return Locale.getDefault(); + } + + @Override + public DatePickerDialog.Version getVersion() { + return DatePickerDialog.Version.VERSION_2; + } + + @Override + public DatePickerDialog.ScrollOrientation getScrollOrientation() { + return DatePickerDialog.ScrollOrientation.HORIZONTAL; + } + }; + + // getters + @Test + public void getSelectableDaysShouldHaveDatesTrimmedToMidnight() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar[] days = new Calendar[3]; + for (int i = 0;i < days.length; i++) { + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1999 + i); + day.set(Calendar.HOUR_OF_DAY, 2); + day.set(Calendar.MINUTE, 10); + day.set(Calendar.SECOND, 30); + day.set(Calendar.MILLISECOND, 25); + days[i] = day; + } + + limiter.setSelectableDays(days); + Calendar[] selectableDays = limiter.getSelectableDays(); + + Assert.assertNotNull(selectableDays); + Assert.assertEquals(days.length, selectableDays.length); + for (Calendar selectableDay : selectableDays) { + Assert.assertEquals(selectableDay.get(Calendar.HOUR_OF_DAY), 0); + Assert.assertEquals(selectableDay.get(Calendar.MINUTE), 0); + Assert.assertEquals(selectableDay.get(Calendar.SECOND), 0); + Assert.assertEquals(selectableDay.get(Calendar.MILLISECOND), 0); + } + } + + @Test + public void getDisabledDaysShouldHaveDatesTrimmedToMidnight() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar[] days = new Calendar[3]; + for (int i = 0;i < days.length; i++) { + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1999 + i); + day.set(Calendar.HOUR_OF_DAY, 2); + day.set(Calendar.MINUTE, 10); + day.set(Calendar.SECOND, 30); + day.set(Calendar.MILLISECOND, 25); + days[i] = day; + } + + limiter.setDisabledDays(days); + Calendar[] disabledDays = limiter.getDisabledDays(); + + Assert.assertNotNull(disabledDays); + Assert.assertEquals(days.length, disabledDays.length); + for (Calendar selectableDay : disabledDays) { + Assert.assertEquals(selectableDay.get(Calendar.HOUR_OF_DAY), 0); + Assert.assertEquals(selectableDay.get(Calendar.MINUTE), 0); + Assert.assertEquals(selectableDay.get(Calendar.SECOND), 0); + Assert.assertEquals(selectableDay.get(Calendar.MILLISECOND), 0); + } + } + + @Test + public void getMinDateShouldHaveDateTrimmedToMidnight() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1999); + day.set(Calendar.HOUR_OF_DAY, 2); + day.set(Calendar.MINUTE, 10); + day.set(Calendar.SECOND, 30); + day.set(Calendar.MILLISECOND, 25); + + limiter.setMinDate(day); + Calendar minDate = limiter.getMinDate(); + + Assert.assertNotNull(minDate); + Assert.assertEquals(minDate.get(Calendar.HOUR_OF_DAY), 0); + Assert.assertEquals(minDate.get(Calendar.MINUTE), 0); + Assert.assertEquals(minDate.get(Calendar.SECOND), 0); + Assert.assertEquals(minDate.get(Calendar.MILLISECOND), 0); + } + + @Test + public void getMaxDateShouldHaveDateTrimmedToMidnight() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1999); + day.set(Calendar.HOUR_OF_DAY, 2); + day.set(Calendar.MINUTE, 10); + day.set(Calendar.SECOND, 30); + day.set(Calendar.MILLISECOND, 25); + + limiter.setMaxDate(day); + Calendar maxDate = limiter.getMaxDate(); + + Assert.assertNotNull(maxDate); + Assert.assertEquals(maxDate.get(Calendar.HOUR_OF_DAY), 0); + Assert.assertEquals(maxDate.get(Calendar.MINUTE), 0); + Assert.assertEquals(maxDate.get(Calendar.SECOND), 0); + Assert.assertEquals(maxDate.get(Calendar.MILLISECOND), 0); + } + + // getStartDate() + @Test + public void getStartDateShouldReturnFirstSelectableDay() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar[] days = new Calendar[3]; + for (int i = 0; i < days.length; i++) { + days[i] = Calendar.getInstance(); + days[i].set(Calendar.YEAR, 1999 + i); + } + + limiter.setSelectableDays(days); + + // selectableDays are manipulated a bit by the limiter + days = limiter.getSelectableDays(); + + Assert.assertNotNull(days); + Assert.assertEquals(limiter.getStartDate().getTimeInMillis(), days[0].getTimeInMillis()); + } + + @Test + public void getStartDateShouldReturnMinDate() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar minDate = Calendar.getInstance(); + + limiter.setMinDate(minDate); + minDate = Utils.trimToMidnight(minDate); + + Assert.assertEquals(limiter.getStartDate().getTimeInMillis(), minDate.getTimeInMillis()); + } + + @Test + public void getStartDateShouldReturnMinDateWhenAControllerIsSet() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + limiter.setController(controller); + Calendar minDate = Calendar.getInstance(); + + limiter.setMinDate(minDate); + minDate = Utils.trimToMidnight(minDate); + + Assert.assertEquals(limiter.getStartDate().getTimeInMillis(), minDate.getTimeInMillis()); + } + + @Test + public void getStartDateShouldPreferSelectableOverMinDate() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar[] days = new Calendar[3]; + for (int i = 0; i < days.length; i++) { + days[i] = Calendar.getInstance(); + days[i].set(Calendar.YEAR, 1999 + i); + } + Calendar minDate = Calendar.getInstance(); + + limiter.setSelectableDays(days); + limiter.setMinDate(minDate); + + // selectableDays are manipulated a bit by the limiter + days = limiter.getSelectableDays(); + + Assert.assertNotNull(days); + Assert.assertEquals(limiter.getStartDate().getTimeInMillis(), days[0].getTimeInMillis()); + } + + // getEndDate() + @Test + public void getEndDateShouldReturnLastSelectableDay() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar[] days = new Calendar[3]; + for (int i = 0; i < days.length; i++) { + days[i] = Calendar.getInstance(); + days[i].set(Calendar.YEAR, 1999 + i); + } + + limiter.setSelectableDays(days); + + // selectableDays are manipulated a bit by the limiter + days = limiter.getSelectableDays(); + + Assert.assertNotNull(days); + Assert.assertEquals(limiter.getEndDate().getTimeInMillis(), days[days.length - 1].getTimeInMillis()); + } + + @Test + public void getEndDateShouldReturnMaxDate() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar maxDate = Calendar.getInstance(); + + limiter.setMaxDate(maxDate); + maxDate = Utils.trimToMidnight(maxDate); + + Assert.assertEquals(limiter.getEndDate().getTimeInMillis(), maxDate.getTimeInMillis()); + } + + @Test + public void getEndDateShouldReturnMaxDateWhenAControllerIsSet() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + limiter.setController(controller); + Calendar maxDate = Calendar.getInstance(); + + limiter.setMaxDate(maxDate); + maxDate = Utils.trimToMidnight(maxDate); + + Assert.assertEquals(limiter.getEndDate().getTimeInMillis(), maxDate.getTimeInMillis()); + } + + @Test + public void getEndDateShouldPreferSelectableOverMaxDate() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar[] days = new Calendar[3]; + for (int i = 0; i < days.length; i++) { + days[i] = Calendar.getInstance(); + days[i].set(Calendar.YEAR, 1999 + i); + } + Calendar maxDate = Calendar.getInstance(); + + limiter.setSelectableDays(days); + limiter.setMinDate(maxDate); + + // selectableDays are manipulated a bit by the limiter + days = limiter.getSelectableDays(); + + Assert.assertNotNull(days); + Assert.assertEquals(limiter.getEndDate().getTimeInMillis(), days[days.length - 1].getTimeInMillis()); + } + + // isOutOfRange() + @Test + public void isOutOfRangeShouldReturnTrueForDisabledDates() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar[] days = new Calendar[1]; + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1999); + days[0] = day; + + limiter.setDisabledDays(days); + int year = day.get(Calendar.YEAR); + int month = day.get(Calendar.MONTH); + int dayNumber = day.get(Calendar.DAY_OF_MONTH); + + Assert.assertTrue(limiter.isOutOfRange(year, month, dayNumber)); + } + + @Test + public void isOutOfRangeShouldReturnFalseForEnabledDates() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar[] days = new Calendar[1]; + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1999); + days[0] = day; + + limiter.setSelectableDays(days); + int year = day.get(Calendar.YEAR); + int month = day.get(Calendar.MONTH); + int dayNumber = day.get(Calendar.DAY_OF_MONTH); + + Assert.assertFalse(limiter.isOutOfRange(year, month, dayNumber)); + } + + @Test + public void isOutOfRangeShouldReturnTrueIfDateIsBeforeMin() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1999); + + limiter.setMinDate(day); + day.add(Calendar.DAY_OF_MONTH, -1); + int year = day.get(Calendar.YEAR); + int month = day.get(Calendar.MONTH); + int dayNumber = day.get(Calendar.DAY_OF_MONTH); + + Assert.assertTrue(limiter.isOutOfRange(year, month, dayNumber)); + } + + @Test + public void isOutOfRangeShouldReturnTrueIfDateIsBeforeMinYear() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + int minYear = 1999; + + limiter.setYearRange(minYear, minYear + 1); + + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, minYear - 1); + int year = day.get(Calendar.YEAR); + int month = day.get(Calendar.MONTH); + int dayNumber = day.get(Calendar.DAY_OF_MONTH); + + Assert.assertTrue(limiter.isOutOfRange(year, month, dayNumber)); + } + + @Test + public void isOutOfRangeShouldReturnTrueIfDateIsAfterMax() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1999); + + limiter.setMaxDate(day); + day.add(Calendar.DAY_OF_MONTH, 1); + int year = day.get(Calendar.YEAR); + int month = day.get(Calendar.MONTH); + int dayNumber = day.get(Calendar.DAY_OF_MONTH); + + Assert.assertTrue(limiter.isOutOfRange(year, month, dayNumber)); + } + + @Test + public void isOutOfRangeShouldReturnTrueIfDateIsAfterMaxYear() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + int maxYear = 1999; + + limiter.setYearRange(maxYear - 1, maxYear); + + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, maxYear + 1); + int year = day.get(Calendar.YEAR); + int month = day.get(Calendar.MONTH); + int dayNumber = day.get(Calendar.DAY_OF_MONTH); + + Assert.assertTrue(limiter.isOutOfRange(year, month, dayNumber)); + } + + @Test + public void isOutOfRangeShouldPreferDisabledOverEnabled() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar[] days = new Calendar[1]; + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1999); + days[0] = day; + + limiter.setSelectableDays(days); + limiter.setDisabledDays(days); + int year = day.get(Calendar.YEAR); + int month = day.get(Calendar.MONTH); + int dayNumber = day.get(Calendar.DAY_OF_MONTH); + + Assert.assertTrue(limiter.isOutOfRange(year, month, dayNumber)); + } + + @Test + public void isOutOfRangeShouldWorkWithCustomTimeZones() { + final String timeZoneString = "America/Los_Angeles"; + TimeZone timeZone = TimeZone.getTimeZone(timeZoneString); + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + + int year = 1985; + int month = 1; + int day = 1; + Calendar disabledDay = Calendar.getInstance(timeZone); + disabledDay.set(Calendar.YEAR, year); + disabledDay.set(Calendar.MONTH, month); + disabledDay.set(Calendar.DAY_OF_MONTH, day); + Calendar[] days = new Calendar[1]; + days[0] = disabledDay; + DatePickerController controller = new DatePickerController() { + @Override + public void onYearSelected(int year) {} + + @Override + public void onDayOfMonthSelected(int year, int month, int day) {} + + @Override + public void registerOnDateChangedListener(DatePickerDialog.OnDateChangedListener listener) {} + + @Override + public void unregisterOnDateChangedListener(DatePickerDialog.OnDateChangedListener listener) {} + + @Override + public MonthAdapter.CalendarDay getSelectedDay() { + return null; + } + + @Override + public boolean isThemeDark() { + return false; + } + + @Override + public int getAccentColor() { + return 0; + } + + @Override + public boolean isHighlighted(int year, int month, int day) { + return false; + } + + @Override + public int getFirstDayOfWeek() { + return 0; + } + + @Override + public int getMinYear() { + return 0; + } + + @Override + public int getMaxYear() { + return 0; + } + + @Override + public Calendar getStartDate() { + return null; + } + + @Override + public Calendar getEndDate() { + return null; + } + + @Override + public boolean isOutOfRange(int year, int month, int day) { + return false; + } + + @Override + public void tryVibrate() { + + } + + @Override + public TimeZone getTimeZone() { + return TimeZone.getTimeZone(timeZoneString); + } + + @Override + public Locale getLocale() { + return Locale.getDefault(); + } + + @Override + public DatePickerDialog.Version getVersion() { + return null; + } + + @Override + public DatePickerDialog.ScrollOrientation getScrollOrientation() { + return null; + } + }; + + limiter.setDisabledDays(days); + limiter.setController(controller); + + Assert.assertTrue(limiter.isOutOfRange(year, month, day)); + } + + // setToNearestDate() + @Test + public void setToNearestShouldReturnTheInputWhenValid() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar day = Calendar.getInstance(); + Calendar expected = (Calendar) day.clone(); + + Assert.assertEquals(limiter.setToNearestDate(day).getTimeInMillis(), expected.getTimeInMillis()); + } + + @Test + public void setToNearestShouldRoundDisabledDates() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar[] days = new Calendar[3]; + for (int i = 0;i < days.length; i++) { + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1999 + i); + day.set(Calendar.HOUR_OF_DAY, 2); + day.set(Calendar.MINUTE, 10); + day.set(Calendar.SECOND, 30); + day.set(Calendar.MILLISECOND, 25); + days[i] = day; + } + + limiter.setDisabledDays(days); + Calendar day = (Calendar) days[0].clone(); + + Assert.assertNotSame(limiter.setToNearestDate(day).getTimeInMillis(), days[0].getTimeInMillis()); + } + + @Test + public void setToNearestShouldRoundToMinDate() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar minDate = Calendar.getInstance(); + minDate.set(Calendar.YEAR, 1999); + + limiter.setMinDate(minDate); + + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1998); + + Assert.assertEquals( + limiter.setToNearestDate(day).getTimeInMillis(), + Utils.trimToMidnight(minDate).getTimeInMillis() + ); + } + + @Test + public void setToNearestShouldRoundToMaxDate() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar maxDate = Calendar.getInstance(); + maxDate.set(Calendar.YEAR, 1999); + + limiter.setMaxDate(maxDate); + + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 2000); + + Assert.assertEquals( + limiter.setToNearestDate(day).getTimeInMillis(), + Utils.trimToMidnight(maxDate).getTimeInMillis() + ); + } + + @Test + public void setToNearestShouldRoundToASelectableDay() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + Calendar[] days = new Calendar[3]; + for (int i = 0;i < days.length; i++) { + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1999 + i); + day.set(Calendar.HOUR_OF_DAY, 2); + day.set(Calendar.MINUTE, 10); + day.set(Calendar.SECOND, 30); + day.set(Calendar.MILLISECOND, 25); + days[i] = day; + } + + limiter.setSelectableDays(days); + Calendar day = Calendar.getInstance(); + + // selectableDays are manipulated a bit by the limiter + days = limiter.getSelectableDays(); + + Assert.assertNotNull(days); + Assert.assertTrue(Arrays.asList(days).contains(limiter.setToNearestDate(day))); + } + + @Test + public void setToNearestShouldRoundToASelectableDayWhenAControllerIsSet() { + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + limiter.setController(controller); + Calendar[] days = new Calendar[3]; + for (int i = 0;i < days.length; i++) { + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1999 + i); + day.set(Calendar.HOUR_OF_DAY, 2); + day.set(Calendar.MINUTE, 10); + day.set(Calendar.SECOND, 30); + day.set(Calendar.MILLISECOND, 25); + days[i] = day; + } + + limiter.setSelectableDays(days); + Calendar day = Calendar.getInstance(); + + // selectableDays are manipulated a bit by the limiter + days = limiter.getSelectableDays(); + + Assert.assertNotNull(days); + Assert.assertTrue(Arrays.asList(days).contains(limiter.setToNearestDate(day))); + } + + @Test + public void setToNearestShouldRoundToFirstJanOfMinYearWhenBeforeMin() { + // Case with just year range and no other restrictions + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + + limiter.setYearRange(1980, 2100); + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1970); + + Calendar expectedDay = Calendar.getInstance(); + expectedDay.set(Calendar.YEAR, 1980); + expectedDay.set(Calendar.MONTH, Calendar.JANUARY); + expectedDay.set(Calendar.DAY_OF_MONTH, 1); + + Assert.assertEquals( + Utils.trimToMidnight(expectedDay).getTimeInMillis(), + limiter.setToNearestDate(day).getTimeInMillis() + ); + } + + @Test + public void setToNearestShouldReturn31stDecOfMaxYearWhenAfterMax() { + // Case with just year range and no other restrictions + DefaultDateRangeLimiter limiter = new DefaultDateRangeLimiter(); + + limiter.setYearRange(1900, 1950); + Calendar day = Calendar.getInstance(); + day.set(Calendar.YEAR, 1970); + + Calendar expectedDay = Calendar.getInstance(); + expectedDay.set(Calendar.YEAR, 1950); + expectedDay.set(Calendar.MONTH, Calendar.DECEMBER); + expectedDay.set(Calendar.DAY_OF_MONTH, 31); + + Assert.assertEquals( + Utils.trimToMidnight(expectedDay).getTimeInMillis(), + limiter.setToNearestDate(day).getTimeInMillis() + ); + } +} \ No newline at end of file diff --git a/library/src/test/java/com/wdullaer/materialdatetimepicker/time/DefaultTimepointLimiterTest.java b/library/src/test/java/com/wdullaer/materialdatetimepicker/time/DefaultTimepointLimiterTest.java new file mode 100644 index 00000000..6f21b879 --- /dev/null +++ b/library/src/test/java/com/wdullaer/materialdatetimepicker/time/DefaultTimepointLimiterTest.java @@ -0,0 +1,835 @@ +package com.wdullaer.materialdatetimepicker.time; + +import static com.wdullaer.materialdatetimepicker.time.TimePickerDialog.HOUR_INDEX; +import static com.wdullaer.materialdatetimepicker.time.TimePickerDialog.MINUTE_INDEX; + +import org.junit.Test; +import org.junit.Assert; + +/** + * Unit tests for the default implementation of TimepointLimiter + * Mostly used to assert that the rounding logic works + * + * Created by wdullaer on 22/06/17. + */ +public class DefaultTimepointLimiterTest { + @Test + public void isAmDisabledShouldReturnTrueWhenMinTimeIsInTheAfternoon() { + Timepoint minTime = new Timepoint(13); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMinTime(minTime); + + Assert.assertTrue(limiter.isAmDisabled()); + } + + @Test + public void isAmDisabledShouldReturnFalseWhenMinTimeIsInTheMorning() { + Timepoint minTime = new Timepoint(8); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMinTime(minTime); + + Assert.assertFalse(limiter.isAmDisabled()); + } + + @Test + public void isAmDisabledShouldReturnTrueWhenMinTimeIsMidday() { + Timepoint minTime = new Timepoint(12); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMinTime(minTime); + + Assert.assertTrue(limiter.isAmDisabled()); + } + + @Test + public void isAmDisabledShouldReturnFalseWhenMaxTimeIsInTheMorning() { + Timepoint maxTime = new Timepoint(8); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMaxTime(maxTime); + + Assert.assertFalse(limiter.isAmDisabled()); + } + + @Test + public void isAmDisabledShouldReturnFalseWhenMaxTimeIsMidday() { + Timepoint maxTime = new Timepoint(12); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMaxTime(maxTime); + + Assert.assertFalse(limiter.isAmDisabled()); + } + + @Test + public void isPmDisabledShouldReturnTrueWhenMaxTimeIsInTheMorning() { + Timepoint maxTime = new Timepoint(9); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMaxTime(maxTime); + + Assert.assertTrue(limiter.isPmDisabled()); + } + + @Test + public void isPmDisabledShouldReturnFalseWhenMinTimeIsInTheAfternoon() { + Timepoint minTime = new Timepoint(13); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMinTime(minTime); + + Assert.assertFalse(limiter.isPmDisabled()); + } + + @Test + public void isPmDisabledShouldReturnFalseWhenMaxTimeIsMidday() { + Timepoint maxTime = new Timepoint(12); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMaxTime(maxTime); + + Assert.assertFalse(limiter.isPmDisabled()); + } + + @Test + public void isAmDisabledShouldReturnTrueIfSelectableDaysAreInTheAfternoon() { + Timepoint[] selectableDays = { + new Timepoint(13), + new Timepoint(22) + }; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableDays); + + Assert.assertTrue(limiter.isAmDisabled()); + } + + @Test + public void isAmDisabledShouldReturnFalseIfSelectableDaysHasOneTimeInTheMorning() { + Timepoint[] selectableDays = { + new Timepoint(4), + new Timepoint(13), + new Timepoint(22) + }; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableDays); + + Assert.assertFalse(limiter.isAmDisabled()); + } + + @Test + public void isPmDisabledShouldReturnTrueIfSelectableDaysAreInTheMorning() { + Timepoint[] selectableDays = { + new Timepoint(4), + new Timepoint(9), + new Timepoint(11) + }; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableDays); + + Assert.assertTrue(limiter.isPmDisabled()); + } + + @Test + public void isPmDisabledShouldReturnFalseIfSelectableDaysHasOneTimeInTheAfternoon() { + Timepoint[] selectableDays = { + new Timepoint(4), + new Timepoint(22) + }; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableDays); + + Assert.assertFalse(limiter.isPmDisabled()); + } + + @Test + public void isPmDisabledShouldReturnFalseIfSelectableDaysContainsMidday() { + Timepoint[] selectableDays = { + new Timepoint(4), + new Timepoint(9), + new Timepoint(12) + }; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableDays); + + Assert.assertFalse(limiter.isPmDisabled()); + } + + @Test + public void isPmDisabledShouldReturnFalseWithoutConstraints() { + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + Assert.assertFalse(limiter.isPmDisabled()); + } + + @Test + public void isAmDisabledShouldReturnFalseWithoutConstraints() { + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + Assert.assertFalse(limiter.isAmDisabled()); + } + + @Test + public void setMinTimeShouldThrowExceptionWhenBiggerThanMaxTime() { + Timepoint maxTime = new Timepoint(2); + Timepoint minTime = new Timepoint(3); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMaxTime(maxTime); + + try { + limiter.setMinTime(minTime); + Assert.fail("setMinTime() should throw IllegalArgumentException"); + } catch (IllegalArgumentException e) { + Assert.assertNotNull(e); + } + } + + @Test + public void setMaxTimeShouldThrowExceptionWhenSmallerThanMinTime() { + Timepoint maxTime = new Timepoint(2); + Timepoint minTime = new Timepoint(3); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMinTime(minTime); + + try { + limiter.setMaxTime(maxTime); + Assert.fail("setMaxTime() should throw IllegalArgumentException"); + } catch (IllegalArgumentException e) { + Assert.assertNotNull(e); + } + } + + @Test + public void isOutOfRangeShouldReturnTrueIfInputSmallerThanMinTime() { + Timepoint minTime = new Timepoint(10); + Timepoint input = new Timepoint(2); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMinTime(minTime); + + Assert.assertTrue(limiter.isOutOfRange(input)); + } + + @Test + public void isOutOfRangeShouldReturnTrueIfInputLargerThanMaxTime() { + Timepoint maxTime = new Timepoint(2); + Timepoint input = new Timepoint(12); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMaxTime(maxTime); + + Assert.assertTrue(limiter.isOutOfRange(input)); + } + + @Test + public void isOutOfRangeShouldReturnFalseIfInputIsBetweenMinAndMaxTime() { + Timepoint minTime = new Timepoint(1); + Timepoint maxTime = new Timepoint(13); + Timepoint input = new Timepoint(4); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMinTime(minTime); + limiter.setMaxTime(maxTime); + + Assert.assertFalse(limiter.isOutOfRange(input)); + } + + @Test + public void isOutOfRangeShouldReturnFalseWithoutRestraints() { + Timepoint input = new Timepoint(14); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + Assert.assertFalse(limiter.isOutOfRange(input)); + } + + @Test + public void isOutOfRangeShouldReturnTrueIfInputNotSelectable() { + Timepoint input = new Timepoint(1); + Timepoint[] selectableDays = { + new Timepoint(13), + new Timepoint(14) + }; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableDays); + + Assert.assertTrue(limiter.isOutOfRange(input)); + } + + @Test + public void isOutOfRangeShouldReturnFalseIfInputSelectable() { + Timepoint input = new Timepoint(15); + Timepoint[] selectableDays = { + new Timepoint(4), + new Timepoint(10), + new Timepoint(15) + }; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableDays); + + Assert.assertFalse(limiter.isOutOfRange(input)); + } + + @Test + public void isOutOfRangeWithIndexShouldHandleNull() { + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + Assert.assertFalse(limiter.isOutOfRange(null, HOUR_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void isOutOfRangeMinuteShouldReturnFalseWhenMinTimeEqualsToTheMinute() { + Timepoint minTime = new Timepoint(12, 13, 14); + Timepoint input = new Timepoint(12, 13); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMinTime(minTime); + + Assert.assertFalse(limiter.isOutOfRange(input, MINUTE_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void isOutOfRangeMinuteShouldReturnFalseWhenMaxTimeEqualsToTheMinute() { + Timepoint maxTime = new Timepoint(12, 13, 14); + Timepoint input = new Timepoint(12, 13); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMaxTime(maxTime); + + Assert.assertFalse(limiter.isOutOfRange(input, MINUTE_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void isOutOfRangeMinuteShouldReturnFalseWhenTimeEqualsSelectableTimeToTheMinute() { + Timepoint[] selectableTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12, 13, 14), + new Timepoint(13, 14, 15) + }; + Timepoint input = new Timepoint(12, 13); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertFalse(limiter.isOutOfRange(input, MINUTE_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void isOutOfRangeMinuteShouldReturnFalseWhenTimeEqualsSelectableTimeToTheMinute2() { + Timepoint[] selectableTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12, 13, 14), + new Timepoint(13, 14, 15) + }; + Timepoint input = new Timepoint(13, 14, 59); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertFalse(limiter.isOutOfRange(input, MINUTE_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void isOutOfRangeMinuteShouldReturnFalseWhenTimeEqualsSelectableTimeToTheMinute3() { + Timepoint[] selectableTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12, 13, 14), + new Timepoint(13, 14, 15) + }; + Timepoint input = new Timepoint(11, 12, 0); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertFalse(limiter.isOutOfRange(input, MINUTE_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void isOutOfRangeMinuteShouldReturnTrueWhenTimeDoesNotEqualSelectableTimeToTheMinute() { + Timepoint[] selectableTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12, 13, 14), + new Timepoint(13, 14, 15) + }; + Timepoint input = new Timepoint(11, 11, 0); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertTrue(limiter.isOutOfRange(input, MINUTE_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void isOutOfRangeHourShouldReturnFalseWhenMinTimeEqualsToTheHour() { + Timepoint minTime = new Timepoint(12, 13, 14); + Timepoint input = new Timepoint(12); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMinTime(minTime); + + Assert.assertFalse(limiter.isOutOfRange(input, HOUR_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void isOutOfRangeHourShouldReturnFalseWhenMaxTimeEqualsToTheHour() { + Timepoint maxTime = new Timepoint(12, 13, 14); + Timepoint input = new Timepoint(12); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMaxTime(maxTime); + + Assert.assertFalse(limiter.isOutOfRange(input, HOUR_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void isOutOfRangeHourShouldReturnFalseWhenTimeEqualsSelectableTimeToTheHour() { + Timepoint[] selectableTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12, 13, 14), + new Timepoint(13, 14, 15) + }; + Timepoint input = new Timepoint(12); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertFalse(limiter.isOutOfRange(input, HOUR_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void isOutOfRangeHourShouldReturnFalseWhenTimeEqualsSelectableTimeToTheHour2() { + Timepoint[] selectableTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12, 13, 14), + new Timepoint(13, 14, 15) + }; + Timepoint input = new Timepoint(13, 15, 15); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertFalse(limiter.isOutOfRange(input, HOUR_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void isOutOfRangeHourShouldReturnFalseWhenTimeEqualsSelectableTimeToTheHour3() { + Timepoint[] selectableTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12, 13, 14), + new Timepoint(13, 14, 15) + }; + Timepoint input = new Timepoint(11); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertFalse(limiter.isOutOfRange(input, HOUR_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void isOutOfRangeShouldWorkWhenSelectableTimesContainsDuplicateEntries() { + Timepoint[] selectableTimes = { + new Timepoint(11), + new Timepoint(12), + new Timepoint(12), + new Timepoint(13) + }; + Timepoint input = new Timepoint(11, 30); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertTrue(limiter.isOutOfRange(input)); + } + + @Test + public void isOutOfRangeShouldReturnTrueWhenInputIsInDisabledTimes() { + Timepoint[] disabledTimes = { + new Timepoint(11), + new Timepoint(12), + new Timepoint(13) + }; + Timepoint input = disabledTimes[0]; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setDisabledTimes(disabledTimes); + + Assert.assertTrue(limiter.isOutOfRange(input)); + } + + @Test + public void isOutOfRangeShouldUseDisabledOverSelectable() { + Timepoint[] disabledTimes = { + new Timepoint(11), + new Timepoint(12), + new Timepoint(13) + }; + Timepoint[] selectableTimes = { + new Timepoint(12), + new Timepoint(14) + }; + Timepoint input = new Timepoint(12); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setDisabledTimes(disabledTimes); + limiter.setSelectableTimes(selectableTimes); + + Assert.assertTrue(limiter.isOutOfRange(input)); + } + + @Test + public void isOutOfRangeHourShouldReturnFalseWhenADisabledTimeIsTestedWithSecondResolution() { + // If there are only disabledTimes, there will still be other times that are valid + Timepoint[] disabledTimes = { + new Timepoint(12), + new Timepoint(13), + new Timepoint(14) + }; + Timepoint input = disabledTimes[0]; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setDisabledTimes(disabledTimes); + + Assert.assertFalse(limiter.isOutOfRange(input, HOUR_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void isOutOfRangeHourShouldReturnFalseWhenADisabledTimeIsTestedWithMinuteResolution() { + // If there are only disabledTimes, there will still be other times that are valid + Timepoint[] disabledTimes = { + new Timepoint(12), + new Timepoint(13), + new Timepoint(14) + }; + Timepoint input = disabledTimes[0]; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setDisabledTimes(disabledTimes); + + Assert.assertFalse(limiter.isOutOfRange(input, HOUR_INDEX, Timepoint.TYPE.MINUTE)); + } + + @Test + public void isOutOfRangeHourShouldReturnTrueWhenADisabledTimeIsTestedWithHourResolution() { + // If there are only disabledTimes, there will still be other times that are valid + Timepoint[] disabledTimes = { + new Timepoint(12), + new Timepoint(13), + new Timepoint(14) + }; + Timepoint input = disabledTimes[0]; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setDisabledTimes(disabledTimes); + + Assert.assertTrue(limiter.isOutOfRange(input, HOUR_INDEX, Timepoint.TYPE.HOUR)); + } + + @Test + public void isOutOfRangeMinuteShouldReturnFalseWhenADisabledTimeIsTestedWithSecondResolution() { + // If there are only disabledTimes, there will still be other times that are valid + Timepoint[] disabledTimes = { + new Timepoint(12, 15), + new Timepoint(13, 16), + new Timepoint(14, 17) + }; + Timepoint input = disabledTimes[0]; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setDisabledTimes(disabledTimes); + + Assert.assertFalse(limiter.isOutOfRange(input, MINUTE_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void isOutOfRangeMinuteShouldReturnFalseWhenADisabledTimeIsTestedWithMinuteResolution() { + // If there are only disabledTimes, there will still be other times that are valid + Timepoint[] disabledTimes = { + new Timepoint(12, 15), + new Timepoint(13, 16), + new Timepoint(14, 17) + }; + Timepoint input = disabledTimes[0]; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setDisabledTimes(disabledTimes); + + Assert.assertTrue(limiter.isOutOfRange(input, MINUTE_INDEX, Timepoint.TYPE.MINUTE)); + } + + @Test + public void isOutOfRangeHourShouldReturnTrueWhenADisabledTimeCancelsASelectableTime() { + Timepoint[] disabledTimes = { + new Timepoint(12), + new Timepoint(13) + }; + Timepoint[] selectableTimes = { + new Timepoint(12), + new Timepoint(14), + new Timepoint(15) + }; + Timepoint input = new Timepoint(12); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setDisabledTimes(disabledTimes); + limiter.setSelectableTimes(selectableTimes); + + Assert.assertTrue(limiter.isOutOfRange(input, HOUR_INDEX, Timepoint.TYPE.SECOND)); + } + + @Test + public void roundToNearestShouldWorkWhenSelectableTimesContainsDuplicateEntries() { + Timepoint[] selectableTimes = { + new Timepoint(11), + new Timepoint(12), + new Timepoint(12), + new Timepoint(13) + }; + Timepoint input = new Timepoint(12, 29); + Timepoint expected = new Timepoint(12); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertEquals(limiter.roundToNearest(input, null, Timepoint.TYPE.SECOND), expected); + } + + @Test + public void roundToNearestShouldReturnMaxTimeIfBiggerThanMaxTime() { + Timepoint maxTime = new Timepoint(8); + Timepoint input = new Timepoint(12); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMaxTime(maxTime); + + Assert.assertEquals(limiter.roundToNearest(input, null, Timepoint.TYPE.SECOND), maxTime); + } + + @Test + public void roundToNearestShouldReturnMinTimeIfSmallerThanMinTime() { + Timepoint minTime = new Timepoint(8); + Timepoint input = new Timepoint(7); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setMinTime(minTime); + + Assert.assertEquals(limiter.roundToNearest(input, null, Timepoint.TYPE.SECOND), minTime); + } + + @Test + public void roundToNearestShouldReturnInputIfNotOutOfRange() { + Timepoint input = new Timepoint(12, 13, 14); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + Assert.assertEquals(limiter.roundToNearest(input, null, Timepoint.TYPE.SECOND), input); + } + + @Test + public void roundToNearestShouldReturnSelectableTime() { + Timepoint[] selectableTimes = { + new Timepoint(11, 12, 14), + new Timepoint(12, 13, 14), + new Timepoint(13, 14, 15) + }; + Timepoint input = new Timepoint(11); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertEquals(limiter.roundToNearest(input, null, Timepoint.TYPE.SECOND), selectableTimes[0]); + } + + @Test + public void roundToNearestShouldNotChangeTheMinutesWhenOptionIsSet() { + Timepoint[] selectableTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12), + new Timepoint(12, 13, 14), + new Timepoint(15, 16, 17) + }; + Timepoint input = new Timepoint(11, 12, 59); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertEquals(limiter.roundToNearest(input, Timepoint.TYPE.MINUTE, Timepoint.TYPE.SECOND).getHour(), input.getHour()); + Assert.assertEquals(limiter.roundToNearest(input, Timepoint.TYPE.MINUTE, Timepoint.TYPE.SECOND).getMinute(), input.getMinute()); + } + + @Test + public void roundToNearestShouldNotChangeTheHourWhenOptionIsSet() { + Timepoint[] selectableTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12), + new Timepoint(12, 13, 14), + new Timepoint(13), + new Timepoint(15, 16, 17) + }; + Timepoint input = new Timepoint(12, 59, 59); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertEquals(limiter.roundToNearest(input, Timepoint.TYPE.HOUR, Timepoint.TYPE.SECOND).getHour(), input.getHour()); + } + + @Test + public void roundToNearestShouldNotChangeTheHourWhenOptionIsSet2() { + Timepoint[] selectableTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12), + new Timepoint(12, 13, 14), + new Timepoint(13), + new Timepoint(15, 16, 17) + }; + Timepoint input = new Timepoint(15, 59, 59); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertEquals(limiter.roundToNearest(input, Timepoint.TYPE.HOUR, Timepoint.TYPE.SECOND).getHour(), input.getHour()); + } + + @Test + public void roundToNearestShouldNotChangeAnythingWhenSecondOptionIsSet() { + Timepoint[] selectableTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12), + new Timepoint(12, 13, 14), + new Timepoint(13), + new Timepoint(15, 16, 17) + }; + Timepoint input = new Timepoint(12, 59, 59); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertEquals(limiter.roundToNearest(input, Timepoint.TYPE.SECOND, Timepoint.TYPE.SECOND), input); + } + + @Test + public void roundToNearestShouldRoundToNearest() { + Timepoint[] selectableTimes = { + new Timepoint(11, 12, 13), + new Timepoint(11, 12, 16), + new Timepoint(12) + }; + Timepoint input = new Timepoint(11, 12, 14); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + + Assert.assertEquals(limiter.roundToNearest(input, null, Timepoint.TYPE.SECOND), selectableTimes[0]); + } + + @Test + public void roundToNearestShouldRoundToNearestSelectableThatIsNotDisabled() { + Timepoint[] selectableTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12, 13, 14), + new Timepoint(13) + }; + Timepoint[] disabledTimes = { + new Timepoint(12, 13, 14) + }; + Timepoint input = new Timepoint(12, 13, 15); + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + + limiter.setSelectableTimes(selectableTimes); + limiter.setDisabledTimes(disabledTimes); + + Assert.assertEquals(limiter.roundToNearest(input, null, Timepoint.TYPE.SECOND), selectableTimes[2]); + } + + @Test + public void roundToNearestShouldRoundWithSecondIncrementsIfInputIsDisabled() { + Timepoint[] disabledTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12, 13, 14), + new Timepoint(13, 14, 15) + }; + Timepoint input = disabledTimes[0]; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + Timepoint expected = new Timepoint(11, 12, 14); + + limiter.setDisabledTimes(disabledTimes); + + Assert.assertEquals(limiter.roundToNearest(input, null, Timepoint.TYPE.SECOND), expected); + } + + @Test + public void roundToNearestShouldRoundWithSecondIncrementsIfInputIsDisabled2() { + Timepoint[] disabledTimes = { + new Timepoint(11, 12, 13), + new Timepoint(11, 12, 14), + new Timepoint(12, 13, 14), + new Timepoint(13, 14, 15) + }; + Timepoint input = disabledTimes[0]; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + Timepoint expected = new Timepoint(11, 12, 12); + + limiter.setDisabledTimes(disabledTimes); + + Assert.assertEquals(limiter.roundToNearest(input, null, Timepoint.TYPE.SECOND), expected); + } + + @Test + public void roundToNearestShouldRoundWithSecondIncrementsIfInputIsDisabled3() { + Timepoint[] disabledTimes = { + new Timepoint(11, 12, 13), + new Timepoint(11, 12, 12), + new Timepoint(11, 12, 14), + new Timepoint(12, 13, 14), + new Timepoint(13, 14, 15) + }; + Timepoint input = disabledTimes[0]; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + Timepoint expected = new Timepoint(11, 12, 15); + + limiter.setDisabledTimes(disabledTimes); + + Assert.assertEquals(limiter.roundToNearest(input, null, Timepoint.TYPE.SECOND), expected); + } + + @Test + public void roundToNearestShouldRoundWithMinuteIncrementsIfInputIsDisabled() { + Timepoint[] disabledTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12, 13, 14), + new Timepoint(13, 14, 15) + }; + Timepoint input = disabledTimes[0]; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + Timepoint expected = new Timepoint(11, 13, 13); + + limiter.setDisabledTimes(disabledTimes); + + Assert.assertEquals(limiter.roundToNearest(input, null, Timepoint.TYPE.MINUTE), expected); + } + + @Test + public void roundToNearestShouldRoundWithHourIncrementsIfInputIsDisabled() { + Timepoint[] disabledTimes = { + new Timepoint(11, 12, 13), + new Timepoint(12, 13, 14), + new Timepoint(13, 14, 15) + }; + Timepoint input = disabledTimes[0]; + DefaultTimepointLimiter limiter = new DefaultTimepointLimiter(); + Timepoint expected = new Timepoint(10, 12, 13); + + limiter.setDisabledTimes(disabledTimes); + + Assert.assertEquals(limiter.roundToNearest(input, null, Timepoint.TYPE.HOUR), expected); + } +} \ No newline at end of file diff --git a/library/src/test/java/com/wdullaer/materialdatetimepicker/time/TimePickerDialogTest.java b/library/src/test/java/com/wdullaer/materialdatetimepicker/time/TimePickerDialogTest.java new file mode 100644 index 00000000..3bad88b4 --- /dev/null +++ b/library/src/test/java/com/wdullaer/materialdatetimepicker/time/TimePickerDialogTest.java @@ -0,0 +1,28 @@ +package com.wdullaer.materialdatetimepicker.time; + +import org.junit.Assert; +import org.junit.Test; + +public class TimePickerDialogTest { + @Test + public void getPickerResolutionShouldReturnSecondIfSecondsAreEnabled() { + TimePickerDialog tpd = TimePickerDialog.newInstance(null, false); + tpd.enableSeconds(true); + Assert.assertEquals(tpd.getPickerResolution(), Timepoint.TYPE.SECOND); + } + + @Test + public void getPickerResolutionShouldReturnMinuteIfMinutesAreEnabled() { + TimePickerDialog tpd = TimePickerDialog.newInstance(null, false); + tpd.enableSeconds(false); + tpd.enableMinutes(true); + Assert.assertEquals(tpd.getPickerResolution(), Timepoint.TYPE.MINUTE); + } + + @Test + public void getPickerResolutionShouldReturnHourIfMinutesAndSecondsAreDisabled() { + TimePickerDialog tpd = TimePickerDialog.newInstance(null, false); + tpd.enableMinutes(false); + Assert.assertEquals(tpd.getPickerResolution(), Timepoint.TYPE.HOUR); + } +} diff --git a/library/src/test/java/com/wdullaer/materialdatetimepicker/time/TimepointTest.java b/library/src/test/java/com/wdullaer/materialdatetimepicker/time/TimepointTest.java new file mode 100644 index 00000000..25035561 --- /dev/null +++ b/library/src/test/java/com/wdullaer/materialdatetimepicker/time/TimepointTest.java @@ -0,0 +1,233 @@ +package com.wdullaer.materialdatetimepicker.time; + +import org.junit.Test; +import org.junit.Assert; + +import java.util.HashSet; + +public class TimepointTest { + + @Test + public void timepointsWithSameFieldsShouldHaveSameHashCode() { + Timepoint first = new Timepoint(12, 0, 0); + Timepoint second = new Timepoint(12, 0, 0); + Assert.assertEquals(first.hashCode(), second.hashCode()); + } + + @Test + public void timepointsWithSameFieldsShouldBeEquals() { + Timepoint first = new Timepoint(12, 0, 0); + Timepoint second = new Timepoint(12, 0, 0); + Assert.assertEquals(first, second); + } + + @Test + public void timepointsWithSameFieldsShouldBeDistinctInHashSet() { + HashSet timepoints = new HashSet<>(4); + timepoints.add(new Timepoint(12, 0, 0)); + timepoints.add(new Timepoint(12, 0, 0)); + timepoints.add(new Timepoint(12, 0, 0)); + timepoints.add(new Timepoint(12, 0, 0)); + Assert.assertEquals(timepoints.size(), 1); + } + + @Test + public void timepointsWithDifferentFieldsShouldNotBeDistinctInHashSet() { + HashSet timepoints = new HashSet<>(4); + timepoints.add(new Timepoint(12, 1, 0)); + timepoints.add(new Timepoint(12, 2, 0)); + timepoints.add(new Timepoint(12, 3, 0)); + timepoints.add(new Timepoint(12, 4, 0)); + Assert.assertEquals(timepoints.size(), 4); + } + + @Test + public void compareToShouldReturnNegativeIfArgumentIsBigger() { + Timepoint orig = new Timepoint(12, 11, 10); + Timepoint arg = new Timepoint(13, 14, 15); + Assert.assertTrue(orig.compareTo(arg) < 0); + } + + @Test + public void compareToShouldReturnPositiveIfArgumentIsSmaller() { + Timepoint orig = new Timepoint(12, 11, 10); + Timepoint arg = new Timepoint(10, 14, 15); + Assert.assertTrue(orig.compareTo(arg) > 0); + } + + @Test + public void compareToShouldReturnZeroIfArgumentIsEqual() { + Timepoint orig = new Timepoint(12, 11, 10); + Timepoint arg = new Timepoint(12, 11, 10); + Assert.assertTrue(orig.compareTo(arg) == 0); + } + + @Test + public void isAMShouldReturnTrueIfTimepointIsBeforeMidday() { + Timepoint timepoint = new Timepoint(11); + Assert.assertTrue(timepoint.isAM()); + } + + @Test + public void isAMShouldReturnFalseIfTimepointIsMidday() { + Timepoint timepoint = new Timepoint(12); + Assert.assertFalse(timepoint.isAM()); + } + + @Test + public void isAMShouldReturnFalseIfTimepointIsAfterMidday() { + Timepoint timepoint = new Timepoint(13); + Assert.assertFalse(timepoint.isAM()); + } + + @Test + public void isAMShouldReturnTrueIfTimepointIsMidnight() { + Timepoint timepoint = new Timepoint(0); + Assert.assertTrue(timepoint.isAM()); + } + + @Test + public void isPMShouldReturnFalseIfTimepointIsBeforeMidday() { + Timepoint timepoint = new Timepoint(11); + Assert.assertFalse(timepoint.isPM()); + } + + @Test + public void isPMShouldReturnTrueIfTimepointIsMidday() { + Timepoint timepoint = new Timepoint(12); + Assert.assertTrue(timepoint.isPM()); + } + + @Test + public void isPMShouldReturnTrueIfTimepointIsAfterMidday() { + Timepoint timepoint = new Timepoint(13); + Assert.assertTrue(timepoint.isPM()); + } + + @Test + public void isPMShouldReturnFalseIfTimepointIsMidnight() { + Timepoint timepoint = new Timepoint(0); + Assert.assertFalse(timepoint.isPM()); + } + + @Test + public void setAMShouldDoNothingIfTimepointIsBeforeMidday() { + Timepoint timepoint = new Timepoint(11); + timepoint.setAM(); + Assert.assertEquals(timepoint.getHour(), 11); + } + + @Test + public void setAMShouldSetToMidnightIfTimepointIsMidday() { + Timepoint timepoint = new Timepoint(12); + timepoint.setAM(); + Assert.assertEquals(timepoint.getHour(), 0); + } + + @Test + public void setAMShouldSetBeforeMiddayIfTimepointIsAfterMidday() { + Timepoint timepoint = new Timepoint(13); + timepoint.setAM(); + Assert.assertEquals(timepoint.getHour(), 1); + } + + @Test + public void setAMShouldDoNothingIfTimepointIsMidnight() { + Timepoint timepoint = new Timepoint(0); + timepoint.setAM(); + Assert.assertEquals(timepoint.getHour(), 0); + } + + @Test + public void setAMShouldNotChangeMinutesOrSeconds() { + Timepoint timepoint = new Timepoint(13, 14, 15); + timepoint.setAM(); + Assert.assertEquals(timepoint.getMinute(), 14); + Assert.assertEquals(timepoint.getSecond(), 15); + } + + @Test + public void setPMShouldDoNothingIfTimepointIsAfterMidday() { + Timepoint timepoint = new Timepoint(13); + timepoint.setPM(); + Assert.assertEquals(timepoint.getHour(), 13); + } + + @Test + public void setPMShouldSetToMiddayIfTimepointIsMidnight() { + Timepoint timepoint = new Timepoint(0); + timepoint.setPM(); + Assert.assertEquals(timepoint.getHour(), 12); + } + + @Test + public void setPMShouldSetAfterMiddayIfTimepointIsBeforeMidday() { + Timepoint timepoint = new Timepoint(5); + timepoint.setPM(); + Assert.assertEquals(timepoint.getHour(), 17); + } + + @Test + public void setPMShouldDoNothingIfTimepointIsMidday() { + Timepoint timepoint = new Timepoint(12); + timepoint.setPM(); + Assert.assertEquals(timepoint.getHour(), 12); + } + + @Test + public void setPMShouldNotChangeMinutesOrSeconds() { + Timepoint timepoint = new Timepoint(1, 14, 15); + timepoint.setPM(); + Assert.assertEquals(timepoint.getMinute(), 14); + Assert.assertEquals(timepoint.getSecond(), 15); + } + + @Test + public void equalsShouldReturnTrueWhenInputsAreEqualWithSecondsResolution() { + Timepoint timepoint1 = new Timepoint(1, 14, 15); + Timepoint timepoint2 = new Timepoint(1, 14, 15); + Assert.assertTrue(timepoint1.equals(timepoint2, Timepoint.TYPE.SECOND)); + } + + @Test + public void equalsShouldReturnFalseWhenInputsDifferWithSecondsResolution() { + Timepoint timepoint1 = new Timepoint(1, 14, 15); + Timepoint timepoint2 = new Timepoint(1, 14, 16); + Assert.assertFalse(timepoint1.equals(timepoint2, Timepoint.TYPE.SECOND)); + } + + @Test + public void equalsShouldIgnoreSecondsWithMinuteResolution() { + Timepoint timepoint1 = new Timepoint(1, 14, 15); + Timepoint timepoint2 = new Timepoint(1, 14, 16); + Assert.assertTrue(timepoint1.equals(timepoint2, Timepoint.TYPE.MINUTE)); + } + + @Test + public void equalsShouldReturnFalseWhenInputsDifferWithMinuteResolution() { + Timepoint timepoint1 = new Timepoint(1, 14, 15); + Timepoint timepoint2 = new Timepoint(1, 15, 15); + Assert.assertFalse(timepoint1.equals(timepoint2, Timepoint.TYPE.MINUTE)); + } + + @Test + public void equalsShouldIgnoreSecondsWithHourResolution() { + Timepoint timepoint1 = new Timepoint(1, 14, 15); + Timepoint timepoint2 = new Timepoint(1, 14, 16); + Assert.assertTrue(timepoint1.equals(timepoint2, Timepoint.TYPE.HOUR)); + } + + @Test + public void equalsShouldIgnoreMinutesWithHourResolution() { + Timepoint timepoint1 = new Timepoint(1, 14, 15); + Timepoint timepoint2 = new Timepoint(1, 15, 16); + Assert.assertTrue(timepoint1.equals(timepoint2, Timepoint.TYPE.HOUR)); + } + + @Test + public void equalsShouldReturnFalseWhenInputsDifferWithHourResolution() { + Timepoint timepoint1 = new Timepoint(1, 14, 15); + Timepoint timepoint2 = new Timepoint(2, 14, 15); + Assert.assertFalse(timepoint1.equals(timepoint2, Timepoint.TYPE.HOUR)); + } +} diff --git a/sample/build.gradle b/sample/build.gradle index a0cff581..9ed16d12 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -17,12 +17,16 @@ android { minifyEnabled false } } + + lintOptions { + abortOnError false + } } dependencies { compile project(':library') compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:25.2.0' - compile 'com.android.support:design:25.2.0' - compile 'com.android.support:support-v13:25.2.0' + compile 'com.android.support:appcompat-v7:27.0.2' + compile 'com.android.support:design:27.0.2' + compile 'com.android.support:support-v13:27.0.2' } diff --git a/sample/src/main/assets/fonts/Montserrat-Bold.ttf b/sample/src/main/assets/fonts/Montserrat-Bold.ttf new file mode 100644 index 00000000..6cd086a6 Binary files /dev/null and b/sample/src/main/assets/fonts/Montserrat-Bold.ttf differ diff --git a/sample/src/main/assets/fonts/Montserrat-Light.ttf b/sample/src/main/assets/fonts/Montserrat-Light.ttf new file mode 100644 index 00000000..18fdb2ed Binary files /dev/null and b/sample/src/main/assets/fonts/Montserrat-Light.ttf differ diff --git a/sample/src/main/assets/fonts/Montserrat-Regular.ttf b/sample/src/main/assets/fonts/Montserrat-Regular.ttf new file mode 100644 index 00000000..5a5a5550 Binary files /dev/null and b/sample/src/main/assets/fonts/Montserrat-Regular.ttf differ diff --git a/sample/src/main/assets/fonts/Montserrat-SemiBold.ttf b/sample/src/main/assets/fonts/Montserrat-SemiBold.ttf new file mode 100644 index 00000000..d310a521 Binary files /dev/null and b/sample/src/main/assets/fonts/Montserrat-SemiBold.ttf differ diff --git a/sample/src/main/java/com/wdullaer/datetimepickerexample/DatePickerFragment.java b/sample/src/main/java/com/wdullaer/datetimepickerexample/DatePickerFragment.java index 35e4aa31..4f426369 100644 --- a/sample/src/main/java/com/wdullaer/datetimepickerexample/DatePickerFragment.java +++ b/sample/src/main/java/com/wdullaer/datetimepickerexample/DatePickerFragment.java @@ -3,16 +3,19 @@ import android.app.Fragment; import android.graphics.Color; import android.os.Bundle; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.CheckBox; +import android.widget.DatePicker; import android.widget.TextView; import com.wdullaer.materialdatetimepicker.date.DatePickerDialog; import java.util.Calendar; +import java.util.Locale; /** * A simple {@link Fragment} subclass. @@ -27,8 +30,10 @@ public class DatePickerFragment extends Fragment implements DatePickerDialog.OnD private CheckBox titleDate; private CheckBox showYearFirst; private CheckBox showVersion2; + private CheckBox switchOrientation; private CheckBox limitSelectableDays; private CheckBox highlightDays; + private DatePickerDialog dpd; public DatePickerFragment() { // Required empty public constructor @@ -41,29 +46,63 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, View view = inflater.inflate(R.layout.datepicker_layout, container, false); // Find our View instances - dateTextView = (TextView) view.findViewById(R.id.date_textview); - Button dateButton = (Button) view.findViewById(R.id.date_button); - modeDarkDate = (CheckBox) view.findViewById(R.id.mode_dark_date); - modeCustomAccentDate = (CheckBox) view.findViewById(R.id.mode_custom_accent_date); - vibrateDate = (CheckBox) view.findViewById(R.id.vibrate_date); - dismissDate = (CheckBox) view.findViewById(R.id.dismiss_date); - titleDate = (CheckBox) view.findViewById(R.id.title_date); - showYearFirst = (CheckBox) view.findViewById(R.id.show_year_first); - showVersion2 = (CheckBox) view.findViewById(R.id.show_version_2); - limitSelectableDays = (CheckBox) view.findViewById(R.id.limit_dates); - highlightDays = (CheckBox) view.findViewById(R.id.highlight_dates); + dateTextView = view.findViewById(R.id.date_textview); + Button dateButton = view.findViewById(R.id.date_button); + modeDarkDate = view.findViewById(R.id.mode_dark_date); + modeCustomAccentDate = view.findViewById(R.id.mode_custom_accent_date); + vibrateDate = view.findViewById(R.id.vibrate_date); + dismissDate = view.findViewById(R.id.dismiss_date); + titleDate = view.findViewById(R.id.title_date); + showYearFirst = view.findViewById(R.id.show_year_first); + showVersion2 = view.findViewById(R.id.show_version_2); + switchOrientation = view.findViewById(R.id.switch_orientation); + limitSelectableDays = view.findViewById(R.id.limit_dates); + highlightDays = view.findViewById(R.id.highlight_dates); - // Show a datepicker when the dateButton is clicked - dateButton.setOnClickListener(new View.OnClickListener() { + view.findViewById(R.id.original_button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Calendar now = Calendar.getInstance(); - DatePickerDialog dpd = DatePickerDialog.newInstance( - DatePickerFragment.this, + new android.app.DatePickerDialog( + getActivity(), + new android.app.DatePickerDialog.OnDateSetListener() { + @Override + public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) { + Log.d("Orignal", "Got clicked"); + } + }, now.get(Calendar.YEAR), now.get(Calendar.MONTH), now.get(Calendar.DAY_OF_MONTH) - ); + ).show(); + } + }); + + // Show a datepicker when the dateButton is clicked + dateButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Calendar now = Calendar.getInstance(); + /* + It is recommended to always create a new instance whenever you need to show a Dialog. + The sample app is reusing them because it is useful when looking for regressions + during testing + */ + if (dpd == null) { + dpd = DatePickerDialog.newInstance( + DatePickerFragment.this, + now.get(Calendar.YEAR), + now.get(Calendar.MONTH), + now.get(Calendar.DAY_OF_MONTH) + ); + } else { + dpd.initialize( + DatePickerFragment.this, + now.get(Calendar.YEAR), + now.get(Calendar.MONTH), + now.get(Calendar.DAY_OF_MONTH) + ); + } dpd.setThemeDark(modeDarkDate.isChecked()); dpd.vibrate(vibrateDate.isChecked()); dpd.dismissOnPause(dismissDate.isChecked()); @@ -93,6 +132,13 @@ public void onClick(View v) { } dpd.setSelectableDays(days); } + if (switchOrientation.isChecked()) { + if (dpd.getVersion() == DatePickerDialog.Version.VERSION_1) { + dpd.setScrollOrientation(DatePickerDialog.ScrollOrientation.HORIZONTAL); + } else { + dpd.setScrollOrientation(DatePickerDialog.ScrollOrientation.VERTICAL); + } + } dpd.show(getFragmentManager(), "Datepickerdialog"); } }); diff --git a/sample/src/main/java/com/wdullaer/datetimepickerexample/DatePickerSupportFragment.java b/sample/src/main/java/com/wdullaer/datetimepickerexample/DatePickerSupportFragment.java deleted file mode 100644 index 42188354..00000000 --- a/sample/src/main/java/com/wdullaer/datetimepickerexample/DatePickerSupportFragment.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.wdullaer.datetimepickerexample; - - -import android.graphics.Color; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.TextView; - -import com.wdullaer.materialdatetimepicker.supportdate.SupportDatePickerDialog; - -import java.util.Calendar; - -/** - * Created by rmore on 06/03/2017. - */ - -public class DatePickerSupportFragment extends Fragment implements SupportDatePickerDialog.OnDateSetListener { - - private TextView dateTextView; - private CheckBox modeDarkDate; - private CheckBox modeCustomAccentDate; - private CheckBox vibrateDate; - private CheckBox dismissDate; - private CheckBox titleDate; - private CheckBox showYearFirst; - private CheckBox showVersion2; - private CheckBox limitSelectableDays; - private CheckBox highlightDays; - - public DatePickerSupportFragment() { - // Required empty public constructor - } - - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.datepicker_layout, container, false); - - // Find our View instances - dateTextView = (TextView) view.findViewById(R.id.date_textview); - Button dateButton = (Button) view.findViewById(R.id.date_button); - modeDarkDate = (CheckBox) view.findViewById(R.id.mode_dark_date); - modeCustomAccentDate = (CheckBox) view.findViewById(R.id.mode_custom_accent_date); - vibrateDate = (CheckBox) view.findViewById(R.id.vibrate_date); - dismissDate = (CheckBox) view.findViewById(R.id.dismiss_date); - titleDate = (CheckBox) view.findViewById(R.id.title_date); - showYearFirst = (CheckBox) view.findViewById(R.id.show_year_first); - showVersion2 = (CheckBox) view.findViewById(R.id.show_version_2); - limitSelectableDays = (CheckBox) view.findViewById(R.id.limit_dates); - highlightDays = (CheckBox) view.findViewById(R.id.highlight_dates); - - // Show a datepicker when the dateButton is clicked - dateButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Calendar now = Calendar.getInstance(); - SupportDatePickerDialog dpd = SupportDatePickerDialog.newInstance( - DatePickerSupportFragment.this, - now.get(Calendar.YEAR), - now.get(Calendar.MONTH), - now.get(Calendar.DAY_OF_MONTH) - ); - dpd.setThemeDark(modeDarkDate.isChecked()); - dpd.vibrate(vibrateDate.isChecked()); - dpd.dismissOnPause(dismissDate.isChecked()); - dpd.showYearPickerFirst(showYearFirst.isChecked()); - dpd.setVersion(showVersion2.isChecked() ? SupportDatePickerDialog.Version.VERSION_2 : SupportDatePickerDialog.Version.VERSION_1); - if (modeCustomAccentDate.isChecked()) { - dpd.setAccentColor(Color.parseColor("#9C27B0")); - } - if (titleDate.isChecked()) { - dpd.setTitle("DatePicker Title"); - } - if (highlightDays.isChecked()) { - Calendar date1 = Calendar.getInstance(); - Calendar date2 = Calendar.getInstance(); - date2.add(Calendar.WEEK_OF_MONTH, -1); - Calendar date3 = Calendar.getInstance(); - date3.add(Calendar.WEEK_OF_MONTH, 1); - Calendar[] days = {date1, date2, date3}; - dpd.setHighlightedDays(days); - } - if (limitSelectableDays.isChecked()) { - Calendar[] days = new Calendar[13]; - for (int i = -6; i < 7; i++) { - Calendar day = Calendar.getInstance(); - day.add(Calendar.DAY_OF_MONTH, i * 2); - days[i + 6] = day; - } - dpd.setSelectableDays(days); - } - dpd.show(getChildFragmentManager(), "Datepickerdialog"); - } - }); - - return view; - } - - @Override - public void onResume() { - super.onResume(); - SupportDatePickerDialog dpd = (SupportDatePickerDialog) getChildFragmentManager().findFragmentByTag("Datepickerdialog"); - if(dpd != null) dpd.setOnDateSetListener(this); - } - - @Override - public void onDateSet(SupportDatePickerDialog view, int year, int monthOfYear, int dayOfMonth) { - String date = "You picked the following date: "+dayOfMonth+"/"+(++monthOfYear)+"/"+year; - dateTextView.setText(date); - } -} - diff --git a/sample/src/main/java/com/wdullaer/datetimepickerexample/MainActivity.java b/sample/src/main/java/com/wdullaer/datetimepickerexample/MainActivity.java index 2f4e9494..7ff3baf0 100644 --- a/sample/src/main/java/com/wdullaer/datetimepickerexample/MainActivity.java +++ b/sample/src/main/java/com/wdullaer/datetimepickerexample/MainActivity.java @@ -4,14 +4,11 @@ import android.support.design.widget.TabLayout; import android.app.Fragment; import android.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; import android.support.v4.view.ViewPager; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.support.v13.app.FragmentPagerAdapter; -import com.wdullaer.materialdatetimepicker.supportdate.SupportDatePickerDialog; - public class MainActivity extends AppCompatActivity { ViewPager viewPager; @@ -20,26 +17,17 @@ public class MainActivity extends AppCompatActivity @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.date_fragment); - - FragmentTransaction transaction = getSupportFragmentManager() - .beginTransaction(); - //transaction.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left); - transaction.replace(R.id.frame, new SupportDatePickerDialog()); - transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); - - transaction.commit(); - - /*setContentView(R.layout.activity_main); + setContentView(R.layout.activity_main); adapter = new PickerAdapter(getFragmentManager()); - viewPager = (ViewPager) findViewById(R.id.pager); + viewPager = findViewById(R.id.pager); viewPager.setAdapter(adapter); setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); - TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); + TabLayout tabLayout = findViewById(R.id.tabs); tabLayout.setupWithViewPager(viewPager); - for(int i=0;i - - - - - \ No newline at end of file diff --git a/sample/src/main/res/layout/datepicker_layout.xml b/sample/src/main/res/layout/datepicker_layout.xml index fb6eeb01..400b4fb6 100644 --- a/sample/src/main/res/layout/datepicker_layout.xml +++ b/sample/src/main/res/layout/datepicker_layout.xml @@ -20,6 +20,12 @@ android:layout_height="wrap_content" android:text="@string/pick_date"/> +