├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 1.7
51 |
52 |
53 |
54 |
55 |
56 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |  
19 |
20 |  
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 |
--------------------------------------------------------------------------------