├── .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 |
7 |
8 |
9 |
10 |
11 |
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 | 
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
--------------------------------------------------------------------------------