├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── cardstackview ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── yuyakaido │ │ └── android │ │ └── cardstackview │ │ ├── CardStackLayoutManager.java │ │ ├── CardStackListener.java │ │ ├── CardStackView.java │ │ ├── Direction.java │ │ ├── Duration.java │ │ ├── RewindAnimationSetting.java │ │ ├── StackFrom.java │ │ ├── SwipeAnimationSetting.java │ │ ├── SwipeableMethod.java │ │ └── internal │ │ ├── AnimationSetting.java │ │ ├── CardStackDataObserver.java │ │ ├── CardStackSetting.java │ │ ├── CardStackSmoothScroller.java │ │ ├── CardStackSnapHelper.java │ │ ├── CardStackState.java │ │ └── DisplayUtil.java │ └── res │ └── layout │ └── overlay.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sample ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── yuyakaido │ │ └── android │ │ └── cardstackview │ │ └── sample │ │ ├── CardStackAdapter.kt │ │ ├── MainActivity.kt │ │ ├── Spot.kt │ │ └── SpotDiffCallback.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── gradation_black.xml │ ├── ic_launcher_background.xml │ ├── like_green_24dp.xml │ ├── like_white_120dp.xml │ ├── overlay_black.xml │ ├── rewind_blue_24dp.xml │ ├── skip_red_24dp.xml │ └── skip_white_120dp.xml │ ├── layout │ ├── activity_main.xml │ └── item_spot.xml │ ├── menu │ └── navigation_main_activity.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 │ ├── overlay_left.png │ └── overlay_right.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ └── strings.xml └── settings.gradle /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # https://circleci.com/docs/2.0/language-android/ 2 | 3 | version: 2 4 | jobs: 5 | build: 6 | working_directory: ~/code 7 | docker: 8 | - image: circleci/android:api-28-alpha 9 | environment: 10 | JVM_OPTS: -Xmx3200m 11 | steps: 12 | - checkout 13 | - restore_cache: 14 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "cardstackview/build.gradle" }} 15 | - run: 16 | name: Download Dependencies 17 | command: ./gradlew androidDependencies 18 | - save_cache: 19 | paths: 20 | - ~/.gradle 21 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "cardstackview/build.gradle" }} 22 | - run: 23 | name: Run Tests 24 | command: ./gradlew lint test 25 | - store_artifacts: 26 | path: cardstackview/build/reports 27 | destination: reports 28 | - store_test_results: 29 | path: cardstackview/build/test-results -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Mac OS X 29 | .DS_Store 30 | 31 | # IntelliJ IDEA 32 | *.iml 33 | *.ipr 34 | *.iws 35 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 yuyakaido 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-logo.png) 2 | 3 | # CardStackView 4 | 5 | ![Platform](http://img.shields.io/badge/platform-android-blue.svg?style=flat) 6 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 7 | [![API](https://img.shields.io/badge/API-14%2B-blue.svg?style=flat)](https://android-arsenal.com/api?level=14) 8 | [![AndroidArsenal](https://img.shields.io/badge/Android%20Arsenal-CardStackView-blue.svg?style=flat)](https://android-arsenal.com/details/1/6075) 9 | [![CircleCI](https://circleci.com/gh/yuyakaido/CardStackView.svg?style=svg)](https://circleci.com/gh/yuyakaido/CardStackView) 10 | 11 | # Overview 12 | 13 | ![Overview](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-overview.gif) 14 | 15 | # Contents 16 | 17 | - [Setup](#setup) 18 | - [Features](#features) 19 | - [Manual Swipe](#manual-swipe) 20 | - [Automatic Swipe](#automatic-swipe) 21 | - [Cancel](#cancel) 22 | - [Rewind](#rewind) 23 | - [Overlay View](#overlay-view) 24 | - [Overlay Interpolator](#overlay-interpolator) 25 | - [Paging](#paging) 26 | - [Reloading](#reloading) 27 | - [Stack From](#stack-from) 28 | - [Visible Count](#visible-count) 29 | - [Translation Interval](#translation-interval) 30 | - [Scale Interval](#scale-interval) 31 | - [Swipe Threshold](#swipe-threshold) 32 | - [Max Degree](#max-degree) 33 | - [Swipe Direction](#swipe-direction) 34 | - [Swipe Restriction](#swipe-restriction) 35 | - [Swipeable Method](#swipeable-method) 36 | - [Public Interfaces](#public-interfaces) 37 | - [Callbacks](#callbacks) 38 | - [Migration Guide](#migration-guide) 39 | - [Installation](#installation) 40 | - [License](#license) 41 | 42 | # Setup 43 | 44 | ```kotlin 45 | val cardStackView = findViewById(R.id.card_stack_view) 46 | cardStackView.layoutManager = CardStackLayoutManager() 47 | cardStackView.adapter = CardStackAdapter() 48 | ``` 49 | 50 | # Features 51 | 52 | ## Manual Swipe 53 | 54 | ![ManualSwipe](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-manual-swipe.gif) 55 | 56 | ## Automatic Swipe 57 | 58 | ![AutomaticSwipe](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-automatic-swipe.gif) 59 | 60 | ```kotlin 61 | CardStackView.swipe() 62 | ``` 63 | 64 | You can set custom swipe animation. 65 | 66 | ```kotlin 67 | val setting = SwipeAnimationSetting.Builder() 68 | .setDirection(Direction.Right) 69 | .setDuration(Duration.Normal.duration) 70 | .setInterpolator(AccelerateInterpolator()) 71 | .build() 72 | CardStackLayoutManager.setSwipeAnimationSetting(setting) 73 | CardStackView.swipe() 74 | ``` 75 | 76 | ## Cancel 77 | 78 | Manual swipe is canceled when the card is dragged less than threshold. 79 | 80 | ![Cancel](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-cancel.gif) 81 | 82 | ## Rewind 83 | 84 | ![Rewind](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-rewind.gif) 85 | 86 | ```kotlin 87 | CardStackView.rewind() 88 | ``` 89 | 90 | You can set custom rewind animation. 91 | 92 | ```kotlin 93 | val setting = RewindAnimationSetting.Builder() 94 | .setDirection(Direction.Bottom) 95 | .setDuration(Duration.Normal.duration) 96 | .setInterpolator(DecelerateInterpolator()) 97 | .build() 98 | CardStackLayoutManager.setRewindAnimationSetting(setting) 99 | CardStackView.rewind() 100 | ``` 101 | 102 | ## Overlay View 103 | 104 | | Value | Sample | 105 | | :----: | :----: | 106 | | Left | ![Overlay-Left](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-overlay-left.png) | 107 | | Right | ![Overlay-Right](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-overlay-right.png) | 108 | 109 | Put overlay view in your item layout of RecyclerView. 110 | 111 | ```xml 112 | 116 | 117 | 118 | 119 | 120 | ``` 121 | 122 | | Value | Layout ID | 123 | | :----: | :----: | 124 | | Left | left_overlay | 125 | | Right | right_overlay | 126 | | Top | top_overlay | 127 | | Bottom | bottom_overlay | 128 | 129 | ## Overlay Interpolator 130 | 131 | You can set own interpolator to define the rate of change of alpha. 132 | 133 | ```kotlin 134 | CardStackLayoutManager.setOverlayInterpolator(LinearInterpolator()) 135 | ``` 136 | 137 | ## Paging 138 | 139 | You can implement paging by using following two ways. 140 | 141 | 1. Use [DiffUtil](https://developer.android.com/reference/android/support/v7/util/DiffUtil). 142 | 2. Call [RecyclerView.Adapter.notifyItemRangeInserted](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter#notifyItemRangeInserted(int,%20int)) manually. 143 | 144 | **Caution** 145 | 146 | You should **NOT** call `RecyclerView.Adapter.notifyDataSetChanged` for paging because this method will reset top position and maybe occur a perfomance issue. 147 | 148 | ## Reloading 149 | 150 | You can implement reloading by calling `RecyclerView.Adapter.notifyDataSetChanged`. 151 | 152 | ## Stack From 153 | 154 | | Default | Value | Sample | 155 | | :----: | :----: | :----: | 156 | | ✅ | None | ![StackFrom-None](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-stack-from-none.png) | 157 | | | Top | ![StackFrom-Top](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-stack-from-top.png) | 158 | | | Bottom | ![StackFrom-Bottom](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-stack-from-bottom.png) | 159 | | | Left | ![StackFrom-Left](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-stack-from-left.png) | 160 | | | Right | ![StackFrom-Right](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-stack-from-right.png) | 161 | 162 | ```kotlin 163 | CardStackLayoutManager.setStackFrom(StackFrom.None) 164 | ``` 165 | 166 | ## Visible Count 167 | 168 | | Default | Value | Sample | 169 | | :----: | :----: | :----: | 170 | | | 2 | ![VisibleCount-2](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-visible-count-2.png) | 171 | | ✅ | 3 | ![VisibleCount-3](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-visible-count-3.png) | 172 | | | 4 | ![VisibleCount-4](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-visible-count-4.png) | 173 | 174 | ```kotlin 175 | CardStackLayoutManager.setVisibleCount(3) 176 | ``` 177 | 178 | ## Translation Interval 179 | 180 | | Default | Value | Sample | 181 | | :----: | :----: | :----: | 182 | | | 4dp | ![TranslationInterval-4dp](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-translation-interval-4dp.png) | 183 | | ✅ | 8dp | ![TranslationInterval-8dp](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-translation-interval-8dp.png) | 184 | | | 12dp | ![TranslationInterval-12dp](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-translation-interval-12dp.png) | 185 | 186 | ```kotlin 187 | CardStackLayoutManager.setTranslationInterval(8.0f) 188 | ``` 189 | 190 | ## Scale Interval 191 | 192 | | Default | Value | Sample | 193 | | :----: | :----: | :----: | 194 | | ✅ | 95% | ![ScaleInterval-95%](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-scale-interval-95.png) | 195 | | | 90% | ![ScaleInterval-90%](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-scale-interval-90.png) | 196 | 197 | ```kotlin 198 | CardStackLayoutManager.setScaleInterval(0.95f) 199 | ``` 200 | 201 | ## Max Degree 202 | 203 | | Default | Value | Sample | 204 | | :----: | :----: | :----: | 205 | | ✅ | 20° | ![MaxDegree-20](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-max-degree-20.png) | 206 | | | 0° | ![MaxDegree-0](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-max-degree-0.png) | 207 | 208 | ```kotlin 209 | CardStackLayoutManager.setMaxDegree(20.0f) 210 | ``` 211 | 212 | ## Swipe Direction 213 | 214 | | Default | Value | Sample | 215 | | :----: | :----: | :----: | 216 | | ✅ | Horizontal | ![SwipeDirection-Horizontal](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipe-direction-horizontal.gif) | 217 | | | Vertical | ![SwipeDirection-Vertical](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipe-direction-vertical.gif) | 218 | | | Freedom | ![SwipeDirection-Freedom](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipe-direction-freedom.gif) | 219 | 220 | ```kotlin 221 | CardStackLayoutManager.setDirections(Direction.HORIZONTAL) 222 | ``` 223 | 224 | ## Swipe Threshold 225 | 226 | | Default | Value | Sample | 227 | | :----: | :----: | :----: | 228 | | ✅ | 30% | ![SwipeThreshold-30%](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipe-threshold-30.gif) | 229 | | | 10% | ![SwipeThreshold-10%](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipe-threshold-10.gif) | 230 | 231 | ```kotlin 232 | CardStackLayoutManager.setSwipeThreshold(0.3f) 233 | ``` 234 | 235 | ## Swipe Restriction 236 | 237 | | CanScrollHorizontal | CanScrollVertical | Sample | 238 | | :----: | :----: | :----: | 239 | | true | true | ![SwipeRestriction-NoRestriction](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipe-restriction-no-restriction.gif) | 240 | | true | false | ![SwipeRestriction-CanScrollHorizontalOnly](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipe-restriction-can-scroll-horizontal-only.gif) | 241 | | false | true | ![SwipeRestriction-CanScrollVerticalOnly](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipe-restriction-can-scroll-vertical-only.gif) | 242 | | false | false | ![SwipeRestriction-CannotSwipe](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipe-restriction-cannot-swipe.gif) | 243 | 244 | ```kotlin 245 | CardStackLayoutManager.setCanScrollHorizontal(true) 246 | CardStackLayoutManager.setCanScrollVertical(true) 247 | ``` 248 | 249 | ## Swipeable Method 250 | 251 | | Default | Value | Sample | 252 | | :----: | :----: | :----: | 253 | | ✅ | AutomaticAndManual | ![SwipeableMethod-AutomaticAndManual](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipeable-method-automatic-and-manual.gif) | 254 | | | Automatic | ![SwipwableMethod-Automatic](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipeable-method-automatic.gif) | 255 | | | Manual | ![SwipwableMethod-Manual](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipeable-method-manual.gif) | 256 | | | None | ![SwipwableMethod-None](https://github.com/yuyakaido/images/blob/master/CardStackView/sample-swipeable-method-none.gif) | 257 | 258 | ```kotlin 259 | CardStackLayoutManager.setSwipeableMethod(SwipeableMethod.AutomaticAndManual) 260 | ``` 261 | 262 | # Public Interfaces 263 | 264 | ## Basic usages 265 | 266 | | Method | Description | 267 | | :---- | :---- | 268 | | CardStackView.swipe() | You can swipe once by calling this method. | 269 | | CardStackView.rewind() | You can rewind once by calling this method. | 270 | | CardStackLayoutManager.getTopPosition() | You can get position displayed on top. | 271 | | CardStackLayoutManager.setStackFrom(StackFrom stackFrom) | You can set StackFrom. | 272 | | CardStackLayoutManager.setTranslationInterval(float translationInterval) | You can set TranslationInterval. | 273 | | CardStackLayoutManager.setScaleInterval(float scaleInterval) | You can set ScaleInterval. | 274 | | CardStackLayoutManager.setSwipeThreshold(float swipeThreshold) | You can set SwipeThreshold. | 275 | | CardStackLayoutManager.setMaxDegree(float maxDegree) | You can set MaxDegree. | 276 | | CardStackLayoutManager.setDirections(List directions) | You can set Direction. | 277 | | CardStackLayoutManager.setCanScrollHorizontal(boolean canScrollHorizontal) | You can set CanScrollHorizontal. | 278 | | CardStackLayoutManager.setCanScrollVertical(boolean canScrollVertical) | You can set CanScrollVertical. | 279 | | CardStackLayoutManager.setSwipeAnimationSetting(SwipeAnimationSetting swipeAnimationSetting) | You can set SwipeAnimationSetting. | 280 | | CardStackLayoutManager.setRewindAnimationSetting(RewindAnimationSetting rewindAnimationSetting) | You can set RewindAnimationSetting. | 281 | 282 | ## Advanced usages 283 | 284 | | Method | Description | 285 | | :---- | :---- | 286 | | CardStackView.smoothScrollToPosition(int position) | You can scroll any position with animation. | 287 | | CardStackView.scrollToPosition(int position) | You can scroll any position without animation. | 288 | 289 | # Callbacks 290 | 291 | | Method | Description | 292 | | :---- | :---- | 293 | | CardStackListener.onCardDragging(Direction direction, float ratio) | This method is called while the card is dragging. | 294 | | CardStackListener.onCardSwiped(Direction direction) | This method is called when the card is swiped. | 295 | | CardStackListener.onCardRewound() | This method is called when the card is rewinded. | 296 | | CardStackListener.onCardCanceled() | This method is called when the card is dragged less than threshold. | 297 | | CardStackListener.onCardAppeared(View view, int position) | This method is called when the card appeared. | 298 | | CardStackListener.onCardDisappeared(View view, int position) | This method is called when the card disappeared. | 299 | 300 | # Migration Guide 301 | 302 | ## Migration of Features 303 | 304 | | 1.x | 2.x | 305 | | :---- | :---- | 306 | | Move to Origin | [Cancel](#cancel) | 307 | | Reverse | [Rewind](#rewind) | 308 | | ElevationEnabled | [Stack From](#stack-from) | 309 | | TranslationDiff | [Translation Interval](#translation-interval) | 310 | | ScaleDiff | [Scale Interval](#scale-interval) | 311 | | SwipeEnabled | [Swipe Restriction](#swipe-restriction) | 312 | 313 | ## Migration of Callbacks 314 | 315 | | 1.x | 2.x | 316 | | :---- | :---- | 317 | | CardStackView.CardEventListener | CardStackListener | 318 | | onCardDragging(float percentX, float percentY) | onCardDragging(Direction direction, float ratio) | 319 | | onCardSwiped(SwipeDirection direction) | onCardSwiped(Direction direction) | 320 | | onCardReversed() | onCardRewound() | 321 | | onCardMovedToOrigin() | onCardCanceled() | 322 | | onCardClicked(int index) | This method is no longer provided. Please implement in your item of RecyclerView. | 323 | 324 | # Installation 325 | 326 | ```groovy 327 | dependencies { 328 | implementation "com.yuyakaido.android:card-stack-view:2.3.4" 329 | } 330 | ``` 331 | 332 | # License 333 | 334 | ``` 335 | Copyright 2018 yuyakaido 336 | 337 | Licensed under the Apache License, Version 2.0 (the "License"); 338 | you may not use this file except in compliance with the License. 339 | You may obtain a copy of the License at 340 | 341 | http://www.apache.org/licenses/LICENSE-2.0 342 | 343 | Unless required by applicable law or agreed to in writing, software 344 | distributed under the License is distributed on an "AS IS" BASIS, 345 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 346 | See the License for the specific language governing permissions and 347 | limitations under the License. 348 | ``` 349 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.7.10' 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:7.3.0' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | mavenCentral() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /cardstackview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 33 5 | 6 | defaultConfig { 7 | minSdkVersion 14 8 | targetSdkVersion 33 9 | } 10 | } 11 | 12 | dependencies { 13 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 14 | } 15 | -------------------------------------------------------------------------------- /cardstackview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/CardStackLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview; 2 | 3 | import android.content.Context; 4 | import android.graphics.PointF; 5 | import android.os.Handler; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.view.animation.Interpolator; 9 | 10 | import androidx.annotation.FloatRange; 11 | import androidx.annotation.IntRange; 12 | import androidx.annotation.NonNull; 13 | import androidx.recyclerview.widget.RecyclerView; 14 | 15 | import com.yuyakaido.android.cardstackview.internal.CardStackSetting; 16 | import com.yuyakaido.android.cardstackview.internal.CardStackSmoothScroller; 17 | import com.yuyakaido.android.cardstackview.internal.CardStackState; 18 | import com.yuyakaido.android.cardstackview.internal.DisplayUtil; 19 | 20 | import java.util.List; 21 | 22 | public class CardStackLayoutManager 23 | extends RecyclerView.LayoutManager 24 | implements RecyclerView.SmoothScroller.ScrollVectorProvider { 25 | 26 | private final Context context; 27 | 28 | private CardStackListener listener = CardStackListener.DEFAULT; 29 | private CardStackSetting setting = new CardStackSetting(); 30 | private CardStackState state = new CardStackState(); 31 | 32 | public CardStackLayoutManager(Context context) { 33 | this(context, CardStackListener.DEFAULT); 34 | } 35 | 36 | public CardStackLayoutManager(Context context, CardStackListener listener) { 37 | this.context = context; 38 | this.listener = listener; 39 | } 40 | 41 | @Override 42 | public RecyclerView.LayoutParams generateDefaultLayoutParams() { 43 | return new RecyclerView.LayoutParams( 44 | ViewGroup.LayoutParams.MATCH_PARENT, 45 | ViewGroup.LayoutParams.MATCH_PARENT 46 | ); 47 | } 48 | 49 | @Override 50 | public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State s) { 51 | update(recycler); 52 | if (s.didStructureChange()) { 53 | View topView = getTopView(); 54 | if (topView != null) { 55 | listener.onCardAppeared(getTopView(), state.topPosition); 56 | } 57 | } 58 | } 59 | 60 | @Override 61 | public boolean canScrollHorizontally() { 62 | return setting.swipeableMethod.canSwipe() && setting.canScrollHorizontal; 63 | } 64 | 65 | @Override 66 | public boolean canScrollVertically() { 67 | return setting.swipeableMethod.canSwipe() && setting.canScrollVertical; 68 | } 69 | 70 | @Override 71 | public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State s) { 72 | if (state.topPosition == getItemCount()) { 73 | return 0; 74 | } 75 | 76 | switch (state.status) { 77 | case Idle: 78 | if (setting.swipeableMethod.canSwipeManually()) { 79 | state.dx -= dx; 80 | update(recycler); 81 | return dx; 82 | } 83 | break; 84 | case Dragging: 85 | if (setting.swipeableMethod.canSwipeManually()) { 86 | state.dx -= dx; 87 | update(recycler); 88 | return dx; 89 | } 90 | break; 91 | case RewindAnimating: 92 | state.dx -= dx; 93 | update(recycler); 94 | return dx; 95 | case AutomaticSwipeAnimating: 96 | if (setting.swipeableMethod.canSwipeAutomatically()) { 97 | state.dx -= dx; 98 | update(recycler); 99 | return dx; 100 | } 101 | break; 102 | case AutomaticSwipeAnimated: 103 | break; 104 | case ManualSwipeAnimating: 105 | if (setting.swipeableMethod.canSwipeManually()) { 106 | state.dx -= dx; 107 | update(recycler); 108 | return dx; 109 | } 110 | break; 111 | case ManualSwipeAnimated: 112 | break; 113 | } 114 | 115 | return 0; 116 | } 117 | 118 | @Override 119 | public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State s) { 120 | if (state.topPosition == getItemCount()) { 121 | return 0; 122 | } 123 | 124 | switch (state.status) { 125 | case Idle: 126 | if (setting.swipeableMethod.canSwipeManually()) { 127 | state.dy -= dy; 128 | update(recycler); 129 | return dy; 130 | } 131 | break; 132 | case Dragging: 133 | if (setting.swipeableMethod.canSwipeManually()) { 134 | state.dy -= dy; 135 | update(recycler); 136 | return dy; 137 | } 138 | break; 139 | case RewindAnimating: 140 | state.dy -= dy; 141 | update(recycler); 142 | return dy; 143 | case AutomaticSwipeAnimating: 144 | if (setting.swipeableMethod.canSwipeAutomatically()) { 145 | state.dy -= dy; 146 | update(recycler); 147 | return dy; 148 | } 149 | break; 150 | case AutomaticSwipeAnimated: 151 | break; 152 | case ManualSwipeAnimating: 153 | if (setting.swipeableMethod.canSwipeManually()) { 154 | state.dy -= dy; 155 | update(recycler); 156 | return dy; 157 | } 158 | break; 159 | case ManualSwipeAnimated: 160 | break; 161 | } 162 | return 0; 163 | } 164 | 165 | @Override 166 | public void onScrollStateChanged(int s) { 167 | switch (s) { 168 | // スクロールが止まったタイミング 169 | case RecyclerView.SCROLL_STATE_IDLE: 170 | if (state.targetPosition == RecyclerView.NO_POSITION) { 171 | // Swipeが完了した場合の処理 172 | state.next(CardStackState.Status.Idle); 173 | state.targetPosition = RecyclerView.NO_POSITION; 174 | } else if (state.topPosition == state.targetPosition) { 175 | // Rewindが完了した場合の処理 176 | state.next(CardStackState.Status.Idle); 177 | state.targetPosition = RecyclerView.NO_POSITION; 178 | } else { 179 | // 2枚以上のカードを同時にスワイプする場合の処理 180 | if (state.topPosition < state.targetPosition) { 181 | // 1枚目のカードをスワイプすると一旦SCROLL_STATE_IDLEが流れる 182 | // そのタイミングで次のアニメーションを走らせることで連続でスワイプしているように見せる 183 | smoothScrollToNext(state.targetPosition); 184 | } else { 185 | // Nextの場合と同様に、1枚目の処理が完了したタイミングで次のアニメーションを走らせる 186 | smoothScrollToPrevious(state.targetPosition); 187 | } 188 | } 189 | break; 190 | // カードをドラッグしている最中 191 | case RecyclerView.SCROLL_STATE_DRAGGING: 192 | if (setting.swipeableMethod.canSwipeManually()) { 193 | state.next(CardStackState.Status.Dragging); 194 | } 195 | break; 196 | // カードが指から離れたタイミング 197 | case RecyclerView.SCROLL_STATE_SETTLING: 198 | break; 199 | } 200 | } 201 | 202 | @Override 203 | public PointF computeScrollVectorForPosition(int targetPosition) { 204 | return null; 205 | } 206 | 207 | @Override 208 | public void scrollToPosition(int position) { 209 | if (setting.swipeableMethod.canSwipeAutomatically()) { 210 | if (state.canScrollToPosition(position, getItemCount())) { 211 | state.topPosition = position; 212 | requestLayout(); 213 | } 214 | } 215 | } 216 | 217 | @Override 218 | public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State s, int position) { 219 | if (setting.swipeableMethod.canSwipeAutomatically()) { 220 | if (state.canScrollToPosition(position, getItemCount())) { 221 | smoothScrollToPosition(position); 222 | } 223 | } 224 | } 225 | 226 | @NonNull 227 | public CardStackSetting getCardStackSetting() { 228 | return setting; 229 | } 230 | 231 | @NonNull 232 | public CardStackState getCardStackState() { 233 | return state; 234 | } 235 | 236 | @NonNull 237 | public CardStackListener getCardStackListener() { 238 | return listener; 239 | } 240 | 241 | void updateProportion(float x, float y) { 242 | if (getTopPosition() < getItemCount()) { 243 | View view = findViewByPosition(getTopPosition()); 244 | if (view != null) { 245 | float half = getHeight() / 2.0f; 246 | state.proportion = -(y - half - view.getTop()) / half; 247 | } 248 | } 249 | } 250 | 251 | private void update(RecyclerView.Recycler recycler) { 252 | state.width = getWidth(); 253 | state.height = getHeight(); 254 | 255 | if (state.isSwipeCompleted()) { 256 | // ■ 概要 257 | // スワイプが完了したタイミングで、スワイプ済みのViewをキャッシュから削除する 258 | // キャッシュの削除を行わないと、次回更新時にスワイプ済みのカードが表示されてしまう 259 | // スワイプ済みカードが表示される場合、データソースは正しく、表示だけが古い状態になっている 260 | // 261 | // ■ 再現手順 262 | // 1. `removeAndRecycleView(getTopView(), recycler);`をコメントアウトする 263 | // 2. VisibleCount=1に設定し、最後のカードがスワイプされたらページングを行うようにする 264 | // 3. カードを1枚だけ画面に表示する(このカードをAとする) 265 | // 4. Aをスワイプする 266 | // 5. カードを1枚だけ画面に表示する(このカードをBとする) 267 | // 6. ページング完了後はBが表示されるはずが、Aが画面に表示される 268 | removeAndRecycleView(getTopView(), recycler); 269 | 270 | final Direction direction = state.getDirection(); 271 | 272 | state.next(state.status.toAnimatedStatus()); 273 | state.topPosition++; 274 | state.dx = 0; 275 | state.dy = 0; 276 | if (state.topPosition == state.targetPosition) { 277 | state.targetPosition = RecyclerView.NO_POSITION; 278 | } 279 | 280 | /* Handlerを経由してイベント通知を行っているのは、以下のエラーを回避するため 281 | * 282 | * 2019-03-31 18:44:29.744 8496-8496/com.yuyakaido.android.cardstackview.sample E/AndroidRuntime: FATAL EXCEPTION: main 283 | * Process: com.yuyakaido.android.cardstackview.sample, PID: 8496 284 | * java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling com.yuyakaido.android.cardstackview.CardStackView{9d8ff78 VFED..... .F....ID 0,0-1080,1353 #7f080027 app:id/card_stack_view}, adapter:com.yuyakaido.android.cardstackview.sample.CardStackAdapter@e0b8651, layout:com.yuyakaido.android.cardstackview.CardStackLayoutManager@17b0eb6, context:com.yuyakaido.android.cardstackview.sample.MainActivity@fe550ca 285 | * at android.support.v7.widget.RecyclerView.assertNotInLayoutOrScroll(RecyclerView.java:2880) 286 | * at android.support.v7.widget.RecyclerView$RecyclerViewDataObserver.onItemRangeInserted(RecyclerView.java:5300) 287 | * at android.support.v7.widget.RecyclerView$AdapterDataObservable.notifyItemRangeInserted(RecyclerView.java:12022) 288 | * at android.support.v7.widget.RecyclerView$Adapter.notifyItemRangeInserted(RecyclerView.java:7214) 289 | * at android.support.v7.util.AdapterListUpdateCallback.onInserted(AdapterListUpdateCallback.java:42) 290 | * at android.support.v7.util.BatchingListUpdateCallback.dispatchLastEvent(BatchingListUpdateCallback.java:61) 291 | * at android.support.v7.util.DiffUtil$DiffResult.dispatchUpdatesTo(DiffUtil.java:852) 292 | * at android.support.v7.util.DiffUtil$DiffResult.dispatchUpdatesTo(DiffUtil.java:802) 293 | * at com.yuyakaido.android.cardstackview.sample.MainActivity.paginate(MainActivity.kt:164) 294 | * at com.yuyakaido.android.cardstackview.sample.MainActivity.onCardSwiped(MainActivity.kt:50) 295 | * at com.yuyakaido.android.cardstackview.CardStackLayoutManager.update(CardStackLayoutManager.java:277) 296 | * at com.yuyakaido.android.cardstackview.CardStackLayoutManager.scrollHorizontallyBy(CardStackLayoutManager.java:92) 297 | * at android.support.v7.widget.RecyclerView.scrollStep(RecyclerView.java:1829) 298 | * at android.support.v7.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:5067) 299 | * at android.view.Choreographer$CallbackRecord.run(Choreographer.java:911) 300 | * at android.view.Choreographer.doCallbacks(Choreographer.java:723) 301 | * at android.view.Choreographer.doFrame(Choreographer.java:655) 302 | * at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:897) 303 | * at android.os.Handler.handleCallback(Handler.java:789) 304 | * at android.os.Handler.dispatchMessage(Handler.java:98) 305 | * at android.os.Looper.loop(Looper.java:164) 306 | * at android.app.ActivityThread.main(ActivityThread.java:6541) 307 | * at java.lang.reflect.Method.invoke(Native Method) 308 | * at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) 309 | * at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767) 310 | */ 311 | new Handler().post(new Runnable() { 312 | @Override 313 | public void run() { 314 | listener.onCardSwiped(direction); 315 | View topView = getTopView(); 316 | if (topView != null) { 317 | listener.onCardAppeared(getTopView(), state.topPosition); 318 | } 319 | } 320 | }); 321 | } 322 | 323 | detachAndScrapAttachedViews(recycler); 324 | 325 | final int parentTop = getPaddingTop(); 326 | final int parentLeft = getPaddingLeft(); 327 | final int parentRight = getWidth() - getPaddingLeft(); 328 | final int parentBottom = getHeight() - getPaddingBottom(); 329 | for (int i = state.topPosition; i < state.topPosition + setting.visibleCount && i < getItemCount(); i++) { 330 | View child = recycler.getViewForPosition(i); 331 | addView(child, 0); 332 | measureChildWithMargins(child, 0, 0); 333 | layoutDecoratedWithMargins(child, parentLeft, parentTop, parentRight, parentBottom); 334 | 335 | resetTranslation(child); 336 | resetScale(child); 337 | resetRotation(child); 338 | resetOverlay(child); 339 | 340 | if (i == state.topPosition) { 341 | updateTranslation(child); 342 | resetScale(child); 343 | updateRotation(child); 344 | updateOverlay(child); 345 | } else { 346 | int currentIndex = i - state.topPosition; 347 | updateTranslation(child, currentIndex); 348 | updateScale(child, currentIndex); 349 | resetRotation(child); 350 | resetOverlay(child); 351 | } 352 | } 353 | 354 | if (state.status.isDragging()) { 355 | listener.onCardDragging(state.getDirection(), state.getRatio()); 356 | } 357 | } 358 | 359 | private void updateTranslation(View view) { 360 | view.setTranslationX(state.dx); 361 | view.setTranslationY(state.dy); 362 | } 363 | 364 | private void updateTranslation(View view, int index) { 365 | int nextIndex = index - 1; 366 | int translationPx = DisplayUtil.dpToPx(context, setting.translationInterval); 367 | float currentTranslation = index * translationPx; 368 | float nextTranslation = nextIndex * translationPx; 369 | float targetTranslation = currentTranslation - (currentTranslation - nextTranslation) * state.getRatio(); 370 | switch (setting.stackFrom) { 371 | case None: 372 | // Do nothing 373 | break; 374 | case Top: 375 | view.setTranslationY(-targetTranslation); 376 | break; 377 | case TopAndLeft: 378 | view.setTranslationY(-targetTranslation); 379 | view.setTranslationX(-targetTranslation); 380 | break; 381 | case TopAndRight: 382 | view.setTranslationY(-targetTranslation); 383 | view.setTranslationX(targetTranslation); 384 | break; 385 | case Bottom: 386 | view.setTranslationY(targetTranslation); 387 | break; 388 | case BottomAndLeft: 389 | view.setTranslationY(targetTranslation); 390 | view.setTranslationX(-targetTranslation); 391 | break; 392 | case BottomAndRight: 393 | view.setTranslationY(targetTranslation); 394 | view.setTranslationX(targetTranslation); 395 | break; 396 | case Left: 397 | view.setTranslationX(-targetTranslation); 398 | break; 399 | case Right: 400 | view.setTranslationX(targetTranslation); 401 | break; 402 | } 403 | } 404 | 405 | private void resetTranslation(View view) { 406 | view.setTranslationX(0.0f); 407 | view.setTranslationY(0.0f); 408 | } 409 | 410 | private void updateScale(View view, int index) { 411 | int nextIndex = index - 1; 412 | float currentScale = 1.0f - index * (1.0f - setting.scaleInterval); 413 | float nextScale = 1.0f - nextIndex * (1.0f - setting.scaleInterval); 414 | float targetScale = currentScale + (nextScale - currentScale) * state.getRatio(); 415 | switch (setting.stackFrom) { 416 | case None: 417 | view.setScaleX(targetScale); 418 | view.setScaleY(targetScale); 419 | break; 420 | case Top: 421 | view.setScaleX(targetScale); 422 | // TODO Should handle ScaleY 423 | break; 424 | case TopAndLeft: 425 | view.setScaleX(targetScale); 426 | // TODO Should handle ScaleY 427 | break; 428 | case TopAndRight: 429 | view.setScaleX(targetScale); 430 | // TODO Should handle ScaleY 431 | break; 432 | case Bottom: 433 | view.setScaleX(targetScale); 434 | // TODO Should handle ScaleY 435 | break; 436 | case BottomAndLeft: 437 | view.setScaleX(targetScale); 438 | // TODO Should handle ScaleY 439 | break; 440 | case BottomAndRight: 441 | view.setScaleX(targetScale); 442 | // TODO Should handle ScaleY 443 | break; 444 | case Left: 445 | // TODO Should handle ScaleX 446 | view.setScaleY(targetScale); 447 | break; 448 | case Right: 449 | // TODO Should handle ScaleX 450 | view.setScaleY(targetScale); 451 | break; 452 | } 453 | } 454 | 455 | private void resetScale(View view) { 456 | view.setScaleX(1.0f); 457 | view.setScaleY(1.0f); 458 | } 459 | 460 | private void updateRotation(View view) { 461 | float degree = state.dx * setting.maxDegree / getWidth() * state.proportion; 462 | view.setRotation(degree); 463 | } 464 | 465 | private void resetRotation(View view) { 466 | view.setRotation(0.0f); 467 | } 468 | 469 | private void updateOverlay(View view) { 470 | View leftOverlay = view.findViewById(R.id.left_overlay); 471 | if (leftOverlay != null) { 472 | leftOverlay.setAlpha(0.0f); 473 | } 474 | View rightOverlay = view.findViewById(R.id.right_overlay); 475 | if (rightOverlay != null) { 476 | rightOverlay.setAlpha(0.0f); 477 | } 478 | View topOverlay = view.findViewById(R.id.top_overlay); 479 | if (topOverlay != null) { 480 | topOverlay.setAlpha(0.0f); 481 | } 482 | View bottomOverlay = view.findViewById(R.id.bottom_overlay); 483 | if (bottomOverlay != null) { 484 | bottomOverlay.setAlpha(0.0f); 485 | } 486 | Direction direction = state.getDirection(); 487 | float alpha = setting.overlayInterpolator.getInterpolation(state.getRatio()); 488 | switch (direction) { 489 | case Left: 490 | if (leftOverlay != null) { 491 | leftOverlay.setAlpha(alpha); 492 | } 493 | break; 494 | case Right: 495 | if (rightOverlay != null) { 496 | rightOverlay.setAlpha(alpha); 497 | } 498 | break; 499 | case Top: 500 | if (topOverlay != null) { 501 | topOverlay.setAlpha(alpha); 502 | } 503 | break; 504 | case Bottom: 505 | if (bottomOverlay != null) { 506 | bottomOverlay.setAlpha(alpha); 507 | } 508 | break; 509 | } 510 | } 511 | 512 | private void resetOverlay(View view) { 513 | View leftOverlay = view.findViewById(R.id.left_overlay); 514 | if (leftOverlay != null) { 515 | leftOverlay.setAlpha(0.0f); 516 | } 517 | View rightOverlay = view.findViewById(R.id.right_overlay); 518 | if (rightOverlay != null) { 519 | rightOverlay.setAlpha(0.0f); 520 | } 521 | View topOverlay = view.findViewById(R.id.top_overlay); 522 | if (topOverlay != null) { 523 | topOverlay.setAlpha(0.0f); 524 | } 525 | View bottomOverlay = view.findViewById(R.id.bottom_overlay); 526 | if (bottomOverlay != null) { 527 | bottomOverlay.setAlpha(0.0f); 528 | } 529 | } 530 | 531 | private void smoothScrollToPosition(int position) { 532 | if (state.topPosition < position) { 533 | smoothScrollToNext(position); 534 | } else { 535 | smoothScrollToPrevious(position); 536 | } 537 | } 538 | 539 | private void smoothScrollToNext(int position) { 540 | state.proportion = 0.0f; 541 | state.targetPosition = position; 542 | CardStackSmoothScroller scroller = new CardStackSmoothScroller(CardStackSmoothScroller.ScrollType.AutomaticSwipe, this); 543 | scroller.setTargetPosition(state.topPosition); 544 | startSmoothScroll(scroller); 545 | } 546 | 547 | private void smoothScrollToPrevious(int position) { 548 | View topView = getTopView(); 549 | if (topView != null) { 550 | listener.onCardDisappeared(getTopView(), state.topPosition); 551 | } 552 | 553 | state.proportion = 0.0f; 554 | state.targetPosition = position; 555 | state.topPosition--; 556 | CardStackSmoothScroller scroller = new CardStackSmoothScroller(CardStackSmoothScroller.ScrollType.AutomaticRewind, this); 557 | scroller.setTargetPosition(state.topPosition); 558 | startSmoothScroll(scroller); 559 | } 560 | 561 | public View getTopView() { 562 | return findViewByPosition(state.topPosition); 563 | } 564 | 565 | public int getTopPosition() { 566 | return state.topPosition; 567 | } 568 | 569 | public void setTopPosition(int topPosition) { 570 | state.topPosition = topPosition; 571 | } 572 | 573 | public void setStackFrom(@NonNull StackFrom stackFrom) { 574 | setting.stackFrom = stackFrom; 575 | } 576 | 577 | public void setVisibleCount(@IntRange(from = 1) int visibleCount) { 578 | if (visibleCount < 1) { 579 | throw new IllegalArgumentException("VisibleCount must be greater than 0."); 580 | } 581 | setting.visibleCount = visibleCount; 582 | } 583 | 584 | public void setTranslationInterval(@FloatRange(from = 0.0f) float translationInterval) { 585 | if (translationInterval < 0.0f) { 586 | throw new IllegalArgumentException("TranslationInterval must be greater than or equal 0.0f"); 587 | } 588 | setting.translationInterval = translationInterval; 589 | } 590 | 591 | public void setScaleInterval(@FloatRange(from = 0.0f) float scaleInterval) { 592 | if (scaleInterval < 0.0f) { 593 | throw new IllegalArgumentException("ScaleInterval must be greater than or equal 0.0f."); 594 | } 595 | setting.scaleInterval = scaleInterval; 596 | } 597 | 598 | public void setSwipeThreshold(@FloatRange(from = 0.0f, to = 1.0f) float swipeThreshold) { 599 | if (swipeThreshold < 0.0f || 1.0f < swipeThreshold) { 600 | throw new IllegalArgumentException("SwipeThreshold must be 0.0f to 1.0f."); 601 | } 602 | setting.swipeThreshold = swipeThreshold; 603 | } 604 | 605 | public void setMaxDegree(@FloatRange(from = -360.0f, to = 360.0f) float maxDegree) { 606 | if (maxDegree < -360.0f || 360.0f < maxDegree) { 607 | throw new IllegalArgumentException("MaxDegree must be -360.0f to 360.0f"); 608 | } 609 | setting.maxDegree = maxDegree; 610 | } 611 | 612 | public void setDirections(@NonNull List directions) { 613 | setting.directions = directions; 614 | } 615 | 616 | public void setCanScrollHorizontal(boolean canScrollHorizontal) { 617 | setting.canScrollHorizontal = canScrollHorizontal; 618 | } 619 | 620 | public void setCanScrollVertical(boolean canScrollVertical) { 621 | setting.canScrollVertical = canScrollVertical; 622 | } 623 | 624 | public void setSwipeableMethod(SwipeableMethod swipeableMethod) { 625 | setting.swipeableMethod = swipeableMethod; 626 | } 627 | 628 | public void setSwipeAnimationSetting(@NonNull SwipeAnimationSetting swipeAnimationSetting) { 629 | setting.swipeAnimationSetting = swipeAnimationSetting; 630 | } 631 | 632 | public void setRewindAnimationSetting(@NonNull RewindAnimationSetting rewindAnimationSetting) { 633 | setting.rewindAnimationSetting = rewindAnimationSetting; 634 | } 635 | 636 | public void setOverlayInterpolator(@NonNull Interpolator overlayInterpolator) { 637 | setting.overlayInterpolator = overlayInterpolator; 638 | } 639 | 640 | } 641 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/CardStackListener.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview; 2 | 3 | import android.view.View; 4 | 5 | public interface CardStackListener { 6 | void onCardDragging(Direction direction, float ratio); 7 | void onCardSwiped(Direction direction); 8 | void onCardRewound(); 9 | void onCardCanceled(); 10 | void onCardAppeared(View view, int position); 11 | void onCardDisappeared(View view, int position); 12 | 13 | CardStackListener DEFAULT = new CardStackListener() { 14 | @Override 15 | public void onCardDragging(Direction direction, float ratio) {} 16 | @Override 17 | public void onCardSwiped(Direction direction) {} 18 | @Override 19 | public void onCardRewound() {} 20 | @Override 21 | public void onCardCanceled() {} 22 | @Override 23 | public void onCardAppeared(View view, int position) {} 24 | @Override 25 | public void onCardDisappeared(View view, int position) {} 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/CardStackView.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.MotionEvent; 6 | 7 | import androidx.annotation.Nullable; 8 | import androidx.recyclerview.widget.RecyclerView; 9 | 10 | import com.yuyakaido.android.cardstackview.internal.CardStackDataObserver; 11 | import com.yuyakaido.android.cardstackview.internal.CardStackSnapHelper; 12 | 13 | public class CardStackView extends RecyclerView { 14 | 15 | private final CardStackDataObserver observer = new CardStackDataObserver(this); 16 | 17 | public CardStackView(Context context) { 18 | this(context, null); 19 | } 20 | 21 | public CardStackView(Context context, @Nullable AttributeSet attrs) { 22 | this(context, attrs, 0); 23 | } 24 | 25 | public CardStackView(Context context, @Nullable AttributeSet attrs, int defStyle) { 26 | super(context, attrs, defStyle); 27 | initialize(); 28 | } 29 | 30 | @Override 31 | public void setLayoutManager(LayoutManager manager) { 32 | if (manager instanceof CardStackLayoutManager) { 33 | super.setLayoutManager(manager); 34 | } else { 35 | throw new IllegalArgumentException("CardStackView must be set CardStackLayoutManager."); 36 | } 37 | } 38 | 39 | @Override 40 | public void setAdapter(Adapter adapter) { 41 | if (getLayoutManager() == null) { 42 | setLayoutManager(new CardStackLayoutManager(getContext())); 43 | } 44 | // Imitate RecyclerView's implementation 45 | // http://tools.oesf.biz/android-9.0.0_r1.0/xref/frameworks/base/core/java/com/android/internal/widget/RecyclerView.java#1005 46 | if (getAdapter() != null) { 47 | getAdapter().unregisterAdapterDataObserver(observer); 48 | getAdapter().onDetachedFromRecyclerView(this); 49 | } 50 | adapter.registerAdapterDataObserver(observer); 51 | super.setAdapter(adapter); 52 | } 53 | 54 | @Override 55 | public boolean onInterceptTouchEvent(MotionEvent event) { 56 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 57 | CardStackLayoutManager manager = (CardStackLayoutManager) getLayoutManager(); 58 | if (manager != null) { 59 | manager.updateProportion(event.getX(), event.getY()); 60 | } 61 | } 62 | return super.onInterceptTouchEvent(event); 63 | } 64 | 65 | public void swipe() { 66 | if (getLayoutManager() instanceof CardStackLayoutManager) { 67 | CardStackLayoutManager manager = (CardStackLayoutManager) getLayoutManager(); 68 | smoothScrollToPosition(manager.getTopPosition() + 1); 69 | } 70 | } 71 | 72 | public void rewind() { 73 | if (getLayoutManager() instanceof CardStackLayoutManager) { 74 | CardStackLayoutManager manager = (CardStackLayoutManager) getLayoutManager(); 75 | smoothScrollToPosition(manager.getTopPosition() - 1); 76 | } 77 | } 78 | 79 | private void initialize() { 80 | new CardStackSnapHelper().attachToRecyclerView(this); 81 | setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/Direction.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | public enum Direction { 7 | Left, 8 | Right, 9 | Top, 10 | Bottom; 11 | 12 | public static final List HORIZONTAL = Arrays.asList(Direction.Left, Direction.Right); 13 | public static final List VERTICAL = Arrays.asList(Direction.Top, Direction.Bottom); 14 | public static final List FREEDOM = Arrays.asList(Direction.values()); 15 | } 16 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/Duration.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview; 2 | 3 | public enum Duration { 4 | Fast(100), 5 | Normal(200), 6 | Slow(500); 7 | 8 | public final int duration; 9 | 10 | Duration(int duration) { 11 | this.duration = duration; 12 | } 13 | 14 | public static Duration fromVelocity(int velocity) { 15 | if (velocity < 1000) { 16 | return Slow; 17 | } else if (velocity < 5000) { 18 | return Normal; 19 | } 20 | return Fast; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/RewindAnimationSetting.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview; 2 | 3 | import android.view.animation.DecelerateInterpolator; 4 | import android.view.animation.Interpolator; 5 | 6 | import com.yuyakaido.android.cardstackview.internal.AnimationSetting; 7 | 8 | public class RewindAnimationSetting implements AnimationSetting { 9 | 10 | private final Direction direction; 11 | private final int duration; 12 | private final Interpolator interpolator; 13 | 14 | private RewindAnimationSetting( 15 | Direction direction, 16 | int duration, 17 | Interpolator interpolator 18 | ) { 19 | this.direction = direction; 20 | this.duration = duration; 21 | this.interpolator = interpolator; 22 | } 23 | 24 | @Override 25 | public Direction getDirection() { 26 | return direction; 27 | } 28 | 29 | @Override 30 | public int getDuration() { 31 | return duration; 32 | } 33 | 34 | @Override 35 | public Interpolator getInterpolator() { 36 | return interpolator; 37 | } 38 | 39 | public static class Builder { 40 | private Direction direction = Direction.Bottom; 41 | private int duration = Duration.Normal.duration; 42 | private Interpolator interpolator = new DecelerateInterpolator(); 43 | 44 | public RewindAnimationSetting.Builder setDirection(Direction direction) { 45 | this.direction = direction; 46 | return this; 47 | } 48 | 49 | public RewindAnimationSetting.Builder setDuration(int duration) { 50 | this.duration = duration; 51 | return this; 52 | } 53 | 54 | public RewindAnimationSetting.Builder setInterpolator(Interpolator interpolator) { 55 | this.interpolator = interpolator; 56 | return this; 57 | } 58 | 59 | public RewindAnimationSetting build() { 60 | return new RewindAnimationSetting( 61 | direction, 62 | duration, 63 | interpolator 64 | ); 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/StackFrom.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview; 2 | 3 | public enum StackFrom { 4 | None, 5 | Top, 6 | TopAndLeft, 7 | TopAndRight, 8 | Bottom, 9 | BottomAndLeft, 10 | BottomAndRight, 11 | Left, 12 | Right, 13 | } 14 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/SwipeAnimationSetting.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview; 2 | 3 | import android.view.animation.AccelerateInterpolator; 4 | import android.view.animation.Interpolator; 5 | 6 | import com.yuyakaido.android.cardstackview.internal.AnimationSetting; 7 | 8 | public class SwipeAnimationSetting implements AnimationSetting { 9 | 10 | private final Direction direction; 11 | private final int duration; 12 | private final Interpolator interpolator; 13 | 14 | private SwipeAnimationSetting( 15 | Direction direction, 16 | int duration, 17 | Interpolator interpolator 18 | ) { 19 | this.direction = direction; 20 | this.duration = duration; 21 | this.interpolator = interpolator; 22 | } 23 | 24 | @Override 25 | public Direction getDirection() { 26 | return direction; 27 | } 28 | 29 | @Override 30 | public int getDuration() { 31 | return duration; 32 | } 33 | 34 | @Override 35 | public Interpolator getInterpolator() { 36 | return interpolator; 37 | } 38 | 39 | public static class Builder { 40 | private Direction direction = Direction.Right; 41 | private int duration = Duration.Normal.duration; 42 | private Interpolator interpolator = new AccelerateInterpolator(); 43 | 44 | public Builder setDirection(Direction direction) { 45 | this.direction = direction; 46 | return this; 47 | } 48 | 49 | public Builder setDuration(int duration) { 50 | this.duration = duration; 51 | return this; 52 | } 53 | 54 | public Builder setInterpolator(Interpolator interpolator) { 55 | this.interpolator = interpolator; 56 | return this; 57 | } 58 | 59 | public SwipeAnimationSetting build() { 60 | return new SwipeAnimationSetting( 61 | direction, 62 | duration, 63 | interpolator 64 | ); 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/SwipeableMethod.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview; 2 | 3 | public enum SwipeableMethod { 4 | AutomaticAndManual, 5 | Automatic, 6 | Manual, 7 | None; 8 | 9 | boolean canSwipe() { 10 | return canSwipeAutomatically() || canSwipeManually(); 11 | } 12 | 13 | boolean canSwipeAutomatically() { 14 | return this == AutomaticAndManual || this == Automatic; 15 | } 16 | 17 | boolean canSwipeManually() { 18 | return this == AutomaticAndManual || this == Manual; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/AnimationSetting.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview.internal; 2 | 3 | import android.view.animation.Interpolator; 4 | 5 | import com.yuyakaido.android.cardstackview.Direction; 6 | 7 | public interface AnimationSetting { 8 | Direction getDirection(); 9 | int getDuration(); 10 | Interpolator getInterpolator(); 11 | } 12 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackDataObserver.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview.internal; 2 | 3 | import androidx.annotation.Nullable; 4 | import androidx.recyclerview.widget.RecyclerView; 5 | 6 | import com.yuyakaido.android.cardstackview.CardStackLayoutManager; 7 | 8 | public class CardStackDataObserver extends RecyclerView.AdapterDataObserver { 9 | 10 | private final RecyclerView recyclerView; 11 | 12 | public CardStackDataObserver(RecyclerView recyclerView) { 13 | this.recyclerView = recyclerView; 14 | } 15 | 16 | @Override 17 | public void onChanged() { 18 | CardStackLayoutManager manager = getCardStackLayoutManager(); 19 | manager.setTopPosition(0); 20 | } 21 | 22 | @Override 23 | public void onItemRangeChanged(int positionStart, int itemCount) { 24 | // Do nothing 25 | } 26 | 27 | @Override 28 | public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { 29 | // Do nothing 30 | } 31 | 32 | @Override 33 | public void onItemRangeInserted(int positionStart, int itemCount) { 34 | // Do nothing 35 | } 36 | 37 | @Override 38 | public void onItemRangeRemoved(int positionStart, int itemCount) { 39 | // 要素が削除された場合はTopPositionの調整が必要になる場合がある 40 | // 具体的には、要素が全て削除された場合と、TopPositionより前の要素が削除された場合は調整が必要 41 | CardStackLayoutManager manager = getCardStackLayoutManager(); 42 | int topPosition = manager.getTopPosition(); 43 | if (manager.getItemCount() == 0) { 44 | // 要素が全て削除された場合 45 | manager.setTopPosition(0); 46 | } else if (positionStart < topPosition) { 47 | // TopPositionよりも前の要素が削除された場合 48 | int diff = topPosition - positionStart; 49 | manager.setTopPosition(Math.min(topPosition - diff, manager.getItemCount() - 1)); 50 | } 51 | } 52 | 53 | @Override 54 | public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { 55 | CardStackLayoutManager manager = getCardStackLayoutManager(); 56 | manager.removeAllViews(); 57 | } 58 | 59 | private CardStackLayoutManager getCardStackLayoutManager() { 60 | RecyclerView.LayoutManager manager = recyclerView.getLayoutManager(); 61 | if (manager instanceof CardStackLayoutManager) { 62 | return (CardStackLayoutManager) manager; 63 | } 64 | throw new IllegalStateException("CardStackView must be set CardStackLayoutManager."); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSetting.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview.internal; 2 | 3 | import android.view.animation.Interpolator; 4 | import android.view.animation.LinearInterpolator; 5 | 6 | import com.yuyakaido.android.cardstackview.Direction; 7 | import com.yuyakaido.android.cardstackview.RewindAnimationSetting; 8 | import com.yuyakaido.android.cardstackview.StackFrom; 9 | import com.yuyakaido.android.cardstackview.SwipeAnimationSetting; 10 | import com.yuyakaido.android.cardstackview.SwipeableMethod; 11 | 12 | import java.util.List; 13 | 14 | public class CardStackSetting { 15 | public StackFrom stackFrom = StackFrom.None; 16 | public int visibleCount = 3; 17 | public float translationInterval = 8.0f; 18 | public float scaleInterval = 0.95f; // 0.0f - 1.0f 19 | public float swipeThreshold = 0.3f; // 0.0f - 1.0f 20 | public float maxDegree = 20.0f; 21 | public List directions = Direction.HORIZONTAL; 22 | public boolean canScrollHorizontal = true; 23 | public boolean canScrollVertical = true; 24 | public SwipeableMethod swipeableMethod = SwipeableMethod.AutomaticAndManual; 25 | public SwipeAnimationSetting swipeAnimationSetting = new SwipeAnimationSetting.Builder().build(); 26 | public RewindAnimationSetting rewindAnimationSetting = new RewindAnimationSetting.Builder().build(); 27 | public Interpolator overlayInterpolator = new LinearInterpolator(); 28 | } 29 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSmoothScroller.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview.internal; 2 | 3 | import android.view.View; 4 | 5 | import com.yuyakaido.android.cardstackview.CardStackLayoutManager; 6 | import com.yuyakaido.android.cardstackview.CardStackListener; 7 | import com.yuyakaido.android.cardstackview.RewindAnimationSetting; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.recyclerview.widget.RecyclerView; 11 | 12 | public class CardStackSmoothScroller extends RecyclerView.SmoothScroller { 13 | 14 | public enum ScrollType { 15 | AutomaticSwipe, 16 | AutomaticRewind, 17 | ManualSwipe, 18 | ManualCancel 19 | } 20 | 21 | private ScrollType type; 22 | private CardStackLayoutManager manager; 23 | 24 | public CardStackSmoothScroller( 25 | ScrollType type, 26 | CardStackLayoutManager manager 27 | ) { 28 | this.type = type; 29 | this.manager = manager; 30 | } 31 | 32 | @Override 33 | protected void onSeekTargetStep( 34 | int dx, 35 | int dy, 36 | @NonNull RecyclerView.State state, 37 | @NonNull Action action 38 | ) { 39 | if (type == ScrollType.AutomaticRewind) { 40 | RewindAnimationSetting setting = manager.getCardStackSetting().rewindAnimationSetting; 41 | action.update( 42 | -getDx(setting), 43 | -getDy(setting), 44 | setting.getDuration(), 45 | setting.getInterpolator() 46 | ); 47 | } 48 | } 49 | 50 | @Override 51 | protected void onTargetFound( 52 | @NonNull View targetView, 53 | @NonNull RecyclerView.State state, 54 | @NonNull Action action 55 | ) { 56 | int x = (int) targetView.getTranslationX(); 57 | int y = (int) targetView.getTranslationY(); 58 | AnimationSetting setting; 59 | switch (type) { 60 | case AutomaticSwipe: 61 | setting = manager.getCardStackSetting().swipeAnimationSetting; 62 | action.update( 63 | -getDx(setting), 64 | -getDy(setting), 65 | setting.getDuration(), 66 | setting.getInterpolator() 67 | ); 68 | break; 69 | case AutomaticRewind: 70 | setting = manager.getCardStackSetting().rewindAnimationSetting; 71 | action.update( 72 | x, 73 | y, 74 | setting.getDuration(), 75 | setting.getInterpolator() 76 | ); 77 | break; 78 | case ManualSwipe: 79 | int dx = -x * 10; 80 | int dy = -y * 10; 81 | setting = manager.getCardStackSetting().swipeAnimationSetting; 82 | action.update( 83 | dx, 84 | dy, 85 | setting.getDuration(), 86 | setting.getInterpolator() 87 | ); 88 | break; 89 | case ManualCancel: 90 | setting = manager.getCardStackSetting().rewindAnimationSetting; 91 | action.update( 92 | x, 93 | y, 94 | setting.getDuration(), 95 | setting.getInterpolator() 96 | ); 97 | break; 98 | } 99 | } 100 | 101 | @Override 102 | protected void onStart() { 103 | CardStackListener listener = manager.getCardStackListener(); 104 | CardStackState state = manager.getCardStackState(); 105 | switch (type) { 106 | case AutomaticSwipe: 107 | state.next(CardStackState.Status.AutomaticSwipeAnimating); 108 | listener.onCardDisappeared(manager.getTopView(), manager.getTopPosition()); 109 | break; 110 | case AutomaticRewind: 111 | state.next(CardStackState.Status.RewindAnimating); 112 | break; 113 | case ManualSwipe: 114 | state.next(CardStackState.Status.ManualSwipeAnimating); 115 | listener.onCardDisappeared(manager.getTopView(), manager.getTopPosition()); 116 | break; 117 | case ManualCancel: 118 | state.next(CardStackState.Status.RewindAnimating); 119 | break; 120 | } 121 | } 122 | 123 | @Override 124 | protected void onStop() { 125 | CardStackListener listener = manager.getCardStackListener(); 126 | switch (type) { 127 | case AutomaticSwipe: 128 | // Notify callback from CardStackLayoutManager 129 | break; 130 | case AutomaticRewind: 131 | listener.onCardRewound(); 132 | listener.onCardAppeared(manager.getTopView(), manager.getTopPosition()); 133 | break; 134 | case ManualSwipe: 135 | // Notify callback from CardStackLayoutManager 136 | break; 137 | case ManualCancel: 138 | listener.onCardCanceled(); 139 | break; 140 | } 141 | } 142 | 143 | private int getDx(AnimationSetting setting) { 144 | CardStackState state = manager.getCardStackState(); 145 | int dx = 0; 146 | switch (setting.getDirection()) { 147 | case Left: 148 | dx = -state.width * 2; 149 | break; 150 | case Right: 151 | dx = state.width * 2; 152 | break; 153 | case Top: 154 | case Bottom: 155 | dx = 0; 156 | break; 157 | } 158 | return dx; 159 | } 160 | 161 | private int getDy(AnimationSetting setting) { 162 | CardStackState state = manager.getCardStackState(); 163 | int dy = 0; 164 | switch (setting.getDirection()) { 165 | case Left: 166 | case Right: 167 | dy = state.height / 4; 168 | break; 169 | case Top: 170 | dy = -state.height * 2; 171 | break; 172 | case Bottom: 173 | dy = state.height * 2; 174 | break; 175 | } 176 | return dy; 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackSnapHelper.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview.internal; 2 | 3 | import android.view.View; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | import androidx.recyclerview.widget.RecyclerView; 8 | import androidx.recyclerview.widget.SnapHelper; 9 | 10 | import com.yuyakaido.android.cardstackview.CardStackLayoutManager; 11 | import com.yuyakaido.android.cardstackview.Duration; 12 | import com.yuyakaido.android.cardstackview.SwipeAnimationSetting; 13 | 14 | public class CardStackSnapHelper extends SnapHelper { 15 | 16 | private int velocityX = 0; 17 | private int velocityY = 0; 18 | 19 | @Nullable 20 | @Override 21 | public int[] calculateDistanceToFinalSnap( 22 | @NonNull RecyclerView.LayoutManager layoutManager, 23 | @NonNull View targetView 24 | ) { 25 | if (layoutManager instanceof CardStackLayoutManager) { 26 | CardStackLayoutManager manager = (CardStackLayoutManager) layoutManager; 27 | if (manager.findViewByPosition(manager.getTopPosition()) != null) { 28 | int x = (int) targetView.getTranslationX(); 29 | int y = (int) targetView.getTranslationY(); 30 | if (x != 0 || y != 0) { 31 | CardStackSetting setting = manager.getCardStackSetting(); 32 | float horizontal = Math.abs(x) / (float) targetView.getWidth(); 33 | float vertical = Math.abs(y) / (float) targetView.getHeight(); 34 | Duration duration = Duration.fromVelocity(velocityY < velocityX ? velocityX : velocityY); 35 | if (duration == Duration.Fast || setting.swipeThreshold < horizontal || setting.swipeThreshold < vertical) { 36 | CardStackState state = manager.getCardStackState(); 37 | if (setting.directions.contains(state.getDirection())) { 38 | state.targetPosition = state.topPosition + 1; 39 | 40 | SwipeAnimationSetting swipeAnimationSetting = new SwipeAnimationSetting.Builder() 41 | .setDirection(setting.swipeAnimationSetting.getDirection()) 42 | .setDuration(duration.duration) 43 | .setInterpolator(setting.swipeAnimationSetting.getInterpolator()) 44 | .build(); 45 | manager.setSwipeAnimationSetting(swipeAnimationSetting); 46 | 47 | this.velocityX = 0; 48 | this.velocityY = 0; 49 | 50 | CardStackSmoothScroller scroller = new CardStackSmoothScroller(CardStackSmoothScroller.ScrollType.ManualSwipe, manager); 51 | scroller.setTargetPosition(manager.getTopPosition()); 52 | manager.startSmoothScroll(scroller); 53 | } else { 54 | CardStackSmoothScroller scroller = new CardStackSmoothScroller(CardStackSmoothScroller.ScrollType.ManualCancel, manager); 55 | scroller.setTargetPosition(manager.getTopPosition()); 56 | manager.startSmoothScroll(scroller); 57 | } 58 | } else { 59 | CardStackSmoothScroller scroller = new CardStackSmoothScroller(CardStackSmoothScroller.ScrollType.ManualCancel, manager); 60 | scroller.setTargetPosition(manager.getTopPosition()); 61 | manager.startSmoothScroll(scroller); 62 | } 63 | } 64 | } 65 | } 66 | return new int[2]; 67 | } 68 | 69 | @Nullable 70 | @Override 71 | public View findSnapView(RecyclerView.LayoutManager layoutManager) { 72 | if (layoutManager instanceof CardStackLayoutManager) { 73 | CardStackLayoutManager manager = (CardStackLayoutManager) layoutManager; 74 | View view = manager.findViewByPosition(manager.getTopPosition()); 75 | if (view != null) { 76 | int x = (int) view.getTranslationX(); 77 | int y = (int) view.getTranslationY(); 78 | if (x == 0 && y == 0) { 79 | return null; 80 | } 81 | return view; 82 | } 83 | } 84 | return null; 85 | } 86 | 87 | @Override 88 | public int findTargetSnapPosition( 89 | RecyclerView.LayoutManager layoutManager, 90 | int velocityX, 91 | int velocityY 92 | ) { 93 | this.velocityX = Math.abs(velocityX); 94 | this.velocityY = Math.abs(velocityY); 95 | if (layoutManager instanceof CardStackLayoutManager) { 96 | CardStackLayoutManager manager = (CardStackLayoutManager) layoutManager; 97 | return manager.getTopPosition(); 98 | } 99 | return RecyclerView.NO_POSITION; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/CardStackState.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview.internal; 2 | 3 | import androidx.recyclerview.widget.RecyclerView; 4 | 5 | import com.yuyakaido.android.cardstackview.Direction; 6 | 7 | public class CardStackState { 8 | public Status status = Status.Idle; 9 | public int width = 0; 10 | public int height = 0; 11 | public int dx = 0; 12 | public int dy = 0; 13 | public int topPosition = 0; 14 | public int targetPosition = RecyclerView.NO_POSITION; 15 | public float proportion = 0.0f; 16 | 17 | public enum Status { 18 | Idle, 19 | Dragging, 20 | RewindAnimating, 21 | AutomaticSwipeAnimating, 22 | AutomaticSwipeAnimated, 23 | ManualSwipeAnimating, 24 | ManualSwipeAnimated; 25 | 26 | public boolean isBusy() { 27 | return this != Idle; 28 | } 29 | 30 | public boolean isDragging() { 31 | return this == Dragging; 32 | } 33 | 34 | public boolean isSwipeAnimating() { 35 | return this == ManualSwipeAnimating || this == AutomaticSwipeAnimating; 36 | } 37 | 38 | public Status toAnimatedStatus() { 39 | switch (this) { 40 | case ManualSwipeAnimating: 41 | return ManualSwipeAnimated; 42 | case AutomaticSwipeAnimating: 43 | return AutomaticSwipeAnimated; 44 | default: 45 | return Idle; 46 | } 47 | } 48 | } 49 | 50 | public void next(Status state) { 51 | this.status = state; 52 | } 53 | 54 | public Direction getDirection() { 55 | if (Math.abs(dy) < Math.abs(dx)) { 56 | if (dx < 0.0f) { 57 | return Direction.Left; 58 | } else { 59 | return Direction.Right; 60 | } 61 | } else { 62 | if (dy < 0.0f) { 63 | return Direction.Top; 64 | } else { 65 | return Direction.Bottom; 66 | } 67 | } 68 | } 69 | 70 | public float getRatio() { 71 | int absDx = Math.abs(dx); 72 | int absDy = Math.abs(dy); 73 | float ratio; 74 | if (absDx < absDy) { 75 | ratio = absDy / (height / 2.0f); 76 | } else { 77 | ratio = absDx / (width / 2.0f); 78 | } 79 | return Math.min(ratio, 1.0f); 80 | } 81 | 82 | public boolean isSwipeCompleted() { 83 | if (status.isSwipeAnimating()) { 84 | if (topPosition < targetPosition) { 85 | if (width < Math.abs(dx) || height < Math.abs(dy)) { 86 | return true; 87 | } 88 | } 89 | } 90 | return false; 91 | } 92 | 93 | public boolean canScrollToPosition(int position, int itemCount) { 94 | if (position == topPosition) { 95 | return false; 96 | } 97 | if (position < 0) { 98 | return false; 99 | } 100 | if (itemCount < position) { 101 | return false; 102 | } 103 | if (status.isBusy()) { 104 | return false; 105 | } 106 | return true; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /cardstackview/src/main/java/com/yuyakaido/android/cardstackview/internal/DisplayUtil.java: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview.internal; 2 | 3 | import android.content.Context; 4 | 5 | public final class DisplayUtil { 6 | 7 | private DisplayUtil() {} 8 | 9 | public static int dpToPx(Context context, float dp) { 10 | float density = context.getResources().getDisplayMetrics().density; 11 | return (int) (dp * density + 0.5f); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /cardstackview/src/main/res/layout/overlay.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | 13 | 14 | 18 | 19 | 20 | 24 | 25 | 26 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /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/yuyakaido/CardStackView/8368daa84eead97fc5e2dc3613fabf47ca46fcac/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Sep 17 16:23:37 JST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | android { 6 | compileSdkVersion 33 7 | 8 | defaultConfig { 9 | applicationId "com.yuyakaido.android.cardstackview.sample" 10 | minSdkVersion 14 11 | targetSdkVersion 33 12 | } 13 | } 14 | 15 | dependencies { 16 | implementation project(':cardstackview') 17 | 18 | // Kotlin 19 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 20 | 21 | // Image Loading 22 | implementation 'com.github.bumptech.glide:glide:4.13.2' 23 | kapt 'com.github.bumptech.glide:compiler:4.13.2' 24 | 25 | // Support Library 26 | implementation 'androidx.appcompat:appcompat:1.5.1' 27 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 28 | implementation 'androidx.cardview:cardview:1.0.0' 29 | implementation 'com.google.android.material:material:1.6.1' 30 | } 31 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yuyakaido/android/cardstackview/sample/CardStackAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview.sample 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.ImageView 7 | import android.widget.TextView 8 | import android.widget.Toast 9 | import androidx.recyclerview.widget.RecyclerView 10 | import com.bumptech.glide.Glide 11 | 12 | class CardStackAdapter( 13 | private var spots: List = emptyList() 14 | ) : RecyclerView.Adapter() { 15 | 16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 17 | val inflater = LayoutInflater.from(parent.context) 18 | return ViewHolder(inflater.inflate(R.layout.item_spot, parent, false)) 19 | } 20 | 21 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 22 | val spot = spots[position] 23 | holder.name.text = "${spot.id}. ${spot.name}" 24 | holder.city.text = spot.city 25 | Glide.with(holder.image) 26 | .load(spot.url) 27 | .into(holder.image) 28 | holder.itemView.setOnClickListener { v -> 29 | Toast.makeText(v.context, spot.name, Toast.LENGTH_SHORT).show() 30 | } 31 | } 32 | 33 | override fun getItemCount(): Int { 34 | return spots.size 35 | } 36 | 37 | fun setSpots(spots: List) { 38 | this.spots = spots 39 | } 40 | 41 | fun getSpots(): List { 42 | return spots 43 | } 44 | 45 | class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 46 | val name: TextView = view.findViewById(R.id.item_name) 47 | var city: TextView = view.findViewById(R.id.item_city) 48 | var image: ImageView = view.findViewById(R.id.item_image) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yuyakaido/android/cardstackview/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview.sample 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.view.View 6 | import android.view.animation.AccelerateInterpolator 7 | import android.view.animation.DecelerateInterpolator 8 | import android.view.animation.LinearInterpolator 9 | import android.widget.TextView 10 | import androidx.appcompat.app.ActionBarDrawerToggle 11 | import androidx.appcompat.app.AppCompatActivity 12 | import androidx.appcompat.widget.Toolbar 13 | import androidx.core.view.GravityCompat 14 | import androidx.drawerlayout.widget.DrawerLayout 15 | import androidx.recyclerview.widget.DefaultItemAnimator 16 | import androidx.recyclerview.widget.DiffUtil 17 | import com.google.android.material.navigation.NavigationView 18 | import com.yuyakaido.android.cardstackview.* 19 | import java.util.* 20 | 21 | class MainActivity : AppCompatActivity(), CardStackListener { 22 | 23 | private val drawerLayout by lazy { findViewById(R.id.drawer_layout) } 24 | private val cardStackView by lazy { findViewById(R.id.card_stack_view) } 25 | private val manager by lazy { CardStackLayoutManager(this, this) } 26 | private val adapter by lazy { CardStackAdapter(createSpots()) } 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setContentView(R.layout.activity_main) 31 | setupNavigation() 32 | setupCardStackView() 33 | setupButton() 34 | } 35 | 36 | override fun onBackPressed() { 37 | if (drawerLayout.isDrawerOpen(GravityCompat.START)) { 38 | drawerLayout.closeDrawers() 39 | } else { 40 | super.onBackPressed() 41 | } 42 | } 43 | 44 | override fun onCardDragging(direction: Direction, ratio: Float) { 45 | Log.d("CardStackView", "onCardDragging: d = ${direction.name}, r = $ratio") 46 | } 47 | 48 | override fun onCardSwiped(direction: Direction) { 49 | Log.d("CardStackView", "onCardSwiped: p = ${manager.topPosition}, d = $direction") 50 | if (manager.topPosition == adapter.itemCount - 5) { 51 | paginate() 52 | } 53 | } 54 | 55 | override fun onCardRewound() { 56 | Log.d("CardStackView", "onCardRewound: ${manager.topPosition}") 57 | } 58 | 59 | override fun onCardCanceled() { 60 | Log.d("CardStackView", "onCardCanceled: ${manager.topPosition}") 61 | } 62 | 63 | override fun onCardAppeared(view: View, position: Int) { 64 | val textView = view.findViewById(R.id.item_name) 65 | Log.d("CardStackView", "onCardAppeared: ($position) ${textView.text}") 66 | } 67 | 68 | override fun onCardDisappeared(view: View, position: Int) { 69 | val textView = view.findViewById(R.id.item_name) 70 | Log.d("CardStackView", "onCardDisappeared: ($position) ${textView.text}") 71 | } 72 | 73 | private fun setupNavigation() { 74 | // Toolbar 75 | val toolbar = findViewById(R.id.toolbar) 76 | setSupportActionBar(toolbar) 77 | 78 | // DrawerLayout 79 | val actionBarDrawerToggle = ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.open_drawer, R.string.close_drawer) 80 | actionBarDrawerToggle.syncState() 81 | drawerLayout.addDrawerListener(actionBarDrawerToggle) 82 | 83 | // NavigationView 84 | val navigationView = findViewById(R.id.navigation_view) 85 | navigationView.setNavigationItemSelectedListener { menuItem -> 86 | when (menuItem.itemId) { 87 | R.id.reload -> reload() 88 | R.id.add_spot_to_first -> addFirst(1) 89 | R.id.add_spot_to_last -> addLast(1) 90 | R.id.remove_spot_from_first -> removeFirst(1) 91 | R.id.remove_spot_from_last -> removeLast(1) 92 | R.id.replace_first_spot -> replace() 93 | R.id.swap_first_for_last -> swap() 94 | } 95 | drawerLayout.closeDrawers() 96 | true 97 | } 98 | } 99 | 100 | private fun setupCardStackView() { 101 | initialize() 102 | } 103 | 104 | private fun setupButton() { 105 | val skip = findViewById(R.id.skip_button) 106 | skip.setOnClickListener { 107 | val setting = SwipeAnimationSetting.Builder() 108 | .setDirection(Direction.Left) 109 | .setDuration(Duration.Normal.duration) 110 | .setInterpolator(AccelerateInterpolator()) 111 | .build() 112 | manager.setSwipeAnimationSetting(setting) 113 | cardStackView.swipe() 114 | } 115 | 116 | val rewind = findViewById(R.id.rewind_button) 117 | rewind.setOnClickListener { 118 | val setting = RewindAnimationSetting.Builder() 119 | .setDirection(Direction.Bottom) 120 | .setDuration(Duration.Normal.duration) 121 | .setInterpolator(DecelerateInterpolator()) 122 | .build() 123 | manager.setRewindAnimationSetting(setting) 124 | cardStackView.rewind() 125 | } 126 | 127 | val like = findViewById(R.id.like_button) 128 | like.setOnClickListener { 129 | val setting = SwipeAnimationSetting.Builder() 130 | .setDirection(Direction.Right) 131 | .setDuration(Duration.Normal.duration) 132 | .setInterpolator(AccelerateInterpolator()) 133 | .build() 134 | manager.setSwipeAnimationSetting(setting) 135 | cardStackView.swipe() 136 | } 137 | } 138 | 139 | private fun initialize() { 140 | manager.setStackFrom(StackFrom.None) 141 | manager.setVisibleCount(3) 142 | manager.setTranslationInterval(8.0f) 143 | manager.setScaleInterval(0.95f) 144 | manager.setSwipeThreshold(0.3f) 145 | manager.setMaxDegree(20.0f) 146 | manager.setDirections(Direction.HORIZONTAL) 147 | manager.setCanScrollHorizontal(true) 148 | manager.setCanScrollVertical(true) 149 | manager.setSwipeableMethod(SwipeableMethod.AutomaticAndManual) 150 | manager.setOverlayInterpolator(LinearInterpolator()) 151 | cardStackView.layoutManager = manager 152 | cardStackView.adapter = adapter 153 | cardStackView.itemAnimator.apply { 154 | if (this is DefaultItemAnimator) { 155 | supportsChangeAnimations = false 156 | } 157 | } 158 | } 159 | 160 | private fun paginate() { 161 | val old = adapter.getSpots() 162 | val new = old.plus(createSpots()) 163 | val callback = SpotDiffCallback(old, new) 164 | val result = DiffUtil.calculateDiff(callback) 165 | adapter.setSpots(new) 166 | result.dispatchUpdatesTo(adapter) 167 | } 168 | 169 | private fun reload() { 170 | val old = adapter.getSpots() 171 | val new = createSpots() 172 | val callback = SpotDiffCallback(old, new) 173 | val result = DiffUtil.calculateDiff(callback) 174 | adapter.setSpots(new) 175 | result.dispatchUpdatesTo(adapter) 176 | } 177 | 178 | private fun addFirst(size: Int) { 179 | val old = adapter.getSpots() 180 | val new = mutableListOf().apply { 181 | addAll(old) 182 | for (i in 0 until size) { 183 | add(manager.topPosition, createSpot()) 184 | } 185 | } 186 | val callback = SpotDiffCallback(old, new) 187 | val result = DiffUtil.calculateDiff(callback) 188 | adapter.setSpots(new) 189 | result.dispatchUpdatesTo(adapter) 190 | } 191 | 192 | private fun addLast(size: Int) { 193 | val old = adapter.getSpots() 194 | val new = mutableListOf().apply { 195 | addAll(old) 196 | addAll(List(size) { createSpot() }) 197 | } 198 | val callback = SpotDiffCallback(old, new) 199 | val result = DiffUtil.calculateDiff(callback) 200 | adapter.setSpots(new) 201 | result.dispatchUpdatesTo(adapter) 202 | } 203 | 204 | private fun removeFirst(size: Int) { 205 | if (adapter.getSpots().isEmpty()) { 206 | return 207 | } 208 | 209 | val old = adapter.getSpots() 210 | val new = mutableListOf().apply { 211 | addAll(old) 212 | for (i in 0 until size) { 213 | removeAt(manager.topPosition) 214 | } 215 | } 216 | val callback = SpotDiffCallback(old, new) 217 | val result = DiffUtil.calculateDiff(callback) 218 | adapter.setSpots(new) 219 | result.dispatchUpdatesTo(adapter) 220 | } 221 | 222 | private fun removeLast(size: Int) { 223 | if (adapter.getSpots().isEmpty()) { 224 | return 225 | } 226 | 227 | val old = adapter.getSpots() 228 | val new = mutableListOf().apply { 229 | addAll(old) 230 | for (i in 0 until size) { 231 | removeAt(this.size - 1) 232 | } 233 | } 234 | val callback = SpotDiffCallback(old, new) 235 | val result = DiffUtil.calculateDiff(callback) 236 | adapter.setSpots(new) 237 | result.dispatchUpdatesTo(adapter) 238 | } 239 | 240 | private fun replace() { 241 | val old = adapter.getSpots() 242 | val new = mutableListOf().apply { 243 | addAll(old) 244 | removeAt(manager.topPosition) 245 | add(manager.topPosition, createSpot()) 246 | } 247 | adapter.setSpots(new) 248 | adapter.notifyItemChanged(manager.topPosition) 249 | } 250 | 251 | private fun swap() { 252 | val old = adapter.getSpots() 253 | val new = mutableListOf().apply { 254 | addAll(old) 255 | val first = removeAt(manager.topPosition) 256 | val last = removeAt(this.size - 1) 257 | add(manager.topPosition, last) 258 | add(first) 259 | } 260 | val callback = SpotDiffCallback(old, new) 261 | val result = DiffUtil.calculateDiff(callback) 262 | adapter.setSpots(new) 263 | result.dispatchUpdatesTo(adapter) 264 | } 265 | 266 | private fun createSpot(): Spot { 267 | return Spot( 268 | name = "Yasaka Shrine", 269 | city = "Kyoto", 270 | url = "https://source.unsplash.com/Xq1ntWruZQI/600x800" 271 | ) 272 | } 273 | 274 | private fun createSpots(): List { 275 | val spots = ArrayList() 276 | spots.add(Spot(name = "Yasaka Shrine", city = "Kyoto", url = "https://source.unsplash.com/Xq1ntWruZQI/600x800")) 277 | spots.add(Spot(name = "Fushimi Inari Shrine", city = "Kyoto", url = "https://source.unsplash.com/NYyCqdBOKwc/600x800")) 278 | spots.add(Spot(name = "Bamboo Forest", city = "Kyoto", url = "https://source.unsplash.com/buF62ewDLcQ/600x800")) 279 | spots.add(Spot(name = "Brooklyn Bridge", city = "New York", url = "https://source.unsplash.com/THozNzxEP3g/600x800")) 280 | spots.add(Spot(name = "Empire State Building", city = "New York", url = "https://source.unsplash.com/USrZRcRS2Lw/600x800")) 281 | spots.add(Spot(name = "The statue of Liberty", city = "New York", url = "https://source.unsplash.com/PeFk7fzxTdk/600x800")) 282 | spots.add(Spot(name = "Louvre Museum", city = "Paris", url = "https://source.unsplash.com/LrMWHKqilUw/600x800")) 283 | spots.add(Spot(name = "Eiffel Tower", city = "Paris", url = "https://source.unsplash.com/HN-5Z6AmxrM/600x800")) 284 | spots.add(Spot(name = "Big Ben", city = "London", url = "https://source.unsplash.com/CdVAUADdqEc/600x800")) 285 | spots.add(Spot(name = "Great Wall of China", city = "China", url = "https://source.unsplash.com/AWh9C-QjhE4/600x800")) 286 | return spots 287 | } 288 | 289 | } 290 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yuyakaido/android/cardstackview/sample/Spot.kt: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview.sample 2 | 3 | data class Spot( 4 | val id: Long = counter++, 5 | val name: String, 6 | val city: String, 7 | val url: String 8 | ) { 9 | companion object { 10 | private var counter = 0L 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yuyakaido/android/cardstackview/sample/SpotDiffCallback.kt: -------------------------------------------------------------------------------- 1 | package com.yuyakaido.android.cardstackview.sample 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | 5 | class SpotDiffCallback( 6 | private val old: List, 7 | private val new: List 8 | ) : DiffUtil.Callback() { 9 | 10 | override fun getOldListSize(): Int { 11 | return old.size 12 | } 13 | 14 | override fun getNewListSize(): Int { 15 | return new.size 16 | } 17 | 18 | override fun areItemsTheSame(oldPosition: Int, newPosition: Int): Boolean { 19 | return old[oldPosition].id == new[newPosition].id 20 | } 21 | 22 | override fun areContentsTheSame(oldPosition: Int, newPosition: Int): Boolean { 23 | return old[oldPosition] == new[newPosition] 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/gradation_black.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /sample/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 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/like_green_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/like_white_120dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/overlay_black.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 15 | 16 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/rewind_blue_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/skip_red_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/skip_white_120dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 14 | 15 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 40 | 41 | 52 | 53 | 66 | 67 | 78 | 79 | 80 | 81 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/item_spot.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 22 | 23 | 30 | 31 | 38 | 39 | 46 | 47 | 48 | 49 | 54 | 55 | 60 | 61 | 62 | 63 | 68 | 69 | 74 | 75 | 76 | 77 | 81 | 82 | 83 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /sample/src/main/res/menu/navigation_main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyakaido/CardStackView/8368daa84eead97fc5e2dc3613fabf47ca46fcac/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyakaido/CardStackView/8368daa84eead97fc5e2dc3613fabf47ca46fcac/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyakaido/CardStackView/8368daa84eead97fc5e2dc3613fabf47ca46fcac/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyakaido/CardStackView/8368daa84eead97fc5e2dc3613fabf47ca46fcac/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyakaido/CardStackView/8368daa84eead97fc5e2dc3613fabf47ca46fcac/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyakaido/CardStackView/8368daa84eead97fc5e2dc3613fabf47ca46fcac/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyakaido/CardStackView/8368daa84eead97fc5e2dc3613fabf47ca46fcac/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyakaido/CardStackView/8368daa84eead97fc5e2dc3613fabf47ca46fcac/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/overlay_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyakaido/CardStackView/8368daa84eead97fc5e2dc3613fabf47ca46fcac/sample/src/main/res/mipmap-xxhdpi/overlay_left.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/overlay_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyakaido/CardStackView/8368daa84eead97fc5e2dc3613fabf47ca46fcac/sample/src/main/res/mipmap-xxhdpi/overlay_right.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyakaido/CardStackView/8368daa84eead97fc5e2dc3613fabf47ca46fcac/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuyakaido/CardStackView/8368daa84eead97fc5e2dc3613fabf47ca46fcac/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CardStackView 6 | 7 | 8 | Open drawer 9 | Close drawer 10 | Reload 11 | Add spot to first 12 | Add spot to last 13 | Remove spot from first 14 | Remove spot from last 15 | Replace first spot 16 | Swap first for last 17 | 18 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':sample', ':cardstackview' 2 | --------------------------------------------------------------------------------