├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── libraries │ ├── animated_vector_drawable_24_2_0.xml │ ├── appcompat_v7_24_2_0.xml │ ├── glide_3_7_0.xml │ ├── hamcrest_core_1_3.xml │ ├── junit_4_12.xml │ ├── recyclerview_v7_24_2_0.xml │ ├── support_annotations_24_2_0.xml │ ├── support_compat_24_2_0.xml │ ├── support_core_ui_24_2_0.xml │ ├── support_core_utils_24_2_0.xml │ ├── support_fragment_24_2_0.xml │ ├── support_media_compat_24_2_0.xml │ ├── support_v4_24_2_0.xml │ └── support_vector_drawable_24_2_0.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── README.md ├── StackCardsView-demo.apk ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── beyondsw │ │ └── widget │ │ ├── BaseCardItem.java │ │ ├── CardAdapter.java │ │ ├── CardFragment.java │ │ ├── DemoActivity.java │ │ ├── ImageCardItem.java │ │ ├── ImageUrls.java │ │ ├── MyViewPager.java │ │ ├── ScrollCardItem.java │ │ ├── SettingFragment.java │ │ └── Utils.java │ └── res │ ├── drawable-xhdpi │ ├── card_bg.9.png │ ├── ic_down.png │ ├── ic_left.png │ ├── ic_right.png │ ├── ic_up.png │ ├── img_dft.png │ └── testdrag.jpg │ ├── drawable │ ├── bg_bottom_btn_normal.xml │ ├── bg_bottom_btn_pressed.xml │ └── bg_bottom_btn_selector.xml │ ├── layout │ ├── item_imagecard.xml │ ├── item_recyclerview_v.xml │ ├── item_scrollcard.xml │ ├── main.xml │ ├── page1.xml │ └── page2.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── demo-images ├── demo1.gif ├── demo1.mp4 ├── demo2.gif ├── demo2.mp4 ├── demo3.gif ├── demo3.mp4 ├── demo4.gif └── demo4.mp4 ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── beyondsw │ │ └── lib │ │ └── widget │ │ ├── ISwipeTouchHelper.java │ │ ├── StackCardsView.java │ │ ├── SwipeTouchHelper.java │ │ └── rebound │ │ ├── AndroidSpringLooperFactory.java │ │ ├── AnimationQueue.java │ │ ├── BaseSpringSystem.java │ │ ├── BouncyConversion.java │ │ ├── ChoreographerCompat.java │ │ ├── OrigamiValueConverter.java │ │ ├── SimpleSpringListener.java │ │ ├── Spring.java │ │ ├── SpringChain.java │ │ ├── SpringConfig.java │ │ ├── SpringConfigRegistry.java │ │ ├── SpringListener.java │ │ ├── SpringLooper.java │ │ ├── SpringSystem.java │ │ ├── SpringSystemListener.java │ │ ├── SpringUtil.java │ │ ├── SteppingLooper.java │ │ └── SynchronousLooper.java │ └── res │ └── values │ ├── attrs_stackcards.xml │ └── strings.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | *.iml 37 | .idea/workspace.xml 38 | 39 | # Keystore files 40 | *.jks 41 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | StackCardsView -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/libraries/animated_vector_drawable_24_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/appcompat_v7_24_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/libraries/glide_3_7_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/hamcrest_core_1_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/junit_4_12.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/recyclerview_v7_24_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/libraries/support_annotations_24_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/support_compat_24_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/libraries/support_core_ui_24_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/libraries/support_core_utils_24_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/libraries/support_fragment_24_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/libraries/support_media_compat_24_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/libraries/support_v4_24_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/libraries/support_vector_drawable_24_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 1.7 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StackCardsView 2 | 3 | #### [demo apk download](https://github.com/wensefu/StackCardsView/blob/master/StackCardsView-demo.apk?raw=true) 4 | 5 | 堆叠滑动控件,类似于社交软件探探的效果,并增加以下扩展: 6 | 7 | - **支持滑动方向控制** 8 | - **支持消失方向控制** 9 | - **支持嵌入到ViewPager等滑动控件** 10 | - **支持内嵌ListView,RecycleView等滑动控件** 11 | 12 | 13 | ------------------- 14 | 15 | 效果演示 16 | ------- 17 | 18 | ![演示1-快速滑动飞出](demo-images/demo1.gif)     ![演示2-代码控制飞出](demo-images/demo2.gif) 19 |

20 | ![演示3-嵌入到ViewPager](demo-images/demo3.gif)     ![演示4-内嵌RecycleView](demo-images/demo4.gif) 21 | 22 | ------------------- 23 | 24 | 如何使用 25 | ------- 26 | 27 | - xml引入StackCardsView: 28 | 29 | ``` xml 30 | 39 | ``` 40 | 支持的xml属性设置: 41 | 42 | | 属性名 | 说明 | 类型 |是否必须| 43 | | :-------- | :------| :-- |:--: | 44 | | itemWidth | 卡片宽度 | dimension |是| 45 | | itemHeight | 卡片高度 | dimension |是| 46 | | maxVisibleCnt | 不滑动时最多可以看到的卡片数 | integer |否| 47 | | edgeHeight | 层叠效果高度 | dimension |否| 48 | | scaleFactor | 每层相对于上层的scale系数 | float |否| 49 | | alphaFactor | 每层相对于上层的alpha系数 | float |否| 50 | | dismissFactor | 滑动距离超过控件宽度的多少比例时消失 | float |否| 51 | | dragSensitivity | 滑动灵敏度 | float |否| 52 | 53 |

