├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── lib ├── .gitignore ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── simplicityapks │ │ └── reminderdatepicker │ │ └── lib │ │ ├── DateItem.java │ │ ├── DateSpinner.java │ │ ├── OnDateSelectedListener.java │ │ ├── PickerSpinner.java │ │ ├── PickerSpinnerAdapter.java │ │ ├── ReminderDatePicker.java │ │ ├── TimeItem.java │ │ ├── TimeSpinner.java │ │ └── TwinTextItem.java │ ├── project.properties │ └── res │ ├── drawable-hdpi │ ├── ic_action_time_dark.png │ └── ic_action_time_light.png │ ├── drawable-mdpi │ ├── ic_action_time_dark.png │ └── ic_action_time_light.png │ ├── drawable-xhdpi │ ├── ic_action_time_dark.png │ └── ic_action_time_light.png │ ├── drawable-xxhdpi │ ├── ic_action_time_dark.png │ └── ic_action_time_light.png │ ├── layout │ ├── reminder_date_picker.xml │ ├── time_button.xml │ ├── twin_text_dropdown_item.xml │ ├── twin_text_dropdown_item_dark.xml │ ├── twin_text_footer.xml │ ├── twin_text_footer_dark.xml │ └── twin_text_item.xml │ ├── values-de │ └── strings.xml │ ├── values-el │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-it │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-tr │ └── strings.xml │ ├── values-v11 │ └── styles.xml │ ├── values-v14 │ └── styles.xml │ ├── values │ ├── attrs.xml │ ├── colors.xml │ ├── ids.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ ├── date_items.xml │ └── time_items.xml ├── maven_push.gradle ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── simplicityapks │ │ └── reminderdatepicker │ │ └── sample │ │ └── MainActivity.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── drawable-xxxhdpi │ └── ic_launcher.png │ ├── layout │ └── activity_main.xml │ ├── menu │ └── main.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Eclipse project files 19 | .classpath 20 | .project 21 | 22 | # Android Studio 23 | .idea/ 24 | .gradle 25 | gradle/ 26 | gradlew 27 | gradlew.bat 28 | build/ 29 | /*/local.properties 30 | /*/out 31 | /*/*/build 32 | /*/*/production 33 | *.iml 34 | *.iws 35 | *.ipr 36 | *~ 37 | *.swp -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *Date and time pickers have always been a hassle for me (no matter how awesome they looked). Having 2 | to choose from a few hundred numbers felt overwhelming and was neither intuitive nor fast. Luckily, 3 | Google found a sleek solution in the Notes app which I decided to replicate:* 4 | 5 | ReminderDatePicker 6 | ================== 7 | 8 | An intuitive and simplistic **Date and Time Picker for reminders**. Per default it almost exactly 9 | matches the picker seen in the [Google Notes App](https://play.google.com/store/apps/details?id=com.google.android.keep) 10 | but adds important features and enhancements. You can **download the [sample app in the Play Store here](https://play.google.com/store/apps/details?id=com.simplicityapks.reminderdatepicker.sample)** to test it! 11 | 12 | Screenshots 13 | ----------- 14 | 15 | 16 | 17 | 18 | Set-up 19 | ------ 20 | 21 | To use this library in your project either 22 | 23 | * Add this line to the dependencies in your build.gradle (note you need `mavenCentral()` in your repositories): 24 | `compile 'com.simplicityapks:reminderdatepicker:1.3.+'` 25 | 26 | * Copy the library into your workspace and [add it as library in Eclipse](http://developer.android.com/tools/projects/projects-eclipse.html#ReferencingLibraryProject) 27 | or add `compile project(':lib')` to the dependencies in your build.gradle in Android Studio. 28 | In Eclipse, you will need to right click on the java folder in this project and select Build Path-->Use as Source Folder. 29 | Also, make sure you have configured both the [appcompat support (v7) library](http://developer.android.com/tools/support-library/features.html#v7) 30 | and the [DateTimePicker](https://github.com/jaydeep17/datetimepicker) correctly and referenced as library. 31 | 32 | The library is designed for Android ICS and above (API level 14+), but it works fine in Android 2.1+ 33 | as well (only the spinners use a dialog instead of the popup menu). Note that you need to use [NineOldAndroids](https://github.com/JakeWharton/NineOldAndroids/) 34 | on pre-Honeycomb for the additional [DateTimePicker](https://github.com/jaydeep17/datetimepicker) 35 | that is integrated out of the box. 36 | 37 | Usage 38 | ----- 39 | 40 | Simply construct a new [ReminderDatePicker](/lib/src/main/java/com/simplicityapks/reminderdatepicker/lib/ReminderDatePicker.java) 41 | or add it to your layout xml files: 42 | 43 | 47 | 48 | See the [sample source code](https://github.com/SimplicityApks/ReminderDatePicker/tree/master/sample/src/main) for more information. 49 | 50 | ###Flags and Modes 51 | 52 | This library supports different behaviours and selectable dates. Based on your needs, call `setFlags()` 53 | or use the `app:flags="..."` attribute in the xml declaration with one or more of the 54 | *ReminderDatePicker.FLAG_...* or *.MODE_...* constants (combined with the `|` operator). For example 55 | use `app:flags="month|more_time"` for a larger set of selectable days and hours. 56 | 57 | Furthermore, you can restrict which days are enabled in the date spinner by calling `setMinDate()` and 58 | `setMaxDate()`. Per default, dates in the past are not user-selectable, but you can re-enable them 59 | by calling `setMinDate(null)` or using *FLAG_PAST*. 60 | 61 | ###Custom additional date or time picker 62 | 63 | After clicking on the footer in one of the spinners, the additional date or time picker will open as 64 | dialog. These are the standard pickers from the Android framework, made Android 2.1+ compatible by 65 | [DateTimePicker](https://github.com/flavienlaurent/datetimepicker). You can, however, use a custom 66 | date or time picker or even implement your own behaviour. To achieve this simply call `setCustomDatePicker()` 67 | or `setCustomTimePicker()`, passing an OnClickListener whose *onClick()* method will be called when the 68 | footer is clicked. 69 | 70 | ###Custom date and time spinner items 71 | 72 | Date and time spinner parse their items from an xml resource file, [R.xml.date_items](/lib/src/main/res/xml/date_items.xml) 73 | and [R.xml.time_items](/lib/src/main/res/xml/time_items.xml) respectively. That means you can implement 74 | a custom item list by overriding those files: In each *DateItem* or *TimeItem* xml tag you should 75 | provide an `id` and `text` attribute (if text is left out the date will be formatted instead). 76 | To declare the item's date (or time), you can use the `abs...` and `rel...` attributes, where *rel* 77 | means relative to the current date and time and *abs* the absolute value. See the *XML_ATTR_...* 78 | constants in the spinners for the supported tags. 79 | 80 | Likewise, it is possible to add and remove spinner items at runtime using `addAdapterItem()`, 81 | `insertAdapterItem()` and `removeAdapterItemById()`. The id value passed to an item's constructor 82 | can be any integer, but should preferably be a resource id declared in [ids.xml](/lib/src/main/res/values/ids.xml). 83 | 84 | ###Advanced Usage 85 | 86 | [DateSpinner](/lib/src/main/java/com/simplicityapks/reminderdatepicker/lib/DateSpinner.java) and 87 | [TimeSpinner](/lib/src/main/java/com/simplicityapks/reminderdatepicker/lib/TimeSpinner.java) can be 88 | used separately (when FLAG_HIDE_TIME does not suffice) and each listen for the xml flags as well. 89 | But make sure you use `style="@style/PickerSpinner"` in the spinner's xml layout tag! 90 | 91 | Alternatively, if you have a totally different usage and need the spinners used here, let your custom 92 | Spinner extend [PickerSpinner](/lib/src/main/java/com/simplicityapks/reminderdatepicker/lib/PickerSpinner.java) 93 | (which uses a [PickerSpinnerAdapter](/lib/src/main/java/com/simplicityapks/reminderdatepicker/lib/PickerSpinnerAdapter.java)). 94 | That way your spinner will have a footer, secondary texts and the ability to have a temporary selection. 95 | The class also allows easy dynamic changes to the spinner items without having to check and reset 96 | the selection. 97 | 98 | Credits 99 | ------- 100 | 101 | **Huge thanks to** 102 | * **Google** for the original design of this picker in Google Keep 103 | * **[flavienlaurent](https://github.com/flavienlaurent)** for his awesome [DateTimePicker](https://github.com/flavienlaurent/datetimepicker) 104 | * **[jaydeep17](https://github.com/jaydeep17)** for forking and continuing work on the [DateTimePicker](https://github.com/jaydeep17/datetimepicker) 105 | * **[chrisbanes](https://github.com/chrisbanes)** for his [maven_push.gradle script](http://chris.banes.me/2013/08/27/pushing-aars-to-maven-central/) which brought this lib to maven central 106 | * **[Oxygen Team](http://www.iconarchive.com/artist/oxygen-icons.org.html)** for designing the [icon of the sample app](http://www.iconarchive.com/show/oxygen-icons-by-oxygen-icons.org/Apps-preferences-system-time-icon.html) 107 | * **[dancer_69](http://forum.xda-developers.com/member.php?u=390873)** for the Greek translation 108 | * **[dahool](https://github.com/dahool)** for the Spanish translation 109 | * **[rampo](https://github.com/rampo)** for the Italian translation 110 | * **[erickpires](https://github.com/erickpires)** for the Brazilian Portuguese translation 111 | * **Cüneyt Ayyıldız** for the Turkish translation 112 | 113 | License 114 | ------- 115 | 116 | Copyright 2014 SimplicityApks 117 | 118 | Licensed under the Apache License, Version 2.0 (the "License"); 119 | you may not use this file except in compliance with the License. 120 | You may obtain a copy of the License at 121 | 122 | http://www.apache.org/licenses/LICENSE-2.0 123 | 124 | Unless required by applicable law or agreed to in writing, software 125 | distributed under the License is distributed on an "AS IS" BASIS, 126 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 127 | See the License for the specific language governing permissions and 128 | limitations under the License. 129 | See the LICENSE file for more information. 130 | 131 | --- 132 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | mavenCentral() 6 | maven { 7 | url 'https://maven.google.com/' 8 | name 'Google' 9 | } 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.0.0' 13 | 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | } 17 | } 18 | 19 | def isReleaseBuild() { 20 | return version.contains("SNAPSHOT") == false 21 | } 22 | 23 | allprojects { 24 | version = VERSION_NAME 25 | group = GROUP 26 | 27 | repositories { 28 | mavenCentral() 29 | maven { 30 | url "https://jitpack.io" 31 | } 32 | google() 33 | } 34 | } 35 | 36 | apply plugin: 'android-reporting' -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Settings specified in this file will override any Gradle settings 5 | # configured through the IDE. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | VERSION_NAME=1.3.1 21 | VERSION_CODE=14 22 | GROUP=com.simplicityapks 23 | 24 | POM_DESCRIPTION=An intuitive and simplistic Date and Time Picker for reminders. This Android library mirrors the date picker seen in the Google Keep app and adds important features and enhancements. 25 | POM_URL=https://github.com/simplicityapks/reminderdatepicker 26 | POM_SCM_URL=https://github.com/simplicityapks/reminderdatepicker 27 | POM_SCM_CONNECTION=scm:git@github.com:simplicityapks/reminderdatepicker.git 28 | POM_SCM_DEV_CONNECTION=scm:git@github.com:simplicityapks/reminderdatepicker.git 29 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 30 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 31 | POM_LICENCE_DIST=repo 32 | POM_DEVELOPER_ID=SimplicityApks 33 | POM_DEVELOPER_NAME=SimplicityApks -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /lib/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 26 5 | buildToolsVersion '27.0.0' 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | targetSdkVersion 26 10 | versionCode Integer.parseInt(project.VERSION_CODE) 11 | versionName project.VERSION_NAME 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | compile 'com.android.support:support-v4:26.1.0' 24 | compile 'com.android.support:appcompat-v7:26.1.0' 25 | compile 'com.android.support:support-annotations:27.0.0' 26 | compile 'com.github.lachlanm:datetimepicker:0.0.5' 27 | // compile 'com.github.jaydeep17:datetimepicker:0.0.4' 28 | // if above doesn't work, use 29 | // compile 'com.github.flavienlaurent.datetimepicker:library:0.0.2' 30 | } 31 | 32 | // Used to push in maven 33 | // apply from: '../maven_push.gradle' 34 | -------------------------------------------------------------------------------- /lib/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=ReminderDatePicker 2 | POM_ARTIFACT_ID=reminderdatepicker 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /lib/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/patrick/Dokumente/AndroidStudioDevelopment/android-studio/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/src/main/java/com/simplicityapks/reminderdatepicker/lib/DateItem.java: -------------------------------------------------------------------------------- 1 | package com.simplicityapks.reminderdatepicker.lib; 2 | 3 | import java.util.Calendar; 4 | import java.util.GregorianCalendar; 5 | 6 | /** 7 | * TwinTextItem to be inserted into the ArrayAdapter of the DateSpinner. The date is saved like the DatePicker. 8 | * The secondary text is currently not in use, so getSecondaryText() returns null. 9 | */ 10 | public class DateItem implements TwinTextItem{ 11 | 12 | private final String label, dateNumbers; 13 | private final int year, month, day, id; 14 | private boolean enabled = true; 15 | 16 | /** 17 | * Constructs a new DateItem holding the specified date and a label to show primarily. 18 | * @param label The string to return when getPrimaryText() is called. 19 | * @param date The date to be returned by getDate(). 20 | * @param id The identifier to find this item with. 21 | */ 22 | public DateItem(String label, Calendar date, int id) { 23 | this(label, date.get(Calendar.YEAR), date.get(Calendar.MONTH), date.get(Calendar.DAY_OF_MONTH), id); 24 | } 25 | 26 | /** 27 | * Constructs a new DateItem holding the specified date and a label to to show primarily. 28 | * @param label The string to return when getPrimaryText() is called. 29 | * @param year The year. 30 | * @param month The month of year, zero-indexed (so 11 is December). 31 | * @param day The day of the month. 32 | * @param id The identifier to find this item with. 33 | */ 34 | public DateItem(String label, int year, int month, int day, int id) { 35 | this.label = label; 36 | this.year = year; 37 | this.month = month; 38 | this.day = day; 39 | this.id = id; 40 | this.dateNumbers = null; 41 | } 42 | 43 | /** 44 | * Constructs a new DateItem holding the specified date and a label to show primarily, as well as 45 | * a dateString to show secondary. 46 | * @param label The string to return when getPrimaryText() is called. 47 | * @param dateString The String to return when getSecondaryText() is called. 48 | * @param date The date to be returned by getDate(). 49 | * @param id The identifier to find this item with. 50 | */ 51 | public DateItem(String label, String dateString, Calendar date, int id) { 52 | this(label, dateString, date.get(Calendar.YEAR), date.get(Calendar.MONTH), date.get(Calendar.DAY_OF_MONTH), id); 53 | } 54 | 55 | /** 56 | * Constructs a new DateItem holding the specified date and a label to to show primarily, as well as 57 | * a dateString to show secondary. 58 | * @param label The string to return when getPrimaryText() is called. 59 | * @param dateString The String to return when getSecondaryText() is called. 60 | * @param year The year. 61 | * @param month The month of year, zero-indexed (so 11 is December). 62 | * @param day The day of the month. 63 | * @param id The identifier to find this item with. 64 | */ 65 | public DateItem(String label, String dateString, int year, int month, int day, int id) { 66 | this.label = label; 67 | this.year = year; 68 | this.month = month; 69 | this.day = day; 70 | this.id = id; 71 | this.dateNumbers = dateString; 72 | } 73 | 74 | /** 75 | * Gets the current date set in this DateItem. 76 | * @return A new GregorianCalendar containing the date. 77 | */ 78 | public Calendar getDate() { 79 | return new GregorianCalendar(year, month, day); 80 | } 81 | 82 | /** 83 | * Gets the day of the month set for this TimeItem. 84 | * @return The day, as int. 85 | */ 86 | public int getDay() { 87 | return this.day; 88 | } 89 | 90 | /** 91 | * Gets the month set for this TimeItem. 92 | * @return The month, as int. 93 | */ 94 | public int getMonth() { 95 | return this.month; 96 | } 97 | 98 | /** 99 | * Gets the year set for this TimeItem. 100 | * @return The year, as int. 101 | */ 102 | public int getYear() { 103 | return this.year; 104 | } 105 | 106 | /** 107 | * Deeply compares this DateItem to the specified Object. Returns true if obj is a DateItem and 108 | * contains the same date (ignoring the label) or is a Calendar and contains the same date 109 | * ignoring hour, minute and second. 110 | * @param obj The Object to compare this to. 111 | * @return true if equal, false otherwise. 112 | */ 113 | @Override 114 | public boolean equals(Object obj) { 115 | int objDay, objMonth, objYear; 116 | if(obj instanceof DateItem) { 117 | DateItem item = (DateItem) obj; 118 | objDay = item.getDay(); 119 | objMonth = item.getMonth(); 120 | objYear = item.getYear(); 121 | } 122 | else if(obj instanceof Calendar) { 123 | Calendar cal = (Calendar) obj; 124 | objDay = cal.get(Calendar.DAY_OF_MONTH); 125 | objMonth = cal.get(Calendar.MONTH); 126 | objYear = cal.get(Calendar.YEAR); 127 | } 128 | else return false; 129 | return objDay==this.day && objMonth==this.month && objYear==this.year; 130 | } 131 | 132 | @Override 133 | public CharSequence getPrimaryText() { 134 | return label; 135 | } 136 | 137 | @Override 138 | public CharSequence getSecondaryText() { 139 | return dateNumbers; 140 | } 141 | 142 | @Override 143 | public int getId() { 144 | return id; 145 | } 146 | 147 | @Override 148 | public boolean isEnabled() { 149 | return enabled; 150 | } 151 | 152 | /** 153 | * Enable or disable this spinner item. 154 | * @param enable true to enable, false to disable this item. 155 | */ 156 | public void setEnabled(boolean enable) { 157 | this.enabled = enable; 158 | } 159 | 160 | /** 161 | * The returned String may be passed to {@link #fromString(String)} to save and recreate this object easily. 162 | * @return The elements of this object separated by \n 163 | */ 164 | @Override 165 | public String toString() { 166 | String sep = "\n"; 167 | return nullToEmpty(label) +sep+ nullToEmpty(dateNumbers) +sep+ year +sep+ month +sep+ day +sep+ id; 168 | } 169 | 170 | /** 171 | * Constructs a new TimeItem from a String previously gotten from the {@link #toString()} method. 172 | * @param code The string to parse from. 173 | * @return A new TimeItem, or null if there was an error. 174 | */ 175 | public static DateItem fromString(String code) { 176 | String[] items = code.split("\n"); 177 | if(items.length != 6) return null; 178 | int year, month, day, id; 179 | try { 180 | year = Integer.parseInt(items[2]); 181 | month = Integer.parseInt(items[3]); 182 | day = Integer.parseInt(items[4]); 183 | id = Integer.parseInt(items[5]); 184 | } 185 | catch (NumberFormatException e) { 186 | e.printStackTrace(); 187 | return null; 188 | } 189 | return new DateItem(emptyToNull(items[0]), emptyToNull(items[1]), year, month, day, id); 190 | } 191 | 192 | /** 193 | * Makes sure s is not null, but the empty string instead. Otherwise just return s. 194 | */ 195 | private static String nullToEmpty(String s) { 196 | return s==null? "" : s; 197 | } 198 | 199 | /** 200 | * Makes sure s is not an empty string, but null instead. Otherwise just return s. 201 | */ 202 | private static String emptyToNull(String s) { 203 | return "".equals(s)? null : s; 204 | } 205 | } -------------------------------------------------------------------------------- /lib/src/main/java/com/simplicityapks/reminderdatepicker/lib/DateSpinner.java: -------------------------------------------------------------------------------- 1 | package com.simplicityapks.reminderdatepicker.lib; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageManager; 5 | import android.content.res.Resources; 6 | import android.content.res.TypedArray; 7 | import android.content.res.XmlResourceParser; 8 | import android.support.annotation.NonNull; 9 | import android.support.annotation.Nullable; 10 | import android.support.annotation.StringRes; 11 | import android.support.v4.app.FragmentActivity; 12 | import android.support.v4.app.FragmentManager; 13 | import android.text.format.DateUtils; 14 | import android.util.AttributeSet; 15 | import android.util.Log; 16 | import android.view.View; 17 | import android.widget.AdapterView; 18 | import android.widget.Toast; 19 | 20 | import com.fourmob.datetimepicker.date.CalendarDay; 21 | import com.fourmob.datetimepicker.date.DatePickerDialog; 22 | 23 | import java.text.DateFormatSymbols; 24 | import java.util.Calendar; 25 | import java.util.GregorianCalendar; 26 | import java.util.List; 27 | 28 | /** 29 | * The left PickerSpinner in the Google Keep app, to select a date. 30 | */ 31 | public class DateSpinner extends PickerSpinner implements AdapterView.OnItemSelectedListener { 32 | 33 | // TODO remove when setMinDate(null) works 34 | private static final CalendarDay MINIMUM_POSSIBLE_DATE = new CalendarDay(1902, 1, 1); 35 | 36 | public static final String XML_TAG_DATEITEM = "DateItem"; 37 | 38 | public static final String XML_ATTR_ABSDAYOFYEAR = "absDayOfYear"; 39 | public static final String XML_ATTR_ABSDAYOFMONTH = "absDayOfMonth"; 40 | public static final String XML_ATTR_ABSMONTH = "absMonth"; 41 | public static final String XML_ATTR_ABSYEAR = "absYear"; 42 | 43 | public static final String XML_ATTR_RELDAY = "relDay"; 44 | public static final String XML_ATTR_RELMONTH = "relMonth"; 45 | public static final String XML_ATTR_RELYEAR = "relYear"; 46 | 47 | 48 | // These listeners don't have to be implemented, if null just ignore 49 | private OnDateSelectedListener dateListener = null; 50 | private OnClickListener customDatePicker = null; 51 | 52 | // The default DatePicker dialog to show if customDatePicker has not been set 53 | private final DatePickerDialog datePickerDialog; 54 | private FragmentManager fragmentManager; 55 | 56 | private boolean showPastItems = false; 57 | private boolean showMonthItem = false; 58 | private boolean showWeekdayNames = false; 59 | private boolean showNumbersInView = false; 60 | 61 | private String[] weekDays = null; 62 | 63 | // To catch twice selecting the same date: 64 | private Calendar lastSelectedDate = null; 65 | 66 | // Min and mix date to be shown (are currently not restored during rotation as they are mostly set in the onCreate() anyway): 67 | private Calendar minDate = null; 68 | private Calendar maxDate = null; 69 | 70 | // The custom DateFormat used to convert Calendars into displayable Strings: 71 | private java.text.DateFormat customDateFormat = null; 72 | private java.text.DateFormat secondaryDateFormat = null; 73 | 74 | /** 75 | * Construct a new DateSpinner with the given context's theme. 76 | * @param context The Context the view is running in, through which it can access the current theme, resources, etc. 77 | */ 78 | public DateSpinner(Context context){ 79 | this(context, null, 0); 80 | } 81 | 82 | /** 83 | * Construct a new DateSpinner with the given context's theme and the supplied attribute set. 84 | * @param context The Context the view is running in, through which it can access the current theme, resources, etc. 85 | * @param attrs The attributes of the XML tag that is inflating the view. May contain a flags attribute. 86 | */ 87 | public DateSpinner(Context context, AttributeSet attrs){ 88 | this(context, attrs, 0); 89 | } 90 | 91 | /** 92 | * Construct a new TimeSpinner with the given context's theme, the supplied attribute set, and default style. 93 | * @param context The Context the view is running in, through which it can access the current theme, resources, etc. 94 | * @param attrs The attributes of the XML tag that is inflating the view. May contain a flags attribute. 95 | * @param defStyle The default style to apply to this view. If 0, no style will be applied (beyond 96 | * what is included in the theme). This may either be an attribute resource, whose 97 | * value will be retrieved from the current theme, or an explicit style resource. 98 | */ 99 | public DateSpinner(Context context, AttributeSet attrs, int defStyle){ 100 | super(context, attrs, defStyle); 101 | // check if the parent activity has our dateSelectedListener, automatically enable it: 102 | if(context instanceof OnDateSelectedListener) 103 | setOnDateSelectedListener((OnDateSelectedListener) context); 104 | setOnItemSelectedListener(this); 105 | 106 | final Calendar calendar = Calendar.getInstance(); 107 | // create the dialog: 108 | datePickerDialog = DatePickerDialog.newInstance( 109 | new DatePickerDialog.OnDateSetListener() { 110 | @Override 111 | public void onDateSet(DatePickerDialog datePickerDialog, int year, int month, int day) { 112 | setSelectedDate(new GregorianCalendar(year, month, day)); 113 | } 114 | }, 115 | calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), 116 | calendar.get(Calendar.DAY_OF_MONTH), hasVibratePermission(context)); 117 | 118 | // the default min date is today: 119 | setMinDate(calendar); 120 | 121 | // get the FragmentManager: 122 | try{ 123 | fragmentManager = ((FragmentActivity) context).getSupportFragmentManager(); 124 | } catch (ClassCastException e) { 125 | Log.d(getClass().getSimpleName(), "Can't get fragment manager from context"); 126 | } 127 | 128 | if(attrs != null) { 129 | // get our flags from xml, if set: 130 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ReminderDatePicker); 131 | int flags = a.getInt(R.styleable.ReminderDatePicker_flags, ReminderDatePicker.MODE_GOOGLE); 132 | setFlags(flags); 133 | a.recycle(); 134 | } 135 | } 136 | 137 | private boolean hasVibratePermission(Context context) { 138 | final String permission = "android.permission.VIBRATE"; 139 | final int res = context.checkCallingOrSelfPermission(permission); 140 | return (res == PackageManager.PERMISSION_GRANTED); 141 | } 142 | 143 | @Override 144 | public List getSpinnerItems() { 145 | try { 146 | return getItemsFromXml(R.xml.date_items); 147 | } catch (Exception e) { 148 | Log.d("DateSpinner", "Error parsing date items from xml"); 149 | e.printStackTrace(); 150 | } 151 | return null; 152 | } 153 | 154 | @Override 155 | protected @Nullable TwinTextItem parseItemFromXmlTag(@NonNull XmlResourceParser parser) { 156 | if(!parser.getName().equals(XML_TAG_DATEITEM)) { 157 | Log.d("DateSpinner", "Unknown xml tag name: " + parser.getName()); 158 | return null; 159 | } 160 | 161 | // parse the DateItem, possible values are 162 | String text = null; 163 | @StringRes int textResource = NO_ID, id = NO_ID; 164 | Calendar date = Calendar.getInstance(); 165 | for(int i=parser.getAttributeCount()-1; i>=0; i--) { 166 | String attrName = parser.getAttributeName(i); 167 | switch (attrName) { 168 | case XML_ATTR_ID: 169 | id = parser.getIdAttributeResourceValue(NO_ID); 170 | break; 171 | case XML_ATTR_TEXT: 172 | text = parser.getAttributeValue(i); 173 | // try to get a resource value, the string is retrieved below 174 | if(text != null && text.startsWith("@")) 175 | textResource = parser.getAttributeResourceValue(i, NO_ID); 176 | break; 177 | 178 | case XML_ATTR_ABSDAYOFYEAR: 179 | final int absDayOfYear = parser.getAttributeIntValue(i, -1); 180 | if(absDayOfYear > 0) 181 | date.set(Calendar.DAY_OF_YEAR, absDayOfYear); 182 | break; 183 | case XML_ATTR_ABSDAYOFMONTH: 184 | final int absDayOfMonth = parser.getAttributeIntValue(i, -1); 185 | if(absDayOfMonth > 0) 186 | date.set(Calendar.DAY_OF_MONTH, absDayOfMonth); 187 | break; 188 | case XML_ATTR_ABSMONTH: 189 | final int absMonth = parser.getAttributeIntValue(i, -1); 190 | if(absMonth >= 0) 191 | date.set(Calendar.MONTH, absMonth); 192 | break; 193 | case XML_ATTR_ABSYEAR: 194 | final int absYear = parser.getAttributeIntValue(i, -1); 195 | if(absYear >= 0) 196 | date.set(Calendar.YEAR, absYear); 197 | break; 198 | 199 | case XML_ATTR_RELDAY: 200 | final int relDay = parser.getAttributeIntValue(i, 0); 201 | date.add(Calendar.DAY_OF_YEAR, relDay); 202 | break; 203 | case XML_ATTR_RELMONTH: 204 | final int relMonth = parser.getAttributeIntValue(i, 0); 205 | date.add(Calendar.MONTH, relMonth); 206 | break; 207 | case XML_ATTR_RELYEAR: 208 | final int relYear = parser.getAttributeIntValue(i, 0); 209 | date.add(Calendar.YEAR, relYear); 210 | break; 211 | default: 212 | Log.d("DateSpinner", "Skipping unknown attribute tag parsing xml resource: " 213 | + attrName + ", maybe a typo?"); 214 | } 215 | }// end for attr 216 | 217 | // now construct the date item from the attributes 218 | 219 | // check if we got a textResource earlier and parse that string together with the weekday 220 | if(textResource != NO_ID) 221 | text = getWeekDay(date.get(Calendar.DAY_OF_WEEK), textResource); 222 | 223 | // when no text is given, format the date to have at least something to show 224 | if(text == null || text.equals("")) 225 | text = formatDate(date); 226 | 227 | return new DateItem(text, date, id); 228 | } 229 | 230 | private String getWeekDay(int weekDay, @StringRes int stringRes) { 231 | if(weekDays == null) weekDays = new DateFormatSymbols().getWeekdays(); 232 | // use a separate string for Saturday and Sunday because of gender variation in Portuguese 233 | if(weekDay==7 || weekDay==1) { 234 | if(stringRes == R.string.date_next_weekday) 235 | stringRes = R.string.date_next_weekday_weekend; 236 | else if(stringRes == R.string.date_last_weekday) 237 | stringRes = R.string.date_last_weekday_weekend; 238 | } 239 | String result = getResources().getString(stringRes, weekDays[weekDay]); 240 | // in some translations (French for instance), the weekday is the first word but is not capitalized, so we'll do that 241 | return Character.toUpperCase(result.charAt(0)) + result.substring(1); 242 | } 243 | 244 | /** 245 | * Gets the currently selected date (that the Spinner is showing) 246 | * @return The selected date as Calendar, or null if there is none. 247 | */ 248 | public Calendar getSelectedDate() { 249 | final Object selectedItem = getSelectedItem(); 250 | if(!(selectedItem instanceof DateItem)) 251 | return null; 252 | return ((DateItem) selectedItem).getDate(); 253 | } 254 | 255 | /** 256 | * Sets the Spinner's selection as date. If the date was not in the possible selections, a temporary 257 | * item is created and passed to selectTemporary(). 258 | * @param date The date to be selected. 259 | */ 260 | public void setSelectedDate(@NonNull Calendar date) { 261 | final int count = getAdapter().getCount() - 1; 262 | int itemPosition = -1; 263 | for(int i=0; i= 0) 270 | setSelection(itemPosition); 271 | else if(showWeekdayNames) { 272 | final long MILLIS_IN_DAY = 1000*60*60*24; 273 | final long dateDifference = (date.getTimeInMillis()/MILLIS_IN_DAY) 274 | - (Calendar.getInstance().getTimeInMillis()/MILLIS_IN_DAY); 275 | if(dateDifference>0 && dateDifference<7) { // if the date is within the next week: 276 | // construct a temporary DateItem to select: 277 | final int day = date.get(Calendar.DAY_OF_WEEK); 278 | 279 | // Because these items are always temporarily selected, we can safely assume that 280 | // they will never appear in the spinner dropdown. When a FLAG_NUMBERS is set, we 281 | // want these items to have the date as secondary text in a short format. 282 | selectTemporary(new DateItem(getWeekDay(day, R.string.date_only_weekday), formatSecondaryDate(date), date, NO_ID)); 283 | } else { 284 | // show the date as a full text, using the current DateFormat: 285 | selectTemporary(new DateItem(formatDate(date), date, NO_ID)); 286 | } 287 | } 288 | else { 289 | // show the date as a full text, using the current DateFormat: 290 | selectTemporary(new DateItem(formatDate(date), date, NO_ID)); 291 | } 292 | } 293 | 294 | private String formatDate(@NonNull Calendar date) { 295 | if(customDateFormat == null) 296 | return DateUtils.formatDateTime(getContext(), date.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE); 297 | else 298 | return customDateFormat.format(date.getTime()); 299 | } 300 | 301 | // only to be used when FLAG_NUMBERS and FLAG_WEEKDAY_NAMES have been set 302 | private String formatSecondaryDate(@NonNull Calendar date) { 303 | if(secondaryDateFormat == null) 304 | return DateUtils.formatDateTime(getContext(), date.getTimeInMillis(), 305 | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE); 306 | else 307 | return secondaryDateFormat.format(date.getTime()); 308 | } 309 | 310 | /** 311 | * Gets the custom DateFormat currently used to format Calendar strings. 312 | * If {@link #setDateFormat(java.text.DateFormat)} has not been called yet, it will return null. 313 | * @return The date format, or null if the Spinner is using the default date format. 314 | */ 315 | public java.text.DateFormat getCustomDateFormat() { 316 | return customDateFormat; 317 | } 318 | 319 | /** 320 | * Sets the custom date format to use for formatting Calendar objects to displayable strings. 321 | * @param dateFormat The new DateFormat, or null to use the default format. 322 | */ 323 | public void setDateFormat(java.text.DateFormat dateFormat) { 324 | setDateFormat(dateFormat, null); 325 | } 326 | 327 | /** 328 | * Sets the custom date format to use for formatting Calendar objects to displayable strings. 329 | * @param dateFormat The new DateFormat, or null to use the default format. 330 | * @param numbersDateFormat The DateFormat for formatting the secondary date when both FLAG_NUMBERS 331 | * and FLAG_WEEKDAY_NAMES are set, or null to use the default format. 332 | */ 333 | public void setDateFormat(java.text.DateFormat dateFormat, java.text.DateFormat numbersDateFormat) { 334 | this.customDateFormat = dateFormat; 335 | this.secondaryDateFormat = numbersDateFormat; 336 | // update the spinner with the new date format: 337 | 338 | // the only spinner item that will be affected is the month item, so just toggle the flag twice 339 | // instead of rebuilding the whole adapter 340 | if(showMonthItem) { 341 | int monthPosition = getAdapterItemPosition(4); 342 | boolean reselectMonthItem = getSelectedItemPosition() == monthPosition; 343 | setShowMonthItem(false); 344 | setShowMonthItem(true); 345 | if(reselectMonthItem) setSelection(monthPosition); 346 | } 347 | 348 | // if we have a temporary date item selected, update that as well 349 | if(getSelectedItemPosition() == getAdapter().getCount()) 350 | setSelectedDate(getSelectedDate()); 351 | } 352 | 353 | 354 | /** 355 | * Sets the minimum allowed date. 356 | * Spinner items and dates in the date picker before the given date will get disabled. 357 | * @param minDate The minimum date, or null to clear the previous min date. 358 | */ 359 | public void setMinDate(@Nullable Calendar minDate) { 360 | this.minDate = minDate; 361 | // update the date picker (even if it is not used right now) 362 | if(minDate == null) 363 | datePickerDialog.setMinDate(MINIMUM_POSSIBLE_DATE); 364 | else if(maxDate != null && compareCalendarDates(minDate, maxDate) > 0) 365 | throw new IllegalArgumentException("Minimum date must be before maximum date!"); 366 | else 367 | datePickerDialog.setMinDate(new CalendarDay(minDate)); 368 | updateEnabledItems(); 369 | } 370 | 371 | /** 372 | * Gets the current minimum allowed date. 373 | * @return The minimum date, or null if there is none. 374 | */ 375 | public @Nullable Calendar getMinDate() { 376 | return minDate; 377 | } 378 | 379 | /** 380 | * Sets the maximum allowed date. 381 | * Spinner items and dates in the date picker after the given date will get disabled. 382 | * @param maxDate The maximum date, or null to clear the previous max date. 383 | */ 384 | public void setMaxDate(@Nullable Calendar maxDate) { 385 | this.maxDate = maxDate; 386 | // update the date picker (even if it is not used right now) 387 | if(maxDate == null) 388 | datePickerDialog.setMaxDate(null); 389 | else if(minDate != null && compareCalendarDates(minDate, maxDate) > 0) 390 | throw new IllegalArgumentException("Maximum date must be after minimum date!"); 391 | else 392 | datePickerDialog.setMaxDate(new CalendarDay(maxDate)); 393 | updateEnabledItems(); 394 | } 395 | 396 | /** 397 | * Gets the current maximum allowed date. 398 | * @return The maximum date, or null if there is none. 399 | */ 400 | public @Nullable Calendar getMaxDate() { 401 | return maxDate; 402 | } 403 | 404 | 405 | /** 406 | * Loops through the Spinner items and disables all that are not within the min/max date range. 407 | */ 408 | private void updateEnabledItems() { 409 | PickerSpinnerAdapter adapter = (PickerSpinnerAdapter) getAdapter(); 410 | // if the current item is out of range, we have no choice but to reset it 411 | if(!isInDateRange(getSelectedDate())) { 412 | final Calendar today = Calendar.getInstance(); 413 | if(isInDateRange(today)) 414 | setSelectedDate(today); 415 | else 416 | // if today itself is not a valid date, we will just use the minimum date (which is always set here) 417 | setSelectedDate(minDate); 418 | } 419 | 420 | for(int position = getLastItemPosition(); position >= 0; position--) { 421 | DateItem item = (DateItem) adapter.getItem(position); 422 | if(isInDateRange(item.getDate())) 423 | item.setEnabled(true); 424 | else 425 | item.setEnabled(false); 426 | } 427 | } 428 | 429 | private boolean isInDateRange(@NonNull Calendar date) { 430 | return (minDate == null || compareCalendarDates(minDate, date) <= 0) // later than minDate 431 | && (maxDate == null || compareCalendarDates(maxDate, date) >= 0); // before maxDate 432 | } 433 | 434 | /** 435 | * Compares the two given Calendar objects, only counting the date, not time. 436 | * @return -1 if first comes before second, 0 if both are the same day, 1 if second is before first. 437 | */ 438 | static int compareCalendarDates(@NonNull Calendar first, @NonNull Calendar second) { 439 | final int firstYear = first.get(Calendar.YEAR); 440 | final int secondYear = second.get(Calendar.YEAR); 441 | final int firstDay = first.get(Calendar.DAY_OF_YEAR); 442 | final int secondDay = second.get(Calendar.DAY_OF_YEAR); 443 | if(firstYear == secondYear) { 444 | if(firstDay == secondDay) 445 | return 0; 446 | else if(firstDay < secondDay) 447 | return -1; 448 | else 449 | return 1; 450 | } 451 | else if(firstYear < secondYear) 452 | return -1; 453 | else 454 | return 1; 455 | } 456 | 457 | /** 458 | * Implement this interface if you want to be notified whenever the selected date changes. 459 | */ 460 | public void setOnDateSelectedListener(OnDateSelectedListener listener) { 461 | this.dateListener = listener; 462 | } 463 | 464 | /** 465 | * Gets the default {@link DatePickerDialog} that is shown when the footer is clicked. 466 | * @return The dialog, or null if a custom date picker has been set and the default one is thus unused. 467 | */ 468 | public @Nullable DatePickerDialog getDatePickerDialog() { 469 | if(customDatePicker != null) 470 | return null; 471 | return datePickerDialog; 472 | } 473 | 474 | /** 475 | * Sets a custom listener whose onClick method will be called to create and handle the custom date picker. 476 | * You should call {@link #setSelectedDate} when the custom picker is finished. 477 | * @param launchPicker An {@link android.view.View.OnClickListener} whose onClick method will be 478 | * called to show the custom date picker, or null to use the default picker. 479 | */ 480 | public void setCustomDatePicker(@Nullable OnClickListener launchPicker) { 481 | this.customDatePicker = launchPicker; 482 | } 483 | 484 | /** 485 | * Toggles showing the past items. Past mode shows the yesterday and last weekday item. 486 | * @param enable True to enable, false to disable past mode. 487 | */ 488 | public void setShowPastItems(boolean enable) { 489 | if(enable && !showPastItems) { 490 | // first reset the minimum date if necessary: 491 | if(getMinDate() != null && compareCalendarDates(getMinDate(), Calendar.getInstance()) == 0) 492 | setMinDate(null); 493 | 494 | // create the yesterday and last Monday item: 495 | final Resources res = getResources(); 496 | final Calendar date = Calendar.getInstance(); 497 | // yesterday: 498 | date.add(Calendar.DAY_OF_YEAR, -1); 499 | insertAdapterItem(new DateItem(res.getString(R.string.date_yesterday), date, R.id.date_yesterday), 0); 500 | // last weekday item: 501 | date.add(Calendar.DAY_OF_YEAR, -6); 502 | int weekday = date.get(Calendar.DAY_OF_WEEK); 503 | insertAdapterItem(new DateItem(getWeekDay(weekday, R.string.date_last_weekday), 504 | date, R.id.date_last_week), 0); 505 | } 506 | else if(!enable && showPastItems) { 507 | // delete the yesterday and last weekday items: 508 | removeAdapterItemById(R.id.date_last_week); 509 | removeAdapterItemById(R.id.date_yesterday); 510 | 511 | // we set the minimum date to today as we don't allow past items 512 | setMinDate(Calendar.getInstance()); 513 | } 514 | showPastItems = enable; 515 | } 516 | 517 | /** 518 | * Toggles showing the month item. Month mode an item in exactly one month from now. 519 | * @param enable True to enable, false to disable month mode. 520 | */ 521 | public void setShowMonthItem(boolean enable) { 522 | if(enable && !showMonthItem) { 523 | // create the in 1 month item 524 | final Calendar date = Calendar.getInstance(); 525 | date.add(Calendar.MONTH, 1); 526 | addAdapterItem(new DateItem(formatDate(date), date, R.id.date_month)); 527 | } 528 | else if(!enable && showMonthItem) { 529 | removeAdapterItemById(R.id.date_month); 530 | } 531 | showMonthItem = enable; 532 | } 533 | 534 | /** 535 | * Toggles showing the weekday names instead of dates for the next week. Turning this on will 536 | * display e.g. "Sunday" for the day after tomorrow, otherwise it'll be January 1. 537 | * @param enable True to enable, false to disable weekday names. 538 | */ 539 | public void setShowWeekdayNames(boolean enable) { 540 | if(showWeekdayNames != enable) { 541 | showWeekdayNames = enable; 542 | // if FLAG_NUMBERS has been set, toggle the secondary text in the adapter 543 | if(showNumbersInView) 544 | setShowNumbersInViewInt(enable); 545 | // reselect the current item so it will use the new setting: 546 | setSelectedDate(getSelectedDate()); 547 | } 548 | } 549 | 550 | /** 551 | * Toggles showing numeric dates for the weekday items in the spinner view. This will only apply 552 | * when a day within the next week is selected and FLAG_WEEKDAY_NAMES has been set, not in the dropdown. 553 | * @param enable True to enable, false to disable numeric mode. 554 | */ 555 | public void setShowNumbersInView(boolean enable) { 556 | showNumbersInView = enable; 557 | // only enable the adapter when FLAG_WEEKDAY_NAMES has been set as well 558 | if(!enable || showWeekdayNames) 559 | setShowNumbersInViewInt(enable); 560 | } 561 | 562 | private void setShowNumbersInViewInt(boolean enable) { 563 | PickerSpinnerAdapter adapter = (PickerSpinnerAdapter) getAdapter(); 564 | // workaround for now. See GitHub issue #2 565 | if (enable != adapter.isShowingSecondaryTextInView() && adapter.getCount() == getSelectedItemPosition()) 566 | setSelection(0); 567 | adapter.setShowSecondaryTextInView(enable); 568 | } 569 | 570 | /** 571 | * Set the flags to use for this date spinner. 572 | * @param modeOrFlags A mode of ReminderDatePicker.MODE_... or multiple ReminderDatePicker.FLAG_... 573 | * combined with the | operator. 574 | */ 575 | public void setFlags(int modeOrFlags) { 576 | setShowPastItems((modeOrFlags & ReminderDatePicker.FLAG_PAST) != 0); 577 | setShowMonthItem((modeOrFlags & ReminderDatePicker.FLAG_MONTH) != 0); 578 | setShowWeekdayNames((modeOrFlags & ReminderDatePicker.FLAG_WEEKDAY_NAMES) != 0); 579 | setShowNumbersInView((modeOrFlags & ReminderDatePicker.FLAG_NUMBERS) != 0); 580 | } 581 | 582 | /** 583 | * {@inheritDoc} 584 | */ 585 | @Override 586 | public void removeAdapterItemAt(int index) { 587 | if(index == getSelectedItemPosition()) { 588 | Calendar date = getSelectedDate(); 589 | selectTemporary(new DateItem(formatDate(date), date, NO_ID)); 590 | } 591 | super.removeAdapterItemAt(index); 592 | } 593 | 594 | @Override 595 | public CharSequence getFooter() { 596 | return getResources().getString(R.string.spinner_date_footer); 597 | } 598 | 599 | @Override 600 | public void onFooterClick() { 601 | if (customDatePicker == null) { 602 | // update the selected date in the dialog 603 | final Calendar date = getSelectedDate(); 604 | datePickerDialog.onDateSelected( 605 | date.get(Calendar.YEAR), date.get(Calendar.MONTH), date.get(Calendar.DAY_OF_MONTH)); 606 | datePickerDialog.show(fragmentManager, "DatePickerDialog"); 607 | } else { 608 | customDatePicker.onClick(this); 609 | } 610 | } 611 | 612 | @Override 613 | protected void restoreTemporarySelection(String codeString) { 614 | selectTemporary(DateItem.fromString(codeString)); 615 | } 616 | 617 | @Override 618 | public void onItemSelected(AdapterView parent, View view, int position, long id) { 619 | if(dateListener != null) { 620 | // catch selecting same date twice 621 | Calendar date = getSelectedDate(); 622 | if(date != null && !date.equals(lastSelectedDate)) { 623 | dateListener.onDateSelected(date); 624 | lastSelectedDate = date; 625 | } 626 | } 627 | } 628 | 629 | // unused 630 | @Override 631 | public void onNothingSelected(AdapterView parent) { 632 | } 633 | } 634 | -------------------------------------------------------------------------------- /lib/src/main/java/com/simplicityapks/reminderdatepicker/lib/OnDateSelectedListener.java: -------------------------------------------------------------------------------- 1 | package com.simplicityapks.reminderdatepicker.lib; 2 | 3 | import java.util.Calendar; 4 | 5 | /** 6 | * Implement this interface if you want to be notified whenever the selected date changes. 7 | */ 8 | public interface OnDateSelectedListener { 9 | /** 10 | * Called whenever a new date is selected in the Picker calling this. 11 | * @param date The new selected date, as Calendar. 12 | */ 13 | public void onDateSelected(Calendar date); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/main/java/com/simplicityapks/reminderdatepicker/lib/PickerSpinner.java: -------------------------------------------------------------------------------- 1 | package com.simplicityapks.reminderdatepicker.lib; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.content.res.XmlResourceParser; 6 | import android.os.Bundle; 7 | import android.os.Parcelable; 8 | import android.support.annotation.NonNull; 9 | import android.support.annotation.Nullable; 10 | import android.support.annotation.XmlRes; 11 | import android.util.AttributeSet; 12 | import android.util.Log; 13 | import android.view.View; 14 | import android.widget.AdapterView; 15 | import android.widget.SpinnerAdapter; 16 | 17 | import org.xmlpull.v1.XmlPullParser; 18 | import org.xmlpull.v1.XmlPullParserException; 19 | 20 | import java.io.IOException; 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | /** 25 | * Base class for both DateSpinner and TimeSpinner. 26 | * This is a Spinner with the following additional, optional features: 27 | * 28 | * 1. A custom last list item (footer), which won't get selected on click. Instead, onFooterClick() will be called. 29 | * 2. Items with secondary text, due to integration with {@link com.simplicityapks.reminderdatepicker.lib.PickerSpinnerAdapter} 30 | * 3. Select items which are not currently in the spinner items (use {@link #selectTemporary(TwinTextItem)}. 31 | * 4. Dynamic and easy modifying the spinner items without having to worry about selection changes (use the ...AdapterItem...() methods) 32 | */ 33 | public abstract class PickerSpinner extends android.support.v7.widget.AppCompatSpinner { 34 | 35 | public static final String XML_ATTR_ID = "id"; 36 | public static final String XML_ATTR_TEXT = "text"; 37 | 38 | // Indicates that the onItemSelectedListener callback should not be passed to the listener. 39 | private final ArrayList interceptSelectionCallbacks = new ArrayList<>(); 40 | // Indicates that the selection should be restored after initialization (setSelection has not been called externally) 41 | private boolean restoreTemporarySelection = false; 42 | // Indicates that the temporary item should be reselected after an item is removed 43 | private boolean reselectTemporaryItem = false; 44 | 45 | /** 46 | * Construct a new PickerSpinner with the given context's theme. 47 | * @param context The Context the view is running in, through which it can access the current theme, resources, etc. 48 | */ 49 | public PickerSpinner(Context context) { 50 | this(context, null); 51 | } 52 | 53 | /** 54 | * Construct a new PickerSpinner with the given context's theme and the supplied attribute set. 55 | * @param context The Context the view is running in, through which it can access the current theme, resources, etc. 56 | * @param attrs The attributes of the XML tag that is inflating the view. 57 | */ 58 | public PickerSpinner(Context context, AttributeSet attrs) { 59 | this(context, attrs, 0); 60 | } 61 | 62 | /** 63 | * Construct a new PickerSpinner with the given context's theme, the supplied attribute set, and default style. 64 | * @param context The Context the view is running in, through which it can access the current theme, resources, etc. 65 | * @param attrs The attributes of the XML tag that is inflating the view. 66 | * @param defStyle The default style to apply to this view. If 0, no style will be applied (beyond 67 | * what is included in the theme). This may either be an attribute resource, whose 68 | * value will be retrieved from the current theme, or an explicit style resource. 69 | */ 70 | public PickerSpinner(Context context, AttributeSet attrs, int defStyle) { 71 | super(context, attrs); 72 | initAdapter(context); 73 | } 74 | 75 | protected void initAdapter(Context context) { 76 | CharSequence footer = getFooter(); 77 | TwinTextItem footerItem = footer == null? null : new TwinTextItem.Simple(footer, null); 78 | // create our simple adapter with default layouts and set it here: 79 | PickerSpinnerAdapter adapter = new PickerSpinnerAdapter(context, getSpinnerItems(), footerItem); 80 | setAdapter(adapter); 81 | } 82 | 83 | @NonNull 84 | @Override 85 | public Parcelable onSaveInstanceState() { 86 | // our temporary selection will not be saved 87 | if(getSelectedItemPosition() == getAdapter().getCount()) { 88 | Bundle state = new Bundle(); 89 | state.putParcelable("superState", super.onSaveInstanceState()); 90 | // save the TwinTextItem using its toString() method 91 | state.putString("temporaryItem", getSelectedItem().toString()); 92 | return state; 93 | } 94 | else return super.onSaveInstanceState(); 95 | } 96 | 97 | @Override 98 | public void onRestoreInstanceState(Parcelable state) { 99 | if(state instanceof Bundle) { 100 | Bundle bundle = (Bundle) state; 101 | super.onRestoreInstanceState(bundle.getParcelable("superState")); 102 | final String tempItem = bundle.getString("temporaryItem"); 103 | restoreTemporarySelection(tempItem); 104 | } 105 | else super.onRestoreInstanceState(state); 106 | } 107 | 108 | /** 109 | * {@inheritDoc} 110 | */ 111 | @Override 112 | public void setVisibility(int visibility) { 113 | super.setVisibility(visibility); 114 | // When going from state gone to visible with a temporary item selected, but the array has 115 | // changed (by toggling FLAG_MORE_TIME), the position is somehow reset by the system, so we 116 | // need to reselect the temporary item (even if it was already selected). 117 | // This is merely a workaround as I can't find a better solution. 118 | if(visibility == VISIBLE) { 119 | reselectTemporaryItem = false; 120 | PickerSpinnerAdapter adapter = (PickerSpinnerAdapter) getAdapter(); 121 | int count = adapter.getCount(); 122 | // check whether we have the temporary item selected 123 | if(getSelectedItemPosition() == count) { 124 | // get the temp item from the adapter to reselect it later: 125 | TwinTextItem tempItem = null; 126 | try { 127 | tempItem = adapter.getItem(count); 128 | } catch (IndexOutOfBoundsException e) { 129 | Log.d("PickerSpinner", "SetVisibility: Couldn't get temporary item from adapter, aborting workaround"); 130 | } 131 | // now reselect the temporary item 132 | if(tempItem != null) { 133 | selectTemporary(tempItem); 134 | } 135 | } 136 | } 137 | 138 | } 139 | 140 | 141 | /** 142 | * Sets the Adapter used to provide the data which backs this Spinner. Needs to be an {@link com.simplicityapks.reminderdatepicker.lib.PickerSpinnerAdapter} 143 | * to be used with this class. Note that a PickerSpinner automatically creates its own adapter 144 | * so you should not need to call this method. 145 | * @param adapter The PickerSpinnerAdapter to be used. 146 | * @throws IllegalArgumentException If adapter is not a PickerSpinnerAdapter. 147 | */ 148 | @Override 149 | public void setAdapter(SpinnerAdapter adapter) { 150 | if(adapter instanceof PickerSpinnerAdapter) 151 | super.setAdapter(adapter); 152 | else throw new IllegalArgumentException( 153 | "adapter must extend PickerSpinnerAdapter to be used with this class"); 154 | } 155 | 156 | /** 157 | * {@inheritDoc} 158 | */ 159 | @Override 160 | public void setSelection(int position) { 161 | PickerSpinnerAdapter adapter = (PickerSpinnerAdapter) getAdapter(); 162 | if(position == adapter.getCount()-1 && adapter.hasFooter()) 163 | onFooterClick(); // the footer has been clicked, so don't update the selection 164 | else { 165 | // remove any previous temporary selection: 166 | ((PickerSpinnerAdapter)getAdapter()).selectTemporary(null); 167 | reselectTemporaryItem = false; 168 | restoreTemporarySelection = false; 169 | // check that the selection goes through: 170 | interceptSelectionCallbacks.clear(); 171 | super.setSelection(position); 172 | super.setSelection(position, false); 173 | } 174 | } 175 | 176 | /** 177 | * Equivalent to {@link #setSelection(int)}, but without calling any onItemSelectedListeners or 178 | * checking for footer clicks. 179 | */ 180 | private void setSelectionQuietly(int position) { 181 | // intercept the callback here: 182 | interceptSelectionCallbacks.add(position); 183 | superSetSelection(position); 184 | } 185 | 186 | private void superSetSelection(int position) { 187 | super.setSelection(position, false); // No idea why both setSelections are needed but it only works with both 188 | super.setSelection(position); 189 | } 190 | 191 | /** 192 | * Push an item to be selected, but not shown in the dropdown menu. This is similar to calling 193 | * setText(item.toString()) if a Spinner had such a method. 194 | * @param item The item to select, or null to remove any temporary selection. 195 | */ 196 | public void selectTemporary(TwinTextItem item) { 197 | // if we just want to clear the selection: 198 | if(item == null) { 199 | setSelection(getLastItemPosition()); 200 | // the call is passed on to the adapter in setSelection. 201 | return; 202 | } 203 | PickerSpinnerAdapter adapter = (PickerSpinnerAdapter) getAdapter(); 204 | // pass on the call to the adapter (just stores the item): 205 | adapter.selectTemporary(item); 206 | final int tempItemPosition = adapter.getCount(); 207 | if(getSelectedItemPosition() == tempItemPosition) { 208 | // this is quite a hack, first reset the position to 0 but intercept the callback, 209 | // then redo the selection: 210 | setSelectionQuietly(0); 211 | } 212 | super.setSelection(tempItemPosition); 213 | // during initialization the system might check our selected position and reset it, 214 | // thus we need to check after the message queue has been settled 215 | if (!restoreTemporarySelection) { 216 | restoreTemporarySelection = true; 217 | post(new Runnable() { 218 | @Override 219 | public void run() { 220 | if (restoreTemporarySelection) { 221 | restoreTemporarySelection = false; 222 | reselectTemporaryItem = false; 223 | final int tempItemPosition = getAdapter().getCount(); 224 | if (getSelectedItemPosition() != tempItemPosition) 225 | superSetSelection(tempItemPosition); 226 | } 227 | } 228 | }); 229 | } 230 | } 231 | 232 | @Override 233 | public void setOnItemSelectedListener(final OnItemSelectedListener listener) { 234 | super.setOnItemSelectedListener( 235 | new OnItemSelectedListener() { 236 | @Override 237 | public void onItemSelected(AdapterView parent, View view, int position, long id) { 238 | if (reselectTemporaryItem) { 239 | reselectTemporaryItem = false; 240 | final int tempItemPosition = getAdapter().getCount(); 241 | if (position != tempItemPosition) 242 | setSelectionQuietly(tempItemPosition); 243 | } 244 | if (interceptSelectionCallbacks.contains(position)) { 245 | interceptSelectionCallbacks.remove((Integer) position); 246 | } 247 | // sometimes during rotation or initialization onItemSelected will be called with the footer selected, catch that 248 | else if (!(((PickerSpinnerAdapter) getAdapter()).hasFooter() 249 | && position == getLastItemPosition() + 1)) 250 | listener.onItemSelected(parent, view, position, id); 251 | } 252 | 253 | @Override 254 | public void onNothingSelected(AdapterView parent) { 255 | listener.onNothingSelected(parent); 256 | } 257 | } 258 | ); 259 | } 260 | 261 | /** 262 | * Gets the position of the last item in the dataset, after which the footer and temporary selection have their index. 263 | * @return The last selectable position. 264 | */ 265 | public int getLastItemPosition() { 266 | PickerSpinnerAdapter adapter = (PickerSpinnerAdapter) getAdapter(); 267 | return adapter.getCount() - (adapter.hasFooter()? 2 : 1); 268 | } 269 | 270 | /** 271 | * Finds a spinner adapter item by its id value (excluding any temporary selection). 272 | * @param id The id of the item to search. 273 | * @return The specified TwinTextItem, or null if no item with the given id was found. 274 | */ 275 | public @Nullable TwinTextItem getAdapterItemById(int id) { 276 | return ((PickerSpinnerAdapter) getAdapter()).getItemById(id); 277 | } 278 | 279 | /** 280 | * Finds a spinner item's position in the data set by its id value (excluding any temporary selection). 281 | * @param id The id of the item to search. 282 | * @return The position of the specified TwinTextItem, or -1 if no item with the given id was found. 283 | */ 284 | public int getAdapterItemPosition(int id) { 285 | return ((PickerSpinnerAdapter) getAdapter()).getItemPosition(id); 286 | } 287 | 288 | /** 289 | * Adds the item to the adapter's data set and takes care of handling selection changes. 290 | * Always call this method instead of getAdapter().add(). 291 | * @param item The item to insert. 292 | */ 293 | public void addAdapterItem(TwinTextItem item) { 294 | insertAdapterItem(item, getLastItemPosition()+1); 295 | } 296 | 297 | /** 298 | * Inserts the item at the specified index into the adapter's data set and takes care of handling selection changes. 299 | * Always call this method instead of getAdapter().insert(). 300 | * @param item The item to insert. 301 | * @param index The index where it'll be at. 302 | */ 303 | public void insertAdapterItem(TwinTextItem item, int index) { 304 | int selection = getSelectedItemPosition(); 305 | Object selectedItem = getSelectedItem(); 306 | ((PickerSpinnerAdapter) getAdapter()).insert(item, index); 307 | // select the new item if there was an equal temporary item selected 308 | if(selectedItem.equals(item)) 309 | setSelectionQuietly(index); 310 | // otherwise keep track when inserting above the selection 311 | else if(index <= selection) 312 | setSelectionQuietly(selection+1); 313 | } 314 | 315 | /** 316 | * Removes the specified item from the adapter and takes care of handling selection changes. 317 | * Always call this method instead of getAdapter().remove(). 318 | * Note that if you remove the selected item here, it will just reselect the next one instead of 319 | * creating a temporary item containing the current selection. 320 | * @param index The index of the item to be removed. 321 | */ 322 | public void removeAdapterItemAt(int index) { 323 | PickerSpinnerAdapter adapter = (PickerSpinnerAdapter) getAdapter(); 324 | int count = adapter.getCount(); 325 | int selection = getSelectedItemPosition(); 326 | 327 | // check which item will be removed: 328 | if(index == count) // temporary selection 329 | selectTemporary(null); 330 | else if (index == count-1 && adapter.hasFooter()) { // footer 331 | if(selection == count) 332 | setSelectionQuietly(selection - 1); 333 | adapter.setFooter(null); 334 | } else { // a normal item 335 | // keep the right selection in either of these cases: 336 | if(index == selection) { // we delete the selected item and 337 | if(index == getLastItemPosition()) // it is the last real item 338 | setSelection(selection - 1); 339 | else { 340 | // we need to reselect the current item 341 | // (this is not guaranteed to fire a selection callback when multiple operations 342 | // modify the dataset, so it is a lot better to first select the item you want 343 | // to have selected, best by overriding this method in your subclass). 344 | setSelectionQuietly(index==0 && count>1? 1 : 0); 345 | setSelection(selection); 346 | } 347 | } 348 | else if(index < selection && selection!=count) // we remove an item above it 349 | setSelectionQuietly(selection - 1); 350 | adapter.remove(adapter.getItem(index)); 351 | if(selection == count) { // we have a temporary item selected 352 | reselectTemporaryItem = true; 353 | setSelectionQuietly(selection - 1); 354 | } 355 | } 356 | } 357 | 358 | /** 359 | * Removes the specified item(s) from the adapter and takes care of handling selection changes. 360 | * Always call this method instead of getAdapter().remove(). 361 | * @param id The id of the item(s) to be removed. All items with this id will be removed. 362 | * @return True if one or more items with the specified id were found and removed, false otherwise. 363 | */ 364 | public boolean removeAdapterItemById(int id) { 365 | PickerSpinnerAdapter adapter = (PickerSpinnerAdapter) getAdapter(); 366 | boolean result = false; 367 | for(int index = adapter.getCount()-1; index >= 0; index--) { 368 | TwinTextItem item = adapter.getItem(index); 369 | if(item.getId() == id) { 370 | removeAdapterItemAt(index); 371 | result = true; 372 | } 373 | } 374 | return result; 375 | } 376 | 377 | /** 378 | * Gets the default list of items to be inflated into the Spinner, will be called once on 379 | * initializing the Spinner. Should use lazy initialization in inherited classes. 380 | * @return The List of Objects whose toString() method will be called for the items, or null. 381 | */ 382 | public abstract List getSpinnerItems(); 383 | 384 | /** 385 | * Gets the CharSequence to be shown as footer in the drop down menu. 386 | * @return The footer, or null to disable showing it. 387 | */ 388 | public abstract CharSequence getFooter(); 389 | 390 | /** 391 | * Built-in listener for clicks on the footer. Note that the footer will not replace the 392 | * selection and you still need a separate OnItemSelectedListener. 393 | */ 394 | public abstract void onFooterClick(); 395 | 396 | /** 397 | * Called to restore a previously saved temporary selection. The given codeString has been saved 398 | * using the toString() method on the TwinTextItem. This method should ideally only call 399 | * {@link #selectTemporary(TwinTextItem)} with a new TwinTextItem parsed from the codeString. 400 | * @param codeString The raw String saved from the item's toString() method. 401 | */ 402 | protected abstract void restoreTemporarySelection(String codeString); 403 | 404 | /** 405 | * 406 | */ 407 | protected ArrayList getItemsFromXml(@XmlRes int xmlResource) 408 | throws XmlPullParserException, IOException { 409 | final Resources res = getResources(); 410 | XmlResourceParser parser = res.getXml(xmlResource); 411 | ArrayList items = new ArrayList<>(); 412 | 413 | int eventType; 414 | while((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) { 415 | if(eventType == XmlPullParser.START_TAG) { 416 | // call our subclass to parse the correct item 417 | TwinTextItem item = parseItemFromXmlTag(parser); 418 | if(item != null) 419 | items.add(item); 420 | } 421 | } 422 | 423 | return items; 424 | } 425 | 426 | /** 427 | * Override this method in your spinner, returning your specific item parsed from the given xml parser at the current tag. 428 | * Do not call parser.next() in here! 429 | */ 430 | protected @Nullable TwinTextItem parseItemFromXmlTag(@NonNull XmlResourceParser parser) { 431 | return null; 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /lib/src/main/java/com/simplicityapks/reminderdatepicker/lib/PickerSpinnerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.simplicityapks.reminderdatepicker.lib; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.os.Build; 6 | import android.support.annotation.LayoutRes; 7 | import android.support.annotation.Nullable; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.ArrayAdapter; 12 | import android.widget.TextView; 13 | 14 | import java.util.List; 15 | 16 | /** 17 | * Serves as Adapter for all PickerSpinner Views and deals with an extra footer and its layout, the 18 | * option to add temporary selections and correctly setting the view for the TwinTextItems. 19 | * 20 | * This is the layout of the items and indexes: 21 | * 22 | * INDEX: | ITEM returned by getView() or getItem(): 23 | * 0 | item1 24 | * 1 | item2 25 | * 2 | item3 26 | * ... ... 27 | * getCount()-1 | footer, if available (else last item) 28 | * getCount() | temporarySelection, if available (else null) 29 | */ 30 | public class PickerSpinnerAdapter extends ArrayAdapter { 31 | 32 | // IDs for both TextViews: 33 | private static final int PRIMARY_TEXT_ID = android.R.id.text1; 34 | private static final int SECONDARY_TEXT_ID = android.R.id.text2; 35 | 36 | @LayoutRes 37 | private int itemResource = R.layout.twin_text_item; 38 | 39 | // Due to only having a light spinner dropdown background even on a dark app theme, we need a 40 | // different resource with inverse text colors on pre Lollipop devices. 41 | private final boolean useAlternativeResources = Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB && isActivityUsingDarkTheme(); 42 | 43 | @LayoutRes 44 | private int dropDownResource = useAlternativeResources? R.layout.twin_text_dropdown_item_dark : R.layout.twin_text_dropdown_item; 45 | 46 | /** 47 | * Resource for the last item in the Spinner, which will be inflated at the last position in dropdown/dialog. 48 | * Set to 0 for use of normal dropDownResource 49 | */ 50 | @LayoutRes 51 | private int footerResource = useAlternativeResources? R.layout.twin_text_footer_dark : R.layout.twin_text_footer; 52 | 53 | /** 54 | * Temporary item which is selected immediately and not shown in the dropdown menu or dialog. 55 | * That is why it does not increase getCount(). 56 | */ 57 | private TwinTextItem temporarySelection; 58 | 59 | /** 60 | * The last item, set to null to disable 61 | */ 62 | private TwinTextItem footer; 63 | 64 | private final LayoutInflater inflater; 65 | 66 | private boolean showSecodaryTextInView = false; 67 | 68 | /** 69 | * Constructs a new PickerSpinnerAdapter with these params: 70 | * @param context The context needed by any Adapter. 71 | * @param items The TwinTextItems to be shown in layout. 72 | * @param footer The item to be shown as footer, use TwinTextItem.Simple for easy creation. 73 | */ 74 | public PickerSpinnerAdapter(Context context, List items, TwinTextItem footer) { 75 | super(context, R.layout.twin_text_item, items); 76 | this.footer = footer; 77 | this.inflater = LayoutInflater.from(context); 78 | } 79 | 80 | /** 81 | * Constructs a new PickerSpinnerAdapter with these params: 82 | * @param context The context needed by any Adapter. 83 | * @param itemResource The resource to be inflated as layout, should contain two TextViews. 84 | * @param dropDownResource The dropDownResource to be inflated in dropDown. 85 | * @param items The TwinTextItems to be shown in layout. 86 | * @param footerResource The resource to be inflated for the footer. 87 | * @param footer The item to be shown as footer, use TwinTextItem.Simple for easy creation. 88 | */ 89 | public PickerSpinnerAdapter(Context context, @LayoutRes int itemResource, @LayoutRes int dropDownResource, 90 | List items, @LayoutRes int footerResource, TwinTextItem footer) { 91 | super(context, itemResource, items); 92 | this.itemResource = itemResource; 93 | this.dropDownResource = dropDownResource; 94 | this.footerResource = footerResource; 95 | this.footer = footer; 96 | this.inflater = LayoutInflater.from(context); 97 | } 98 | 99 | /** 100 | * {@inheritDoc} 101 | */ 102 | @Override 103 | public View getView(int position, View convertView, ViewGroup parent) { 104 | View view; 105 | if (convertView == null) { 106 | view = inflater.inflate(itemResource, parent, false); 107 | } else { 108 | view = convertView; 109 | } 110 | if(temporarySelection != null && position == getCount()) { 111 | // our inflated view acts as temporaryView: 112 | return setTextsAndCheck(view, temporarySelection, showSecodaryTextInView); 113 | } else { 114 | // we have a normal item, set the texts: 115 | return setTextsAndCheck(view, getItem(position), showSecodaryTextInView); 116 | } 117 | } 118 | 119 | /** 120 | * {@inheritDoc} 121 | */ 122 | @Override 123 | public View getDropDownView(int position, View convertView, ViewGroup parent) { 124 | // depending on the position, use super method or create our own 125 | // we don't need to inflate a footer view if it uses the default resource, the superclass will do it: 126 | if(footer == null || footerResource == 0 || position != getCount()-1) { 127 | // we have a normal item or a footer with same resource 128 | return setTextsAndCheck(inflater.inflate(dropDownResource, parent, false), getItem(position), true); 129 | } else { 130 | // if we want the footer, create it: 131 | return setTextsAndCheck(inflater.inflate(footerResource, parent, false), footer, true); 132 | } 133 | } 134 | 135 | private View setTextsAndCheck(View view, TwinTextItem item, boolean showSecondaryText) { 136 | if (view == null) throw new IllegalArgumentException( 137 | "The resource passed to constructor or setItemResource()/setFooterResource() is invalid"); 138 | final TextView primaryText = (TextView) view.findViewById(PRIMARY_TEXT_ID); 139 | if (primaryText == null) throw new IllegalArgumentException( 140 | "The resource passed to constructor or setItemResource()/setFooterResource() does not " + 141 | "contain a textview with id set to android.R.id.text1" 142 | ); 143 | primaryText.setText(item.getPrimaryText()); 144 | // show a disabled state if the item is disabled 145 | primaryText.setEnabled(item.isEnabled()); 146 | 147 | final TextView secondaryText = (TextView) view.findViewById(SECONDARY_TEXT_ID); 148 | if (secondaryText != null) { 149 | if (showSecondaryText) 150 | // Note that we're including the secondary view in the measure even if no secondary text is there. 151 | // The reason is that the spinner should never change its size when an item is selected, 152 | // which would otherwise be possible when a temporary selection (but no other item) has a secondary text. 153 | secondaryText.setText(item.getSecondaryText()); 154 | else 155 | secondaryText.setVisibility(View.GONE); 156 | } 157 | return view; 158 | } 159 | 160 | /** 161 | * Push an item to be selected, but not shown in the dropdown menu. This is similar to calling 162 | * setText(item.toString()) if a Spinner had such a method. 163 | * @param item The item to select, or null to remove any temporary selection. 164 | */ 165 | public void selectTemporary(TwinTextItem item) { 166 | this.temporarySelection = item; 167 | } 168 | 169 | 170 | /** 171 | * {@inheritDoc} 172 | */ 173 | @Override 174 | public TwinTextItem getItem(int position) { 175 | if(temporarySelection != null && position == getCount()) 176 | return temporarySelection; 177 | else if(footer != null && position == getCount()-1) 178 | return footer; 179 | else 180 | return super.getItem(position); 181 | } 182 | 183 | /** 184 | * Finds a spinner item by its id value (excluding any temporary selection). 185 | * @param id The id of the item to search. 186 | * @return The specified TwinTextItem, or null if no item with the given id was found. 187 | */ 188 | public @Nullable TwinTextItem getItemById(int id) { 189 | for(int index = getCount()-1; index >= 0; index--) { 190 | TwinTextItem item = getItem(index); 191 | if(item.getId() == id) 192 | return item; 193 | } 194 | return null; 195 | } 196 | 197 | /** 198 | * Finds a spinner item's position in the data set by its id value (excluding any temporary selection). 199 | * @param id The id of the item to search. 200 | * @return The position of the specified TwinTextItem, or -1 if no item with the given id was found. 201 | */ 202 | public int getItemPosition(int id) { 203 | for(int index = getCount()-1; index >= 0; index--) { 204 | TwinTextItem item = getItem(index); 205 | if(item.getId() == id) 206 | return index; 207 | } 208 | return -1; 209 | } 210 | 211 | /** 212 | * {@inheritDoc} 213 | */ 214 | @Override 215 | public int getCount() { 216 | // we need one extra item which is not in the array. 217 | return super.getCount() + (footer==null? 0 : 1); 218 | } 219 | 220 | /** 221 | * {@inheritDoc} 222 | */ 223 | @Override 224 | public boolean areAllItemsEnabled() { 225 | return false; 226 | } 227 | 228 | /** 229 | * {@inheritDoc} 230 | */ 231 | @Override 232 | public boolean isEnabled(int position) { 233 | return getItem(position).isEnabled(); 234 | } 235 | 236 | /** 237 | * Checks if the Spinner will show a footer, previously set using setFooter(). 238 | * @return True if there is a footer, false otherwise. 239 | */ 240 | public boolean hasFooter() { 241 | return this.footer != null; 242 | } 243 | 244 | /** 245 | * Sets the text to be shown in the footer. 246 | * @param footer An Object whose toString() will be the footer text, or null to disable the footer. 247 | */ 248 | public void setFooter(TwinTextItem footer) { 249 | this.footer = footer; 250 | } 251 | 252 | /** 253 | * Sets the layout resource to be inflated as footer. It should contain a TextView with id set 254 | * to android.R.id.text1, where the text will be added. 255 | * @param footerResource A valid xml layout resource, or 0 to use dropDownResource instead. 256 | */ 257 | public void setFooterResource(@LayoutRes int footerResource) { 258 | this.footerResource = footerResource; 259 | } 260 | 261 | /** 262 | *

