├── .editorconfig ├── .github └── workflows │ ├── build-and-test.yml │ ├── lint.yml │ ├── publish.yml │ └── spotless.yml ├── .gitignore ├── .idea ├── .gitignore └── compiler.xml ├── LICENSE ├── README.md ├── _publish.gradle ├── art └── revealswipe.gif ├── build.gradle ├── buildCompose.gradle ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── revealswipe ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ └── java │ └── de │ └── charlex │ └── compose │ └── RevealSwipe.kt ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── de │ └── charlex │ └── compose │ └── sample │ └── MainActivity.kt ├── settings.gradle └── spotless └── greclipse.properties /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | disabled_rules=no-wildcard-imports,import-ordering,indent 3 | indent_size=4 #Setting -> Editor -> Kotlin -> Tabs and Indents -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | paths-ignore: 7 | - 'README.md' 8 | branches: 9 | - develop 10 | - main 11 | 12 | jobs: 13 | debug_build: 14 | name: Debug build 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out code 18 | uses: actions/checkout@v3 19 | - name: Set up JDK 17 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: 17 23 | distribution: 'zulu' 24 | - name: Build 25 | run: ./gradlew assembleDebug 26 | 27 | release_build: 28 | name: Release build 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Check out code 32 | uses: actions/checkout@v3 33 | - name: Set up JDK 17 34 | uses: actions/setup-java@v2 35 | with: 36 | java-version: 17 37 | distribution: 'zulu' 38 | - name: Build 39 | run: ./gradlew assembleRelease 40 | 41 | tests: 42 | name: Tests 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Check out code 46 | uses: actions/checkout@v3 47 | - name: Set up JDK 17 48 | uses: actions/setup-java@v2 49 | with: 50 | java-version: 17 51 | distribution: 'zulu' 52 | - name: Unit tests 53 | run: ./gradlew testDebug --stacktrace 54 | 55 | - name: Upload testDebugUnitTest results 56 | uses: actions/upload-artifact@v4 57 | if: failure() 58 | with: 59 | name: testDebugUnitTest 60 | path: ./**/build/reports/tests/testDebugUnitTest -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | push: 6 | paths-ignore: 7 | - 'README.md' 8 | branches: 9 | - develop 10 | - main 11 | 12 | jobs: 13 | lint: 14 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]') && ! (contains(toJSON(github.event.commits.*.message), '[skip ') && contains(toJSON(github.event.commits.*.message), '#lint'))" 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up JDK 17 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: 17 23 | distribution: 'zulu' 24 | 25 | - name: Grant execute permission for gradlew 26 | run: chmod +x gradlew 27 | 28 | - name: Build with Gradle 29 | run: ./gradlew lint -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | # We'll run this workflow when a new GitHub release is created 6 | types: [released] 7 | 8 | jobs: 9 | publish: 10 | name: Release build and publish 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v3 15 | - name: Set up JDK 17 16 | uses: actions/setup-java@v2 17 | with: 18 | java-version: 17 19 | distribution: 'zulu' 20 | 21 | # Base64 decodes and pipes the GPG key content into the secret file 22 | - name: Prepare environment 23 | env: 24 | GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }} 25 | SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }} 26 | run: | 27 | git fetch --unshallow 28 | sudo bash -c "echo '$GPG_KEY_CONTENTS' | base64 -d > '$SIGNING_SECRET_KEY_RING_FILE'" 29 | 30 | # Builds the release artifacts of the library 31 | - name: Release build 32 | run: ./gradlew assembleRelease 33 | 34 | # Generates other artifacts 35 | - name: Source jar and dokka 36 | run: ./gradlew androidSourcesJar 37 | 38 | # Runs upload, and then closes & releases the repository 39 | - name: Publish to MavenCentral 40 | run: ./gradlew publishReleasePublicationToSonatypeRepository --max-workers 1 closeAndReleaseRepository 41 | env: 42 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 43 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 44 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} 45 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 46 | SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }} 47 | SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} -------------------------------------------------------------------------------- /.github/workflows/spotless.yml: -------------------------------------------------------------------------------- 1 | name: Spotless 2 | 3 | on: 4 | pull_request: 5 | push: 6 | paths-ignore: 7 | - 'README.md' 8 | branches: 9 | - develop 10 | - main 11 | 12 | jobs: 13 | spotless: 14 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]') && ! (contains(toJSON(github.event.commits.*.message), '[skip ') && contains(toJSON(github.event.commits.*.message), '#spotless'))" 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up JDK 17 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: 17 23 | distribution: 'zulu' 24 | 25 | - name: Grant execute permission for gradlew 26 | run: chmod +x gradlew 27 | 28 | - name: Build with Gradle 29 | run: ./gradlew spotlessCheck -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/android,windows,macos 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=android,windows,macos 4 | 5 | ### Android ### 6 | # Built application files 7 | *.apk 8 | *.aar 9 | *.ap_ 10 | *.aab 11 | 12 | # Files for the ART/Dalvik VM 13 | *.dex 14 | 15 | # Java class files 16 | *.class 17 | 18 | # Generated files 19 | bin/ 20 | gen/ 21 | out/ 22 | # Uncomment the following line in case you need and you don't have the release build type files in your app 23 | # release/ 24 | 25 | # Gradle files 26 | .gradle/ 27 | build/ 28 | 29 | # Local configuration file (sdk path, etc) 30 | local.properties 31 | 32 | # Proguard folder generated by Eclipse 33 | proguard/ 34 | 35 | # Log Files 36 | *.log 37 | 38 | # Android Studio Navigation editor temp files 39 | .navigation/ 40 | 41 | # Android Studio captures folder 42 | captures/ 43 | 44 | # IntelliJ 45 | *.iml 46 | .idea/workspace.xml 47 | .idea/tasks.xml 48 | .idea/gradle.xml 49 | .idea/assetWizardSettings.xml 50 | .idea/dictionaries 51 | .idea/libraries 52 | # Android Studio 3 in .gitignore file. 53 | .idea/caches 54 | .idea/modules.xml 55 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 56 | .idea/navEditor.xml 57 | 58 | # Keystore files 59 | # Uncomment the following lines if you do not want to check your keystore files in. 60 | #*.jks 61 | #*.keystore 62 | 63 | # External native build folder generated in Android Studio 2.2 and later 64 | .externalNativeBuild 65 | .cxx/ 66 | 67 | # Google Services (e.g. APIs or Firebase) 68 | # google-services.json 69 | 70 | # Freeline 71 | freeline.py 72 | freeline/ 73 | freeline_project_description.json 74 | 75 | # fastlane 76 | fastlane/report.xml 77 | fastlane/Preview.html 78 | fastlane/screenshots 79 | fastlane/test_output 80 | fastlane/readme.md 81 | 82 | # Version control 83 | vcs.xml 84 | 85 | # lint 86 | lint/intermediates/ 87 | lint/generated/ 88 | lint/outputs/ 89 | lint/tmp/ 90 | # lint/reports/ 91 | 92 | ### Android Patch ### 93 | gen-external-apklibs 94 | output.json 95 | 96 | # Replacement of .externalNativeBuild directories introduced 97 | # with Android Studio 3.5. 98 | 99 | ### macOS ### 100 | # General 101 | .DS_Store 102 | .AppleDouble 103 | .LSOverride 104 | 105 | # Icon must end with two \r 106 | Icon 107 | 108 | 109 | # Thumbnails 110 | ._* 111 | 112 | # Files that might appear in the root of a volume 113 | .DocumentRevisions-V100 114 | .fseventsd 115 | .Spotlight-V100 116 | .TemporaryItems 117 | .Trashes 118 | .VolumeIcon.icns 119 | .com.apple.timemachine.donotpresent 120 | 121 | # Directories potentially created on remote AFP share 122 | .AppleDB 123 | .AppleDesktop 124 | Network Trash Folder 125 | Temporary Items 126 | .apdisk 127 | 128 | ### Windows ### 129 | # Windows thumbnail cache files 130 | Thumbs.db 131 | Thumbs.db:encryptable 132 | ehthumbs.db 133 | ehthumbs_vista.db 134 | 135 | # Dump file 136 | *.stackdump 137 | 138 | # Folder config file 139 | [Dd]esktop.ini 140 | 141 | # Recycle Bin used on file shares 142 | $RECYCLE.BIN/ 143 | 144 | # Windows Installer files 145 | *.cab 146 | *.msi 147 | *.msix 148 | *.msm 149 | *.msp 150 | 151 | # Windows shortcuts 152 | *.lnk 153 | 154 | # End of https://www.toptal.com/developers/gitignore/api/android,windows,macos -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | # RevealSwipe 2 | Compose RevealSwipe (Material 3) 3 | 4 | Swipable in both directions 5 | 6 | Build and test 7 | Lint 8 | Spotless 9 | 10 | CodeFactor 11 | Maven Central 12 | 13 | 14 | # Add to your project 15 | 16 | Add actual RevealSwipe library: 17 | 18 | ```groovy 19 | dependencies { 20 | implementation 'de.charlex.compose:revealswipe:2.0.0-beta01' 21 | } 22 | ``` 23 | 24 | # How does it work? 25 | 26 | Surround your content with the RevealSwipe 27 | 28 | ```kotlin 29 | RevealSwipe( 30 | modifier = Modifier.padding(vertical = 5.dp), 31 | directions = setOf( 32 | // RevealDirection.StartToEnd, 33 | RevealDirection.EndToStart 34 | ), 35 | hiddenContentStart = { 36 | Icon( 37 | modifier = Modifier.padding(horizontal = 25.dp), 38 | imageVector = Icons.Outlined.Star, 39 | contentDescription = null, 40 | tint = Color.White 41 | ) 42 | }, 43 | hiddenContentEnd = { 44 | Icon( 45 | modifier = Modifier.padding(horizontal = 25.dp), 46 | imageVector = Icons.Outlined.Delete, 47 | contentDescription = null 48 | ) 49 | } 50 | ) { 51 | Card( 52 | modifier = Modifier.fillMaxSize().requiredHeight(80.dp), 53 | backgroundColor = Color(item.second), 54 | shape = it, 55 | ){ 56 | Text( 57 | modifier = Modifier.padding(start = 20.dp, top = 20.dp), 58 | text = item.first 59 | ) 60 | } 61 | } 62 | ``` 63 | 64 | # Preview 65 | 66 | ![RevealSwipe](https://github.com/ch4rl3x/RevealSwipe/blob/main/art/revealswipe.gif) 67 | 68 | 69 | That's it! 70 | 71 | License 72 | -------- 73 | 74 | Copyright 2021 Alexander Karkossa 75 | 76 | Licensed under the Apache License, Version 2.0 (the "License"); 77 | you may not use this file except in compliance with the License. 78 | You may obtain a copy of the License at 79 | 80 | http://www.apache.org/licenses/LICENSE-2.0 81 | 82 | Unless required by applicable law or agreed to in writing, software 83 | distributed under the License is distributed on an "AS IS" BASIS, 84 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 85 | See the License for the specific language governing permissions and 86 | limitations under the License. 87 | -------------------------------------------------------------------------------- /_publish.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | apply plugin: 'signing' 3 | 4 | task androidSourcesJar(type: Jar) { 5 | archiveClassifier.set('sources') 6 | if (project.plugins.findPlugin("com.android.library")) { 7 | // For Android libraries 8 | from android.sourceSets.main.java.srcDirs 9 | from android.sourceSets.main.kotlin.srcDirs 10 | } else { 11 | // For pure Kotlin libraries, in case you have them 12 | from sourceSets.main.java.srcDirs 13 | from sourceSets.main.kotlin.srcDirs 14 | } 15 | } 16 | 17 | artifacts { 18 | archives androidSourcesJar 19 | } 20 | 21 | group = PUBLISH_GROUP_ID 22 | version = PUBLISH_VERSION 23 | 24 | ext["signing.keyId"] = '' 25 | ext["signing.password"] = '' 26 | ext["signing.secretKeyRingFile"] = '' 27 | ext["ossrhUsername"] = '' 28 | ext["ossrhPassword"] = '' 29 | ext["sonatypeStagingProfileId"] = '' 30 | 31 | File secretPropsFile = project.rootProject.file('local.properties') 32 | if (secretPropsFile.exists()) { 33 | Properties p = new Properties() 34 | p.load(new FileInputStream(secretPropsFile)) 35 | p.each { name, value -> 36 | ext[name] = value 37 | } 38 | } else { 39 | ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') 40 | ext["signing.password"] = System.getenv('SIGNING_PASSWORD') 41 | ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE') 42 | ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') 43 | ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') 44 | ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') 45 | } 46 | 47 | nexusStaging { 48 | packageGroup = PUBLISH_GROUP_ID 49 | stagingProfileId = sonatypeStagingProfileId 50 | username = ossrhUsername 51 | password = ossrhPassword 52 | serverUrl = "https://s01.oss.sonatype.org/service/local/" 53 | } 54 | 55 | publishing { 56 | publications { 57 | release(MavenPublication) { 58 | // The coordinates of the library, being set from variables that 59 | // we'll set up later 60 | groupId PUBLISH_GROUP_ID 61 | artifactId PUBLISH_ARTIFACT_ID 62 | version PUBLISH_VERSION 63 | 64 | // Two artifacts, the `aar` (or `jar`) and the sources 65 | if (project.plugins.findPlugin("com.android.library")) { 66 | artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") 67 | } else { 68 | artifact("$buildDir/libs/${project.getName()}-${version}.jar") 69 | } 70 | artifact androidSourcesJar 71 | 72 | // Mostly self-explanatory metadata 73 | pom { 74 | name = PUBLISH_ARTIFACT_ID 75 | description = 'Jetpack Compose RevealSwipe' 76 | url = 'https://github.com/ch4rl3x/RevealSwipe' 77 | licenses { 78 | license { 79 | name = 'Apache-2.0 License' 80 | url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' 81 | } 82 | } 83 | developers { 84 | developer { 85 | id = 'ch4rl3x' 86 | name = 'Alexander Karkossa' 87 | email = 'alexander.karkossa@googlemail.com' 88 | } 89 | developer { 90 | id = 'kalinjul' 91 | name = 'Julian Kalinowski' 92 | email = 'j@kalinowski.email' 93 | } 94 | // Add all other devs here... 95 | } 96 | // Version control info - if you're using GitHub, follow the format as seen here 97 | scm { 98 | connection = 'scm:git:github.com/ch4rl3x/RevealSwipe.git' 99 | developerConnection = 'scm:git:ssh://github.com/ch4rl3x/RevealSwipe.git' 100 | url = 'https://github.com/ch4rl3x/RevealSwipe/tree/main' 101 | } 102 | // A slightly hacky fix so that your POM will include any transitive dependencies 103 | // that your library builds upon 104 | withXml { 105 | def dependenciesNode = asNode().appendNode('dependencies') 106 | 107 | project.configurations.implementation.allDependencies.each { 108 | def dependencyNode = dependenciesNode.appendNode('dependency') 109 | dependencyNode.appendNode('groupId', it.group) 110 | dependencyNode.appendNode('artifactId', it.name) 111 | dependencyNode.appendNode('version', it.version) 112 | } 113 | } 114 | } 115 | } 116 | } 117 | repositories { 118 | maven { 119 | name = "sonatype" 120 | 121 | def releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" 122 | def snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/" 123 | url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl 124 | 125 | credentials { 126 | username ossrhUsername 127 | password ossrhPassword 128 | } 129 | } 130 | } 131 | } 132 | 133 | 134 | signing { 135 | sign publishing.publications 136 | } 137 | -------------------------------------------------------------------------------- /art/revealswipe.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch4rl3x/RevealSwipe/cf93244e4bce17bdc5c08d617394d8a2e1ff26af/art/revealswipe.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | dependencies { 4 | classpath libs.kotlin.pluginGradle 5 | classpath libs.gradle 6 | } 7 | } 8 | 9 | plugins { 10 | alias libs.plugins.spotless apply false 11 | alias libs.plugins.nexus.staging apply true 12 | } 13 | 14 | subprojects { 15 | project.afterEvaluate { 16 | spotless { 17 | kotlin { 18 | target "/.kt" 19 | ktlint(libs.versions.ktlint.get()) 20 | } 21 | 22 | groovyGradle { 23 | target '/.gradle' 24 | greclipse().configFile(rootProject.file('spotless/greclipse.properties')) 25 | } 26 | } 27 | } 28 | } 29 | 30 | tasks.register('clean', Delete) { 31 | delete rootProject.layout.buildDir 32 | } -------------------------------------------------------------------------------- /buildCompose.gradle: -------------------------------------------------------------------------------- 1 | android { 2 | kotlinOptions { 3 | jvmTarget = '17' 4 | freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"] 5 | } 6 | buildFeatures { 7 | compose true 8 | } 9 | } 10 | 11 | dependencies { 12 | /** 13 | * Compose 14 | */ 15 | implementation(platform(libs.compose.bom)) 16 | androidTestImplementation(platform(libs.compose.bom)) 17 | implementation(libs.compose.ui.ui) 18 | implementation(libs.compose.foundation.foundation) 19 | implementation(libs.compose.ui.util) 20 | debugImplementation(libs.compose.ui.tooling) 21 | implementation(libs.compose.ui.tooling.preview) 22 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | javaVersion = "17" 3 | 4 | compileSdk = "35" 5 | gradleNexusStagingPlugin = "0.30.0" 6 | minSdk = "21" 7 | targetSdk = "34" 8 | ktlint = "0.42.1" 9 | 10 | composeBom = "2025.05.00" # https://developer.android.com/jetpack/compose/bom/bom-mapping 11 | kotlin = "2.1.20" # https://developer.android.com/jetpack/androidx/releases/compose-kotlin 12 | gradlePlugin = "8.7.3" # https://developer.android.com/build/releases/gradle-plugin 13 | 14 | foundation = "1.7.0-beta03" 15 | material3 = "1.3.0-beta03" 16 | 17 | activityComposeVersion = "1.10.1" 18 | lifecycleRuntimeKtxVersion = "2.8.5" 19 | 20 | spotless = "7.0.2" 21 | 22 | [libraries] 23 | compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } 24 | compose-foundation-foundation = { module = "androidx.compose.foundation:foundation" } 25 | compose-material3-material3 = { module = "androidx.compose.material3:material3" } 26 | compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } 27 | compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } 28 | compose-ui-ui = { module = "androidx.compose.ui:ui" } 29 | compose-ui-util = { module = "androidx.compose.ui:ui-util" } 30 | gradle = { module = "com.android.tools.build:gradle", version.ref = "gradlePlugin" } 31 | kotlin-pluginGradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 32 | kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } 33 | activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityComposeVersion" } 34 | lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtxVersion" } 35 | 36 | [plugins] 37 | spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } 38 | nexus-staging = { id = 'io.codearte.nexus-staging', version.ref = "gradleNexusStagingPlugin" } 39 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 40 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch4rl3x/RevealSwipe/cf93244e4bce17bdc5c08d617394d8a2e1ff26af/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Aug 22 22:07:44 CEST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /revealswipe/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /revealswipe/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | alias libs.plugins.spotless 5 | alias libs.plugins.compose.compiler 6 | } 7 | 8 | apply from: '../buildCompose.gradle' 9 | 10 | ext { 11 | PUBLISH_GROUP_ID = 'de.charlex.compose' 12 | PUBLISH_VERSION = '3.0.0' 13 | PUBLISH_ARTIFACT_ID = 'revealswipe' 14 | } 15 | 16 | apply from: '../_publish.gradle' 17 | 18 | android { 19 | namespace = "de.charlex.compose.revealswipe" 20 | compileSdk libs.versions.compileSdk.get().toInteger() 21 | 22 | defaultConfig { 23 | minSdk libs.versions.minSdk.get().toInteger() 24 | targetSdk libs.versions.targetSdk.get().toInteger() 25 | 26 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 27 | } 28 | 29 | buildFeatures { 30 | buildConfig = false 31 | } 32 | 33 | buildTypes { 34 | release { 35 | minifyEnabled false 36 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 37 | } 38 | } 39 | 40 | compileOptions { 41 | sourceCompatibility JavaVersion.toVersion(libs.versions.javaVersion.get()) 42 | targetCompatibility JavaVersion.toVersion(libs.versions.javaVersion.get()) 43 | } 44 | 45 | kotlinOptions { 46 | jvmTarget = JavaVersion.toVersion(libs.versions.javaVersion.get()) 47 | } 48 | } 49 | 50 | dependencies { 51 | implementation libs.kotlin.stdlib.jdk8 52 | } 53 | -------------------------------------------------------------------------------- /revealswipe/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 -------------------------------------------------------------------------------- /revealswipe/src/main/java/de/charlex/compose/RevealSwipe.kt: -------------------------------------------------------------------------------- 1 | package de.charlex.compose 2 | 3 | 4 | import androidx.compose.animation.core.CubicBezierEasing 5 | import androidx.compose.animation.core.Easing 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.LocalIndication 8 | import androidx.compose.foundation.background 9 | import androidx.compose.foundation.clickable 10 | import androidx.compose.foundation.combinedClickable 11 | import androidx.compose.foundation.gestures.AnchoredDraggableDefaults 12 | import androidx.compose.foundation.gestures.AnchoredDraggableState 13 | import androidx.compose.foundation.gestures.DraggableAnchors 14 | import androidx.compose.foundation.gestures.FlingBehavior 15 | import androidx.compose.foundation.gestures.Orientation 16 | import androidx.compose.foundation.gestures.anchoredDraggable 17 | import androidx.compose.foundation.gestures.animateTo 18 | import androidx.compose.foundation.gestures.awaitEachGesture 19 | import androidx.compose.foundation.gestures.awaitFirstDown 20 | import androidx.compose.foundation.gestures.snapTo 21 | import androidx.compose.foundation.interaction.MutableInteractionSource 22 | import androidx.compose.foundation.layout.Box 23 | import androidx.compose.foundation.layout.BoxScope 24 | import androidx.compose.foundation.layout.BoxWithConstraints 25 | import androidx.compose.foundation.layout.ColumnScope 26 | import androidx.compose.foundation.layout.fillMaxHeight 27 | import androidx.compose.foundation.layout.fillMaxSize 28 | import androidx.compose.foundation.layout.offset 29 | import androidx.compose.foundation.layout.width 30 | import androidx.compose.foundation.shape.CornerBasedShape 31 | import androidx.compose.foundation.shape.CornerSize 32 | import androidx.compose.runtime.Composable 33 | import androidx.compose.runtime.getValue 34 | import androidx.compose.runtime.mutableStateOf 35 | import androidx.compose.runtime.remember 36 | import androidx.compose.runtime.rememberCoroutineScope 37 | import androidx.compose.runtime.setValue 38 | import androidx.compose.ui.Alignment 39 | import androidx.compose.ui.Modifier 40 | import androidx.compose.ui.draw.alpha 41 | import androidx.compose.ui.geometry.Size 42 | import androidx.compose.ui.graphics.Color 43 | import androidx.compose.ui.graphics.Shape 44 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType 45 | import androidx.compose.ui.input.pointer.pointerInput 46 | import androidx.compose.ui.platform.LocalDensity 47 | import androidx.compose.ui.platform.LocalHapticFeedback 48 | import androidx.compose.ui.platform.LocalLayoutDirection 49 | import androidx.compose.ui.semantics.CustomAccessibilityAction 50 | import androidx.compose.ui.semantics.customActions 51 | import androidx.compose.ui.semantics.semantics 52 | import androidx.compose.ui.unit.Density 53 | import androidx.compose.ui.unit.Dp 54 | import androidx.compose.ui.unit.DpOffset 55 | import androidx.compose.ui.unit.IntOffset 56 | import androidx.compose.ui.unit.LayoutDirection 57 | import androidx.compose.ui.unit.dp 58 | import androidx.compose.ui.util.lerp 59 | import kotlinx.coroutines.CoroutineScope 60 | import kotlinx.coroutines.launch 61 | import kotlin.math.absoluteValue 62 | import kotlin.math.roundToInt 63 | 64 | 65 | @Composable 66 | fun RevealSwipe( 67 | modifier: Modifier = Modifier, 68 | enableSwipe: Boolean = true, 69 | onContentClick: (() -> Unit)? = null, 70 | onContentLongClick: ((DpOffset) -> Unit)? = null, 71 | backgroundStartActionLabel: String?, 72 | onBackgroundStartClick: () -> Boolean = { true }, 73 | backgroundEndActionLabel: String?, 74 | onBackgroundEndClick: () -> Boolean = { true }, 75 | closeOnContentClick: Boolean = true, 76 | closeOnBackgroundClick: Boolean = true, 77 | shape: CornerBasedShape, 78 | alphaEasing: Easing = CubicBezierEasing(0.4f, 0.4f, 0.17f, 0.9f), 79 | backgroundCardStartColor: Color, 80 | backgroundCardEndColor: Color, 81 | card: @Composable BoxScope.( 82 | shape: Shape, 83 | content: @Composable ColumnScope.() -> Unit 84 | ) -> Unit, 85 | coroutineScope: CoroutineScope = rememberCoroutineScope(), 86 | state: RevealState = rememberRevealState( 87 | maxRevealDp = 75.dp, 88 | directions = setOf( 89 | RevealDirection.StartToEnd, 90 | RevealDirection.EndToStart 91 | ) 92 | ), 93 | hiddenContentEnd: @Composable BoxScope.() -> Unit = {}, 94 | hiddenContentStart: @Composable BoxScope.() -> Unit = {}, 95 | content: @Composable (Shape) -> Unit 96 | ) { 97 | val closeOnContentClickHandler: () -> Unit = remember(coroutineScope, state) { 98 | { 99 | coroutineScope.launch { 100 | state.reset() 101 | } 102 | } 103 | } 104 | 105 | val backgroundStartClick = remember(coroutineScope, state, onBackgroundStartClick) { 106 | { 107 | if (closeOnBackgroundClick) { 108 | coroutineScope.launch { 109 | state.reset() 110 | } 111 | } 112 | onBackgroundStartClick() 113 | } 114 | } 115 | 116 | val backgroundEndClick = remember(coroutineScope, state, onBackgroundEndClick) { 117 | { 118 | if (closeOnBackgroundClick) { 119 | coroutineScope.launch { 120 | state.reset() 121 | } 122 | } 123 | onBackgroundEndClick() 124 | } 125 | } 126 | 127 | val hapticFeedback = LocalHapticFeedback.current 128 | var pressOffset by remember { mutableStateOf(DpOffset.Zero) } 129 | 130 | BaseRevealSwipe( 131 | modifier = modifier.semantics { 132 | customActions = buildList { 133 | backgroundStartActionLabel?.let { 134 | add( 135 | CustomAccessibilityAction( 136 | it, 137 | onBackgroundStartClick 138 | ) 139 | ) 140 | } 141 | backgroundEndActionLabel?.let { 142 | add( 143 | CustomAccessibilityAction( 144 | it, 145 | onBackgroundEndClick 146 | ) 147 | ) 148 | } 149 | } 150 | }, 151 | enableSwipe = enableSwipe, 152 | animateBackgroundCardColor = enableSwipe, 153 | shape = shape, 154 | alphaEasing = alphaEasing, 155 | backgroundCardStartColor = backgroundCardStartColor, 156 | backgroundCardEndColor = backgroundCardEndColor, 157 | card = card, 158 | state = state, 159 | hiddenContentEnd = { 160 | Box( 161 | modifier = Modifier 162 | .fillMaxSize() 163 | .clickable { 164 | backgroundEndClick() 165 | }, 166 | contentAlignment = Alignment.Center 167 | ) { 168 | hiddenContentEnd() 169 | } 170 | }, 171 | hiddenContentStart = { 172 | Box( 173 | modifier = Modifier 174 | .fillMaxSize() 175 | .clickable { 176 | backgroundStartClick() 177 | }, 178 | contentAlignment = Alignment.Center 179 | ) { 180 | hiddenContentStart() 181 | } 182 | }, 183 | content = { 184 | val clickableModifier = when { 185 | onContentClick != null && !closeOnContentClick -> { 186 | Modifier.combinedClickable( 187 | onClick = onContentClick, 188 | onLongClick = { 189 | onContentLongClick?.let { 190 | hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) 191 | it.invoke(pressOffset) 192 | } 193 | } 194 | ) 195 | } 196 | onContentClick == null && closeOnContentClick -> { 197 | // if no onContentClick handler passed, add click handler with no indication to enable close on content click 198 | Modifier.combinedClickable( 199 | onClick = closeOnContentClickHandler, 200 | onLongClick = { 201 | onContentLongClick?.let { 202 | hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) 203 | it.invoke(pressOffset) 204 | } 205 | }, 206 | indication = null, 207 | interactionSource = remember { MutableInteractionSource() } 208 | ) 209 | } 210 | onContentClick != null && closeOnContentClick -> { 211 | // decide based on state: 212 | // 1. if open, just close without indication 213 | // 2. if closed, call click handler 214 | Modifier.combinedClickable( 215 | onClick = 216 | { 217 | val isOpen = 218 | state.anchoredDraggableState.targetValue != RevealValue.Default 219 | // if open, just close. No click event. 220 | if (isOpen) { 221 | closeOnContentClickHandler() 222 | } else { 223 | onContentClick() 224 | } 225 | }, 226 | onLongClick = { 227 | onContentLongClick?.let { 228 | hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) 229 | it.invoke(pressOffset) 230 | } 231 | }, 232 | // no indication if just closing 233 | indication = if (state.anchoredDraggableState.targetValue != RevealValue.Default) null else LocalIndication.current, 234 | interactionSource = remember { MutableInteractionSource() } 235 | ) 236 | } 237 | else -> Modifier 238 | } 239 | 240 | Box( 241 | modifier = clickableModifier.pointerInput(true) { 242 | kotlinx.coroutines.coroutineScope { 243 | awaitEachGesture { 244 | val down = awaitFirstDown() 245 | pressOffset = DpOffset( 246 | down.position.x.toDp(), 247 | down.position.y.toDp() 248 | ) 249 | } 250 | } 251 | } 252 | ) { 253 | content(it) 254 | } 255 | } 256 | ) 257 | } 258 | 259 | @Composable 260 | fun BaseRevealSwipe( 261 | modifier: Modifier = Modifier, 262 | enableSwipe: Boolean = true, 263 | animateBackgroundCardColor: Boolean = true, 264 | shape: CornerBasedShape, 265 | alphaEasing: Easing = CubicBezierEasing(0.4f, 0.4f, 0.17f, 0.9f), 266 | backgroundCardStartColor: Color, 267 | backgroundCardEndColor: Color, 268 | card: @Composable BoxScope.( 269 | shape: Shape, 270 | content: @Composable ColumnScope.() -> Unit 271 | ) -> Unit, 272 | state: RevealState = rememberRevealState( 273 | maxRevealDp = 75.dp, 274 | directions = setOf( 275 | RevealDirection.StartToEnd, 276 | RevealDirection.EndToStart 277 | ) 278 | ), 279 | flingBehavior: FlingBehavior? = AnchoredDraggableDefaults.flingBehavior( 280 | state = state.anchoredDraggableState, 281 | positionalThreshold = { distance: Float -> distance * 0.5f }, 282 | animationSpec = tween() 283 | ), 284 | hiddenContentEnd: @Composable BoxScope.() -> Unit = {}, 285 | hiddenContentStart: @Composable BoxScope.() -> Unit = {}, 286 | content: @Composable BoxScope.(Shape) -> Unit 287 | ) { 288 | Box( 289 | modifier = modifier 290 | ) { 291 | var shapeSize: Size by remember { mutableStateOf(Size(0f, 0f)) } 292 | 293 | val density = LocalDensity.current 294 | 295 | val cornerRadiusBottomEnd = remember(shapeSize, density) { 296 | shape.bottomEnd.toPx( 297 | shapeSize = shapeSize, 298 | density = density 299 | ) 300 | } 301 | val cornerRadiusTopEnd = remember(shapeSize, density) { 302 | shape.topEnd.toPx( 303 | shapeSize = shapeSize, 304 | density = density 305 | ) 306 | } 307 | 308 | val cornerRadiusBottomStart = remember(shapeSize, density) { 309 | shape.bottomStart.toPx( 310 | shapeSize = shapeSize, 311 | density = density 312 | ) 313 | } 314 | val cornerRadiusTopStart = remember(shapeSize, density) { 315 | shape.topStart.toPx( 316 | shapeSize = shapeSize, 317 | density = density 318 | ) 319 | } 320 | 321 | val minDragAmountForStraightCorner = 322 | kotlin.math.max(cornerRadiusTopEnd, cornerRadiusBottomEnd) 323 | 324 | val cornerFactorEnd = 325 | (-state.anchoredDraggableState.offset / minDragAmountForStraightCorner).nonNaNorZero().coerceIn(0f, 1f).or(0f) { 326 | state.directions.contains(RevealDirection.EndToStart).not() 327 | } 328 | 329 | val cornerFactorStart = 330 | (state.anchoredDraggableState.offset / minDragAmountForStraightCorner).nonNaNorZero().coerceIn(0f, 1f).or(0f) { 331 | state.directions.contains(RevealDirection.StartToEnd).not() 332 | } 333 | 334 | val animatedCornerRadiusTopEnd: Float = lerp(cornerRadiusTopEnd, 0f, cornerFactorEnd) 335 | val animatedCornerRadiusBottomEnd: Float = lerp(cornerRadiusBottomEnd, 0f, cornerFactorEnd) 336 | 337 | val animatedCornerRadiusTopStart: Float = lerp(cornerRadiusTopStart, 0f, cornerFactorStart) 338 | val animatedCornerRadiusBottomStart: Float = lerp(cornerRadiusBottomStart, 0f, cornerFactorStart) 339 | 340 | val animatedShape = shape.copy( 341 | bottomStart = CornerSize(animatedCornerRadiusBottomStart), 342 | bottomEnd = CornerSize(animatedCornerRadiusBottomEnd), 343 | topStart = CornerSize(animatedCornerRadiusTopStart), 344 | topEnd = CornerSize(animatedCornerRadiusTopEnd) 345 | ) 346 | 347 | // alpha for background 348 | val maxRevealPx = with(LocalDensity.current) { state.maxRevealDp.toPx() } 349 | val draggedRatio = (state.anchoredDraggableState.offset.absoluteValue / maxRevealPx.absoluteValue).coerceIn(0f, 1f) 350 | 351 | // cubic parameters can be evaluated here https://cubic-bezier.com/ 352 | val alpha = alphaEasing.transform(draggedRatio) 353 | 354 | val animatedBackgroundEndColor = if (alpha in 0f..1f && animateBackgroundCardColor) backgroundCardEndColor.copy( 355 | alpha = alpha 356 | ) else backgroundCardEndColor 357 | 358 | val animatedBackgroundStartColor = if (alpha in 0f..1f && animateBackgroundCardColor) backgroundCardStartColor.copy( 359 | alpha = alpha 360 | ) else backgroundCardStartColor 361 | 362 | // non swipeable with hidden content 363 | card(shape) { 364 | Box( 365 | modifier = Modifier 366 | .fillMaxSize() 367 | .alpha(alpha) 368 | ) { 369 | val hasStartContent = state.directions.contains(RevealDirection.StartToEnd) 370 | val hasEndContent = state.directions.contains(RevealDirection.EndToStart) 371 | if (hasStartContent) { 372 | Box( 373 | modifier = Modifier 374 | .width(state.maxRevealDp) 375 | .align(Alignment.CenterStart) 376 | .fillMaxHeight() 377 | .background(animatedBackgroundStartColor), 378 | content = hiddenContentStart 379 | ) 380 | } 381 | if (hasEndContent) { 382 | Box( 383 | modifier = Modifier 384 | .width(state.maxRevealDp) 385 | .align(Alignment.CenterEnd) 386 | .fillMaxHeight() 387 | .background(animatedBackgroundEndColor), 388 | content = hiddenContentEnd 389 | ) 390 | } 391 | } 392 | } 393 | 394 | BoxWithConstraints( 395 | modifier = Modifier 396 | .then( 397 | if (enableSwipe) 398 | Modifier 399 | .offset { 400 | IntOffset( 401 | x = state.anchoredDraggableState 402 | .requireOffset() 403 | .roundToInt(), 404 | y = 0, 405 | ) 406 | } 407 | .anchoredDraggable( 408 | state = state.anchoredDraggableState, 409 | orientation = Orientation.Horizontal, 410 | enabled = true, // state.value == RevealValue.Default, 411 | reverseDirection = LocalLayoutDirection.current == LayoutDirection.Rtl, 412 | flingBehavior = flingBehavior 413 | ) 414 | else Modifier 415 | ) 416 | ) { 417 | shapeSize = Size(constraints.maxWidth.toFloat(), constraints.maxHeight.toFloat()) 418 | 419 | content(animatedShape) 420 | } 421 | 422 | // // This box is used to determine shape size. 423 | // // The box is sized to match it's parent, which in turn is sized according to its first child - the card. 424 | // BoxWithConstraints( 425 | // modifier = Modifier.matchParentSize() 426 | // ) { 427 | // } 428 | } 429 | } 430 | 431 | /** 432 | * Return an alternative value if whenClosure is true. Replaces if/else 433 | */ 434 | private fun T.or(orValue: T, whenClosure: T.() -> Boolean): T { 435 | return if (whenClosure()) orValue else this 436 | } 437 | 438 | private fun Float.nonNaNorZero() = if (isNaN()) 0f else this 439 | 440 | enum class RevealDirection { 441 | /** 442 | * Can be dismissed by swiping in the reading direction. 443 | */ 444 | StartToEnd, 445 | 446 | /** 447 | * Can be dismissed by swiping in the reverse of the reading direction. 448 | */ 449 | EndToStart 450 | } 451 | 452 | /** 453 | * Possible values of [RevealState]. 454 | */ 455 | enum class RevealValue { 456 | /** 457 | * Indicates the component has not been revealed yet. 458 | */ 459 | Default, 460 | 461 | /** 462 | * Fully revealed to end 463 | */ 464 | FullyRevealedEnd, 465 | 466 | /** 467 | * Fully revealed to start 468 | */ 469 | FullyRevealedStart, 470 | } 471 | 472 | /** 473 | * Create and [remember] a [RevealState] with the default animation clock. 474 | * 475 | */ 476 | @Composable 477 | fun rememberRevealState( 478 | maxRevealDp: Dp = 75.dp, 479 | directions: Set = setOf(RevealDirection.StartToEnd, RevealDirection.EndToStart), 480 | ): RevealState { 481 | val density = LocalDensity.current 482 | return remember { 483 | RevealState( 484 | maxRevealDp = maxRevealDp, 485 | directions = directions, 486 | density = density, 487 | ) 488 | } 489 | } 490 | 491 | data class RevealState( 492 | val maxRevealDp: Dp = 75.dp, 493 | val directions: Set, 494 | private val density: Density, 495 | private val initialValue: RevealValue = RevealValue.Default, 496 | ) { 497 | 498 | val anchoredDraggableState: AnchoredDraggableState = AnchoredDraggableState( 499 | initialValue = initialValue, 500 | anchors = DraggableAnchors { 501 | RevealValue.Default at 0f 502 | if (RevealDirection.StartToEnd in directions) RevealValue.FullyRevealedEnd at with(density) { maxRevealDp.toPx() } 503 | if (RevealDirection.EndToStart in directions) RevealValue.FullyRevealedStart at -with(density) { maxRevealDp.toPx() } 504 | }, 505 | ) 506 | } 507 | 508 | /** 509 | * Reset the component to the default position, with an animation. 510 | */ 511 | suspend fun RevealState.reset() { 512 | anchoredDraggableState.animateTo( 513 | targetValue = RevealValue.Default, 514 | ) 515 | } 516 | 517 | /** 518 | * Reset the component to the default position, with an animation. 519 | */ 520 | suspend fun RevealState.resetFast() { 521 | anchoredDraggableState.snapTo( 522 | targetValue = RevealValue.Default, 523 | ) 524 | } 525 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | alias libs.plugins.spotless 5 | alias libs.plugins.compose.compiler 6 | } 7 | 8 | android { 9 | namespace = "de.charlex.compose.revealswipe.sample" 10 | compileSdk libs.versions.compileSdk.get().toInteger() 11 | 12 | defaultConfig { 13 | minSdk libs.versions.minSdk.get().toInteger() 14 | targetSdk libs.versions.targetSdk.get().toInteger() 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildFeatures { 20 | buildConfig = false 21 | compose true 22 | } 23 | 24 | buildTypes { 25 | release { 26 | minifyEnabled false 27 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 28 | } 29 | } 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.toVersion(libs.versions.javaVersion.get()) 33 | targetCompatibility JavaVersion.toVersion(libs.versions.javaVersion.get()) 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = JavaVersion.toVersion(libs.versions.javaVersion.get()) 38 | } 39 | 40 | packagingOptions { 41 | resources { 42 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 43 | } 44 | } 45 | } 46 | 47 | dependencies { 48 | implementation project(':revealswipe') 49 | 50 | /** 51 | * Compose 52 | */ 53 | implementation(platform(libs.compose.bom)) 54 | androidTestImplementation(platform(libs.compose.bom)) 55 | 56 | implementation(libs.compose.ui.ui) 57 | implementation(libs.compose.foundation.foundation) 58 | implementation(libs.compose.ui.util) 59 | debugImplementation(libs.compose.ui.tooling) 60 | implementation(libs.compose.ui.tooling.preview) 61 | implementation(libs.compose.material3.material3) 62 | 63 | implementation libs.lifecycle.runtime.ktx 64 | implementation libs.activity.compose 65 | } -------------------------------------------------------------------------------- /sample/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 -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /sample/src/main/java/de/charlex/compose/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package de.charlex.compose.sample 2 | 3 | import android.os.Bundle 4 | import android.widget.Toast 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.height 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.width 16 | import androidx.compose.foundation.rememberScrollState 17 | import androidx.compose.foundation.text.BasicTextField 18 | import androidx.compose.foundation.verticalScroll 19 | import androidx.compose.material3.Button 20 | import androidx.compose.material3.Card 21 | import androidx.compose.material3.CardDefaults 22 | import androidx.compose.material3.IconButton 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.Surface 25 | import androidx.compose.material3.Text 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.mutableStateOf 28 | import androidx.compose.runtime.remember 29 | import androidx.compose.ui.Alignment 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.graphics.Color 32 | import androidx.compose.ui.platform.LocalContext 33 | import androidx.compose.ui.tooling.preview.Preview 34 | import androidx.compose.ui.unit.dp 35 | import de.charlex.compose.BaseRevealSwipe 36 | import de.charlex.compose.RevealDirection 37 | import de.charlex.compose.RevealSwipe 38 | import de.charlex.compose.rememberRevealState 39 | 40 | data class Item( 41 | val label: String, 42 | val color: Color, 43 | val directions: Set, 44 | val closeOnClick: Boolean = true 45 | ) 46 | 47 | class MainActivity : ComponentActivity() { 48 | override fun onCreate(savedInstanceState: Bundle?) { 49 | super.onCreate(savedInstanceState) 50 | setContent { 51 | MaterialTheme { 52 | // A surface container using the 'background' color from the theme 53 | Surface( 54 | modifier = Modifier.fillMaxSize(), 55 | color = MaterialTheme.colorScheme.background, 56 | contentColor = MaterialTheme.colorScheme.onPrimary 57 | ) { 58 | 59 | Column( 60 | modifier = Modifier 61 | .padding(16.dp) 62 | .verticalScroll(rememberScrollState()) 63 | ) { 64 | RevealSamples( 65 | items = listOf( 66 | Item( 67 | label = "Both directions", 68 | color = MaterialTheme.colorScheme.primary, 69 | directions = setOf( 70 | RevealDirection.StartToEnd, 71 | RevealDirection.EndToStart, 72 | ), 73 | ), 74 | Item( 75 | label = "Both directions, closeOnClick = false", 76 | color = MaterialTheme.colorScheme.secondary, 77 | directions = setOf( 78 | RevealDirection.StartToEnd, 79 | RevealDirection.EndToStart, 80 | ), 81 | closeOnClick = false, 82 | ), 83 | Item( 84 | label = "StartToEnd", 85 | color = MaterialTheme.colorScheme.tertiary, 86 | directions = setOf( 87 | RevealDirection.StartToEnd, 88 | ), 89 | ), 90 | Item( 91 | label = "EndToStart", 92 | color = MaterialTheme.colorScheme.primary, 93 | directions = setOf( 94 | RevealDirection.EndToStart, 95 | ), 96 | ) 97 | ) 98 | ) 99 | 100 | ComplexRevealSamples() 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | @Composable 109 | fun RevealSamples(items: List) { 110 | Column() { 111 | val context = LocalContext.current 112 | items.forEach { item -> 113 | RevealSwipe( 114 | modifier = Modifier.padding(vertical = 5.dp), 115 | state = rememberRevealState(directions = item.directions), 116 | hiddenContentStart = { 117 | SText() 118 | }, 119 | hiddenContentEnd = { 120 | TText() 121 | }, 122 | backgroundStartActionLabel = "Mark entry as favorite", 123 | backgroundEndActionLabel = "Delete entry", 124 | closeOnContentClick = item.closeOnClick, 125 | onContentClick = { 126 | Toast.makeText(context, "Test", Toast.LENGTH_SHORT).show() 127 | }, 128 | onContentLongClick = { offset -> 129 | Toast.makeText(context, "LongClick: $offset", Toast.LENGTH_SHORT).show() 130 | }, 131 | onBackgroundEndClick = { 132 | Toast.makeText(context, "End", Toast.LENGTH_SHORT).show() 133 | true 134 | }, onBackgroundStartClick = { 135 | Toast.makeText(context, "Start", Toast.LENGTH_SHORT).show() 136 | true 137 | }, 138 | backgroundCardEndColor = MaterialTheme.colorScheme.secondaryContainer, 139 | backgroundCardStartColor = MaterialTheme.colorScheme.tertiaryContainer, 140 | shape = MaterialTheme.shapes.medium, 141 | card = { shape, content -> 142 | Card( 143 | modifier = Modifier.matchParentSize(), 144 | colors = CardDefaults.cardColors( 145 | contentColor = MaterialTheme.colorScheme.onSecondary, 146 | containerColor = Color.Transparent 147 | ), 148 | shape = shape, 149 | content = content 150 | ) 151 | } 152 | ) { 153 | Card( 154 | colors = CardDefaults.cardColors( 155 | containerColor = item.color 156 | ), 157 | shape = it, 158 | ){ 159 | Box( 160 | modifier = Modifier 161 | .height(80.dp) 162 | .fillMaxWidth(), 163 | contentAlignment = Alignment.CenterStart 164 | ) { 165 | Text( 166 | modifier = Modifier.padding(start = 20.dp), 167 | text = item.label 168 | ) 169 | } 170 | } 171 | } 172 | } 173 | } 174 | } 175 | 176 | @Composable 177 | fun ComplexRevealSamples() { 178 | Column() { 179 | BaseRevealSwipe() 180 | TextFieldRevealSwipe() 181 | ButtonRevealSwipe() 182 | ButtonRevealSwipe2() 183 | ContentClickRevealSwipe() 184 | } 185 | } 186 | 187 | @Composable 188 | private fun BaseRevealSwipe() { 189 | val context = LocalContext.current 190 | 191 | BaseRevealSwipe( 192 | modifier = Modifier.padding(vertical = 5.dp), 193 | hiddenContentEnd = { 194 | Row( 195 | // modifier = Modifier.fillMaxSize(), 196 | verticalAlignment = Alignment.CenterVertically 197 | ) { 198 | IconButton( 199 | modifier = Modifier 200 | .fillMaxSize() 201 | .weight(1f), 202 | onClick = { 203 | Toast.makeText(context, "S", Toast.LENGTH_SHORT).show() 204 | } 205 | ) { 206 | SText() 207 | } 208 | IconButton( 209 | modifier = Modifier 210 | .fillMaxSize() 211 | .weight(1f), 212 | onClick = { 213 | Toast.makeText(context, "T", Toast.LENGTH_SHORT).show() 214 | } 215 | ) { 216 | TText() 217 | } 218 | } 219 | }, 220 | state = rememberRevealState( 221 | maxRevealDp = 150.dp, 222 | directions = setOf( 223 | RevealDirection.EndToStart, 224 | ) 225 | ), 226 | backgroundCardEndColor = MaterialTheme.colorScheme.secondaryContainer, 227 | backgroundCardStartColor = MaterialTheme.colorScheme.tertiaryContainer, 228 | shape = MaterialTheme.shapes.medium, 229 | card = { shape, content -> 230 | Card( 231 | modifier = Modifier.matchParentSize(), 232 | colors = CardDefaults.cardColors( 233 | contentColor = MaterialTheme.colorScheme.onSecondary, 234 | containerColor = Color.Transparent 235 | ), 236 | shape = shape, 237 | content = content 238 | ) 239 | } 240 | ) { 241 | Card( 242 | colors = CardDefaults.cardColors( 243 | contentColor = MaterialTheme.colorScheme.primary, 244 | containerColor = MaterialTheme.colorScheme.onPrimary 245 | ), 246 | elevation = CardDefaults.elevatedCardElevation(), 247 | shape = it 248 | ) { 249 | Box( 250 | modifier = Modifier 251 | .height(80.dp) 252 | .fillMaxWidth(), 253 | contentAlignment = Alignment.CenterStart 254 | ) { 255 | Text( 256 | modifier = Modifier.padding(start = 20.dp), 257 | text = "BaseRevealSwipe" 258 | ) 259 | } 260 | } 261 | } 262 | } 263 | 264 | @Composable 265 | private fun ButtonRevealSwipe() { 266 | RevealSwipe( 267 | onContentClick = null, 268 | modifier = Modifier.padding(vertical = 5.dp), 269 | hiddenContentStart = { 270 | SText() 271 | }, 272 | hiddenContentEnd = { 273 | TText() 274 | }, 275 | backgroundStartActionLabel = "Mark entry as favorite", 276 | backgroundEndActionLabel = "Delete entry", 277 | backgroundCardEndColor = MaterialTheme.colorScheme.secondaryContainer, 278 | backgroundCardStartColor = MaterialTheme.colorScheme.tertiaryContainer, 279 | shape = MaterialTheme.shapes.medium, 280 | card = { shape, content -> 281 | Card( 282 | modifier = Modifier.matchParentSize(), 283 | colors = CardDefaults.cardColors( 284 | contentColor = MaterialTheme.colorScheme.onSecondary, 285 | containerColor = Color.Transparent 286 | ), 287 | shape = shape, 288 | content = content 289 | ) 290 | } 291 | ) { 292 | Card( 293 | shape = it, 294 | colors = CardDefaults.cardColors( 295 | containerColor = MaterialTheme.colorScheme.secondary 296 | ) 297 | ) { 298 | Row( 299 | modifier = Modifier 300 | .height(80.dp) 301 | .fillMaxWidth(), 302 | verticalAlignment = Alignment.CenterVertically, 303 | ) { 304 | 305 | Spacer(modifier = Modifier.width(16.dp)) 306 | val state = remember { mutableStateOf(false) } 307 | Text("onContentClick = null") 308 | Spacer(modifier = Modifier.width(16.dp)) 309 | Button(onClick = { state.value = !state.value }) { 310 | Text("Click me!") 311 | } 312 | Spacer(modifier = Modifier.width(16.dp)) 313 | Text(if (state.value) "Clicked!" else "") 314 | } 315 | } 316 | } 317 | } 318 | @Composable 319 | private fun ButtonRevealSwipe2() { 320 | RevealSwipe( 321 | onContentClick = { 322 | 323 | }, 324 | modifier = Modifier.padding(vertical = 5.dp), 325 | hiddenContentStart = { 326 | SText() 327 | }, 328 | hiddenContentEnd = { 329 | TText() 330 | }, 331 | backgroundStartActionLabel = "Mark entry as favorite", 332 | backgroundEndActionLabel = "Delete entry", 333 | backgroundCardEndColor = MaterialTheme.colorScheme.secondaryContainer, 334 | backgroundCardStartColor = MaterialTheme.colorScheme.tertiaryContainer, 335 | shape = MaterialTheme.shapes.medium, 336 | card = { shape, content -> 337 | Card( 338 | modifier = Modifier.matchParentSize(), 339 | colors = CardDefaults.cardColors( 340 | contentColor = MaterialTheme.colorScheme.onSecondary, 341 | containerColor = Color.Transparent 342 | ), 343 | shape = shape, 344 | content = content 345 | ) 346 | } 347 | ) { 348 | Card( 349 | shape = it, 350 | colors = CardDefaults.cardColors( 351 | containerColor = MaterialTheme.colorScheme.secondary 352 | ) 353 | ) { 354 | Row( 355 | modifier = Modifier 356 | .height(80.dp) 357 | .fillMaxWidth(), 358 | verticalAlignment = Alignment.CenterVertically, 359 | ) { 360 | 361 | Spacer(modifier = Modifier.width(16.dp)) 362 | val state = remember { mutableStateOf(false) } 363 | Text("onContentClick = { }") 364 | Spacer(modifier = Modifier.width(16.dp)) 365 | Button(onClick = { state.value = !state.value }) { 366 | Text("Click me!") 367 | } 368 | Spacer(modifier = Modifier.width(16.dp)) 369 | Text(if (state.value) "Clicked!" else "") 370 | } 371 | } 372 | } 373 | } 374 | 375 | @Composable 376 | private fun TextFieldRevealSwipe() { 377 | RevealSwipe( 378 | modifier = Modifier.padding(vertical = 5.dp), 379 | hiddenContentStart = { 380 | SText() 381 | }, 382 | hiddenContentEnd = { 383 | TText() 384 | }, 385 | backgroundStartActionLabel = "Mark entry as favorite", 386 | backgroundEndActionLabel = "Delete entry", 387 | backgroundCardEndColor = MaterialTheme.colorScheme.secondaryContainer, 388 | backgroundCardStartColor = MaterialTheme.colorScheme.tertiaryContainer, 389 | shape = MaterialTheme.shapes.medium, 390 | card = { shape, content -> 391 | Card( 392 | modifier = Modifier.matchParentSize(), 393 | colors = CardDefaults.cardColors( 394 | contentColor = MaterialTheme.colorScheme.onSecondary, 395 | containerColor = Color.Transparent 396 | ), 397 | shape = shape, 398 | content = content 399 | ) 400 | } 401 | ) { 402 | Card( 403 | shape = it, 404 | ) { 405 | Box( 406 | modifier = Modifier 407 | .height(80.dp) 408 | .fillMaxWidth(), 409 | contentAlignment = Alignment.CenterStart 410 | ) { 411 | 412 | val text = remember { mutableStateOf("") } 413 | BasicTextField( 414 | modifier = Modifier 415 | .fillMaxWidth() 416 | .padding(start = 20.dp), 417 | value = text.value, 418 | onValueChange = { text.value = it }, 419 | decorationBox = { 420 | it() 421 | if (text.value.isBlank()) { 422 | Text("Enter Name") 423 | } 424 | } 425 | ) 426 | } 427 | } 428 | } 429 | } 430 | 431 | @Composable 432 | private fun ContentClickRevealSwipe() { 433 | val state = remember { mutableStateOf(false) } 434 | RevealSwipe( 435 | onContentClick = { state.value = !state.value }, 436 | closeOnContentClick = true, 437 | modifier = Modifier.padding(vertical = 5.dp), 438 | hiddenContentStart = { 439 | SText() 440 | }, 441 | hiddenContentEnd = { 442 | TText() 443 | }, 444 | backgroundStartActionLabel = "Mark entry as favorite", 445 | backgroundEndActionLabel = "Delete entry", 446 | backgroundCardEndColor = MaterialTheme.colorScheme.secondaryContainer, 447 | backgroundCardStartColor = MaterialTheme.colorScheme.tertiaryContainer, 448 | shape = MaterialTheme.shapes.medium, 449 | card = { shape, content -> 450 | Card( 451 | modifier = Modifier.matchParentSize(), 452 | colors = CardDefaults.cardColors( 453 | contentColor = MaterialTheme.colorScheme.onSecondary, 454 | containerColor = Color.Transparent 455 | ), 456 | shape = shape, 457 | content = content 458 | ) 459 | } 460 | ) { 461 | Card( 462 | shape = it, 463 | colors = CardDefaults.cardColors( 464 | containerColor = MaterialTheme.colorScheme.tertiary 465 | ) 466 | ) { 467 | Row( 468 | modifier = Modifier 469 | .height(80.dp) 470 | .fillMaxWidth(), 471 | verticalAlignment = Alignment.CenterVertically, 472 | ) { 473 | 474 | Spacer(modifier = Modifier.width(16.dp)) 475 | Text("Click me!") 476 | Spacer(modifier = Modifier.width(16.dp)) 477 | Text(if (state.value) "Clicked!" else "") 478 | } 479 | } 480 | } 481 | } 482 | 483 | @Composable 484 | private fun TText() { 485 | Text( 486 | modifier = Modifier.padding(horizontal = 25.dp), 487 | text = "T" 488 | ) 489 | } 490 | 491 | @Composable 492 | private fun SText() { 493 | Text( 494 | modifier = Modifier.padding(horizontal = 25.dp), 495 | text = "S" 496 | ) 497 | } 498 | 499 | @Preview(showBackground = true) 500 | @Composable 501 | fun DefaultPreview() { 502 | MaterialTheme { 503 | RevealSamples(listOf( 504 | Item( 505 | label = "Both directions", 506 | color = Color.DarkGray, 507 | directions = setOf( 508 | RevealDirection.StartToEnd, 509 | RevealDirection.EndToStart, 510 | ), 511 | ) 512 | )) 513 | } 514 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | } 15 | } 16 | 17 | rootProject.name = "RevealSwipe" 18 | include ':revealswipe' 19 | include ':sample' 20 | -------------------------------------------------------------------------------- /spotless/greclipse.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2020 The Android Open Source Project 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 | # https://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 | #Whether to use 'space', 'tab' or 'mixed' (both) characters for indentation. 18 | #The default value is 'tab'. 19 | org.eclipse.jdt.core.formatter.tabulation.char=space 20 | 21 | #Number of spaces used for indentation in case 'space' characters 22 | #have been selected. The default value is 4. 23 | org.eclipse.jdt.core.formatter.tabulation.size=4 24 | 25 | #Number of spaces used for indentation in case 'mixed' characters 26 | #have been selected. The default value is 4. 27 | org.eclipse.jdt.core.formatter.indentation.size=4 28 | 29 | #Whether or not indentation characters are inserted into empty lines. 30 | #The default value is 'true'. 31 | org.eclipse.jdt.core.formatter.indent_empty_lines=false 32 | 33 | #Number of spaces used for multiline indentation. 34 | #The default value is 2. 35 | groovy.formatter.multiline.indentation=2 36 | 37 | #Length after which list are considered too long. These will be wrapped. 38 | #The default value is 30. 39 | groovy.formatter.longListLength=30 40 | 41 | #Whether opening braces position shall be the next line. 42 | #The default value is 'same'. 43 | groovy.formatter.braces.start=same 44 | 45 | #Whether closing braces position shall be the next line. 46 | #The default value is 'next'. 47 | groovy.formatter.braces.end=next 48 | 49 | #Remove unnecessary semicolons. The default value is 'false'. 50 | groovy.formatter.remove.unnecessary.semicolons=false --------------------------------------------------------------------------------