├── .github └── workflows │ └── android.yml ├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── misc.xml └── runConfigurations.xml ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── app ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── liuzhenlin │ │ └── simrv │ │ └── sample │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── liuzhenlin │ │ │ └── simrv │ │ │ └── sample │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── item_simrv.xml │ │ ├── menu │ │ └── menu_see_github.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── liuzhenlin │ └── simrv │ └── sample │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── settings.gradle └── slidingitemmenu-recyclerview ├── build.gradle └── src ├── androidTest └── java │ └── com │ └── liuzhenlin │ └── simrv │ └── ExampleInstrumentedTest.java ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── liuzhenlin │ │ └── simrv │ │ ├── SlidingItemMenuRecyclerView.java │ │ ├── Utils.java │ │ ├── ViscousFluidInterpolator.java │ │ └── reservation │ │ ├── ScrollerLinearLayout.java │ │ ├── ScrollerView.java │ │ └── TopWrappedDividerItemDecoration.java └── res │ ├── drawable-v21 │ └── default_selector_recycler_item.xml │ ├── drawable │ └── default_selector_recycler_item.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ └── tags.xml └── test └── java └── com └── liuzhenlin └── simrv └── ExampleUnitTest.java /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ developers ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: set up JDK 11 18 | uses: actions/setup-java@v3 19 | with: 20 | java-version: '11' 21 | distribution: 'temurin' 22 | cache: gradle 23 | 24 | - name: Grant execute permission for gradlew 25 | run: chmod +x gradlew 26 | - name: Build with Gradle 27 | run: ./gradlew build 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | *.aar 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | # Android Studio 3 in .gitignore file. 48 | .idea/caches 49 | .idea/modules.xml 50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 51 | .idea/navEditor.xml 52 | 53 | # Keystore files 54 | # Uncomment the following lines if you do not want to check your keystore files in. 55 | #*.jks 56 | #*.keystore 57 | 58 | # External native build folder generated in Android Studio 2.2 and later 59 | .externalNativeBuild 60 | .cxx/ 61 | 62 | # Google Services (e.g. APIs or Firebase) 63 | # google-services.json 64 | 65 | # Freeline 66 | freeline.py 67 | freeline/ 68 | freeline_project_description.json 69 | 70 | # fastlane 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | fastlane/readme.md 76 | 77 | # Version control 78 | vcs.xml 79 | 80 | # lint 81 | lint/intermediates/ 82 | lint/generated/ 83 | lint/outputs/ 84 | lint/tmp/ 85 | # lint/reports/ 86 | 87 | # Desktop Services Store (Mac OS) 88 | .DS_Store -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 26 | 27 | 28 | 35 | 44 | 45 | 46 | 47 | 49 | 50 | 51 |
52 | 53 | 54 | 55 | xmlns:android 56 | ^$ 57 | 58 | 59 | 60 |
61 |
62 | 63 | 64 | 65 | xmlns:.* 66 | ^$ 67 | 68 | 69 | BY_NAME 70 | 71 |
72 |
73 | 74 | 75 | 76 | .*:id 77 | http://schemas.android.com/apk/res/android 78 | 79 | 80 | 81 |
82 |
83 | 84 | 85 | 86 | .*:name 87 | http://schemas.android.com/apk/res/android 88 | 89 | 90 | 91 |
92 |
93 | 94 | 95 | 96 | name 97 | ^$ 98 | 99 | 100 | 101 |
102 |
103 | 104 | 105 | 106 | style 107 | ^$ 108 | 109 | 110 | 111 |
112 |
113 | 114 | 115 | 116 | .* 117 | ^$ 118 | 119 | 120 | BY_NAME 121 | 122 |
123 |
124 | 125 | 126 | 127 | .* 128 | http://schemas.android.com/apk/res/android 129 | 130 | 131 | ANDROID_ATTRIBUTE_ORDER 132 | 133 |
134 |
135 | 136 | 137 | 138 | .* 139 | .* 140 | 141 | 142 | BY_NAME 143 | 144 |
145 |
146 |
147 |
148 |
149 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /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 text of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SlidingItemMenuRecyclerView [![](https://jitpack.io/v/lzls/SlidingItemMenuRecyclerView.svg)](https://jitpack.io/#lzls/SlidingItemMenuRecyclerView) 2 | 3 | Though the styles of left sliding to open the hidden menu of RecyclerView's itemView are diverse, 4 | the common case is that they are not elegant enough. This kind of item menu imitated from a 5 | new feature as introduced in iOS 11 like iOS' current version of WeChat shows, may be an unusual one 6 | as there may not have been a similar implementation on other RecyclerView libraries yet. 7 | 8 |
9 | 10 |
11 | 12 | 13 | ## Documentation 14 | 15 | * The [release notes][] document the major changes in each release. 16 | 17 | [release notes]: https://github.com/lzls/SlidingItemMenuRecyclerView/blob/master/RELEASE_NOTES.md 18 | 19 | 20 | ## Usage 21 | The layout file of RecyclerView's itemView needs just to follow the one below and everything 22 | will work normally without any more extra code to be written in your class file. 23 | Then set OnClickListeners for your menu items to respond to the user's interactions as needed. 24 | ```xml 25 | 26 | 30 | 31 | 45 | 46 | 49 | 50 | 51 | 52 | 56 | 57 | 65 | 66 | 67 | 71 | 72 | 80 | 81 | 82 | 86 | 87 | 95 | 96 | 97 | 98 | ``` 99 | For more usages, please download source code to see. 100 | 101 | 102 | ## Pull Requests 103 | I will gladly accept pull requests for bug fixes and feature enhancements but please do them 104 | in the `developers` branch. 105 | 106 | 107 | ## License 108 | Copyright 2017–2021 刘振林 109 | 110 | Licensed under the Apache License, Version 2.0 (the "License");
111 | you may not use this file except in compliance with the License. You may obtain a copy of 112 | the License at 113 | 114 |   http://www.apache.org/licenses/LICENSE-2.0 115 | 116 | Unless required by applicable law or agreed to in writing, software distributed under the License 117 | is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 118 | or implied. See the License for the specific language governing permissions and limitations 119 | under the License. 120 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ### 2.0.1 (2023-02-13) 4 | * Library 5 | * Fix bug where visibility set for child of RecyclerView item might not work as expected. 6 | 7 | ### 2.0 (2021-03-02) 8 | * No notes provided. 9 | 10 | ### 1.0.4 (2019-11-21) 11 | * No notes provided. 12 | 13 | ### 1.0.3 (2019-08-27) 14 | * Library 15 | * Update default recycler item selector. 16 | * Item menu can now be opened programmatically. 17 | 18 | ### 1.0.2 (2019-02-27) 19 | * Library 20 | * Fix a bug on platforms prior to `Nougat`. 21 | 22 | ### 1.0.1 (2018-12-15) 23 | * Library 24 | * Touch event optimizations. 25 | 26 | ### 1.0 (2018-11-24) 27 | * No notes provided. -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion rootProject.ext.compileSdkVersion 5 | 6 | defaultConfig { 7 | applicationId "com.liuzhenlin.simrv.sample" 8 | minSdkVersion 14 9 | targetSdkVersion rootProject.ext.targetSdkVersion 10 | versionCode 1 11 | versionName "1.0" 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | implementation fileTree(dir: 'libs', include: ['*.jar']) 25 | implementation "androidx.appcompat:appcompat:$rootProject.ext.appcompatVersion" 26 | implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' 27 | 28 | implementation project(':slidingitemmenu-recyclerview') 29 | 30 | testImplementation "junit:junit:$rootProject.ext.testJunitVersion" 31 | androidTestImplementation "androidx.test.ext:junit:$rootProject.ext.androidTestJunitVersion" 32 | } 33 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/liuzhenlin/simrv/sample/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.liuzhenlin.simrv.sample; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.ext.junit.runners.AndroidJUnit4; 6 | import androidx.test.platform.app.InstrumentationRegistry; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | 25 | assertEquals("com.liuzhenlin.simrv.sample", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/liuzhenlin/simrv/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.liuzhenlin.simrv.sample; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.os.Bundle; 7 | import android.view.LayoutInflater; 8 | import android.view.Menu; 9 | import android.view.MenuItem; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.TextView; 13 | import android.widget.Toast; 14 | 15 | import androidx.annotation.NonNull; 16 | import androidx.appcompat.app.AppCompatActivity; 17 | import androidx.recyclerview.widget.DividerItemDecoration; 18 | import androidx.recyclerview.widget.LinearLayoutManager; 19 | import androidx.recyclerview.widget.RecyclerView; 20 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 21 | 22 | import com.liuzhenlin.simrv.SlidingItemMenuRecyclerView; 23 | 24 | /** 25 | * @author 刘振林 26 | */ 27 | public class MainActivity extends AppCompatActivity { 28 | @Override 29 | protected void onCreate(Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | 32 | final SwipeRefreshLayout srl = (SwipeRefreshLayout) View.inflate(this, 33 | R.layout.activity_main, null); 34 | srl.setColorSchemeResources(R.color.colorAccent, R.color.colorPrimary); 35 | 36 | setContentView(srl); 37 | 38 | final SlidingItemMenuRecyclerView simrv = srl.findViewById(R.id.simrv); 39 | simrv.setLayoutManager(new LinearLayoutManager(this)); 40 | simrv.setAdapter(new RecyclerAdapter()); 41 | simrv.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); 42 | 43 | srl.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { 44 | final Runnable refreshListRunnable = () -> { 45 | RecyclerAdapter adapter = ((RecyclerAdapter) simrv.getAdapter()); 46 | assert adapter != null; 47 | final int old = adapter.itemCount; 48 | adapter.itemCount += 3; 49 | adapter.notifyItemRangeInserted(old, 2); 50 | 51 | srl.setRefreshing(false); 52 | simrv.setItemDraggable(true); 53 | }; 54 | 55 | @Override 56 | public void onRefresh() { 57 | simrv.releaseItemView(false); 58 | simrv.setItemDraggable(false); 59 | 60 | srl.postDelayed(refreshListRunnable, 2000); 61 | } 62 | }); 63 | 64 | final int duration = simrv.getItemScrollDuration(); 65 | simrv.post(() -> { 66 | simrv.openItemAtPosition(0); 67 | simrv.postDelayed(() -> { 68 | simrv.openItemAtPosition(1); 69 | simrv.postDelayed(() -> { 70 | simrv.openItemAtPosition(2); 71 | simrv.postDelayed(() -> { 72 | simrv.openItemAtPosition(3); 73 | simrv.postDelayed(() -> { 74 | simrv.openItemAtPosition(4); 75 | simrv.postDelayed(() -> simrv.openItemAtPosition(5), duration); 76 | }, duration); 77 | }, duration); 78 | }, duration); 79 | }, duration); 80 | }); 81 | } 82 | 83 | @Override 84 | public boolean onCreateOptionsMenu(Menu menu) { 85 | getMenuInflater().inflate(R.menu.menu_see_github, menu); 86 | return true; 87 | } 88 | 89 | @Override 90 | public boolean onOptionsItemSelected(MenuItem item) { 91 | //noinspection SwitchStatementWithTooFewBranches 92 | switch (item.getItemId()) { 93 | case R.id.option_see_github: 94 | startActivity( 95 | new Intent(Intent.ACTION_VIEW) 96 | .setData(Uri.parse("https://github.com/lzls/SlidingItemMenuRecyclerView"))); 97 | return true; 98 | default: 99 | return super.onOptionsItemSelected(item); 100 | } 101 | } 102 | 103 | private final class RecyclerAdapter extends RecyclerView.Adapter 104 | implements View.OnClickListener { 105 | int itemCount = 5; 106 | 107 | RecyclerAdapter() { 108 | } 109 | 110 | @NonNull 111 | @Override 112 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 113 | return new ViewHolder( 114 | LayoutInflater.from(parent.getContext()) 115 | .inflate(R.layout.item_simrv, parent, false)); 116 | } 117 | 118 | @SuppressLint("SetTextI18n") 119 | @Override 120 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 121 | holder.text.setText("ItemView " + position); 122 | holder.text.setTag(position); 123 | holder.renameButton.setTag(position); 124 | holder.deleteButton.setTag(position); 125 | holder.topButton.setTag(position); 126 | } 127 | 128 | @Override 129 | public int getItemCount() { 130 | return itemCount; 131 | } 132 | 133 | final class ViewHolder extends RecyclerView.ViewHolder { 134 | final TextView text; 135 | final TextView renameButton; 136 | final TextView deleteButton; 137 | final TextView topButton; 138 | 139 | ViewHolder(View itemView) { 140 | super(itemView); 141 | text = itemView.findViewById(R.id.text); 142 | renameButton = itemView.findViewById(R.id.button_rename); 143 | deleteButton = itemView.findViewById(R.id.button_delete); 144 | topButton = itemView.findViewById(R.id.button_top); 145 | 146 | text.setOnClickListener(RecyclerAdapter.this); 147 | renameButton.setOnClickListener(RecyclerAdapter.this); 148 | deleteButton.setOnClickListener(RecyclerAdapter.this); 149 | topButton.setOnClickListener(RecyclerAdapter.this); 150 | } 151 | } 152 | 153 | @Override 154 | public void onClick(View v) { 155 | switch (v.getId()) { 156 | case R.id.text: 157 | Toast.makeText(MainActivity.this, 158 | "Click itemView " + v.getTag().toString(), 159 | Toast.LENGTH_SHORT).show(); 160 | break; 161 | case R.id.button_rename: 162 | Toast.makeText(MainActivity.this, 163 | "Rename itemView " + v.getTag().toString(), 164 | Toast.LENGTH_SHORT).show(); 165 | break; 166 | case R.id.button_delete: 167 | Toast.makeText(MainActivity.this, 168 | "Delete itemView " + v.getTag().toString(), 169 | Toast.LENGTH_SHORT).show(); 170 | break; 171 | case R.id.button_top: 172 | Toast.makeText(MainActivity.this, 173 | "Top itemView " + v.getTag().toString(), 174 | Toast.LENGTH_SHORT).show(); 175 | break; 176 | } 177 | } 178 | } 179 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 13 | 14 | 20 | 23 | 26 | 27 | 28 | 29 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_simrv.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 23 | 24 | 27 | 28 | 29 | 30 | 34 | 35 | 43 | 44 | 45 | 49 | 50 | 58 | 59 | 60 | 64 | 65 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_see_github.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzls/SlidingItemMenuRecyclerView/844fd768c460d4c3f6f956587850bf80537a0ca5/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzls/SlidingItemMenuRecyclerView/844fd768c460d4c3f6f956587850bf80537a0ca5/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzls/SlidingItemMenuRecyclerView/844fd768c460d4c3f6f956587850bf80537a0ca5/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzls/SlidingItemMenuRecyclerView/844fd768c460d4c3f6f956587850bf80537a0ca5/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzls/SlidingItemMenuRecyclerView/844fd768c460d4c3f6f956587850bf80537a0ca5/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzls/SlidingItemMenuRecyclerView/844fd768c460d4c3f6f956587850bf80537a0ca5/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzls/SlidingItemMenuRecyclerView/844fd768c460d4c3f6f956587850bf80537a0ca5/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzls/SlidingItemMenuRecyclerView/844fd768c460d4c3f6f956587850bf80537a0ca5/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzls/SlidingItemMenuRecyclerView/844fd768c460d4c3f6f956587850bf80537a0ca5/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzls/SlidingItemMenuRecyclerView/844fd768c460d4c3f6f956587850bf80537a0ca5/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | #FFA500 8 | #FF0000 9 | #00BFFF 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SlidingItemMenuRecyclerView 4 | RENAME 5 | DELETE 6 | TOP 7 | GitHub 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/test/java/com/liuzhenlin/simrv/sample/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.liuzhenlin.simrv.sample; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:4.2.2' 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | task clean(type: Delete) { 25 | delete rootProject.buildDir 26 | } 27 | 28 | ext { 29 | compileSdkVersion = 31 30 | targetSdkVersion = 31 31 | 32 | appcompatVersion = '1.4.2' 33 | 34 | testJunitVersion = '4.13.2' 35 | androidTestJunitVersion = '1.1.4' 36 | } 37 | -------------------------------------------------------------------------------- /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 | android.enableJetifier=true 10 | android.useAndroidX=true 11 | org.gradle.jvmargs=-Xmx1536m 12 | # When configured, Gradle will run in incubating parallel mode. 13 | # This option should only be used with decoupled projects. More details, visit 14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 15 | # org.gradle.parallel=true 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzls/SlidingItemMenuRecyclerView/844fd768c460d4c3f6f956587850bf80537a0ca5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Feb 13 15:57:40 CST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip 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 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app',':slidingitemmenu-recyclerview' 2 | -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion rootProject.ext.compileSdkVersion 5 | 6 | defaultConfig { 7 | //noinspection MinSdkTooLow 8 | minSdkVersion 11 9 | targetSdkVersion rootProject.ext.targetSdkVersion 10 | versionCode 7 11 | versionName "2.0.1" 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | implementation fileTree(dir: 'libs', include: ['*.jar']) 26 | implementation "androidx.appcompat:appcompat:$rootProject.ext.appcompatVersion" 27 | api 'androidx.recyclerview:recyclerview:1.2.1' 28 | 29 | testImplementation "junit:junit:$rootProject.ext.testJunitVersion" 30 | androidTestImplementation "androidx.test.ext:junit:$rootProject.ext.androidTestJunitVersion" 31 | } 32 | -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/src/androidTest/java/com/liuzhenlin/simrv/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.liuzhenlin.simrv; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.ext.junit.runners.AndroidJUnit4; 6 | import androidx.test.platform.app.InstrumentationRegistry; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | 25 | assertEquals("com.liuzhenlin.simrv.test", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/src/main/java/com/liuzhenlin/simrv/SlidingItemMenuRecyclerView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Created on 2017/12/16. 3 | * Copyright © 2017–2020 刘振林. All rights reserved. 4 | */ 5 | 6 | package com.liuzhenlin.simrv; 7 | 8 | import android.animation.Animator; 9 | import android.animation.AnimatorListenerAdapter; 10 | import android.animation.ValueAnimator; 11 | import android.annotation.SuppressLint; 12 | import android.content.Context; 13 | import android.content.res.TypedArray; 14 | import android.graphics.Rect; 15 | import android.os.Build; 16 | import android.util.AttributeSet; 17 | import android.view.MotionEvent; 18 | import android.view.VelocityTracker; 19 | import android.view.View; 20 | import android.view.ViewConfiguration; 21 | import android.view.ViewGroup; 22 | import android.view.ViewParent; 23 | import android.view.animation.Interpolator; 24 | import android.view.animation.OvershootInterpolator; 25 | import android.widget.FrameLayout; 26 | 27 | import androidx.annotation.Nullable; 28 | import androidx.collection.SimpleArrayMap; 29 | import androidx.core.view.ViewCompat; 30 | import androidx.recyclerview.widget.RecyclerView; 31 | 32 | import java.util.LinkedList; 33 | import java.util.List; 34 | 35 | /** 36 | * @author 刘振林 37 | */ 38 | public class SlidingItemMenuRecyclerView extends RecyclerView { 39 | private static final String TAG = "SlidingItemMenuRecyclerView"; 40 | 41 | private boolean mIsVerticalScrollBarEnabled; 42 | 43 | /** 44 | * @see #isItemDraggable() 45 | * @see #setItemDraggable(boolean) 46 | */ 47 | private boolean mIsItemDraggable; 48 | 49 | /** True, if an item view is being dragged by the user. */ 50 | private boolean mIsItemBeingDragged; 51 | 52 | /** 53 | * Whether or not some item view is fully open when this view receives the 54 | * {@link MotionEvent#ACTION_DOWN} event. 55 | */ 56 | private boolean mHasItemFullyOpenOnActionDown; 57 | 58 | /** Distance to travel before drag may begin */ 59 | protected final int mTouchSlop; 60 | 61 | private int mDownX; 62 | private int mDownY; 63 | 64 | private final float[] mTouchX = new float[2]; 65 | private final float[] mTouchY = new float[2]; 66 | 67 | private VelocityTracker mVelocityTracker; 68 | 69 | /** Minimum gesture speed along the x axis to automatically scroll item views */ 70 | private final float mItemMinimumFlingVelocity; // 200 dp/s 71 | 72 | /** 73 | * The bounds of the currently touched item View {@link #mActiveItem} (relative to current view). 74 | */ 75 | private final Rect mActiveItemBounds = new Rect(); 76 | /** 77 | * The bounds of the currently touched item view's menu (relative to current view). 78 | */ 79 | private final Rect mActiveItemMenuBounds = new Rect(); 80 | 81 | /** The item view that is currently being touched or dragged by the user */ 82 | private ViewGroup mActiveItem; 83 | 84 | /** The item view that is fully open or to be opened through the animator associated to it */ 85 | private ViewGroup mFullyOpenedItem; 86 | 87 | /** The set of opened item views */ 88 | private final List mOpenedItems = new LinkedList<>(); 89 | 90 | /** Tag used to get the width of an item view's menu */ 91 | private static final int TAG_ITEM_MENU_WIDTH = R.id.tag_itemMenuWidth; 92 | 93 | /** Tag used to get the widths of the menu items of an item view */ 94 | private static final int TAG_MENU_ITEM_WIDTHS = R.id.tag_menuItemWidths; 95 | 96 | /** Tag used to get the animator of the item view to which it associated */ 97 | private static final int TAG_ITEM_ANIMATOR = R.id.tag_itemAnimator; 98 | 99 | /** 100 | * Time interval in milliseconds of automatically scrolling item views 101 | * 102 | * @see #getItemScrollDuration() 103 | * @see #setItemScrollDuration(int) 104 | */ 105 | private int mItemScrollDuration; 106 | 107 | /** Default value of {@link #mItemScrollDuration} if no value is set for it */ 108 | public static final int DEFAULT_ITEM_SCROLL_DURATION = 500; // ms 109 | 110 | private static final Interpolator sViscousFluidInterpolator = 111 | new ViscousFluidInterpolator(6.66f); 112 | private static final Interpolator sOvershootInterpolator = 113 | new OvershootInterpolator(1.0f); 114 | 115 | /** 116 | * @deprecated Use {@link #isItemDraggable()} instead 117 | */ 118 | @Deprecated 119 | public boolean isItemScrollingEnabled() { 120 | return isItemDraggable(); 121 | } 122 | 123 | /** 124 | * @return whether it is enabled to scroll item views in touch mode or not 125 | */ 126 | public boolean isItemDraggable() { 127 | return mIsItemDraggable; 128 | } 129 | 130 | /** 131 | * @deprecated Use {@link #setItemDraggable(boolean)} instead 132 | */ 133 | @Deprecated 134 | public void setItemScrollingEnabled(boolean enabled) { 135 | setItemDraggable(enabled); 136 | } 137 | 138 | /** 139 | * Sets whether the item views can be dragged by user. 140 | *

141 | * If unable to be dragged, they may be scrolled through the code like: 142 | * simrv.openItemAtPosition(0, true); 143 | */ 144 | public void setItemDraggable(boolean draggable) { 145 | mIsItemDraggable = draggable; 146 | } 147 | 148 | /** 149 | * Gets the lasting time of the animator for opening/closing the item view to which 150 | * the animator associated. 151 | * The default duration is {@value DEFAULT_ITEM_SCROLL_DURATION} milliseconds. 152 | * 153 | * @return the duration of the animator 154 | */ 155 | public int getItemScrollDuration() { 156 | return mItemScrollDuration; 157 | } 158 | 159 | /** 160 | * Sets the duration for the animators used to open/close the item views. 161 | * 162 | * @throws IllegalArgumentException if a negative 'duration' is passed in 163 | */ 164 | public void setItemScrollDuration(int duration) { 165 | if (duration < 0) { 166 | throw new IllegalArgumentException("The animators for opening/closing the item views " + 167 | "cannot have negative duration: " + duration); 168 | } 169 | mItemScrollDuration = duration; 170 | } 171 | 172 | public SlidingItemMenuRecyclerView(Context context) { 173 | this(context, null); 174 | } 175 | 176 | public SlidingItemMenuRecyclerView(Context context, @Nullable AttributeSet attrs) { 177 | this(context, attrs, 0); 178 | } 179 | 180 | public SlidingItemMenuRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { 181 | super(context, attrs, defStyle); 182 | mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 183 | mItemMinimumFlingVelocity = 200f * getResources().getDisplayMetrics().density; 184 | 185 | final TypedArray ta = context.obtainStyledAttributes( 186 | attrs, R.styleable.SlidingItemMenuRecyclerView, defStyle, 0); 187 | if (ta.hasValue(R.styleable.SlidingItemMenuRecyclerView_itemDraggable)) { 188 | setItemDraggable(ta.getBoolean(R.styleable 189 | .SlidingItemMenuRecyclerView_itemDraggable, true)); 190 | } else { 191 | // Libraries with version code prior to 5 use the itemScrollingEnabled attr only. 192 | setItemDraggable(ta.getBoolean(R.styleable 193 | .SlidingItemMenuRecyclerView_itemScrollingEnabled /* deprecated */, true)); 194 | } 195 | setItemScrollDuration(ta.getInteger(R.styleable 196 | .SlidingItemMenuRecyclerView_itemScrollDuration, DEFAULT_ITEM_SCROLL_DURATION)); 197 | ta.recycle(); 198 | } 199 | 200 | @Override 201 | public void setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled) { 202 | mIsVerticalScrollBarEnabled = verticalScrollBarEnabled; 203 | super.setVerticalScrollBarEnabled(verticalScrollBarEnabled); 204 | } 205 | 206 | private boolean childHasMenu(ViewGroup itemView) { 207 | if (itemView.getVisibility() != VISIBLE) return false; 208 | 209 | final int itemChildCount = itemView.getChildCount(); 210 | final View itemLastChild = itemView.getChildAt(itemChildCount >= 2 ? itemChildCount - 1 : 1); 211 | if (!(itemLastChild instanceof FrameLayout) 212 | || itemLastChild.getVisibility() != View.VISIBLE) 213 | return false; 214 | 215 | final FrameLayout itemMenu = (FrameLayout) itemLastChild; 216 | final int menuItemCount = itemMenu.getChildCount(); 217 | final int[] menuItemWidths = new int[menuItemCount]; 218 | int itemMenuWidth = 0; 219 | for (int i = 0; i < menuItemCount; i++) { 220 | final FrameLayout menuItemBg = (FrameLayout) itemMenu.getChildAt(i); 221 | // We can not just add up the item menu width with the width of the menu item without 222 | // checking the visibilities of it and its parents, as the visibility of a view 223 | // changing from visible to gone will just exclude it from the subsequent layout passes 224 | // and therefore will usually not have its width and height properties updated. 225 | if (menuItemBg.getVisibility() == View.VISIBLE) { 226 | final View menuItem = menuItemBg.getChildAt(0); 227 | if (menuItem.getVisibility() == View.VISIBLE) { 228 | menuItemWidths[i] = menuItem.getWidth(); 229 | itemMenuWidth += menuItemWidths[i]; 230 | } 231 | } 232 | } 233 | if (itemMenuWidth > 0) { 234 | itemView.setTag(TAG_ITEM_MENU_WIDTH, itemMenuWidth); 235 | itemView.setTag(TAG_MENU_ITEM_WIDTHS, menuItemWidths); 236 | return true; 237 | } 238 | return false; 239 | } 240 | 241 | private void resolveActiveItemMenuBounds() { 242 | final int itemMenuWidth = (int) mActiveItem.getTag(TAG_ITEM_MENU_WIDTH); 243 | final int left = Utils.isLayoutRtl(mActiveItem) ? 0 : mActiveItem.getRight() - itemMenuWidth; 244 | final int right = left + itemMenuWidth; 245 | mActiveItemMenuBounds.set(left, mActiveItemBounds.top, 246 | right, mActiveItemBounds.bottom); 247 | } 248 | 249 | @Override 250 | public boolean onInterceptTouchEvent(MotionEvent e) { 251 | final int action = e.getAction(); 252 | if (action == MotionEvent.ACTION_DOWN) { 253 | // Reset things for a new event stream, just in case we didn't get 254 | // the whole previous stream. 255 | resetTouch(); 256 | } 257 | 258 | if (mVelocityTracker == null) 259 | mVelocityTracker = VelocityTracker.obtain(); 260 | mVelocityTracker.addMovement(e); 261 | 262 | boolean intercept = false; 263 | switch (action) { 264 | case MotionEvent.ACTION_DOWN: 265 | mDownX = Utils.roundFloat(e.getX()); 266 | mDownY = Utils.roundFloat(e.getY()); 267 | markCurrTouchPoint(mDownX, mDownY); 268 | 269 | for (int i = getChildCount() - 1; i >= 0; i--) { 270 | final View child = getChildAt(i); 271 | if (!(child instanceof ViewGroup)) continue; 272 | 273 | final ViewGroup itemView = (ViewGroup) child; 274 | itemView.getHitRect(mActiveItemBounds); 275 | if (!mActiveItemBounds.contains(mDownX, mDownY)) continue; 276 | 277 | if (childHasMenu(itemView)) { 278 | mActiveItem = itemView; 279 | } 280 | break; 281 | } 282 | 283 | if (mOpenedItems.size() == 0) break; 284 | // Disallow our parent Views to intercept the touch events so long as there is 285 | // at least one item view in the open or being closed state. 286 | requestParentDisallowInterceptTouchEvent(); 287 | if (mFullyOpenedItem != null) { 288 | mHasItemFullyOpenOnActionDown = true; 289 | if (mActiveItem == mFullyOpenedItem) { 290 | resolveActiveItemMenuBounds(); 291 | // If the user's finger downs on the completely opened itemView's menu area, 292 | // do not intercept the subsequent touch events (ACTION_MOVE, ACTION_UP, etc.) 293 | // as we receive the ACTION_DOWN event. 294 | if (mActiveItemMenuBounds.contains(mDownX, mDownY)) { 295 | break; 296 | // If the user's finger downs on the fully opened itemView but not on 297 | // its menu, then we need to intercept them. 298 | } else if (mActiveItemBounds.contains(mDownX, mDownY)) { 299 | return true; 300 | } 301 | } 302 | // If 1) the fully opened itemView is not the current one or 2) the user's 303 | // finger downs outside of the area in which this view displays the itemViews, 304 | // make the itemView's menu hidden and intercept the subsequent touch events. 305 | releaseItemViewInternal(mFullyOpenedItem, mItemScrollDuration); 306 | } 307 | // Intercept the next touch events as long as there exists some item view open 308 | // (full open is not necessary for it). This prevents the onClick() method of 309 | // the pressed child from being called in the pending ACTION_UP event. 310 | return true; 311 | 312 | case MotionEvent.ACTION_MOVE: 313 | markCurrTouchPoint(e.getX(), e.getY()); 314 | 315 | intercept = tryHandleItemScrollingEvent(); 316 | // If the user initially put his/her finger down on the fully opened itemView's menu, 317 | // disallow our parent class to intercept the touch events since we will do that 318 | // as the user tends to scroll the current touched itemView horizontally. 319 | if (mHasItemFullyOpenOnActionDown && mActiveItemMenuBounds.contains(mDownX, mDownY)) { 320 | return intercept; 321 | } 322 | break; 323 | 324 | case MotionEvent.ACTION_UP: 325 | case MotionEvent.ACTION_CANCEL: 326 | // If the user initially placed his/her finger on the fully opened itemView's menu 327 | // and has clicked it or has not scrolled that itemView, hide it as his/her last 328 | // finger touching the screen lifts. 329 | if (mHasItemFullyOpenOnActionDown && mActiveItemMenuBounds.contains(mDownX, mDownY)) { 330 | releaseItemView(true); 331 | } 332 | clearTouch(); 333 | break; 334 | } 335 | return intercept || super.onInterceptTouchEvent(e); 336 | } 337 | 338 | @SuppressLint("ClickableViewAccessibility") 339 | @Override 340 | public boolean onTouchEvent(MotionEvent e) { 341 | if (mIsVerticalScrollBarEnabled) { 342 | // Makes the vertical scroll bar disappear while an itemView is being dragged. 343 | super.setVerticalScrollBarEnabled(!mIsItemBeingDragged); 344 | } 345 | 346 | if (mVelocityTracker == null) 347 | mVelocityTracker = VelocityTracker.obtain(); 348 | mVelocityTracker.addMovement(e); 349 | 350 | switch (e.getAction() & MotionEvent.ACTION_MASK) { 351 | case MotionEvent.ACTION_POINTER_DOWN: 352 | case MotionEvent.ACTION_POINTER_UP: 353 | if (mIsItemBeingDragged || mHasItemFullyOpenOnActionDown || mOpenedItems.size() > 0) { 354 | return true; 355 | } 356 | break; 357 | 358 | case MotionEvent.ACTION_MOVE: 359 | markCurrTouchPoint(e.getX(), e.getY()); 360 | 361 | if (!mIsItemDraggable && cancelTouch()) { 362 | return true; 363 | } 364 | if (mIsItemBeingDragged) { 365 | // Positive when the user's finger slides towards the right. 366 | float dx = mTouchX[mTouchX.length - 1] - mTouchX[mTouchX.length - 2]; 367 | // Positive when the itemView scrolls towards the right. 368 | final float translationX = mActiveItem.getChildAt(0).getTranslationX(); 369 | final boolean rtl = Utils.isLayoutRtl(mActiveItem); 370 | final int finalXFromEndToStart = 371 | rtl ? (int) mActiveItem.getTag(TAG_ITEM_MENU_WIDTH) 372 | : -(int) (mActiveItem.getTag(TAG_ITEM_MENU_WIDTH)); 373 | // Swipe the itemView towards the horizontal start over the width of 374 | // the itemView's menu. 375 | if (!rtl && dx + translationX < finalXFromEndToStart 376 | || rtl && dx + translationX > finalXFromEndToStart) { 377 | dx = dx / 3f; 378 | // Swipe the itemView towards the end of horizontal to (0,0). 379 | } else if (!rtl && dx + translationX > 0 || rtl && dx + translationX < 0) { 380 | dx = 0 - translationX; 381 | } 382 | translateItemViewXBy(mActiveItem, dx); 383 | 384 | // Consume this touch event and do not invoke the method onTouchEvent(e) of 385 | // the parent class to temporarily make this view unable to scroll up or down. 386 | return true; 387 | } else { 388 | // If there existed itemView whose menu was fully open when the user initially 389 | // put his/her finger down, always consume the touch event and only when the item 390 | // has a tend of scrolling horizontally will we handle the next events. 391 | if (mHasItemFullyOpenOnActionDown | tryHandleItemScrollingEvent()) { 392 | return true; 393 | } 394 | // Disallow current view to scroll while an/some item view(s) is/are scrolling. 395 | if (mOpenedItems.size() > 0) { 396 | return true; 397 | } 398 | } 399 | break; 400 | 401 | case MotionEvent.ACTION_UP: 402 | if (mIsItemDraggable && mIsItemBeingDragged) { 403 | final boolean rtl = Utils.isLayoutRtl(mActiveItem); 404 | final float translationX = mActiveItem.getChildAt(0).getTranslationX(); 405 | final int itemMenuWidth = (int) mActiveItem.getTag(TAG_ITEM_MENU_WIDTH); 406 | //noinspection StatementWithEmptyBody 407 | if (translationX == 0) { // itemView's menu is closed 408 | 409 | // itemView's menu is totally opened 410 | } else if (!rtl && translationX == -itemMenuWidth 411 | || rtl && translationX == itemMenuWidth) { 412 | mFullyOpenedItem = mActiveItem; 413 | 414 | } else { 415 | final float dx = 416 | rtl ? mTouchX[mTouchX.length - 2] - mTouchX[mTouchX.length - 1] 417 | : mTouchX[mTouchX.length - 1] - mTouchX[mTouchX.length - 2]; 418 | mVelocityTracker.computeCurrentVelocity(1000); 419 | final float velocityX = Math.abs(mVelocityTracker.getXVelocity()); 420 | // If the speed at which the user's finger lifted is greater than 200 dp/s 421 | // while user was scrolling itemView towards the horizontal start, 422 | // make it automatically scroll to open and show its menu. 423 | if (dx < 0 && velocityX >= mItemMinimumFlingVelocity) { 424 | smoothTranslateItemViewXTo( 425 | mActiveItem, 426 | rtl ? itemMenuWidth : -itemMenuWidth, 427 | mItemScrollDuration); 428 | mFullyOpenedItem = mActiveItem; 429 | clearTouch(); 430 | cancelParentTouch(e); 431 | return true; 432 | 433 | // If the speed at which the user's finger lifted is greater than 200 dp/s 434 | // while user was scrolling itemView towards the end of horizontal, 435 | // make its menu hidden. 436 | } else if (dx > 0 && velocityX >= mItemMinimumFlingVelocity) { 437 | releaseItemView(true); 438 | clearTouch(); 439 | cancelParentTouch(e); 440 | return true; 441 | } 442 | 443 | final float middle = itemMenuWidth / 2f; 444 | // If the sliding distance is less than half of its slidable distance, 445 | // hide its menu, 446 | if (Math.abs(translationX) < middle) { 447 | releaseItemView(true); 448 | 449 | // else open its menu. 450 | } else { 451 | smoothTranslateItemViewXTo( 452 | mActiveItem, 453 | rtl ? itemMenuWidth : -itemMenuWidth, 454 | mItemScrollDuration); 455 | mFullyOpenedItem = mActiveItem; 456 | } 457 | } 458 | clearTouch(); 459 | cancelParentTouch(e); 460 | return true; // Returns true here in case of a fling started in this up event. 461 | } 462 | case MotionEvent.ACTION_CANCEL: 463 | cancelTouch(); 464 | break; 465 | } 466 | 467 | return super.onTouchEvent(e); 468 | } 469 | 470 | private void markCurrTouchPoint(float x, float y) { 471 | System.arraycopy(mTouchX, 1, mTouchX, 0, mTouchX.length - 1); 472 | mTouchX[mTouchX.length - 1] = x; 473 | System.arraycopy(mTouchY, 1, mTouchY, 0, mTouchY.length - 1); 474 | mTouchY[mTouchY.length - 1] = y; 475 | } 476 | 477 | private boolean tryHandleItemScrollingEvent() { 478 | if (mActiveItem == null /* There's no scrollable itemView being touched by user */ 479 | || !mIsItemDraggable /* Unable to scroll it */ 480 | || getScrollState() != SCROLL_STATE_IDLE /* The list may be currently scrolling */) { 481 | return false; 482 | } 483 | // The layout's orientation may not be vertical. 484 | //noinspection ConstantConditions 485 | if (getLayoutManager().canScrollHorizontally()) { 486 | return false; 487 | } 488 | 489 | final float absDy = Math.abs(mTouchY[mTouchY.length - 1] - mDownY); 490 | if (absDy <= mTouchSlop) { 491 | final float dx = mTouchX[mTouchX.length - 1] - mDownX; 492 | if (mOpenedItems.size() == 0) { 493 | final boolean rtl = Utils.isLayoutRtl(mActiveItem); 494 | mIsItemBeingDragged = rtl && dx > mTouchSlop || !rtl && dx < -mTouchSlop; 495 | } else { 496 | mIsItemBeingDragged = Math.abs(dx) > mTouchSlop; 497 | } 498 | if (mIsItemBeingDragged) { 499 | requestParentDisallowInterceptTouchEvent(); 500 | return true; 501 | } 502 | } 503 | return false; 504 | } 505 | 506 | private void requestParentDisallowInterceptTouchEvent() { 507 | final ViewParent parent = getParent(); 508 | if (parent != null) { 509 | parent.requestDisallowInterceptTouchEvent(true); 510 | } 511 | } 512 | 513 | private boolean cancelTouch() { 514 | return cancelTouch(true); 515 | } 516 | 517 | private boolean cancelTouch(boolean animate) { 518 | if (mIsItemBeingDragged) { 519 | releaseItemView(animate); 520 | clearTouch(); 521 | return true; 522 | } 523 | // 1. If the itemView previously opened equals the current touched one and 524 | // the user hasn't scrolled it since he/she initially put his/her finger down, 525 | // hide it on the movements canceled. 526 | // 2. If the previously opened itemView differs from the one currently touched, 527 | // and the current one has not been scrolled at all, set 'mActiveItem' to null. 528 | if (mHasItemFullyOpenOnActionDown) { 529 | if (mActiveItem == mFullyOpenedItem) { 530 | releaseItemView(animate); 531 | } 532 | clearTouch(); 533 | return true; 534 | } 535 | return false; 536 | } 537 | 538 | private void clearTouch() { 539 | if (mVelocityTracker != null) { 540 | mVelocityTracker.recycle(); 541 | mVelocityTracker = null; 542 | } 543 | resetTouch(); 544 | } 545 | 546 | private void resetTouch() { 547 | mActiveItem = null; 548 | mHasItemFullyOpenOnActionDown = false; 549 | mActiveItemBounds.setEmpty(); 550 | mActiveItemMenuBounds.setEmpty(); 551 | mIsItemBeingDragged = false; 552 | if (mVelocityTracker != null) { 553 | mVelocityTracker.clear(); 554 | } 555 | } 556 | 557 | private void cancelParentTouch(MotionEvent e) { 558 | final int action = e.getAction(); 559 | e.setAction(MotionEvent.ACTION_CANCEL); 560 | super.onTouchEvent(e); 561 | e.setAction(action); 562 | } 563 | 564 | /** 565 | * Smoothly scrolls the current item view whose menu is open back to its original position. 566 | * 567 | * @see #releaseItemView(boolean) 568 | */ 569 | public void releaseItemView() { 570 | releaseItemView(true); 571 | } 572 | 573 | /** 574 | * Scrolls the current item view whose menu is open back to its original position. 575 | * 576 | * @param animate whether this scroll should be smooth 577 | */ 578 | public void releaseItemView(boolean animate) { 579 | releaseItemViewInternal(mIsItemBeingDragged ? mActiveItem : mFullyOpenedItem, 580 | animate ? mItemScrollDuration : 0); 581 | } 582 | 583 | private void releaseItemViewInternal(ViewGroup itemView, int duration) { 584 | if (itemView != null) { 585 | if (duration > 0) { 586 | smoothTranslateItemViewXTo(itemView, 0, duration); 587 | } else { 588 | translateItemViewXTo(itemView, 0); 589 | } 590 | if (mFullyOpenedItem == itemView) { 591 | mFullyOpenedItem = null; 592 | } 593 | } 594 | } 595 | 596 | /** 597 | * Smoothly opens the menu of the item view at the specified adapter position 598 | * 599 | * @param position the position of the item in the data set of the adapter 600 | * @return true if the menu of the child view that represents the given position can be opened; 601 | * false if the position is not laid out or the item does not have a menu. 602 | * @see #openItemAtPosition(int, boolean) 603 | */ 604 | public boolean openItemAtPosition(int position) { 605 | return openItemAtPosition(position, true); 606 | } 607 | 608 | /** 609 | * Opens the menu of the item view at the specified adapter position 610 | * 611 | * @param position the position of the item in the data set of the adapter 612 | * @param animate whether this scroll should be smooth 613 | * @return true if the menu of the child view that represents the given position can be opened; 614 | * false if the position is not laid out or the item does not have a menu. 615 | */ 616 | public boolean openItemAtPosition(int position, boolean animate) { 617 | final LayoutManager lm = getLayoutManager(); 618 | if (lm == null) return false; 619 | 620 | final View view = lm.findViewByPosition(position); 621 | if (!(view instanceof ViewGroup)) return false; 622 | 623 | final ViewGroup itemView = (ViewGroup) view; 624 | if (mFullyOpenedItem != itemView && childHasMenu(itemView)) { 625 | // First, cancels the item view being touched or previously fully opened (if any) 626 | if (!cancelTouch(animate)) { 627 | releaseItemView(animate); 628 | } 629 | 630 | smoothTranslateItemViewXTo( 631 | itemView, 632 | Utils.isLayoutRtl(itemView) 633 | ? (int) itemView.getTag(TAG_ITEM_MENU_WIDTH) 634 | : -(int) (itemView.getTag(TAG_ITEM_MENU_WIDTH)), 635 | animate ? mItemScrollDuration : 0); 636 | mFullyOpenedItem = itemView; 637 | return true; 638 | } 639 | return false; 640 | } 641 | 642 | private void smoothTranslateItemViewXTo(ViewGroup itemView, float x, int duration) { 643 | smoothTranslateItemViewXBy(itemView, x - itemView.getChildAt(0).getTranslationX(), duration); 644 | } 645 | 646 | private void smoothTranslateItemViewXBy(ViewGroup itemView, float dx, int duration) { 647 | TranslateItemViewXAnimator animator = 648 | (TranslateItemViewXAnimator) itemView.getTag(TAG_ITEM_ANIMATOR); 649 | 650 | if (dx != 0 && duration > 0) { 651 | boolean canceled = false; 652 | if (animator == null) { 653 | animator = new TranslateItemViewXAnimator(this, itemView); 654 | itemView.setTag(TAG_ITEM_ANIMATOR, animator); 655 | 656 | } else if (animator.isRunning()) { 657 | animator.removeListener(animator.listener); 658 | animator.cancel(); 659 | canceled = true; 660 | } 661 | animator.setFloatValues(0, dx); 662 | 663 | final boolean rtl = Utils.isLayoutRtl(itemView); 664 | final Interpolator interpolator = 665 | !rtl && dx < 0 || rtl && dx > 0 ? 666 | sOvershootInterpolator : sViscousFluidInterpolator; 667 | 668 | animator.setInterpolator(interpolator); 669 | animator.setDuration(duration); 670 | animator.start(); 671 | if (canceled) { 672 | animator.addListener(animator.listener); 673 | } 674 | } else { 675 | // Checks if there is an animator running for the given item view even if dx == 0 676 | if (animator != null && animator.isRunning()) { 677 | animator.cancel(); 678 | } 679 | // If duration <= 0, then scroll the 'itemView' directly to prevent a redundant call 680 | // to the animator. 681 | baseTranslateItemViewXBy(itemView, dx); 682 | } 683 | } 684 | 685 | private void translateItemViewXTo( 686 | ViewGroup itemView, @SuppressWarnings("SameParameterValue") float x) { 687 | translateItemViewXBy(itemView, x - itemView.getChildAt(0).getTranslationX()); 688 | } 689 | 690 | private void translateItemViewXBy(ViewGroup itemView, float dx) { 691 | final TranslateItemViewXAnimator animator = 692 | (TranslateItemViewXAnimator) itemView.getTag(TAG_ITEM_ANIMATOR); 693 | if (animator != null && animator.isRunning()) { 694 | // Cancels the running animator associated to the 'itemView' as we horizontally 695 | // scroll it to a position immediately to avoid inconsistencies in its translation X. 696 | animator.cancel(); 697 | } 698 | 699 | baseTranslateItemViewXBy(itemView, dx); 700 | } 701 | 702 | /* 703 | * This method does not cancel the translation animator of the 'itemView', for which it is used 704 | * to update the item view's horizontal scrolled position. 705 | */ 706 | /*synthetic*/ void baseTranslateItemViewXBy(ViewGroup itemView, float dx) { 707 | if (dx == 0) return; 708 | 709 | final float translationX = itemView.getChildAt(0).getTranslationX() + dx; 710 | final int itemMenuWidth = (int) itemView.getTag(TAG_ITEM_MENU_WIDTH); 711 | 712 | final boolean rtl = Utils.isLayoutRtl(itemView); 713 | if (!rtl && translationX > -itemMenuWidth * 0.05f 714 | || rtl && translationX < itemMenuWidth * 0.05f) { 715 | mOpenedItems.remove(itemView); 716 | 717 | } else if (!mOpenedItems.contains(itemView)) { 718 | mOpenedItems.add(itemView); 719 | } 720 | 721 | final int itemChildCount = itemView.getChildCount(); 722 | for (int i = 0; i < itemChildCount; i++) { 723 | itemView.getChildAt(i).setTranslationX(translationX); 724 | } 725 | 726 | final FrameLayout itemMenu = (FrameLayout) itemView.getChildAt(itemChildCount - 1); 727 | final int[] menuItemWidths = (int[]) itemView.getTag(TAG_MENU_ITEM_WIDTHS); 728 | float menuItemFrameDx = 0; 729 | for (int i = 1, menuItemCount = itemMenu.getChildCount(); i < menuItemCount; i++) { 730 | final FrameLayout menuItemFrame = (FrameLayout) itemMenu.getChildAt(i); 731 | menuItemFrameDx -= dx * (float) menuItemWidths[i - 1] / (float) itemMenuWidth; 732 | menuItemFrame.setTranslationX(menuItemFrame.getTranslationX() + menuItemFrameDx); 733 | } 734 | } 735 | 736 | private static final class TranslateItemViewXAnimator extends ValueAnimator { 737 | final AnimatorListener listener; 738 | 739 | float cachedDeltaTransX; 740 | 741 | TranslateItemViewXAnimator(final SlidingItemMenuRecyclerView parent, final ViewGroup itemView) { 742 | listener = new AnimatorListenerAdapter() { 743 | final SimpleArrayMap childrenLayerTypes = 744 | new SimpleArrayMap<>(0); 745 | 746 | void ensureChildrenLayerTypes() { 747 | final int itemChildCount = itemView.getChildCount(); 748 | final ViewGroup itemMenu = (ViewGroup) itemView.getChildAt( 749 | itemChildCount - 1); 750 | final int menuItemCount = itemMenu.getChildCount(); 751 | 752 | // We do not know whether the cached children are valid or not, so just 753 | // clear the Map and re-put some children into it, of which the layer types 754 | // will also be up-to-date. 755 | childrenLayerTypes.clear(); 756 | childrenLayerTypes.ensureCapacity( 757 | itemChildCount - 1 + menuItemCount); 758 | for (int i = 0; i < itemChildCount - 1; i++) { 759 | final View itemChild = itemView.getChildAt(i); 760 | childrenLayerTypes.put(itemChild, itemChild.getLayerType()); 761 | } 762 | for (int i = 0; i < menuItemCount; i++) { 763 | final View menuItemFrame = itemMenu.getChildAt(i); 764 | childrenLayerTypes.put(menuItemFrame, menuItemFrame.getLayerType()); 765 | } 766 | } 767 | 768 | @SuppressLint("ObsoleteSdkInt") 769 | @Override 770 | public void onAnimationStart(Animator animation) { 771 | ensureChildrenLayerTypes(); 772 | for (int i = childrenLayerTypes.size() - 1; i >= 0; i--) { 773 | final View child = childrenLayerTypes.keyAt(i); 774 | child.setLayerType(LAYER_TYPE_HARDWARE, null); 775 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1 776 | && ViewCompat.isAttachedToWindow(child)) { 777 | child.buildLayer(); 778 | } 779 | } 780 | } 781 | 782 | @Override 783 | public void onAnimationEnd(Animator animation) { 784 | for (int i = childrenLayerTypes.size() - 1; i >= 0; i--) { 785 | childrenLayerTypes.keyAt(i).setLayerType( 786 | childrenLayerTypes.valueAt(i), null); 787 | } 788 | } 789 | }; 790 | addListener(listener); 791 | addUpdateListener(animation -> { 792 | final float deltaTransX = (float) animation.getAnimatedValue(); 793 | parent.baseTranslateItemViewXBy(itemView, deltaTransX - cachedDeltaTransX); 794 | cachedDeltaTransX = deltaTransX; 795 | }); 796 | } 797 | 798 | @Override 799 | public void start() { 800 | // NOTE: 'cachedDeltaTransX' MUST be reset before super.start() is invoked 801 | // for the reason that 'onAnimationUpdate' will be called in the super method 802 | // on platforms prior to Nougat. 803 | cachedDeltaTransX = 0; 804 | super.start(); 805 | } 806 | } 807 | 808 | @Override 809 | protected void onDetachedFromWindow() { 810 | super.onDetachedFromWindow(); 811 | releaseItemViewInternal(mFullyOpenedItem, 0); 812 | if (mOpenedItems.size() > 0) { 813 | final ViewGroup[] openedItems = mOpenedItems.toArray(new ViewGroup[0]); 814 | for (ViewGroup openedItem : openedItems) { 815 | final Animator animator = (Animator) openedItem.getTag(TAG_ITEM_ANIMATOR); 816 | if (animator != null && animator.isRunning()) { 817 | animator.end(); 818 | } 819 | } 820 | mOpenedItems.clear(); 821 | } 822 | } 823 | } -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/src/main/java/com/liuzhenlin/simrv/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Created on 2018/11/6 5:48 PM. 3 | * Copyright © 2018 刘振林. All rights reserved. 4 | */ 5 | 6 | package com.liuzhenlin.simrv; 7 | 8 | import android.view.View; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.core.view.ViewCompat; 12 | 13 | /** 14 | * @author 刘振林 15 | */ 16 | public class Utils { 17 | private Utils() { 18 | } 19 | 20 | /** Lightweight choice to {@link Math#round(float)} */ 21 | public static int roundFloat(float value) { 22 | return (int) (value > 0 ? value + 0.5f : value - 0.5f); 23 | } 24 | 25 | /** Lightweight choice to {@link Math#round(double)} */ 26 | public static long roundDouble(double value) { 27 | return (long) (value > 0 ? value + 0.5 : value - 0.5); 28 | } 29 | 30 | public static boolean isLayoutRtl(@NonNull View view) { 31 | return ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/src/main/java/com/liuzhenlin/simrv/ViscousFluidInterpolator.java: -------------------------------------------------------------------------------- 1 | package com.liuzhenlin.simrv; 2 | 3 | import android.view.animation.Interpolator; 4 | 5 | public class ViscousFluidInterpolator implements Interpolator { 6 | /** Controls the viscous fluid effect (how much of it). */ 7 | private final float mViscousFluidScale; 8 | 9 | private final float mViscousFluidNormalize; 10 | private final float mViscousFluidOffset; 11 | 12 | public ViscousFluidInterpolator() { 13 | this(8.0f); 14 | } 15 | 16 | public ViscousFluidInterpolator(float viscousFluidScale) { 17 | mViscousFluidScale = viscousFluidScale; 18 | // must be set to 1.0 (used in viscousFluid()) 19 | mViscousFluidNormalize = 1.0f / viscousFluid(1.0f); 20 | // account for very small floating-point error 21 | mViscousFluidOffset = 1.0f - mViscousFluidNormalize * viscousFluid(1.0f); 22 | } 23 | 24 | private float viscousFluid(float x) { 25 | x *= mViscousFluidScale; 26 | if (x < 1.0f) { 27 | x -= (1.0f - (float) Math.exp(-x)); 28 | } else { 29 | float start = 0.36787944117f; // 1/e == exp(-1) 30 | x = 1.0f - (float) Math.exp(1.0f - x); 31 | x = start + x * (1.0f - start); 32 | } 33 | return x; 34 | } 35 | 36 | @Override 37 | public float getInterpolation(float input) { 38 | final float interpolated = mViscousFluidNormalize * viscousFluid(input); 39 | if (interpolated > 0) { 40 | return interpolated + mViscousFluidOffset; 41 | } 42 | return interpolated; 43 | } 44 | } -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/src/main/java/com/liuzhenlin/simrv/reservation/ScrollerLinearLayout.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Created on 2017/10/21. 3 | * Copyright © 2017 刘振林. All rights reserved. 4 | */ 5 | 6 | package com.liuzhenlin.simrv.reservation; 7 | 8 | import android.content.Context; 9 | import android.util.AttributeSet; 10 | import android.widget.LinearLayout; 11 | import android.widget.Scroller; 12 | 13 | import androidx.annotation.Nullable; 14 | import androidx.core.view.ViewCompat; 15 | 16 | /** 17 | * @author 刘振林 18 | */ 19 | public class ScrollerLinearLayout extends LinearLayout implements ScrollerView { 20 | private final Scroller mScroller; 21 | 22 | public ScrollerLinearLayout(Context context) { 23 | this(context, null); 24 | } 25 | 26 | public ScrollerLinearLayout(Context context, @Nullable AttributeSet attrs) { 27 | super(context, attrs); 28 | mScroller = new Scroller(context); 29 | } 30 | 31 | public Scroller getScroller() { 32 | return mScroller; 33 | } 34 | 35 | @Override 36 | public void smoothScrollBy(int dx, int dy, int duration) { 37 | if (dx == 0 && dy == 0) { 38 | // Nothing to do. Cancel animation 39 | mScroller.abortAnimation(); 40 | } else { 41 | mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, duration); 42 | invalidate(); 43 | } 44 | } 45 | 46 | @Override 47 | public void smoothScrollTo(int x, int y, int duration) { 48 | final int scrollX = getScrollX(); 49 | final int scrollY = getScrollY(); 50 | 51 | final boolean finished = mScroller.isFinished(); 52 | if (finished && (scrollX != x || scrollY != y) 53 | || !finished && (mScroller.getFinalX() != x || mScroller.getFinalY() != y)) { 54 | 55 | final int deltaX = x - scrollX; 56 | final int deltaY = y - scrollY; 57 | smoothScrollBy(deltaX, deltaY, duration); 58 | } 59 | } 60 | 61 | @Override 62 | public void computeScroll() { 63 | // Override to implement the smooth scrolling logic 64 | if (mScroller.computeScrollOffset()) { 65 | scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 66 | ViewCompat.postInvalidateOnAnimation(this); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/src/main/java/com/liuzhenlin/simrv/reservation/ScrollerView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Created on 2017/10/21. 3 | * Copyright © 2017 刘振林. All rights reserved. 4 | */ 5 | 6 | package com.liuzhenlin.simrv.reservation; 7 | 8 | /** 9 | * @author 刘振林 10 | */ 11 | public interface ScrollerView { 12 | 13 | /** 14 | * Start scrolling by providing the distance to travel and the duration of the scroll. 15 | * 16 | * @param dx Horizontal distance to travel. Positive numbers will scroll the 17 | * content to the left. 18 | * @param dy Vertical distance to travel. Positive numbers will scroll the 19 | * content up. 20 | * @param duration Duration of the scroll in milliseconds. 21 | */ 22 | void smoothScrollBy(int dx, int dy, int duration); 23 | 24 | /** 25 | * Smoothly move the scrolled position of your view to (x, y). 26 | * 27 | * @param x the x position to scroll to 28 | * @param y the y position to scroll to 29 | * @param duration duration of the scroll in milliseconds. 30 | */ 31 | void smoothScrollTo(int x, int y, int duration); 32 | } 33 | -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/src/main/java/com/liuzhenlin/simrv/reservation/TopWrappedDividerItemDecoration.java: -------------------------------------------------------------------------------- 1 | package com.liuzhenlin.simrv.reservation; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Canvas; 7 | import android.graphics.Rect; 8 | import android.graphics.drawable.Drawable; 9 | import android.util.Log; 10 | import android.view.View; 11 | import android.widget.LinearLayout; 12 | 13 | import androidx.annotation.NonNull; 14 | import androidx.recyclerview.widget.LinearLayoutManager; 15 | import androidx.recyclerview.widget.RecyclerView; 16 | 17 | import com.liuzhenlin.simrv.Utils; 18 | 19 | public class TopWrappedDividerItemDecoration extends RecyclerView.ItemDecoration { 20 | public static final int HORIZONTAL = LinearLayout.HORIZONTAL; 21 | public static final int VERTICAL = LinearLayout.VERTICAL; 22 | 23 | private static final String TAG = "TopWrappedDividerItemDecoration"; 24 | private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; 25 | 26 | private Drawable mDivider; 27 | 28 | /** 29 | * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}. 30 | */ 31 | private int mOrientation; 32 | 33 | private final Rect mBounds = new Rect(); 34 | 35 | /** 36 | * Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a 37 | * {@link LinearLayoutManager}. 38 | * 39 | * @param context Current context, it will be used to access resources. 40 | * @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}. 41 | */ 42 | @SuppressLint("LongLogTag") 43 | public TopWrappedDividerItemDecoration(Context context, int orientation) { 44 | final TypedArray a = context.obtainStyledAttributes(ATTRS); 45 | mDivider = a.getDrawable(0); 46 | if (mDivider == null) { 47 | Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this " 48 | + "DividerItemDecoration. Please set that attribute all call setDivider()"); 49 | } 50 | a.recycle(); 51 | setOrientation(orientation); 52 | } 53 | 54 | /** 55 | * @return the orientation of this divider, either {@link #HORIZONTAL} or {@link #VERTICAL} 56 | */ 57 | public int getOrientation() { 58 | return mOrientation; 59 | } 60 | 61 | /** 62 | * Sets the orientation for this divider. This should be called if 63 | * {@link RecyclerView.LayoutManager} changes orientation. 64 | * 65 | * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} 66 | */ 67 | public void setOrientation(int orientation) { 68 | if (orientation != HORIZONTAL && orientation != VERTICAL) { 69 | throw new IllegalArgumentException( 70 | "Invalid orientation. It should be either HORIZONTAL or VERTICAL"); 71 | } 72 | mOrientation = orientation; 73 | } 74 | 75 | /** 76 | * @return the Drawable for this divider 77 | */ 78 | @NonNull 79 | public Drawable getDivider() { 80 | return mDivider; 81 | } 82 | 83 | /** 84 | * Sets the {@link Drawable} for this divider. 85 | * 86 | * @param divider Drawable that should be used as a divider. 87 | */ 88 | public void setDivider(@NonNull Drawable divider) { 89 | mDivider = divider; 90 | } 91 | 92 | @Override 93 | public void onDraw( 94 | @NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { 95 | if (parent.getLayoutManager() == null || mDivider == null) 96 | return; 97 | if (mOrientation == VERTICAL) 98 | drawVertical(c, parent); 99 | else 100 | drawHorizontal(c, parent); 101 | } 102 | 103 | private void drawVertical(Canvas canvas, RecyclerView parent) { 104 | canvas.save(); 105 | final int left, right; 106 | if (parent.getClipToPadding()) { 107 | left = parent.getPaddingLeft(); 108 | right = parent.getWidth() - parent.getPaddingRight(); 109 | canvas.clipRect(left, parent.getPaddingTop(), right, 110 | parent.getHeight() - parent.getPaddingBottom()); 111 | } else { 112 | left = 0; 113 | right = parent.getWidth(); 114 | } 115 | 116 | final int childCount = parent.getChildCount(); 117 | for (int i = 0; i < childCount; i++) { 118 | View child = parent.getChildAt(i); 119 | parent.getDecoratedBoundsWithMargins(child, mBounds); 120 | final int bottom = mBounds.bottom + Utils.roundFloat(child.getTranslationY()); 121 | final int top = bottom - mDivider.getIntrinsicHeight(); 122 | mDivider.setBounds(left, top, right, bottom); 123 | mDivider.draw(canvas); 124 | // Draw the divider for RecyclerView's top edge 125 | if (i == 0) { 126 | mDivider.setBounds(left, parent.getPaddingTop(), right, 127 | parent.getPaddingTop() + mDivider.getIntrinsicHeight()); 128 | mDivider.draw(canvas); 129 | } 130 | } 131 | canvas.restore(); 132 | } 133 | 134 | private void drawHorizontal(Canvas canvas, RecyclerView parent) { 135 | canvas.save(); 136 | final int top, bottom; 137 | if (parent.getClipToPadding()) { 138 | top = parent.getPaddingTop(); 139 | bottom = parent.getHeight() - parent.getPaddingBottom(); 140 | canvas.clipRect(parent.getPaddingLeft(), top, 141 | parent.getWidth() - parent.getPaddingRight(), bottom); 142 | } else { 143 | top = 0; 144 | bottom = parent.getHeight(); 145 | } 146 | 147 | final int childCount = parent.getChildCount(); 148 | for (int i = 0; i < childCount; i++) { 149 | View child = parent.getChildAt(i); 150 | parent.getDecoratedBoundsWithMargins(child, mBounds); 151 | final int right = mBounds.right + Utils.roundFloat(child.getTranslationX()); 152 | final int left = right - mDivider.getIntrinsicWidth(); 153 | mDivider.setBounds(left, top, right, bottom); 154 | mDivider.draw(canvas); 155 | // Draw the divider for RecyclerView's horizontal start edge 156 | if (i == 0) { 157 | mDivider.setBounds(parent.getPaddingLeft(), top, 158 | parent.getPaddingLeft() + mDivider.getIntrinsicWidth(), bottom); 159 | mDivider.draw(canvas); 160 | } 161 | } 162 | canvas.restore(); 163 | } 164 | 165 | @Override 166 | public void getItemOffsets( 167 | @NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, 168 | @NonNull RecyclerView.State state) { 169 | if (mDivider == null) { 170 | outRect.set(0, 0, 0, 0); 171 | return; 172 | } 173 | if (mOrientation == VERTICAL) { 174 | final int dividerHeight = mDivider.getIntrinsicHeight(); 175 | if (parent.getChildAdapterPosition(view) == 0) { 176 | outRect.set(0, dividerHeight, 0, dividerHeight); 177 | } else { 178 | outRect.set(0, 0, 0, dividerHeight); 179 | } 180 | } else { 181 | final int dividerWidth = mDivider.getIntrinsicWidth(); 182 | if (parent.getChildAdapterPosition(view) == 0) { 183 | outRect.set(dividerWidth, 0, dividerWidth, 0); 184 | } else { 185 | outRect.set(0, 0, dividerWidth, 0); 186 | } 187 | } 188 | } 189 | } -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/src/main/res/drawable-v21/default_selector_recycler_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/src/main/res/drawable/default_selector_recycler_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #E0E0E0 4 | #E6E6E6 5 | #C9C9C9 6 | 7 | -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/src/main/res/values/tags.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /slidingitemmenu-recyclerview/src/test/java/com/liuzhenlin/simrv/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.liuzhenlin.simrv; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } --------------------------------------------------------------------------------