Sets the layout resource to create the view.

263 | * 264 | * @param resource the layout resource defining the view, which should contain two text views. 265 | * @see #getDropDownView(int, android.view.View, android.view.ViewGroup) 266 | */ 267 | public void setItemResource(@LayoutRes int resource) { 268 | this.itemResource = resource; 269 | } 270 | 271 | /** 272 | *

Sets the layout resource to create the drop down views.

273 | * 274 | * @param resource the layout resource defining the drop down views, which should contain two text views. 275 | * @see #getDropDownView(int, android.view.View, android.view.ViewGroup) 276 | */ 277 | public void setDropDownViewResource(@LayoutRes int resource) { 278 | this.dropDownResource = resource; 279 | } 280 | 281 | /** 282 | * Enables showing the secondary text in the view. The dropdown view will always include the secondary text. 283 | * @param enable True to enable showing it, false to disable. 284 | */ 285 | public void setShowSecondaryTextInView(boolean enable) { 286 | if (showSecodaryTextInView != enable) { 287 | showSecodaryTextInView = enable; 288 | notifyDataSetChanged(); 289 | } 290 | } 291 | 292 | /** 293 | * Checks whether showing secondary text in view is enabled for this spinner (defaults to false). 294 | * @return True if this PickerSpinner shows the item's secondary text in the view as well as in 295 | * dropdown, false if only in dropdown. 296 | */ 297 | public boolean isShowingSecondaryTextInView() { 298 | return this.showSecodaryTextInView; 299 | } 300 | 301 | private boolean isActivityUsingDarkTheme() { 302 | TypedArray themeArray = getContext().getTheme().obtainStyledAttributes( 303 | new int[] {android.R.attr.textColorPrimary}); 304 | int textColor = themeArray.getColor(0, 0); 305 | return brightness(textColor) > 0.5f; 306 | } 307 | 308 | /** 309 | * Returns the brightness component of a color int. Taken from android.graphics.Color. 310 | * 311 | * @return A value between 0.0f and 1.0f 312 | */ 313 | private float brightness(int color) { 314 | int r = (color >> 16) & 0xFF; 315 | int g = (color >> 8) & 0xFF; 316 | int b = color & 0xFF; 317 | 318 | int V = Math.max(b, Math.max(r, g)); 319 | return (V / 255.f); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /lib/src/main/java/com/simplicityapks/reminderdatepicker/lib/ReminderDatePicker.java: -------------------------------------------------------------------------------- 1 | package com.simplicityapks.reminderdatepicker.lib; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.res.TypedArray; 6 | import android.os.Build; 7 | import android.os.Parcelable; 8 | import android.support.annotation.Nullable; 9 | import android.util.AttributeSet; 10 | import android.view.Gravity; 11 | import android.view.LayoutInflater; 12 | import android.view.View; 13 | import android.widget.AdapterView; 14 | import android.widget.ImageButton; 15 | import android.widget.LinearLayout; 16 | 17 | import com.fourmob.datetimepicker.date.DatePickerDialog; 18 | import com.sleepbot.datetimepicker.time.TimePickerDialog; 19 | 20 | import java.util.Calendar; 21 | import java.util.GregorianCalendar; 22 | 23 | /** 24 | * A Google Keep like date and time picker for reminders, to be inflated via xml or constructor. 25 | * Holds both DateSpinner and TimeSpinner and takes care of handling selection layout changes. 26 | * 27 | * Refer to the project's github page for official documentation. 28 | */ 29 | public class ReminderDatePicker extends LinearLayout implements AdapterView.OnItemSelectedListener{ 30 | 31 | /** 32 | * Mode for {@link #setFlags(int)}. Base mode, same items as in the Google Keep app. 33 | */ 34 | public static final int MODE_GOOGLE = 0; // 000000 35 | 36 | /** 37 | * Mode for {@link #setFlags(int)}. Include all possible items and show numbers in the time spinner. 38 | */ 39 | public static final int MODE_EVERYTHING = 31; // 011111 40 | 41 | /** 42 | * Flag for {@link #setFlags(int)}. Include a yesterday and last weekday item. 43 | */ 44 | public static final int FLAG_PAST = 1; // 000001 45 | 46 | /** 47 | * Flag for {@link #setFlags(int)}. Include a month item exactly one month from today. 48 | */ 49 | public static final int FLAG_MONTH = 2; // 000010 50 | 51 | /** 52 | * Flag for {@link #setFlags(int)}. Include a noon and late night item in the time spinner. 53 | */ 54 | public static final int FLAG_MORE_TIME = 4; // 000100 55 | 56 | /** 57 | * Flag for {@link #setFlags(int)}. Show numeric time in the time spinner view and in the date 58 | * spinner view when a day within the next week is shown with FLAG_WEEKDAY_NAMES. Note that time 59 | * will always be shown in dropdown. 60 | */ 61 | public static final int FLAG_NUMBERS = 8; // 001000 62 | 63 | /** 64 | * Flag for {@link #setFlags(int)}. Show the weekday name when a date within the next week is 65 | * selected instead of the standard date format. 66 | */ 67 | public static final int FLAG_WEEKDAY_NAMES = 16; // 010000 68 | 69 | /** 70 | * Flag for {@link #setFlags(int)}. Hide the time picker and show a button to show it. 71 | */ 72 | public static final int FLAG_HIDE_TIME = 32; // 100000 73 | 74 | // has FLAG_HIDE_TIME been set? 75 | private boolean shouldHideTime = false; 76 | 77 | private DateSpinner dateSpinner; 78 | private TimeSpinner timeSpinner; 79 | 80 | // This listener doesn't have to be implemented, if it is null just ignore it 81 | private OnDateSelectedListener listener = null; 82 | 83 | // To catch twice selecting the same date: 84 | private Calendar lastSelectedDate = null; 85 | 86 | // To keep track whether we need to selectDefaultDate in onAttachToWindow(): 87 | private boolean shouldSelectDefault = true; 88 | 89 | /** 90 | * Construct a new ReminderDatePicker with the given context's theme but without any flags. 91 | * @param context The Context the view is running in, through which it can access the current theme, resources, etc. 92 | */ 93 | public ReminderDatePicker(Context context) { 94 | this(context, null); 95 | } 96 | 97 | /** 98 | * Construct a new ReminderDatePicker with the given context's theme and the supplied attribute set. 99 | * @param context The Context the view is running in, through which it can access the current theme, resources, etc. 100 | * @param attrs The attributes of the XML tag that is inflating the view. May contain a flags attribute. 101 | */ 102 | public ReminderDatePicker(Context context, AttributeSet attrs) { 103 | super(context, attrs); 104 | init(context, attrs); 105 | } 106 | 107 | /** 108 | * Construct a new ReminderDatePicker with the given context's theme and the supplied attribute set. 109 | * @param context The Context the view is running in, through which it can access the current theme, resources, etc. 110 | * @param attrs The attributes of the XML tag that is inflating the view. May contain a flags attribute. 111 | * @param defStyle The default style to apply to this view. If 0, no style will be applied (beyond 112 | * what is included in the theme). This may either be an attribute resource, whose 113 | * value will be retrieved from the current theme, or an explicit style resource. 114 | */ 115 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 116 | public ReminderDatePicker(Context context, AttributeSet attrs, int defStyle) { 117 | super(context, attrs, defStyle); 118 | init(context, attrs); 119 | // Additional styling work is done here 120 | } 121 | 122 | private void init(Context context, AttributeSet attrs) { 123 | View.inflate(context, R.layout.reminder_date_picker, this); 124 | dateSpinner = (DateSpinner) findViewById(R.id.date_spinner); 125 | dateSpinner.setOnItemSelectedListener(this); 126 | 127 | timeSpinner = (TimeSpinner) findViewById(R.id.time_spinner); 128 | timeSpinner.setOnItemSelectedListener(this); 129 | // check if the parent activity has our dateSelectedListener, automatically enable it: 130 | if(context instanceof OnDateSelectedListener) 131 | setOnDateSelectedListener((OnDateSelectedListener) context); 132 | 133 | // set gravity, for the timeButton when th eTimeSpinner is hidden: 134 | setGravity(Gravity.CENTER_VERTICAL); 135 | 136 | if(attrs != null) { 137 | // get our flags from xml, if set: 138 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ReminderDatePicker); 139 | int flags = a.getInt(R.styleable.ReminderDatePicker_flags, MODE_GOOGLE); 140 | setFlags(flags); 141 | a.recycle(); 142 | } 143 | } 144 | 145 | @Override 146 | protected void onRestoreInstanceState(Parcelable state) { 147 | super.onRestoreInstanceState(state); 148 | if(state != null) 149 | shouldSelectDefault = false; 150 | } 151 | 152 | @Override 153 | protected void onAttachedToWindow() { 154 | super.onAttachedToWindow(); 155 | // we may need to initialize the selected date 156 | if(shouldSelectDefault) 157 | selectDefaultDate(); 158 | } 159 | 160 | /** 161 | * Selects the next best date (and time) after today. 162 | * Requires that the items are in ascending order (and that there is at least one item to select). 163 | */ 164 | private void selectDefaultDate() { 165 | Calendar today = Calendar.getInstance(); 166 | int hour = -1, minute = -1; 167 | 168 | // get the next possible selection 169 | Calendar date = getNextItemDate(today); 170 | // if it is the today item, we need to take a look the time 171 | if(date != null && DateSpinner.compareCalendarDates(date, today) == 0) { 172 | // same as getNextTimeDate for TimeSpinner 173 | final int last = timeSpinner.getLastItemPosition(); 174 | final int searchHour = today.get(Calendar.HOUR_OF_DAY), 175 | searchMinute = today.get(Calendar.MINUTE); 176 | for (int i=0; i<=last; i++) { 177 | final TimeItem time = ((TimeItem) timeSpinner.getItemAtPosition(i)); 178 | if(time.getHour() > searchHour || (time.getHour() == searchHour && time.getMinute() >= searchMinute)) { 179 | hour = time.getHour(); 180 | minute = time.getMinute(); 181 | break; 182 | } 183 | } 184 | 185 | // it may be too late in the evening to select the today item 186 | // or if FLAG_HIDE_TIME has been set, set it to tomorrow morning: 187 | if((hour == -1 && minute == -1) || shouldHideTime) { 188 | Calendar tomorrow = (Calendar) today.clone(); 189 | tomorrow.add(Calendar.DAY_OF_YEAR, 1); 190 | date = getNextItemDate(tomorrow); // if this returns null it'll be set to today below 191 | } 192 | } 193 | if(date == null) { 194 | // it seems this spinner only contains past items, use the last one 195 | date = ((DateItem) dateSpinner.getItemAtPosition(dateSpinner.getLastItemPosition())).getDate(); 196 | } 197 | 198 | if(hour == -1 && minute == -1) { 199 | // the date is not today, just select the earliest possible time 200 | final TimeItem time = ((TimeItem) timeSpinner.getItemAtPosition(0)); 201 | hour = time.getHour(); 202 | minute = time.getMinute(); 203 | } 204 | 205 | // finally, select what we found 206 | date.set(Calendar.HOUR_OF_DAY, hour); 207 | date.set(Calendar.MINUTE, minute); 208 | setSelectedDate(date); 209 | } 210 | 211 | /** 212 | * Gets the next date item's date equal to or later than the given date in the DateSpinner. 213 | * Requires that the items are in ascending order. 214 | * @return A date from the next item in the DateSpinner, or no such date was found. 215 | */ 216 | private @Nullable Calendar getNextItemDate(Calendar searchDate) { 217 | final int last = dateSpinner.getLastItemPosition(); 218 | for (int i=0; i<=last; i++) { 219 | final Calendar date = ((DateItem) dateSpinner.getItemAtPosition(i)).getDate(); 220 | // use the DateSpinner's compare method so hours and minutes are not considered 221 | if(DateSpinner.compareCalendarDates(date, searchDate) >= 0) 222 | return date; 223 | } 224 | // not found 225 | return null; 226 | } 227 | 228 | /** 229 | * Gets the currently selected date (that the Spinners are showing) 230 | * @return The selected date as Calendar, or null if there is none. 231 | */ 232 | public Calendar getSelectedDate() { 233 | Calendar result = dateSpinner.getSelectedDate(); 234 | Calendar time = timeSpinner.getSelectedTime(); 235 | if(result!=null && time!=null) { 236 | result.set(Calendar.HOUR_OF_DAY, time.get(Calendar.HOUR_OF_DAY)); 237 | result.set(Calendar.MINUTE, time.get(Calendar.MINUTE)); 238 | return result; 239 | } 240 | else return null; 241 | } 242 | 243 | /** 244 | * Sets the Spinners' selection as date considering both time and day. 245 | * @param date The date to be selected. 246 | */ 247 | public void setSelectedDate(Calendar date) { 248 | if(date!=null) { 249 | dateSpinner.setSelectedDate(date); 250 | timeSpinner.setSelectedTime(date.get(Calendar.HOUR_OF_DAY), date.get(Calendar.MINUTE)); 251 | // a custom selection has been set, don't select the default date: 252 | shouldSelectDefault = false; 253 | } 254 | } 255 | 256 | /** 257 | * Sets the Spinners' date selection as integers considering only day. 258 | */ 259 | public void setSelectedDate(int year, int month, int day) { 260 | dateSpinner.setSelectedDate(new GregorianCalendar(year, month, day)); 261 | // a custom selection has been set, don't select the default date: 262 | shouldSelectDefault = false; 263 | } 264 | 265 | /** 266 | * Sets the Spinners' time selection as integers considering only time. 267 | */ 268 | public void setSelectedTime(int hour, int minute) { 269 | timeSpinner.setSelectedTime(hour, minute); 270 | // a custom selection has been set, don't select the default date: 271 | shouldSelectDefault = false; 272 | } 273 | 274 | /** 275 | * Implement this interface if you want to be notified whenever the selected date changes. 276 | */ 277 | public void setOnDateSelectedListener(OnDateSelectedListener listener) { 278 | this.listener = listener; 279 | } 280 | 281 | /** 282 | * Gets the default {@link DatePickerDialog} that is shown when the footer in the DateSpinner is clicked. 283 | * @return The dialog, or null if a custom date picker has been set and the default one is thus unused. 284 | */ 285 | public @Nullable DatePickerDialog getDatePickerDialog() { 286 | return dateSpinner.getDatePickerDialog(); 287 | } 288 | 289 | /** 290 | * Gets the default {@link TimePickerDialog} that is shown when the footer in the TimeSpinner is clicked. 291 | * @return The dialog, or null if a custom time picker has been set and the default one is thus unused. 292 | */ 293 | public @Nullable TimePickerDialog getTimePickerDialog() { 294 | return timeSpinner.getTimePickerDialog(); 295 | } 296 | 297 | /** 298 | * Sets a custom listener whose onClick method will be called to create and handle the custom date picker. 299 | * You should call {@link #setSelectedDate(int, int, int)} when the custom picker is finished. 300 | * @param launchPicker An {@link android.view.View.OnClickListener} whose onClick method will be 301 | * called to show the custom date picker, or null to use the default picker. 302 | */ 303 | public void setCustomDatePicker(@Nullable OnClickListener launchPicker) { 304 | dateSpinner.setCustomDatePicker(launchPicker); 305 | } 306 | 307 | /** 308 | * Sets a custom listener whose onClick method will be called to create and handle the custom time picker. 309 | * You should call {@link #setSelectedTime} when the custom picker is finished. 310 | * @param launchPicker An {@link android.view.View.OnClickListener} whose onClick method will be 311 | * called to show the custom time picker, or null to use the default picker. 312 | */ 313 | public void setCustomTimePicker(@Nullable OnClickListener launchPicker) { 314 | timeSpinner.setCustomTimePicker(launchPicker); 315 | } 316 | 317 | /** 318 | * Checks if the time spinner is currently invisible (with {@link #FLAG_HIDE_TIME}), so the user didn't choose a time. 319 | * @return True if the time is not visible, false otherwise. 320 | */ 321 | public boolean isTimeHidden() { 322 | return timeSpinner.getVisibility() == GONE; 323 | } 324 | 325 | /** 326 | * Toggles hiding the Time Spinner and replaces it with a Button. 327 | * @param enable True to hide the Spinner, false to show it. 328 | * @param useDarkTheme True if a white icon shall be used, false for a dark one. 329 | */ 330 | public void setHideTime(boolean enable, final boolean useDarkTheme) { 331 | if(enable && !shouldHideTime) { 332 | // hide the time spinner and show a button to show it instead 333 | timeSpinner.setVisibility(GONE); 334 | ImageButton timeButton = (ImageButton) LayoutInflater.from(getContext()).inflate(R.layout.time_button, null); 335 | timeButton.setImageResource(useDarkTheme ? R.drawable.ic_action_time_dark : R.drawable.ic_action_time_light); 336 | timeButton.setOnClickListener(new OnClickListener() { 337 | @Override 338 | public void onClick(View v) { 339 | setHideTime(false, useDarkTheme); 340 | } 341 | }); 342 | this.addView(timeButton); 343 | } else if(!enable && shouldHideTime) { 344 | timeSpinner.setVisibility(VISIBLE); 345 | this.removeViewAt(2); 346 | } 347 | shouldHideTime = enable; 348 | } 349 | 350 | private boolean isActivityUsingDarkTheme() { 351 | TypedArray themeArray = getContext().getTheme().obtainStyledAttributes( 352 | new int[] {android.R.attr.textColorPrimary}); 353 | int textColor = themeArray.getColor(0, 0); 354 | return brightness(textColor) > 0.5f; 355 | } 356 | 357 | /** 358 | * Returns the brightness component of a color int. Taken from android.graphics.Color. 359 | * 360 | * @return A value between 0.0f and 1.0f 361 | */ 362 | private float brightness(int color) { 363 | int r = (color >> 16) & 0xFF; 364 | int g = (color >> 8) & 0xFF; 365 | int b = color & 0xFF; 366 | 367 | int V = Math.max(b, Math.max(r, g)); 368 | return (V / 255.f); 369 | } 370 | 371 | /** 372 | * Gets the custom DateFormat currently used in the DateSpinner to format Calendar strings. 373 | * If {@link #setDateFormat(java.text.DateFormat)} has not been called yet, it will return null. 374 | * @return The time format, or null if the Spinner is using the default date format. 375 | */ 376 | public java.text.DateFormat getCustomDateFormat() { 377 | return dateSpinner.getCustomDateFormat(); 378 | } 379 | 380 | /** 381 | * Sets the custom date format to use for formatting Calendar objects to displayable strings in the DateSpinner. 382 | * @param dateFormat The new DateFormat, or null to use the default format. 383 | */ 384 | public void setDateFormat(java.text.DateFormat dateFormat) { 385 | dateSpinner.setDateFormat(dateFormat); 386 | } 387 | 388 | /** 389 | * Gets the time format (as java.text.DateFormat) currently used in the TimeSpinner to format Calendar strings. 390 | * Defaults to the short time instance for your locale. 391 | * @return The time format, which will never be null. 392 | */ 393 | public java.text.DateFormat getTimeFormat() { 394 | return timeSpinner.getTimeFormat(); 395 | } 396 | 397 | /** 398 | * Sets the time format to use for formatting Calendar objects to displayable strings in the TimeSpinner. 399 | * @param timeFormat The new time format (as java.text.DateFormat), or null to use the default format. 400 | */ 401 | public void setTimeFormat(java.text.DateFormat timeFormat) { 402 | timeSpinner.setTimeFormat(timeFormat); 403 | } 404 | 405 | 406 | /** 407 | * Sets the minimum allowed date for the DateSpinner. 408 | * Spinner items and dates in the date picker before the given date will get disabled. 409 | * Does not affect the TimeSpinner. 410 | * @param minDate The minimum date, or null to clear the previous min date. 411 | */ 412 | public void setMinDate(@Nullable Calendar minDate) { 413 | dateSpinner.setMinDate(minDate); 414 | } 415 | 416 | /** 417 | * Gets the current minimum allowed date for the DateSpinner. 418 | * @return The minimum date, or null if there is none. 419 | */ 420 | public @Nullable Calendar getMinDate() { 421 | return dateSpinner.getMinDate(); 422 | } 423 | 424 | /** 425 | * Sets the maximum allowed date for the DateSpinner. 426 | * Spinner items and dates in the date picker before the given date will get disabled. 427 | * Does not affect the TimeSpinner. 428 | * @param maxDate The maximum date, or null to clear the previous max date. 429 | */ 430 | public void setMaxDate(@Nullable Calendar maxDate) { 431 | dateSpinner.setMaxDate(maxDate); 432 | } 433 | 434 | /** 435 | * Gets the current maximum allowed date for the DateSpinner. 436 | * @return The maximum date, or null if there is none. 437 | */ 438 | public @Nullable Calendar getMaxDate() { 439 | return dateSpinner.getMaxDate(); 440 | } 441 | 442 | 443 | /** 444 | * Set the flags to use for the picker. 445 | * @param modeOrFlags A mode of ReminderDatePicker.MODE_... or multiple ReminderDatePicker.FLAG_... 446 | * combined with the | operator. 447 | */ 448 | public void setFlags(int modeOrFlags) { 449 | // check each flag and pass it on if needed: 450 | setHideTime((modeOrFlags & FLAG_HIDE_TIME) != 0, isActivityUsingDarkTheme()); 451 | dateSpinner.setFlags(modeOrFlags); 452 | timeSpinner.setFlags(modeOrFlags); 453 | } 454 | 455 | @Override 456 | public void onItemSelected(AdapterView parent, View view, int position, long id) { 457 | // An item has been selected in one of our child spinners, so get the selected Date and call the listeners 458 | if(listener != null) { 459 | // catch selecting same date twice 460 | Calendar date = getSelectedDate(); 461 | if(date != null && !date.equals(lastSelectedDate)) { 462 | listener.onDateSelected(date); 463 | lastSelectedDate = date; 464 | } 465 | } 466 | } 467 | 468 | // unused 469 | @Override 470 | public void onNothingSelected(AdapterView parent) { 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /lib/src/main/java/com/simplicityapks/reminderdatepicker/lib/TimeItem.java: -------------------------------------------------------------------------------- 1 | package com.simplicityapks.reminderdatepicker.lib; 2 | 3 | import java.util.Calendar; 4 | 5 | /** 6 | * Object to be inserted into the ArrayAdapter of the TimeSpinner. The time is saved as well as a label. 7 | */ 8 | public class TimeItem implements TwinTextItem { 9 | 10 | private final String label, digitalTime; 11 | private final int hour, minute, id; 12 | private boolean enabled = true; 13 | 14 | /** 15 | * Constructs a new TimeItem holding the specified time and a label to show primarily. 16 | * @param label The String to return when getPrimaryText() is called, but the first text in brackets is set as secondary text. 17 | * @param time The time to be returned by getTime(), as Calendar. 18 | * @param id The identifier to find this item with. 19 | */ 20 | public TimeItem(String label, Calendar time, int id) { 21 | this(label, time.get(Calendar.HOUR_OF_DAY), time.get(Calendar.MINUTE), id); 22 | } 23 | 24 | /** 25 | * Constructs a new TimeItem holding the specified time and a label to to show primarily. 26 | * @param label The String to return when getPrimaryText() is called, but the first text in brackets is set as secondary text. 27 | * @param hour The hour of the day. 28 | * @param minute The minute of the hour. 29 | * @param id The identifier to find this item with. 30 | */ 31 | public TimeItem(String label, int hour, int minute, int id) { 32 | this.hour = hour; 33 | this.minute = minute; 34 | this.id = id; 35 | 36 | // parse the digital time from the label and set both label and digitalTime: 37 | int timeStart = label.indexOf('('); 38 | int timeEnd = label.indexOf(')'); 39 | if(timeStart>0 || timeEnd>0) { 40 | digitalTime = label.substring(timeStart+1, timeEnd); 41 | this.label = label.substring(0, timeStart) + label.substring(timeEnd+1); 42 | } else { 43 | // something went wrong, assume that label is only the primary text: 44 | digitalTime = null; 45 | this.label = label; 46 | } 47 | } 48 | 49 | /** 50 | * Constructs a new TimeItem holding the specified time and a label to to show primarily. 51 | * @param label The String to return when getPrimaryText() is called. 52 | * @param timeString The String to return when getSecondaryText() is called. 53 | * @param hour The hour of the day. 54 | * @param minute The minute of the hour. 55 | * @param id The identifier to find this item with. 56 | */ 57 | public TimeItem(String label, String timeString, int hour, int minute, int id) { 58 | this.label = label; 59 | this.digitalTime = timeString; 60 | this.hour = hour; 61 | this.minute = minute; 62 | this.id = id; 63 | } 64 | 65 | /** 66 | * Gets the current time set in this TimeItem. 67 | * @return A new Calendar containing the time. 68 | */ 69 | public Calendar getTime() { 70 | Calendar result = Calendar.getInstance(); 71 | result.set(Calendar.HOUR_OF_DAY, hour); 72 | result.set(Calendar.MINUTE, minute); 73 | return result; 74 | } 75 | 76 | /** 77 | * Gets the hour set for this TimeItem. 78 | * @return The hour, as int. 79 | */ 80 | public int getHour() { 81 | return this.hour; 82 | } 83 | 84 | /** 85 | * Gets the minute set for this TimeItem. 86 | * @return The minute, as int. 87 | */ 88 | public int getMinute() { 89 | return this.minute; 90 | } 91 | 92 | /** 93 | * Deeply compares this TimeItem to the specified Object. Returns true if obj is a TimeItem and 94 | * contains the same date (ignoring the label) or is a Calendar and contains the same hour and minute. 95 | * @param obj The Object to compare this to. 96 | * @return true if equal, false otherwise. 97 | */ 98 | @Override 99 | public boolean equals(Object obj) { 100 | int objHour, objMinute; 101 | if(obj instanceof TimeItem) { 102 | TimeItem item = (TimeItem) obj; 103 | objHour = item.getHour(); 104 | objMinute = item.getMinute(); 105 | } 106 | else if(obj instanceof Calendar) { 107 | Calendar cal = (Calendar) obj; 108 | objHour = cal.get(Calendar.HOUR_OF_DAY); 109 | objMinute = cal.get(Calendar.MINUTE); 110 | } 111 | else return false; 112 | return objHour==this.hour && objMinute==this.minute; 113 | } 114 | 115 | @Override 116 | public CharSequence getPrimaryText() { 117 | return label; 118 | } 119 | 120 | @Override 121 | public CharSequence getSecondaryText() { 122 | return digitalTime; 123 | } 124 | 125 | @Override 126 | public int getId() { 127 | return id; 128 | } 129 | 130 | @Override 131 | public boolean isEnabled() { 132 | return enabled; 133 | } 134 | 135 | /** 136 | * Enable or disable this spinner item. 137 | * @param enable true to enable, false to disable this item. 138 | */ 139 | public void setEnabled(boolean enable) { 140 | this.enabled = enable; 141 | } 142 | 143 | /** 144 | * The returned String may be passed to {@link #fromString(String)} to save and recreate this object easily. 145 | * @return The elements of this object separated by \n 146 | */ 147 | @Override 148 | public String toString() { 149 | String sep = "\n"; 150 | return nullToEmpty(label) +sep+ nullToEmpty(digitalTime) +sep+ hour +sep+ minute +sep+ id; 151 | } 152 | 153 | /** 154 | * Constructs a new TimeItem from a String previously gotten from the {@link #toString()} method. 155 | * @param code The string to parse from. 156 | * @return A new TimeItem, or null if there was an error. 157 | */ 158 | public static TimeItem fromString(String code) { 159 | String[] items = code.split("\n"); 160 | if(items.length != 5) return null; 161 | int hour, minute, id; 162 | try { 163 | hour = Integer.parseInt(items[2]); 164 | minute = Integer.parseInt(items[3]); 165 | id = Integer.parseInt(items[4]); 166 | } 167 | catch (NumberFormatException e) { 168 | e.printStackTrace(); 169 | return null; 170 | } 171 | return new TimeItem(emptyToNull(items[0]), emptyToNull(items[1]), hour, minute, id); 172 | } 173 | 174 | /** 175 | * Makes sure s is not null, but the empty string instead. Otherwise just return s. 176 | */ 177 | private static String nullToEmpty(String s) { 178 | return s==null? "" : s; 179 | } 180 | 181 | /** 182 | * Makes sure s is not an empty string, but null instead. Otherwise just return s. 183 | */ 184 | private static String emptyToNull(String s) { 185 | return "".equals(s)? null : s; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /lib/src/main/java/com/simplicityapks/reminderdatepicker/lib/TimeSpinner.java: -------------------------------------------------------------------------------- 1 | package com.simplicityapks.reminderdatepicker.lib; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageManager; 5 | import android.content.res.Resources; 6 | import android.content.res.TypedArray; 7 | import android.content.res.XmlResourceParser; 8 | import android.support.annotation.NonNull; 9 | import android.support.annotation.Nullable; 10 | import android.support.annotation.StringRes; 11 | import android.support.v4.app.FragmentActivity; 12 | import android.support.v4.app.FragmentManager; 13 | import android.text.format.DateFormat; 14 | import android.util.AttributeSet; 15 | import android.util.Log; 16 | import android.view.View; 17 | import android.widget.AdapterView; 18 | 19 | import com.sleepbot.datetimepicker.time.RadialPickerLayout; 20 | import com.sleepbot.datetimepicker.time.TimePickerDialog; 21 | 22 | import java.text.SimpleDateFormat; 23 | import java.util.Calendar; 24 | import java.util.GregorianCalendar; 25 | import java.util.List; 26 | 27 | /** 28 | * The right PickerSpinner of the Google Keep app, to select a time within one day. 29 | */ 30 | public class TimeSpinner extends PickerSpinner implements AdapterView.OnItemSelectedListener { 31 | 32 | public static final String XML_TAG_TIMEITEM = "TimeItem"; 33 | 34 | public static final String XML_ATTR_ABSHOUR = "absHour"; 35 | public static final String XML_ATTR_ABSMINUTE= "absMinute"; 36 | 37 | public static final String XML_ATTR_RELHOUR = "relHour"; 38 | public static final String XML_ATTR_RELMINUTE = "relMinute"; 39 | 40 | /** 41 | * Implement this interface if you want to be notified whenever the selected time changes. 42 | */ 43 | public interface OnTimeSelectedListener { 44 | void onTimeSelected(int hour, int minute); 45 | } 46 | 47 | // These listeners don't have to be implemented, if null just ignore 48 | private OnTimeSelectedListener timeListener = null; 49 | private OnClickListener customTimePicker = null; 50 | 51 | // The default time picker dialog to show when the custom one is null: 52 | private TimePickerDialog timePickerDialog; 53 | private FragmentManager fragmentManager; 54 | 55 | private boolean showMoreTimeItems = false; 56 | 57 | // The time format used to convert Calendars into displayable Strings: 58 | private java.text.DateFormat timeFormat = null; 59 | 60 | private int lastSelectedHour = -1; 61 | private int lastSelectedMinute = -1; 62 | 63 | /** 64 | * Construct a new TimeSpinner with the given context's theme. 65 | * @param context The Context the view is running in, through which it can access the current theme, resources, etc. 66 | */ 67 | public TimeSpinner(Context context){ 68 | this(context, null, 0); 69 | } 70 | 71 | /** 72 | * Construct a new TimeSpinner with the given context's theme and the supplied attribute set. 73 | * @param context The Context the view is running in, through which it can access the current theme, resources, etc. 74 | * @param attrs The attributes of the XML tag that is inflating the view. May contain a flags attribute. 75 | */ 76 | public TimeSpinner(Context context, AttributeSet attrs){ 77 | this(context, attrs, 0); 78 | } 79 | 80 | /** 81 | * Construct a new TimeSpinner with the given context's theme, the supplied attribute set, and default style. 82 | * @param context The Context the view is running in, through which it can access the current theme, resources, etc. 83 | * @param attrs The attributes of the XML tag that is inflating the view. May contain a flags attribute. 84 | * @param defStyle The default style to apply to this view. If 0, no style will be applied (beyond 85 | * what is included in the theme). This may either be an attribute resource, whose 86 | * value will be retrieved from the current theme, or an explicit style resource. 87 | */ 88 | public TimeSpinner(Context context, AttributeSet attrs, int defStyle) { 89 | super(context, attrs, defStyle); 90 | // check if the parent activity has our timeSelectedListener, automatically enable it: 91 | if(context instanceof OnTimeSelectedListener) 92 | setOnTimeSelectedListener((OnTimeSelectedListener) context); 93 | setOnItemSelectedListener(this); 94 | 95 | initTimePickerDialog(context); 96 | 97 | // get the FragmentManager: 98 | try{ 99 | fragmentManager = ((FragmentActivity) context).getSupportFragmentManager(); 100 | } catch (ClassCastException e) { 101 | Log.d(getClass().getSimpleName(), "Can't get fragment manager from context"); 102 | } 103 | 104 | if(attrs != null) { 105 | // get our flags from xml, if set: 106 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ReminderDatePicker); 107 | int flags = a.getInt(R.styleable.ReminderDatePicker_flags, ReminderDatePicker.MODE_GOOGLE); 108 | setFlags(flags); 109 | a.recycle(); 110 | } 111 | } 112 | 113 | private void initTimePickerDialog(Context context) { 114 | final Calendar calendar = Calendar.getInstance(); 115 | // create the dialog to show later: 116 | timePickerDialog = TimePickerDialog.newInstance( 117 | new TimePickerDialog.OnTimeSetListener() { 118 | @Override 119 | public void onTimeSet(RadialPickerLayout radialPickerLayout, int hour, int minute) { 120 | setSelectedTime(hour, minute); 121 | } 122 | }, 123 | calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), 124 | is24HourFormat(getTimeFormat()), hasVibratePermission(context)); 125 | } 126 | 127 | private boolean is24HourFormat(java.text.DateFormat timeFormat) { 128 | String pattern; 129 | try { 130 | pattern = ((SimpleDateFormat) timeFormat).toLocalizedPattern(); 131 | } catch (ClassCastException e) { 132 | // we cannot get the pattern, use the default setting for out context: 133 | return DateFormat.is24HourFormat(getContext()); 134 | } 135 | // if pattern does not contain the 12 hour formats, we return true (regardless of any 'a' (am/pm) modifier) 136 | return !(pattern.contains("h") || pattern.contains("K")); 137 | } 138 | 139 | private boolean hasVibratePermission(Context context) { 140 | final String permission = "android.permission.VIBRATE"; 141 | final int res = context.checkCallingOrSelfPermission(permission); 142 | return (res == PackageManager.PERMISSION_GRANTED); 143 | } 144 | 145 | @Override 146 | public List getSpinnerItems() { 147 | try { 148 | return getItemsFromXml(R.xml.time_items); 149 | } catch (Exception e) { 150 | Log.d("TimeSpinner", "Error parsing time items from xml"); 151 | e.printStackTrace(); 152 | } 153 | return null; 154 | } 155 | 156 | @Override 157 | protected @Nullable TwinTextItem parseItemFromXmlTag(@NonNull XmlResourceParser parser) { 158 | if(!parser.getName().equals(XML_TAG_TIMEITEM)) { 159 | Log.d("TimeSpinner", "Unknown xml tag name: " + parser.getName()); 160 | return null; 161 | } 162 | 163 | // parse the TimeItem, possible values are 164 | String text = null; 165 | @StringRes int textResource = NO_ID, id = NO_ID; 166 | int hour = 0, minute = 0; 167 | for(int i=parser.getAttributeCount()-1; i>=0; i--) { 168 | String attrName = parser.getAttributeName(i); 169 | switch (attrName) { 170 | case XML_ATTR_ID: 171 | id = parser.getIdAttributeResourceValue(NO_ID); 172 | break; 173 | case XML_ATTR_TEXT: 174 | text = parser.getAttributeValue(i); 175 | // try to get a resource value, the string is retrieved below 176 | if(text != null && text.startsWith("@")) 177 | textResource = parser.getAttributeResourceValue(i, NO_ID); 178 | break; 179 | 180 | case XML_ATTR_ABSHOUR: 181 | hour = parser.getAttributeIntValue(i, -1); 182 | break; 183 | case XML_ATTR_ABSMINUTE: 184 | minute = parser.getAttributeIntValue(i, -1); 185 | break; 186 | 187 | case XML_ATTR_RELHOUR: 188 | hour += parser.getAttributeIntValue(i, 0); 189 | break; 190 | case XML_ATTR_RELMINUTE: 191 | minute += parser.getAttributeIntValue(i, 0); 192 | break; 193 | default: 194 | Log.d("TimeSpinner", "Skipping unknown attribute tag parsing xml resource: " 195 | + attrName + ", maybe a typo?"); 196 | } 197 | }// end for attr 198 | 199 | // now construct the time item from the attributes 200 | if(textResource != NO_ID) 201 | text = getResources().getString(textResource); 202 | 203 | // when no text is given, format the date to have at least something to show 204 | if(text == null || text.equals("")) 205 | text = formatTime(hour, minute); 206 | 207 | return new TimeItem(text, formatTime(hour, minute), hour, minute, id); 208 | } 209 | 210 | /** 211 | * Gets the currently selected time (that the Spinner is showing) 212 | * @return The selected time as Calendar, or null if there is none. 213 | */ 214 | public Calendar getSelectedTime() { 215 | final Object selectedItem = getSelectedItem(); 216 | if(!(selectedItem instanceof TimeItem)) 217 | return null; 218 | return ((TimeItem) selectedItem).getTime(); 219 | } 220 | 221 | /** 222 | * Sets the Spinner's selection as time in hour and minute. If the time was not in the possible 223 | * selections, a temporary item is created and passed to selectTemporary(). 224 | * @param hour The hour to be selected. 225 | * @param minute The minute in the hour. 226 | */ 227 | public void setSelectedTime(int hour, int minute) { 228 | final int count = getAdapter().getCount() - 1; 229 | int itemPosition = -1; 230 | for(int i=0; i= 0) 238 | setSelection(itemPosition); 239 | else { 240 | // create a temporary TimeItem to select: 241 | selectTemporary(new TimeItem(formatTime(hour, minute), hour, minute, NO_ID)); 242 | } 243 | } 244 | 245 | private String formatTime(int hour, int minute) { 246 | return getTimeFormat().format(new GregorianCalendar(0,0,0,hour,minute).getTime()); 247 | } 248 | 249 | /** 250 | * Gets the time format (as java.text.DateFormat) currently used to format Calendar strings. 251 | * Defaults to the short time instance for your locale. 252 | * @return The time format, which will never be null. 253 | */ 254 | public java.text.DateFormat getTimeFormat() { 255 | if(timeFormat == null) 256 | timeFormat = java.text.DateFormat.getTimeInstance(java.text.DateFormat.SHORT); 257 | return timeFormat; 258 | } 259 | 260 | /** 261 | * Sets the time format to use for formatting Calendar objects to displayable strings. 262 | * @param timeFormat The new time format (as java.text.DateFormat), or null to use the default format. 263 | */ 264 | public void setTimeFormat(java.text.DateFormat timeFormat) { 265 | this.timeFormat = timeFormat; 266 | // update our pre-built timePickerDialog with the new timeFormat: 267 | initTimePickerDialog(getContext()); 268 | 269 | // save the flags and selection first: 270 | final PickerSpinnerAdapter adapter = ((PickerSpinnerAdapter)getAdapter()); 271 | final boolean moreTimeItems = isShowingMoreTimeItems(); 272 | final boolean numbersInView = adapter.isShowingSecondaryTextInView(); 273 | final Calendar selection = getSelectedTime(); 274 | // we need to restore differently if we have a temporary selection: 275 | final boolean temporarySelected = getSelectedItemPosition() == adapter.getCount(); 276 | 277 | // to rebuild the spinner items, we need to recreate our adapter: 278 | initAdapter(getContext()); 279 | 280 | // force restore flags and selection to the new Adapter: 281 | setShowNumbersInView(numbersInView); 282 | this.showMoreTimeItems = false; 283 | if(temporarySelected) { 284 | // for some reason these calls have to be exactly in this order! 285 | setSelectedTime(selection.get(Calendar.HOUR_OF_DAY), selection.get(Calendar.MINUTE)); 286 | setShowMoreTimeItems(moreTimeItems); 287 | } else { 288 | // this way it works when a date from the array is selected (like the default) 289 | setShowMoreTimeItems(moreTimeItems); 290 | setSelectedTime(selection.get(Calendar.HOUR_OF_DAY), selection.get(Calendar.MINUTE)); 291 | } 292 | } 293 | 294 | /** 295 | * Implement this interface if you want to be notified whenever the selected time changes. 296 | */ 297 | public void setOnTimeSelectedListener(OnTimeSelectedListener listener) { 298 | this.timeListener = listener; 299 | } 300 | 301 | /** 302 | * Gets the default {@link TimePickerDialog} that is shown when the footer is clicked. 303 | * @return The dialog, or null if a custom time picker has been set and the default one is thus unused. 304 | */ 305 | public @Nullable TimePickerDialog getTimePickerDialog() { 306 | if(customTimePicker != null) 307 | return null; 308 | return timePickerDialog; 309 | } 310 | 311 | /** 312 | * Sets a custom listener whose onClick method will be called to create and handle the custom time picker. 313 | * You should call {@link #setSelectedTime} when the custom picker is finished. 314 | * @param launchPicker An {@link android.view.View.OnClickListener} whose onClick method will be 315 | * called to show the custom time picker, or null to use the default picker. 316 | */ 317 | public void setCustomTimePicker(@Nullable OnClickListener launchPicker) { 318 | this.customTimePicker = launchPicker; 319 | } 320 | 321 | /** 322 | * Checks whether the spinner is showing all time items, including noon and late night. 323 | * @return True if FLAG_MORE_TIME has been set or {@link #setShowMoreTimeItems(boolean)} was called, false otherwise. 324 | */ 325 | public boolean isShowingMoreTimeItems() { 326 | return this.showMoreTimeItems; 327 | } 328 | 329 | /** 330 | * Toggles showing more time items. If enabled, a noon and a late night time item are shown. 331 | * @param enable True to enable, false to disable more time items. 332 | */ 333 | public void setShowMoreTimeItems(boolean enable) { 334 | if(enable && !showMoreTimeItems) { 335 | // create the noon and late night item: 336 | final Resources res = getResources(); 337 | // switch the afternoon item to 2pm: 338 | insertAdapterItem(new TimeItem(res.getString(R.string.time_afternoon_2), formatTime(14, 0), 14, 0, R.id.time_afternoon_2), 2); 339 | removeAdapterItemById(R.id.time_afternoon); 340 | // noon item: 341 | insertAdapterItem(new TimeItem(res.getString(R.string.time_noon), formatTime(12, 0), 12, 0, R.id.time_noon), 1); 342 | // late night item: 343 | addAdapterItem(new TimeItem(res.getString(R.string.time_late_night), formatTime(23, 0), 23, 0, R.id.time_late_night)); 344 | } 345 | else if(!enable && showMoreTimeItems) { 346 | // switch back the afternoon item: 347 | insertAdapterItem(new TimeItem(getResources().getString(R.string.time_afternoon), formatTime(13, 0), 13, 0, R.id.time_afternoon), 3); 348 | removeAdapterItemById(R.id.time_afternoon_2); 349 | removeAdapterItemById(R.id.time_noon); 350 | removeAdapterItemById(R.id.time_late_night); 351 | } 352 | showMoreTimeItems = enable; 353 | } 354 | 355 | /** 356 | * Toggles showing numeric time in the view. Note that time will always be shown in dropdown. 357 | * @param enable True to enable, false to disable numeric mode. 358 | */ 359 | public void setShowNumbersInView(boolean enable) { 360 | PickerSpinnerAdapter adapter = (PickerSpinnerAdapter) getAdapter(); 361 | // workaround for now. 362 | if(enable != adapter.isShowingSecondaryTextInView() && adapter.getCount() == getSelectedItemPosition()) 363 | setSelection(0); 364 | adapter.setShowSecondaryTextInView(enable); 365 | } 366 | 367 | /** 368 | * Set the flags to use for this time spinner. 369 | * @param modeOrFlags A mode of ReminderDatePicker.MODE_... or multiple ReminderDatePicker.FLAG_... 370 | * combined with the | operator. 371 | */ 372 | public void setFlags(int modeOrFlags) { 373 | setShowMoreTimeItems((modeOrFlags & ReminderDatePicker.FLAG_MORE_TIME) != 0); 374 | setShowNumbersInView((modeOrFlags & ReminderDatePicker.FLAG_NUMBERS) != 0); 375 | } 376 | 377 | /** 378 | * {@inheritDoc} 379 | */ 380 | @Override 381 | public void removeAdapterItemAt(int index) { 382 | if(index == getSelectedItemPosition()) { 383 | Calendar time = getSelectedTime(); 384 | selectTemporary(new TimeItem(formatTime(time.get(Calendar.HOUR_OF_DAY), time.get(Calendar.MINUTE)), time, NO_ID)); 385 | } 386 | super.removeAdapterItemAt(index); 387 | } 388 | 389 | @Override 390 | public CharSequence getFooter() { 391 | return getResources().getString(R.string.spinner_time_footer); 392 | } 393 | 394 | @Override 395 | public void onFooterClick() { 396 | if (customTimePicker == null) { 397 | // update the selected time in the dialog 398 | final Calendar time = getSelectedTime(); 399 | timePickerDialog.setStartTime(time.get(Calendar.HOUR_OF_DAY), time.get(Calendar.MINUTE)); 400 | timePickerDialog.show(fragmentManager, "TimePickerDialog"); 401 | } else { 402 | customTimePicker.onClick(this); 403 | } 404 | } 405 | 406 | @Override 407 | protected void restoreTemporarySelection(String codeString) { 408 | selectTemporary(TimeItem.fromString(codeString)); 409 | } 410 | 411 | @Override 412 | public void onItemSelected(AdapterView parent, View view, int position, long id) { 413 | if(timeListener != null) { 414 | Object selectedObj = getSelectedItem(); 415 | if(selectedObj instanceof TimeItem) { 416 | TimeItem selected = (TimeItem) selectedObj; 417 | int hour = selected.getHour(); 418 | int minute = selected.getMinute(); 419 | if(hour != lastSelectedHour || minute != lastSelectedMinute) { 420 | timeListener.onTimeSelected(hour, minute); 421 | lastSelectedHour = hour; 422 | lastSelectedMinute = minute; 423 | } 424 | } 425 | } 426 | } 427 | 428 | // unused 429 | @Override 430 | public void onNothingSelected(AdapterView parent) { 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /lib/src/main/java/com/simplicityapks/reminderdatepicker/lib/TwinTextItem.java: -------------------------------------------------------------------------------- 1 | package com.simplicityapks.reminderdatepicker.lib; 2 | 3 | /** 4 | * Base interface for list items used by a PickerSpinnerAdapter. Enables having both primary and secondary text. 5 | */ 6 | public interface TwinTextItem { 7 | 8 | /** 9 | * Base class for fast creating of TwinTextItems. 10 | */ 11 | class Simple implements TwinTextItem{ 12 | private final CharSequence primary, secondary; 13 | private final int id; 14 | 15 | /** 16 | * Constructs a new simple TwinTextItem. 17 | * @param primaryText The text to be shown primarily. 18 | * @param secondaryText The text to be shown secondarily. 19 | */ 20 | public Simple(CharSequence primaryText, CharSequence secondaryText) { 21 | this(primaryText, secondaryText, 0); 22 | } 23 | 24 | /** 25 | * Constructs a new simple TwinTextItem. 26 | * @param primaryText The text to be shown primarily. 27 | * @param secondaryText The text to be shown secondarily. 28 | * @param itemId The id value to find this item with. 29 | */ 30 | public Simple(CharSequence primaryText, CharSequence secondaryText, int itemId) { 31 | primary = primaryText; 32 | secondary = secondaryText; 33 | id = itemId; 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | @Override 40 | public CharSequence getPrimaryText() { 41 | return primary; 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | @Override 48 | public CharSequence getSecondaryText() { 49 | return secondary; 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | @Override 56 | public int getId() { 57 | return id; 58 | } 59 | 60 | /** 61 | * {@inheritDoc} 62 | */ 63 | @Override 64 | public boolean isEnabled() { 65 | return true; 66 | } 67 | } 68 | 69 | /** 70 | * Returns the identifier with which the item can be found and removed from the adapter. 71 | * Does not have to be unique for each item if you don't use runtime item modifications. 72 | */ 73 | int getId(); 74 | 75 | /** 76 | * Gets the text to be shown primarily. 77 | * @return The text (probably as String). 78 | */ 79 | CharSequence getPrimaryText(); 80 | 81 | /** 82 | * Gets the text to be shown secondarily. 83 | * @return The text (probably as String). 84 | */ 85 | CharSequence getSecondaryText(); 86 | 87 | /** 88 | * Whether this item is enabled. Return false to show this spinner item in a disabled state with grey text. 89 | * @return true to enable, false to disable this item. 90 | */ 91 | boolean isEnabled(); 92 | } 93 | -------------------------------------------------------------------------------- /lib/src/main/project.properties: -------------------------------------------------------------------------------- 1 | android.library=true 2 | 3 | # Make sure you have both android.support.v7.appcompat and com.fourmob.datetimepicker setup correctly 4 | # and referenced as android.library.reference.x=../path/to/lib here! -------------------------------------------------------------------------------- /lib/src/main/res/drawable-hdpi/ic_action_time_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimplicityApks/ReminderDatePicker/7596fbac77a5d26f687fec11758935a2b7db156f/lib/src/main/res/drawable-hdpi/ic_action_time_dark.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-hdpi/ic_action_time_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimplicityApks/ReminderDatePicker/7596fbac77a5d26f687fec11758935a2b7db156f/lib/src/main/res/drawable-hdpi/ic_action_time_light.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-mdpi/ic_action_time_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimplicityApks/ReminderDatePicker/7596fbac77a5d26f687fec11758935a2b7db156f/lib/src/main/res/drawable-mdpi/ic_action_time_dark.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-mdpi/ic_action_time_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimplicityApks/ReminderDatePicker/7596fbac77a5d26f687fec11758935a2b7db156f/lib/src/main/res/drawable-mdpi/ic_action_time_light.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-xhdpi/ic_action_time_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimplicityApks/ReminderDatePicker/7596fbac77a5d26f687fec11758935a2b7db156f/lib/src/main/res/drawable-xhdpi/ic_action_time_dark.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-xhdpi/ic_action_time_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimplicityApks/ReminderDatePicker/7596fbac77a5d26f687fec11758935a2b7db156f/lib/src/main/res/drawable-xhdpi/ic_action_time_light.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-xxhdpi/ic_action_time_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimplicityApks/ReminderDatePicker/7596fbac77a5d26f687fec11758935a2b7db156f/lib/src/main/res/drawable-xxhdpi/ic_action_time_dark.png -------------------------------------------------------------------------------- /lib/src/main/res/drawable-xxhdpi/ic_action_time_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimplicityApks/ReminderDatePicker/7596fbac77a5d26f687fec11758935a2b7db156f/lib/src/main/res/drawable-xxhdpi/ic_action_time_light.png -------------------------------------------------------------------------------- /lib/src/main/res/layout/reminder_date_picker.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /lib/src/main/res/layout/time_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /lib/src/main/res/layout/twin_text_dropdown_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 25 | 26 | 38 | -------------------------------------------------------------------------------- /lib/src/main/res/layout/twin_text_dropdown_item_dark.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 25 | 27 | 39 | -------------------------------------------------------------------------------- /lib/src/main/res/layout/twin_text_footer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 25 | 26 | 38 | -------------------------------------------------------------------------------- /lib/src/main/res/layout/twin_text_footer_dark.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 25 | 26 | 38 | -------------------------------------------------------------------------------- /lib/src/main/res/layout/twin_text_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | 17 | 18 | 28 | -------------------------------------------------------------------------------- /lib/src/main/res/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Datum auswählen… 3 | Uhrzeit auswählen… 4 | Zeit wählen 5 | 6 | 7 | Letzten %1$s 8 | Letzten %1$s 9 | Gestern 10 | Heute 11 | Morgen 12 | Nächsten %1$s 13 | Nächsten %1$s 14 | 15 | 16 | Morgens 17 | Mittags 18 | Nachmittags 19 | Nachmittags 20 | Spätnachmittags 21 | Abends 22 | Nachts 23 | 24 | -------------------------------------------------------------------------------- /lib/src/main/res/values-el/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Διαλέξτε κάποια ημερομηνία… 3 | Διαλέξτε κάποια ώρα… 4 | Επιλογή ώρας 5 | 6 | 7 | Τελευταία %1$s 8 | Τελευταία %1$s 9 | Χθες 10 | Σήμερα 11 | Αύριο 12 | Επόμενη %1$s 13 | Επόμενη %1$s 14 | 15 | 16 | Πρωί 17 | Μεσημέρι 18 | Απόγευμα 19 | Απόγευμα 20 | Βράδυ 21 | Νύχτα (20:00) 22 | Αργά τη νύχτα 23 | -------------------------------------------------------------------------------- /lib/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Elegir una fecha… 3 | Elegir un horario… 4 | Elegir horario 5 | 6 | 7 | %1$s anterior 8 | %1$s anterior 9 | Ayer 10 | Hoy 11 | Mañana 12 | Siguiente %1$s 13 | Siguiente %1$s 14 | 15 | 16 | Por la mañana 17 | Al mediodía 18 | Por la tarde 19 | Por la tarde 20 | Al anochecer 21 | Por la noche 22 | Por la noche 23 | 24 | -------------------------------------------------------------------------------- /lib/src/main/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Sélectionner une date… 3 | Choisir l\'heure… 4 | Sélectionner l\'heure 5 | 6 | 7 | %1$s dernier 8 | %1$s dernier 9 | Hier 10 | Aujourd\'hui 11 | Demain 12 | %1$s prochain 13 | %1$s prochain 14 | 15 | 16 | Matin 17 | Midi 18 | Après-midi 19 | Après-midi 20 | Début de soirée 21 | Soir 22 | Fin de soirée 23 | 24 | -------------------------------------------------------------------------------- /lib/src/main/res/values-it/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Seleziona una data… 3 | Seleziona l\'ora… 4 | Seleziona l\'ora 5 | 6 | 7 | Ultimo %1$s 8 | Ultimo %1$s 9 | Ieri 10 | Oggi 11 | Domani 12 | Prossimo %1$s 13 | Prossimo %1$s 14 | 15 | 16 | Mattina 17 | Mezzogiorno 18 | Pomeriggio 19 | Pomeriggio 20 | Sera 21 | Notte 22 | Tarda notte 23 | 24 | -------------------------------------------------------------------------------- /lib/src/main/res/values-pt-rBR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Escolha uma data… 3 | Escolha um horário… 4 | Escolha um horário 5 | 6 | 7 | Última %1$s 8 | Último %1$s 9 | Ontem 10 | Hoje 11 | Amanhã 12 | Próxima %1$s 13 | Próximo %1$s 14 | 15 | 16 | Manhã 17 | Meio-dia 18 | Tarde 19 | Tarde 20 | Anoitecer 21 | Noite 22 | Noite 23 | 24 | -------------------------------------------------------------------------------- /lib/src/main/res/values-tr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Bir tarih seçin… 3 | Bir saat seçin… 4 | Saat seç 5 | 6 | 7 | Son %1$s 8 | Son %1$s 9 | Dün 10 | Bugün 11 | Yarın 12 | Sonraki %1$s 13 | Sonraki %1$s 14 | 15 | 16 | Sabah 17 | Öğlen 18 | Öğleden sonra 19 | Öğleden sonra 20 | Akşam 21 | Gece 22 | Gece yarısı 23 | -------------------------------------------------------------------------------- /lib/src/main/res/values-v11/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /lib/src/main/res/values-v14/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /lib/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /lib/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ffb2b2b2 4 | #48b7b7b7 5 | 6 | -------------------------------------------------------------------------------- /lib/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /lib/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Pick a date… 3 | Pick a time… 4 | Select time 5 | 6 | 7 | Last %1$s 8 | Last %1$s 9 | Yesterday 10 | Today 11 | Tomorrow 12 | Next %1$s 13 | Next %1$s 14 | %1$s 15 | 16 | 17 | Morning 18 | Noon 19 | Afternoon 20 | Afternoon 21 | Evening 22 | Night 23 | Late night 24 | 25 | -------------------------------------------------------------------------------- /lib/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 14 | 15 | 17 | 18 | 23 | 24 | 27 | -------------------------------------------------------------------------------- /lib/src/main/res/xml/date_items.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | -------------------------------------------------------------------------------- /lib/src/main/res/xml/time_items.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | 19 | -------------------------------------------------------------------------------- /maven_push.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven' 2 | apply plugin: 'signing' 3 | 4 | def sonatypeRepositoryUrl 5 | if (isReleaseBuild()) { 6 | println 'RELEASE BUILD' 7 | sonatypeRepositoryUrl = hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 8 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 9 | } else { 10 | println 'DEBUG BUILD' 11 | sonatypeRepositoryUrl = hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 12 | : "https://oss.sonatype.org/content/repositories/snapshots/" 13 | } 14 | 15 | def getRepositoryUsername() { 16 | return hasProperty('nexusUsername') ? nexusUsername : "" 17 | } 18 | 19 | def getRepositoryPassword() { 20 | return hasProperty('nexusPassword') ? nexusPassword : "" 21 | } 22 | 23 | afterEvaluate { project -> 24 | uploadArchives { 25 | repositories { 26 | mavenDeployer { 27 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 28 | 29 | pom.artifactId = POM_ARTIFACT_ID 30 | 31 | repository(url: sonatypeRepositoryUrl) { 32 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 33 | } 34 | 35 | pom.project { 36 | name POM_NAME 37 | packaging POM_PACKAGING 38 | description POM_DESCRIPTION 39 | url POM_URL 40 | 41 | scm { 42 | url POM_SCM_URL 43 | connection POM_SCM_CONNECTION 44 | developerConnection POM_SCM_DEV_CONNECTION 45 | } 46 | 47 | licenses { 48 | license { 49 | name POM_LICENCE_NAME 50 | url POM_LICENCE_URL 51 | distribution POM_LICENCE_DIST 52 | } 53 | } 54 | 55 | developers { 56 | developer { 57 | id POM_DEVELOPER_ID 58 | name POM_DEVELOPER_NAME 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | signing { 67 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 68 | sign configurations.archives 69 | } 70 | 71 | task androidJavadocs(type: Javadoc) { 72 | source = android.sourceSets.main.java.sourceFiles 73 | } 74 | 75 | task androidJavadocsJar(type: Jar) { 76 | classifier = 'javadoc' 77 | //basename = artifact_id 78 | from androidJavadocs.destinationDir 79 | } 80 | 81 | task androidSourcesJar(type: Jar) { 82 | classifier = 'sources' 83 | //basename = artifact_id 84 | from android.sourceSets.main.java.sourceFiles 85 | } 86 | 87 | artifacts { 88 | //archives packageReleaseJar 89 | archives androidSourcesJar 90 | archives androidJavadocsJar 91 | } 92 | } -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | buildToolsVersion '27.0.0' 6 | 7 | defaultConfig { 8 | applicationId 'com.simplicityapks.reminderdatepicker.sample' 9 | minSdkVersion 14 10 | targetSdkVersion 26 11 | versionCode 3 12 | versionName '1.2.0' 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled true 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | productFlavors { 21 | } 22 | } 23 | 24 | dependencies { 25 | compile fileTree(dir: 'libs', include: ['*.jar']) 26 | compile 'com.android.support:appcompat-v7:26.+' 27 | compile project(':lib') 28 | } 29 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/patrick/Dokumente/AndroidStudioDevelopment/android-studio/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /sample/src/main/java/com/simplicityapks/reminderdatepicker/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.simplicityapks.reminderdatepicker.sample; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import android.os.Bundle; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | import android.widget.CheckBox; 10 | import android.widget.CompoundButton; 11 | import android.widget.Toast; 12 | 13 | import com.simplicityapks.reminderdatepicker.lib.OnDateSelectedListener; 14 | import com.simplicityapks.reminderdatepicker.lib.ReminderDatePicker; 15 | 16 | import java.text.DateFormat; 17 | import java.util.Calendar; 18 | 19 | public class MainActivity extends AppCompatActivity implements CompoundButton.OnCheckedChangeListener{ 20 | 21 | private String FLAG_DARK_THEME = "flag_dark_theme"; 22 | private boolean useDarkTheme = false; 23 | 24 | private ReminderDatePicker datePicker; 25 | 26 | private CheckBox cbPast, cbMonth, cbMoreTime, cbNumbers, cbWeekdayNames, cbHideTime; 27 | 28 | @Override 29 | protected void onCreate(Bundle savedInstanceState) { 30 | // do we want to change the theme to dark? 31 | useDarkTheme = getIntent().getBooleanExtra(FLAG_DARK_THEME, false); 32 | if(useDarkTheme) setTheme(R.style.Theme_AppCompat); 33 | 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_main); 36 | datePicker = (ReminderDatePicker) findViewById(R.id.date_picker); 37 | 38 | // setup listener for a date change: 39 | datePicker.setOnDateSelectedListener(new OnDateSelectedListener() { 40 | @Override 41 | public void onDateSelected(Calendar date) { 42 | Toast.makeText(MainActivity.this, "Selected date: "+ getDateFormat().format(date.getTime()), Toast.LENGTH_SHORT).show(); 43 | } 44 | }); 45 | 46 | cbPast = (CheckBox) findViewById(R.id.cb_past); 47 | cbMonth = (CheckBox) findViewById(R.id.cb_month); 48 | cbMoreTime = (CheckBox) findViewById(R.id.cb_more_time); 49 | cbNumbers = (CheckBox) findViewById(R.id.cb_numbers); 50 | cbWeekdayNames = (CheckBox) findViewById(R.id.cb_weekday_names); 51 | cbHideTime = (CheckBox) findViewById(R.id.cb_hide_time); 52 | 53 | // setup flag change listeners: 54 | cbPast.setOnCheckedChangeListener(this); 55 | cbMonth.setOnCheckedChangeListener(this); 56 | cbMoreTime.setOnCheckedChangeListener(this); 57 | cbNumbers.setOnCheckedChangeListener(this); 58 | cbWeekdayNames.setOnCheckedChangeListener(this); 59 | cbHideTime.setOnCheckedChangeListener(this); 60 | } 61 | 62 | private java.text.DateFormat savedFormat; 63 | public java.text.DateFormat getDateFormat() { 64 | if(savedFormat == null) 65 | savedFormat = DateFormat.getDateTimeInstance(); 66 | return savedFormat; 67 | } 68 | 69 | @Override 70 | public boolean onCreateOptionsMenu(Menu menu) { 71 | // Inflate the menu; this adds items to the action bar if it is present. 72 | getMenuInflater().inflate(R.menu.main, menu); 73 | return true; 74 | } 75 | 76 | @Override 77 | public boolean onOptionsItemSelected(MenuItem item) { 78 | // Handle action bar item clicks here. The action bar will 79 | // automatically handle clicks on the Home/Up button, so long 80 | // as you specify a parent activity in AndroidManifest.xml. 81 | switch (item.getItemId()) { 82 | case R.id.action_switch_theme: 83 | Intent restart = new Intent(this, MainActivity.class); 84 | // add boolean extra to switch theme: 85 | restart.putExtra(FLAG_DARK_THEME, !useDarkTheme); 86 | // kill current activity and start again 87 | overridePendingTransition(0, 0); 88 | restart.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); 89 | finish(); 90 | overridePendingTransition(0, 0); 91 | startActivity(restart); 92 | break; 93 | case R.id.action_view_source: 94 | Intent viewSource = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.uri_github_source))); 95 | startActivity(viewSource); 96 | break; 97 | } 98 | return super.onOptionsItemSelected(item); 99 | } 100 | 101 | private int getCheckedFlags() { 102 | return (cbPast.isChecked()? ReminderDatePicker.FLAG_PAST : 0) | 103 | (cbMonth.isChecked()? ReminderDatePicker.FLAG_MONTH : 0) | 104 | (cbMoreTime.isChecked()? ReminderDatePicker.FLAG_MORE_TIME : 0) | 105 | (cbNumbers.isChecked()? ReminderDatePicker.FLAG_NUMBERS : 0) | 106 | (cbWeekdayNames.isChecked()? ReminderDatePicker.FLAG_WEEKDAY_NAMES : 0) | 107 | (cbHideTime.isChecked()? ReminderDatePicker.FLAG_HIDE_TIME : 0); 108 | } 109 | 110 | @Override 111 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 112 | datePicker.setFlags(getCheckedFlags()); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimplicityApks/ReminderDatePicker/7596fbac77a5d26f687fec11758935a2b7db156f/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimplicityApks/ReminderDatePicker/7596fbac77a5d26f687fec11758935a2b7db156f/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimplicityApks/ReminderDatePicker/7596fbac77a5d26f687fec11758935a2b7db156f/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimplicityApks/ReminderDatePicker/7596fbac77a5d26f687fec11758935a2b7db156f/sample/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimplicityApks/ReminderDatePicker/7596fbac77a5d26f687fec11758935a2b7db156f/sample/src/main/res/drawable-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 19 | 20 | 26 | 27 | 35 | 36 | 42 | 43 | 49 | 50 | 56 | 57 | 63 | 64 | 70 | 71 | 77 | 78 | -------------------------------------------------------------------------------- /sample/src/main/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 5 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /sample/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ReminderDatePicker Sample 5 | Switch theme 6 | Get source code 7 | https://github.com/SimplicityApks/ReminderDatePicker 8 | 9 | Select Flags 10 | 11 | 12 | FLAG_PAST 13 | FLAG_MONTH 14 | FLAG_MORE_TIME 15 | FLAG_NUMBERS 16 | FLAG_WEEKDAY_NAMES 17 | FLAG_HIDE_TIME 18 | 19 | 20 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 15 | 16 | 25 | 26 | 29 | 30 | 36 | 37 | 41 | 42 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':lib', ':sample' 2 | --------------------------------------------------------------------------------