54 | 55 | 56 | ``` java 57 | 设置adapter: 58 | 59 | mCardsView = Utils.findViewById(root,R.id.cards); 60 | mCardsView.addOnCardSwipedListener(this); 61 | mAdapter = new CardAdapter(); 62 | mCardsView.setAdapter(mAdapter); 63 | 64 | 65 | public class CardAdapter extends StackCardsView.Adapter { 66 | 67 | private List mItems; 68 | 69 | public void appendItems(List items){ 70 | int size = items == null ? 0 : items.size(); 71 | if (size == 0) { 72 | return; 73 | } 74 | if (mItems == null) { 75 | mItems = new ArrayList<>(size); 76 | } 77 | mItems.addAll(items); 78 | notifyDataSetChanged(); 79 | } 80 | 81 | public void remove(int position){ 82 | mItems.remove(position); 83 | notifyItemRemoved(position); 84 | } 85 | 86 | @Override 87 | public int getCount() { 88 | return mItems == null ? 0 : mItems.size(); 89 | } 90 | 91 | @Override 92 | public View getView(int position, View convertView, ViewGroup parent) { 93 | return mItems.get(position).getView(convertView,parent); 94 | } 95 | 96 | @Override 97 | public int getSwipeDirection(int position) { 98 | //这里控制每张卡的支持滑动超过一定距离消失的方向 99 | BaseCardItem item = mItems.get(position); 100 | return item.swipeDir; 101 | } 102 | 103 | @Override 104 | public int getDismissDirection(int position) { 105 | //这里控制每张卡的支持滑动超过一定距离消失的方向 106 | BaseCardItem item = mItems.get(position); 107 | return item.dismissDir; 108 | } 109 | 110 | @Override 111 | public boolean isFastDismissAllowed(int position) { 112 | //这里控制每张卡的支持快速滑动消失的方向 113 | BaseCardItem item = mItems.get(position); 114 | return item.fastDismissAllowed; 115 | } 116 | 117 | @Override 118 | public int getMaxRotation(int position) { 119 | //这里控制每张卡的最大旋转角 120 | BaseCardItem item = mItems.get(position); 121 | return item.maxRotation; 122 | } 123 | } 124 | ``` 125 | 126 | 127 | [问题反馈](https://github.com/wensefu/StackCardsView/issues "问题反馈") 128 | 129 | 130 | License 131 | ------- 132 | 133 | Copyright 2017 wensefu 134 | Licensed under the Apache License, Version 2.0 (the "License"); 135 | you may not use this file except in compliance with the License. 136 | You may obtain a copy of the License at 137 | 138 | http://www.apache.org/licenses/LICENSE-2.0 139 | 140 | Unless required by applicable law or agreed to in writing, software 141 | distributed under the License is distributed on an "AS IS" BASIS, 142 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 143 | See the License for the specific language governing permissions and 144 | limitations under the License. -------------------------------------------------------------------------------- /StackCardsView-demo.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/StackCardsView-demo.apk -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 24 5 | buildToolsVersion "24.0.2" 6 | 7 | defaultConfig { 8 | applicationId "com.beyondsw.widget" 9 | minSdkVersion 14 10 | targetSdkVersion 24 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(include: ['*.jar'], dir: 'libs') 24 | compile 'com.android.support:appcompat-v7:24.2.0' 25 | compile 'com.github.bumptech.glide:glide:3.7.0' 26 | compile project(':library') 27 | compile 'com.android.support:support-v4:24.0.2' 28 | compile 'com.android.support:recyclerview-v7:24.2.0' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/wensefu/tools/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/beyondsw/widget/BaseCardItem.java: -------------------------------------------------------------------------------- 1 | package com.beyondsw.widget; 2 | 3 | import android.content.Context; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | 7 | import com.beyondsw.lib.widget.StackCardsView; 8 | 9 | /** 10 | * Created by wensefu on 17-3-4. 11 | */ 12 | public abstract class BaseCardItem { 13 | 14 | boolean fastDismissAllowed = true; 15 | int swipeDir = StackCardsView.SWIPE_ALL; 16 | int dismissDir = StackCardsView.SWIPE_ALL; 17 | int maxRotation = 8; 18 | 19 | protected Context mContext; 20 | 21 | public BaseCardItem(Context context) { 22 | mContext = context; 23 | } 24 | 25 | public abstract View getView(View convertView, ViewGroup parent); 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/beyondsw/widget/CardAdapter.java: -------------------------------------------------------------------------------- 1 | package com.beyondsw.widget; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import com.beyondsw.lib.widget.StackCardsView; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * Created by wensefu on 17-3-4. 13 | */ 14 | public class CardAdapter extends StackCardsView.Adapter { 15 | 16 | private List mItems; 17 | 18 | public void appendItems(List items){ 19 | int size = items == null ? 0 : items.size(); 20 | if (size == 0) { 21 | return; 22 | } 23 | if (mItems == null) { 24 | mItems = new ArrayList<>(size); 25 | } 26 | mItems.addAll(items); 27 | notifyDataSetChanged(); 28 | } 29 | 30 | public void remove(int position){ 31 | mItems.remove(position); 32 | notifyItemRemoved(position); 33 | } 34 | 35 | @Override 36 | public int getCount() { 37 | return mItems == null ? 0 : mItems.size(); 38 | } 39 | 40 | @Override 41 | public View getView(int position, View convertView, ViewGroup parent) { 42 | return mItems.get(position).getView(convertView,parent); 43 | } 44 | 45 | @Override 46 | public int getSwipeDirection(int position) { 47 | BaseCardItem item = mItems.get(position); 48 | return item.swipeDir; 49 | } 50 | 51 | @Override 52 | public int getDismissDirection(int position) { 53 | BaseCardItem item = mItems.get(position); 54 | return item.dismissDir; 55 | } 56 | 57 | @Override 58 | public boolean isFastDismissAllowed(int position) { 59 | BaseCardItem item = mItems.get(position); 60 | return item.fastDismissAllowed; 61 | } 62 | 63 | @Override 64 | public int getMaxRotation(int position) { 65 | BaseCardItem item = mItems.get(position); 66 | return item.maxRotation; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/beyondsw/widget/CardFragment.java: -------------------------------------------------------------------------------- 1 | package com.beyondsw.widget; 2 | 3 | import android.os.Bundle; 4 | import android.os.Handler; 5 | import android.os.HandlerThread; 6 | import android.os.Message; 7 | import android.support.annotation.Nullable; 8 | import android.support.v4.app.Fragment; 9 | import android.util.Log; 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.CheckBox; 14 | import android.widget.CompoundButton; 15 | 16 | import com.beyondsw.lib.widget.StackCardsView; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | /** 22 | * Created by wensefu on 17-3-4. 23 | */ 24 | public class CardFragment extends Fragment implements Handler.Callback,StackCardsView.OnCardSwipedListener 25 | ,View.OnClickListener,CompoundButton.OnCheckedChangeListener{ 26 | 27 | private static final String TAG ="StackCardsView-DEMO"; 28 | 29 | private StackCardsView mCardsView; 30 | private CardAdapter mAdapter; 31 | private HandlerThread mWorkThread; 32 | private Handler mWorkHandler; 33 | private Handler mMainHandler; 34 | private static final int MSG_START_LOAD_DATA = 1; 35 | private static final int MSG_DATA_LOAD_DONE = 2; 36 | private volatile int mStartIndex; 37 | private static final int PAGE_COUNT = 10; 38 | 39 | private View mLeftBtn; 40 | private View mRightBtn; 41 | private View mUpBtn; 42 | private View mDownBtn; 43 | private CheckBox mCb; 44 | 45 | private Callback mCallback; 46 | 47 | public interface Callback { 48 | void onViewPagerCbChanged(boolean checked); 49 | } 50 | 51 | @Nullable 52 | @Override 53 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 54 | View root = inflater.inflate(R.layout.page1,null); 55 | 56 | mCb = Utils.findViewById(root, R.id.view_pager_cb); 57 | mCb.setOnCheckedChangeListener(this); 58 | if (mCallback != null) { 59 | mCallback.onViewPagerCbChanged(mCb.isChecked()); 60 | } 61 | mLeftBtn = Utils.findViewById(root,R.id.left); 62 | mRightBtn = Utils.findViewById(root,R.id.right); 63 | mUpBtn = Utils.findViewById(root,R.id.up); 64 | mDownBtn = Utils.findViewById(root, R.id.down); 65 | mLeftBtn.setOnClickListener(this); 66 | mRightBtn.setOnClickListener(this); 67 | mUpBtn.setOnClickListener(this); 68 | mDownBtn.setOnClickListener(this); 69 | 70 | mCardsView = Utils.findViewById(root,R.id.cards); 71 | mCardsView.addOnCardSwipedListener(this); 72 | mAdapter = new CardAdapter(); 73 | mCardsView.setAdapter(mAdapter); 74 | mMainHandler = new Handler(this); 75 | mWorkThread = new HandlerThread("data_loader"); 76 | mWorkThread.start(); 77 | mWorkHandler = new Handler(mWorkThread.getLooper(),this); 78 | mWorkHandler.obtainMessage(MSG_START_LOAD_DATA).sendToTarget(); 79 | return root; 80 | } 81 | 82 | void setCallback(Callback callback) { 83 | mCallback = callback; 84 | if (mCb != null) { 85 | mCallback.onViewPagerCbChanged(mCb.isChecked()); 86 | } 87 | } 88 | 89 | @Override 90 | public void onDestroyView() { 91 | super.onDestroyView(); 92 | mCardsView.removeOnCardSwipedListener(this); 93 | mWorkThread.quit(); 94 | mWorkHandler.removeMessages(MSG_START_LOAD_DATA); 95 | mMainHandler.removeMessages(MSG_DATA_LOAD_DONE); 96 | mStartIndex = 0; 97 | } 98 | 99 | @Override 100 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 101 | if (mCallback != null) { 102 | mCallback.onViewPagerCbChanged(isChecked); 103 | } 104 | } 105 | 106 | @Override 107 | public void onClick(View v) { 108 | if (v == mLeftBtn) { 109 | mCardsView.removeCover(StackCardsView.SWIPE_LEFT); 110 | } else if (v == mRightBtn) { 111 | mCardsView.removeCover(StackCardsView.SWIPE_RIGHT); 112 | } else if (v == mUpBtn) { 113 | mCardsView.removeCover(StackCardsView.SWIPE_UP); 114 | } else if (v == mDownBtn) { 115 | mCardsView.removeCover(StackCardsView.SWIPE_DOWN); 116 | } 117 | } 118 | 119 | @Override 120 | public void onCardDismiss(int direction) { 121 | mAdapter.remove(0); 122 | if (mAdapter.getCount() < 3) { 123 | if (!mWorkHandler.hasMessages(MSG_START_LOAD_DATA)) { 124 | mWorkHandler.obtainMessage(MSG_START_LOAD_DATA).sendToTarget(); 125 | } 126 | } 127 | } 128 | 129 | @Override 130 | public void onCardScrolled(View view, float progress, int direction) { 131 | Log.d(TAG, "onCardScrolled: view=" + view.hashCode() + ", progress=" + progress + ",direction=" + direction); 132 | Object tag = view.getTag(); 133 | if (tag instanceof ImageCardItem.ViewHolder) { 134 | ImageCardItem.ViewHolder vh = (ImageCardItem.ViewHolder)tag; 135 | if (progress > 0) { 136 | switch (direction){ 137 | case StackCardsView.SWIPE_LEFT: 138 | vh.left.setAlpha(progress); 139 | vh.right.setAlpha(0f); 140 | vh.up.setAlpha(0f); 141 | vh.down.setAlpha(0f); 142 | break; 143 | case StackCardsView.SWIPE_RIGHT: 144 | vh.right.setAlpha(progress); 145 | vh.left.setAlpha(0f); 146 | vh.up.setAlpha(0f); 147 | vh.down.setAlpha(0f); 148 | break; 149 | case StackCardsView.SWIPE_UP: 150 | vh.up.setAlpha(progress); 151 | vh.left.setAlpha(0f); 152 | vh.right.setAlpha(0f); 153 | vh.down.setAlpha(0f); 154 | break; 155 | case StackCardsView.SWIPE_DOWN: 156 | vh.down.setAlpha(progress); 157 | vh.left.setAlpha(0f); 158 | vh.right.setAlpha(0f); 159 | vh.up.setAlpha(0f); 160 | break; 161 | } 162 | } else { 163 | vh.left.setAlpha(0f); 164 | vh.right.setAlpha(0f); 165 | vh.up.setAlpha(0f); 166 | vh.down.setAlpha(0f); 167 | } 168 | } 169 | } 170 | 171 | @SuppressWarnings("unchecked") 172 | @Override 173 | public boolean handleMessage(Message msg) { 174 | switch (msg.what){ 175 | case MSG_START_LOAD_DATA:{ 176 | List data = loadData(mStartIndex); 177 | mMainHandler.obtainMessage(MSG_DATA_LOAD_DONE,data).sendToTarget(); 178 | break; 179 | } 180 | case MSG_DATA_LOAD_DONE:{ 181 | List data = (List) msg.obj; 182 | mAdapter.appendItems(data); 183 | mStartIndex += sizeOfImage(data); 184 | break; 185 | } 186 | } 187 | return true; 188 | } 189 | 190 | private int sizeOfImage(List items){ 191 | if(items==null){ 192 | return 0; 193 | } 194 | int size = 0; 195 | for (BaseCardItem item : items) { 196 | if (item instanceof ImageCardItem) { 197 | size++; 198 | } 199 | } 200 | return size; 201 | } 202 | 203 | 204 | private List loadData(int startIndex) { 205 | if (startIndex < ImageUrls.images.length) { 206 | final int endIndex = Math.min(mStartIndex + PAGE_COUNT, ImageUrls.images.length - 1); 207 | List result = new ArrayList<>(endIndex - startIndex + 1); 208 | for (int i = startIndex; i <= endIndex; i++) { 209 | ImageCardItem item = new ImageCardItem(getActivity(), ImageUrls.images[i], ImageUrls.labels[i]); 210 | item.dismissDir = StackCardsView.SWIPE_ALL; 211 | item.fastDismissAllowed = true; 212 | result.add(item); 213 | } 214 | if (startIndex == 0) { 215 | ScrollCardItem item = new ScrollCardItem(getActivity()); 216 | result.add(result.size() / 2, item); 217 | } 218 | return result; 219 | } 220 | return null; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /app/src/main/java/com/beyondsw/widget/DemoActivity.java: -------------------------------------------------------------------------------- 1 | package com.beyondsw.widget; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.app.Fragment; 5 | import android.support.v4.app.FragmentManager; 6 | import android.support.v4.app.FragmentPagerAdapter; 7 | import android.support.v7.app.AppCompatActivity; 8 | 9 | /** 10 | * Created by wensefu on 2017/2/12. 11 | */ 12 | 13 | public class DemoActivity extends AppCompatActivity implements CardFragment.Callback{ 14 | 15 | private static final String TAG = "DemoActivity"; 16 | 17 | private MyViewPager mPager; 18 | private Fragment mSettingFragment; 19 | private CardFragment mCardFragment; 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.main); 25 | mSettingFragment = new SettingFragment(); 26 | mCardFragment = new CardFragment(); 27 | mCardFragment.setCallback(this); 28 | mPager = Utils.findViewById(this,R.id.viewpager); 29 | mPager.setAdapter(new MyPagerAdapter(getSupportFragmentManager())); 30 | } 31 | 32 | @Override 33 | public void onViewPagerCbChanged(boolean checked) { 34 | mPager.setScrollable(checked); 35 | } 36 | 37 | private class MyPagerAdapter extends FragmentPagerAdapter { 38 | 39 | public MyPagerAdapter(FragmentManager fm) { 40 | super(fm); 41 | } 42 | 43 | @Override 44 | public Fragment getItem(int position) { 45 | return position == 0 ? mCardFragment : mSettingFragment; 46 | } 47 | 48 | @Override 49 | public int getCount() { 50 | return 2; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/beyondsw/widget/ImageCardItem.java: -------------------------------------------------------------------------------- 1 | package com.beyondsw.widget; 2 | 3 | import android.content.Context; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.ImageView; 7 | import android.widget.TextView; 8 | 9 | import com.bumptech.glide.Glide; 10 | 11 | /** 12 | * Created by wensefu on 17-3-4. 13 | */ 14 | public class ImageCardItem extends BaseCardItem { 15 | 16 | private static final String TAG = "ImageCardItem"; 17 | 18 | private String url; 19 | private String label; 20 | 21 | public ImageCardItem(Context context, String url, String label) { 22 | super(context); 23 | this.url = url; 24 | this.label = label; 25 | } 26 | 27 | public static class ViewHolder{ 28 | ImageView left; 29 | ImageView right; 30 | ImageView up; 31 | ImageView down; 32 | } 33 | 34 | @Override 35 | public View getView(View convertView, ViewGroup parent) { 36 | convertView = View.inflate(mContext,R.layout.item_imagecard,null); 37 | ImageView imageView = Utils.findViewById(convertView,R.id.image); 38 | TextView labelview = Utils.findViewById(convertView,R.id.label); 39 | ImageView left = Utils.findViewById(convertView,R.id.left); 40 | ImageView right = Utils.findViewById(convertView,R.id.right); 41 | ImageView up = Utils.findViewById(convertView,R.id.up); 42 | ImageView down = Utils.findViewById(convertView,R.id.down); 43 | ViewHolder vh = new ViewHolder(); 44 | vh.left = left; 45 | vh.right = right; 46 | vh.up = up; 47 | vh.down = down; 48 | convertView.setTag(vh); 49 | Glide.with(mContext) 50 | .load(url) 51 | .placeholder(R.drawable.img_dft) 52 | .centerCrop() 53 | .crossFade() 54 | .into(imageView); 55 | labelview.setText(label); 56 | return convertView; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/beyondsw/widget/ImageUrls.java: -------------------------------------------------------------------------------- 1 | package com.beyondsw.widget; 2 | 3 | /** 4 | * Created by wensefu on 2017/2/12. 5 | */ 6 | 7 | public class ImageUrls { 8 | 9 | public static final String[] images = { 10 | "http://p1.bpimg.com/4862/85a35130b810b20as.jpg", 11 | "http://p1.bpimg.com/4862/666e742b9ccbd2d0s.jpg", 12 | "http://p1.bpimg.com/4862/6834da29f2fc5eb8s.jpg", 13 | "http://p1.bpimg.com/4862/3b9c551468b3031bs.jpg", 14 | "http://p1.bpimg.com/4862/2a4c1a5cca89563es.jpg", 15 | "http://p1.bpimg.com/4862/b4289d92b2836b3fs.jpg", 16 | "http://p1.bpimg.com/4862/6bb6fdc93c96fc4ds.jpg", 17 | "http://p1.bpimg.com/4862/a7710cb7e90a9d9bs.jpg", 18 | "http://p1.bpimg.com/4862/b612e86bb23e6031s.jpg", 19 | "http://p1.bpimg.com/4862/477b3d1b99775519s.jpg", 20 | "http://p1.bpimg.com/4862/b22b2aa9eef325d6s.jpg", 21 | "http://p1.bpimg.com/4862/e3410480c3700c5bs.jpg", 22 | "http://p1.bpimg.com/4862/49604cf86c2e431bs.jpg", 23 | "http://p1.bpimg.com/4862/2266446327937beds.jpg", 24 | "http://p1.bpimg.com/4862/5798081c23d6d05as.jpg", 25 | "http://p1.bpimg.com/4862/de3507a035041046s.jpg", 26 | "http://p1.bpimg.com/4862/d748f2e29341e6fes.jpg", 27 | "http://p1.bpimg.com/4862/5728eb126c0aef42s.jpg", 28 | "http://p1.bpimg.com/4862/be4ca57c35844766s.jpg", 29 | "http://p1.bpimg.com/4862/a718d50dc1b3c984s.jpg", 30 | "http://p1.bpimg.com/4862/4ce98c6721319a7ds.jpg", 31 | "http://p1.bpimg.com/4862/2cc78d04187f65e7s.jpg", 32 | "http://p1.bpimg.com/4862/051ece48a6d98499s.jpg", 33 | "http://p1.bpimg.com/4862/626d4af216f9d33es.jpg", 34 | "http://p1.bpimg.com/4862/61cb617aa8c3501bs.jpg", 35 | "http://p1.bpimg.com/4862/09129eb397d9de6cs.jpg", 36 | "http://p1.bpimg.com/4862/64de982150cfad02s.jpg", 37 | "http://p1.bpimg.com/4862/b1f7b1d418d0f8bas.jpg", 38 | "http://p1.bpimg.com/4862/0182f165eac905efs.jpg", 39 | "http://p1.bpimg.com/4862/d5a35ad258fc2f4cs.jpg", 40 | "http://p1.bpimg.com/4862/e0e488e94b86b787s.jpg", 41 | }; 42 | 43 | public static final String[] labels; 44 | 45 | 46 | static { 47 | labels = new String[images.length]; 48 | for (int i = 0; i < labels.length; i++) { 49 | labels[i] = "图片 " + (i + 1); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/beyondsw/widget/MyViewPager.java: -------------------------------------------------------------------------------- 1 | package com.beyondsw.widget; 2 | 3 | import android.content.Context; 4 | import android.support.v4.view.ViewPager; 5 | import android.util.AttributeSet; 6 | import android.view.MotionEvent; 7 | 8 | /** 9 | * Created by wensefu on 17-3-6. 10 | */ 11 | public class MyViewPager extends ViewPager{ 12 | 13 | private boolean mScrollAble; 14 | 15 | public MyViewPager(Context context) { 16 | super(context); 17 | } 18 | 19 | public MyViewPager(Context context, AttributeSet attrs) { 20 | super(context, attrs); 21 | } 22 | 23 | public void setScrollable(boolean scrollable) { 24 | if (mScrollAble != scrollable) { 25 | mScrollAble = scrollable; 26 | } 27 | } 28 | 29 | @Override 30 | public boolean onInterceptTouchEvent(MotionEvent ev) { 31 | if (!mScrollAble) { 32 | return false; 33 | } 34 | return super.onInterceptTouchEvent(ev); 35 | } 36 | 37 | @Override 38 | public boolean onTouchEvent(MotionEvent ev) { 39 | if (!mScrollAble) { 40 | return false; 41 | } 42 | return super.onTouchEvent(ev); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/beyondsw/widget/ScrollCardItem.java: -------------------------------------------------------------------------------- 1 | package com.beyondsw.widget; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.LinearLayoutManager; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.TextView; 10 | 11 | import com.beyondsw.lib.widget.StackCardsView; 12 | 13 | /** 14 | * Created by wensefu on 17-3-4. 15 | */ 16 | public class ScrollCardItem extends BaseCardItem { 17 | 18 | 19 | public ScrollCardItem(Context context) { 20 | super(context); 21 | swipeDir = StackCardsView.SWIPE_LEFT | StackCardsView.SWIPE_RIGHT; 22 | } 23 | 24 | @Override 25 | public View getView(View convertView, ViewGroup parent) { 26 | convertView = View.inflate(mContext, R.layout.item_scrollcard, null); 27 | RecyclerView recyclerView = Utils.findViewById(convertView, R.id.recyclerView); 28 | recyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false)); 29 | recyclerView.setAdapter(new VerticalAdapter()); 30 | return convertView; 31 | } 32 | 33 | private static class VerticalVH extends RecyclerView.ViewHolder { 34 | 35 | TextView textView; 36 | 37 | public VerticalVH(View itemView) { 38 | super(itemView); 39 | textView = Utils.findViewById(itemView,R.id.text_v); 40 | } 41 | } 42 | 43 | private class VerticalAdapter extends RecyclerView.Adapter { 44 | 45 | private String[] array = { 46 | "My life is brilliant.", 47 | "My life is brilliant.", 48 | "My love is pure.", 49 | "I saw an angel.", 50 | "Of that I'm sure.", 51 | "She smiled at me on the subway.", 52 | "She was with another man.", 53 | "But I won't lose no sleep on that,", 54 | "'Cause I've got a plan.", 55 | "You're beautiful. You're beautiful,", 56 | "You're beautiful, it's true.", 57 | "I saw your face in a crowded place,", 58 | "And I don't know what to do,", 59 | "'Cause I'll never be with you.", 60 | "Yeah, she caught my eye,", 61 | "As we walked on by.", 62 | "She could see from my face that I was,", 63 | "flying high,", 64 | "And I don't think that I'll see her again,", 65 | "But we shared a moment that will last till the end.", 66 | "You're beautiful. You're beautiful.", 67 | "You're beautiful, it's true.", 68 | "I saw your face in a crowded place,", 69 | "And I don't know what to do,", 70 | "'Cause I'll never be with you.", 71 | "You're beautiful. You're beautiful.", 72 | "You're beautiful, it's true.", 73 | "There must be an angel with a smile on her face,", 74 | "When she thought up that I should be with you.", 75 | "But it's time to face the truth,", 76 | "I will never be with you.", 77 | }; 78 | 79 | @Override 80 | public VerticalVH onCreateViewHolder(ViewGroup parent, int viewType) { 81 | View itemView = LayoutInflater.from(mContext).inflate(R.layout.item_recyclerview_v, parent, false); 82 | return new VerticalVH(itemView); 83 | } 84 | 85 | @Override 86 | public void onBindViewHolder(VerticalVH holder, int position) { 87 | holder.textView.setText(array[position]); 88 | } 89 | 90 | @Override 91 | public int getItemCount() { 92 | return array.length; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/beyondsw/widget/SettingFragment.java: -------------------------------------------------------------------------------- 1 | package com.beyondsw.widget; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | /** 11 | * Created by wensefu on 17-3-4. 12 | */ 13 | public class SettingFragment extends Fragment{ 14 | 15 | @Nullable 16 | @Override 17 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 18 | return inflater.inflate(R.layout.page2,null); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/beyondsw/widget/Utils.java: -------------------------------------------------------------------------------- 1 | package com.beyondsw.widget; 2 | 3 | import android.app.Activity; 4 | import android.view.View; 5 | 6 | /** 7 | * Created by wensefu on 17-3-4. 8 | */ 9 | public class Utils { 10 | 11 | @SuppressWarnings("unchecked") 12 | public static T findViewById(Activity act, int id){ 13 | return (T)act.findViewById(id); 14 | } 15 | 16 | @SuppressWarnings("unchecked") 17 | public static T findViewById(View parent,int id){ 18 | return (T)parent.findViewById(id); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/card_bg.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/app/src/main/res/drawable-xhdpi/card_bg.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/app/src/main/res/drawable-xhdpi/ic_down.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/app/src/main/res/drawable-xhdpi/ic_left.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/app/src/main/res/drawable-xhdpi/ic_right.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/app/src/main/res/drawable-xhdpi/ic_up.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/img_dft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/app/src/main/res/drawable-xhdpi/img_dft.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/testdrag.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/app/src/main/res/drawable-xhdpi/testdrag.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_bottom_btn_normal.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_bottom_btn_pressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_bottom_btn_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_imagecard.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 21 | 22 | 28 | 29 | 40 | 41 | 52 | 53 | 64 | 65 | 76 | 77 | 78 | 86 | 87 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_recyclerview_v.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_scrollcard.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/page1.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 23 | 24 | 25 | 35 | 43 | 48 | 56 | 61 | 69 | 74 | 82 | 83 | 84 | 93 | 94 | -------------------------------------------------------------------------------- /app/src/main/res/layout/page2.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | StackCardsView 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.1.2' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /demo-images/demo1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/demo-images/demo1.gif -------------------------------------------------------------------------------- /demo-images/demo1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/demo-images/demo1.mp4 -------------------------------------------------------------------------------- /demo-images/demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/demo-images/demo2.gif -------------------------------------------------------------------------------- /demo-images/demo2.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/demo-images/demo2.mp4 -------------------------------------------------------------------------------- /demo-images/demo3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/demo-images/demo3.gif -------------------------------------------------------------------------------- /demo-images/demo3.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/demo-images/demo3.mp4 -------------------------------------------------------------------------------- /demo-images/demo4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/demo-images/demo4.gif -------------------------------------------------------------------------------- /demo-images/demo4.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/demo-images/demo4.mp4 -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wensefu/StackCardsView/3427ecdfc119007ba682139c7d957d10ba393970/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 24 5 | buildToolsVersion "24.0.2" 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | targetSdkVersion 24 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | testCompile 'junit:junit:4.12' 24 | compile 'com.android.support:appcompat-v7:24.2.0' 25 | } 26 | -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/wensefu/tools/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/ISwipeTouchHelper.java: -------------------------------------------------------------------------------- 1 | package com.beyondsw.lib.widget; 2 | 3 | import android.view.MotionEvent; 4 | 5 | /** 6 | * Created by wensefu on 17-2-12. 7 | */ 8 | public interface ISwipeTouchHelper { 9 | 10 | boolean onInterceptTouchEvent(MotionEvent ev); 11 | 12 | boolean onTouchEvent(MotionEvent ev); 13 | 14 | /** 15 | * 当ViewGroup的子view列表发生变化并且layout完成,设置好scale等属性后回调 16 | */ 17 | void onChildChanged(); 18 | 19 | void onChildAppend(); 20 | 21 | /** 22 | * @return 当前是否有子view在拖动,做消失动画等,如果有则不进行数据刷新,等待空闲状态时再刷新 23 | */ 24 | boolean isCoverIdle(); 25 | 26 | void removeCover(int direction); 27 | } 28 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/StackCardsView.java: -------------------------------------------------------------------------------- 1 | package com.beyondsw.lib.widget; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.database.Observable; 6 | import android.util.AttributeSet; 7 | import android.util.DisplayMetrics; 8 | import android.util.Log; 9 | import android.util.TypedValue; 10 | import android.view.Gravity; 11 | import android.view.MotionEvent; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.widget.FrameLayout; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | 20 | /** 21 | * Created by wensefu on 2017/2/10. 22 | */ 23 | 24 | public class StackCardsView extends FrameLayout { 25 | 26 | private static final String TAG = "StackCardsView"; 27 | 28 | public static boolean DEBUG = true; 29 | 30 | /** 31 | * 左滑 32 | */ 33 | public static final int SWIPE_LEFT = 1; 34 | 35 | /** 36 | * 右滑 37 | */ 38 | public static final int SWIPE_RIGHT = 1 << 1; 39 | 40 | /** 41 | * 上滑 42 | */ 43 | public static final int SWIPE_UP = 1 << 2; 44 | 45 | /** 46 | * 下滑 47 | */ 48 | public static final int SWIPE_DOWN = 1 << 3; 49 | 50 | /** 51 | * 任意方向滑动 52 | */ 53 | public static final int SWIPE_ALL = SWIPE_LEFT | SWIPE_RIGHT | SWIPE_UP | SWIPE_DOWN; 54 | 55 | /** 56 | * 禁止滑动 57 | */ 58 | public static final int SWIPE_NONE = 0; 59 | 60 | private Adapter mAdapter; 61 | 62 | /** 63 | * 默认静止时最多可以看到的卡片数 64 | */ 65 | private static final int MAX_VISIBLE_CNT = 3; 66 | 67 | /** 68 | * 默认层叠效果高度(dp) 69 | */ 70 | private static final int EDGE_HEIGHT = 8; 71 | 72 | /** 73 | * 默认相对前一张卡片的缩放比例 74 | */ 75 | private static final float SCALE_FACTOR = .8f; 76 | 77 | /** 78 | * 默认相对前一张卡片的透明度比例 79 | */ 80 | private static final float ALPHA_FACTOR = .8f; 81 | 82 | /** 83 | * 默认可以消失的滑动距离与控件宽度比 84 | */ 85 | private static final float DISMISS_FACTOR = .4f; 86 | 87 | /** 88 | * 默认卡片消失时的透明度 89 | */ 90 | private static final float DISMISS_ALPHA = .3f; 91 | 92 | private static final float DRAG_SENSITIVITY = 2f; 93 | 94 | private static final int INVALID_SIZE = Integer.MIN_VALUE; 95 | private int mItemWidth; 96 | private int mItemHeight; 97 | private int mMaxVisibleCnt; 98 | private float mScaleFactor; 99 | private float mAlphaFactor; 100 | private float mDismissFactor; 101 | private int mLayerEdgeHeight; 102 | private float mDismissAlpha; 103 | private float mDragSensitivity; 104 | private float mDismissDistance; 105 | 106 | private InnerDataObserver mDataObserver; 107 | private boolean mHasRegisteredObserver; 108 | 109 | private ISwipeTouchHelper mTouchHelper; 110 | private List mCardSwipedListeners; 111 | 112 | private boolean mNeedAdjustChildren; 113 | 114 | private Runnable mPendingTask; 115 | 116 | private float[] mScaleArray; 117 | private float[] mAlphaArray; 118 | private float[] mTranslationYArray; 119 | 120 | private int mLastLeft; 121 | private int mLastTop; 122 | private int mLastRight; 123 | private int mLastBottom; 124 | 125 | public StackCardsView(Context context) { 126 | this(context, null); 127 | } 128 | 129 | public StackCardsView(Context context, AttributeSet attrs) { 130 | this(context, attrs, 0); 131 | } 132 | 133 | public StackCardsView(Context context, AttributeSet attrs, int defStyleAttr) { 134 | super(context, attrs, defStyleAttr); 135 | setChildrenDrawingOrderEnabled(true); 136 | final TypedArray a = context.obtainStyledAttributes(attrs, 137 | R.styleable.StackCardsView, defStyleAttr, 0); 138 | mItemWidth = a.getDimensionPixelSize(R.styleable.StackCardsView_itemWidth, INVALID_SIZE); 139 | if (mItemWidth == INVALID_SIZE) { 140 | throw new IllegalArgumentException("itemWidth must be specified"); 141 | } 142 | mItemHeight = a.getDimensionPixelSize(R.styleable.StackCardsView_itemHeight, INVALID_SIZE); 143 | if (mItemHeight == INVALID_SIZE) { 144 | throw new IllegalArgumentException("itemHeight must be specified"); 145 | } 146 | mMaxVisibleCnt = a.getInt(R.styleable.StackCardsView_maxVisibleCnt, MAX_VISIBLE_CNT); 147 | mScaleFactor = a.getFloat(R.styleable.StackCardsView_scaleFactor, SCALE_FACTOR); 148 | mAlphaFactor = a.getFloat(R.styleable.StackCardsView_alphaFactor, ALPHA_FACTOR); 149 | mDismissFactor = a.getFloat(R.styleable.StackCardsView_dismissFactor, DISMISS_FACTOR); 150 | mLayerEdgeHeight = a.getDimensionPixelSize(R.styleable.StackCardsView_edgeHeight, (int) dp2px(context, EDGE_HEIGHT)); 151 | mDismissAlpha = a.getFloat(R.styleable.StackCardsView_dismissAlpha, DISMISS_ALPHA); 152 | mDragSensitivity = a.getFloat(R.styleable.StackCardsView_dragSensitivity, DRAG_SENSITIVITY); 153 | a.recycle(); 154 | } 155 | 156 | public static float dp2px(Context context, float dp) { 157 | DisplayMetrics dm = context.getResources().getDisplayMetrics(); 158 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, dm); 159 | } 160 | 161 | public interface OnCardSwipedListener { 162 | 163 | void onCardDismiss(int direction); 164 | 165 | void onCardScrolled(View view, float progress, int direction); 166 | } 167 | 168 | public void addOnCardSwipedListener(OnCardSwipedListener listener) { 169 | if (mCardSwipedListeners == null) { 170 | mCardSwipedListeners = new ArrayList<>(); 171 | mCardSwipedListeners.add(listener); 172 | } else if (!mCardSwipedListeners.contains(listener)) { 173 | mCardSwipedListeners.add(listener); 174 | } 175 | } 176 | 177 | public void removeOnCardSwipedListener(OnCardSwipedListener listener) { 178 | if (mCardSwipedListeners != null && mCardSwipedListeners.contains(listener)) { 179 | mCardSwipedListeners.remove(listener); 180 | } 181 | } 182 | 183 | @Override 184 | public void addView(View child) { 185 | throw new UnsupportedOperationException("addView(View) is not supported"); 186 | } 187 | 188 | @Override 189 | public void addView(View child, int index) { 190 | throw new UnsupportedOperationException("addView(View, int) is not supported"); 191 | } 192 | 193 | @Override 194 | public void removeView(View child) { 195 | throw new UnsupportedOperationException("removeView(View) is not supported"); 196 | } 197 | 198 | @Override 199 | public void removeViewAt(int index) { 200 | throw new UnsupportedOperationException("removeViewAt(int) is not supported"); 201 | } 202 | 203 | @Override 204 | public void removeAllViews() { 205 | throw new UnsupportedOperationException("removeAllViews() is not supported"); 206 | } 207 | 208 | float getDragSensitivity() { 209 | return mDragSensitivity; 210 | } 211 | 212 | public float getDismissDistance() { 213 | if (mDismissDistance > 0) { 214 | return mDismissDistance; 215 | } 216 | mDismissDistance = getWidth() * mDismissFactor; 217 | return mDismissDistance; 218 | } 219 | 220 | float getDismissAlpha() { 221 | return mDismissAlpha; 222 | } 223 | 224 | @Override 225 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 226 | super.onLayout(changed, left, top, right, bottom); 227 | if (mNeedAdjustChildren) { 228 | adjustChildren(); 229 | if (mTouchHelper != null) { 230 | mTouchHelper.onChildChanged(); 231 | } 232 | mNeedAdjustChildren = false; 233 | } 234 | int cnt = getChildCount(); 235 | if (cnt > 0) { 236 | View last = getChildAt(cnt - 1); 237 | mLastLeft = last.getLeft(); 238 | mLastTop = last.getTop(); 239 | mLastRight = last.getRight(); 240 | mLastBottom = last.getBottom(); 241 | } 242 | } 243 | 244 | private void adjustChildren() { 245 | final int cnt = getChildCount(); 246 | if (cnt == 0) { 247 | return; 248 | } 249 | float scale = 0; 250 | float alpha; 251 | float translationY = 0; 252 | int half_childHeight = 0; 253 | int maxVisibleIndex = Math.min(cnt, mMaxVisibleCnt) - 1; 254 | mScaleArray = new float[cnt]; 255 | mAlphaArray = new float[cnt]; 256 | mTranslationYArray = new float[cnt]; 257 | for (int i = 0; i <= maxVisibleIndex; i++) { 258 | View child = getChildAt(i); 259 | if (half_childHeight == 0) { 260 | half_childHeight = child.getMeasuredHeight() / 2; 261 | } 262 | scale = (float) Math.pow(mScaleFactor, i); 263 | mScaleArray[i] = scale; 264 | alpha = (float) Math.pow(mAlphaFactor, i); 265 | mAlphaArray[i] = alpha; 266 | translationY = half_childHeight * (1 - scale) + mLayerEdgeHeight * i; 267 | mTranslationYArray[i] = translationY; 268 | 269 | child.setScaleX(scale); 270 | child.setScaleY(scale); 271 | child.setAlpha(alpha); 272 | child.setTranslationY(translationY); 273 | } 274 | for (int i = maxVisibleIndex + 1; i < cnt; i++) { 275 | View child = getChildAt(i); 276 | mScaleArray[i] = scale; 277 | mAlphaArray[i] = 0; 278 | mTranslationYArray[i] = translationY; 279 | child.setScaleX(scale); 280 | child.setScaleY(scale); 281 | child.setAlpha(0); 282 | child.setTranslationY(translationY); 283 | } 284 | } 285 | 286 | void onCoverStatusChanged(boolean idle) { 287 | if (idle) { 288 | if (mPendingTask != null) { 289 | mPendingTask.run(); 290 | mPendingTask = null; 291 | } 292 | } 293 | } 294 | 295 | void onCardDismissed(int direction) { 296 | if (mCardSwipedListeners != null) { 297 | for (OnCardSwipedListener listener : mCardSwipedListeners) { 298 | listener.onCardDismiss(direction); 299 | } 300 | } 301 | } 302 | 303 | void tryAppendChild() { 304 | final int childCount = getChildCount(); 305 | if (mAdapter.getCount() > childCount) { 306 | View view = mAdapter.getView(childCount, null, StackCardsView.this); 307 | addViewInLayout(view, -1, buildLayoutParams(mAdapter, childCount), true); 308 | view.layout(mLastLeft, mLastTop, mLastRight, mLastBottom); 309 | if (mTouchHelper != null) { 310 | mTouchHelper.onChildAppend(); 311 | } 312 | } 313 | } 314 | 315 | void onCoverScrolled(View scrollingView, float progress, int direction) { 316 | if (mCardSwipedListeners != null) { 317 | for (OnCardSwipedListener listener : mCardSwipedListeners) { 318 | listener.onCardScrolled(scrollingView, progress, direction); 319 | } 320 | } 321 | } 322 | 323 | void updateChildrenProgress(float progress, View scrollingView) { 324 | final int cnt = getChildCount(); 325 | int startIndex = indexOfChild(scrollingView) + 1; 326 | if (startIndex >= cnt) { 327 | return; 328 | } 329 | float oriScale; 330 | float oriAlpha; 331 | float oriTranslationY; 332 | float maxScale; 333 | float maxAlpha; 334 | float maxTranslationY; 335 | float progressScale; 336 | for (int i = startIndex; i < cnt; i++) { 337 | View child = getChildAt(i); 338 | int oriIndex = Math.min(mScaleArray.length - 1, i - startIndex + 1); 339 | if (child.getVisibility() != View.GONE) { 340 | if (mScaleArray != null) { 341 | oriScale = mScaleArray[oriIndex]; 342 | maxScale = mScaleArray[i - startIndex]; 343 | progressScale = oriScale + (maxScale - oriScale) * progress; 344 | child.setScaleX(progressScale); 345 | child.setScaleY(progressScale); 346 | } 347 | 348 | if (mAlphaArray != null) { 349 | oriAlpha = mAlphaArray[oriIndex]; 350 | maxAlpha = mAlphaArray[i - startIndex]; 351 | child.setAlpha(oriAlpha + (maxAlpha - oriAlpha) * progress); 352 | } 353 | 354 | if (mTranslationYArray != null) { 355 | oriTranslationY = mTranslationYArray[oriIndex]; 356 | maxTranslationY = mTranslationYArray[i - startIndex]; 357 | child.setTranslationY(oriTranslationY + (maxTranslationY - oriTranslationY) * progress); 358 | } 359 | } 360 | } 361 | } 362 | 363 | @Override 364 | protected int getChildDrawingOrder(int childCount, int i) { 365 | return childCount - 1 - i; 366 | } 367 | 368 | 369 | @Override 370 | protected void onAttachedToWindow() { 371 | super.onAttachedToWindow(); 372 | safeRegisterObserver(); 373 | } 374 | 375 | @Override 376 | protected void onDetachedFromWindow() { 377 | super.onDetachedFromWindow(); 378 | safeUnRegisterObserver(); 379 | } 380 | 381 | private void safeUnRegisterObserver() { 382 | if (mAdapter != null && mDataObserver != null && mHasRegisteredObserver) { 383 | mAdapter.unregisterDataObserver(mDataObserver); 384 | mHasRegisteredObserver = false; 385 | } 386 | } 387 | 388 | private void safeRegisterObserver() { 389 | safeUnRegisterObserver(); 390 | if (mDataObserver == null) { 391 | mDataObserver = new InnerDataObserver(); 392 | } 393 | if (mAdapter != null) { 394 | mAdapter.registerDataObserver(mDataObserver); 395 | mHasRegisteredObserver = true; 396 | } 397 | } 398 | 399 | private LayoutParams buildLayoutParams(Adapter adapter, int position) { 400 | return new LayoutParams(mItemWidth, mItemHeight, Gravity.CENTER) 401 | .swipeDirection(adapter.getSwipeDirection(position)) 402 | .dismissDirection(adapter.getDismissDirection(position)) 403 | .fastDismissAllowed(adapter.isFastDismissAllowed(position)) 404 | .maxRotation(adapter.getMaxRotation(position)); 405 | } 406 | 407 | private void initChildren() { 408 | int cnt = mAdapter == null ? 0 : mAdapter.getCount(); 409 | if (cnt == 0) { 410 | removeAllViewsInLayout(); 411 | } else { 412 | removeAllViewsInLayout(); 413 | cnt = Math.min(cnt, mMaxVisibleCnt + 1); 414 | for (int i = 0; i < cnt; i++) { 415 | addViewInLayout(mAdapter.getView(i, null, this), -1, buildLayoutParams(mAdapter, i), true); 416 | } 417 | } 418 | mNeedAdjustChildren = true; 419 | requestLayout(); 420 | } 421 | 422 | public void setAdapter(Adapter adapter) { 423 | safeUnRegisterObserver(); 424 | mAdapter = adapter; 425 | safeRegisterObserver(); 426 | initChildren(); 427 | } 428 | 429 | public void removeCover(int direction) { 430 | if (mTouchHelper != null) { 431 | mTouchHelper.removeCover(direction); 432 | } 433 | } 434 | 435 | private class InnerDataObserver extends CardDataObserver { 436 | 437 | @Override 438 | public void onDataSetChanged() { 439 | super.onDataSetChanged(); 440 | if (mTouchHelper != null && !mTouchHelper.isCoverIdle()) { 441 | mPendingTask = new Runnable() { 442 | @Override 443 | public void run() { 444 | initChildren(); 445 | } 446 | }; 447 | } else { 448 | initChildren(); 449 | } 450 | } 451 | 452 | @Override 453 | public void onItemInserted(int position) { 454 | super.onItemInserted(position); 455 | 456 | } 457 | 458 | @Override 459 | public void onItemRemoved(int position) { 460 | View toRemove = getChildAt(position); 461 | removeViewInLayout(toRemove); 462 | requestLayout(); 463 | } 464 | } 465 | 466 | @Override 467 | public boolean dispatchTouchEvent(MotionEvent ev) { 468 | return super.dispatchTouchEvent(ev); 469 | } 470 | 471 | @Override 472 | public boolean onInterceptTouchEvent(MotionEvent ev) { 473 | if (mTouchHelper == null) { 474 | mTouchHelper = new SwipeTouchHelper(this); 475 | } 476 | return mTouchHelper.onInterceptTouchEvent(ev); 477 | } 478 | 479 | @Override 480 | public boolean onTouchEvent(MotionEvent ev) { 481 | return mTouchHelper.onTouchEvent(ev); 482 | } 483 | 484 | public static class LayoutParams extends FrameLayout.LayoutParams { 485 | 486 | public int swipeDirection = SWIPE_ALL; 487 | public int dismissDirection = SWIPE_ALL; 488 | public boolean fastDismissAllowed = true; 489 | public float maxRotation; 490 | 491 | 492 | public LayoutParams(int width, int height, int gravity) { 493 | super(width, height); 494 | this.gravity = gravity; 495 | } 496 | 497 | 498 | public LayoutParams swipeDirection(int direction) { 499 | this.swipeDirection = direction; 500 | return this; 501 | } 502 | 503 | public LayoutParams dismissDirection(int direction) { 504 | this.dismissDirection = direction; 505 | return this; 506 | } 507 | 508 | public LayoutParams fastDismissAllowed(boolean allowed) { 509 | this.fastDismissAllowed = allowed; 510 | return this; 511 | } 512 | 513 | public LayoutParams maxRotation(float maxRotation) { 514 | this.maxRotation = maxRotation; 515 | return this; 516 | } 517 | } 518 | 519 | public static abstract class Adapter { 520 | 521 | private final CardDataObservable mObservable = new CardDataObservable(); 522 | 523 | public final void registerDataObserver(CardDataObserver observer) { 524 | mObservable.registerObserver(observer); 525 | } 526 | 527 | public final void unregisterDataObserver(CardDataObserver observer) { 528 | mObservable.unregisterObserver(observer); 529 | } 530 | 531 | public abstract int getCount(); 532 | 533 | public abstract View getView(int position, View convertView, ViewGroup parent); 534 | 535 | public final void notifyDataSetChanged() { 536 | mObservable.notifyDataSetChanged(); 537 | } 538 | 539 | public final void notifyItemInserted(int position) { 540 | mObservable.notifyItemInserted(position); 541 | } 542 | 543 | public final void notifyItemRemoved(int position) { 544 | mObservable.notifyItemRemoved(position); 545 | } 546 | 547 | public int getSwipeDirection(int position) { 548 | return SWIPE_ALL; 549 | } 550 | 551 | public int getDismissDirection(int position) { 552 | return SWIPE_ALL; 553 | } 554 | 555 | public boolean isFastDismissAllowed(int position) { 556 | return true; 557 | } 558 | 559 | public int getMaxRotation(int position) { 560 | return 0; 561 | } 562 | } 563 | 564 | public static abstract class CardDataObserver { 565 | 566 | public void onDataSetChanged() { 567 | 568 | } 569 | 570 | public void onItemInserted(int position) { 571 | 572 | } 573 | 574 | public void onItemRemoved(int position) { 575 | 576 | } 577 | } 578 | 579 | static class CardDataObservable extends Observable { 580 | 581 | public void notifyDataSetChanged() { 582 | for (int i = mObservers.size() - 1; i >= 0; i--) { 583 | mObservers.get(i).onDataSetChanged(); 584 | } 585 | } 586 | 587 | public void notifyItemInserted(int position) { 588 | for (int i = mObservers.size() - 1; i >= 0; i--) { 589 | mObservers.get(i).onItemInserted(position); 590 | } 591 | } 592 | 593 | public void notifyItemRemoved(int position) { 594 | for (int i = mObservers.size() - 1; i >= 0; i--) { 595 | mObservers.get(i).onItemRemoved(position); 596 | } 597 | } 598 | } 599 | 600 | private static void log(String tag, String msg) { 601 | if (StackCardsView.DEBUG) { 602 | Log.d(tag, msg); 603 | } 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/SwipeTouchHelper.java: -------------------------------------------------------------------------------- 1 | package com.beyondsw.lib.widget; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ObjectAnimator; 6 | import android.animation.PropertyValuesHolder; 7 | import android.animation.ValueAnimator; 8 | import android.content.Context; 9 | import android.graphics.Rect; 10 | import android.util.Log; 11 | import android.view.MotionEvent; 12 | import android.view.VelocityTracker; 13 | import android.view.View; 14 | import android.view.ViewConfiguration; 15 | import android.view.ViewParent; 16 | import android.view.animation.Interpolator; 17 | import android.view.animation.LinearInterpolator; 18 | 19 | import com.beyondsw.lib.widget.rebound.SimpleSpringListener; 20 | import com.beyondsw.lib.widget.rebound.Spring; 21 | import com.beyondsw.lib.widget.rebound.SpringConfig; 22 | import com.beyondsw.lib.widget.rebound.SpringListener; 23 | import com.beyondsw.lib.widget.rebound.SpringSystem; 24 | 25 | 26 | /** 27 | * Created by wensefu on 17-2-12. 28 | */ 29 | public class SwipeTouchHelper implements ISwipeTouchHelper { 30 | 31 | private static final String TAG = "StackCardsView-touch"; 32 | 33 | private static final float SLOPE = 1.732f; 34 | private StackCardsView mSwipeView; 35 | private float mCurProgress; 36 | private ValueAnimator mSmoothUpdater; 37 | private ManualDisappearUpdateListener mManualUpdateListener; 38 | private float mLastX; 39 | private float mLastY; 40 | private float mInitDownX; 41 | private float mInitDownY; 42 | private int mDragSlop; 43 | private int mMaxVelocity; 44 | private float mMinVelocity; 45 | private float mMinFastDisappearVelocity; 46 | private VelocityTracker mVelocityTracker; 47 | private static final int INVALID_POINTER = -1; 48 | private int mActivePointerId = INVALID_POINTER; 49 | private boolean mOnTouchableChild; 50 | private boolean mIsBeingDragged; 51 | private boolean mIsTouchOn; 52 | private int mDisappearingCnt; 53 | private View mTouchChild; 54 | private float mChildInitX; 55 | private float mChildInitY; 56 | private float mChildInitRotation; 57 | private boolean mInitPropSetted; 58 | private float mAnimStartX; 59 | private float mAnimStartY; 60 | private float mAnimStartRotation; 61 | private SpringSystem mSpringSystem; 62 | private Spring mSpring; 63 | 64 | private static final int MIN_FLING_VELOCITY = 1200; 65 | 66 | public SwipeTouchHelper(StackCardsView view) { 67 | mSwipeView = view; 68 | final Context context = view.getContext(); 69 | final ViewConfiguration configuration = ViewConfiguration.get(context); 70 | mDragSlop = (int) (configuration.getScaledTouchSlop() / mSwipeView.getDragSensitivity()); 71 | mMaxVelocity = configuration.getScaledMaximumFlingVelocity(); 72 | mMinVelocity = configuration.getScaledMinimumFlingVelocity(); 73 | float density = context.getResources().getDisplayMetrics().density; 74 | mMinFastDisappearVelocity = (int) (MIN_FLING_VELOCITY * density); 75 | mSpringSystem = SpringSystem.create(); 76 | updateTouchChild(); 77 | } 78 | 79 | //cp from ViewDragHelper 80 | private static final Interpolator sInterpolator = new Interpolator() { 81 | @Override 82 | public float getInterpolation(float t) { 83 | t -= 1.0f; 84 | return t * t * t * t * t + 1.0f; 85 | } 86 | }; 87 | 88 | private SpringListener mSpringListener = new SimpleSpringListener() { 89 | @Override 90 | public void onSpringUpdate(Spring spring) { 91 | float value = (float) spring.getCurrentValue(); 92 | mTouchChild.setX(mAnimStartX - (mAnimStartX - mChildInitX) * value); 93 | mTouchChild.setY(mAnimStartY - (mAnimStartY - mChildInitY) * value); 94 | mTouchChild.setRotation(mAnimStartRotation - (mAnimStartRotation - mChildInitRotation) * value); 95 | onCoverScrolled(mTouchChild); 96 | } 97 | 98 | @Override 99 | public void onSpringAtRest(Spring spring) { 100 | super.onSpringAtRest(spring); 101 | mSwipeView.onCoverStatusChanged(isCoverIdle()); 102 | } 103 | }; 104 | 105 | @Override 106 | public boolean isCoverIdle() { 107 | boolean springIdle = (mSpring == null || mSpring.isAtRest()); 108 | return springIdle && !mIsTouchOn && (mDisappearingCnt == 0); 109 | } 110 | 111 | @Override 112 | public void onChildChanged() { 113 | mTouchChild = null; 114 | updateTouchChild(); 115 | } 116 | 117 | @Override 118 | public void onChildAppend() { 119 | if (mTouchChild == null) { 120 | updateTouchChild(); 121 | } 122 | } 123 | 124 | @Override 125 | public void removeCover(int direction) { 126 | doManualDisappear(direction); 127 | } 128 | 129 | private void updateTouchChild() { 130 | int index = mSwipeView.indexOfChild(mTouchChild); 131 | int nextIndex = index + 1; 132 | mTouchChild = nextIndex < mSwipeView.getChildCount() ? mSwipeView.getChildAt(nextIndex) : null; 133 | if (mTouchChild != null) { 134 | if (!mInitPropSetted) { 135 | mChildInitX = mTouchChild.getX(); 136 | mChildInitY = mTouchChild.getY(); 137 | mChildInitRotation = mTouchChild.getRotation(); 138 | mInitPropSetted = true; 139 | } 140 | } 141 | } 142 | 143 | private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { 144 | final ViewParent parent = mSwipeView.getParent(); 145 | if (parent != null) { 146 | parent.requestDisallowInterceptTouchEvent(disallowIntercept); 147 | } 148 | } 149 | 150 | private static boolean isTouchOnView(View view, float x, float y) { 151 | if (view == null) { 152 | return false; 153 | } 154 | Rect rect = new Rect(); 155 | view.getHitRect(rect); 156 | return rect.contains((int) x, (int) y); 157 | } 158 | 159 | private boolean isDirectionAllowDismiss() { 160 | final StackCardsView.LayoutParams lp = (StackCardsView.LayoutParams) mTouchChild.getLayoutParams(); 161 | final int direction = lp.dismissDirection; 162 | if (direction == StackCardsView.SWIPE_ALL) { 163 | return true; 164 | } else if (direction == 0) { 165 | return false; 166 | } 167 | float dx = mTouchChild.getX() - mChildInitX; 168 | float dy = mTouchChild.getY() - mChildInitY; 169 | //斜率小于SLOPE时,认为是水平滑动 170 | if (Math.abs(dx) * SLOPE > Math.abs(dy)) { 171 | if (dx > 0) { 172 | return (direction & StackCardsView.SWIPE_RIGHT) != 0; 173 | } else { 174 | return (direction & StackCardsView.SWIPE_LEFT) != 0; 175 | } 176 | } else { 177 | if (dy > 0) { 178 | return (direction & StackCardsView.SWIPE_DOWN) != 0; 179 | } else { 180 | return (direction & StackCardsView.SWIPE_UP) != 0; 181 | } 182 | } 183 | } 184 | 185 | private boolean isDistanceAllowDismiss() { 186 | if (mTouchChild == null) { 187 | return false; 188 | } 189 | float dx = mTouchChild.getX() - mChildInitX; 190 | float dy = mTouchChild.getY() - mChildInitY; 191 | double distance = Math.sqrt(dx * dx + dy * dy); 192 | float dismiss_distance = mSwipeView.getDismissDistance(); 193 | return distance >= dismiss_distance; 194 | } 195 | 196 | private boolean isVDirectionAllowDismiss(float vx, float vy) { 197 | final StackCardsView.LayoutParams lp = (StackCardsView.LayoutParams) mTouchChild.getLayoutParams(); 198 | final int direction = lp.dismissDirection; 199 | if (direction == StackCardsView.SWIPE_ALL) { 200 | return true; 201 | } else if (direction == 0) { 202 | return false; 203 | } 204 | //斜率小于SLOPE时,认为是水平滑动 205 | if (Math.abs(vx) * SLOPE > Math.abs(vy)) { 206 | if (vy > 0) { 207 | return (direction & StackCardsView.SWIPE_RIGHT) != 0; 208 | } else { 209 | return (direction & StackCardsView.SWIPE_LEFT) != 0; 210 | } 211 | } else { 212 | if (vy > 0) { 213 | return (direction & StackCardsView.SWIPE_DOWN) != 0; 214 | } else { 215 | return (direction & StackCardsView.SWIPE_UP) != 0; 216 | } 217 | } 218 | } 219 | 220 | private boolean canDrag(float dx, float dy) { 221 | final StackCardsView.LayoutParams lp = (StackCardsView.LayoutParams) mTouchChild.getLayoutParams(); 222 | final int direction = lp.swipeDirection; 223 | if (direction == StackCardsView.SWIPE_ALL) { 224 | return true; 225 | } else if (direction == 0) { 226 | return false; 227 | } 228 | //斜率小于SLOPE时,认为是水平滑动 229 | if (Math.abs(dx) * SLOPE > Math.abs(dy)) { 230 | if (dx > 0) { 231 | return (direction & StackCardsView.SWIPE_RIGHT) != 0; 232 | } else { 233 | return (direction & StackCardsView.SWIPE_LEFT) != 0; 234 | } 235 | } else { 236 | if (dy > 0) { 237 | return (direction & StackCardsView.SWIPE_DOWN) != 0; 238 | } else { 239 | return (direction & StackCardsView.SWIPE_UP) != 0; 240 | } 241 | } 242 | } 243 | 244 | private void performDrag(float dx, float dy) { 245 | if (mTouchChild == null) { 246 | return; 247 | } 248 | if (mSmoothUpdater != null && mSmoothUpdater.isRunning()) { 249 | mSmoothUpdater.end(); 250 | } 251 | if (mManualUpdateListener != null) { 252 | mManualUpdateListener.end(); 253 | mManualUpdateListener = null; 254 | } 255 | mTouchChild.setX(mTouchChild.getX() + dx); 256 | mTouchChild.setY(mTouchChild.getY() + dy); 257 | final StackCardsView.LayoutParams lp = (StackCardsView.LayoutParams) mTouchChild.getLayoutParams(); 258 | final float maxRotation = lp.maxRotation; 259 | float rotation = maxRotation * (mTouchChild.getX() - mChildInitX) / mSwipeView.getDismissDistance(); 260 | if (rotation > maxRotation) { 261 | rotation = maxRotation; 262 | } else if (rotation < -maxRotation) { 263 | rotation = -maxRotation; 264 | } 265 | mTouchChild.setRotation(rotation); 266 | onCoverScrolled(mTouchChild); 267 | } 268 | 269 | private void animateToInitPos() { 270 | if (mTouchChild != null) { 271 | if (mSpring != null) { 272 | mSpring.removeAllListeners(); 273 | } 274 | mAnimStartX = mTouchChild.getX(); 275 | mAnimStartY = mTouchChild.getY(); 276 | float dx = mAnimStartX - mChildInitX; 277 | float dy = mAnimStartY - mChildInitY; 278 | if (Float.compare(dx, 0) == 0 && Float.compare(dy, 0) == 0) { 279 | return; 280 | } 281 | mAnimStartRotation = mTouchChild.getRotation(); 282 | mSpring = mSpringSystem.createSpring(); 283 | mSpring.setSpringConfig(SpringConfig.fromOrigamiTensionAndFriction(40, 5)); 284 | mSpring.addListener(mSpringListener); 285 | mSpring.setEndValue(1); 286 | mSwipeView.onCoverStatusChanged(false); 287 | } 288 | } 289 | 290 | private void doManualDisappear(final int direction) { 291 | if (mTouchChild == null) { 292 | return; 293 | } 294 | if (mSmoothUpdater != null && mSmoothUpdater.isRunning()) { 295 | mSmoothUpdater.end(); 296 | } 297 | if (mManualUpdateListener != null) { 298 | mManualUpdateListener.end(); 299 | mManualUpdateListener = null; 300 | } 301 | mDisappearingCnt++; 302 | final View disappearView = mTouchChild; 303 | mSwipeView.tryAppendChild(); 304 | updateTouchChild(); 305 | Rect rect = new Rect(); 306 | disappearView.getHitRect(rect); 307 | String property = null; 308 | float target = 0; 309 | long duration = 0; 310 | float delta; 311 | if (direction == StackCardsView.SWIPE_RIGHT || direction == StackCardsView.SWIPE_LEFT) { 312 | final int pWidth = mSwipeView.getWidth(); 313 | final float curX = disappearView.getX(); 314 | property = "x"; 315 | if (direction == StackCardsView.SWIPE_RIGHT) { 316 | delta = Math.max(pWidth - rect.left, 0); 317 | } else { 318 | delta = -Math.max(rect.right, 0); 319 | } 320 | target = curX + delta; 321 | duration = computeSettleDuration((int) delta, 0, 0, 0); 322 | } else if (direction == StackCardsView.SWIPE_DOWN || direction == StackCardsView.SWIPE_UP) { 323 | final int pHeight = mSwipeView.getHeight(); 324 | final float curY = disappearView.getY(); 325 | property = "y"; 326 | if (direction == StackCardsView.SWIPE_DOWN) { 327 | delta = Math.max(pHeight - rect.top, 0); 328 | } else { 329 | delta = -Math.max(rect.bottom, 0); 330 | } 331 | target = curY + delta; 332 | duration = computeSettleDuration(0, (int) delta, 0, 0); 333 | } 334 | if (property != null) { 335 | ObjectAnimator animator = ObjectAnimator.ofFloat(disappearView, property, target).setDuration(duration); 336 | animator.setInterpolator(sInterpolator); 337 | animator.addListener(new AnimatorListenerAdapter() { 338 | 339 | @Override 340 | public void onAnimationEnd(Animator animation) { 341 | mDisappearingCnt--; 342 | mSwipeView.onCardDismissed(direction); 343 | mSwipeView.onCoverStatusChanged(isCoverIdle()); 344 | } 345 | 346 | @Override 347 | public void onAnimationStart(Animator animation) { 348 | super.onAnimationStart(animation); 349 | mSwipeView.onCoverStatusChanged(false); 350 | } 351 | }); 352 | mManualUpdateListener = new ManualDisappearUpdateListener(disappearView); 353 | animator.addUpdateListener(mManualUpdateListener); 354 | animator.start(); 355 | } 356 | } 357 | 358 | private class ManualDisappearUpdateListener implements ValueAnimator.AnimatorUpdateListener { 359 | 360 | View disappearView; 361 | boolean isCanceled; 362 | 363 | ManualDisappearUpdateListener(View disappearView) { 364 | this.disappearView = disappearView; 365 | } 366 | 367 | @Override 368 | public void onAnimationUpdate(ValueAnimator animation) { 369 | ScrollInfo info = calcScrollInfo(disappearView); 370 | if (!isCanceled) { 371 | mSwipeView.updateChildrenProgress(info.progress, disappearView); 372 | } 373 | mSwipeView.onCoverScrolled(disappearView, info.progress, info.direction); 374 | } 375 | 376 | void end() { 377 | isCanceled = true; 378 | mSwipeView.updateChildrenProgress(1, disappearView); 379 | } 380 | } 381 | 382 | private void doSlowDisappear() { 383 | if (mTouchChild == null) { 384 | return; 385 | } 386 | mDisappearingCnt++; 387 | final View disappearView = mTouchChild; 388 | final float initX = mChildInitX; 389 | final float initY = mChildInitY; 390 | mSwipeView.tryAppendChild(); 391 | updateTouchChild(); 392 | final float curX = disappearView.getX(); 393 | final float curY = disappearView.getY(); 394 | final float dx = curX - initX; 395 | final float dy = curY - initY; 396 | Rect rect = new Rect(); 397 | disappearView.getHitRect(rect); 398 | String property; 399 | float target; 400 | int dir; 401 | long duration; 402 | float delta; 403 | if (Math.abs(dx) * SLOPE > Math.abs(dy)) { 404 | final int pWidth = mSwipeView.getWidth(); 405 | property = "x"; 406 | if (dx > 0) { 407 | delta = Math.max(pWidth - rect.left, 0); 408 | dir = StackCardsView.SWIPE_RIGHT; 409 | } else { 410 | delta = -Math.max(rect.right, 0); 411 | dir = StackCardsView.SWIPE_LEFT; 412 | } 413 | target = curX + delta; 414 | duration = computeSettleDuration((int) delta, 0, 0, 0); 415 | } else { 416 | final int pHeight = mSwipeView.getHeight(); 417 | property = "y"; 418 | if (dy > 0) { 419 | delta = Math.max(pHeight - rect.top, 0); 420 | dir = StackCardsView.SWIPE_DOWN; 421 | } else { 422 | delta = -Math.max(rect.bottom, 0); 423 | dir = StackCardsView.SWIPE_UP; 424 | } 425 | target = curY + delta; 426 | duration = computeSettleDuration(0, (int) delta, 0, 0); 427 | } 428 | final int direction = dir; 429 | ObjectAnimator animator = ObjectAnimator.ofFloat(disappearView, property, target).setDuration(duration); 430 | animator.setInterpolator(sInterpolator); 431 | animator.addListener(new AnimatorListenerAdapter() { 432 | 433 | @Override 434 | public void onAnimationEnd(Animator animation) { 435 | mDisappearingCnt--; 436 | mSwipeView.onCardDismissed(direction); 437 | mSwipeView.onCoverStatusChanged(isCoverIdle()); 438 | } 439 | 440 | @Override 441 | public void onAnimationStart(Animator animation) { 442 | super.onAnimationStart(animation); 443 | mSwipeView.onCoverStatusChanged(false); 444 | } 445 | }); 446 | animator.start(); 447 | } 448 | 449 | private int[] calcScrollDistance(View view, float vx, float vy, float dx, float dy) { 450 | int[] result = new int[2]; 451 | float edgeDeltaX = 0; 452 | float edgeDeltaY = 0; 453 | Rect rect = new Rect(); 454 | view.getHitRect(rect); 455 | if (vx > 0) { 456 | edgeDeltaX = Math.max(0, mSwipeView.getWidth() - rect.left); 457 | } else if (vx < 0) { 458 | edgeDeltaX = Math.max(0, rect.right); 459 | } 460 | if (vy > 0) { 461 | edgeDeltaY = Math.max(0, mSwipeView.getHeight() - rect.top); 462 | } else if (vy < 0) { 463 | edgeDeltaY = Math.max(0, rect.bottom); 464 | } 465 | float scrollDx; 466 | float scrollDy; 467 | if (edgeDeltaX * Math.abs(dy) >= edgeDeltaY * Math.abs(dx)) { 468 | scrollDy = vy > 0 ? edgeDeltaY : -edgeDeltaY; 469 | float value = Math.abs(scrollDy * dx / dy); 470 | scrollDx = vx > 0 ? value : -value; 471 | } else { 472 | scrollDx = vx > 0 ? edgeDeltaX : -edgeDeltaX; 473 | float value = Math.abs(scrollDx * dy / dx); 474 | scrollDy = vy > 0 ? value : -value; 475 | } 476 | result[0] = (int) scrollDx; 477 | result[1] = (int) scrollDy; 478 | return result; 479 | } 480 | 481 | private void smoothUpdatePosition(final View scrollingView) { 482 | long duration = 160 + (int) (100 * (1 - mCurProgress)); 483 | mSmoothUpdater = ValueAnimator.ofFloat(mCurProgress, 1).setDuration(duration); 484 | mSmoothUpdater.setInterpolator(new LinearInterpolator()); 485 | mSmoothUpdater.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 486 | @Override 487 | public void onAnimationUpdate(ValueAnimator animation) { 488 | mSwipeView.updateChildrenProgress((float) animation.getAnimatedValue(), scrollingView); 489 | } 490 | }); 491 | mSmoothUpdater.start(); 492 | } 493 | 494 | private boolean doFastDisappear(float vx, float vy) { 495 | if (mTouchChild == null) { 496 | return false; 497 | } 498 | if (vx * vx + vy * vy < mMinFastDisappearVelocity * mMinFastDisappearVelocity) { 499 | return false; 500 | } 501 | if (!isVDirectionAllowDismiss(vx, vx)) { 502 | return false; 503 | } 504 | log(TAG, "doFastDisappear"); 505 | final View disappearView = mTouchChild; 506 | final float initX = mChildInitX; 507 | final float initY = mChildInitY; 508 | 509 | mDisappearingCnt++; 510 | 511 | mSwipeView.tryAppendChild(); 512 | updateTouchChild(); 513 | if (mManualUpdateListener != null) { 514 | mManualUpdateListener.end(); 515 | mManualUpdateListener = null; 516 | } 517 | smoothUpdatePosition(disappearView); 518 | 519 | float dx = disappearView.getX() - initX; 520 | float dy = disappearView.getY() - initY; 521 | int[] fdxArray = calcScrollDistance(disappearView, vx, vy, dx, dy); 522 | float animDx = fdxArray[0]; 523 | float animDy = fdxArray[1]; 524 | long duration = computeSettleDuration((int) animDx, (int) animDy, (int) vx, (int) vy); 525 | 526 | PropertyValuesHolder xp = PropertyValuesHolder.ofFloat("x", disappearView.getX() + animDx); 527 | PropertyValuesHolder yp = PropertyValuesHolder.ofFloat("y", disappearView.getY() + animDy); 528 | ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(disappearView, xp, yp).setDuration(duration); 529 | animator.setInterpolator(sInterpolator); 530 | animator.addListener(new AnimatorListenerAdapter() { 531 | @Override 532 | public void onAnimationEnd(Animator animation) { 533 | mDisappearingCnt--; 534 | mSwipeView.onCardDismissed(0); //// // FIXME 535 | mSwipeView.onCoverStatusChanged(isCoverIdle()); 536 | } 537 | 538 | @Override 539 | public void onAnimationStart(Animator animation) { 540 | mSwipeView.onCoverStatusChanged(false); 541 | } 542 | }); 543 | animator.start(); 544 | return true; 545 | } 546 | 547 | private int clampMag(int value, int absMin, int absMax) { 548 | final int absValue = Math.abs(value); 549 | if (absValue < absMin) return 0; 550 | if (absValue > absMax) return value > 0 ? absMax : -absMax; 551 | return value; 552 | } 553 | 554 | //cp from ViewDragHelper 555 | private int computeSettleDuration(int dx, int dy, int xvel, int yvel) { 556 | xvel = clampMag(xvel, (int) mMinVelocity, mMaxVelocity); 557 | yvel = clampMag(yvel, (int) mMinVelocity, mMaxVelocity); 558 | final int absDx = Math.abs(dx); 559 | final int absDy = Math.abs(dy); 560 | final int absXVel = Math.abs(xvel); 561 | final int absYVel = Math.abs(yvel); 562 | final int addedVel = absXVel + absYVel; 563 | final int addedDistance = absDx + absDy; 564 | 565 | final float xweight = xvel != 0 ? (float) absXVel / addedVel : 566 | (float) absDx / addedDistance; 567 | final float yweight = yvel != 0 ? (float) absYVel / addedVel : 568 | (float) absDy / addedDistance; 569 | 570 | int xduration = computeAxisDuration(dx, xvel, 256); 571 | int yduration = computeAxisDuration(dy, yvel, 256); 572 | return (int) (xduration * xweight + yduration * yweight); 573 | } 574 | 575 | //cp from ViewDragHelper 576 | private int computeAxisDuration(int delta, int velocity, int motionRange) { 577 | if (delta == 0) { 578 | return 0; 579 | } 580 | 581 | final int width = mSwipeView.getWidth(); 582 | final int halfWidth = width / 2; 583 | final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width); 584 | final float distance = halfWidth + halfWidth 585 | * distanceInfluenceForSnapDuration(distanceRatio); 586 | 587 | int duration; 588 | velocity = Math.abs(velocity); 589 | if (velocity > 0) { 590 | duration = 4 * Math.round(2400 * Math.abs(distance / velocity)); 591 | } else { 592 | final float range = (float) Math.abs(delta) / motionRange; 593 | duration = (int) ((range + 1) * 256); 594 | } 595 | return Math.min(duration, 600); 596 | } 597 | 598 | //cp from ViewDragHelper 599 | private float distanceInfluenceForSnapDuration(float f) { 600 | f -= 0.5f; // center the values about 0. 601 | f *= 0.3f * Math.PI / 2.0f; 602 | return (float) Math.sin(f); 603 | } 604 | 605 | private class ScrollInfo { 606 | float progress; 607 | int direction; 608 | } 609 | 610 | private ScrollInfo calcScrollInfo(View movingView) { 611 | ScrollInfo result = new ScrollInfo(); 612 | float dx = movingView.getX() - mChildInitX; 613 | float dy = movingView.getY() - mChildInitY; 614 | int direction; 615 | if (Float.compare(dx, 0) == 0 && Float.compare(dy, 0) == 0) { 616 | direction = StackCardsView.SWIPE_NONE; 617 | } else { 618 | if (Math.abs(dx) * SLOPE > Math.abs(dy)) { 619 | direction = dx > 0 ? StackCardsView.SWIPE_RIGHT : StackCardsView.SWIPE_LEFT; 620 | } else { 621 | direction = dy > 0 ? StackCardsView.SWIPE_DOWN : StackCardsView.SWIPE_UP; 622 | } 623 | } 624 | log(TAG, "calcScrollInfo,direction=" + direction + ",dx=" + dx + ",dy=" + dy); 625 | result.direction = direction; 626 | double distance = Math.sqrt(dx * dx + dy * dy); 627 | float dismiss_distance = mSwipeView.getDismissDistance(); 628 | if (distance >= dismiss_distance) { 629 | result.progress = 1; 630 | } else { 631 | result.progress = (float) distance / dismiss_distance; 632 | } 633 | return result; 634 | } 635 | 636 | private void onCoverScrolled(View movingView) { 637 | ScrollInfo info = calcScrollInfo(movingView); 638 | final float progress = info.progress; 639 | mCurProgress = progress; 640 | mSwipeView.onCoverScrolled(movingView, progress, info.direction); 641 | mSwipeView.updateChildrenProgress(progress, movingView); 642 | } 643 | 644 | private void cancelSpringIfNeeded() { 645 | if (mSpring != null && !mSpring.isAtRest()) { 646 | mSpring.setAtRest(); 647 | mSpring.removeAllListeners(); 648 | } 649 | } 650 | 651 | private void clearVelocityTracker() { 652 | if (mVelocityTracker != null) { 653 | mVelocityTracker.recycle(); 654 | mVelocityTracker = null; 655 | } 656 | } 657 | 658 | private void resetTouch() { 659 | mIsTouchOn = false; 660 | mIsBeingDragged = false; 661 | mActivePointerId = INVALID_POINTER; 662 | } 663 | 664 | private void onTouchRelease() { 665 | final StackCardsView.LayoutParams lp = (StackCardsView.LayoutParams) mTouchChild.getLayoutParams(); 666 | if (lp.fastDismissAllowed) { 667 | final VelocityTracker velocityTracker2 = mVelocityTracker; 668 | velocityTracker2.computeCurrentVelocity(1000, mMaxVelocity); 669 | float xv = velocityTracker2.getXVelocity(mActivePointerId); 670 | float yv = velocityTracker2.getYVelocity(mActivePointerId); 671 | if (doFastDisappear(xv, yv)) { 672 | resetTouch(); 673 | return; 674 | } 675 | } 676 | if (isDistanceAllowDismiss() && isDirectionAllowDismiss()) { 677 | doSlowDisappear(); 678 | } else { 679 | animateToInitPos(); 680 | } 681 | resetTouch(); 682 | mSwipeView.onCoverStatusChanged(isCoverIdle()); 683 | } 684 | 685 | @Override 686 | public boolean onInterceptTouchEvent(MotionEvent ev) { 687 | if (mTouchChild == null) { 688 | logw(TAG, "onInterceptTouchEvent,mTouchChild == null"); 689 | return false; 690 | } 691 | final View touchChild = mTouchChild; 692 | final int action = ev.getAction() & MotionEvent.ACTION_MASK; 693 | if (action == MotionEvent.ACTION_DOWN) { 694 | clearVelocityTracker(); 695 | } 696 | if (mVelocityTracker == null) { 697 | mVelocityTracker = VelocityTracker.obtain(); 698 | } 699 | if (mIsBeingDragged && action != MotionEvent.ACTION_DOWN) { 700 | return true; 701 | } 702 | switch (action) { 703 | case MotionEvent.ACTION_DOWN: { 704 | float x = ev.getX(); 705 | float y = ev.getY(); 706 | if (!(mOnTouchableChild = isTouchOnView(touchChild, x, y))) { 707 | return false; 708 | } 709 | mActivePointerId = ev.getPointerId(0); 710 | mIsTouchOn = true; 711 | mSwipeView.onCoverStatusChanged(false); 712 | requestParentDisallowInterceptTouchEvent(true); 713 | mInitDownX = mLastX = x; 714 | mInitDownY = mLastY = y; 715 | break; 716 | } 717 | case MotionEvent.ACTION_MOVE: { 718 | if (mActivePointerId == INVALID_POINTER) { 719 | break; 720 | } 721 | int pointerIndex = ev.findPointerIndex(mActivePointerId); 722 | float x = ev.getX(pointerIndex); 723 | float y = ev.getY(pointerIndex); 724 | float dx = x - mInitDownX; 725 | float dy = y - mInitDownY; 726 | mLastX = x; 727 | mLastY = y; 728 | if ((Math.abs(dx) > mDragSlop || (Math.abs(dy) > mDragSlop)) && canDrag(dx, dy)) { 729 | cancelSpringIfNeeded(); 730 | mIsBeingDragged = true; 731 | } 732 | break; 733 | } 734 | case MotionEvent.ACTION_POINTER_DOWN: 735 | log(TAG, "onInterceptTouchEvent ACTION_POINTER_DOWN"); 736 | break; 737 | case MotionEvent.ACTION_POINTER_UP: 738 | log(TAG, "onInterceptTouchEvent ACTION_POINTER_UP"); 739 | break; 740 | case MotionEvent.ACTION_UP: 741 | case MotionEvent.ACTION_CANCEL: { 742 | log(TAG, "onInterceptTouchEvent ACTION_UP,mActivePointerId=" + mActivePointerId); 743 | if (mActivePointerId == INVALID_POINTER) { 744 | break; 745 | } 746 | resetTouch(); 747 | mSwipeView.onCoverStatusChanged(isCoverIdle()); 748 | break; 749 | } 750 | } 751 | log(TAG, "onInterceptTouchEvent action=" + action + ",mIsBeingDragged=" + mIsBeingDragged); 752 | return mIsBeingDragged; 753 | } 754 | 755 | @Override 756 | public boolean onTouchEvent(MotionEvent ev) { 757 | final int action = ev.getAction() & MotionEvent.ACTION_MASK; 758 | if (mTouchChild == null) { 759 | return false; 760 | } 761 | mVelocityTracker.addMovement(ev); 762 | switch (action) { 763 | case MotionEvent.ACTION_DOWN: { 764 | log(TAG, "onTouchEvent ACTION_DOWN"); 765 | if (!mOnTouchableChild) { 766 | return false; 767 | } 768 | break; 769 | } 770 | case MotionEvent.ACTION_MOVE: { 771 | //子view未消费down事件时,mIsBeingDragged为false 772 | log(TAG, "onTouchEvent ACTION_MOVE,mActivePointerId=" + mActivePointerId); 773 | if (mActivePointerId == INVALID_POINTER) { 774 | log(TAG, "onTouchEvent ACTION_MOVE,INVALID_POINTER"); 775 | break; 776 | } 777 | int pointerIndex = ev.findPointerIndex(mActivePointerId); 778 | float x = ev.getX(pointerIndex); 779 | float y = ev.getY(pointerIndex); 780 | if (!mIsBeingDragged) { 781 | cancelSpringIfNeeded(); 782 | float dx = x - mInitDownX; 783 | float dy = y - mInitDownY; 784 | if ((Math.abs(dx) <= mDragSlop && (Math.abs(dy) <= mDragSlop)) || !canDrag(dx, dy)) { 785 | mLastX = x; 786 | mLastY = y; 787 | return false; 788 | } 789 | mIsBeingDragged = true; 790 | } 791 | performDrag(x - mLastX, y - mLastY); 792 | mLastX = x; 793 | mLastY = y; 794 | break; 795 | } 796 | case MotionEvent.ACTION_POINTER_DOWN: 797 | log(TAG, "onTouchEvent ACTION_POINTER_DOWN"); 798 | break; 799 | case MotionEvent.ACTION_CANCEL: 800 | case MotionEvent.ACTION_UP: { 801 | log(TAG, "onTouchEvent ACTION_UP,mActivePointerId=" + mActivePointerId); 802 | if (mActivePointerId == INVALID_POINTER) { 803 | break; 804 | } 805 | onTouchRelease(); 806 | break; 807 | } 808 | case MotionEvent.ACTION_POINTER_UP: { 809 | log(TAG, "onTouchEvent ACTION_POINTER_UP,mActivePointerId=" + mActivePointerId); 810 | int activePointerIndex = ev.findPointerIndex(mActivePointerId); 811 | if (activePointerIndex == ev.getActionIndex()) { 812 | onTouchRelease(); 813 | } 814 | break; 815 | } 816 | } 817 | return true; 818 | } 819 | 820 | private static void log(String tag, String msg) { 821 | if (StackCardsView.DEBUG) { 822 | Log.d(tag, msg); 823 | } 824 | } 825 | 826 | private static void logw(String tag, String msg) { 827 | if (StackCardsView.DEBUG) { 828 | Log.w(tag, msg); 829 | } 830 | } 831 | } 832 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/AndroidSpringLooperFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.beyondsw.lib.widget.rebound; 12 | 13 | import android.annotation.TargetApi; 14 | import android.os.Build; 15 | import android.os.Handler; 16 | import android.os.SystemClock; 17 | import android.view.Choreographer; 18 | 19 | /** 20 | * Android version of the spring looper that uses the most appropriate frame callback mechanism 21 | * available. It uses Android's {@link Choreographer} when available, otherwise it uses a 22 | * {@link Handler}. 23 | */ 24 | abstract class AndroidSpringLooperFactory { 25 | 26 | /** 27 | * Create an Android {@link com.facebook.rebound.SpringLooper} for the detected Android platform. 28 | * @return a SpringLooper 29 | */ 30 | public static SpringLooper createSpringLooper() { 31 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { 32 | return ChoreographerAndroidSpringLooper.create(); 33 | } else { 34 | return LegacyAndroidSpringLooper.create(); 35 | } 36 | } 37 | 38 | /** 39 | * The base implementation of the Android spring looper, using a {@link Handler} for the 40 | * frame callbacks. 41 | */ 42 | private static class LegacyAndroidSpringLooper extends SpringLooper { 43 | 44 | private final Handler mHandler; 45 | private final Runnable mLooperRunnable; 46 | private boolean mStarted; 47 | private long mLastTime; 48 | 49 | /** 50 | * @return an Android spring looper using a new {@link Handler} instance 51 | */ 52 | public static SpringLooper create() { 53 | return new LegacyAndroidSpringLooper(new Handler()); 54 | } 55 | 56 | public LegacyAndroidSpringLooper(Handler handler) { 57 | mHandler = handler; 58 | mLooperRunnable = new Runnable() { 59 | @Override 60 | public void run() { 61 | if (!mStarted || mSpringSystem == null) { 62 | return; 63 | } 64 | long currentTime = SystemClock.uptimeMillis(); 65 | mSpringSystem.loop(currentTime - mLastTime); 66 | mLastTime = currentTime; 67 | mHandler.post(mLooperRunnable); 68 | } 69 | }; 70 | } 71 | 72 | @Override 73 | public void start() { 74 | if (mStarted) { 75 | return; 76 | } 77 | mStarted = true; 78 | mLastTime = SystemClock.uptimeMillis(); 79 | mHandler.removeCallbacks(mLooperRunnable); 80 | mHandler.post(mLooperRunnable); 81 | } 82 | 83 | @Override 84 | public void stop() { 85 | mStarted = false; 86 | mHandler.removeCallbacks(mLooperRunnable); 87 | } 88 | } 89 | 90 | /** 91 | * The Jelly Bean and up implementation of the spring looper that uses Android's 92 | * {@link Choreographer} instead of a {@link Handler} 93 | */ 94 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 95 | private static class ChoreographerAndroidSpringLooper extends SpringLooper { 96 | 97 | private final Choreographer mChoreographer; 98 | private final Choreographer.FrameCallback mFrameCallback; 99 | private boolean mStarted; 100 | private long mLastTime; 101 | 102 | /** 103 | * @return an Android spring choreographer using the system {@link Choreographer} 104 | */ 105 | public static ChoreographerAndroidSpringLooper create() { 106 | return new ChoreographerAndroidSpringLooper(Choreographer.getInstance()); 107 | } 108 | 109 | public ChoreographerAndroidSpringLooper(Choreographer choreographer) { 110 | mChoreographer = choreographer; 111 | mFrameCallback = new Choreographer.FrameCallback() { 112 | @Override 113 | public void doFrame(long frameTimeNanos) { 114 | if (!mStarted || mSpringSystem == null) { 115 | return; 116 | } 117 | long currentTime = SystemClock.uptimeMillis(); 118 | mSpringSystem.loop(currentTime - mLastTime); 119 | mLastTime = currentTime; 120 | mChoreographer.postFrameCallback(mFrameCallback); 121 | } 122 | }; 123 | } 124 | 125 | @Override 126 | public void start() { 127 | if (mStarted) { 128 | return; 129 | } 130 | mStarted = true; 131 | mLastTime = SystemClock.uptimeMillis(); 132 | mChoreographer.removeFrameCallback(mFrameCallback); 133 | mChoreographer.postFrameCallback(mFrameCallback); 134 | } 135 | 136 | @Override 137 | public void stop() { 138 | mStarted = false; 139 | mChoreographer.removeFrameCallback(mFrameCallback); 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/AnimationQueue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.beyondsw.lib.widget.rebound; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Collection; 14 | import java.util.LinkedList; 15 | import java.util.List; 16 | import java.util.Queue; 17 | 18 | /** 19 | * AnimationQueue provides a way to trigger a delayed stream of animations off of a stream of 20 | * values. Each callback that is added the AnimationQueue will be process the stream delayed by 21 | * the number of animation frames equal to its position in the callback list. This makes it easy 22 | * to build cascading animations. 23 | * 24 | * TODO: Add options for changing the delay after which a callback receives a value from the 25 | * animation queue value stream. 26 | */ 27 | public class AnimationQueue { 28 | 29 | /** 30 | * AnimationQueue.Callback receives the value from the stream that it should use in its onFrame 31 | * method. 32 | */ 33 | public interface Callback { 34 | void onFrame(Double value); 35 | } 36 | 37 | private final ChoreographerCompat mChoreographer; 38 | private final Queue mPendingQueue = new LinkedList(); 39 | private final Queue mAnimationQueue = new LinkedList(); 40 | private final List mCallbacks = new ArrayList(); 41 | private final ArrayList mTempValues = new ArrayList(); 42 | private final ChoreographerCompat.FrameCallback mChoreographerCallback; 43 | private boolean mRunning; 44 | 45 | public AnimationQueue() { 46 | mChoreographer = ChoreographerCompat.getInstance(); 47 | mChoreographerCallback = new ChoreographerCompat.FrameCallback() { 48 | @Override 49 | public void doFrame(long frameTimeNanos) { 50 | onFrame(frameTimeNanos); 51 | } 52 | }; 53 | } 54 | 55 | /* Values */ 56 | 57 | /** 58 | * Add a single value to the pending animation queue. 59 | * @param value the single value to add 60 | */ 61 | public void addValue(Double value) { 62 | mPendingQueue.add(value); 63 | runIfIdle(); 64 | } 65 | 66 | /** 67 | * Add a collection of values to the pending animation value queue 68 | * @param values the collection of values to add 69 | */ 70 | public void addAllValues(Collection values) { 71 | mPendingQueue.addAll(values); 72 | runIfIdle(); 73 | } 74 | 75 | /** 76 | * Clear all pending animation values. 77 | */ 78 | public void clearValues() { 79 | mPendingQueue.clear(); 80 | } 81 | 82 | /* Callbacks */ 83 | 84 | /** 85 | * Add a callback to the AnimationQueue. 86 | * @param callback the callback to add 87 | */ 88 | public void addCallback(Callback callback) { 89 | mCallbacks.add(callback); 90 | } 91 | 92 | /** 93 | * Remove the specified callback from the AnimationQueue. 94 | * @param callback the callback to remove 95 | */ 96 | public void removeCallback(Callback callback) { 97 | mCallbacks.remove(callback); 98 | } 99 | 100 | /** 101 | * Remove any callbacks from the AnimationQueue. 102 | */ 103 | public void clearCallbacks() { 104 | mCallbacks.clear(); 105 | } 106 | 107 | /** 108 | * Start the animation loop if it is not currently running. 109 | */ 110 | private void runIfIdle() { 111 | if (!mRunning) { 112 | mRunning = true; 113 | mChoreographer.postFrameCallback(mChoreographerCallback); 114 | } 115 | } 116 | 117 | /** 118 | * Called every time a new frame is ready to be rendered. 119 | * 120 | * Values are processed FIFO and each callback is given a chance to handle each value when its 121 | * turn comes before a value is poll'd off the AnimationQueue. 122 | * 123 | * @param frameTimeNanos The time in nanoseconds when the frame started being rendered, in the 124 | * nanoTime() timebase. Divide this value by 1000000 to convert it to the 125 | * uptimeMillis() time base. 126 | */ 127 | private void onFrame(long frameTimeNanos) { 128 | Double nextPendingValue = mPendingQueue.poll(); 129 | 130 | int drainingOffset; 131 | if (nextPendingValue != null) { 132 | mAnimationQueue.offer(nextPendingValue); 133 | drainingOffset = 0; 134 | } else { 135 | drainingOffset = Math.max(mCallbacks.size() - mAnimationQueue.size(), 0); 136 | } 137 | 138 | // Copy the values into a temporary ArrayList for processing. 139 | mTempValues.addAll(mAnimationQueue); 140 | for (int i = mTempValues.size() - 1; i > -1; i--) { 141 | Double val = mTempValues.get(i); 142 | int cbIdx = mTempValues.size() - 1 - i + drainingOffset; 143 | if (mCallbacks.size() > cbIdx) { 144 | mCallbacks.get(cbIdx).onFrame(val); 145 | } 146 | } 147 | mTempValues.clear(); 148 | 149 | while (mAnimationQueue.size() + drainingOffset >= mCallbacks.size()) { 150 | mAnimationQueue.poll(); 151 | } 152 | 153 | if (mAnimationQueue.isEmpty() && mPendingQueue.isEmpty()) { 154 | mRunning = false; 155 | } else { 156 | mChoreographer.postFrameCallback(mChoreographerCallback); 157 | } 158 | } 159 | 160 | } 161 | 162 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/BaseSpringSystem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.beyondsw.lib.widget.rebound; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.Collections; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.Set; 20 | import java.util.concurrent.CopyOnWriteArraySet; 21 | 22 | /** 23 | * BaseSpringSystem maintains the set of springs within an Application context. It is responsible for 24 | * Running the spring integration loop and maintains a registry of all the Springs it solves for. 25 | * In addition to listening to physics events on the individual Springs in the system, listeners 26 | * can be added to the BaseSpringSystem itself to provide pre and post integration setup. 27 | */ 28 | public class BaseSpringSystem { 29 | 30 | private final Map mSpringRegistry = new HashMap(); 31 | private final Set mActiveSprings = new CopyOnWriteArraySet(); 32 | private final SpringLooper mSpringLooper; 33 | private final CopyOnWriteArraySet mListeners = new CopyOnWriteArraySet(); 34 | private boolean mIdle = true; 35 | 36 | /** 37 | * create a new BaseSpringSystem 38 | * @param springLooper parameterized springLooper to allow testability of the 39 | * physics loop 40 | */ 41 | public BaseSpringSystem(SpringLooper springLooper) { 42 | if (springLooper == null) { 43 | throw new IllegalArgumentException("springLooper is required"); 44 | } 45 | mSpringLooper = springLooper; 46 | mSpringLooper.setSpringSystem(this); 47 | } 48 | 49 | /** 50 | * check if the system is idle 51 | * @return is the system idle 52 | */ 53 | public boolean getIsIdle() { 54 | return mIdle; 55 | } 56 | 57 | /** 58 | * create a spring with a random uuid for its name. 59 | * @return the spring 60 | */ 61 | public Spring createSpring() { 62 | Spring spring = new Spring(this); 63 | registerSpring(spring); 64 | return spring; 65 | } 66 | 67 | /** 68 | * get a spring by name 69 | * @param id id of the spring to retrieve 70 | * @return Spring with the specified key 71 | */ 72 | public Spring getSpringById(String id) { 73 | if (id == null) { 74 | throw new IllegalArgumentException("id is required"); 75 | } 76 | return mSpringRegistry.get(id); 77 | } 78 | 79 | /** 80 | * return all the springs in the simulator 81 | * @return all the springs 82 | */ 83 | public List getAllSprings() { 84 | Collection collection = mSpringRegistry.values(); 85 | List list; 86 | if (collection instanceof List) { 87 | list = (List)collection; 88 | } else { 89 | list = new ArrayList(collection); 90 | } 91 | return Collections.unmodifiableList(list); 92 | } 93 | 94 | /** 95 | * Registers a Spring to this BaseSpringSystem so it can be iterated if active. 96 | * @param spring the Spring to register 97 | */ 98 | void registerSpring(Spring spring) { 99 | if (spring == null) { 100 | throw new IllegalArgumentException("spring is required"); 101 | } 102 | if (mSpringRegistry.containsKey(spring.getId())) { 103 | throw new IllegalArgumentException("spring is already registered"); } 104 | mSpringRegistry.put(spring.getId(), spring); 105 | } 106 | 107 | /** 108 | * Deregisters a Spring from this BaseSpringSystem, so it won't be iterated anymore. The Spring should 109 | * not be used anymore after doing this. 110 | * 111 | * @param spring the Spring to deregister 112 | */ 113 | void deregisterSpring(Spring spring) { 114 | if (spring == null) { 115 | throw new IllegalArgumentException("spring is required"); 116 | } 117 | mActiveSprings.remove(spring); 118 | mSpringRegistry.remove(spring.getId()); 119 | } 120 | 121 | /** 122 | * update the springs in the system 123 | * @param deltaTime delta since last update in millis 124 | */ 125 | void advance(double deltaTime) { 126 | for (Spring spring : mActiveSprings) { 127 | // advance time in seconds 128 | if (spring.systemShouldAdvance()) { 129 | spring.advance(deltaTime / 1000.0); 130 | } else { 131 | mActiveSprings.remove(spring); 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * loop the system until idle 138 | * @param elapsedMillis elapsed milliseconds 139 | */ 140 | public void loop(double elapsedMillis) { 141 | for (SpringSystemListener listener : mListeners) { 142 | listener.onBeforeIntegrate(this); 143 | } 144 | advance(elapsedMillis); 145 | if (mActiveSprings.isEmpty()) { 146 | mIdle = true; 147 | } 148 | for (SpringSystemListener listener : mListeners) { 149 | listener.onAfterIntegrate(this); 150 | } 151 | if (mIdle) { 152 | mSpringLooper.stop(); 153 | } 154 | } 155 | 156 | /** 157 | * This is used internally by the {@link Spring}s created by this {@link BaseSpringSystem} to notify 158 | * it has reached a state where it needs to be iterated. This will add the spring to the list of 159 | * active springs on this system and start the iteration if the system was idle before this call. 160 | * @param springId the id of the Spring to be activated 161 | */ 162 | void activateSpring(String springId) { 163 | Spring spring = mSpringRegistry.get(springId); 164 | if (spring == null) { 165 | throw new IllegalArgumentException("springId " + springId + " does not reference a registered spring"); 166 | } 167 | mActiveSprings.add(spring); 168 | if (getIsIdle()) { 169 | mIdle = false; 170 | mSpringLooper.start(); 171 | } 172 | } 173 | 174 | /** listeners **/ 175 | 176 | /** 177 | * Add new listener object. 178 | * @param newListener listener 179 | */ 180 | public void addListener(SpringSystemListener newListener) { 181 | if (newListener == null) { 182 | throw new IllegalArgumentException("newListener is required"); 183 | } 184 | mListeners.add(newListener); 185 | } 186 | 187 | /** 188 | * Remove listener object. 189 | * @param listenerToRemove listener 190 | */ 191 | public void removeListener(SpringSystemListener listenerToRemove) { 192 | if (listenerToRemove == null) { 193 | throw new IllegalArgumentException("listenerToRemove is required"); 194 | } 195 | mListeners.remove(listenerToRemove); 196 | } 197 | 198 | /** 199 | * Remove all listeners. 200 | */ 201 | public void removeAllListeners() { 202 | mListeners.clear(); 203 | } 204 | } 205 | 206 | 207 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/BouncyConversion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.beyondsw.lib.widget.rebound; 12 | 13 | /** 14 | * This class converts values from the Quartz Composer Bouncy patch into Bouncy QC tension and 15 | * friction values. 16 | */ 17 | public class BouncyConversion { 18 | 19 | private final double mBouncyTension; 20 | private final double mBouncyFriction; 21 | private final double mSpeed; 22 | private final double mBounciness; 23 | 24 | public BouncyConversion(double speed, double bounciness) { 25 | mSpeed = speed; 26 | mBounciness = bounciness; 27 | double b = normalize(bounciness / 1.7, 0, 20.); 28 | b = project_normal(b, 0.0, 0.8); 29 | double s = normalize(speed / 1.7, 0, 20.); 30 | mBouncyTension = project_normal(s, 0.5, 200); 31 | mBouncyFriction = quadratic_out_interpolation(b, b3_nobounce(mBouncyTension), 0.01); 32 | } 33 | 34 | public double getSpeed() { 35 | return mSpeed; 36 | } 37 | 38 | public double getBounciness() { 39 | return mBounciness; 40 | } 41 | 42 | public double getBouncyTension() { 43 | return mBouncyTension; 44 | } 45 | 46 | public double getBouncyFriction() { 47 | return mBouncyFriction; 48 | } 49 | 50 | private double normalize(double value, double startValue, double endValue) { 51 | return (value - startValue) / (endValue - startValue); 52 | } 53 | 54 | private double project_normal(double n, double start, double end) { 55 | return start + (n * (end - start)); 56 | } 57 | 58 | private double linear_interpolation(double t, double start, double end) { 59 | return t * end + (1.f - t) * start; 60 | } 61 | 62 | private double quadratic_out_interpolation(double t, double start, double end) { 63 | return linear_interpolation(2*t - t*t, start, end); 64 | } 65 | 66 | private double b3_friction1(double x) { 67 | return (0.0007 * Math.pow(x, 3)) - (0.031 * Math.pow(x, 2)) + 0.64 * x + 1.28; 68 | } 69 | 70 | private double b3_friction2(double x) { 71 | return (0.000044 * Math.pow(x, 3)) - (0.006 * Math.pow(x, 2)) + 0.36 * x + 2.; 72 | } 73 | 74 | private double b3_friction3(double x) { 75 | return (0.00000045 * Math.pow(x, 3)) - (0.000332 * Math.pow(x, 2)) + 0.1078 * x + 5.84; 76 | } 77 | 78 | private double b3_nobounce(double tension) { 79 | double friction = 0; 80 | if (tension <= 18) { 81 | friction = b3_friction1(tension); 82 | } else if (tension > 18 && tension <= 44) { 83 | friction = b3_friction2(tension); 84 | } else if (tension > 44) { 85 | friction = b3_friction3(tension); 86 | } else { 87 | assert(false); 88 | } 89 | return friction; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/ChoreographerCompat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.beyondsw.lib.widget.rebound; 12 | 13 | import android.annotation.TargetApi; 14 | import android.os.Build; 15 | import android.os.Handler; 16 | import android.os.Looper; 17 | import android.view.Choreographer; 18 | 19 | /** 20 | * Wrapper class for abstracting away availability of the JellyBean Choreographer. If Choreographer 21 | * is unavailable we fallback to using a normal Handler. 22 | */ 23 | public class ChoreographerCompat { 24 | 25 | private static final long ONE_FRAME_MILLIS = 17; 26 | private static final boolean IS_JELLYBEAN_OR_HIGHER = 27 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; 28 | private static final ChoreographerCompat INSTANCE = new ChoreographerCompat(); 29 | 30 | private Handler mHandler; 31 | private Choreographer mChoreographer; 32 | 33 | public static ChoreographerCompat getInstance() { 34 | return INSTANCE; 35 | } 36 | 37 | private ChoreographerCompat() { 38 | if (IS_JELLYBEAN_OR_HIGHER) { 39 | mChoreographer = getChoreographer(); 40 | } else { 41 | mHandler = new Handler(Looper.getMainLooper()); 42 | } 43 | } 44 | 45 | public void postFrameCallback(FrameCallback callbackWrapper) { 46 | if (IS_JELLYBEAN_OR_HIGHER) { 47 | choreographerPostFrameCallback(callbackWrapper.getFrameCallback()); 48 | } else { 49 | mHandler.postDelayed(callbackWrapper.getRunnable(), 0); 50 | } 51 | } 52 | 53 | public void postFrameCallbackDelayed(FrameCallback callbackWrapper, long delayMillis) { 54 | if (IS_JELLYBEAN_OR_HIGHER) { 55 | choreographerPostFrameCallbackDelayed(callbackWrapper.getFrameCallback(), delayMillis); 56 | } else { 57 | mHandler.postDelayed(callbackWrapper.getRunnable(), delayMillis + ONE_FRAME_MILLIS); 58 | } 59 | } 60 | 61 | public void removeFrameCallback(FrameCallback callbackWrapper) { 62 | if (IS_JELLYBEAN_OR_HIGHER) { 63 | choreographerRemoveFrameCallback(callbackWrapper.getFrameCallback()); 64 | } else { 65 | mHandler.removeCallbacks(callbackWrapper.getRunnable()); 66 | } 67 | } 68 | 69 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 70 | private Choreographer getChoreographer() { 71 | return Choreographer.getInstance(); 72 | } 73 | 74 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 75 | private void choreographerPostFrameCallback(Choreographer.FrameCallback frameCallback) { 76 | mChoreographer.postFrameCallback(frameCallback); 77 | } 78 | 79 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 80 | private void choreographerPostFrameCallbackDelayed( 81 | Choreographer.FrameCallback frameCallback, 82 | long delayMillis) { 83 | mChoreographer.postFrameCallbackDelayed(frameCallback, delayMillis); 84 | } 85 | 86 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 87 | private void choreographerRemoveFrameCallback(Choreographer.FrameCallback frameCallback) { 88 | mChoreographer.removeFrameCallback(frameCallback); 89 | } 90 | 91 | /** 92 | * This class provides a compatibility wrapper around the JellyBean FrameCallback with methods 93 | * to access cached wrappers for submitting a real FrameCallback to a Choreographer or a Runnable 94 | * to a Handler. 95 | */ 96 | public static abstract class FrameCallback { 97 | 98 | private Runnable mRunnable; 99 | private Choreographer.FrameCallback mFrameCallback; 100 | 101 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 102 | Choreographer.FrameCallback getFrameCallback() { 103 | if (mFrameCallback == null) { 104 | mFrameCallback = new Choreographer.FrameCallback() { 105 | @Override 106 | public void doFrame(long frameTimeNanos) { 107 | FrameCallback.this.doFrame(frameTimeNanos); 108 | } 109 | }; 110 | } 111 | return mFrameCallback; 112 | } 113 | 114 | Runnable getRunnable() { 115 | if (mRunnable == null) { 116 | mRunnable = new Runnable() { 117 | @Override 118 | public void run() { 119 | doFrame(System.nanoTime()); 120 | } 121 | }; 122 | } 123 | return mRunnable; 124 | } 125 | 126 | public abstract void doFrame(long frameTimeNanos); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/OrigamiValueConverter.java: -------------------------------------------------------------------------------- 1 | package com.beyondsw.lib.widget.rebound; 2 | 3 | /** 4 | * Helper math util to convert tension & friction values from the Origami design tool to values 5 | * that the spring system needs. 6 | */ 7 | public class OrigamiValueConverter { 8 | 9 | public static double tensionFromOrigamiValue(double oValue) { 10 | return oValue == 0 ? 0 : (oValue - 30.0) * 3.62 + 194.0; 11 | } 12 | 13 | public static double origamiValueFromTension(double tension) { 14 | return tension == 0 ? 0 : (tension - 194.0) / 3.62 + 30.0; 15 | } 16 | 17 | public static double frictionFromOrigamiValue(double oValue) { 18 | return oValue == 0 ? 0 : (oValue - 8.0) * 3.0 + 25.0; 19 | } 20 | 21 | public static double origamiValueFromFriction(double friction) { 22 | return friction == 0 ? 0 : (friction - 25.0) / 3.0 + 8.0; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/SimpleSpringListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.beyondsw.lib.widget.rebound; 12 | 13 | public class SimpleSpringListener implements SpringListener { 14 | @Override 15 | public void onSpringUpdate(Spring spring) { 16 | } 17 | 18 | @Override 19 | public void onSpringAtRest(Spring spring) { 20 | } 21 | 22 | @Override 23 | public void onSpringActivate(Spring spring) { 24 | } 25 | 26 | @Override 27 | public void onSpringEndStateChange(Spring spring) { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/Spring.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.beyondsw.lib.widget.rebound; 12 | 13 | import java.util.concurrent.CopyOnWriteArraySet; 14 | 15 | /** 16 | * Classical spring implementing Hooke's law with configurable friction and tension. 17 | */ 18 | public class Spring { 19 | 20 | // unique incrementer id for springs 21 | private static int ID = 0; 22 | 23 | // maximum amount of time to simulate per physics iteration in seconds (4 frames at 60 FPS) 24 | private static final double MAX_DELTA_TIME_SEC = 0.064; 25 | // fixed timestep to use in the physics solver in seconds 26 | private static final double SOLVER_TIMESTEP_SEC = 0.001; 27 | private SpringConfig mSpringConfig; 28 | private boolean mOvershootClampingEnabled; 29 | 30 | // storage for the current and prior physics state while integration is occurring 31 | private static class PhysicsState { 32 | double position; 33 | double velocity; 34 | } 35 | 36 | // unique id for the spring in the system 37 | private final String mId; 38 | // all physics simulation objects are final and reused in each processing pass 39 | private final PhysicsState mCurrentState = new PhysicsState(); 40 | private final PhysicsState mPreviousState = new PhysicsState(); 41 | private final PhysicsState mTempState = new PhysicsState(); 42 | private double mStartValue; 43 | private double mEndValue; 44 | private boolean mWasAtRest = true; 45 | // thresholds for determining when the spring is at rest 46 | private double mRestSpeedThreshold = 0.005; 47 | private double mDisplacementFromRestThreshold = 0.005; 48 | private double mTimeAccumulator = 0; 49 | private final CopyOnWriteArraySet mListeners = 50 | new CopyOnWriteArraySet(); 51 | 52 | private final BaseSpringSystem mSpringSystem; 53 | 54 | /** 55 | * create a new spring 56 | */ 57 | Spring(BaseSpringSystem springSystem) { 58 | if (springSystem == null) { 59 | throw new IllegalArgumentException("Spring cannot be created outside of a BaseSpringSystem"); 60 | } 61 | mSpringSystem = springSystem; 62 | mId = "spring:" + ID++; 63 | setSpringConfig(SpringConfig.defaultConfig); 64 | } 65 | 66 | /** 67 | * Destroys this Spring, meaning that it will be deregistered from its BaseSpringSystem so it won't be 68 | * iterated anymore and will clear its set of listeners. Do not use the Spring after calling this, 69 | * doing so may just cause an exception to be thrown. 70 | */ 71 | public void destroy() { 72 | mListeners.clear(); 73 | mSpringSystem.deregisterSpring(this); 74 | } 75 | 76 | /** 77 | * get the unique id for this spring 78 | * @return the unique id 79 | */ 80 | public String getId() { 81 | return mId; 82 | } 83 | 84 | /** 85 | * set the config class 86 | * @param springConfig config class for the spring 87 | * @return this Spring instance for chaining 88 | */ 89 | public Spring setSpringConfig(SpringConfig springConfig) { 90 | if (springConfig == null) { 91 | throw new IllegalArgumentException("springConfig is required"); 92 | } 93 | mSpringConfig = springConfig; 94 | return this; 95 | } 96 | 97 | /** 98 | * retrieve the spring config for this spring 99 | * @return the SpringConfig applied to this spring 100 | */ 101 | public SpringConfig getSpringConfig() { 102 | return mSpringConfig; 103 | } 104 | 105 | /** 106 | * Set the displaced value to determine the displacement for the spring from the rest value. 107 | * This value is retained and used to calculate the displacement ratio. 108 | * The default signature also sets the Spring at rest to facilitate the common behavior of moving 109 | * a spring to a new position. 110 | * @param currentValue the new start and current value for the spring 111 | * @return the spring for chaining 112 | */ 113 | public Spring setCurrentValue(double currentValue) { 114 | return setCurrentValue(currentValue, true); 115 | } 116 | 117 | /** 118 | * The full signature for setCurrentValue includes the option of not setting the spring at rest 119 | * after updating its currentValue. Passing setAtRest false means that if the endValue of the 120 | * spring is not equal to the currentValue, the physics system will start iterating to resolve 121 | * the spring to the end value. This is almost never the behavior that you want, so the default 122 | * setCurrentValue signature passes true. 123 | * @param currentValue the new start and current value for the spring 124 | * @param setAtRest optionally set the spring at rest after updating its current value. 125 | * see {@link com.facebook.rebound.Spring#setAtRest()} 126 | * @return the spring for chaining 127 | */ 128 | public Spring setCurrentValue(double currentValue, boolean setAtRest) { 129 | mStartValue = currentValue; 130 | mCurrentState.position = currentValue; 131 | mSpringSystem.activateSpring(this.getId()); 132 | for (SpringListener listener : mListeners) { 133 | listener.onSpringUpdate(this); 134 | } 135 | if (setAtRest) { 136 | setAtRest(); 137 | } 138 | return this; 139 | } 140 | 141 | /** 142 | * Get the displacement value from the last time setCurrentValue was called. 143 | * @return displacement value 144 | */ 145 | public double getStartValue() { 146 | return mStartValue; 147 | } 148 | 149 | /** 150 | * Get the current 151 | * @return current value 152 | */ 153 | public double getCurrentValue() { 154 | return mCurrentState.position; 155 | } 156 | 157 | /** 158 | * get the displacement of the springs current value from its rest value. 159 | * @return the distance displaced by 160 | */ 161 | public double getCurrentDisplacementDistance() { 162 | return getDisplacementDistanceForState(mCurrentState); 163 | } 164 | 165 | /** 166 | * get the displacement from rest for a given physics state 167 | * @param state the state to measure from 168 | * @return the distance displaced by 169 | */ 170 | private double getDisplacementDistanceForState(PhysicsState state) { 171 | return Math.abs(mEndValue - state.position); 172 | } 173 | 174 | /** 175 | * set the rest value to determine the displacement for the spring 176 | * @param endValue the endValue for the spring 177 | * @return the spring for chaining 178 | */ 179 | public Spring setEndValue(double endValue) { 180 | if (mEndValue == endValue && isAtRest()) { 181 | return this; 182 | } 183 | mStartValue = getCurrentValue(); 184 | mEndValue = endValue; 185 | mSpringSystem.activateSpring(this.getId()); 186 | for (SpringListener listener : mListeners) { 187 | listener.onSpringEndStateChange(this); 188 | } 189 | return this; 190 | } 191 | 192 | /** 193 | * get the rest value used for determining the displacement of the spring 194 | * @return the rest value for the spring 195 | */ 196 | public double getEndValue() { 197 | return mEndValue; 198 | } 199 | 200 | /** 201 | * set the velocity on the spring in pixels per second 202 | * @param velocity velocity value 203 | * @return the spring for chaining 204 | */ 205 | public Spring setVelocity(double velocity) { 206 | if (velocity == mCurrentState.velocity) { 207 | return this; 208 | } 209 | mCurrentState.velocity = velocity; 210 | mSpringSystem.activateSpring(this.getId()); 211 | return this; 212 | } 213 | 214 | /** 215 | * get the velocity of the spring 216 | * @return the current velocity 217 | */ 218 | public double getVelocity() { 219 | return mCurrentState.velocity; 220 | } 221 | 222 | /** 223 | * Sets the speed at which the spring should be considered at rest. 224 | * @param restSpeedThreshold speed pixels per second 225 | * @return the spring for chaining 226 | */ 227 | public Spring setRestSpeedThreshold(double restSpeedThreshold) { 228 | mRestSpeedThreshold = restSpeedThreshold; 229 | return this; 230 | } 231 | 232 | /** 233 | * Returns the speed at which the spring should be considered at rest in pixels per second 234 | * @return speed in pixels per second 235 | */ 236 | public double getRestSpeedThreshold() { 237 | return mRestSpeedThreshold; 238 | } 239 | 240 | /** 241 | * set the threshold of displacement from rest below which the spring should be considered at rest 242 | * @param displacementFromRestThreshold displacement to consider resting below 243 | * @return the spring for chaining 244 | */ 245 | public Spring setRestDisplacementThreshold(double displacementFromRestThreshold) { 246 | mDisplacementFromRestThreshold = displacementFromRestThreshold; 247 | return this; 248 | } 249 | 250 | /** 251 | * get the threshold of displacement from rest below which the spring should be considered at rest 252 | * @return displacement to consider resting below 253 | */ 254 | public double getRestDisplacementThreshold() { 255 | return mDisplacementFromRestThreshold; 256 | } 257 | 258 | /** 259 | * Force the spring to clamp at its end value to avoid overshooting the target value. 260 | * @param overshootClampingEnabled whether or not to enable overshoot clamping 261 | * @return the spring for chaining 262 | */ 263 | public Spring setOvershootClampingEnabled(boolean overshootClampingEnabled) { 264 | mOvershootClampingEnabled = overshootClampingEnabled; 265 | return this; 266 | } 267 | 268 | /** 269 | * Check if overshoot clamping is enabled. 270 | * @return is overshoot clamping enabled 271 | */ 272 | public boolean isOvershootClampingEnabled() { 273 | return mOvershootClampingEnabled; 274 | } 275 | 276 | /** 277 | * Check if the spring is overshooting beyond its target. 278 | * @return true if the spring is overshooting its target 279 | */ 280 | public boolean isOvershooting() { 281 | return mSpringConfig.tension > 0 && 282 | ((mStartValue < mEndValue && getCurrentValue() > mEndValue) || 283 | (mStartValue > mEndValue && getCurrentValue() < mEndValue)); 284 | } 285 | 286 | /** 287 | * advance the physics simulation in SOLVER_TIMESTEP_SEC sized chunks to fulfill the required 288 | * realTimeDelta. 289 | * The math is inlined inside the loop since it made a huge performance impact when there are 290 | * several springs being advanced. 291 | * @param realDeltaTime clock drift 292 | */ 293 | void advance(double realDeltaTime) { 294 | 295 | boolean isAtRest = isAtRest(); 296 | 297 | if (isAtRest && mWasAtRest) { 298 | /* begin debug 299 | Log.d(TAG, "bailing out because we are at rest:" + getName()); 300 | end debug */ 301 | return; 302 | } 303 | 304 | // clamp the amount of realTime to simulate to avoid stuttering in the UI. We should be able 305 | // to catch up in a subsequent advance if necessary. 306 | double adjustedDeltaTime = realDeltaTime; 307 | if (realDeltaTime > MAX_DELTA_TIME_SEC) { 308 | adjustedDeltaTime = MAX_DELTA_TIME_SEC; 309 | } 310 | 311 | /* begin debug 312 | long startTime = System.currentTimeMillis(); 313 | int iterations = 0; 314 | end debug */ 315 | 316 | mTimeAccumulator += adjustedDeltaTime; 317 | 318 | double tension = mSpringConfig.tension; 319 | double friction = mSpringConfig.friction; 320 | 321 | double position = mCurrentState.position; 322 | double velocity = mCurrentState.velocity; 323 | double tempPosition = mTempState.position; 324 | double tempVelocity = mTempState.velocity; 325 | 326 | double aVelocity, aAcceleration; 327 | double bVelocity, bAcceleration; 328 | double cVelocity, cAcceleration; 329 | double dVelocity, dAcceleration; 330 | 331 | double dxdt, dvdt; 332 | 333 | // iterate over the true time 334 | while (mTimeAccumulator >= SOLVER_TIMESTEP_SEC) { 335 | /* begin debug 336 | iterations++; 337 | end debug */ 338 | mTimeAccumulator -= SOLVER_TIMESTEP_SEC; 339 | 340 | if (mTimeAccumulator < SOLVER_TIMESTEP_SEC) { 341 | // This will be the last iteration. Remember the previous state in case we need to 342 | // interpolate 343 | mPreviousState.position = position; 344 | mPreviousState.velocity = velocity; 345 | } 346 | 347 | // Perform an RK4 integration to provide better detection of the acceleration curve via 348 | // sampling of Euler integrations at 4 intervals feeding each derivative into the calculation 349 | // of the next and taking a weighted sum of the 4 derivatives as the final output. 350 | 351 | // This math was inlined since it made for big performance improvements when advancing several 352 | // springs in one pass of the BaseSpringSystem. 353 | 354 | // The initial derivative is based on the current velocity and the calculated acceleration 355 | aVelocity = velocity; 356 | aAcceleration = (tension * (mEndValue - tempPosition)) - friction * velocity; 357 | 358 | // Calculate the next derivatives starting with the last derivative and integrating over the 359 | // timestep 360 | tempPosition = position + aVelocity * SOLVER_TIMESTEP_SEC * 0.5; 361 | tempVelocity = velocity + aAcceleration * SOLVER_TIMESTEP_SEC * 0.5; 362 | bVelocity = tempVelocity; 363 | bAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity; 364 | 365 | tempPosition = position + bVelocity * SOLVER_TIMESTEP_SEC * 0.5; 366 | tempVelocity = velocity + bAcceleration * SOLVER_TIMESTEP_SEC * 0.5; 367 | cVelocity = tempVelocity; 368 | cAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity; 369 | 370 | tempPosition = position + cVelocity * SOLVER_TIMESTEP_SEC; 371 | tempVelocity = velocity + cAcceleration * SOLVER_TIMESTEP_SEC; 372 | dVelocity = tempVelocity; 373 | dAcceleration = (tension * (mEndValue - tempPosition)) - friction * tempVelocity; 374 | 375 | // Take the weighted sum of the 4 derivatives as the final output. 376 | dxdt = 1.0/6.0 * (aVelocity + 2.0 * (bVelocity + cVelocity) + dVelocity); 377 | dvdt = 1.0/6.0 * (aAcceleration + 2.0 * (bAcceleration + cAcceleration) + dAcceleration); 378 | 379 | position += dxdt * SOLVER_TIMESTEP_SEC; 380 | velocity += dvdt * SOLVER_TIMESTEP_SEC; 381 | } 382 | 383 | mTempState.position = tempPosition; 384 | mTempState.velocity = tempVelocity; 385 | 386 | mCurrentState.position = position; 387 | mCurrentState.velocity = velocity; 388 | 389 | if (mTimeAccumulator > 0) { 390 | interpolate(mTimeAccumulator / SOLVER_TIMESTEP_SEC); 391 | } 392 | 393 | // End the spring immediately if it is overshooting and overshoot clamping is enabled. 394 | // Also make sure that if the spring was considered within a resting threshold that it's now 395 | // snapped to its end value. 396 | if (isAtRest() || (mOvershootClampingEnabled && isOvershooting())) { 397 | // Don't call setCurrentValue because that forces a call to onSpringUpdate 398 | if (tension > 0) { 399 | mStartValue = mEndValue; 400 | mCurrentState.position = mEndValue; 401 | } else { 402 | mEndValue = mCurrentState.position; 403 | mStartValue = mEndValue; 404 | } 405 | setVelocity(0); 406 | isAtRest = true; 407 | } 408 | 409 | /* begin debug 410 | long endTime = System.currentTimeMillis(); 411 | long elapsedMillis = endTime - startTime; 412 | Log.d(TAG, 413 | "iterations:" + iterations + 414 | " iterationTime:" + elapsedMillis + 415 | " position:" + mCurrentState.position + 416 | " velocity:" + mCurrentState.velocity + 417 | " realDeltaTime:" + realDeltaTime + 418 | " adjustedDeltaTime:" + adjustedDeltaTime + 419 | " isAtRest:" + isAtRest + 420 | " wasAtRest:" + mWasAtRest); 421 | end debug */ 422 | 423 | // NB: do these checks outside the loop so all listeners are properly notified of the state 424 | // transition 425 | boolean notifyActivate = false; 426 | if (mWasAtRest) { 427 | mWasAtRest = false; 428 | notifyActivate = true; 429 | } 430 | boolean notifyAtRest = false; 431 | if (isAtRest) { 432 | mWasAtRest = true; 433 | notifyAtRest = true; 434 | } 435 | for (SpringListener listener : mListeners) { 436 | // starting to move 437 | if (notifyActivate) { 438 | listener.onSpringActivate(this); 439 | } 440 | 441 | // updated 442 | listener.onSpringUpdate(this); 443 | 444 | // coming to rest 445 | if (notifyAtRest) { 446 | listener.onSpringAtRest(this); 447 | } 448 | } 449 | } 450 | 451 | /** 452 | * Check if this spring should be advanced by the system. * The rule is if the spring is 453 | * currently at rest and it was at rest in the previous advance, the system can skip this spring 454 | * @return should the system process this spring 455 | */ 456 | public boolean systemShouldAdvance() { 457 | return !isAtRest() || !wasAtRest(); 458 | } 459 | 460 | /** 461 | * Check if the spring was at rest in the prior iteration. This is used for ensuring the ending 462 | * callbacks are fired as the spring comes to a rest. 463 | * @return true if the spring was at rest in the prior iteration 464 | */ 465 | public boolean wasAtRest() { 466 | return mWasAtRest; 467 | } 468 | 469 | /** 470 | * check if the current state is at rest 471 | * @return is the spring at rest 472 | */ 473 | public boolean 474 | isAtRest() { 475 | return Math.abs(mCurrentState.velocity) <= mRestSpeedThreshold && 476 | (getDisplacementDistanceForState(mCurrentState) <= mDisplacementFromRestThreshold || 477 | mSpringConfig.tension == 0); 478 | } 479 | 480 | /** 481 | * Set the spring to be at rest by making its end value equal to its current value and setting 482 | * velocity to 0. 483 | * @return this object 484 | */ 485 | public Spring setAtRest() { 486 | mEndValue = mCurrentState.position; 487 | mTempState.position = mCurrentState.position; 488 | mCurrentState.velocity = 0; 489 | return this; 490 | } 491 | 492 | /** 493 | * linear interpolation between the previous and current physics state based on the amount of 494 | * timestep remaining after processing the rendering delta time in timestep sized chunks. 495 | * @param alpha from 0 to 1, where 0 is the previous state, 1 is the current state 496 | */ 497 | private void interpolate(double alpha) { 498 | mCurrentState.position = mCurrentState.position * alpha + mPreviousState.position *(1-alpha); 499 | mCurrentState.velocity = mCurrentState.velocity * alpha + mPreviousState.velocity *(1-alpha); 500 | } 501 | 502 | /** listeners **/ 503 | 504 | /** 505 | * add a listener 506 | * @param newListener to add 507 | * @return the spring for chaining 508 | */ 509 | public Spring addListener(SpringListener newListener) { 510 | if (newListener == null) { 511 | throw new IllegalArgumentException("newListener is required"); 512 | } 513 | mListeners.add(newListener); 514 | return this; 515 | } 516 | 517 | /** 518 | * remove a listener 519 | * @param listenerToRemove to remove 520 | * @return the spring for chaining 521 | */ 522 | public Spring removeListener(SpringListener listenerToRemove) { 523 | if (listenerToRemove == null) { 524 | throw new IllegalArgumentException("listenerToRemove is required"); 525 | } 526 | mListeners.remove(listenerToRemove); 527 | return this; 528 | } 529 | 530 | /** 531 | * remove all of the listeners 532 | * @return the spring for chaining 533 | */ 534 | public Spring removeAllListeners() { 535 | mListeners.clear(); 536 | return this; 537 | } 538 | 539 | /** 540 | * This method checks to see that the current spring displacement value is equal to the input, 541 | * accounting for the spring's rest displacement threshold. 542 | * @param value The value to compare the spring value to 543 | * @return Whether the displacement value from the spring is within the bounds of the compare 544 | * value, accounting for threshold 545 | */ 546 | public boolean currentValueIsApproximately(double value) { 547 | return Math.abs(getCurrentValue() - value) <= getRestDisplacementThreshold(); 548 | } 549 | 550 | } 551 | 552 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/SpringChain.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.beyondsw.lib.widget.rebound; 11 | 12 | import java.util.List; 13 | import java.util.concurrent.CopyOnWriteArrayList; 14 | 15 | /** 16 | * SpringChain is a helper class for creating spring animations with multiple springs in a chain. 17 | * Chains of springs can be used to create cascading animations that maintain individual physics 18 | * state for each member of the chain. One spring in the chain is chosen to be the control spring. 19 | * Springs before and after the control spring in the chain are pulled along by their predecessor. 20 | * You can change which spring is the control spring at any point by calling 21 | * {@link SpringChain#setControlSpringIndex(int)}. 22 | */ 23 | public class SpringChain implements SpringListener { 24 | 25 | /** 26 | * Add these spring configs to the registry to support live tuning through the 27 | * {@link com.facebook.rebound.ui.SpringConfiguratorView} 28 | */ 29 | private static final SpringConfigRegistry registry = SpringConfigRegistry.getInstance(); 30 | private static final int DEFAULT_MAIN_TENSION = 40; 31 | private static final int DEFAULT_MAIN_FRICTION = 6; 32 | private static final int DEFAULT_ATTACHMENT_TENSION = 70; 33 | private static final int DEFAULT_ATTACHMENT_FRICTION = 10; 34 | private static int id = 0; 35 | 36 | 37 | /** 38 | * Factory method for creating a new SpringChain with default SpringConfig. 39 | * @return the newly created SpringChain 40 | */ 41 | public static SpringChain create() { 42 | return new SpringChain(); 43 | } 44 | 45 | /** 46 | * Factory method for creating a new SpringChain with the provided SpringConfig. 47 | * @param mainTension tension for the main spring 48 | * @param mainFriction friction for the main spring 49 | * @param attachmentTension tension for the attachment spring 50 | * @param attachmentFriction friction for the attachment spring 51 | * @return the newly created SpringChain 52 | */ 53 | public static SpringChain create( 54 | int mainTension, 55 | int mainFriction, 56 | int attachmentTension, 57 | int attachmentFriction) { 58 | return new SpringChain(mainTension, mainFriction, attachmentTension, attachmentFriction); 59 | } 60 | 61 | private final SpringSystem mSpringSystem = SpringSystem.create(); 62 | private final CopyOnWriteArrayList mListeners = 63 | new CopyOnWriteArrayList(); 64 | private final CopyOnWriteArrayList mSprings = new CopyOnWriteArrayList(); 65 | private int mControlSpringIndex = -1; 66 | 67 | // The main spring config defines the tension and friction for the control spring. Keeping these 68 | // values separate allows the behavior of the trailing springs to be different than that of the 69 | // control point. 70 | private final SpringConfig mMainSpringConfig; 71 | 72 | // The attachment spring config defines the tension and friction for the rest of the springs in 73 | // the chain. 74 | private final SpringConfig mAttachmentSpringConfig; 75 | 76 | private SpringChain() { 77 | this( 78 | DEFAULT_MAIN_TENSION, 79 | DEFAULT_MAIN_FRICTION, 80 | DEFAULT_ATTACHMENT_TENSION, 81 | DEFAULT_ATTACHMENT_FRICTION); 82 | } 83 | 84 | private SpringChain( 85 | int mainTension, 86 | int mainFriction, 87 | int attachmentTension, 88 | int attachmentFriction) { 89 | mMainSpringConfig = SpringConfig.fromOrigamiTensionAndFriction(mainTension, mainFriction); 90 | mAttachmentSpringConfig = 91 | SpringConfig.fromOrigamiTensionAndFriction(attachmentTension, attachmentFriction); 92 | registry.addSpringConfig(mMainSpringConfig, "main spring " + id++); 93 | registry.addSpringConfig(mAttachmentSpringConfig, "attachment spring " + id++); 94 | } 95 | 96 | public SpringConfig getMainSpringConfig() { 97 | return mMainSpringConfig; 98 | } 99 | 100 | public SpringConfig getAttachmentSpringConfig() { 101 | return mAttachmentSpringConfig; 102 | } 103 | 104 | /** 105 | * Add a spring to the chain that will callback to the provided listener. 106 | * @param listener the listener to notify for this Spring in the chain 107 | * @return this SpringChain for chaining 108 | */ 109 | public SpringChain addSpring(final SpringListener listener) { 110 | // We listen to each spring added to the SpringChain and dynamically chain the springs together 111 | // whenever the control spring state is modified. 112 | Spring spring = mSpringSystem 113 | .createSpring() 114 | .addListener(this) 115 | .setSpringConfig(mAttachmentSpringConfig); 116 | mSprings.add(spring); 117 | mListeners.add(listener); 118 | return this; 119 | } 120 | 121 | /** 122 | * Set the index of the control spring. This spring will drive the positions of all the springs 123 | * before and after it in the list when moved. 124 | * @param i the index to use for the control spring 125 | * @return this SpringChain 126 | */ 127 | public SpringChain setControlSpringIndex(int i) { 128 | mControlSpringIndex = i; 129 | Spring controlSpring = mSprings.get(mControlSpringIndex); 130 | if (controlSpring == null) { 131 | return null; 132 | } 133 | for (Spring spring : mSpringSystem.getAllSprings()) { 134 | spring.setSpringConfig(mAttachmentSpringConfig); 135 | } 136 | getControlSpring().setSpringConfig(mMainSpringConfig); 137 | return this; 138 | } 139 | 140 | /** 141 | * Retrieve the control spring so you can manipulate it to drive the positions of the other 142 | * springs. 143 | * @return the control spring. 144 | */ 145 | public Spring getControlSpring() { 146 | return mSprings.get(mControlSpringIndex); 147 | } 148 | 149 | /** 150 | * Retrieve the list of springs in the chain. 151 | * @return the list of springs 152 | */ 153 | public List getAllSprings() { 154 | return mSprings; 155 | } 156 | 157 | @Override 158 | public void onSpringUpdate(Spring spring) { 159 | // Get the control spring index and update the endValue of each spring above and below it in the 160 | // spring collection triggering a cascading effect. 161 | int idx = mSprings.indexOf(spring); 162 | SpringListener listener = mListeners.get(idx); 163 | int above = -1; 164 | int below = -1; 165 | if (idx == mControlSpringIndex) { 166 | below = idx - 1; 167 | above = idx + 1; 168 | } else if (idx < mControlSpringIndex) { 169 | below = idx - 1; 170 | } else if (idx > mControlSpringIndex) { 171 | above = idx + 1; 172 | } 173 | if (above > -1 && above < mSprings.size()) { 174 | mSprings.get(above).setEndValue(spring.getCurrentValue()); 175 | } 176 | if (below > -1 && below < mSprings.size()) { 177 | mSprings.get(below).setEndValue(spring.getCurrentValue()); 178 | } 179 | listener.onSpringUpdate(spring); 180 | } 181 | 182 | @Override 183 | public void onSpringAtRest(Spring spring) { 184 | int idx = mSprings.indexOf(spring); 185 | mListeners.get(idx).onSpringAtRest(spring); 186 | } 187 | 188 | @Override 189 | public void onSpringActivate(Spring spring) { 190 | int idx = mSprings.indexOf(spring); 191 | mListeners.get(idx).onSpringActivate(spring); 192 | } 193 | 194 | @Override 195 | public void onSpringEndStateChange(Spring spring) { 196 | int idx = mSprings.indexOf(spring); 197 | mListeners.get(idx).onSpringEndStateChange(spring); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/SpringConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.beyondsw.lib.widget.rebound; 12 | 13 | /** 14 | * Data structure for storing spring configuration. 15 | */ 16 | public class SpringConfig { 17 | public double friction; 18 | public double tension; 19 | 20 | public static SpringConfig defaultConfig = SpringConfig.fromOrigamiTensionAndFriction(40, 7); 21 | 22 | /** 23 | * constructor for the SpringConfig 24 | * @param tension tension value for the SpringConfig 25 | * @param friction friction value for the SpringConfig 26 | */ 27 | public SpringConfig(double tension, double friction) { 28 | this.tension = tension; 29 | this.friction = friction; 30 | } 31 | 32 | /** 33 | * A helper to make creating a SpringConfig easier with values mapping to the Origami values. 34 | * @param qcTension tension as defined in the Quartz Composition 35 | * @param qcFriction friction as defined in the Quartz Composition 36 | * @return a SpringConfig that maps to these values 37 | */ 38 | public static SpringConfig fromOrigamiTensionAndFriction(double qcTension, double qcFriction) { 39 | return new SpringConfig( 40 | OrigamiValueConverter.tensionFromOrigamiValue(qcTension), 41 | OrigamiValueConverter.frictionFromOrigamiValue(qcFriction) 42 | ); 43 | } 44 | 45 | /** 46 | * Map values from the Origami POP Animation patch, which are based on a bounciness and speed 47 | * value. 48 | * @param bounciness bounciness of the POP Animation 49 | * @param speed speed of the POP Animation 50 | * @return a SpringConfig mapping to the specified POP Animation values. 51 | */ 52 | public static SpringConfig fromBouncinessAndSpeed(double bounciness, double speed) { 53 | BouncyConversion bouncyConversion = new BouncyConversion(speed, bounciness); 54 | return fromOrigamiTensionAndFriction( 55 | bouncyConversion.getBouncyTension(), 56 | bouncyConversion.getBouncyFriction()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/SpringConfigRegistry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.beyondsw.lib.widget.rebound; 12 | 13 | import java.util.Collections; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | /** 18 | * class for maintaining a registry of all spring configs 19 | */ 20 | public class SpringConfigRegistry { 21 | 22 | private static final SpringConfigRegistry INSTANCE = new SpringConfigRegistry(true); 23 | 24 | public static SpringConfigRegistry getInstance() { 25 | return INSTANCE; 26 | } 27 | 28 | private final Map mSpringConfigMap; 29 | 30 | /** 31 | * constructor for the SpringConfigRegistry 32 | */ 33 | SpringConfigRegistry(boolean includeDefaultEntry) { 34 | mSpringConfigMap = new HashMap(); 35 | if (includeDefaultEntry) { 36 | addSpringConfig(SpringConfig.defaultConfig, "default config"); 37 | } 38 | } 39 | 40 | /** 41 | * add a SpringConfig to the registry 42 | * 43 | * @param springConfig SpringConfig to add to the registry 44 | * @param configName name to give the SpringConfig in the registry 45 | * @return true if the SpringConfig was added, false if a config with that name is already 46 | * present. 47 | */ 48 | public boolean addSpringConfig(SpringConfig springConfig, String configName) { 49 | if (springConfig == null) { 50 | throw new IllegalArgumentException("springConfig is required"); 51 | } 52 | if (configName == null) { 53 | throw new IllegalArgumentException("configName is required"); 54 | } 55 | if (mSpringConfigMap.containsKey(springConfig)) { 56 | return false; 57 | } 58 | mSpringConfigMap.put(springConfig, configName); 59 | return true; 60 | } 61 | 62 | /** 63 | * remove a specific SpringConfig from the registry 64 | * @param springConfig the of the SpringConfig to remove 65 | * @return true if the SpringConfig was removed, false if it was not present. 66 | */ 67 | public boolean removeSpringConfig(SpringConfig springConfig) { 68 | if (springConfig == null) { 69 | throw new IllegalArgumentException("springConfig is required"); 70 | } 71 | return mSpringConfigMap.remove(springConfig) != null; 72 | } 73 | 74 | /** 75 | * retrieve all SpringConfig in the registry 76 | * @return a list of all SpringConfig 77 | */ 78 | public Map getAllSpringConfig() { 79 | return Collections.unmodifiableMap(mSpringConfigMap); 80 | } 81 | 82 | /** 83 | * clear all SpringConfig in the registry 84 | */ 85 | public void removeAllSpringConfig() { 86 | mSpringConfigMap.clear(); 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/SpringListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.beyondsw.lib.widget.rebound; 12 | 13 | public interface SpringListener { 14 | 15 | /** 16 | * called whenever the spring is updated 17 | * @param spring the Spring sending the update 18 | */ 19 | void onSpringUpdate(Spring spring); 20 | 21 | /** 22 | * called whenever the spring achieves a resting state 23 | * @param spring the spring that's now resting 24 | */ 25 | void onSpringAtRest(Spring spring); 26 | 27 | /** 28 | * called whenever the spring leaves its resting state 29 | * @param spring the spring that has left its resting state 30 | */ 31 | void onSpringActivate(Spring spring); 32 | 33 | /** 34 | * called whenever the spring notifies of displacement state changes 35 | * @param spring the spring whose end state has changed 36 | */ 37 | void onSpringEndStateChange(Spring spring); 38 | } 39 | 40 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/SpringLooper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.beyondsw.lib.widget.rebound; 12 | 13 | /** 14 | * The spring looper is an interface for implementing platform-dependent run loops. 15 | */ 16 | public abstract class SpringLooper { 17 | 18 | protected BaseSpringSystem mSpringSystem; 19 | 20 | /** 21 | * Set the BaseSpringSystem that the SpringLooper will call back to. 22 | * @param springSystem the spring system to call loop on. 23 | */ 24 | public void setSpringSystem(BaseSpringSystem springSystem) { 25 | mSpringSystem = springSystem; 26 | } 27 | 28 | /** 29 | * The BaseSpringSystem has requested that the looper begins running this {@link Runnable} 30 | * on every frame. The {@link Runnable} will continue running on every frame until 31 | * {@link #stop()} is called. 32 | * If an existing {@link Runnable} had been started on this looper, it will be cancelled. 33 | */ 34 | public abstract void start(); 35 | 36 | /** 37 | * The looper will no longer run the {@link Runnable}. 38 | */ 39 | public abstract void stop(); 40 | } -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/SpringSystem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.beyondsw.lib.widget.rebound; 12 | 13 | /** 14 | * This is a wrapper for BaseSpringSystem that provides the convenience of automatically providing 15 | * the AndroidSpringLooper dependency in {@link SpringSystem#create}. 16 | */ 17 | public class SpringSystem extends BaseSpringSystem { 18 | 19 | /** 20 | * Create a new SpringSystem providing the appropriate constructor parameters to work properly 21 | * in an Android environment. 22 | * @return the SpringSystem 23 | */ 24 | public static SpringSystem create() { 25 | return new SpringSystem(AndroidSpringLooperFactory.createSpringLooper()); 26 | } 27 | 28 | private SpringSystem(SpringLooper springLooper) { 29 | super(springLooper); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/SpringSystemListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.beyondsw.lib.widget.rebound; 12 | 13 | /** 14 | * SpringSystemListener provides an interface for listening to events before and after each Physics 15 | * solving loop the BaseSpringSystem runs. 16 | */ 17 | public interface SpringSystemListener { 18 | 19 | /** 20 | * Runs before each pass through the physics integration loop providing an opportunity to do any 21 | * setup or alterations to the Physics state before integrating. 22 | * @param springSystem the BaseSpringSystem listened to 23 | */ 24 | void onBeforeIntegrate(BaseSpringSystem springSystem); 25 | 26 | /** 27 | * Runs after each pass through the physics integration loop providing an opportunity to do any 28 | * setup or alterations to the Physics state after integrating. 29 | * @param springSystem the BaseSpringSystem listened to 30 | */ 31 | void onAfterIntegrate(BaseSpringSystem springSystem); 32 | } 33 | 34 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/SpringUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | */ 10 | 11 | package com.beyondsw.lib.widget.rebound; 12 | 13 | public class SpringUtil { 14 | 15 | /** 16 | * Map a value within a given range to another range. 17 | * @param value the value to map 18 | * @param fromLow the low end of the range the value is within 19 | * @param fromHigh the high end of the range the value is within 20 | * @param toLow the low end of the range to map to 21 | * @param toHigh the high end of the range to map to 22 | * @return the mapped value 23 | */ 24 | public static double mapValueFromRangeToRange( 25 | double value, 26 | double fromLow, 27 | double fromHigh, 28 | double toLow, 29 | double toHigh) { 30 | double fromRangeSize = fromHigh - fromLow; 31 | double toRangeSize = toHigh - toLow; 32 | double valueScale = (value - fromLow) / fromRangeSize; 33 | return toLow + (valueScale * toRangeSize); 34 | } 35 | 36 | /** 37 | * Clamp a value to be within the provided range. 38 | * @param value the value to clamp 39 | * @param low the low end of the range 40 | * @param high the high end of the range 41 | * @return the clamped value 42 | */ 43 | public static double clamp(double value, double low, double high) { 44 | return Math.min(Math.max(value, low), high); 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/SteppingLooper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.beyondsw.lib.widget.rebound; 11 | 12 | public class SteppingLooper extends SpringLooper { 13 | 14 | private boolean mStarted; 15 | private long mLastTime; 16 | 17 | @Override 18 | public void start() { 19 | mStarted = true; 20 | mLastTime = 0; 21 | } 22 | 23 | public boolean step(long interval) { 24 | if (mSpringSystem == null || !mStarted) { 25 | return false; 26 | } 27 | long currentTime = mLastTime + interval; 28 | mSpringSystem.loop(currentTime); 29 | mLastTime = currentTime; 30 | return mSpringSystem.getIsIdle(); 31 | } 32 | 33 | @Override 34 | public void stop() { 35 | mStarted = false; 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /library/src/main/java/com/beyondsw/lib/widget/rebound/SynchronousLooper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | package com.beyondsw.lib.widget.rebound; 11 | 12 | public class SynchronousLooper extends SpringLooper { 13 | 14 | public static final double SIXTY_FPS = 16.6667; 15 | private double mTimeStep; 16 | private boolean mRunning; 17 | 18 | public SynchronousLooper() { 19 | mTimeStep = SIXTY_FPS; 20 | } 21 | 22 | public double getTimeStep() { 23 | return mTimeStep; 24 | } 25 | 26 | public void setTimeStep(double timeStep) { 27 | mTimeStep = timeStep; 28 | } 29 | 30 | @Override 31 | public void start() { 32 | mRunning = true; 33 | while (!mSpringSystem.getIsIdle()) { 34 | if (mRunning == false) { 35 | break; 36 | } 37 | mSpringSystem.loop(mTimeStep); 38 | } 39 | } 40 | 41 | @Override 42 | public void stop() { 43 | mRunning = false; 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /library/src/main/res/values/attrs_stackcards.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | library 3 | 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':library' 2 | --------------------------------------------------------------------------------