├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── gradlew-validate.yaml │ ├── library-lint.yaml │ └── library-test.yaml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── api │ └── library.api ├── build.gradle.kts ├── detekt.yml ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── de │ │ │ └── Maxr1998 │ │ │ └── modernpreferences │ │ │ ├── Preferences.kt │ │ │ ├── PreferencesAdapter.kt │ │ │ ├── helpers │ │ │ ├── DependencyManager.kt │ │ │ ├── PreferenceMarker.kt │ │ │ ├── PreferencesDsl.kt │ │ │ └── Utils.kt │ │ │ ├── preferences │ │ │ ├── AccentButtonPreference.kt │ │ │ ├── Badge.kt │ │ │ ├── CategoryHeader.kt │ │ │ ├── CheckBoxPreference.kt │ │ │ ├── CollapsePreference.kt │ │ │ ├── DialogPreference.kt │ │ │ ├── EditTextPreference.kt │ │ │ ├── ExpandableTextPreference.kt │ │ │ ├── ImagePreference.kt │ │ │ ├── SeekBarPreference.kt │ │ │ ├── StatefulPreference.kt │ │ │ ├── SwitchPreference.kt │ │ │ ├── TwoStatePreference.kt │ │ │ └── choice │ │ │ │ ├── AbstractChoiceDialogPreference.kt │ │ │ │ ├── AbstractSingleChoiceDialogPreference.kt │ │ │ │ ├── MultiChoiceDialogPreference.kt │ │ │ │ ├── OnItemClickListener.kt │ │ │ │ ├── SelectionAdapter.kt │ │ │ │ ├── SelectionItem.kt │ │ │ │ ├── SingleChoiceDialogPreference.kt │ │ │ │ └── SingleIntChoiceDialogPreference.kt │ │ │ └── views │ │ │ └── ModernSeekBar.kt │ └── res │ │ ├── drawable │ │ ├── map_badge_background.xml │ │ ├── map_collapse_to_expand_animation.xml │ │ ├── map_expand_animated_selector.xml │ │ ├── map_expand_to_collapse_animation.xml │ │ ├── map_ic_collapse_24dp.xml │ │ ├── map_ic_expand_24dp.xml │ │ ├── map_scrim.xml │ │ ├── map_seekbar_default_marker.xml │ │ └── map_seekbar_tick_mark.xml │ │ ├── layout │ │ ├── map_accent_button_preference.xml │ │ ├── map_dialog_multi_choice_item.xml │ │ ├── map_dialog_single_choice_item.xml │ │ ├── map_image_preference.xml │ │ ├── map_preference.xml │ │ ├── map_preference_category.xml │ │ ├── map_preference_expand_text.xml │ │ ├── map_preference_widget_checkbox.xml │ │ ├── map_preference_widget_expand_arrow.xml │ │ ├── map_preference_widget_seekbar.xml │ │ ├── map_preference_widget_seekbar_stub.xml │ │ └── map_preference_widget_switch.xml │ │ └── values │ │ ├── ids.xml │ │ ├── public.xml │ │ ├── strings.xml │ │ └── values.xml │ └── test │ └── java │ └── de │ └── Maxr1998 │ └── modernpreferences │ └── testing │ ├── PreferencesTests.kt │ ├── SharedPreferencesMock.kt │ └── TestHelpers.kt ├── renovate.json ├── screenshots ├── screenshot_1.png └── screenshot_2.png ├── settings.gradle.kts └── testapp ├── .gitignore ├── LICENSE ├── build.gradle.kts ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── assets └── earthview_6300.jpg ├── java └── de │ └── Maxr1998 │ └── modernpreferences │ └── example │ ├── BaseActivity.kt │ ├── Common.kt │ ├── TestActivity.kt │ ├── TestDialog.kt │ └── view_model │ ├── MainViewModel.kt │ └── ViewModelTestActivity.kt └── res ├── anim ├── preference_item_fall_down.xml └── preference_layout_fall_down.xml ├── drawable-v24 └── ic_launcher_foreground.xml ├── drawable ├── ic_apps_24dp.xml ├── ic_emoji_24dp.xml ├── ic_info_24dp.xml ├── ic_kotlin.xml ├── ic_launcher_background.xml └── ic_list_24dp.xml ├── layout └── activity_main.xml ├── mipmap-anydpi-v26 ├── ic_launcher.xml └── ic_launcher_round.xml ├── mipmap-hdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-mdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xhdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xxhdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xxxhdpi ├── ic_launcher.png └── ic_launcher_round.png ├── values ├── strings.xml └── styles.xml └── xml ├── data_extraction_rules.xml └── full_backup_content.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = false 6 | ij_visual_guides = 120, 200 7 | 8 | [*.bat] 9 | end_of_line = crlf 10 | 11 | [*.{kt,kts,java}] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 4 15 | max_line_length = 120 16 | trim_trailing_whitespace = true 17 | ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL 18 | ij_kotlin_allow_trailing_comma = true 19 | ij_kotlin_allow_trailing_comma_on_call_site = true 20 | ij_kotlin_line_break_after_multiline_when_entry = false 21 | ij_kotlin_name_count_to_use_star_import = 999 22 | ij_kotlin_name_count_to_use_star_import_for_members = 999 23 | ij_kotlin_packages_to_use_import_on_demand = -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Maxr1998] 2 | -------------------------------------------------------------------------------- /.github/workflows/gradlew-validate.yaml: -------------------------------------------------------------------------------- 1 | name: Gradle / Validate wrapper 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | paths: 9 | - '**/gradlе-wrapper.jar' 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | validate: 16 | name: Validate 17 | runs-on: ubuntu-22.04 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 21 | - name: Validate Gradle Wrapper 22 | uses: gradle/wrapper-validation-action@b231772637bb498f11fdbc86052b6e8a8dc9fc92 # v2.1.2 -------------------------------------------------------------------------------- /.github/workflows/library-lint.yaml: -------------------------------------------------------------------------------- 1 | name: Library / Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | security-events: write 12 | 13 | jobs: 14 | api_check: 15 | name: API check 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 20 | - name: Setup Java 21 | uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 22 | with: 23 | distribution: temurin 24 | java-version: 17 25 | - name: Setup Gradle 26 | uses: gradle/actions/setup-gradle@e24011a3b5db78bd5ab798036042d9312002f252 # v3.2.0 27 | - name: Run API check task 28 | run: ./gradlew :library:apiCheck 29 | detekt: 30 | name: detekt 31 | runs-on: ubuntu-22.04 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 35 | - name: Setup Java 36 | uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 37 | with: 38 | distribution: temurin 39 | java-version: 17 40 | - name: Setup Gradle 41 | uses: gradle/actions/setup-gradle@e24011a3b5db78bd5ab798036042d9312002f252 # v3.2.0 42 | - name: Run detekt task 43 | run: ./gradlew :library:detekt 44 | - name: Upload SARIF files 45 | uses: github/codeql-action/upload-sarif@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10 46 | if: ${{ always() }} 47 | with: 48 | sarif_file: . 49 | lint: 50 | # Only run Android Lint in pull requests 51 | if: ${{ github.event_name == 'pull_request' }} 52 | name: Lint 53 | runs-on: ubuntu-22.04 54 | steps: 55 | - name: Checkout repository 56 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 57 | - name: Setup Java 58 | uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 59 | with: 60 | distribution: temurin 61 | java-version: 17 62 | - name: Setup Gradle 63 | uses: gradle/actions/setup-gradle@e24011a3b5db78bd5ab798036042d9312002f252 # v3.2.0 64 | - name: Run lint task 65 | run: ./gradlew :library:lintDebug 66 | - name: Upload SARIF files 67 | uses: github/codeql-action/upload-sarif@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10 68 | if: ${{ always() }} 69 | with: 70 | sarif_file: . -------------------------------------------------------------------------------- /.github/workflows/library-test.yaml: -------------------------------------------------------------------------------- 1 | name: Library / Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | runs-on: ubuntu-22.04 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 19 | - name: Setup Java 20 | uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 21 | with: 22 | distribution: temurin 23 | java-version: 17 24 | - name: Setup Gradle 25 | uses: gradle/actions/setup-gradle@e24011a3b5db78bd5ab798036042d9312002f252 # v3.2.0 26 | - name: Run test task 27 | run: ./gradlew :library:test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | build 7 | /captures 8 | .externalNativeBuild 9 | 10 | /library/key.properties -------------------------------------------------------------------------------- /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. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ModernAndroidPreferences 2 | 3 | [![GitHub release](https://img.shields.io/github/v/release/Maxr1998/ModernAndroidPreferences)](https://github.com/Maxr1998/ModernAndroidPreferences/releases) 4 | [![Maven Central](https://img.shields.io/maven-central/v/de.maxr1998/modernandroidpreferences)](https://repo.maven.apache.org/maven2/de/maxr1998/modernandroidpreferences/) 5 | [![Build status](https://img.shields.io/github/actions/workflow/status/Maxr1998/ModernAndroidPreferences/library-test.yaml?branch=master)](https://github.com/Maxr1998/ModernAndroidPreferences/actions/workflows/library-test.yaml) 6 | [![Lint status](https://img.shields.io/github/actions/workflow/status/Maxr1998/ModernAndroidPreferences/library-lint.yaml?branch=master&label=detekt%20%26%20lint)](https://github.com/Maxr1998/ModernAndroidPreferences/actions/workflows/library-lint.yaml) 7 | [![License](https://img.shields.io/github/license/Maxr1998/ModernAndroidPreferences)](https://github.com/Maxr1998/ModernAndroidPreferences/blob/master/LICENSE) 8 | 9 | Android preferences in Kotlin DSL, displayed in a RecyclerView. 10 | 11 | _No XML, no troubles with PreferenceManager, Fragments, or styling, no more ListView._ :tada: 12 | 13 | ### Code example 14 | ```Kotlin 15 | // Setup a preference screen 16 | val screen = screen(context) { 17 | pref("first") { 18 | title = "A preference" 19 | summary = "Click me to do stuff" 20 | click { 21 | doStuff() 22 | } 23 | } 24 | pref("second") { 25 | title = "Another one" 26 | iconRes = R.drawable.preference_icon_24dp 27 | } 28 | categoryHeader("more") { 29 | titleRes = R.string.category_more 30 | } 31 | switch("toggle_feature") { 32 | title = "Also supports switches" 33 | } 34 | // and many other preference widgets! 35 | } 36 | 37 | // Wrap the created screen in a preference adapter… 38 | val preferencesAdapter = PreferencesAdapter(screen) 39 | 40 | // …that can be attached to a RecyclerView 41 | recyclerView.adapter = preferencesAdapter 42 | ``` 43 | 44 | ### Example app 45 | Example Activities ([with](https://github.com/Maxr1998/ModernAndroidPreferences/tree/master/testapp/src/main/java/de/Maxr1998/modernpreferences/example/view_model) and [without](https://github.com/Maxr1998/ModernAndroidPreferences/blob/master/testapp/src/main/java/de/Maxr1998/modernpreferences/example/TestActivity.kt) using ViewModel) 46 | show advanced info like back handling, saving/restoring scroll position, and using the `OnScreenChangeListener`. 47 | 48 | ### Screenshots 49 |
50 | Click to show 51 | 52 | | ![](screenshots/screenshot_1.png) | ![](screenshots/screenshot_2.png) | 53 | |:---------------------------------:|:---------------------------------:| 54 | 55 |
56 | 57 | ## Include to project 58 | ModernAndroidPreferences is on [Maven Central](https://search.maven.org/artifact/de.maxr1998/modernandroidpreferences), 59 | so you can get it like any other dependency: 60 | 61 | ```gradle 62 | dependencies { 63 | implementation 'de.maxr1998:modernandroidpreferences:2.3.2' 64 | } 65 | ``` 66 | 67 | --- 68 | 69 | **NOTE:** This library has previously been available as `de.Maxr1998.android:modernpreferences` on Bintray JCenter, 70 | but was migrated to Sonatype Maven Central in light of the impending JCenter sunsetting. 71 | To get in line with Maven naming standards, it was renamed to `de.maxr1998:modernandroidpreferences`. 72 | 73 | ## License 74 | Copyright © 2018-2021 Max Rumpf alias Maxr1998 75 | 76 | This library is released under the Apache License version 2.0. 77 | If you use this library (or code from it) in your projects, crediting me is appreciated. 78 | 79 | The example application however is licensed under the GNU General Public version 3, or any later version. -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | plugins { 4 | alias(libs.plugins.android.library) apply false 5 | alias(libs.plugins.android.app) apply false 6 | alias(libs.plugins.kotlin.android) apply false 7 | } 8 | 9 | allprojects { 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | 16 | tasks { 17 | wrapper { 18 | distributionType = Wrapper.DistributionType.ALL 19 | } 20 | 21 | create("clean") { 22 | delete(rootProject.buildDir) 23 | } 24 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 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 | org.gradle.jvmargs=-Xmx4G 13 | # AndroidX package structure to make it clearer which packages are bundled with the 14 | # Android operating system, and which are packaged with your app"s APK 15 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 16 | android.useAndroidX=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # Plugins 3 | android-plugin = "8.3.1" 4 | kotlin = "1.9.23" 5 | binarycompatibilityvalidator = "0.14.0" 6 | detekt = "1.21.0" 7 | android-junit5 = "1.9.3.0" 8 | testlogger = "3.2.0" 9 | 10 | # Core 11 | androidx-core = "1.10.1" 12 | androidx-appcompat = "1.6.1" 13 | androidx-activity = "1.7.2" 14 | 15 | # UI 16 | androidx-constraintlayout = "2.1.4" 17 | androidx-recyclerview = "1.3.1" 18 | google-material = "1.9.0" 19 | 20 | # Lifecycle 21 | androidx-lifecycle = "2.6.2" 22 | 23 | # Testing 24 | junit = "5.10.0" 25 | kotest = "5.7.2" 26 | mockk = "1.13.7" 27 | 28 | # Debug 29 | leakcanary = "2.12" 30 | 31 | [plugins] 32 | android-library = { id = "com.android.library", version.ref = "android-plugin" } 33 | android-app = { id = "com.android.application", version.ref = "android-plugin" } 34 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 35 | kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } 36 | binarycompatibilityvalidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binarycompatibilityvalidator" } 37 | android-junit5 = { id = "de.mannodermaus.android-junit5", version.ref = "android-junit5" } 38 | testlogger = { id = "com.adarshr.test-logger", version.ref = "testlogger" } 39 | detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } 40 | 41 | [libraries] 42 | # Core 43 | androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" } 44 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } 45 | androidx-activity = { group = "androidx.activity", name = "activity-ktx", version.ref = "androidx-activity" } 46 | 47 | # UI 48 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } 49 | androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "androidx-recyclerview" } 50 | google-material = { group = "com.google.android.material", name = "material", version.ref = "google-material" } 51 | 52 | # Lifecycle 53 | androidx-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } 54 | androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } 55 | androidx-lifecycle-common = { group = "androidx.lifecycle", name = "lifecycle-common-java8", version.ref = "androidx-lifecycle" } 56 | 57 | # Testing 58 | junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" } 59 | junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" } 60 | kotest-runner = { group = "io.kotest", name = "kotest-runner-junit5-jvm", version.ref = "kotest" } 61 | kotest-assertions = { group = "io.kotest", name = "kotest-assertions-core-jvm", version.ref = "kotest" } 62 | kotest-property = { group = "io.kotest", name = "kotest-property-jvm", version.ref = "kotest" } 63 | mockk = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" } 64 | 65 | # Debug 66 | leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakcanary" } 67 | 68 | # Detekt plugins 69 | detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" } 70 | 71 | [bundles] 72 | androidx-lifecycle = [ 73 | "androidx-lifecycle-viewmodel", 74 | "androidx-lifecycle-runtime", 75 | "androidx-lifecycle-common", 76 | ] 77 | kotest = [ 78 | "kotest-runner", 79 | "kotest-assertions", 80 | "kotest-property", 81 | ] -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxr1998/ModernAndroidPreferences/30aa7cd8363842071df2446b1fa13aada69a0870/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=194717442575a6f96e1c1befa2c30e9a4fc90f701d7aee33eb879b79e7ff05c0 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import io.gitlab.arturbosch.detekt.Detekt 2 | import java.io.FileInputStream 3 | import java.util.Properties 4 | 5 | plugins { 6 | id("com.android.library") 7 | kotlin("android") 8 | id("kotlin-parcelize") 9 | alias(libs.plugins.detekt) 10 | alias(libs.plugins.binarycompatibilityvalidator) 11 | alias(libs.plugins.android.junit5) 12 | alias(libs.plugins.testlogger) 13 | `maven-publish` 14 | signing 15 | } 16 | 17 | // Versions 18 | val libraryVersion = "2.4.0-beta2" 19 | val libraryGroup = "de.maxr1998" 20 | val libraryName = "modernandroidpreferences" 21 | val prettyLibraryName = "ModernAndroidPreferences" 22 | 23 | detekt { 24 | buildUponDefaultConfig = true 25 | allRules = false 26 | config = files("$projectDir/detekt.yml") 27 | autoCorrect = true 28 | } 29 | 30 | android { 31 | namespace = "de.Maxr1998.modernpreferences" 32 | compileSdk = 34 33 | defaultConfig { 34 | minSdk = 21 35 | } 36 | buildTypes { 37 | getByName("release") { 38 | isMinifyEnabled = false 39 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 40 | lint { 41 | disable.add("MissingTranslation") 42 | disable.add("ExtraTranslation") 43 | } 44 | } 45 | } 46 | kotlinOptions { 47 | freeCompilerArgs += listOf("-module-name", libraryName) 48 | } 49 | lint { 50 | abortOnError = false 51 | sarifReport = true 52 | } 53 | compileOptions { 54 | sourceCompatibility = JavaVersion.VERSION_11 55 | targetCompatibility = JavaVersion.VERSION_11 56 | } 57 | kotlinOptions { 58 | jvmTarget = JavaVersion.VERSION_11.toString() 59 | } 60 | publishing { 61 | singleVariant("release") { 62 | withSourcesJar() 63 | withJavadocJar() 64 | } 65 | } 66 | } 67 | 68 | dependencies { 69 | // Core 70 | implementation(libs.androidx.core) 71 | implementation(libs.androidx.appcompat) 72 | 73 | // UI 74 | implementation(libs.androidx.constraintlayout) 75 | implementation(libs.androidx.recyclerview) 76 | 77 | // Lifecycle 78 | implementation(libs.androidx.lifecycle.runtime) 79 | 80 | // Testing 81 | testImplementation(libs.junit.api) 82 | testRuntimeOnly(libs.junit.engine) 83 | testImplementation(libs.bundles.kotest) 84 | testImplementation(libs.mockk) 85 | 86 | // Detekt formatting 87 | detektPlugins(libs.detekt.formatting) 88 | } 89 | 90 | tasks { 91 | withType { 92 | jvmTarget = JavaVersion.VERSION_11.toString() 93 | 94 | reports { 95 | html.required.set(true) 96 | xml.required.set(false) 97 | txt.required.set(true) 98 | sarif.required.set(true) 99 | } 100 | } 101 | } 102 | 103 | var ossrhUsername: String? = null 104 | var ossrhPassword: String? = null 105 | var githubToken: String? = null 106 | val propFile: File = project.file("key.properties") 107 | if (propFile.exists()) { 108 | val props = Properties() 109 | props.load(FileInputStream(propFile)) 110 | ext["signing.gnupg.keyName"] = props["signingKeyName"] as? String 111 | ossrhUsername = props["ossrhUsername"] as? String 112 | ossrhPassword = props["ossrhPassword"] as? String 113 | githubToken = props["githubToken"] as? String 114 | } 115 | 116 | // Maven publishing config 117 | afterEvaluate { 118 | publishing { 119 | publications { 120 | register("production") { 121 | groupId = libraryGroup 122 | artifactId = libraryName 123 | version = libraryVersion 124 | 125 | from(components["release"]) 126 | 127 | pom { 128 | name.set(prettyLibraryName) 129 | description.set("Android Preferences defined through Kotlin DSL, shown in a RecyclerView") 130 | url.set("https://github.com/Maxr1998/ModernAndroidPreferences") 131 | 132 | licenses { 133 | license { 134 | name.set("The Apache License, Version 2.0") 135 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 136 | } 137 | } 138 | developers { 139 | developer { 140 | id.set("Maxr1998") 141 | name.set("Max Rumpf") 142 | url.set("https://github.com/Maxr1998") 143 | } 144 | } 145 | scm { 146 | connection.set("scm:git:github.com/Maxr1998/ModernAndroidPreferences.git") 147 | developerConnection.set("scm:git:ssh://github.com/Maxr1998/ModernAndroidPreferences.git") 148 | url.set("https://github.com/Maxr1998/ModernAndroidPreferences/tree/master") 149 | } 150 | } 151 | } 152 | } 153 | repositories { 154 | maven { 155 | name = "sonatype" 156 | setUrl("https://oss.sonatype.org/service/local/staging/deploy/maven2/") 157 | credentials { 158 | username = ossrhUsername 159 | password = ossrhPassword 160 | } 161 | } 162 | maven { 163 | name = "GitHubPackages" 164 | setUrl("https://maven.pkg.github.com/Maxr1998/ModernAndroidPreferences") 165 | credentials { 166 | username = "Maxr1998" 167 | password = githubToken 168 | } 169 | } 170 | } 171 | } 172 | } 173 | 174 | signing { 175 | useGpgCmd() 176 | sign(publishing.publications) 177 | } -------------------------------------------------------------------------------- /library/detekt.yml: -------------------------------------------------------------------------------- 1 | build: 2 | maxIssues: 0 3 | 4 | complexity: 5 | LongMethod: 6 | threshold: 80 7 | TooManyFunctions: 8 | thresholdInFiles: 20 9 | thresholdInClasses: 20 10 | thresholdInInterfaces: 8 11 | thresholdInObjects: 16 12 | thresholdInEnums: 8 13 | ignoreDeprecated: true 14 | 15 | formatting: 16 | Filename: 17 | active: false # active in naming rules 18 | FinalNewline: 19 | insertFinalNewLine: false 20 | MaximumLineLength: 21 | active: false # active in style rules 22 | maxLineLength: 120 23 | PackageName: 24 | active: false # active in naming rules 25 | TrailingComma: 26 | active: true 27 | allowTrailingComma: true 28 | allowTrailingCommaOnCallSite: true 29 | 30 | naming: 31 | PackageNaming: 32 | # Package names must be lowercase letters but may include underscores 33 | packagePattern: '[a-z]+(\.[a-zA-Z][a-z0-9]*)*' 34 | 35 | style: 36 | ForbiddenComment: 37 | # Allow TODOs 38 | values: [ 'FIXME:', 'STOPSHIP:' ] 39 | MaxLineLength: 40 | maxLineLength: 120 41 | NewLineAtEndOfFile: 42 | active: false -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/helpers/DependencyManager.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.helpers 2 | 3 | import android.content.SharedPreferences 4 | import de.Maxr1998.modernpreferences.Preference 5 | import de.Maxr1998.modernpreferences.preferences.StatefulPreference 6 | import java.lang.ref.WeakReference 7 | import java.util.LinkedList 8 | 9 | internal object DependencyManager { 10 | private val preferences = HashMap>>() 11 | private val stateCache = HashMap() 12 | 13 | /** 14 | * Register a preference with the manager. 15 | * If the preference's [dependency][Preference.dependency] field is set, 16 | * it's added to internal data structures and gets updated with the latest state 17 | * supplied by the dependency. 18 | */ 19 | fun register(preference: Preference) { 20 | val screen = preference.parent 21 | check(screen != null) { "Preference must be attached to a screen first" } 22 | val dependency = preference.dependency ?: return 23 | val key = PreferenceKey(screen.prefs, dependency) 24 | preferences.getOrPut(key) { LinkedList() }.add(WeakReference(preference)) 25 | stateCache[key]?.let { state -> preference.enabled = state } 26 | } 27 | 28 | /** 29 | * Update dependencies of this [StatefulPreference] with the latest state. 30 | */ 31 | fun publishState(preference: StatefulPreference) { 32 | val screen = preference.parent 33 | check(screen != null) { "Preference must be attached to a screen first" } 34 | val key = PreferenceKey(screen.prefs, preference.key) 35 | val state = preference.state // Cache state so that every dependent gets the same value 36 | stateCache[key] = state 37 | preferences[key]?.forEach { it.get()?.enabled = state } 38 | } 39 | 40 | private data class PreferenceKey(val preferenceStore: SharedPreferences?, val key: String) 41 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/helpers/PreferenceMarker.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.helpers 2 | 3 | @DslMarker 4 | @Retention(AnnotationRetention.SOURCE) 5 | internal annotation class PreferenceMarker -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/helpers/PreferencesDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused", "TooManyFunctions") 18 | 19 | package de.Maxr1998.modernpreferences.helpers 20 | 21 | import android.content.Context 22 | import android.view.View 23 | import de.Maxr1998.modernpreferences.Preference 24 | import de.Maxr1998.modernpreferences.PreferenceScreen 25 | import de.Maxr1998.modernpreferences.preferences.AccentButtonPreference 26 | import de.Maxr1998.modernpreferences.preferences.CategoryHeader 27 | import de.Maxr1998.modernpreferences.preferences.CheckBoxPreference 28 | import de.Maxr1998.modernpreferences.preferences.CollapsePreference 29 | import de.Maxr1998.modernpreferences.preferences.EditTextPreference 30 | import de.Maxr1998.modernpreferences.preferences.ExpandableTextPreference 31 | import de.Maxr1998.modernpreferences.preferences.ImagePreference 32 | import de.Maxr1998.modernpreferences.preferences.SeekBarPreference 33 | import de.Maxr1998.modernpreferences.preferences.SwitchPreference 34 | import de.Maxr1998.modernpreferences.preferences.TwoStatePreference 35 | import de.Maxr1998.modernpreferences.preferences.choice.AbstractSingleChoiceDialogPreference 36 | import de.Maxr1998.modernpreferences.preferences.choice.MultiChoiceDialogPreference 37 | import de.Maxr1998.modernpreferences.preferences.choice.SelectionItem 38 | import de.Maxr1998.modernpreferences.preferences.choice.SingleChoiceDialogPreference 39 | import de.Maxr1998.modernpreferences.preferences.choice.SingleIntChoiceDialogPreference 40 | 41 | // PreferenceScreen DSL functions 42 | inline fun screen(context: Context?, block: PreferenceScreen.Builder.() -> Unit): PreferenceScreen { 43 | return PreferenceScreen.Builder(context).apply(block).build() 44 | } 45 | 46 | inline fun PreferenceScreen.Builder.subScreen( 47 | key: String = "", 48 | block: PreferenceScreen.Builder.() -> Unit, 49 | ): PreferenceScreen { 50 | return PreferenceScreen.Builder(this, key).apply(block).build().also(::addPreferenceItem) 51 | } 52 | 53 | // Preference DSL functions 54 | inline fun PreferenceScreen.Appendable.categoryHeader(key: String, block: Preference.() -> Unit) { 55 | addPreferenceItem(CategoryHeader(key).apply(block)) 56 | } 57 | 58 | inline fun PreferenceScreen.Appendable.pref(key: String, block: Preference.() -> Unit): Preference { 59 | return Preference(key).apply(block).also(::addPreferenceItem) 60 | } 61 | 62 | inline fun PreferenceScreen.Appendable.accentButtonPref(key: String, block: Preference.() -> Unit): Preference { 63 | return AccentButtonPreference(key).apply(block).also(::addPreferenceItem) 64 | } 65 | 66 | inline fun PreferenceScreen.Appendable.switch(key: String, block: SwitchPreference.() -> Unit): SwitchPreference { 67 | return SwitchPreference(key).apply(block).also(::addPreferenceItem) 68 | } 69 | 70 | inline fun PreferenceScreen.Appendable.checkBox(key: String, block: CheckBoxPreference.() -> Unit): CheckBoxPreference { 71 | return CheckBoxPreference(key).apply(block).also(::addPreferenceItem) 72 | } 73 | 74 | inline fun PreferenceScreen.Appendable.image(key: String, block: ImagePreference.() -> Unit): ImagePreference { 75 | return ImagePreference(key).apply(block).also(::addPreferenceItem) 76 | } 77 | 78 | inline fun PreferenceScreen.Appendable.seekBar(key: String, block: SeekBarPreference.() -> Unit): SeekBarPreference { 79 | return SeekBarPreference(key).apply(block).also(::addPreferenceItem) 80 | } 81 | 82 | inline fun PreferenceScreen.Appendable.expandText( 83 | key: String, 84 | block: ExpandableTextPreference.() -> Unit, 85 | ): ExpandableTextPreference { 86 | return ExpandableTextPreference(key).apply(block).also(::addPreferenceItem) 87 | } 88 | 89 | inline fun PreferenceScreen.Appendable.singleChoice( 90 | key: String, 91 | items: List>, 92 | block: SingleChoiceDialogPreference.() -> Unit, 93 | ): SingleChoiceDialogPreference { 94 | return SingleChoiceDialogPreference(key, items).apply(block).also(::addPreferenceItem) 95 | } 96 | 97 | inline fun PreferenceScreen.Appendable.singleChoice( 98 | key: String, 99 | items: List>, 100 | block: SingleIntChoiceDialogPreference.() -> Unit, 101 | ): SingleIntChoiceDialogPreference { 102 | return SingleIntChoiceDialogPreference(key, items).apply(block).also(::addPreferenceItem) 103 | } 104 | 105 | inline fun PreferenceScreen.Appendable.multiChoice( 106 | key: String, 107 | items: List>, 108 | block: MultiChoiceDialogPreference.() -> Unit, 109 | ): MultiChoiceDialogPreference { 110 | return MultiChoiceDialogPreference(key, items).apply(block).also(::addPreferenceItem) 111 | } 112 | 113 | inline fun PreferenceScreen.Appendable.editText(key: String, block: EditTextPreference.() -> Unit): EditTextPreference { 114 | return EditTextPreference(key).apply(block).also(::addPreferenceItem) 115 | } 116 | 117 | inline fun PreferenceScreen.Appendable.custom(key: String, block: T.() -> Unit): T { 118 | return T::class.java.getConstructor(String::class.java).newInstance(key).apply(block).also(::addPreferenceItem) 119 | } 120 | 121 | inline fun PreferenceScreen.Builder.collapse( 122 | key: String = "advanced", 123 | block: CollapsePreference.() -> Unit, 124 | ): CollapsePreference { 125 | return CollapsePreference(this, key).also(::addPreferenceItem).apply { 126 | block() 127 | clearContext() 128 | } 129 | } 130 | 131 | inline fun CollapsePreference.subScreen(key: String = "", block: PreferenceScreen.Builder.() -> Unit) { 132 | addPreferenceItem(PreferenceScreen.Builder(this, key).apply(block).build()) 133 | } 134 | 135 | // Listener helpers 136 | 137 | /** 138 | * [Preference.OnClickListener] shorthand without parameters. 139 | * Callback return value determines whether the Preference changed/requires a rebind. 140 | */ 141 | inline fun Preference.onClick(crossinline callback: () -> Boolean) { 142 | clickListener = Preference.OnClickListener { _, _ -> callback() } 143 | } 144 | 145 | /** 146 | * [Preference.OnClickListener] shorthand without parameters that returns false by default, 147 | * meaning the Preference didn't get changed and doesn't require a rebind/redraw. 148 | */ 149 | inline fun Preference.defaultOnClick(crossinline callback: () -> Unit) { 150 | clickListener = Preference.OnClickListener { _, _ -> 151 | callback() 152 | false 153 | } 154 | } 155 | 156 | /** 157 | * [Preference.OnClickListener] shorthand that only passes the view of the clicked item and returns false by default, 158 | * meaning the Preference didn't get changed and doesn't require a rebind/redraw. 159 | */ 160 | inline fun Preference.onClickView(crossinline callback: (View) -> Unit) { 161 | clickListener = Preference.OnClickListener { _, holder -> 162 | callback(holder.itemView) 163 | false 164 | } 165 | } 166 | 167 | /** 168 | * [TwoStatePreference.OnCheckedChangeListener] shorthand. 169 | * Supplies the changed state, return value determines whether that state should be persisted 170 | * to [SharedPreferences][android.content.SharedPreferences]. 171 | */ 172 | inline fun TwoStatePreference.onCheckedChange(crossinline callback: (Boolean) -> Boolean) { 173 | checkedChangeListener = TwoStatePreference.OnCheckedChangeListener { _, _, checked -> 174 | callback(checked) 175 | } 176 | } 177 | 178 | /** 179 | * [TwoStatePreference.OnCheckedChangeListener] shorthand. 180 | * Always persists the change to [SharedPreferences][android.content.SharedPreferences]. 181 | */ 182 | inline fun TwoStatePreference.defaultOnCheckedChange(crossinline callback: (Boolean) -> Unit) { 183 | checkedChangeListener = TwoStatePreference.OnCheckedChangeListener { _, _, checked -> 184 | callback(checked) 185 | true 186 | } 187 | } 188 | 189 | /** 190 | * [AbstractSingleChoiceDialogPreference.OnSelectionChangeListener] shorthand. 191 | * Supplies the changed selection, return value determines whether that state should be persisted 192 | * to [SharedPreferences][android.content.SharedPreferences]. 193 | */ 194 | inline fun SingleChoiceDialogPreference.onSelectionChange(crossinline callback: (String) -> Boolean) { 195 | selectionChangeListener = AbstractSingleChoiceDialogPreference.OnSelectionChangeListener { _, selection -> 196 | callback(selection) 197 | } 198 | } 199 | 200 | /** 201 | * [AbstractSingleChoiceDialogPreference.OnSelectionChangeListener] shorthand. 202 | * Always persists the change to [SharedPreferences][android.content.SharedPreferences]. 203 | */ 204 | inline fun SingleChoiceDialogPreference.defaultOnSelectionChange(crossinline callback: (String) -> Unit) { 205 | selectionChangeListener = AbstractSingleChoiceDialogPreference.OnSelectionChangeListener { _, selection -> 206 | callback(selection) 207 | true 208 | } 209 | } 210 | 211 | /** 212 | * [MultiChoiceDialogPreference.OnSelectionChangeListener] shorthand. 213 | * Supplies the changed selections, return value determines whether that state should be persisted 214 | * to [SharedPreferences][android.content.SharedPreferences]. 215 | */ 216 | inline fun MultiChoiceDialogPreference.onSelectionChange(crossinline callback: (Set) -> Boolean) { 217 | selectionChangeListener = MultiChoiceDialogPreference.OnSelectionChangeListener { _, selection -> 218 | callback(selection) 219 | } 220 | } 221 | 222 | /** 223 | * [MultiChoiceDialogPreference.OnSelectionChangeListener] shorthand. 224 | * Always persists the change to [SharedPreferences][android.content.SharedPreferences]. 225 | */ 226 | inline fun MultiChoiceDialogPreference.defaultOnSelectionChange(crossinline callback: (Set) -> Unit) { 227 | selectionChangeListener = MultiChoiceDialogPreference.OnSelectionChangeListener { _, selection -> 228 | callback(selection) 229 | true 230 | } 231 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/helpers/Utils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.Maxr1998.modernpreferences.helpers 18 | 19 | import android.widget.SeekBar 20 | 21 | const val KEY_ROOT_SCREEN = "root" 22 | 23 | /** 24 | * A resource ID as a default value for optional attributes. 25 | */ 26 | internal const val DISABLED_RESOURCE_ID = -1 27 | 28 | @Deprecated("This constant was accidentally exposed and should not be used.") 29 | const val DEFAULT_RES_ID = DISABLED_RESOURCE_ID 30 | 31 | internal fun SeekBar.onSeek(callback: (Int, Boolean) -> Unit) { 32 | setOnSeekBarChangeListener( 33 | object : SeekBar.OnSeekBarChangeListener { 34 | @Suppress("EmptyFunctionBlock") 35 | override fun onStartTrackingTouch(seekBar: SeekBar) { 36 | } 37 | 38 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 39 | if (fromUser) callback(progress, false) 40 | } 41 | 42 | override fun onStopTrackingTouch(seekBar: SeekBar) { 43 | callback(seekBar.progress, true) 44 | } 45 | }, 46 | ) 47 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/AccentButtonPreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Max Rumpf alias Maxr1998 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.Maxr1998.modernpreferences.preferences 18 | 19 | import android.annotation.SuppressLint 20 | import de.Maxr1998.modernpreferences.Preference 21 | 22 | class AccentButtonPreference(key: String) : Preference(key) { 23 | @SuppressLint("ResourceType") 24 | override fun getWidgetLayoutResource() = RESOURCE_CONST 25 | 26 | internal companion object { 27 | internal const val RESOURCE_CONST = -3 28 | } 29 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/Badge.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.preferences 2 | 3 | import android.content.res.ColorStateList 4 | import androidx.annotation.StringRes 5 | import de.Maxr1998.modernpreferences.helpers.DISABLED_RESOURCE_ID 6 | 7 | data class Badge internal constructor( 8 | @StringRes 9 | val textRes: Int = DISABLED_RESOURCE_ID, 10 | val text: CharSequence? = null, 11 | val badgeColor: ColorStateList? = null, 12 | ) { 13 | constructor(text: CharSequence?, badgeColor: ColorStateList? = null) : this( 14 | textRes = DISABLED_RESOURCE_ID, 15 | text = text, 16 | badgeColor = badgeColor, 17 | ) 18 | 19 | constructor(@StringRes textRes: Int, badgeColor: ColorStateList? = null) : this( 20 | textRes = textRes, 21 | text = null, 22 | badgeColor = badgeColor, 23 | ) 24 | 25 | val isVisible: Boolean 26 | get() = textRes != DISABLED_RESOURCE_ID || text != null 27 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/CategoryHeader.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.Maxr1998.modernpreferences.preferences 18 | 19 | import android.annotation.SuppressLint 20 | import de.Maxr1998.modernpreferences.Preference 21 | 22 | class CategoryHeader(key: String) : Preference(key) { 23 | @SuppressLint("ResourceType") 24 | override fun getWidgetLayoutResource() = RESOURCE_CONST 25 | 26 | internal companion object { 27 | internal const val RESOURCE_CONST = -2 28 | } 29 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/CheckBoxPreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.Maxr1998.modernpreferences.preferences 18 | 19 | import de.Maxr1998.modernpreferences.R 20 | 21 | class CheckBoxPreference(key: String) : TwoStatePreference(key) { 22 | override fun getWidgetLayoutResource() = R.layout.map_preference_widget_checkbox 23 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/CollapsePreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.Maxr1998.modernpreferences.preferences 18 | 19 | import android.annotation.SuppressLint 20 | import android.content.Context 21 | import android.text.TextUtils 22 | import de.Maxr1998.modernpreferences.Preference 23 | import de.Maxr1998.modernpreferences.PreferenceScreen 24 | import de.Maxr1998.modernpreferences.PreferencesAdapter 25 | import de.Maxr1998.modernpreferences.R 26 | import de.Maxr1998.modernpreferences.helpers.DISABLED_RESOURCE_ID 27 | 28 | /** 29 | * IMPORTANT: If you're using this independently from the helper DSLs, 30 | * make sure to call [clearContext] after you have completed with all [addPreferenceItem] operations, 31 | * to not leak the context supplied in [screen]. 32 | */ 33 | class CollapsePreference(screen: PreferenceScreen.Builder, key: String) : Preference(key), PreferenceScreen.Appendable { 34 | internal var screen: PreferenceScreen.Builder? = screen 35 | private val preferences = ArrayList() 36 | 37 | init { 38 | titleRes = R.string.pref_advanced_block 39 | iconRes = R.drawable.map_ic_expand_24dp 40 | } 41 | 42 | override fun addPreferenceItem(p: Preference) { 43 | val screen = checkNotNull(screen) { 44 | "Don't call clearContext before you've finished all addPreferenceItem operations!" 45 | } 46 | screen += p 47 | preferences += p.apply { visible = false } 48 | } 49 | 50 | fun clearContext() { 51 | screen = null 52 | } 53 | 54 | /* 55 | This makes sure we don't get recycled for any other preference. 56 | Hopefully, no one else uses this constant for the exact same purpose… 57 | But that will probably not be our problem. 58 | */ 59 | @SuppressLint("ResourceType") 60 | override fun getWidgetLayoutResource() = MARKER_RES_ID 61 | 62 | override fun bindViews(holder: PreferencesAdapter.ViewHolder) { 63 | if (visible) buildSummary(holder.itemView.context) 64 | super.bindViews(holder) 65 | holder.summary?.apply { 66 | setSingleLine() 67 | ellipsize = TextUtils.TruncateAt.END 68 | } 69 | } 70 | 71 | private fun buildSummary(context: Context) { 72 | if (summaryRes != DISABLED_RESOURCE_ID || summary != null) return 73 | 74 | summary = preferences.asSequence() 75 | .filter(Preference::includeInCollapseSummary) 76 | .take(MAX_PREFS_IN_SUMMARY) 77 | .joinToString { p -> 78 | when { 79 | p.titleRes != DISABLED_RESOURCE_ID -> context.getString(p.titleRes) 80 | else -> p.title 81 | } 82 | } 83 | } 84 | 85 | override fun onClick(holder: PreferencesAdapter.ViewHolder) { 86 | visible = false 87 | for (i in preferences.indices) { 88 | preferences[i].visible = true 89 | } 90 | parent?.requestRebind(screenPosition, 1 + preferences.size) 91 | } 92 | 93 | fun reset() { 94 | visible = true 95 | for (i in preferences.indices) { 96 | preferences[i].visible = false 97 | } 98 | parent?.requestRebind(screenPosition, 1 + preferences.size) 99 | } 100 | 101 | internal companion object { 102 | internal const val MARKER_RES_ID = -10 103 | internal const val MAX_PREFS_IN_SUMMARY = 5 104 | } 105 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/DialogPreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.Maxr1998.modernpreferences.preferences 18 | 19 | import android.app.Dialog 20 | import android.content.Context 21 | import androidx.annotation.CallSuper 22 | import androidx.lifecycle.Lifecycle 23 | import androidx.lifecycle.LifecycleEventObserver 24 | import androidx.lifecycle.LifecycleOwner 25 | import de.Maxr1998.modernpreferences.Preference 26 | import de.Maxr1998.modernpreferences.PreferencesAdapter 27 | 28 | /** 29 | * DialogPreference is a helper class to display custom [Dialog]s on preference clicks. 30 | * It is recommended to override this class once and then inflate different dialogs 31 | * based on their keys in [createDialog]. 32 | */ 33 | abstract class DialogPreference(key: String) : Preference(key), LifecycleEventObserver { 34 | 35 | private var dialog: Dialog? = null 36 | 37 | /** 38 | * This flag tells the preference whether to recreate the dialog after a configuration change 39 | */ 40 | private var recreateDialog = false 41 | 42 | override fun onClick(holder: PreferencesAdapter.ViewHolder) { 43 | createAndShowDialog(holder.itemView.context) 44 | } 45 | 46 | /** 47 | * Subclasses must create the dialog which will managed by this preference here. 48 | * However, they should not [show][Dialog.show] it already, that will be done in [onClick]. 49 | * 50 | * @param context the context to create your Dialog with, has a window attached 51 | */ 52 | abstract fun createDialog(context: Context): Dialog 53 | 54 | private fun createAndShowDialog(context: Context) { 55 | (dialog ?: createDialog(context).apply { dialog = this }).show() 56 | } 57 | 58 | /** 59 | * Dismiss the currently attached dialog, if any 60 | */ 61 | fun dismiss() = dialog?.dismiss() 62 | 63 | @CallSuper 64 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 65 | when (event) { 66 | Lifecycle.Event.ON_CREATE -> { 67 | if (recreateDialog && source is Context) { 68 | recreateDialog = false 69 | createAndShowDialog(source) 70 | } 71 | } 72 | Lifecycle.Event.ON_DESTROY -> { 73 | dialog?.apply { 74 | recreateDialog = isShowing 75 | dismiss() 76 | } 77 | dialog = null 78 | } 79 | else -> Unit // ignore 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/EditTextPreference.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.preferences 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.text.InputType 6 | import android.view.ViewGroup 7 | import android.view.ViewGroup.LayoutParams.MATCH_PARENT 8 | import android.view.ViewGroup.LayoutParams.WRAP_CONTENT 9 | import android.widget.FrameLayout 10 | import androidx.annotation.StringRes 11 | import androidx.appcompat.widget.AppCompatEditText 12 | import de.Maxr1998.modernpreferences.helpers.DISABLED_RESOURCE_ID 13 | 14 | class EditTextPreference(key: String) : DialogPreference(key) { 15 | 16 | var currentInput: CharSequence? = null 17 | private set 18 | 19 | /** 20 | * The default value of this preference, when nothing was committed to storage yet 21 | */ 22 | var defaultValue: String = "" 23 | 24 | /** 25 | * The [InputType] applied to the contained [EditText][AppCompatEditText] 26 | */ 27 | var textInputType: Int = InputType.TYPE_NULL 28 | 29 | @StringRes 30 | var textInputHintRes: Int = DISABLED_RESOURCE_ID 31 | var textInputHint: CharSequence? = null 32 | 33 | var textChangeListener: OnTextChangeListener? = null 34 | 35 | /** 36 | * Allows to override the summary, providing the current input value when called. 37 | * 38 | * Summary falls back to [summary] or [summaryRes] when null is returned. 39 | */ 40 | var summaryProvider: (CharSequence?) -> CharSequence? = { null } 41 | 42 | override fun onAttach() { 43 | super.onAttach() 44 | if (currentInput == null) { 45 | currentInput = getString() ?: defaultValue.takeUnless(String::isEmpty) 46 | } 47 | } 48 | 49 | override fun createDialog(context: Context): Dialog = Config.dialogBuilderFactory(context).apply { 50 | when { 51 | titleRes != DISABLED_RESOURCE_ID -> setTitle(titleRes) 52 | else -> setTitle(title) 53 | } 54 | val editText = AppCompatEditText(context).apply { 55 | if (textInputType != InputType.TYPE_NULL) { 56 | inputType = textInputType 57 | } 58 | when { 59 | textInputHintRes != DISABLED_RESOURCE_ID -> setHint(textInputHintRes) 60 | textInputHint != null -> hint = textInputHint 61 | } 62 | setText(currentInput) 63 | } 64 | val dialogContent = FrameLayout(context).apply { 65 | val layoutParams = ViewGroup.MarginLayoutParams(MATCH_PARENT, WRAP_CONTENT).apply { 66 | @Suppress("MagicNumber") 67 | val tenDp = (10 * context.resources.displayMetrics.density).toInt() 68 | marginStart = 2 * tenDp 69 | marginEnd = 2 * tenDp 70 | topMargin = tenDp 71 | } 72 | addView(editText, layoutParams) 73 | } 74 | setView(dialogContent) 75 | setCancelable(false) 76 | setPositiveButton(android.R.string.ok) { _, _ -> 77 | editText.text?.let(::persist) 78 | requestRebind() 79 | } 80 | setNegativeButton(android.R.string.cancel) { _, _ -> 81 | editText.setText(currentInput) 82 | } 83 | }.create() 84 | 85 | private fun persist(input: CharSequence) { 86 | if (textChangeListener?.onTextChange(this, input) != false) { 87 | currentInput = input 88 | commitString(input.toString()) 89 | } 90 | } 91 | 92 | override fun resolveSummary(context: Context): CharSequence? { 93 | return summaryProvider(currentInput) ?: super.resolveSummary(context) 94 | } 95 | 96 | fun interface OnTextChangeListener { 97 | /** 98 | * Notified when the value of the connected [EditTextPreference] changes, 99 | * meaning after the user closes the dialog by pressing "ok". 100 | * This is called before the change gets persisted and can be prevented by returning false. 101 | * 102 | * @param text the new value 103 | * 104 | * @return true to commit the new value to [SharedPreferences][android.content.SharedPreferences] 105 | */ 106 | fun onTextChange(preference: EditTextPreference, text: CharSequence): Boolean 107 | } 108 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/ExpandableTextPreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.Maxr1998.modernpreferences.preferences 18 | 19 | import android.graphics.Typeface 20 | import android.transition.ChangeBounds 21 | import android.transition.TransitionManager 22 | import android.view.LayoutInflater 23 | import android.view.ViewGroup 24 | import android.widget.CheckBox 25 | import android.widget.TextView 26 | import androidx.annotation.StringRes 27 | import androidx.core.content.ContextCompat 28 | import androidx.core.view.isVisible 29 | import de.Maxr1998.modernpreferences.Preference 30 | import de.Maxr1998.modernpreferences.PreferencesAdapter 31 | import de.Maxr1998.modernpreferences.R 32 | import de.Maxr1998.modernpreferences.helpers.DISABLED_RESOURCE_ID 33 | 34 | class ExpandableTextPreference(key: String) : Preference(key) { 35 | private var expanded = false 36 | 37 | @StringRes 38 | var textRes: Int = DISABLED_RESOURCE_ID 39 | var text: CharSequence? = null 40 | 41 | var monospace = true 42 | 43 | override fun getWidgetLayoutResource() = R.layout.map_preference_widget_expand_arrow 44 | 45 | override fun bindViews(holder: PreferencesAdapter.ViewHolder) { 46 | super.bindViews(holder) 47 | val widget = holder.widget as CheckBox 48 | val tv: TextView = widget.tag as? TextView ?: run { 49 | val inflater = LayoutInflater.from(widget.context) 50 | inflater.inflate(R.layout.map_preference_expand_text, holder.root).findViewById(android.R.id.message) 51 | } 52 | widget.tag = tv 53 | tv.apply { 54 | when { 55 | textRes != DISABLED_RESOURCE_ID -> setText(textRes) 56 | else -> text = this@ExpandableTextPreference.text 57 | } 58 | typeface = when { 59 | monospace -> Typeface.MONOSPACE 60 | else -> Typeface.SANS_SERIF 61 | } 62 | with(context.obtainStyledAttributes(intArrayOf(R.attr.expandableTextBackgroundColor))) { 63 | val fallback = ContextCompat.getColor(context, R.color.expandableTextBackgroundColorDefault) 64 | setBackgroundColor(getColor(0, fallback)) 65 | recycle() 66 | } 67 | isEnabled = enabled 68 | } 69 | refreshArrowState(widget) 70 | refreshTextExpandState(tv) 71 | } 72 | 73 | override fun onClick(holder: PreferencesAdapter.ViewHolder) { 74 | expanded = !expanded 75 | refreshArrowState(holder.widget as CheckBox) 76 | refreshTextExpandState(holder.widget.tag as TextView) 77 | } 78 | 79 | private fun refreshArrowState(widget: CheckBox) { 80 | widget.isChecked = expanded 81 | } 82 | 83 | private fun refreshTextExpandState(text: TextView) { 84 | TransitionManager.beginDelayedTransition(text.parent as ViewGroup, ChangeBounds()) 85 | text.isVisible = expanded 86 | } 87 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/ImagePreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 Max Rumpf alias Maxr1998 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.Maxr1998.modernpreferences.preferences 18 | 19 | import android.graphics.ColorFilter 20 | import android.graphics.PorterDuff 21 | import android.graphics.PorterDuffColorFilter 22 | import android.graphics.drawable.Drawable 23 | import android.widget.ImageView 24 | import androidx.annotation.ColorInt 25 | import androidx.annotation.DrawableRes 26 | import androidx.core.view.isVisible 27 | import de.Maxr1998.modernpreferences.Preference 28 | import de.Maxr1998.modernpreferences.PreferencesAdapter 29 | import de.Maxr1998.modernpreferences.R 30 | import de.Maxr1998.modernpreferences.helpers.DISABLED_RESOURCE_ID 31 | 32 | /** 33 | * Shows a drawable inside an ImageView 34 | * 35 | * The drawable can be set via [imageRes], [imageDrawable], or lazily via [lazyImage]; 36 | * with priorities being evaluated in the same order. 37 | * 38 | * By default, a scrim is shown over the image to improve [title] legibility, which can be disabled 39 | * by setting [showScrim] to false. 40 | * 41 | * The image can be tinted by applying a [ColorFilter], either via [tint], 42 | * or through the helper method [setTintColor]. 43 | */ 44 | @Suppress("MemberVisibilityCanBePrivate") 45 | class ImagePreference(key: String) : Preference(key) { 46 | override fun getWidgetLayoutResource() = RESOURCE_CONST 47 | 48 | @DrawableRes 49 | var imageRes: Int = DISABLED_RESOURCE_ID 50 | var imageDrawable: Drawable? = null 51 | var lazyImage: (() -> Drawable)? = null 52 | 53 | var maxImageHeight = Integer.MAX_VALUE 54 | 55 | var showScrim = true 56 | var tint: ColorFilter? = null 57 | 58 | fun setTintColor(@ColorInt color: Int, mode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP) { 59 | tint = PorterDuffColorFilter(color, mode) 60 | } 61 | 62 | override fun bindViews(holder: PreferencesAdapter.ViewHolder) { 63 | super.bindViews(holder) 64 | val image = holder.root.findViewById(R.id.map_image) 65 | when { 66 | imageRes != DISABLED_RESOURCE_ID -> image.setImageResource(imageRes) 67 | imageDrawable != null -> image.setImageDrawable(imageDrawable) 68 | lazyImage != null -> image.setImageDrawable(lazyImage?.invoke()) 69 | else -> image.setImageDrawable(null) 70 | } 71 | image.maxHeight = maxImageHeight 72 | image.colorFilter = tint 73 | holder.root.findViewById(R.id.map_image_scrim).isVisible = showScrim && title.isNotBlank() 74 | } 75 | 76 | internal companion object { 77 | internal const val RESOURCE_CONST = -4 78 | } 79 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/SeekBarPreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.Maxr1998.modernpreferences.preferences 18 | 19 | import android.view.LayoutInflater 20 | import android.widget.Space 21 | import android.widget.TextView 22 | import androidx.constraintlayout.widget.ConstraintLayout 23 | import androidx.core.view.updateLayoutParams 24 | import de.Maxr1998.modernpreferences.Preference 25 | import de.Maxr1998.modernpreferences.PreferencesAdapter 26 | import de.Maxr1998.modernpreferences.R 27 | import de.Maxr1998.modernpreferences.helpers.onSeek 28 | import de.Maxr1998.modernpreferences.views.ModernSeekBar 29 | 30 | class SeekBarPreference(key: String) : Preference(key) { 31 | 32 | var min = 0 33 | var max = 0 34 | var default: Int? = null 35 | var step = 1 36 | set(value) { 37 | require(value > 0) { "Stepping value must be >= 1" } 38 | field = value 39 | } 40 | 41 | var showTickMarks = false 42 | 43 | /** 44 | * The internal backing field of [value] 45 | */ 46 | private var valueInternal = 0 47 | var value: Int 48 | get() = valueInternal 49 | set(v) { 50 | if (v != valueInternal && seekListener?.onSeek(this, null, v) != false) { 51 | valueInternal = v 52 | commitInt(value) 53 | requestRebind() 54 | } 55 | } 56 | 57 | var seekListener: OnSeekListener? = null 58 | var formatter: (Int) -> String = Int::toString 59 | 60 | override fun getWidgetLayoutResource() = R.layout.map_preference_widget_seekbar_stub 61 | 62 | override fun onAttach() { 63 | check(min <= max) { "Minimum value can't be greater than maximum!" } 64 | default?.let { default -> 65 | check(default in min..max) { "Default value must be in between min and max!" } 66 | } 67 | valueInternal = getInt(default ?: min) 68 | } 69 | 70 | override fun bindViews(holder: PreferencesAdapter.ViewHolder) { 71 | super.bindViews(holder) 72 | holder.root.apply { 73 | background = null 74 | clipChildren = false 75 | } 76 | holder.iconFrame.updateLayoutParams { 77 | bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID 78 | @Suppress("MagicNumber") 79 | bottomMargin = (40 * holder.itemView.resources.displayMetrics.density).toInt() 80 | } 81 | holder.title.updateLayoutParams { 82 | goneBottomMargin = 0 83 | } 84 | holder.summary?.updateLayoutParams { 85 | bottomMargin = 0 86 | } 87 | val widget = holder.widget as Space 88 | val sb = widget.tag as? ModernSeekBar ?: run { 89 | val inflater = LayoutInflater.from(widget.context) 90 | inflater.inflate(R.layout.map_preference_widget_seekbar, holder.root).findViewById(android.R.id.progress) 91 | } 92 | val tv = sb.tag as? TextView ?: holder.itemView.findViewById(R.id.progress_text) 93 | widget.tag = sb.apply { 94 | isEnabled = enabled 95 | max = calcRaw(this@SeekBarPreference.max) 96 | progress = calcRaw(valueInternal) 97 | hasTickMarks = showTickMarks 98 | this@SeekBarPreference.default?.let { default = calcRaw(it) } 99 | 100 | onSeek { v, done -> 101 | if (done) { 102 | // Commit the last selected value 103 | commitInt(valueInternal) 104 | } else { 105 | val next = calcValue(v) 106 | // Check if listener allows the value change 107 | if (seekListener?.onSeek(this@SeekBarPreference, holder, next) != false) { 108 | // Update internal value 109 | valueInternal = next 110 | } else { 111 | // Restore previous value 112 | progress = calcRaw(valueInternal) 113 | } 114 | // Update preview text 115 | tv.text = formatter(valueInternal) 116 | } 117 | } 118 | } 119 | sb.tag = tv.apply { 120 | isEnabled = enabled 121 | text = formatter(valueInternal) 122 | } 123 | } 124 | 125 | private fun calcRaw(value: Int) = (value - min) / step 126 | private fun calcValue(raw: Int) = min + raw * step 127 | 128 | fun interface OnSeekListener { 129 | /** 130 | * Notified when the [value][SeekBarPreference.value] of the connected [SeekBarPreference] changes. 131 | * This is called *before* the change gets persisted, which can be prevented by returning false. 132 | * 133 | * @param holder the [ViewHolder][PreferencesAdapter.ViewHolder] with the views of the Preference instance, 134 | * or null if the change didn't occur as part of a click event 135 | * @param value the new state 136 | * 137 | * @return true to commit the new slider value to [SharedPreferences][android.content.SharedPreferences] 138 | */ 139 | fun onSeek(preference: SeekBarPreference, holder: PreferencesAdapter.ViewHolder?, value: Int): Boolean 140 | } 141 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/StatefulPreference.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.preferences 2 | 3 | import de.Maxr1998.modernpreferences.Preference 4 | import de.Maxr1998.modernpreferences.helpers.DependencyManager 5 | 6 | abstract class StatefulPreference(key: String) : Preference(key) { 7 | internal abstract val state: Boolean 8 | 9 | override fun onAttach() { 10 | publishState() 11 | } 12 | 13 | internal fun publishState() = DependencyManager.publishState(this) 14 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/SwitchPreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.Maxr1998.modernpreferences.preferences 18 | 19 | import de.Maxr1998.modernpreferences.R 20 | 21 | class SwitchPreference(key: String) : TwoStatePreference(key) { 22 | override fun getWidgetLayoutResource() = R.layout.map_preference_widget_switch 23 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/TwoStatePreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.Maxr1998.modernpreferences.preferences 18 | 19 | import android.content.Context 20 | import android.graphics.drawable.StateListDrawable 21 | import android.widget.CompoundButton 22 | import de.Maxr1998.modernpreferences.PreferencesAdapter 23 | import de.Maxr1998.modernpreferences.helpers.DISABLED_RESOURCE_ID 24 | 25 | @Suppress("MemberVisibilityCanBePrivate") 26 | abstract class TwoStatePreference(key: String) : StatefulPreference(key) { 27 | private var checkedInternal = false 28 | var checked: Boolean 29 | get() = checkedInternal 30 | set(value) { 31 | checkNotNull(parent) { 32 | "Setting the checked value before the preference was attached isn't supported. " + 33 | "Consider using `defaultValue` instead." 34 | } 35 | if (value != checkedInternal) { 36 | updateState(null, value) 37 | } 38 | } 39 | 40 | /** 41 | * The default value of this preference, when nothing was committed to storage yet 42 | */ 43 | var defaultValue = false 44 | var checkedChangeListener: OnCheckedChangeListener? = null 45 | 46 | var summaryOn: CharSequence? = null 47 | var summaryOnRes: Int = DISABLED_RESOURCE_ID 48 | 49 | /** 50 | * When set to true, dependents are disabled when this preference is checked, 51 | * and are enabled when it's not 52 | */ 53 | var disableDependents = false 54 | 55 | override val state: Boolean get() = checkedInternal xor disableDependents 56 | 57 | override fun onAttach() { 58 | checkedInternal = getBoolean(defaultValue) 59 | super.onAttach() 60 | } 61 | 62 | override fun resolveSummary(context: Context) = when { 63 | checkedInternal && summaryOnRes != DISABLED_RESOURCE_ID -> context.resources.getText(summaryOnRes) 64 | checkedInternal && summaryOn != null -> summaryOn 65 | else -> super.resolveSummary(context) 66 | } 67 | 68 | override fun bindViews(holder: PreferencesAdapter.ViewHolder) { 69 | super.bindViews(holder) 70 | updateButton(holder) 71 | } 72 | 73 | private fun updateState(holder: PreferencesAdapter.ViewHolder?, new: Boolean) { 74 | if (checkedChangeListener?.onCheckedChanged(this, holder, new) != false) { 75 | commitBoolean(new) 76 | checkedInternal = new // Update internal state 77 | if (holder != null) { 78 | if (summaryOnRes != DISABLED_RESOURCE_ID || summaryOn != null) { 79 | bindViews(holder) 80 | } else { 81 | updateButton(holder) 82 | } 83 | } else { 84 | requestRebind() 85 | } 86 | publishState() 87 | } 88 | } 89 | 90 | private fun updateButton(holder: PreferencesAdapter.ViewHolder) { 91 | holder.icon?.drawable?.apply { 92 | if (this is StateListDrawable) { 93 | state = when { 94 | checkedInternal -> intArrayOf(android.R.attr.state_checked) 95 | else -> IntArray(0) 96 | } 97 | } 98 | } 99 | (holder.widget as CompoundButton).isChecked = checkedInternal 100 | } 101 | 102 | override fun onClick(holder: PreferencesAdapter.ViewHolder) { 103 | updateState(holder, !checkedInternal) 104 | } 105 | 106 | fun interface OnCheckedChangeListener { 107 | /** 108 | * Notified when the [checked][TwoStatePreference.checked] state of the connected [TwoStatePreference] changes. 109 | * This is called before the change gets persisted and can be prevented by returning false. 110 | * 111 | * @param holder the [ViewHolder][PreferencesAdapter.ViewHolder] with the views of the Preference instance, 112 | * or null if the change didn't occur as part of a click event 113 | * @param checked the new state 114 | * 115 | * @return true to commit the new button state to [SharedPreferences][android.content.SharedPreferences] 116 | */ 117 | fun onCheckedChanged( 118 | preference: TwoStatePreference, 119 | holder: PreferencesAdapter.ViewHolder?, 120 | checked: Boolean, 121 | ): Boolean 122 | } 123 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/choice/AbstractChoiceDialogPreference.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.preferences.choice 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.lifecycle.LifecycleOwner 7 | import androidx.recyclerview.widget.LinearLayoutManager 8 | import androidx.recyclerview.widget.RecyclerView 9 | import de.Maxr1998.modernpreferences.helpers.DISABLED_RESOURCE_ID 10 | import de.Maxr1998.modernpreferences.preferences.DialogPreference 11 | 12 | abstract class AbstractChoiceDialogPreference( 13 | key: String, 14 | protected val items: List>, 15 | private val allowMultiSelect: Boolean, 16 | ) : DialogPreference(key) { 17 | 18 | internal var selectionAdapter: SelectionAdapter? = null 19 | 20 | var onItemClickListener: OnItemClickListener? = null 21 | 22 | /** 23 | * Whether the summary should be auto-generated from the current selection. 24 | * If true, [summary] and [summaryRes] are ignored. 25 | * 26 | * Default true, set to false to turn off this feature. 27 | */ 28 | var autoGeneratedSummary = true 29 | 30 | init { 31 | require(items.isNotEmpty()) { "Supplied list of items may not be empty!" } 32 | } 33 | 34 | override fun createDialog(context: Context): Dialog = Config.dialogBuilderFactory(context).apply { 35 | if (titleRes != DISABLED_RESOURCE_ID) setTitle(titleRes) else setTitle(title) 36 | val dialogContent = RecyclerView(context).apply { 37 | selectionAdapter = SelectionAdapter( 38 | this@AbstractChoiceDialogPreference, 39 | items, 40 | allowMultiSelect, 41 | ) 42 | adapter = selectionAdapter 43 | layoutManager = LinearLayoutManager(context) 44 | } 45 | setView(dialogContent) 46 | setCancelable(false) 47 | setPositiveButton(android.R.string.ok) { _, _ -> 48 | persistSelection() 49 | requestRebind() 50 | } 51 | setNegativeButton(android.R.string.cancel) { _, _ -> 52 | resetSelection() 53 | } 54 | }.create() 55 | 56 | internal fun shouldSelect(item: SelectionItem): Boolean { 57 | return onItemClickListener?.onItemSelected(item) ?: true 58 | } 59 | 60 | internal abstract fun select(item: SelectionItem) 61 | 62 | protected abstract fun persistSelection() 63 | 64 | protected abstract fun resetSelection() 65 | 66 | abstract fun isSelected(item: SelectionItem): Boolean 67 | 68 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 69 | super.onStateChanged(source, event) 70 | if (event == Lifecycle.Event.ON_DESTROY) { 71 | selectionAdapter = null 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/choice/AbstractSingleChoiceDialogPreference.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.preferences.choice 2 | 3 | import android.content.Context 4 | import de.Maxr1998.modernpreferences.helpers.DISABLED_RESOURCE_ID 5 | 6 | abstract class AbstractSingleChoiceDialogPreference( 7 | key: String, 8 | items: List>, 9 | ) : AbstractChoiceDialogPreference(key, items, false) { 10 | 11 | /** 12 | * The initial selection if no choice has been made and no value persisted 13 | * to [SharedPreferences][android.content.SharedPreferences] yet. 14 | * 15 | * Must match a [SelectionItem.key] in [items]. 16 | */ 17 | var initialSelection: T? = null 18 | 19 | var currentSelection: SelectionItem? = null 20 | internal set 21 | 22 | var selectionChangeListener: OnSelectionChangeListener? = null 23 | 24 | override fun onAttach() { 25 | super.onAttach() 26 | if (currentSelection == null) { 27 | resetSelection() 28 | } 29 | } 30 | 31 | override fun select(item: SelectionItem) { 32 | currentSelection = item 33 | selectionAdapter?.notifySelectionChanged() 34 | } 35 | 36 | override fun isSelected(item: SelectionItem): Boolean = item == currentSelection 37 | 38 | internal abstract fun saveKey(key: T) 39 | 40 | internal abstract fun loadKey(defaultValue: T?): T? 41 | 42 | override fun persistSelection() { 43 | currentSelection?.let { selection -> 44 | if (selectionChangeListener?.onSelectionChange(this, selection.key) != false) { 45 | saveKey(selection.key) 46 | } 47 | } 48 | } 49 | 50 | override fun resetSelection() { 51 | val persisted = loadKey(initialSelection) 52 | currentSelection = persisted?.let { items.find { item -> item.key == persisted } } 53 | selectionAdapter?.notifySelectionChanged() 54 | } 55 | 56 | override fun resolveSummary(context: Context): CharSequence? { 57 | val selection = currentSelection 58 | return when { 59 | autoGeneratedSummary && selection != null -> when { 60 | selection.titleRes != DISABLED_RESOURCE_ID -> context.resources.getText(selection.titleRes) 61 | else -> selection.title 62 | } 63 | else -> super.resolveSummary(context) 64 | } 65 | } 66 | 67 | fun interface OnSelectionChangeListener { 68 | /** 69 | * Notified when the selection of the connected [AbstractSingleChoiceDialogPreference] changes, 70 | * meaning after the user closes the dialog by pressing "ok". 71 | * This is called before the change gets persisted and can be prevented by returning false. 72 | * 73 | * @param selection the new selection 74 | * 75 | * @return true to commit the new selection to [SharedPreferences][android.content.SharedPreferences] 76 | */ 77 | fun onSelectionChange(preference: AbstractSingleChoiceDialogPreference, selection: T): Boolean 78 | } 79 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/choice/MultiChoiceDialogPreference.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.preferences.choice 2 | 3 | import android.content.Context 4 | import de.Maxr1998.modernpreferences.helpers.DISABLED_RESOURCE_ID 5 | 6 | class MultiChoiceDialogPreference( 7 | key: String, 8 | items: List>, 9 | ) : AbstractChoiceDialogPreference(key, items, true) { 10 | 11 | /** 12 | * The initial selections if no choice has been made yet and no value 13 | * was persisted to [SharedPreferences][android.content.SharedPreferences] 14 | */ 15 | var initialSelections: Set? = null 16 | 17 | private val selections: MutableSet> = HashSet() 18 | 19 | val currentSelections: Set> 20 | get() = HashSet(selections) 21 | 22 | var selectionChangeListener: OnSelectionChangeListener? = null 23 | 24 | override fun onAttach() { 25 | super.onAttach() 26 | if (selections.isEmpty()) { 27 | resetSelection() 28 | } 29 | } 30 | 31 | override fun select(item: SelectionItem) { 32 | if (!selections.add(item)) { 33 | selections.remove(item) 34 | } 35 | selectionAdapter?.notifySelectionChanged() 36 | } 37 | 38 | override fun isSelected(item: SelectionItem): Boolean = item in selections 39 | 40 | override fun persistSelection() { 41 | val resultSet = HashSet() 42 | selections.mapTo(resultSet, SelectionItem::key) 43 | if (selectionChangeListener?.onSelectionChange(this, HashSet(resultSet)) != false) { 44 | commitStringSet(resultSet) 45 | } 46 | } 47 | 48 | override fun resetSelection() { 49 | val persisted = getStringSet() ?: initialSelections?.toList() ?: emptyList() 50 | selections.clear() 51 | selections += persisted.mapNotNull { key -> 52 | items.find { item -> item.key == key } 53 | } 54 | selectionAdapter?.notifySelectionChanged() 55 | } 56 | 57 | override fun resolveSummary(context: Context): CharSequence? = when { 58 | autoGeneratedSummary && selections.isNotEmpty() -> { 59 | selections.joinToString(limit = 3, truncated = "…") { selection -> 60 | when { 61 | selection.titleRes != DISABLED_RESOURCE_ID -> context.resources.getText(selection.titleRes) 62 | else -> selection.title 63 | } 64 | } 65 | } 66 | else -> super.resolveSummary(context) 67 | } 68 | 69 | fun interface OnSelectionChangeListener { 70 | /** 71 | * Notified when the selection of the connected [MultiChoiceDialogPreference] changes, 72 | * meaning after the user closes the dialog by pressing "ok". 73 | * This is called before the change gets persisted and can be prevented by returning false. 74 | * 75 | * @param selection the new selection 76 | * 77 | * @return true to commit the new selection to [SharedPreferences][android.content.SharedPreferences] 78 | */ 79 | fun onSelectionChange(preference: MultiChoiceDialogPreference, selection: Set): Boolean 80 | } 81 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/choice/OnItemClickListener.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.preferences.choice 2 | 3 | fun interface OnItemClickListener { 4 | /** 5 | * Notified when the user clicks a [SelectionItem]. 6 | * This is called before the change gets persisted and can be prevented by returning false. 7 | * 8 | * @param item the clicked item 9 | * 10 | * @return true to to allow the selection of the item 11 | */ 12 | fun onItemSelected(item: SelectionItem): Boolean 13 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/choice/SelectionAdapter.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.preferences.choice 2 | 3 | import android.content.res.ColorStateList 4 | import android.graphics.Color 5 | import android.graphics.PorterDuff 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.CompoundButton 10 | import android.widget.TextView 11 | import androidx.core.content.res.use 12 | import androidx.core.view.isVisible 13 | import androidx.recyclerview.widget.RecyclerView 14 | import de.Maxr1998.modernpreferences.R 15 | import de.Maxr1998.modernpreferences.helpers.DISABLED_RESOURCE_ID 16 | 17 | internal class SelectionAdapter( 18 | private val preference: AbstractChoiceDialogPreference, 19 | private val items: List>, 20 | private val allowMultiSelect: Boolean, 21 | ) : RecyclerView.Adapter() { 22 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SelectionViewHolder { 23 | val layoutInflater = LayoutInflater.from(parent.context) 24 | val layout = when { 25 | allowMultiSelect -> R.layout.map_dialog_multi_choice_item 26 | else -> R.layout.map_dialog_single_choice_item 27 | } 28 | val view = layoutInflater.inflate(layout, parent, false) 29 | return SelectionViewHolder(view) 30 | } 31 | 32 | @Suppress("ComplexMethod", "NestedBlockDepth") 33 | override fun onBindViewHolder(holder: SelectionViewHolder, position: Int) { 34 | val item = items[position] 35 | holder.apply { 36 | selector.isChecked = preference.isSelected(item) 37 | title.apply { 38 | when { 39 | item.titleRes != DISABLED_RESOURCE_ID -> setText(item.titleRes) 40 | else -> text = item.title 41 | } 42 | } 43 | summary.apply { 44 | when { 45 | item.summaryRes != DISABLED_RESOURCE_ID -> setText(item.summaryRes) 46 | else -> text = item.summary 47 | } 48 | isVisible = item.summaryRes != DISABLED_RESOURCE_ID || item.summary != null 49 | } 50 | if (item.badgeInfo != null) { 51 | badge.apply { 52 | when { 53 | item.badgeInfo.textRes != DISABLED_RESOURCE_ID -> setText(item.badgeInfo.textRes) 54 | else -> text = item.badgeInfo.text 55 | } 56 | isVisible = item.badgeInfo.isVisible 57 | } 58 | setBadgeColor(item.badgeInfo.badgeColor) 59 | } else { 60 | badge.isVisible = false 61 | } 62 | itemView.setOnClickListener { 63 | if (preference.shouldSelect(item)) { 64 | preference.select(item) 65 | when { 66 | allowMultiSelect -> notifyItemChanged(position) 67 | else -> notifySelectionChanged() 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | override fun getItemCount(): Int = items.size 75 | 76 | fun notifySelectionChanged() { 77 | notifyItemRangeChanged(0, itemCount) 78 | } 79 | 80 | class SelectionViewHolder(view: View) : RecyclerView.ViewHolder(view) { 81 | val selector: CompoundButton = itemView.findViewById(R.id.map_selector) 82 | val title: TextView = itemView.findViewById(android.R.id.title) 83 | val summary: TextView = itemView.findViewById(android.R.id.summary) 84 | val badge: TextView = itemView.findViewById(R.id.badge) 85 | 86 | private val accentTextColor: ColorStateList 87 | 88 | init { 89 | // Apply accent text color via theme attribute from library or fallback to AppCompat 90 | val attrs = intArrayOf(R.attr.mapAccentTextColor, androidx.appcompat.R.attr.colorAccent) 91 | accentTextColor = itemView.context.theme.obtainStyledAttributes(attrs).use { array -> 92 | // Return first resolved attribute or null 93 | when { 94 | array.indexCount > 0 -> array.getColorStateList(array.getIndex(0)) 95 | else -> null 96 | } 97 | } ?: ColorStateList.valueOf(Color.BLACK) // fallback to black if no colorAccent is defined (unlikely) 98 | 99 | // Set initial badge color 100 | setBadgeColor(null) 101 | } 102 | 103 | internal fun setBadgeColor(color: ColorStateList?) { 104 | badge.apply { 105 | setTextColor(color ?: accentTextColor) 106 | backgroundTintList = color ?: accentTextColor 107 | backgroundTintMode = PorterDuff.Mode.SRC_ATOP 108 | } 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/choice/SelectionItem.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.preferences.choice 2 | 3 | import androidx.annotation.StringRes 4 | import de.Maxr1998.modernpreferences.helpers.DISABLED_RESOURCE_ID 5 | import de.Maxr1998.modernpreferences.preferences.Badge 6 | 7 | /** 8 | * Represents a selectable item in a selection dialog preference, 9 | * e.g. the [AbstractSingleChoiceDialogPreference] 10 | * 11 | * @param key The key of this item, will be committed to preferences if selected 12 | */ 13 | @Suppress("DataClassPrivateConstructor") 14 | data class SelectionItem private constructor( 15 | val key: T, 16 | @StringRes 17 | val titleRes: Int, 18 | val title: CharSequence, 19 | @StringRes 20 | val summaryRes: Int, 21 | val summary: CharSequence?, 22 | val badgeInfo: Badge?, 23 | ) { 24 | /** 25 | * @see SelectionItem 26 | */ 27 | constructor( 28 | key: T, 29 | @StringRes 30 | titleRes: Int, 31 | @StringRes 32 | summaryRes: Int = DISABLED_RESOURCE_ID, 33 | badgeInfo: Badge? = null, 34 | ) : this( 35 | key = key, 36 | titleRes = titleRes, 37 | title = "", 38 | summaryRes = summaryRes, 39 | summary = null, 40 | badgeInfo = badgeInfo, 41 | ) 42 | 43 | /** 44 | * @see SelectionItem 45 | */ 46 | constructor( 47 | key: T, 48 | title: CharSequence, 49 | summary: CharSequence? = null, 50 | badgeInfo: Badge? = null, 51 | ) : this( 52 | key = key, 53 | titleRes = DISABLED_RESOURCE_ID, 54 | title = title, 55 | summaryRes = DISABLED_RESOURCE_ID, 56 | summary = summary, 57 | badgeInfo = badgeInfo, 58 | ) 59 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/choice/SingleChoiceDialogPreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.Maxr1998.modernpreferences.preferences.choice 18 | 19 | class SingleChoiceDialogPreference( 20 | key: String, 21 | items: List>, 22 | ) : AbstractSingleChoiceDialogPreference(key, items) { 23 | override fun saveKey(key: String) { 24 | commitString(key) 25 | } 26 | 27 | override fun loadKey(defaultValue: String?): String? { 28 | return getString() ?: defaultValue 29 | } 30 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/preferences/choice/SingleIntChoiceDialogPreference.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package de.Maxr1998.modernpreferences.preferences.choice 18 | 19 | class SingleIntChoiceDialogPreference( 20 | key: String, 21 | items: List>, 22 | ) : AbstractSingleChoiceDialogPreference(key, items) { 23 | override fun saveKey(key: Int) { 24 | commitInt(key) 25 | } 26 | 27 | override fun loadKey(defaultValue: Int?): Int? { 28 | return if (hasValue()) getInt(0) else defaultValue 29 | } 30 | } -------------------------------------------------------------------------------- /library/src/main/java/de/Maxr1998/modernpreferences/views/ModernSeekBar.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.views 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.drawable.Drawable 6 | import android.util.AttributeSet 7 | import androidx.appcompat.widget.AppCompatSeekBar 8 | import androidx.core.content.ContextCompat 9 | import androidx.core.graphics.drawable.DrawableCompat 10 | import androidx.core.view.ViewCompat 11 | import de.Maxr1998.modernpreferences.R 12 | 13 | class ModernSeekBar( 14 | context: Context, 15 | attrs: AttributeSet?, 16 | defStyleAttr: Int, 17 | ) : AppCompatSeekBar(context, attrs, defStyleAttr) { 18 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, androidx.appcompat.R.attr.seekBarStyle) 19 | constructor(context: Context) : this(context, null) 20 | 21 | private var tickMarkDrawable: Drawable? = null 22 | set(value) { 23 | field?.callback = null 24 | field = value 25 | 26 | if (value != null) { 27 | value.callback = this 28 | DrawableCompat.setLayoutDirection(value, ViewCompat.getLayoutDirection(this)) 29 | if (value.isStateful) value.state = drawableState 30 | } 31 | 32 | invalidate() 33 | } 34 | 35 | private var defaultMarkerDrawable: Drawable? = null 36 | set(value) { 37 | field?.callback = null 38 | field = value 39 | 40 | if (value != null) { 41 | value.callback = this 42 | DrawableCompat.setLayoutDirection(value, ViewCompat.getLayoutDirection(this)) 43 | if (value.isStateful) value.state = drawableState 44 | } 45 | 46 | invalidate() 47 | } 48 | 49 | var hasTickMarks 50 | get() = tickMarkDrawable != null 51 | set(value) { 52 | tickMarkDrawable = when { 53 | value -> when (tickMarkDrawable) { 54 | null -> ContextCompat.getDrawable(context, R.drawable.map_seekbar_tick_mark) 55 | else -> tickMarkDrawable 56 | } 57 | else -> null 58 | } 59 | } 60 | 61 | var default: Int? = null 62 | set(value) { 63 | require(value == null || value in 0..max) { 64 | "Default must be in range 0 to max (is $value)" 65 | } 66 | field = value 67 | if (value != null) { 68 | if (defaultMarkerDrawable == null) { 69 | defaultMarkerDrawable = ContextCompat.getDrawable(context, R.drawable.map_seekbar_default_marker) 70 | } 71 | } else defaultMarkerDrawable = null 72 | } 73 | 74 | override fun onDraw(canvas: Canvas) { 75 | super.onDraw(canvas) 76 | drawTickMarks(canvas) 77 | drawDefaultMarker(canvas) 78 | } 79 | 80 | override fun drawableStateChanged() { 81 | super.drawableStateChanged() 82 | fun stateChanged(d: Drawable?) { 83 | if (d != null && d.isStateful && d.setState(drawableState)) { 84 | invalidateDrawable(d) 85 | } 86 | } 87 | stateChanged(tickMarkDrawable) 88 | stateChanged(defaultMarkerDrawable) 89 | } 90 | 91 | override fun jumpDrawablesToCurrentState() { 92 | super.jumpDrawablesToCurrentState() 93 | tickMarkDrawable?.jumpToCurrentState() 94 | defaultMarkerDrawable?.jumpToCurrentState() 95 | } 96 | 97 | private fun drawTickMarks(canvas: Canvas) { 98 | tickMarkDrawable?.let { tickMark -> 99 | val count = max 100 | if (count > 0) { 101 | val w = tickMark.intrinsicWidth 102 | val h = tickMark.intrinsicHeight 103 | val halfW = if (w >= 0) w / 2 else 1 104 | val halfH = if (h >= 0) h / 2 else 1 105 | tickMark.setBounds(-halfW, -halfH, halfW, halfH) 106 | val spacing: Float = (width - paddingLeft - paddingRight) / count.toFloat() 107 | val saveCount = canvas.save() 108 | canvas.translate(paddingLeft.toFloat(), height / 2.toFloat()) 109 | repeat(count + 1) { 110 | tickMark.draw(canvas) 111 | canvas.translate(spacing, 0f) 112 | } 113 | canvas.restoreToCount(saveCount) 114 | } 115 | } 116 | } 117 | 118 | private fun drawDefaultMarker(canvas: Canvas) { 119 | val default = default ?: return 120 | if (default == progress) return 121 | defaultMarkerDrawable?.let { defaultMarker -> 122 | val w: Int = defaultMarker.intrinsicWidth 123 | val h: Int = defaultMarker.intrinsicHeight 124 | val halfW = if (w >= 0) w / 2 else 1 125 | val halfH = if (h >= 0) h / 2 else 1 126 | defaultMarker.setBounds(-halfW, -halfH, halfW, halfH) 127 | val saveCount = canvas.save() 128 | val spacing: Float = default * (width - paddingLeft - paddingRight) / max.toFloat() 129 | canvas.translate(paddingLeft.toFloat() + spacing, height / 2.toFloat()) 130 | defaultMarker.draw(canvas) 131 | canvas.restoreToCount(saveCount) 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /library/src/main/res/drawable/map_badge_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/map_collapse_to_expand_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/map_expand_animated_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 14 | 18 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/map_expand_to_collapse_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/map_ic_collapse_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/map_ic_expand_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/map_scrim.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/map_seekbar_default_marker.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /library/src/main/res/drawable/map_seekbar_tick_mark.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /library/src/main/res/layout/map_accent_button_preference.xml: -------------------------------------------------------------------------------- 1 | 3 | 16 | 17 | 30 | 31 | 37 | 38 | 39 | 55 | -------------------------------------------------------------------------------- /library/src/main/res/layout/map_dialog_multi_choice_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 24 | 25 | 40 | 41 | 59 | 60 | 75 | -------------------------------------------------------------------------------- /library/src/main/res/layout/map_dialog_single_choice_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 24 | 25 | 40 | 41 | 59 | 60 | 75 | -------------------------------------------------------------------------------- /library/src/main/res/layout/map_image_preference.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 22 | 23 | 35 | 36 | 48 | 49 | 62 | -------------------------------------------------------------------------------- /library/src/main/res/layout/map_preference.xml: -------------------------------------------------------------------------------- 1 | 3 | 15 | 16 | 24 | 25 | 38 | 39 | 45 | 46 | 47 | 63 | 64 | 78 | 79 | 96 | 97 | 108 | -------------------------------------------------------------------------------- /library/src/main/res/layout/map_preference_category.xml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 15 | 25 | 26 | 31 | 32 | 38 | -------------------------------------------------------------------------------- /library/src/main/res/layout/map_preference_expand_text.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/res/layout/map_preference_widget_checkbox.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/res/layout/map_preference_widget_expand_arrow.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/res/layout/map_preference_widget_seekbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 18 | 19 | 30 | -------------------------------------------------------------------------------- /library/src/main/res/layout/map_preference_widget_seekbar_stub.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/res/layout/map_preference_widget_switch.xml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 15 | -------------------------------------------------------------------------------- /library/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /library/src/main/res/values/public.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Advanced 4 | M7.41,8.59 12,13.17 16.59,8.59 18,10 12,16 6,10z 5 | M6,14 12,8 18,14 16.59,15.41 12,10.83 7.41,15.41z 6 | -------------------------------------------------------------------------------- /library/src/main/res/values/values.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | #0c000000 6 | -------------------------------------------------------------------------------- /library/src/test/java/de/Maxr1998/modernpreferences/testing/PreferencesTests.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.testing 2 | 3 | import android.content.Context 4 | import de.Maxr1998.modernpreferences.Preference 5 | import de.Maxr1998.modernpreferences.PreferenceScreen 6 | import de.Maxr1998.modernpreferences.PreferencesAdapter 7 | import de.Maxr1998.modernpreferences.helpers.pref 8 | import de.Maxr1998.modernpreferences.helpers.screen 9 | import de.Maxr1998.modernpreferences.helpers.subScreen 10 | import de.Maxr1998.modernpreferences.helpers.switch 11 | import de.Maxr1998.modernpreferences.preferences.SwitchPreference 12 | import de.Maxr1998.modernpreferences.preferences.TwoStatePreference 13 | import io.kotest.assertions.throwables.shouldThrow 14 | import io.kotest.data.blocking.forAll 15 | import io.kotest.data.row 16 | import io.kotest.matchers.booleans.shouldBeTrue 17 | import io.kotest.matchers.shouldBe 18 | import io.kotest.property.Exhaustive 19 | import io.kotest.property.checkAll 20 | import io.kotest.property.exhaustive.boolean 21 | import io.mockk.every 22 | import io.mockk.mockk 23 | import io.mockk.spyk 24 | import io.mockk.verify 25 | import kotlinx.coroutines.runBlocking 26 | import org.junit.jupiter.api.BeforeAll 27 | import org.junit.jupiter.api.Test 28 | import org.junit.jupiter.api.TestInstance 29 | 30 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 31 | class PreferencesTests { 32 | 33 | private val contextMock: Context = mockk() 34 | 35 | @BeforeAll 36 | fun setup() { 37 | // Setup mocks for SharedPreferences 38 | val sharedPreferences = SharedPreferencesMock() 39 | every { contextMock.packageName } returns "package" 40 | every { contextMock.getSharedPreferences(any(), any()) } returns sharedPreferences 41 | } 42 | 43 | @Test 44 | fun `Non-persistent preferences should return default value or null for all get operations`() { 45 | runBlocking { 46 | val pref = Preference(uniqueKeySequence.next()).apply { persistent = false } 47 | checkAll(10) { value -> 48 | pref.getInt(value) shouldBe value 49 | } 50 | checkAll(2, Exhaustive.boolean()) { value -> 51 | pref.getBoolean(value) shouldBe value 52 | } 53 | pref.getString() shouldBe null 54 | pref.getStringSet() shouldBe null 55 | } 56 | } 57 | 58 | @Test 59 | fun `Setting checked before TwoStatePreference is attached should throw`() { 60 | shouldThrow { 61 | SwitchPreference(uniqueKeySequence.next()).apply { 62 | checked = true 63 | } 64 | } 65 | } 66 | 67 | @Test 68 | fun `TwoStatePreference should respect checked and disableDependents for state`() { 69 | runBlocking { 70 | forAll( 71 | // Basically a XOR, but written out for testing 72 | row(a = false, b = false, c = false), 73 | row(a = true, b = false, c = true), 74 | row(a = false, b = true, c = true), 75 | row(a = true, b = true, c = false), 76 | ) { checked: Boolean, disableDependents: Boolean, state: Boolean -> 77 | lateinit var pref: TwoStatePreference 78 | screen(contextMock) { 79 | pref = switch(uniqueKeySequence.next()) { 80 | this.disableDependents = disableDependents 81 | } 82 | } 83 | pref.checked = checked 84 | pref.state shouldBe state 85 | } 86 | } 87 | } 88 | 89 | private suspend fun check(dependent: Preference, dependency: TwoStatePreference) { 90 | // Check initial 91 | dependent.enabled shouldBe dependency.state 92 | 93 | // Check after changing the state in multiple iterations 94 | checkAll(4, Exhaustive.boolean()) { value -> 95 | dependency.checked = value 96 | dependent.enabled shouldBe dependency.state 97 | } 98 | } 99 | 100 | @Test 101 | fun `Dependents should properly follow their dependency's state`() { 102 | lateinit var dependent: Preference 103 | lateinit var dependency: TwoStatePreference 104 | 105 | runBlocking { 106 | checkAll(2, Exhaustive.boolean()) { disableDependents -> 107 | screen(contextMock) { 108 | val dependencyKey = uniqueKeySequence.next() 109 | dependent = pref(uniqueKeySequence.next()) { 110 | this.dependency = dependencyKey 111 | } 112 | dependency = switch(dependencyKey) { 113 | this.disableDependents = disableDependents 114 | } 115 | } 116 | check(dependent, dependency) 117 | 118 | // With inverted order of dependent and dependency 119 | screen(contextMock) { 120 | val dependencyKey = uniqueKeySequence.next() 121 | dependency = switch(dependencyKey) { 122 | this.disableDependents = disableDependents 123 | } 124 | dependent = pref(uniqueKeySequence.next()) { 125 | this.dependency = dependencyKey 126 | } 127 | } 128 | check(dependent, dependency) 129 | 130 | // With sub-screens 131 | screen(contextMock) { 132 | val dependencyKey = uniqueKeySequence.next() 133 | dependent = pref(uniqueKeySequence.next()) { 134 | this.dependency = dependencyKey 135 | } 136 | subScreen { 137 | dependency = switch(dependencyKey) { 138 | this.disableDependents = disableDependents 139 | } 140 | } 141 | } 142 | check(dependent, dependency) 143 | 144 | screen(contextMock) { 145 | val dependencyKey = uniqueKeySequence.next() 146 | subScreen { 147 | dependent = pref(uniqueKeySequence.next()) { 148 | this.dependency = dependencyKey 149 | } 150 | } 151 | dependency = switch(dependencyKey) { 152 | this.disableDependents = disableDependents 153 | } 154 | } 155 | check(dependent, dependency) 156 | 157 | screen(contextMock) { 158 | val dependencyKey = uniqueKeySequence.next() 159 | dependency = switch(dependencyKey) { 160 | this.disableDependents = disableDependents 161 | } 162 | subScreen { 163 | dependent = pref(uniqueKeySequence.next()) { 164 | this.dependency = dependencyKey 165 | } 166 | } 167 | } 168 | check(dependent, dependency) 169 | 170 | screen(contextMock) { 171 | val dependencyKey = uniqueKeySequence.next() 172 | subScreen { 173 | dependency = switch(dependencyKey) { 174 | this.disableDependents = disableDependents 175 | } 176 | } 177 | dependent = pref(uniqueKeySequence.next()) { 178 | this.dependency = dependencyKey 179 | } 180 | } 181 | check(dependent, dependency) 182 | } 183 | } 184 | } 185 | 186 | @Test 187 | fun `Screen changes should call screen change listeners`() { 188 | val adapter = createPreferenceAdapter() 189 | 190 | // Setup screens 191 | lateinit var subScreen: PreferenceScreen 192 | val rootScreen = screen(contextMock) { 193 | subScreen = +PreferenceScreen.Builder(this, "").build() 194 | } 195 | adapter.setRootScreen(rootScreen) 196 | 197 | // Setup listeners 198 | val beforeChangeListener = spyk { 199 | every { beforeScreenChange(any()) } returns true 200 | } 201 | adapter.beforeScreenChangeListener = beforeChangeListener 202 | val onChangeListener: PreferencesAdapter.OnScreenChangeListener = spyk() 203 | adapter.onScreenChangeListener = onChangeListener 204 | 205 | // Initial state dispatch 206 | verify(exactly = 1) { onChangeListener.onScreenChanged(rootScreen, false) } 207 | 208 | // Dispatch on screen change 209 | adapter.openScreen(subScreen) 210 | verify(exactly = 1) { beforeChangeListener.beforeScreenChange(subScreen) } 211 | verify(exactly = 1) { onChangeListener.onScreenChanged(subScreen, true) } 212 | 213 | // Dispatch when returning 214 | adapter.goBack() 215 | verify(exactly = 1) { beforeChangeListener.beforeScreenChange(rootScreen) } 216 | verify(exactly = 2) { onChangeListener.onScreenChanged(rootScreen, false) } 217 | } 218 | 219 | @Test 220 | fun `beforeScreenChangeListener can prevent screen changes`() { 221 | val adapter = createPreferenceAdapter() 222 | 223 | // Setup screens 224 | lateinit var subScreen: PreferenceScreen 225 | val rootScreen = screen(contextMock) { 226 | subScreen = +PreferenceScreen.Builder(this, "").build() 227 | } 228 | adapter.setRootScreen(rootScreen) 229 | 230 | val beforeChangeListener = spyk { 231 | every { beforeScreenChange(any()) } returns false 232 | } 233 | adapter.beforeScreenChangeListener = beforeChangeListener 234 | 235 | // Listener should prevent changes 236 | adapter.openScreen(subScreen) 237 | verify(exactly = 1) { beforeChangeListener.beforeScreenChange(subScreen) } 238 | adapter.currentScreen shouldBe rootScreen // Ensure screen didn't change 239 | 240 | // Temporarily remove listener to change screen 241 | adapter.beforeScreenChangeListener = null 242 | adapter.openScreen(subScreen) 243 | adapter.beforeScreenChangeListener = beforeChangeListener 244 | 245 | // Dispatch when returning 246 | adapter.goBack() 247 | verify(exactly = 1) { beforeChangeListener.beforeScreenChange(rootScreen) } 248 | adapter.currentScreen shouldBe subScreen // Ensure screen didn't change 249 | } 250 | 251 | @Test 252 | fun `Saved state should be empty for adapter without content`() { 253 | val adapter = createPreferenceAdapter() 254 | adapter.getSavedState().screenPath.size shouldBe 0 255 | } 256 | 257 | @Test 258 | fun `Saved state should be empty on root screen`() { 259 | val adapter = createPreferenceAdapter() 260 | adapter.setRootScreen(screen(contextMock) {}) 261 | adapter.getSavedState().screenPath.size shouldBe 0 262 | } 263 | 264 | @Test 265 | fun `Saved state should save and restore properly`() { 266 | val adapter = createPreferenceAdapter() 267 | 268 | // Setup screens 269 | lateinit var subScreen: PreferenceScreen 270 | val rootScreen = screen(contextMock) { 271 | subScreen = +PreferenceScreen.Builder(this, "").build() 272 | } 273 | adapter.setRootScreen(rootScreen) 274 | adapter.openScreen(subScreen) 275 | 276 | // Save state and assert correct size and content 277 | val savedState = adapter.getSavedState() 278 | savedState.screenPath.size shouldBe 1 279 | savedState.screenPath[0] shouldBe 0 280 | 281 | // Go back to root 282 | @Suppress("ControlFlowWithEmptyBody") 283 | while (adapter.goBack()); 284 | 285 | // Restore state 286 | adapter.loadSavedState(savedState).shouldBeTrue() 287 | 288 | // Assert current screen 289 | adapter.currentScreen shouldBe subScreen 290 | } 291 | } -------------------------------------------------------------------------------- /library/src/test/java/de/Maxr1998/modernpreferences/testing/SharedPreferencesMock.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Taken from https://github.com/IvanShafran/shared-preferences-mock 3 | * 4 | * Copyright (c) 2019 Ivan Shafran 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package de.Maxr1998.modernpreferences.testing 25 | 26 | import android.content.SharedPreferences 27 | import android.content.SharedPreferences.Editor 28 | import android.content.SharedPreferences.OnSharedPreferenceChangeListener 29 | 30 | class SharedPreferencesMock : SharedPreferences { 31 | private val preferencesMap: MutableMap = HashMap() 32 | private val listeners: MutableSet = HashSet() 33 | override fun getAll(): Map { 34 | return HashMap(preferencesMap) 35 | } 36 | 37 | override fun getString(key: String, defValue: String?): String? { 38 | val string = preferencesMap[key] as String? 39 | return string ?: defValue 40 | } 41 | 42 | @Suppress("UNCHECKED_CAST") 43 | override fun getStringSet(key: String, defValues: Set?): Set? { 44 | val stringSet = preferencesMap[key] as Set? 45 | return stringSet ?: defValues 46 | } 47 | 48 | override fun getInt(key: String, defValue: Int): Int { 49 | val integer = preferencesMap[key] as Int? 50 | return integer ?: defValue 51 | } 52 | 53 | override fun getLong(key: String, defValue: Long): Long { 54 | val longValue = preferencesMap[key] as Long? 55 | return longValue ?: defValue 56 | } 57 | 58 | override fun getFloat(key: String, defValue: Float): Float { 59 | val floatValue = preferencesMap[key] as Float? 60 | return floatValue ?: defValue 61 | } 62 | 63 | override fun getBoolean(key: String, defValue: Boolean): Boolean { 64 | val booleanValue = preferencesMap[key] as Boolean? 65 | return booleanValue ?: defValue 66 | } 67 | 68 | override fun contains(key: String): Boolean { 69 | return preferencesMap.containsKey(key) 70 | } 71 | 72 | override fun edit(): Editor { 73 | return EditorImpl() 74 | } 75 | 76 | override fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) { 77 | listeners.add(listener) 78 | } 79 | 80 | override fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) { 81 | listeners.remove(listener) 82 | } 83 | 84 | open inner class EditorImpl : Editor { 85 | private val newValuesMap: MutableMap = HashMap() 86 | private var shouldClear = false 87 | override fun putString(key: String, value: String?): Editor { 88 | newValuesMap[key] = value 89 | return this 90 | } 91 | 92 | override fun putStringSet(key: String, values: Set?): Editor { 93 | newValuesMap[key] = values?.let { HashSet(it) } 94 | return this 95 | } 96 | 97 | override fun putInt(key: String, value: Int): Editor { 98 | newValuesMap[key] = value 99 | return this 100 | } 101 | 102 | override fun putLong(key: String, value: Long): Editor { 103 | newValuesMap[key] = value 104 | return this 105 | } 106 | 107 | override fun putFloat(key: String, value: Float): Editor { 108 | newValuesMap[key] = value 109 | return this 110 | } 111 | 112 | override fun putBoolean(key: String, value: Boolean): Editor { 113 | newValuesMap[key] = value 114 | return this 115 | } 116 | 117 | override fun remove(key: String): Editor { 118 | // 'this' is marker for remove operation 119 | newValuesMap[key] = this 120 | return this 121 | } 122 | 123 | override fun clear(): Editor { 124 | shouldClear = true 125 | return this 126 | } 127 | 128 | override fun commit(): Boolean { 129 | apply() 130 | return true 131 | } 132 | 133 | override fun apply() { 134 | clearIfNeeded() 135 | val changedKeys = applyNewValues() 136 | notifyListeners(changedKeys) 137 | } 138 | 139 | private fun clearIfNeeded() { 140 | if (shouldClear) { 141 | shouldClear = false 142 | preferencesMap.clear() 143 | } 144 | } 145 | 146 | /** @return changed keys list 147 | */ 148 | private fun applyNewValues(): MutableList { 149 | val changedKeys: MutableList = ArrayList() 150 | for ((key, value) in newValuesMap) { 151 | val isSomethingChanged = if (isRemoveValue(value)) { 152 | removeIfNeeded(key) 153 | } else { 154 | putValueIfNeeded(key, value) 155 | } 156 | if (isSomethingChanged) { 157 | changedKeys.add(key) 158 | } 159 | } 160 | newValuesMap.clear() 161 | return changedKeys 162 | } 163 | 164 | private fun isRemoveValue(value: Any?): Boolean { 165 | // 'this' is marker for remove operation 166 | return value === this || value == null 167 | } 168 | 169 | /** @return true if element was removed 170 | */ 171 | private fun removeIfNeeded(key: String): Boolean { 172 | return if (preferencesMap.containsKey(key)) { 173 | preferencesMap.remove(key) 174 | true 175 | } else { 176 | false 177 | } 178 | } 179 | 180 | /** @return true if element was changed 181 | */ 182 | private fun putValueIfNeeded(key: String, value: Any?): Boolean { 183 | if (preferencesMap.containsKey(key)) { 184 | val oldValue = preferencesMap[key] 185 | if (value == oldValue) { 186 | return false 187 | } 188 | } 189 | preferencesMap[key] = value 190 | return true 191 | } 192 | 193 | private fun notifyListeners(changedKeys: MutableList) { 194 | for (i in changedKeys.indices.reversed()) { 195 | for (listener in listeners) { 196 | listener.onSharedPreferenceChanged(this@SharedPreferencesMock, changedKeys[i]) 197 | } 198 | } 199 | changedKeys.clear() 200 | } 201 | } 202 | } -------------------------------------------------------------------------------- /library/src/test/java/de/Maxr1998/modernpreferences/testing/TestHelpers.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.testing 2 | 3 | import de.Maxr1998.modernpreferences.PreferencesAdapter 4 | import io.mockk.every 5 | import io.mockk.spyk 6 | 7 | val uniqueKeySequence = iterator { 8 | var i = 0 9 | while (true) { 10 | yield("key_${i++}") 11 | } 12 | } 13 | 14 | fun createPreferenceAdapter(): PreferencesAdapter = spyk(PreferencesAdapter(hasStableIds = false)) { 15 | every { notifyDataSetChanged() } returns Unit 16 | } -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>Maxr1998-bot/.github//renovate-presets/gradle" 5 | ], 6 | "dependencyDashboard": true, 7 | "commitMessagePrefix": ":arrow_up:" 8 | } 9 | -------------------------------------------------------------------------------- /screenshots/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxr1998/ModernAndroidPreferences/30aa7cd8363842071df2446b1fa13aada69a0870/screenshots/screenshot_1.png -------------------------------------------------------------------------------- /screenshots/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxr1998/ModernAndroidPreferences/30aa7cd8363842071df2446b1fa13aada69a0870/screenshots/screenshot_2.png -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include(":library", ":testapp") 2 | 3 | rootProject.name = "ModernAndroidPreferences" 4 | 5 | pluginManagement { 6 | repositories { 7 | google() 8 | mavenCentral() 9 | gradlePluginPortal() 10 | } 11 | } -------------------------------------------------------------------------------- /testapp/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /testapp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | kotlin("android") 4 | } 5 | 6 | android { 7 | namespace = "de.Maxr1998.modernpreferences.example" 8 | compileSdk = 33 9 | defaultConfig { 10 | minSdk = 21 11 | targetSdk = 33 12 | versionCode = 1 13 | versionName = "1.0" 14 | } 15 | buildTypes { 16 | getByName("release") { 17 | isMinifyEnabled = false 18 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 19 | } 20 | } 21 | buildFeatures { 22 | viewBinding = true 23 | } 24 | lint { 25 | abortOnError = false 26 | } 27 | compileOptions { 28 | sourceCompatibility = JavaVersion.VERSION_11 29 | targetCompatibility = JavaVersion.VERSION_11 30 | } 31 | kotlinOptions { 32 | jvmTarget = JavaVersion.VERSION_11.toString() 33 | } 34 | } 35 | 36 | dependencies { 37 | // Core 38 | implementation(libs.androidx.core) 39 | implementation(libs.androidx.appcompat) 40 | implementation(libs.androidx.activity) 41 | 42 | // UI 43 | implementation(libs.androidx.recyclerview) 44 | implementation(libs.google.material) 45 | 46 | // Preferences library 47 | implementation(project(":library")) 48 | 49 | // Lifecycle 50 | implementation(libs.bundles.androidx.lifecycle) 51 | 52 | // Debug 53 | debugImplementation(libs.leakcanary) 54 | } -------------------------------------------------------------------------------- /testapp/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /testapp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /testapp/src/main/assets/earthview_6300.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxr1998/ModernAndroidPreferences/30aa7cd8363842071df2446b1fa13aada69a0870/testapp/src/main/assets/earthview_6300.jpg -------------------------------------------------------------------------------- /testapp/src/main/java/de/Maxr1998/modernpreferences/example/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package de.Maxr1998.modernpreferences.example 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import android.view.animation.AnimationUtils 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.recyclerview.widget.LinearLayoutManager 8 | import androidx.recyclerview.widget.RecyclerView 9 | import de.Maxr1998.modernpreferences.PreferenceScreen 10 | import de.Maxr1998.modernpreferences.PreferencesAdapter 11 | import de.Maxr1998.modernpreferences.example.databinding.ActivityMainBinding 12 | 13 | abstract class BaseActivity : AppCompatActivity(), PreferencesAdapter.OnScreenChangeListener { 14 | 15 | protected abstract val preferencesAdapter: PreferencesAdapter 16 | protected lateinit var preferencesView: RecyclerView 17 | private lateinit var binding: ActivityMainBinding 18 | private lateinit var layoutManager: LinearLayoutManager 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | binding = ActivityMainBinding.inflate(layoutInflater) 23 | setContentView(binding.root) 24 | 25 | setSupportActionBar(binding.toolbar) 26 | layoutManager = LinearLayoutManager(this) 27 | preferencesView = binding.recyclerView.apply { 28 | layoutManager = this@BaseActivity.layoutManager 29 | adapter = preferencesAdapter 30 | layoutAnimation = AnimationUtils.loadLayoutAnimation(this@BaseActivity, R.anim.preference_layout_fall_down) 31 | } 32 | } 33 | 34 | override fun onScreenChanged(screen: PreferenceScreen, subScreen: Boolean) { 35 | supportActionBar?.setDisplayHomeAsUpEnabled(subScreen) 36 | preferencesView.scheduleLayoutAnimation() 37 | screen["25"]?.let { pref -> 38 | val viewOffset = ((preferencesView.height - 64 * resources.displayMetrics.density) / 2).toInt() 39 | layoutManager.scrollToPositionWithOffset(pref.screenPosition, viewOffset) 40 | pref.requestRebindAndHighlight() 41 | } 42 | } 43 | 44 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 45 | return when (item.itemId) { 46 | android.R.id.home -> { 47 | onBackPressed() 48 | true 49 | } 50 | else -> super.onOptionsItemSelected(item) 51 | } 52 | } 53 | 54 | override fun onBackPressed() { 55 | if (!preferencesAdapter.goBack()) 56 | super.onBackPressed() 57 | } 58 | 59 | override fun onDestroy() { 60 | preferencesAdapter.onScreenChangeListener = null 61 | preferencesView.adapter = null 62 | super.onDestroy() 63 | } 64 | } -------------------------------------------------------------------------------- /testapp/src/main/java/de/Maxr1998/modernpreferences/example/Common.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * ModernAndroidPreferences Example Application 3 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package de.Maxr1998.modernpreferences.example 20 | 21 | import android.content.Context 22 | import android.content.res.ColorStateList 23 | import android.graphics.Color 24 | import android.graphics.drawable.BitmapDrawable 25 | import android.util.Log 26 | import android.widget.Toast 27 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 28 | import de.Maxr1998.modernpreferences.Preference 29 | import de.Maxr1998.modernpreferences.helpers.accentButtonPref 30 | import de.Maxr1998.modernpreferences.helpers.categoryHeader 31 | import de.Maxr1998.modernpreferences.helpers.checkBox 32 | import de.Maxr1998.modernpreferences.helpers.collapse 33 | import de.Maxr1998.modernpreferences.helpers.editText 34 | import de.Maxr1998.modernpreferences.helpers.expandText 35 | import de.Maxr1998.modernpreferences.helpers.image 36 | import de.Maxr1998.modernpreferences.helpers.multiChoice 37 | import de.Maxr1998.modernpreferences.helpers.pref 38 | import de.Maxr1998.modernpreferences.helpers.screen 39 | import de.Maxr1998.modernpreferences.helpers.seekBar 40 | import de.Maxr1998.modernpreferences.helpers.singleChoice 41 | import de.Maxr1998.modernpreferences.helpers.subScreen 42 | import de.Maxr1998.modernpreferences.helpers.switch 43 | import de.Maxr1998.modernpreferences.preferences.Badge 44 | import de.Maxr1998.modernpreferences.preferences.SeekBarPreference 45 | import de.Maxr1998.modernpreferences.preferences.choice.SelectionItem 46 | import java.util.Locale 47 | 48 | object Common { 49 | init { 50 | Preference.Config.dialogBuilderFactory = { context -> 51 | MaterialAlertDialogBuilder(context) 52 | } 53 | } 54 | 55 | @Suppress("LongMethod", "MagicNumber") 56 | fun createRootScreen(context: Context) = screen(context) { 57 | subScreen("types") { 58 | title = "Preference types" 59 | summary = "Overview over all the different preference items, with various widgets" 60 | iconRes = R.drawable.ic_apps_24dp 61 | centerIcon = false 62 | 63 | categoryHeader("header_plain") { 64 | title = "Plain" 65 | } 66 | pref("plain") { 67 | title = "A plain preference…" 68 | } 69 | pref("with-summary") { 70 | title = "…that doesn't have a widget" 71 | summary = "But a summary this time!" 72 | } 73 | pref("with-icon") { 74 | title = "There's also icon support, yay!" 75 | iconRes = R.drawable.ic_emoji_24dp 76 | } 77 | pref("with-badge") { 78 | title = "And badges!" 79 | badgeInfo = Badge("pro", ColorStateList.valueOf(Color.RED)) 80 | } 81 | accentButtonPref("accent-button") { 82 | title = "Button style".uppercase(Locale.getDefault()) 83 | } 84 | categoryHeader("header_two_state") { 85 | title = "Two state" 86 | } 87 | switch("switch") { 88 | title = "A simple switch" 89 | } 90 | pref("dependent") { 91 | title = "Toggle the switch above" 92 | dependency = "switch" 93 | clickListener = Preference.OnClickListener { _, holder -> 94 | Toast.makeText(holder.itemView.context, "Preference was clicked!", Toast.LENGTH_SHORT).show() 95 | false 96 | } 97 | } 98 | checkBox("checkbox") { 99 | title = "A checkbox" 100 | } 101 | categoryHeader("header_advanced") { 102 | title = "Advanced" 103 | } 104 | image("image-kotlin") { 105 | imageRes = R.drawable.ic_kotlin 106 | showScrim = false 107 | } 108 | image("image-earth") { 109 | title = "\u00A9 2019 DigitalGlobe" 110 | val imageStream = context.assets.open("earthview_6300.jpg") 111 | imageDrawable = BitmapDrawable.createFromStream(imageStream, null) 112 | } 113 | seekBar("seekbar") { 114 | title = "A seekbar" 115 | min = 1 116 | max = 100 117 | } 118 | seekBar("seekbar-stepped") { 119 | title = "A seekbar with steps" 120 | min = -100 121 | step = 10 122 | max = 100 123 | } 124 | seekBar("seekbar-ticks") { 125 | title = "A seekbar with tick marks" 126 | min = 1 127 | max = 10 128 | showTickMarks = true 129 | } 130 | seekBar("seekbar-default") { 131 | title = "A seekbar with a default value" 132 | min = 1 133 | max = 5 134 | default = 3 135 | 136 | // Callback listener 137 | seekListener = SeekBarPreference.OnSeekListener { _, _, i -> 138 | Log.d("Preferences", "SeekBar changed to $i") 139 | true 140 | } 141 | } 142 | +TestDialog().apply { 143 | title = "Show dialog" 144 | iconRes = R.drawable.ic_info_24dp 145 | } 146 | val selectableItems = listOf( 147 | SelectionItem("key_0", "Option 1", null), 148 | SelectionItem("key_1", "Option 2", "Second option"), 149 | SelectionItem("key_2", "Option 3", "You can put anything you want into this summary!"), 150 | SelectionItem("key_3", "Option 4", "Even supports badges!", Badge("pro")), 151 | ) 152 | singleChoice("single-choice-dialog", selectableItems) { 153 | title = "Single choice selection dialog" 154 | summary = "Only one item is selectable, de-selection is impossible" 155 | } 156 | multiChoice("multi-choice-dialog", selectableItems) { 157 | title = "Multi choice selection dialog" 158 | summary = "None, one or multiple items are selectable" 159 | } 160 | editText("edit-text") { 161 | title = "Text input" 162 | textInputHint = "Enter whatever you want!" 163 | } 164 | expandText("expand-text") { 165 | title = "Expandable text" 166 | text = "This is an example implementation of ModernAndroidPreferences, check out " + 167 | "the source on https://github.com/Maxr1998/ModernAndroidPreferences" 168 | } 169 | collapse { 170 | pref("collapsed_one") { 171 | title = "Collapsed by default" 172 | } 173 | pref("collapsed_two") { 174 | title = "Another preference" 175 | } 176 | pref("collapsed_three") { 177 | title = "A longer title to trigger ellipsize" 178 | } 179 | } 180 | } 181 | subScreen("list") { 182 | title = "Long list" 183 | summary = "A longer list to see how well library performs thanks to the backing RecyclerView" 184 | iconRes = R.drawable.ic_list_24dp 185 | collapseIcon = true 186 | 187 | for (i in 1..100) { 188 | pref(i.toString()) { 189 | title = "Preference item #$i" 190 | summary = "Lorem ipsum …" 191 | } 192 | } 193 | } 194 | } 195 | } -------------------------------------------------------------------------------- /testapp/src/main/java/de/Maxr1998/modernpreferences/example/TestActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * ModernAndroidPreferences Example Application 3 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package de.Maxr1998.modernpreferences.example 20 | 21 | import android.os.Bundle 22 | import de.Maxr1998.modernpreferences.PreferencesAdapter 23 | 24 | class TestActivity : BaseActivity() { 25 | 26 | override val preferencesAdapter = PreferencesAdapter() 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | 31 | preferencesAdapter.setRootScreen(Common.createRootScreen(this)) 32 | 33 | // Restore adapter state from saved state 34 | savedInstanceState?.getParcelable("adapter") 35 | ?.let(preferencesAdapter::loadSavedState) 36 | preferencesAdapter.onScreenChangeListener = this 37 | } 38 | 39 | override fun onSaveInstanceState(outState: Bundle) { 40 | super.onSaveInstanceState(outState) 41 | // Save the adapter state as a parcelable into the Android-managed instance state 42 | outState.putParcelable("adapter", preferencesAdapter.getSavedState()) 43 | } 44 | } -------------------------------------------------------------------------------- /testapp/src/main/java/de/Maxr1998/modernpreferences/example/TestDialog.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * ModernAndroidPreferences Example Application 3 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package de.Maxr1998.modernpreferences.example 20 | 21 | import android.app.Dialog 22 | import android.content.Context 23 | import de.Maxr1998.modernpreferences.preferences.DialogPreference 24 | 25 | class TestDialog : DialogPreference("dialog") { 26 | override fun createDialog(context: Context): Dialog = 27 | Config.dialogBuilderFactory(context) 28 | .setTitle("Info") 29 | .setMessage("You opened this dialog!") 30 | .setPositiveButton(android.R.string.ok, null) 31 | .create() 32 | } -------------------------------------------------------------------------------- /testapp/src/main/java/de/Maxr1998/modernpreferences/example/view_model/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * ModernAndroidPreferences Example Application 3 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package de.Maxr1998.modernpreferences.example.view_model 20 | 21 | import android.app.Application 22 | import androidx.lifecycle.AndroidViewModel 23 | import de.Maxr1998.modernpreferences.PreferencesAdapter 24 | import de.Maxr1998.modernpreferences.example.Common 25 | 26 | class MainViewModel(app: Application) : AndroidViewModel(app) { 27 | val preferencesAdapter = PreferencesAdapter(Common.createRootScreen(getApplication())) 28 | } -------------------------------------------------------------------------------- /testapp/src/main/java/de/Maxr1998/modernpreferences/example/view_model/ViewModelTestActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * ModernAndroidPreferences Example Application 3 | * Copyright (C) 2018 Max Rumpf alias Maxr1998 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package de.Maxr1998.modernpreferences.example.view_model 20 | 21 | import android.os.Bundle 22 | import androidx.activity.viewModels 23 | import de.Maxr1998.modernpreferences.example.BaseActivity 24 | 25 | class ViewModelTestActivity : BaseActivity() { 26 | 27 | private val viewModel: MainViewModel by viewModels() 28 | override val preferencesAdapter get() = viewModel.preferencesAdapter 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | 33 | preferencesAdapter.restoreAndObserveScrollPosition(preferencesView) 34 | onScreenChanged(preferencesAdapter.currentScreen, preferencesAdapter.isInSubScreen()) 35 | preferencesAdapter.onScreenChangeListener = this 36 | } 37 | } -------------------------------------------------------------------------------- /testapp/src/main/res/anim/preference_item_fall_down.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /testapp/src/main/res/anim/preference_layout_fall_down.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testapp/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /testapp/src/main/res/drawable/ic_apps_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /testapp/src/main/res/drawable/ic_emoji_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /testapp/src/main/res/drawable/ic_info_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /testapp/src/main/res/drawable/ic_kotlin.xml: -------------------------------------------------------------------------------- 1 | 7 | 14 | 21 | 27 | 28 | 34 | 37 | 40 | 43 | 46 | 49 | 50 | 51 | 52 | 58 | 59 | 65 | 68 | 71 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 93 | 96 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /testapp/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /testapp/src/main/res/drawable/ic_list_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /testapp/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 16 | 17 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /testapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testapp/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxr1998/ModernAndroidPreferences/30aa7cd8363842071df2446b1fa13aada69a0870/testapp/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /testapp/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxr1998/ModernAndroidPreferences/30aa7cd8363842071df2446b1fa13aada69a0870/testapp/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /testapp/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxr1998/ModernAndroidPreferences/30aa7cd8363842071df2446b1fa13aada69a0870/testapp/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /testapp/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxr1998/ModernAndroidPreferences/30aa7cd8363842071df2446b1fa13aada69a0870/testapp/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /testapp/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxr1998/ModernAndroidPreferences/30aa7cd8363842071df2446b1fa13aada69a0870/testapp/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /testapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxr1998/ModernAndroidPreferences/30aa7cd8363842071df2446b1fa13aada69a0870/testapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /testapp/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxr1998/ModernAndroidPreferences/30aa7cd8363842071df2446b1fa13aada69a0870/testapp/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /testapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxr1998/ModernAndroidPreferences/30aa7cd8363842071df2446b1fa13aada69a0870/testapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /testapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxr1998/ModernAndroidPreferences/30aa7cd8363842071df2446b1fa13aada69a0870/testapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /testapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxr1998/ModernAndroidPreferences/30aa7cd8363842071df2446b1fa13aada69a0870/testapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /testapp/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ModernPreferences Example 3 | ModernPreferences Example (+VM) 4 | -------------------------------------------------------------------------------- /testapp/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /testapp/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /testapp/src/main/res/xml/full_backup_content.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | --------------------------------------------------------------------------------