├── .gitignore
├── LICENSE
├── README.md
├── README_CN.md
├── WhyDontIUseOtherSolution.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── yy
│ │ └── mobile
│ │ └── slidablelayout
│ │ ├── BaseDemoActivity.kt
│ │ ├── DemoForAutoSlide.kt
│ │ ├── DemoForCrossScroll.kt
│ │ ├── DemoForDataSetChanged.kt
│ │ ├── DemoForFragment.kt
│ │ ├── DemoForLoop.kt
│ │ ├── DemoForLoopHorizontally.kt
│ │ ├── DemoForNestedScroll.kt
│ │ ├── DemoForView.kt
│ │ ├── MainActivity.kt
│ │ ├── PageInfo.kt
│ │ ├── PageInfoRepository.kt
│ │ ├── SimpleQueue.kt
│ │ └── Util.kt
│ └── res
│ ├── drawable
│ ├── a.gif
│ ├── b.gif
│ ├── c.gif
│ ├── d.gif
│ ├── e.gif
│ ├── f.gif
│ ├── g.gif
│ ├── h.gif
│ └── ic_launcher_background.xml
│ ├── layout
│ ├── activity_demo.xml
│ ├── activity_demo_horizontally.xml
│ ├── activity_main.xml
│ └── page_main_content.xml
│ ├── mipmap
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ └── values
│ ├── colors.xml
│ ├── ids.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── lib
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── yy
│ │ └── mobile
│ │ └── widget
│ │ ├── FragmentViewHolder.kt
│ │ ├── SlidableDataObservable.kt
│ │ ├── SlidableDataObserver.kt
│ │ ├── SlidableLayout.kt
│ │ ├── SlidableUI.kt
│ │ ├── SlideAdapter.kt
│ │ ├── SlideDirection.kt
│ │ ├── SlideFragmentAdapter.kt
│ │ ├── SlideViewAdapter.kt
│ │ └── SlideViewHolder.kt
│ └── res
│ └── values
│ ├── attrs.xml
│ └── strings.xml
├── material
├── NestedScroll.gif
├── OppositeNestedScroll.gif
├── SlidableLayoutHorizontal.gif
└── slidableLayout.gif
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .idea/
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 |
--------------------------------------------------------------------------------
/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 | # SlidableLayout
2 |
3 | [](https://www.apache.org/licenses/LICENSE-2.0) [](https://jitpack.io/#YvesCheung/SlidableLayout)
4 |
5 | **SlidableLayout** is devoted to build a stable, easy-to-use and smooth sliding layout.
6 |
7 | [中文版](README_CN.md)
8 |
9 | Preview
10 | ========
11 | | **Vertical** | **Horizontal** |
12 | | :----: | :----: |
13 | | ![SlidableLayout][1] | ![SlidableLayoutHorizontal][2] |
14 | | **Nested scroll** | **Opposite nested scroll** |
15 | | ![NestedScroll][3] | ![OppositeNestedScroll][4] |
16 |
17 | Features
18 | ========
19 |
20 | - Support adapt to the `View` or `Fragment`
21 | - Switch the position of two reusable `View` in turn when sliding
22 | - Abundant callback to cover lifecycle
23 | - Support infinite sliding
24 | - Support `NestedScrolling` and can be used with layouts that implement the `NestedScrollingParent` interface, such as [SwipeRefreshLayout][5].
25 |
26 | Usage
27 | ========
28 |
29 | ### write in XML
30 | ```xml
31 |
32 |
35 |
36 |
37 |
41 | ```
42 |
43 | Implements the [NestedScrollingChild][6] interface, `SlidableLayout` can nest into other refresh layouts, so you can customize your pull-down-refresh and pull-up-load-more behavior.
44 |
45 | ### finish adapter code
46 |
47 |
48 | ```kotlin
49 | class MyAdapter(fm: FragmentManager) : SlideFragmentAdapter(fm) {
50 |
51 | private val data = listOf("a", "b", "c", "d")
52 | private var currentIndex = 0
53 |
54 | /**
55 | * Whether it can slide in the direction of [direction].
56 | */
57 | override fun canSlideTo(direction: SlideDirection): Boolean {
58 | val index =
59 | when (direction) {
60 | SlideDirection.Next -> currentIndex + 1 //can slide to next page
61 | SlideDirection.Prev -> currentIndex - 1 //can slide to previous page
62 | else -> currentIndex
63 | }
64 | return index in 0 until data.size
65 | }
66 |
67 | /**
68 | * Called by [SlidableLayout] to create the content [Fragment].
69 | */
70 | override fun onCreateFragment(context: Context): Fragment {
71 | return DemoFragment()
72 | }
73 |
74 | /**
75 | * Called by [SlidableLayout] when the [fragment] starts to slide to visible.
76 | * This method should update the contents of the [fragment] to reflect the
77 | * item at the [direction].
78 | */
79 | override fun onBindFragment(fragment: Fragment, direction: SlideDirection) {
80 | val index =
81 | when (direction) {
82 | SlideDirection.Next -> currentIndex + 1 //to next page
83 | SlideDirection.Prev -> currentIndex - 1 //to previous page
84 | SlideDirection.Origin -> currentIndex
85 | }
86 | //bind data to the ui
87 | (fragment as DemoFragment).currentData = data[index]
88 | }
89 |
90 | /**
91 | * Called by [SlidableLayout] when the view finishes sliding.
92 | */
93 | override fun finishSlide(direction: SlideDirection) {
94 | super.finishSlide(direction)
95 | //update current index
96 | currentIndex =
97 | when (direction) {
98 | SlideDirection.Next -> currentIndex + 1 //already to next page
99 | SlideDirection.Prev -> currentIndex - 1 //already to previous page
100 | SlideDirection.Origin -> currentIndex //rebound to origin page
101 | }
102 | }
103 | }
104 | ```
105 |
106 | Call `setAdapter` to add `Fragment` into `SlideableLayout` :
107 |
108 | ```kotlin
109 | slidable_layout.setAdapter(MyAdapter(supportFragmentManager))
110 | ```
111 |
112 | The [demo][7] provides more detail.
113 |
114 | ### Callback
115 |
116 | The `View` or `Fragment` created by `SlideAdapter` can implement the `SlidableUI` interface:
117 |
118 | ```kotlin
119 | class DemoFragment : Fragment(), SlidableUI {
120 |
121 | override fun startVisible(direction: SlideDirection) {
122 | //At the beginning of the slide, the current view will be visible.
123 | //Binding data into view can be implemented in this callback,
124 | //such as displaying place holder pictures.
125 | }
126 |
127 | override fun completeVisible(direction: SlideDirection) {
128 | //After sliding, the current view is completely visible.
129 | //You can start the main business in this callback,
130 | //such as starting to play video, page exposure statistics...
131 | }
132 |
133 | override fun invisible(direction: SlideDirection) {
134 | //After sliding, the current view is completely invisible.
135 | //You can do some cleaning work in this callback,
136 | //such as closing the video player.
137 | }
138 |
139 | override fun preload(direction: SlideDirection) {
140 | //Have completed a sliding in the direction, and the user is likely to
141 | //continue sliding in the same direction.
142 | //You can preload the next page in this callback,
143 | //such as download the next video or prepare the cover image.
144 | }
145 | }
146 | ```
147 |
148 | Install
149 | ========
150 |
151 | 1. Add it in your root build.gradle at the end of repositories:
152 | ```groovy
153 | allprojects {
154 | repositories {
155 | ...
156 | maven { url 'https://jitpack.io' }
157 | }
158 | }
159 | ```
160 |
161 | 2. Add the dependency
162 | ```groovy
163 | dependencies {
164 | implementation 'com.github.YvesCheung:SlidableLayout:1.2.0'
165 | }
166 | ```
167 |
168 |
169 | License
170 | ========
171 |
172 | Copyright 2019 YvesCheung
173 |
174 | Licensed under the Apache License, Version 2.0 (the "License");
175 | you may not use this file except in compliance with the License.
176 | You may obtain a copy of the License at
177 |
178 | http://www.apache.org/licenses/LICENSE-2.0
179 |
180 | Unless required by applicable law or agreed to in writing, software
181 | distributed under the License is distributed on an "AS IS" BASIS,
182 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
183 | See the License for the specific language governing permissions and
184 | limitations under the License.
185 |
186 |
187 | [1]: https://raw.githubusercontent.com/YvesCheung/SlidableLayout/master/material/slidableLayout.gif
188 | [2]: https://github.com/YvesCheung/SlidableLayout/raw/master/material/SlidableLayoutHorizontal.gif
189 | [3]: https://raw.githubusercontent.com/YvesCheung/SlidableLayout/master/material/NestedScroll.gif
190 | [4]: https://raw.githubusercontent.com/YvesCheung/SlidableLayout/master/material/OppositeNestedScroll.gif
191 | [5]: https://developer.android.com/reference/android/support/v4/widget/SwipeRefreshLayout
192 | [6]: https://developer.android.com/reference/android/support/v4/view/NestedScrollingChild
193 | [7]: https://github.com/YvesCheung/SlidableLayout/tree/master/app/src/main/java/com/yy/mobile/slidablelayout
194 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 | # SlidableLayout
2 |
3 | [](https://www.apache.org/licenses/LICENSE-2.0) [](https://jitpack.io/#YvesCheung/SlidableLayout)
4 |
5 | **SlidableLayout** 致力于打造通用、易用和流畅的滑动翻页布局。专注于通用的双向切换场景,包括但不限于直播间切换、阅读图书翻页、短视频应用等。
6 |
7 | ## 效果预览
8 |
9 | | **垂直方向** | **水平方向** |
10 | | :----: | :----: |
11 | | ![SlidableLayout][1] | ![SlidableLayoutHorizontal][2] |
12 | | **同向嵌套滑动** | **异向嵌套滑动** |
13 | |![NestedScroll][3] | ![OppositeNestedScoll][4]|
14 |
15 |
16 | ## 特性
17 | - 通用的基本场景,可以滑动切换 `View` 或者 `Fragment`
18 | - 使用适配器模式,继承 `SlideAdapter` 、 `SlideViewAdapter` 或者 `SlideFragmentAdapter` 来自定义业务逻辑
19 | - 只复用两个 `View` ( `Fragment` ), 滑动只是轮流切换两个 `View` 的位置,没有多余的性能消耗
20 | - 充足的时序回调,可以在滑动过程中掌握 *开始可见* ,*完全可见*,*完全不可见* 的时机
21 | - 支持无限滑动
22 | - 支持嵌套滑动,可与其他实现 `NestedScrolling` 机制的布局配合使用,比如 [SwipeRefreshLayout][5] 等刷新加载布局
23 |
24 | ## 使用
25 |
26 | ### 在XML或者代码中添加SlidableLayout
27 | ```xml
28 |
29 |
32 |
33 |
34 |
38 | ```
39 |
40 | `SlidableLayout` 本身实现了 [NestedScrollingChild][7] 接口,因此可以在外层嵌套其他滑动布局,比如自定义你的下拉刷新与上拉加载。
41 |
42 | ### 适配器业务逻辑
43 |
44 | 以滑动切换 `Fragment` 为例子,先自定义继承 `SlideFragmentAdapter` 并实现对 UI 的绑定,以及是否可以滑动的判断:
45 |
46 | ```kotlin
47 | class MyAdapter(fm: FragmentManager) : SlideFragmentAdapter(fm) {
48 |
49 | private val data = listOf("a", "b", "c", "d")
50 | private var currentIndex = 0
51 |
52 | /**
53 | * 能否向 [direction] 的方向滑动。
54 | *
55 | * @param direction 滑动的方向
56 | * @return 返回 true 表示可以滑动, false 表示不可滑动。
57 | * 如果有嵌套其他外层滑动布局(比如下拉刷新),当且仅当返回 false 时会触发外层的嵌套滑动。
58 | */
59 | override fun canSlideTo(direction: SlideDirection): Boolean {
60 | val index =
61 | when (direction) {
62 | SlideDirection.Next -> currentIndex + 1 //能否滑向下一个
63 | SlideDirection.Prev -> currentIndex - 1 //能否滑向上一个
64 | else -> currentIndex
65 | }
66 | return index in 0 until data.size
67 | }
68 |
69 | /**
70 | * 创建要显示的 [Fragment]。
71 | * 一般来说,该方法会在 [SlidableLayout.setAdapter] 调用时触发一次,创建当前显示的 [Fragment],
72 | * 会在首次开始滑动时触发第二次,创建滑动目标的 [Fragment]。
73 | */
74 | override fun onCreateFragment(context: Context): Fragment {
75 | return DemoFragment()
76 | }
77 |
78 | /**
79 | * 把 [direction] 方向那个数据与 [fragment] 绑定。做一些 ui 的显示操作。
80 | */
81 | override fun onBindFragment(fragment: Fragment, direction: SlideDirection) {
82 | val index =
83 | when (direction) {
84 | SlideDirection.Next -> currentIndex + 1 //绑定下一个的数据
85 | SlideDirection.Prev -> currentIndex - 1 //绑定上一个的数据
86 | SlideDirection.Origin -> currentIndex
87 | }
88 | //bind data to the ui
89 | (fragment as DemoFragment).currentData = data[index]
90 | }
91 |
92 | /**
93 | * 滑动结束后回调
94 | */
95 | override fun finishSlide(direction: SlideDirection) {
96 | super.finishSlide(direction)
97 | // 修正当前的索引
98 | currentIndex =
99 | when (direction) {
100 | SlideDirection.Next -> currentIndex + 1 //已经滑向下一个
101 | SlideDirection.Prev -> currentIndex - 1 //已经滑向上一个
102 | SlideDirection.Origin -> currentIndex //原地回弹
103 | }
104 | }
105 | }
106 | ```
107 |
108 | 通过 `setAdapter` 方法就会把 `Fragment` 显示到 `SlideableLayout` 上:
109 |
110 | ```kotlin
111 | slidable_layout.setAdapter(MyAdapter(supportFragmentManager))
112 | ```
113 |
114 | 更详细的适配器使用可以参照 [demo][8] 。
115 |
116 | ### 滑动时机回调
117 |
118 | 在 `SlideAdapter` 中,通过 `onCreateView` 或者 `onCreateFragment` 创建滑动切换的 `View` 或者 `Fragment` 。这些自定义的 `View` 或者 `Fragment` 可以实现 `SlidableUI` 接口,来监听滑动的时机回调:
119 |
120 | ```kotlin
121 | class DemoFragment : Fragment(), SlidableUI {
122 |
123 | override fun startVisible(direction: SlideDirection) {
124 | // 滑动开始,当前视图将要可见
125 | // 可以在该回调中实现数据与视图的绑定,比如显示占位的图片
126 | }
127 |
128 | override fun completeVisible(direction: SlideDirection) {
129 | // 滑动完成,当前视图完全可见
130 | // 可以在该回调中开始主业务,比如开始播放视频
131 | }
132 |
133 | override fun invisible(direction: SlideDirection) {
134 | // 滑动完成,当前视图完全不可见
135 | // 可以在该回调中做一些清理工作,比如关闭播放器
136 | }
137 |
138 | override fun preload(direction: SlideDirection) {
139 | // 已经完成了一次 direction 方向的滑动,用户很可能会在这个方向上继续滑动
140 | // 可以在该回调中实现下一次滑动的预加载,比如开始下载下一个视频或者准备好封面图
141 | }
142 | }
143 | ```
144 |
145 | ## 安装
146 | 1. 根目录的 build.gradle 中添加
147 | ```groovy
148 | allprojects {
149 | repositories {
150 | ...
151 | maven { url 'https://jitpack.io' }
152 | }
153 | }
154 | ```
155 |
156 | 2. 对应要使用的模块中添加依赖
157 | ```groovy
158 | dependencies {
159 | // Support library
160 | // 如果使用的是Support包,添加以下依赖
161 | implementation 'com.github.YvesCheung:SlidableLayout:1.1.0'
162 | //implementation "com.android.support:support-fragment:$support_version"
163 |
164 | // AndroidX
165 | // 如果使用的是AndroidX,添加以下依赖
166 | implementation 'com.github.YvesCheung:SlidableLayout:1.1.0.x'
167 | //implementation "androidx.fragment:fragment:$androidx_version"
168 | }
169 | ```
170 |
171 |
172 | ## 许可证
173 |
174 | Copyright 2019 YvesCheung
175 |
176 | Licensed under the Apache License, Version 2.0 (the "License");
177 | you may not use this file except in compliance with the License.
178 | You may obtain a copy of the License at
179 |
180 | http://www.apache.org/licenses/LICENSE-2.0
181 |
182 | Unless required by applicable law or agreed to in writing, software
183 | distributed under the License is distributed on an "AS IS" BASIS,
184 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
185 | See the License for the specific language governing permissions and
186 | limitations under the License.
187 |
188 |
189 | [1]: https://raw.githubusercontent.com/YvesCheung/SlidableLayout/master/material/slidableLayout.gif
190 | [2]: https://github.com/YvesCheung/SlidableLayout/raw/master/material/SlidableLayoutHorizontal.gif
191 | [3]: https://raw.githubusercontent.com/YvesCheung/SlidableLayout/master/material/NestedScroll.gif
192 | [4]: https://raw.githubusercontent.com/YvesCheung/SlidableLayout/master/material/OppositeNestedScroll.gif
193 | [5]: https://developer.android.com/reference/android/support/v4/widget/SwipeRefreshLayout
194 | [6]: https://github.com/YvesCheung/SlidableLayout/blob/master/WhyDontIUseOtherSolution.md
195 | [7]: https://developer.android.com/reference/android/support/v4/view/NestedScrollingChild
196 | [8]: https://github.com/YvesCheung/SlidableLayout/tree/master/app/src/main/java/com/yy/mobile/slidablelayout
197 |
--------------------------------------------------------------------------------
/WhyDontIUseOtherSolution.md:
--------------------------------------------------------------------------------
1 | # 为什么我不用ViewPager或RecyclerView来做上下滑切换
2 |
3 | 上下滑切换翻页大概是这样的效果:
4 |
5 | ![SlidableLayout][1]
6 |
7 | 目前网上有诸多如 *“仿抖音上下滑...”* *“仿花椒映客直播...”* 之类的技术分享,都有讲述实现上下滑切换页面的方案,其中以 `ViewPager` 和 `RecyclerView` + `SnapHelper` 两种方案为多,但是都有明显的缺点。以下是一些个人的看法:
8 |
9 | ## 为什么ViewPager不合适
10 |
11 | `ViewPager` 自带的滑动效果完全满足场景,而且支持 `Fragment` 和 `View` 等UI绑定,只要对布局和触摸事件部分作一些修改,就可以把横向的 `ViewPager` 改成竖向。
12 |
13 | 但是**没有复用是个最致命的问题**。在 `onLayout` 方法中,所有子View会实例化并一字排开在布局上。当Item数量很大时,将会是很大的性能浪费。
14 |
15 | 其次是**可见性判断的问题**。很多人会以为 `Fragment` 在 `onResume` 的时候就是可见的,而 `ViewPager` 中的 `Fragment` 就是个反例,尤其是多个 `ViewPager` 嵌套时,会同时有多个父 `Fragment` 多个子 `Fragment` 处于 `onResume` 的状态,却只有其中一个是可见的。除非放弃 `ViewPager` 的预加载机制。在页面内容曝光等重要的数据上报时,就需要判断很多条件:`onResumed` 、 `setUserVisibleHint` 、 `setOnPageChangeListener` 等。
16 |
17 | 最后是**嵌套滑动的问题**。同向嵌套滑动是很常见的场景,Google 新出的滑动布局基本都使用 NestedScrolling 机制来解决嵌套滑动。但是 ViewPager 依然需要开发者自己来处理复杂的滑动冲突。
18 |
19 | ## 为什么RecyclerView不合适
20 |
21 | `RecyclerView` + `SnapHelper` 的方案比 `ViewPager` 好得多,既有对 `View` 的复用,滑动事件也已经处理好。
22 |
23 | 但是依然**无法双向无限**滑动。我们可以在 `getItemCount` 方法中返回 Integer.MAX_VALUE 来假装无限个滑动元素。但是为了从头开始就可以下拉滑到上一个,元素列表的索引就不能初始化为0,那初始值为 Integer.MAX_VALUE/2 ?
24 | 无论怎么掩饰,理论上还是有滑动到头的一天。
25 |
26 | ## 更优的一种解决方案
27 |
28 | **使用两个 View 轮流切换就能完成上下滑的场景**。这种方案也有APP在用,但是网上几乎找不到源码。因此我把它抽成独立的库放在Github仓库:[致力于打造通用、易用和流畅的上下滑动翻页布局SlidableLayout][2]。
29 |
30 | [SlidableLayout][2] 本质是一个包含两个相同大小子 `View` 的 `FrameLayout` 。两个子 `View` 分别作为 **TopView** 和 **BackView** 。
31 |
32 | 静止状态下,用户只会看见 **TopView** ,而 **BackView** 被移除或隐藏。
33 |
34 | 手指向上拖动时, **TopView** 在y轴上向上偏移, **BackView** 开始出现,而且 **BackView** 的顶部与 **TopView** 的底部相接。
35 |
36 | 手指向上拖动一定距离后放手,**TopView** 继续在y轴上做动画直到完全消失, **BackView** 向上直到完全出现。然后 **TopView** 和 **BackView** 互换身份,原来的 **BackView** 成为现在的 **TopView** ,原来的 **TopView** 被移除或隐藏,成为下一次滑动的 **BackView** 。互换后完成一次滑动。
37 |
38 | 反之,手指向下滑动亦然。
39 |
40 | 同时要考虑手指放手后,滑动距离不够或者速度不够时,**TopView** 会沿着y轴回弹到原来的位置。 **BackView** 也跟着原路返回,直到被移除或隐藏。
41 |
42 | [SlidableLayout][2] 还实现了 NestedScrollingChild 接口,使其能够与自定义的下拉刷新布局嵌套滑动。
43 |
44 | 源码和使用例子参照 [https://github.com/YvesCheung/SlidableLayout][2] 。如有不同意的地方,请在 Github 留下 **Issue**。
45 |
46 |
47 |
48 | [1]: https://raw.githubusercontent.com/YvesCheung/SlidableLayout/master/material/slidableLayout.gif
49 | [2]: https://github.com/YvesCheung/SlidableLayout
50 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | android {
6 | compileSdkVersion 31
7 | defaultConfig {
8 | applicationId "com.yy.mobile.slidablelayout"
9 | minSdkVersion 16
10 | targetSdkVersion 31
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 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
24 | implementation 'androidx.appcompat:appcompat:1.4.2'
25 | implementation 'androidx.recyclerview:recyclerview:1.2.1'
26 | implementation 'io.github.scwang90:refresh-layout-kernel:2.0.5'
27 | implementation 'io.github.scwang90:refresh-header-classics:2.0.5'
28 | implementation 'io.github.scwang90:refresh-footer-classics:2.0.5'
29 | implementation 'com.github.bumptech.glide:glide:4.13.2' //not compat androidx yet.
30 |
31 | implementation project(":lib")
32 | }
33 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
14 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yy/mobile/slidablelayout/BaseDemoActivity.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.slidablelayout
2 |
3 | import android.os.Build
4 | import android.os.Bundle
5 | import android.view.View
6 | import android.view.Window
7 | import android.view.WindowManager
8 | import androidx.fragment.app.FragmentActivity
9 | import com.scwang.smart.refresh.footer.ClassicsFooter
10 | import com.scwang.smart.refresh.header.ClassicsHeader
11 | import com.yy.mobile.widget.SlideAdapter
12 | import com.yy.mobile.widget.SlideViewHolder
13 | import kotlinx.android.synthetic.main.activity_demo.*
14 |
15 | /**
16 | * Created by 张宇 on 2019/5/6.
17 | * E-mail: zhangyu4@yy.com
18 | * YY: 909017428
19 | */
20 | @Suppress("MemberVisibilityCanBePrivate")
21 | abstract class BaseDemoActivity : FragmentActivity() {
22 |
23 | protected open val dataList = SimpleListQueue()
24 |
25 | protected val repo = PageInfoRepository()
26 |
27 | protected var offset = 0
28 |
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 | immersive()
32 | setContentView(R.layout.activity_demo)
33 |
34 | requestDataAndAddToAdapter(false)
35 |
36 | initRefreshLayout()
37 |
38 | slidable_layout.setAdapter(createAdapter(dataList))
39 | }
40 |
41 | /**
42 | * 全屏
43 | */
44 | private fun immersive() {
45 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
46 | window.decorView.systemUiVisibility =
47 | View.SYSTEM_UI_FLAG_FULLSCREEN or
48 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
49 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
50 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
51 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
52 | View.SYSTEM_UI_FLAG_LOW_PROFILE
53 | }
54 | requestWindowFeature(Window.FEATURE_NO_TITLE)
55 | window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
56 | }
57 |
58 | /**
59 | * 这里可替换为任意支持NestedScroll的刷新布局
60 | */
61 | private fun initRefreshLayout() {
62 | refresh_layout
63 | .setEnableNestedScroll(true)
64 | .setEnableRefresh(true)
65 | .setEnableLoadMore(true)
66 | .setRefreshHeader(ClassicsHeader(this))
67 | .setRefreshFooter(ClassicsFooter(this))
68 | .setOnRefreshListener {
69 | requestDataAndAddToAdapter(true, 1000L)
70 | }
71 | .setOnLoadMoreListener {
72 | requestDataAndAddToAdapter(false, 1000L)
73 | }
74 | }
75 |
76 | protected open fun requestDataAndAddToAdapter(
77 | insertToFirst: Boolean = true,
78 | delayMills: Long = 0L
79 | ) {
80 | repo.requestPageInfo(offset, pageSize, delayMills) { result, isLastPage ->
81 | if (insertToFirst) {
82 | dataList.addFirst(result)
83 | refresh_layout.finishRefresh(0, true, isLastPage)
84 | } else {
85 | dataList.addLast(result)
86 | refresh_layout.finishLoadMore(0, true, isLastPage)
87 | }
88 | offset += result.size
89 | }
90 | }
91 |
92 | protected open val pageSize: Int get() = 4
93 |
94 | protected abstract fun createAdapter(data: SimpleQueue): SlideAdapter
95 |
96 | protected open class SimpleListQueue(
97 | protected val actual: MutableList = mutableListOf()
98 | ) : SimpleQueue, List by actual {
99 |
100 | protected var curIdx = 0
101 |
102 | open fun addFirst(data: List) {
103 | actual.addAll(0, data)
104 | curIdx += data.size
105 | }
106 |
107 | open fun addLast(data: List) {
108 | actual.addAll(data)
109 | }
110 |
111 | override fun next(): Element? {
112 | return actual.getOrNull(curIdx + 1)
113 | }
114 |
115 | override fun current(): Element? {
116 | return actual.getOrNull(curIdx)
117 | }
118 |
119 | override fun moveToNext() {
120 | curIdx++
121 | }
122 |
123 | override fun prev(): Element? {
124 | return actual.getOrNull(curIdx - 1)
125 | }
126 |
127 | override fun moveToPrev() {
128 | curIdx--
129 | }
130 |
131 | fun moveTo(idx: Int) {
132 | if (idx in 0 until actual.size) {
133 | curIdx = idx
134 | } else {
135 | throw IndexOutOfBoundsException("index must between 0 and ${actual.size} " +
136 | "but now is $idx")
137 | }
138 | }
139 | }
140 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/yy/mobile/slidablelayout/DemoForAutoSlide.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.slidablelayout
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import android.view.View
6 | import com.yy.mobile.widget.SlideAdapter
7 | import com.yy.mobile.widget.SlideDirection
8 | import com.yy.mobile.widget.SlideViewHolder
9 | import kotlinx.android.synthetic.main.activity_demo.*
10 |
11 | /**
12 | * Created by 张宇 on 2019/5/20.
13 | * E-mail: zhangyu4@yy.com
14 | * YY: 909017428
15 | */
16 | class DemoForAutoSlide : BaseDemoActivity() {
17 |
18 | override fun createAdapter(data: SimpleQueue): SlideAdapter =
19 | AutoSlideAdapter(data)
20 |
21 | override val pageSize: Int get() = 8
22 |
23 | private var autoSlideDirection = SlideDirection.Next
24 |
25 | private val handler = Handler(Looper.getMainLooper())
26 |
27 | /**
28 | * 视图出现的两秒后,自动滑到下一个视图
29 | */
30 | private inner class AutoSlideAdapter(data: SimpleQueue) :
31 | DemoForView.DemoViewAdapter(data) {
32 |
33 | override fun onViewComplete(view: View, direction: SlideDirection) {
34 | super.onViewComplete(view, direction)
35 | handler.removeCallbacks(autoSlide)
36 | handler.postDelayed(autoSlide, 2000L)
37 | }
38 | }
39 |
40 | override fun onDestroy() {
41 | handler.removeCallbacks(autoSlide)
42 | super.onDestroy()
43 | }
44 |
45 | private val autoSlide = Runnable {
46 | if (!slidable_layout.slideTo(autoSlideDirection)) {
47 | //如果当前方向不能再滑了,往反方向滑
48 | autoSlideDirection = toggle()
49 | slidable_layout.slideTo(autoSlideDirection)
50 | }
51 | }
52 |
53 | private fun toggle(): SlideDirection {
54 | return if (autoSlideDirection == SlideDirection.Next) {
55 | SlideDirection.Prev
56 | } else {
57 | SlideDirection.Next
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/yy/mobile/slidablelayout/DemoForCrossScroll.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.slidablelayout
2 |
3 | import android.content.Context
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import com.yy.mobile.widget.SlidableLayout
8 | import com.yy.mobile.widget.SlidableLayout.Companion.HORIZONTAL
9 | import com.yy.mobile.widget.SlideAdapter
10 | import com.yy.mobile.widget.SlideDirection
11 | import com.yy.mobile.widget.SlideViewAdapter
12 | import com.yy.mobile.widget.SlideViewHolder
13 |
14 | /**
15 | * @author YvesCheung
16 | * 2020-02-23
17 | */
18 | class DemoForCrossScroll : BaseDemoActivity() {
19 |
20 | override fun createAdapter(data: SimpleQueue): SlideAdapter {
21 | return OuterScrollAdapter(data)
22 | }
23 |
24 | private class OuterScrollAdapter(val data: SimpleQueue) : SlideViewAdapter() {
25 |
26 | override fun onCreateView(
27 | context: Context,
28 | parent: ViewGroup,
29 | inflater: LayoutInflater
30 | ): View = SlidableLayout(context).apply {
31 | orientation = HORIZONTAL
32 | setAdapter(InnerScrollAdapter(data))
33 | }
34 |
35 | override fun onBindView(view: View, direction: SlideDirection) {}
36 |
37 | override fun canSlideTo(direction: SlideDirection): Boolean = true
38 | }
39 |
40 | private class InnerScrollAdapter(data: SimpleQueue) : DemoForLoop.LoopAdapter(data)
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/yy/mobile/slidablelayout/DemoForDataSetChanged.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.slidablelayout
2 |
3 | import com.yy.mobile.widget.SlidableLayout
4 | import kotlinx.android.synthetic.main.activity_demo.*
5 |
6 | /**
7 | * @author YvesCheung
8 | * 2020-02-22
9 | */
10 | class DemoForDataSetChanged : DemoForFragment() {
11 |
12 | override val dataList: SimpleListQueue = IndexedListQueue()
13 |
14 | override val pageSize: Int = 1
15 |
16 | override fun requestDataAndAddToAdapter(insertToFirst: Boolean, delayMills: Long) {
17 | repo.requestPageInfo(offset, pageSize, delayMills) { result, isLastPage ->
18 | if (insertToFirst) {
19 | dataList.addFirst(result)
20 | /**
21 | * Note: add this code to refresh the [SlidableLayout]
22 | */
23 | slidable_layout.notifyDataSetChanged()
24 | refresh_layout.finishRefresh(0, true, isLastPage)
25 | } else {
26 | dataList.addLast(result)
27 | refresh_layout.finishLoadMore(0, true, isLastPage)
28 | }
29 | offset += result.size
30 | }
31 | }
32 |
33 | private open class IndexedListQueue : SimpleListQueue() {
34 |
35 | override fun addFirst(data: List) {
36 | actual.addAll(0, data)
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/yy/mobile/slidablelayout/DemoForFragment.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.slidablelayout
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.os.Bundle
6 | import androidx.fragment.app.Fragment
7 | import androidx.fragment.app.FragmentManager
8 | import android.util.Log
9 | import android.view.LayoutInflater
10 | import android.view.View
11 | import android.view.ViewGroup
12 | import com.yy.mobile.widget.SlidableUI
13 | import com.yy.mobile.widget.SlideAdapter
14 | import com.yy.mobile.widget.SlideDirection
15 | import com.yy.mobile.widget.SlideFragmentAdapter
16 | import com.yy.mobile.widget.SlideViewHolder
17 | import kotlinx.android.synthetic.main.page_main_content.*
18 |
19 | /**
20 | * Created by 张宇 on 2019/4/15.
21 | * E-mail: zhangyu4@yy.com
22 | * YY: 909017428
23 | */
24 | open class DemoForFragment : BaseDemoActivity() {
25 |
26 | override fun createAdapter(data: SimpleQueue): SlideAdapter =
27 | FragmentAdapter(data, supportFragmentManager)
28 |
29 | private class FragmentAdapter(
30 | private val data: SimpleQueue, fm: FragmentManager
31 | ) : SlideFragmentAdapter(fm) {
32 |
33 | override fun onCreateFragment(context: Context): Fragment = DemoFragment()
34 |
35 | override fun onBindFragment(fragment: Fragment, direction: SlideDirection) {
36 | super.onBindFragment(fragment, direction)
37 | (fragment as DemoFragment).setCurrentData(getData(direction)!!)
38 | }
39 |
40 | override fun canSlideTo(direction: SlideDirection): Boolean {
41 | return getData(direction) != null
42 | }
43 |
44 | override fun finishSlide(direction: SlideDirection) {
45 | if (direction == SlideDirection.Next) {
46 | data.moveToNext()
47 | } else if (direction == SlideDirection.Prev) {
48 | data.moveToPrev()
49 | }
50 | }
51 |
52 | fun getData(direction: SlideDirection): PageInfo? {
53 | return when (direction) {
54 | SlideDirection.Next -> data.next()
55 | SlideDirection.Prev -> data.prev()
56 | else -> data.current()
57 | }
58 | }
59 | }
60 |
61 | class DemoFragment : Fragment(), SlidableUI {
62 |
63 | private var currentInfo: PageInfo? = null
64 |
65 | @SuppressLint("InflateParams")
66 | override fun onCreateView(
67 | inflater: LayoutInflater,
68 | container: ViewGroup?,
69 | savedInstanceState: Bundle?
70 | ): View? {
71 | return inflater.inflate(R.layout.page_main_content, null, false)
72 | }
73 |
74 | fun setCurrentData(data: PageInfo) {
75 | currentInfo = data
76 | }
77 |
78 | override fun startVisible(direction: SlideDirection) {
79 | currentInfo?.let {
80 | content_title.text = it.title
81 | content_player.setImageDrawable(null) //should be snapshot
82 | content_player.setGifResource(it.drawableRes)
83 | }
84 | }
85 |
86 | override fun completeVisible(direction: SlideDirection) {
87 | content_player.setTag(R.id.completeVisible, true)
88 | content_player.startAnimation()
89 | }
90 |
91 | override fun invisible(direction: SlideDirection) {
92 | //clean up resource
93 | content_player.setImageDrawable(null)
94 | content_player.setTag(R.id.completeVisible, false)
95 | }
96 |
97 | override fun onCreate(savedInstanceState: Bundle?) {
98 | super.onCreate(savedInstanceState)
99 | Log.i("SlidableLayout", "onCreate")
100 | }
101 |
102 | override fun onHiddenChanged(hidden: Boolean) {
103 | super.onHiddenChanged(hidden)
104 | Log.i(
105 | "SlidableLayout", "onHiddenChanged " +
106 | "${if (hidden) "->hidden" else "->show"} " +
107 | "$currentInfo"
108 | )
109 | }
110 |
111 | override fun onResume() {
112 | super.onResume()
113 | Log.i("SlidableLayout", "onResume")
114 | }
115 |
116 | override fun onStop() {
117 | super.onStop()
118 | Log.i("SlidableLayout", "onStop")
119 | }
120 |
121 | override fun onDestroy() {
122 | super.onDestroy()
123 | Log.i("SlidableLayout", "onDestroy")
124 | }
125 |
126 | override fun setUserVisibleHint(isVisibleToUser: Boolean) {
127 | super.setUserVisibleHint(isVisibleToUser)
128 | Log.i("SlidableLayout", "setUserVisibleHint isVisible = $isVisibleToUser $currentInfo")
129 | }
130 | }
131 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/yy/mobile/slidablelayout/DemoForLoop.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.slidablelayout
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import com.yy.mobile.widget.SlideAdapter
9 | import com.yy.mobile.widget.SlideDirection
10 | import com.yy.mobile.widget.SlideViewAdapter
11 | import com.yy.mobile.widget.SlideViewHolder
12 | import kotlinx.android.synthetic.main.page_main_content.view.*
13 |
14 | /**
15 | * Created by 张宇 on 2019/5/7.
16 | * E-mail: zhangyu4@yy.com
17 | * YY: 909017428
18 | */
19 | open class DemoForLoop : BaseDemoActivity() {
20 |
21 | override fun createAdapter(data: SimpleQueue): SlideAdapter =
22 | LoopAdapter(data)
23 |
24 | open class LoopAdapter(val data: SimpleQueue) : SlideViewAdapter() {
25 |
26 | private var curIdx = 0
27 |
28 | override fun canSlideTo(direction: SlideDirection): Boolean = true
29 |
30 | @SuppressLint("InflateParams")
31 | override fun onCreateView(
32 | context: Context,
33 | parent: ViewGroup,
34 | inflater: LayoutInflater
35 | ): View {
36 | return inflater.inflate(R.layout.page_main_content, null, false)
37 | }
38 |
39 | override fun onBindView(view: View, direction: SlideDirection) {
40 | val info = data[normalize(direction.moveTo(curIdx))]
41 | view.content_title.text = info.title
42 | view.content_player.setImageDrawable(null) //should be snapshot
43 | view.content_player.setGifResource(info.drawableRes)
44 | }
45 |
46 | override fun onViewComplete(view: View, direction: SlideDirection) {
47 | view.content_player.setTag(R.id.completeVisible, true)
48 | view.content_player.startAnimation()
49 | }
50 |
51 | override fun onViewDismiss(view: View, parent: ViewGroup, direction: SlideDirection) {
52 | super.onViewDismiss(view, parent, direction)
53 | view.content_player.setTag(R.id.completeVisible, false)
54 | view.content_player.setImageDrawable(null)
55 | }
56 |
57 | override fun finishSlide(direction: SlideDirection) {
58 | curIdx = normalize(direction.moveTo(curIdx))
59 | }
60 |
61 | private fun normalize(newIdx: Int): Int {
62 | return (newIdx + data.size) % data.size
63 | }
64 | }
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/yy/mobile/slidablelayout/DemoForLoopHorizontally.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.slidablelayout
2 |
3 | import android.os.Bundle
4 | import kotlinx.android.synthetic.main.activity_demo_horizontally.*
5 |
6 | /**
7 | * Created by 张宇 on 2019-10-22.
8 | * E-mail: zhangyu4@yy.com
9 | * YY: 909017428
10 | */
11 | class DemoForLoopHorizontally : DemoForLoop() {
12 |
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 | setContentView(R.layout.activity_demo_horizontally)
16 | slidable_layout_horizontal.setAdapter(createAdapter(dataList))
17 | requestDataAndAddToAdapter(false)
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/yy/mobile/slidablelayout/DemoForNestedScroll.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.slidablelayout
2 |
3 | import android.content.Context
4 | import android.graphics.Color
5 | import android.os.Bundle
6 | import androidx.recyclerview.widget.LinearLayoutManager
7 | import androidx.recyclerview.widget.RecyclerView
8 | import androidx.recyclerview.widget.RecyclerView.VERTICAL
9 | import android.view.LayoutInflater
10 | import android.view.View
11 | import android.view.ViewGroup
12 | import android.widget.TextView
13 | import com.yy.mobile.widget.SlideAdapter
14 | import com.yy.mobile.widget.SlideDirection
15 | import com.yy.mobile.widget.SlideViewAdapter
16 | import com.yy.mobile.widget.SlideViewHolder
17 | import kotlinx.android.synthetic.main.activity_demo.*
18 | import java.util.*
19 |
20 | /**
21 | * @author YvesCheung
22 | * 2020-02-18
23 | */
24 | class DemoForNestedScroll : BaseDemoActivity() {
25 |
26 | private val orientation = VERTICAL //HORIZONTAL also ok
27 |
28 | override fun onCreate(savedInstanceState: Bundle?) {
29 | super.onCreate(savedInstanceState)
30 | slidable_layout.orientation = orientation
31 | }
32 |
33 | override fun createAdapter(data: SimpleQueue): SlideAdapter {
34 | return NestedScrollingAdapter(orientation)
35 | }
36 |
37 | open class NestedScrollingAdapter(private val orientation: Int) : SlideViewAdapter() {
38 |
39 | override fun onCreateView(
40 | context: Context,
41 | parent: ViewGroup,
42 | inflater: LayoutInflater
43 | ): View {
44 | val backgroundColor = Random().nextInt() or 0xFF000000.toInt()
45 | return RecyclerView(context).apply {
46 | layoutManager = LinearLayoutManager(
47 | context,
48 | orientation,
49 | false
50 | )
51 | adapter = RecyclerViewAdapter()
52 | setBackgroundColor(backgroundColor)
53 | }
54 | }
55 |
56 | override fun onBindView(view: View, direction: SlideDirection) {
57 | //Do Nothing.
58 | }
59 |
60 | override fun canSlideTo(direction: SlideDirection): Boolean = true
61 | }
62 |
63 | private class RecyclerViewAdapter : RecyclerView.Adapter() {
64 |
65 | override fun onCreateViewHolder(parent: ViewGroup, position: Int): Holder =
66 | Holder(TextView(parent.context))
67 |
68 | override fun getItemCount(): Int = 30
69 |
70 | override fun onBindViewHolder(viewHolder: Holder, position: Int) {
71 | viewHolder.textView.text = position.toString()
72 | viewHolder.textView.setTextColor(Color.BLACK)
73 | viewHolder.textView.textSize = 25f
74 | viewHolder.textView.setPadding(20, 20, 20, 20)
75 | }
76 | }
77 |
78 | private class Holder(val textView: TextView) : RecyclerView.ViewHolder(textView)
79 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/yy/mobile/slidablelayout/DemoForView.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.slidablelayout
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import com.yy.mobile.widget.SlideAdapter
9 | import com.yy.mobile.widget.SlideDirection
10 | import com.yy.mobile.widget.SlideViewAdapter
11 | import com.yy.mobile.widget.SlideViewHolder
12 | import kotlinx.android.synthetic.main.page_main_content.view.*
13 |
14 | /**
15 | * Created by 张宇 on 2019/5/6.
16 | * E-mail: zhangyu4@yy.com
17 | * YY: 909017428
18 | */
19 | open class DemoForView : BaseDemoActivity() {
20 |
21 | override fun createAdapter(data: SimpleQueue): SlideAdapter =
22 | DemoViewAdapter(data)
23 |
24 | open class DemoViewAdapter(private val data: SimpleQueue) : SlideViewAdapter() {
25 |
26 | @Suppress("CascadeIf")
27 | override fun canSlideTo(direction: SlideDirection): Boolean {
28 | val info =
29 | when (direction) {
30 | SlideDirection.Next -> data.next()
31 | SlideDirection.Prev -> data.prev()
32 | else -> data.current()
33 | }
34 | return info != null
35 | }
36 |
37 | @SuppressLint("InflateParams")
38 | override fun onCreateView(
39 | context: Context,
40 | parent: ViewGroup,
41 | inflater: LayoutInflater
42 | ): View {
43 | return inflater.inflate(R.layout.page_main_content, null, false)
44 | }
45 |
46 | override fun onBindView(view: View, direction: SlideDirection) {
47 | val info =
48 | if (direction == SlideDirection.Next) {
49 | data.next()!!
50 | } else if (direction == SlideDirection.Prev) {
51 | data.prev()!!
52 | } else {
53 | data.current()!!
54 | }
55 | view.content_title.text = info.title
56 | view.content_player.setImageDrawable(null) //should be snapshot
57 | view.content_player.setGifResource(info.drawableRes)
58 | }
59 |
60 | override fun onViewDismiss(view: View, parent: ViewGroup, direction: SlideDirection) {
61 | //clean up the resource
62 | view.content_player.setImageDrawable(null)
63 | view.content_player.setTag(R.id.completeVisible, false)
64 | super.onViewDismiss(view, parent, direction)
65 | }
66 |
67 | override fun finishSlide(direction: SlideDirection) {
68 | if (direction == SlideDirection.Next) {
69 | data.moveToNext()
70 | } else if (direction == SlideDirection.Prev) {
71 | data.moveToPrev()
72 | }
73 | }
74 |
75 | override fun onViewComplete(view: View, direction: SlideDirection) {
76 | view.content_player.startAnimation()
77 | view.content_player.setTag(R.id.completeVisible, true)
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/yy/mobile/slidablelayout/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.slidablelayout
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import androidx.fragment.app.FragmentActivity
6 | import android.view.View
7 |
8 | class MainActivity : FragmentActivity() {
9 |
10 | override fun onCreate(savedInstanceState: Bundle?) {
11 | super.onCreate(savedInstanceState)
12 | setContentView(R.layout.activity_main)
13 | }
14 |
15 | fun toDemoForFragment(v: View) {
16 | startActivity(Intent(this, DemoForFragment::class.java))
17 | }
18 |
19 | fun toDemoForView(v: View) {
20 | startActivity(Intent(this, DemoForView::class.java))
21 | }
22 |
23 | fun toDemoForLoop(v: View) {
24 | startActivity(Intent(this, DemoForLoop::class.java))
25 | }
26 |
27 | fun toDemoForLoopHorizontally(v: View) {
28 | startActivity(Intent(this, DemoForLoopHorizontally::class.java))
29 | }
30 |
31 | fun toDemoForAutoSlide(v: View) {
32 | startActivity(Intent(this, DemoForAutoSlide::class.java))
33 | }
34 |
35 | fun toDemoForDataSetChanged(v: View) {
36 | startActivity(Intent(this, DemoForDataSetChanged::class.java))
37 | }
38 |
39 | fun toDemoForNestedScroll(v: View) {
40 | startActivity(Intent(this, DemoForNestedScroll::class.java))
41 | }
42 |
43 | fun toDemoForCrossScroll(v: View) {
44 | startActivity(Intent(this, DemoForCrossScroll::class.java))
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yy/mobile/slidablelayout/PageInfo.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.slidablelayout
2 |
3 | import androidx.annotation.DrawableRes
4 |
5 | data class PageInfo(
6 | @DrawableRes
7 | val drawableRes: Int,
8 | val title: String
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yy/mobile/slidablelayout/PageInfoRepository.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.slidablelayout
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 |
6 | /**
7 | * Created by 张宇 on 2019/5/6.
8 | * E-mail: zhangyu4@yy.com
9 | * YY: 909017428
10 | */
11 | class PageInfoRepository {
12 |
13 | private val fakeRemoteResource = listOf(
14 | PageInfo(R.drawable.a, "接受挑战"),
15 | PageInfo(R.drawable.b, "钢铁侠指日可待"),
16 | PageInfo(R.drawable.c, "才知道原来有这个功能"),
17 | PageInfo(R.drawable.d, "google拍下摔跤瞬间"),
18 | PageInfo(R.drawable.e, "惊天一锤"),
19 | PageInfo(R.drawable.f, "为了吃提高智商"),
20 | PageInfo(R.drawable.g, "趁没人注意赶紧走"),
21 | PageInfo(R.drawable.h, "可以说明质量好多了")
22 | )
23 |
24 | private val handler = Handler(Looper.getMainLooper())
25 |
26 | /**
27 | * get the `pageInfo` list in the range of [offset,offset+size)
28 | */
29 | fun requestPageInfo(
30 | offset: Int,
31 | size: Int,
32 | delayMills: Long = 0,
33 | callback: (result: List, isLastPage: Boolean) -> Unit
34 | ) {
35 | var isLastPage = true
36 | var result: List = listOf()
37 |
38 | if (offset < fakeRemoteResource.size) {
39 | if (offset + size <= fakeRemoteResource.size) {
40 | result = fakeRemoteResource.subList(offset, offset + size).toList()
41 | isLastPage = false
42 | } else {
43 | result = fakeRemoteResource.subList(offset, fakeRemoteResource.size).toList()
44 | }
45 | }
46 |
47 | if (delayMills > 0) {
48 | handler.postDelayed({
49 | callback(result, isLastPage)
50 | }, delayMills)
51 | } else {
52 | callback(result, isLastPage)
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/yy/mobile/slidablelayout/SimpleQueue.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.slidablelayout
2 |
3 | /**
4 | * Created by 张宇 on 2019/5/6.
5 | * E-mail: zhangyu4@yy.com
6 | * YY: 909017428
7 | */
8 | interface SimpleQueue : List {
9 |
10 | fun moveToNext()
11 |
12 | fun moveToPrev()
13 |
14 | fun next(): Element?
15 |
16 | fun current(): Element?
17 |
18 | fun prev(): Element?
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/yy/mobile/slidablelayout/Util.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.slidablelayout
2 |
3 | import android.app.Activity
4 | import android.graphics.drawable.Animatable
5 | import android.graphics.drawable.Drawable
6 | import android.os.Build
7 | import android.widget.ImageView
8 | import androidx.annotation.DrawableRes
9 | import com.bumptech.glide.Glide
10 | import com.bumptech.glide.request.target.SimpleTarget
11 | import com.bumptech.glide.request.transition.Transition
12 |
13 | /**
14 | * Created by 张宇 on 2019/5/7.
15 | * E-mail: zhangyu4@yy.com
16 | * YY: 909017428
17 | */
18 | fun ImageView.setGifResource(@DrawableRes resId: Int) {
19 | val view = this
20 | val ctx = view.context as? Activity
21 | if (ctx == null ||
22 | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && ctx.isDestroyed)
23 | ) {
24 | return
25 | }
26 | Glide.with(view).load(resId).into(object : SimpleTarget() {
27 | override fun onResourceReady(resource: Drawable, transition: Transition?) {
28 | view.setImageDrawable(resource)
29 | if (view.getTag(R.id.completeVisible) == true) {
30 | view.startAnimation()
31 | }
32 | }
33 | })
34 | }
35 |
36 | fun ImageView.startAnimation() {
37 | val d = this.drawable
38 | if (d is Animatable) {
39 | d.start()
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/a.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/app/src/main/res/drawable/a.gif
--------------------------------------------------------------------------------
/app/src/main/res/drawable/b.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/app/src/main/res/drawable/b.gif
--------------------------------------------------------------------------------
/app/src/main/res/drawable/c.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/app/src/main/res/drawable/c.gif
--------------------------------------------------------------------------------
/app/src/main/res/drawable/d.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/app/src/main/res/drawable/d.gif
--------------------------------------------------------------------------------
/app/src/main/res/drawable/e.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/app/src/main/res/drawable/e.gif
--------------------------------------------------------------------------------
/app/src/main/res/drawable/f.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/app/src/main/res/drawable/f.gif
--------------------------------------------------------------------------------
/app/src/main/res/drawable/g.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/app/src/main/res/drawable/g.gif
--------------------------------------------------------------------------------
/app/src/main/res/drawable/h.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/app/src/main/res/drawable/h.gif
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
16 |
21 |
26 |
31 |
36 |
41 |
46 |
51 |
56 |
61 |
66 |
71 |
76 |
81 |
86 |
91 |
96 |
101 |
106 |
111 |
116 |
121 |
126 |
131 |
136 |
141 |
146 |
151 |
156 |
161 |
166 |
171 |
172 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_demo_horizontally.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
19 |
20 |
31 |
32 |
43 |
44 |
55 |
56 |
67 |
68 |
79 |
80 |
91 |
92 |
103 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/page_main_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/app/src/main/res/mipmap/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/app/src/main/res/mipmap/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SlidableLayout
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 | ext.kotlin_version = '1.5.31'
5 | repositories {
6 | google()
7 | mavenCentral()
8 | maven { url "https://jitpack.io" }
9 | }
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:7.0.3'
12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13 | }
14 | }
15 |
16 | allprojects {
17 | repositories {
18 | google()
19 | mavenCentral()
20 | jcenter()
21 | maven { url 'https://jitpack.io' }
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
28 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | android.useAndroidX=true
15 | android.enableJetifier=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/lib/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/lib/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | group = 'com.github.YvesCheung'
5 |
6 | android {
7 | compileSdkVersion 31
8 |
9 | defaultConfig {
10 | minSdkVersion 14
11 | targetSdkVersion 31
12 | }
13 |
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 |
21 | sourceSets {
22 | main {
23 | java {
24 | include '**/*.java'
25 | include '**/*.kt'
26 | }
27 | }
28 | }
29 | }
30 |
31 | dependencies {
32 | implementation "androidx.fragment:fragment-ktx:1.4.1"
33 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
34 | }
35 |
36 | tasks.withType(JavaCompile) {
37 | options.encoding = "UTF-8"
38 | }
--------------------------------------------------------------------------------
/lib/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/lib/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/yy/mobile/widget/FragmentViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.widget
2 |
3 | import android.view.ViewGroup
4 | import androidx.fragment.app.Fragment
5 |
6 | class FragmentViewHolder(v: ViewGroup, val f: Fragment) : SlideViewHolder(v)
7 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/yy/mobile/widget/SlidableDataObservable.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.widget
2 |
3 | import android.database.Observable
4 |
5 | /**
6 | * @author YvesCheung
7 | * 2020-02-22
8 | */
9 | internal class SlidableDataObservable : Observable() {
10 |
11 | fun notifyDataSetChanged() {
12 | val copyList =
13 | synchronized(mObservers) {
14 | mObservers.toList()
15 | }
16 | copyList.forEach { it.onChanged() }
17 | }
18 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/yy/mobile/widget/SlidableDataObserver.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.widget
2 |
3 | /**
4 | * Receives call backs when a data set has been changed.
5 | *
6 | * @author YvesCheung
7 | * 2020-02-22
8 | */
9 | interface SlidableDataObserver {
10 |
11 | /**
12 | * This method is called when the entire data set has changed
13 | */
14 | fun onChanged()
15 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/yy/mobile/widget/SlidableLayout.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.widget
2 |
3 | import android.animation.Animator
4 | import android.animation.AnimatorListenerAdapter
5 | import android.animation.ValueAnimator
6 | import android.annotation.SuppressLint
7 | import android.content.Context
8 | import android.util.AttributeSet
9 | import android.util.Log
10 | import android.view.*
11 | import android.view.animation.Interpolator
12 | import android.widget.FrameLayout
13 | import android.widget.Scroller
14 | import androidx.annotation.IntDef
15 | import androidx.core.view.*
16 | import androidx.core.view.ViewCompat.TYPE_NON_TOUCH
17 | import androidx.core.view.ViewCompat.TYPE_TOUCH
18 | import androidx.fragment.app.Fragment
19 | import kotlin.LazyThreadSafetyMode.NONE
20 | import kotlin.math.abs
21 | import kotlin.math.min
22 | import kotlin.math.roundToInt
23 | import kotlin.math.sin
24 |
25 | /**
26 | * Layout supports Sliding.
27 | * Use the [setAdapter] to construct the view.
28 | *
29 | * The child view in the layout can implement the [SlidableUI] and listen to
30 | * the callback method.
31 | *
32 | * @see SlideViewAdapter adapt to the sliding of [View].
33 | * @see SlideFragmentAdapter adapt to the sliding of [Fragment].
34 | *
35 | * ——————————————————————————————————————————————————————————————————————————————
36 | * 支持上下滑的布局。
37 | * 使用 [setAdapter] 方法来构造上下滑切换的视图。
38 | *
39 | * 布局中的子视图可以实现[SlidableUI]方法,监听对应的回调方法。
40 | *
41 | * 可以直接对 [View] 进行上下滑,参考 [SlideAdapter] 或者 [SlideViewAdapter]。
42 | * 可以对 [Fragment] 进行上下滑,参考 [SlideFragmentAdapter]。
43 | *
44 | * @author YvesCheung
45 | * 2019/4/11
46 | */
47 | @Suppress("MemberVisibilityCanBePrivate")
48 | class SlidableLayout @JvmOverloads constructor(
49 | context: Context,
50 | attrs: AttributeSet? = null,
51 | defStyleAttr: Int = 0
52 | ) : FrameLayout(context, attrs, defStyleAttr), NestedScrollingChild2, NestedScrollingParent2 {
53 |
54 | @Suppress("unused")
55 | private enum class State(val flag: Int) {
56 | /**
57 | * IDLE
58 | * 静止状态
59 | */
60 | IDLE(Mask.IDLE),
61 |
62 | /**
63 | * Dragging to the next page
64 | * 正在向下一页拖动
65 | */
66 | SLIDE_NEXT(Mask.SLIDE or Mask.NEXT),
67 |
68 | /**
69 | * Dragging to the previous page
70 | * 正在向上一页拖动
71 | */
72 | SLIDE_PREV(Mask.SLIDE or Mask.PREV),
73 |
74 | /**
75 | * Can't drag to next page
76 | * 无法拖动到下一页
77 | */
78 | SLIDE_REJECT_NEXT(Mask.REJECT or Mask.SLIDE or Mask.NEXT),
79 |
80 | /**
81 | * Can't drag to previous page
82 | * 无法拖动到上一页
83 | */
84 | SLIDE_REJECT_PREV(Mask.REJECT or Mask.SLIDE or Mask.PREV),
85 |
86 | /**
87 | * Coasting to the next page
88 | * 手指离开,惯性滑行到下一页
89 | */
90 | FLING_NEXT(Mask.FLING or Mask.NEXT),
91 |
92 | /**
93 | * Coasting to the previous page
94 | * 手指离开,惯性滑行到上一页
95 | */
96 | FLING_PREV(Mask.FLING or Mask.PREV);
97 |
98 | infix fun satisfy(mask: Int): Boolean =
99 | flag and mask == mask
100 |
101 | companion object {
102 |
103 | fun of(vararg mask: Int): State {
104 | val flag = mask.fold(0) { acc, next -> acc or next }
105 | return values().first { it.flag == flag }
106 | }
107 | }
108 | }
109 |
110 | private object Mask {
111 | const val IDLE = 0b000001
112 | const val NEXT = 0b000010
113 | const val PREV = 0b000100
114 | const val SLIDE = 0b001000
115 | const val FLING = 0b010000
116 | const val REJECT = 0b100000
117 | }
118 |
119 | private var mState = State.of(Mask.IDLE)
120 |
121 | @IntDef(value = [HORIZONTAL, VERTICAL])
122 | annotation class OrientationMode
123 |
124 | @OrientationMode
125 | var orientation: Int = VERTICAL
126 | set(value) {
127 | if (value != HORIZONTAL && value != VERTICAL) {
128 | throw IllegalArgumentException(
129 | "orientation should be 'SlidableLayout.HORIZONTAL' or 'SlidableLayout.VERTICAL'."
130 | )
131 | }
132 | if (mState != State.IDLE) {
133 | throw IllegalStateException(
134 | "Can't change orientation when the layout is not IDLE."
135 | )
136 | }
137 | field = value
138 | }
139 |
140 | //定义滑动速度足够快的标准
141 | private val mMinFlingSpeed: Float =
142 | MIN_FLING_VELOCITY * context.resources.displayMetrics.density
143 |
144 | private val childHelper = NestedScrollingChildHelper(this)
145 | private val parentHelper = NestedScrollingParentHelper(this)
146 |
147 | private val mTouchSlop: Int = ViewConfiguration.get(context).scaledPagingTouchSlop
148 |
149 | private val mDataObservable = SlidableDataObservable()
150 | private val mDataObserver = Observer()
151 |
152 | init {
153 | val a =
154 | context.obtainStyledAttributes(attrs, R.styleable.SlidableLayout, defStyleAttr, 0)
155 | try {
156 | orientation = a.getInt(R.styleable.SlidableLayout_android_orientation, VERTICAL)
157 | } finally {
158 | a.recycle()
159 | }
160 | isNestedScrollingEnabled = true
161 | }
162 |
163 | private val mInflater by lazy(NONE) { LayoutInflater.from(context) }
164 |
165 | private val mScroller = Scroller(context, sInterpolator)
166 | private var mAnimator: ValueAnimator? = null
167 |
168 | private var mViewHolderDelegate: ViewHolderDelegate? = null
169 |
170 | private inline val mCurrentView: View?
171 | get() = mViewHolderDelegate?.currentViewHolder?.view
172 |
173 | private inline val mBackupView: View?
174 | get() = mViewHolderDelegate?.backupViewHolder?.view
175 |
176 | private var downY = 0f
177 | private var downX = 0f
178 |
179 | private var mScrollConsumed: IntArray = IntArray(2)
180 | private var mScrollOffset: IntArray = IntArray(2)
181 |
182 | private val mGestureCallback = GestureCallback()
183 | private val mHorizontalGesture = HorizontalMode()
184 | private val mVerticalGesture = VerticalMode()
185 | private inline val mGesture
186 | get() = if (orientation == HORIZONTAL) mHorizontalGesture else mVerticalGesture
187 |
188 | private inline val mScrollAxis
189 | get() = if (orientation == HORIZONTAL) ViewCompat.SCROLL_AXIS_HORIZONTAL else ViewCompat.SCROLL_AXIS_VERTICAL
190 |
191 | private val gestureDetector = GestureDetector(context, mGestureCallback)
192 |
193 | private var shouldDetermineIfStartNestedScroll = false
194 | private var nestedScrolling = false
195 |
196 | @SuppressLint("ClickableViewAccessibility")
197 | override fun onTouchEvent(event: MotionEvent): Boolean {
198 | val action = event.action and MotionEvent.ACTION_MASK
199 | if (gestureDetector.onTouchEvent(event)) {
200 | return true
201 | } else if (action == MotionEvent.ACTION_UP
202 | || action == MotionEvent.ACTION_CANCEL
203 | ) {
204 | log("onUp $action state = $mState")
205 | if (mGestureCallback.onUp()) {
206 | return true
207 | }
208 | }
209 | return super.onTouchEvent(event)
210 | }
211 |
212 | override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
213 | return mGestureCallback.onInterceptTouchEvent(event) || super.onInterceptTouchEvent(event)
214 | }
215 |
216 | // just like ViewPager
217 | private fun calculateDuration(velocity: Float, maxDistance: Int, currentDistance: Int): Int {
218 |
219 | // We want the duration of the page snap animation to be influenced by the distance that
220 | // the screen has to travel, however, we don't want this duration to be effected in a
221 | // purely linear fashion. Instead, we use this method to moderate the effect that the distance
222 | // of travel has on the overall snap duration.
223 | fun distanceInfluenceForSnapDuration(f: Float): Float {
224 | var t: Double = f.toDouble()
225 | t -= 0.5 // center the values about 0.
226 | t *= 0.3 * Math.PI / 2.0
227 | return sin(t).toFloat()
228 | }
229 |
230 | val half = maxDistance / 2
231 | val distanceRatio = min(1f, abs(currentDistance).toFloat() / maxDistance)
232 | val distance = half + half * distanceInfluenceForSnapDuration(distanceRatio)
233 |
234 | val v = abs(velocity)
235 | val duration: Int =
236 | if (v > 0) {
237 | 4 * (1000 * abs(distance / v)).roundToInt()
238 | } else {
239 | val pageDelta = abs(currentDistance).toFloat() / maxDistance
240 | ((pageDelta + 1f) * 100).toInt()
241 | }
242 | return min(duration, MAX_DURATION)
243 | }
244 |
245 | private fun requestParentDisallowInterceptTouchEvent() {
246 | parent?.requestDisallowInterceptTouchEvent(true)
247 | }
248 |
249 | override fun setNestedScrollingEnabled(enabled: Boolean) {
250 | childHelper.isNestedScrollingEnabled = enabled
251 | }
252 |
253 | override fun hasNestedScrollingParent() = childHelper.hasNestedScrollingParent()
254 |
255 | override fun hasNestedScrollingParent(type: Int) = childHelper.hasNestedScrollingParent(type)
256 |
257 | override fun isNestedScrollingEnabled() = childHelper.isNestedScrollingEnabled
258 |
259 | override fun startNestedScroll(axes: Int) = childHelper.startNestedScroll(axes)
260 |
261 | override fun startNestedScroll(axes: Int, type: Int) = childHelper.startNestedScroll(axes, type)
262 |
263 | override fun stopNestedScroll(type: Int) = childHelper.stopNestedScroll(type)
264 |
265 | override fun stopNestedScroll() = childHelper.stopNestedScroll()
266 |
267 | override fun dispatchNestedScroll(
268 | dxConsumed: Int, dyConsumed: Int,
269 | dxUnconsumed: Int, dyUnconsumed: Int,
270 | offsetInWindow: IntArray?, type: Int
271 | ) = childHelper.dispatchNestedScroll(
272 | dxConsumed, dyConsumed, dxUnconsumed,
273 | dyUnconsumed, offsetInWindow, type
274 | )
275 |
276 | override fun dispatchNestedScroll(
277 | dxConsumed: Int, dyConsumed: Int,
278 | dxUnconsumed: Int, dyUnconsumed: Int,
279 | offsetInWindow: IntArray?
280 | ) = childHelper.dispatchNestedScroll(
281 | dxConsumed, dyConsumed,
282 | dxUnconsumed, dyUnconsumed, offsetInWindow
283 | )
284 |
285 | override fun dispatchNestedPreScroll(
286 | dx: Int, dy: Int, consumed: IntArray?,
287 | offsetInWindow: IntArray?, type: Int
288 | ) = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
289 |
290 | override fun dispatchNestedPreScroll(
291 | dx: Int, dy: Int, consumed: IntArray?,
292 | offsetInWindow: IntArray?
293 | ) = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
294 |
295 | override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean) =
296 | childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
297 |
298 | override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float) =
299 | childHelper.dispatchNestedPreFling(velocityX, velocityY)
300 |
301 | override fun onNestedScrollAccepted(child: View, target: View, axes: Int) =
302 | parentHelper.onNestedScrollAccepted(child, target, axes)
303 |
304 | override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) =
305 | parentHelper.onNestedScrollAccepted(child, target, axes, type)
306 |
307 | override fun getNestedScrollAxes(): Int = parentHelper.nestedScrollAxes
308 |
309 | override fun onStopNestedScroll(child: View) {
310 | log("onStopNestedScroll")
311 | val topView = mCurrentView
312 | val backView = mBackupView
313 | if (mState satisfy Mask.SLIDE && topView != null && backView != null) {
314 | mGestureCallback.performFling(topView, backView, 0f, 0f)
315 | } else if (!(mState satisfy Mask.FLING)) {
316 | mState = State.IDLE
317 | }
318 | nestedScrolling = false
319 | shouldDetermineIfStartNestedScroll = false
320 | stopNestedScroll()
321 | parentHelper.onStopNestedScroll(child)
322 | }
323 |
324 | override fun onStopNestedScroll(child: View, type: Int) = onStopNestedScroll(child)
325 |
326 | override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean {
327 | return onStartNestedScroll(child, target, nestedScrollAxes, TYPE_TOUCH)
328 | }
329 |
330 | override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
331 | onNestedPreScroll(target, dx, dy, consumed, TYPE_TOUCH)
332 | }
333 |
334 | override fun onNestedScroll(
335 | target: View, dxConsumed: Int, dyConsumed: Int,
336 | dxUnconsumed: Int, dyUnconsumed: Int
337 | ) {
338 | onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, TYPE_TOUCH)
339 | }
340 |
341 | override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
342 | val matchHorizontalMode =
343 | orientation == HORIZONTAL && axes and ViewCompat.SCROLL_AXIS_HORIZONTAL != 0
344 | val matchVerticalMode =
345 | orientation == VERTICAL && axes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
346 |
347 | log(
348 | "onStartNestedScroll target = $target type = $type " +
349 | "matchHorizontal = $matchHorizontalMode " +
350 | "matchVertical = $matchVerticalMode"
351 | )
352 | if (type == TYPE_TOUCH && (matchHorizontalMode || matchVerticalMode)) {
353 | shouldDetermineIfStartNestedScroll = true
354 | nestedScrolling = true
355 | startNestedScroll(axes)
356 | return true
357 | }
358 | return false
359 | }
360 |
361 | override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
362 | val topView = mCurrentView
363 | val backView = mBackupView
364 | if (!(mState satisfy Mask.REJECT) && mState satisfy Mask.SLIDE && topView != null && backView != null) {
365 | val dxFromDownX = topView.x - dx
366 | val dyFromDownY = topView.y - dy
367 | mGesture.scrollChildView(
368 | topView, backView,
369 | dxFromDownX, dyFromDownY, dx, dy
370 | )
371 | consumed[0] = dx
372 | consumed[1] = dy
373 | return
374 | }
375 | dispatchNestedPreScroll(dx, dy, consumed, null, type)
376 | }
377 |
378 | override fun onNestedScroll(
379 | target: View, dxConsumed: Int, dyConsumed: Int,
380 | dxUnconsumed: Int, dyUnconsumed: Int, type: Int
381 | ) {
382 |
383 | fun dispatchToChild() {
384 | dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, null, type)
385 | }
386 |
387 | val delegate = mViewHolderDelegate
388 | val topView = mCurrentView
389 | if (delegate == null || topView == null ||
390 | (dxUnconsumed == 0 && dyUnconsumed == 0)
391 | ) {
392 | return dispatchToChild()
393 | }
394 |
395 | val adapter = delegate.adapter
396 |
397 | val dxFromDownX = topView.x - dxUnconsumed
398 | val dyFromDownY = topView.y - dyUnconsumed
399 |
400 | if (shouldDetermineIfStartNestedScroll) {
401 | shouldDetermineIfStartNestedScroll = false
402 | val direction = mGesture.gestureDirection(dxFromDownX, dyFromDownY)
403 | val startToMove = mGesture.isStartToMove(dxFromDownX, dyFromDownY)
404 | val changeDirection = mGesture.isChangeDirection(dxFromDownX, dyFromDownY)
405 |
406 | if (startToMove || changeDirection) {
407 | val directionMask =
408 | if (direction == SlideDirection.Next) Mask.NEXT else Mask.PREV
409 | if (!adapter.canSlideTo(direction)) {
410 | mState = State.of(directionMask, Mask.SLIDE, Mask.REJECT)
411 | } else {
412 | mState = State.of(directionMask, Mask.SLIDE)
413 | delegate.prepareBackup(direction)
414 | }
415 | }
416 | log(
417 | "onNestedScroll dx = $dxFromDownX dy = $dyFromDownY type = $type " +
418 | "startToMove = $startToMove changeDirection = $changeDirection state = $mState"
419 | )
420 | } else {
421 | log("onNestedScroll dx = $dxFromDownX dy = $dyFromDownY type = $type state = $mState")
422 | }
423 |
424 | val backView = mBackupView
425 | if (!(mState satisfy Mask.REJECT) && mState satisfy Mask.SLIDE && backView != null) {
426 | mGesture.scrollChildView(
427 | topView, backView,
428 | dxFromDownX, dyFromDownY,
429 | dxUnconsumed, dyUnconsumed
430 | )
431 | return
432 | }
433 |
434 | dispatchToChild()
435 | }
436 |
437 | override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
438 | return dispatchNestedPreFling(velocityX, velocityY)
439 | }
440 |
441 | override fun onNestedFling(
442 | target: View,
443 | velocityX: Float,
444 | velocityY: Float,
445 | consumed: Boolean
446 | ): Boolean {
447 | log("onNestedFling vx = $velocityX vy = $velocityY consumed = $consumed")
448 | if (!consumed) {
449 | val topView = mCurrentView
450 | val backView = mBackupView
451 | if (topView != null && backView != null &&
452 | mGestureCallback.performFling(topView, backView, velocityX, velocityY)
453 | ) {
454 | return true
455 | }
456 | }
457 | return dispatchNestedFling(velocityX, velocityY, consumed)
458 | }
459 |
460 | @Suppress("ConstantConditionIf")
461 | private fun log(str: String) {
462 | if (DEBUG) Log.i("SlidableLayout", str)
463 | }
464 |
465 | /**
466 | * Set a new adapter to provide child views.
467 | *
468 | * @see SlideViewAdapter
469 | * @see SlideFragmentAdapter
470 | */
471 | fun setAdapter(adapter: SlideAdapter?) {
472 | if (mViewHolderDelegate != null) {
473 | removeAllViews()
474 | unregisterDataSetObserver(mDataObserver)
475 | }
476 | if (adapter != null) {
477 | mViewHolderDelegate =
478 | ViewHolderDelegate(adapter).apply {
479 | prepareCurrent(SlideDirection.Origin)
480 | onCompleteCurrent(SlideDirection.Origin, true)
481 | }
482 | registerDataSetObserver(mDataObserver)
483 | }
484 | }
485 |
486 | /**
487 | * Automatically slide the view in the [direction] direction.
488 | * This method will work when and only when the current state is [State.IDLE].
489 | *
490 | * ————————————————————————————————————————————————————————————————————————————————————————
491 | * 自动滑到 [direction] 方向的视图。
492 | * 当且仅当布局处于静止状态时有效。
493 | *
494 | * @param direction the slide direction:[SlideDirection.Next] or [SlideDirection.Prev]
495 | *
496 | * @return true if successfully sliding.
497 | */
498 | fun slideTo(direction: SlideDirection): Boolean {
499 | if (direction != SlideDirection.Origin &&
500 | mState satisfy Mask.IDLE
501 | ) {
502 |
503 | val delegate = mViewHolderDelegate
504 | ?: return false
505 | val adapter = delegate.adapter
506 |
507 | startNestedScroll(
508 | if (orientation == VERTICAL) ViewCompat.SCROLL_AXIS_VERTICAL
509 | else ViewCompat.SCROLL_AXIS_HORIZONTAL
510 | )
511 | requestParentDisallowInterceptTouchEvent()
512 |
513 | //Simulate sliding at a [mockSpeed] in this direction
514 | val directionMask =
515 | if (direction == SlideDirection.Prev) Mask.PREV else Mask.NEXT
516 | val mockSpeed =
517 | if (direction == SlideDirection.Prev) mMinFlingSpeed else -mMinFlingSpeed
518 |
519 | mState =
520 | if (adapter.canSlideTo(direction)) {
521 | delegate.prepareBackup(direction)
522 | State.of(directionMask, Mask.SLIDE)
523 | } else {
524 | State.of(directionMask, Mask.SLIDE, Mask.REJECT)
525 | }
526 |
527 | val canSlide = !(mState satisfy Mask.REJECT)
528 | log("Auto slide to $direction" + if (canSlide) "" else " but reject")
529 | if (orientation == VERTICAL) {
530 | mGestureCallback.onUp(0f, mockSpeed)
531 | } else {
532 | mGestureCallback.onUp(mockSpeed, 0f)
533 | }
534 | return canSlide
535 | }
536 | return false
537 | }
538 |
539 | @Deprecated(
540 | message = "Use slideTo(direction) instead.",
541 | replaceWith = ReplaceWith("slideTo(direction)")
542 | )
543 | fun slideTo(direction: SlideDirection, duration: Int) =
544 | slideTo(direction)
545 |
546 | /**
547 | * Register a new observer to listen for data changes.
548 | *
549 | * @see unregisterDataSetObserver
550 | */
551 | fun registerDataSetObserver(observer: SlidableDataObserver) {
552 | mDataObservable.registerObserver(observer)
553 | }
554 |
555 | /**
556 | * Unregister an observer currently listening for data changes.
557 | *
558 | * @see registerDataSetObserver
559 | */
560 | fun unregisterDataSetObserver(observer: SlidableDataObserver) {
561 | mDataObservable.unregisterObserver(observer)
562 | }
563 |
564 | /**
565 | * Notify any registered observers that the data set has changed.
566 | */
567 | fun notifyDataSetChanged() {
568 | mDataObservable.notifyDataSetChanged()
569 | }
570 |
571 | private inner class ViewHolderDelegate(
572 | val adapter: SlideAdapter
573 | ) {
574 |
575 | var currentViewHolder: ViewHolder? = null
576 |
577 | var backupViewHolder: ViewHolder? = null
578 |
579 | private fun ViewHolder?.prepare(direction: SlideDirection): ViewHolder {
580 | val holder = this ?: adapter.onCreateViewHolder(context, this@SlidableLayout, mInflater)
581 | if (holder.view.parent == null) {
582 | addView(holder.view, 0)
583 | }
584 | adapter.onBindView(holder, direction)
585 | return holder
586 | }
587 |
588 | fun prepareCurrent(direction: SlideDirection) =
589 | currentViewHolder.prepare(direction).also { currentViewHolder = it }
590 |
591 | fun prepareBackup(direction: SlideDirection) =
592 | backupViewHolder.prepare(direction).also { backupViewHolder = it }
593 |
594 | fun onCompleteCurrent(direction: SlideDirection, isInit: Boolean = false) {
595 | currentViewHolder?.let {
596 | if (isInit) {
597 | it.view.post {
598 | adapter.onViewComplete(it, direction)
599 | }
600 | } else {
601 | adapter.onViewComplete(it, direction)
602 | }
603 | }
604 | }
605 |
606 | fun finishSlide(direction: SlideDirection) {
607 | val visible = currentViewHolder
608 | val dismiss = backupViewHolder
609 | if (visible != null && dismiss != null) {
610 | adapter.finishSlide(dismiss, visible, direction)
611 | }
612 | }
613 |
614 | fun onDismissBackup(direction: SlideDirection) {
615 | backupViewHolder?.let { adapter.onViewDismiss(it, this@SlidableLayout, direction) }
616 | }
617 |
618 | fun swap() {
619 | val tmp = currentViewHolder
620 | currentViewHolder = backupViewHolder
621 | backupViewHolder = tmp
622 | }
623 | }
624 |
625 | private inner class Observer : SlidableDataObserver {
626 |
627 | override fun onChanged() {
628 | assertNotInLayoutOrScroll()
629 | mViewHolderDelegate?.apply {
630 | prepareCurrent(SlideDirection.Origin)
631 | onCompleteCurrent(SlideDirection.Origin, true)
632 | }
633 | }
634 |
635 | private fun assertNotInLayoutOrScroll() {
636 | if (mState satisfy Mask.IDLE || mState satisfy Mask.REJECT) {
637 | return
638 | }
639 | throw IllegalStateException(
640 | "Cannot call this method while RecyclerView is "
641 | + "computing a layout or scrolling"
642 | )
643 | }
644 | }
645 |
646 | private inner class GestureCallback : GestureDetector.SimpleOnGestureListener() {
647 |
648 | override fun onScroll(
649 | e1: MotionEvent?, e2: MotionEvent,
650 | distanceX: Float, distanceY: Float
651 | ): Boolean {
652 | if (mState satisfy Mask.FLING) {
653 | mGesture.dontConsumeTouchEvent(distanceX, distanceY)
654 | return true
655 | }
656 | val topView = mCurrentView ?: return false
657 | val delegate = mViewHolderDelegate
658 | ?: return false
659 | val adapter = delegate.adapter
660 |
661 | var dyFromDownY = e2.y - downY
662 | var dxFromDownX = e2.x - downX
663 |
664 | val direction = mGesture.gestureDirection(dxFromDownX, dyFromDownY)
665 |
666 | val startToMove = mGesture.isStartToMove(dxFromDownX, dyFromDownY)
667 |
668 | val changeDirection = mGesture.isChangeDirection(dxFromDownX, dyFromDownY)
669 |
670 | if (startToMove) {
671 | requestParentDisallowInterceptTouchEvent()
672 | }
673 |
674 | if (startToMove || changeDirection) {
675 | val directionMask =
676 | if (direction == SlideDirection.Next) Mask.NEXT else Mask.PREV
677 |
678 | if (!adapter.canSlideTo(direction)) {
679 | mState = State.of(directionMask, Mask.SLIDE, Mask.REJECT)
680 | } else {
681 | mState = State.of(directionMask, Mask.SLIDE)
682 | delegate.prepareBackup(direction)
683 | }
684 | log(
685 | "onMove state = $mState, start = $startToMove, " +
686 | "changeDirection = $changeDirection"
687 | )
688 | }
689 |
690 | if (mState satisfy Mask.REJECT || mState satisfy Mask.SLIDE) {
691 | var dx = distanceX.toInt()
692 | var dy = distanceY.toInt()
693 | if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
694 | dx -= mScrollConsumed[0]
695 | dy -= mScrollConsumed[1]
696 | dxFromDownX -= mScrollConsumed[0]
697 | dyFromDownY -= mScrollConsumed[1]
698 | }
699 |
700 | if (mState satisfy Mask.REJECT) {
701 | return dispatchNestedScroll(
702 | mScrollConsumed[0], mScrollConsumed[1],
703 | dx, dy, mScrollOffset
704 | )
705 | } else if (mState satisfy Mask.SLIDE) {
706 | val backView = mBackupView ?: return false
707 | return mGesture.scrollChildView(
708 | topView, backView, dxFromDownX, dyFromDownY, dx, dy
709 | )
710 | }
711 | }
712 | return false
713 | }
714 |
715 | override fun onFling(
716 | e1: MotionEvent?, e2: MotionEvent,
717 | velocityX: Float, velocityY: Float
718 | ): Boolean {
719 | log("onFling ${e2.action} vY = $velocityY state = $mState")
720 | onUp(velocityX, velocityY)
721 | return true
722 | }
723 |
724 | fun onUp(velocityX: Float = 0f, velocityY: Float = 0f): Boolean {
725 | if (!(mState satisfy Mask.SLIDE)) {
726 | stopNestedScroll()
727 | return false
728 | }
729 |
730 | val topView = mCurrentView ?: return resetTouch()
731 | val currentOffsetY = topView.y.toInt()
732 | val currentOffsetX = topView.x.toInt()
733 | // if state is reject, don't consume the flingChildView.
734 | val consumedFling = mGesture.shouldConsumedFling(currentOffsetX, currentOffsetY)
735 | if (!dispatchNestedPreFling(velocityX, velocityY)) {
736 | dispatchNestedFling(velocityX, velocityY, consumedFling)
737 | }
738 | stopNestedScroll()
739 |
740 | val backView = mBackupView
741 | if (backView != null &&
742 | performFling(topView, backView, velocityX, velocityY)
743 | ) {
744 | return true
745 | }
746 |
747 | return resetTouch()
748 | }
749 |
750 | fun performFling(
751 | topView: View, backView: View,
752 | velocityX: Float, velocityY: Float
753 | ): Boolean {
754 | val currentOffsetX = topView.x.toInt()
755 | val currentOffsetY = topView.y.toInt()
756 |
757 | val delegate = mViewHolderDelegate
758 |
759 | var direction: SlideDirection? = null
760 | var duration: Int? = null
761 |
762 | val widgetHeight = measuredHeight
763 | val widgetWidth = measuredWidth
764 | if (mGesture.shouldConsumedFling(currentOffsetX, currentOffsetY)) {
765 | var dy: Int? = null
766 | var dx: Int? = null
767 |
768 | if (mGesture.isFling(currentOffsetX, currentOffsetY, velocityX, velocityY)) {
769 | if (mState == State.SLIDE_NEXT) {
770 | direction = SlideDirection.Next
771 | dy = -currentOffsetY - widgetHeight
772 | dx = -currentOffsetX - widgetWidth
773 | } else if (mState == State.SLIDE_PREV) {
774 | direction = SlideDirection.Prev
775 | dy = widgetHeight - currentOffsetY
776 | dx = widgetWidth - currentOffsetX
777 | }
778 | } else { //back to origin
779 | direction = SlideDirection.Origin
780 | dy = -currentOffsetY
781 | dx = -currentOffsetX
782 | }
783 |
784 | duration = mGesture.startScroll(
785 | currentOffsetX, currentOffsetY,
786 | dx, dy, velocityX, velocityY
787 | )
788 | }
789 |
790 | //perform flingChildView animation
791 | if (delegate != null && direction != null && duration != null) {
792 | mAnimator?.cancel()
793 | mAnimator = ValueAnimator.ofFloat(1f).apply {
794 | setDuration(duration.toLong())
795 | addUpdateListener {
796 | if (mScroller.computeScrollOffset()) {
797 | mGesture.flingChildView(
798 | topView,
799 | backView,
800 | currentOffsetX,
801 | currentOffsetY
802 | )
803 | }
804 | }
805 | addListener(object : AnimatorListenerAdapter() {
806 |
807 | override fun onAnimationCancel(animation: Animator?) =
808 | onAnimationEnd(animation)
809 |
810 | override fun onAnimationEnd(animation: Animator?) {
811 | if (direction != SlideDirection.Origin) {
812 | delegate.swap()
813 | }
814 | delegate.onDismissBackup(direction)
815 | mState = State.of(Mask.IDLE)
816 | if (direction != SlideDirection.Origin) {
817 | delegate.onCompleteCurrent(direction)
818 | }
819 | delegate.finishSlide(direction)
820 | }
821 | })
822 | start()
823 | }
824 |
825 | val directionMask = if (mState satisfy Mask.NEXT) Mask.NEXT else Mask.PREV
826 | mState = State.of(directionMask, Mask.FLING)
827 | return true
828 | }
829 | return false
830 | }
831 |
832 | private fun resetTouch(): Boolean {
833 | mState = State.of(Mask.IDLE)
834 | mBackupView?.let(::removeView)
835 | return false
836 | }
837 |
838 | override fun onDown(e: MotionEvent): Boolean {
839 | downY = e.y
840 | downX = e.x
841 | startNestedScroll(mScrollAxis)
842 | return true
843 | }
844 |
845 | fun onInterceptTouchEvent(event: MotionEvent): Boolean {
846 | val action = event.action and MotionEvent.ACTION_MASK
847 | log("onInterceptTouchEvent action = $action, state = $mState")
848 | var intercept = false
849 |
850 | if (action != MotionEvent.ACTION_MOVE) {
851 | if (mState != State.IDLE) {
852 | intercept = true
853 | }
854 | }
855 |
856 | when (action) {
857 | MotionEvent.ACTION_DOWN -> {
858 | downX = event.x
859 | downY = event.y
860 | startNestedScroll(mScrollAxis)
861 | }
862 | MotionEvent.ACTION_MOVE -> {
863 | val dy = abs(event.y - downY)
864 | val dx = abs(event.x - downX)
865 | if (!nestedScrolling && mGesture.interceptTouchEvent(dx, dy)) {
866 | log("onInterceptTouchEvent requestDisallow")
867 | requestParentDisallowInterceptTouchEvent()
868 | intercept = true
869 | }
870 | }
871 | }
872 | return intercept
873 | }
874 | }
875 |
876 | private interface OrientationGestureCallback {
877 |
878 | fun interceptTouchEvent(dxFromDownX: Float, dyFromDownY: Float): Boolean
879 |
880 | fun gestureDirection(dxFromDownX: Float, dyFromDownY: Float): SlideDirection
881 |
882 | fun isStartToMove(dxFromDownX: Float, dyFromDownY: Float): Boolean
883 |
884 | fun isChangeDirection(dxFromDownX: Float, dyFromDownY: Float): Boolean
885 |
886 | fun scrollChildView(
887 | topView: View,
888 | backView: View,
889 | dxFromDownX: Float,
890 | dyFromDownY: Float,
891 | dx: Int,
892 | dy: Int
893 | ): Boolean
894 |
895 | fun shouldConsumedFling(offsetX: Int, offsetY: Int): Boolean
896 |
897 | fun isFling(offsetX: Int, offsetY: Int, velocityX: Float, velocityY: Float): Boolean
898 |
899 | fun startScroll(
900 | offsetX: Int,
901 | offsetY: Int,
902 | dx: Int?,
903 | dy: Int?,
904 | velocityX: Float,
905 | velocityY: Float
906 | ): Int?
907 |
908 | fun flingChildView(topView: View, backView: View, offsetX: Int, offsetY: Int)
909 |
910 | fun dontConsumeTouchEvent(dx: Float, dy: Float)
911 | }
912 |
913 | private inner class HorizontalMode : OrientationGestureCallback {
914 |
915 | override fun interceptTouchEvent(dxFromDownX: Float, dyFromDownY: Float): Boolean {
916 | return dxFromDownX > mTouchSlop && dxFromDownX > 2 * dyFromDownY
917 | }
918 |
919 | override fun gestureDirection(dxFromDownX: Float, dyFromDownY: Float): SlideDirection =
920 | when {
921 | dxFromDownX < 0 -> SlideDirection.Next
922 | dxFromDownX > 0 -> SlideDirection.Prev
923 | else -> SlideDirection.Origin
924 | }
925 |
926 | override fun isStartToMove(dxFromDownX: Float, dyFromDownY: Float): Boolean {
927 | return mState satisfy Mask.IDLE && abs(dxFromDownX) > 2 * abs(dyFromDownY)
928 | }
929 |
930 | override fun isChangeDirection(dxFromDownX: Float, dyFromDownY: Float): Boolean {
931 | return (mState satisfy Mask.PREV && dxFromDownX < 0) ||
932 | (mState satisfy Mask.NEXT && dxFromDownX > 0)
933 | }
934 |
935 | override fun scrollChildView(
936 | topView: View,
937 | backView: View,
938 | dxFromDownX: Float,
939 | dyFromDownY: Float,
940 | dx: Int,
941 | dy: Int
942 | ): Boolean {
943 | topView.x = dxFromDownX
944 | backView.x =
945 | if (mState satisfy Mask.NEXT) dxFromDownX + measuredWidth
946 | else dxFromDownX - measuredWidth
947 | return dispatchNestedScroll(dx, 0, 0, dy, mScrollOffset)
948 | }
949 |
950 | override fun shouldConsumedFling(offsetX: Int, offsetY: Int): Boolean {
951 | return !(mState satisfy Mask.REJECT) || offsetX != 0
952 | }
953 |
954 | override fun isFling(
955 | offsetX: Int,
956 | offsetY: Int,
957 | velocityX: Float,
958 | velocityY: Float
959 | ): Boolean {
960 | val highSpeed = abs(velocityX) >= mMinFlingSpeed
961 |
962 | val sameDirection =
963 | (mState == State.SLIDE_NEXT && velocityX < 0) ||
964 | (mState == State.SLIDE_PREV && velocityX > 0)
965 |
966 | val moveLongDistance = abs(offsetX) > measuredWidth / 3
967 |
968 | return (highSpeed && sameDirection) || (!highSpeed && moveLongDistance)
969 | }
970 |
971 | override fun startScroll(
972 | offsetX: Int,
973 | offsetY: Int,
974 | dx: Int?,
975 | dy: Int?,
976 | velocityX: Float,
977 | velocityY: Float
978 | ): Int? {
979 | if (dx != null) {
980 | val duration = calculateDuration(velocityX, measuredWidth, dx)
981 | mScroller.startScroll(offsetX, 0, dx, 0, duration)
982 | return duration
983 | }
984 | return null
985 | }
986 |
987 | override fun flingChildView(topView: View, backView: View, offsetX: Int, offsetY: Int) {
988 | val widgetWidth = measuredWidth
989 | val offset = mScroller.currX.toFloat()
990 | topView.x = offset
991 | backView.x =
992 | if (mState == State.FLING_NEXT) offset + widgetWidth
993 | else offset - widgetWidth
994 | }
995 |
996 | override fun dontConsumeTouchEvent(dx: Float, dy: Float) {
997 | //eat all the dx
998 | val consumedX = dx.toInt()
999 | val unconsumedY = dy.toInt()
1000 | if (!dispatchNestedPreScroll(
1001 | consumedX, unconsumedY, mScrollConsumed,
1002 | mScrollOffset, TYPE_NON_TOUCH
1003 | )
1004 | ) {
1005 | dispatchNestedScroll(
1006 | consumedX, 0, 0, unconsumedY,
1007 | mScrollOffset, TYPE_NON_TOUCH
1008 | )
1009 | }
1010 | }
1011 | }
1012 |
1013 | private inner class VerticalMode : OrientationGestureCallback {
1014 |
1015 | override fun interceptTouchEvent(dxFromDownX: Float, dyFromDownY: Float): Boolean {
1016 | return dyFromDownY > mTouchSlop && dyFromDownY > 2 * dxFromDownX
1017 | }
1018 |
1019 | override fun gestureDirection(dxFromDownX: Float, dyFromDownY: Float): SlideDirection =
1020 | when {
1021 | dyFromDownY < 0 -> SlideDirection.Next
1022 | dyFromDownY > 0 -> SlideDirection.Prev
1023 | else -> SlideDirection.Origin
1024 | }
1025 |
1026 | override fun isStartToMove(dxFromDownX: Float, dyFromDownY: Float): Boolean {
1027 | return mState satisfy Mask.IDLE && abs(dyFromDownY) > 2 * abs(dxFromDownX)
1028 | }
1029 |
1030 | override fun isChangeDirection(dxFromDownX: Float, dyFromDownY: Float): Boolean {
1031 | return (mState satisfy Mask.PREV && dyFromDownY < 0) ||
1032 | (mState satisfy Mask.NEXT && dyFromDownY > 0)
1033 | }
1034 |
1035 | override fun scrollChildView(
1036 | topView: View,
1037 | backView: View,
1038 | dxFromDownX: Float,
1039 | dyFromDownY: Float,
1040 | dx: Int,
1041 | dy: Int
1042 | ): Boolean {
1043 | topView.y = dyFromDownY
1044 | backView.y =
1045 | if (mState satisfy Mask.NEXT) dyFromDownY + measuredHeight
1046 | else dyFromDownY - measuredHeight
1047 | return dispatchNestedScroll(0, dy, dx, 0, mScrollOffset)
1048 | }
1049 |
1050 | override fun shouldConsumedFling(offsetX: Int, offsetY: Int): Boolean {
1051 | return !(mState satisfy Mask.REJECT) || offsetY != 0
1052 | }
1053 |
1054 | override fun isFling(
1055 | offsetX: Int,
1056 | offsetY: Int,
1057 | velocityX: Float,
1058 | velocityY: Float
1059 | ): Boolean {
1060 | val highSpeed = abs(velocityY) >= mMinFlingSpeed
1061 |
1062 | val sameDirection =
1063 | (mState == State.SLIDE_NEXT && velocityY < 0) ||
1064 | (mState == State.SLIDE_PREV && velocityY > 0)
1065 |
1066 | val moveLongDistance = abs(offsetY) > measuredHeight / 3
1067 |
1068 | return (highSpeed && sameDirection) || (!highSpeed && moveLongDistance)
1069 | }
1070 |
1071 | override fun startScroll(
1072 | offsetX: Int,
1073 | offsetY: Int,
1074 | dx: Int?,
1075 | dy: Int?,
1076 | velocityX: Float,
1077 | velocityY: Float
1078 | ): Int? {
1079 | if (dy != null) {
1080 | val duration = calculateDuration(velocityY, measuredHeight, dy)
1081 | mScroller.startScroll(0, offsetY, 0, dy, duration)
1082 | return duration
1083 | }
1084 | return null
1085 | }
1086 |
1087 | override fun flingChildView(topView: View, backView: View, offsetX: Int, offsetY: Int) {
1088 | val widgetHeight = measuredHeight
1089 | val offset = mScroller.currY.toFloat()
1090 | topView.y = offset
1091 | backView.y =
1092 | if (mState == State.FLING_NEXT) offset + widgetHeight
1093 | else offset - widgetHeight
1094 | }
1095 |
1096 | override fun dontConsumeTouchEvent(dx: Float, dy: Float) {
1097 | //eat all the dy
1098 | val unconsumedX = dx.toInt()
1099 | val consumedY = dy.toInt()
1100 | if (!dispatchNestedPreScroll(
1101 | unconsumedX, consumedY, mScrollConsumed,
1102 | mScrollOffset, TYPE_NON_TOUCH
1103 | )
1104 | ) {
1105 | dispatchNestedScroll(
1106 | 0, consumedY, unconsumedX, 0,
1107 | mScrollOffset, TYPE_NON_TOUCH
1108 | )
1109 | }
1110 | }
1111 | }
1112 |
1113 | companion object {
1114 |
1115 | const val HORIZONTAL = 0
1116 | const val VERTICAL = 1
1117 |
1118 | private const val DEBUG = true
1119 |
1120 | private const val MIN_FLING_VELOCITY = 400 // dips
1121 |
1122 | const val MAX_DURATION = 600 //最大滑行时间ms
1123 |
1124 | private val sInterpolator = Interpolator { t ->
1125 | val f = t - 1.0f
1126 | f * f * f * f * f + 1.0f
1127 | }
1128 | }
1129 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/yy/mobile/widget/SlidableUI.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.widget
2 |
3 | interface SlidableUI {
4 |
5 | /**
6 | * At the beginning of the slide, the current view will be visible.
7 | * Binding data into view can be implemented in this callback,
8 | * such as displaying place holder pictures.
9 | *
10 | * ——————————————————————————————————————————————————————————————————————————
11 | * 滑动开始,当前视图将要可见
12 | * 可以在该回调中实现数据与视图的绑定,比如显示占位的图片
13 | */
14 | fun startVisible(direction: SlideDirection) {}
15 |
16 | /**
17 | * After sliding, the current view is completely visible.
18 | * You can start the main business in this callback,
19 | * such as starting to play video, page exposure statistics...
20 | *
21 | * ——————————————————————————————————————————————————————————————————————————
22 | * 滑动完成,当前视图完全可见
23 | * 可以在该回调中开始主业务,比如开始播放视频,比如广告曝光统计
24 | */
25 | fun completeVisible(direction: SlideDirection) {}
26 |
27 | /**
28 | * After sliding, the current view is completely invisible.
29 | * You can do some cleaning work in this callback,
30 | * such as closing the video player.
31 | *
32 | * ——————————————————————————————————————————————————————————————————————————
33 | * 滑动完成,当前视图完全不可见
34 | * 可以在该回调中做一些清理工作,比如关闭播放器
35 | */
36 | fun invisible(direction: SlideDirection) {}
37 |
38 | /**
39 | *
40 | * Have completed a sliding in the direction, and the user is likely to
41 | * continue sliding in the same direction. You can preload the next page in this callback,
42 | * such as download the next video or prepare the cover image.
43 | *
44 | * ——————————————————————————————————————————————————————————————————————————
45 | * 已经完成了一次 direction 方向的滑动,用户很可能会在这个方向上继续滑动
46 | * 可以在该回调中实现下一次滑动的预加载,比如开始下载下一个视频或者准备好封面图
47 | */
48 | fun preload(direction: SlideDirection) {}
49 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/yy/mobile/widget/SlideAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.widget
2 |
3 | import android.content.Context
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 |
7 | /**
8 | * Adapt to the view in layout.
9 | *
10 | * If page【A】 is initialized for the first time, the callback will be:
11 | * - onCreateViewHolder(context, inflater)
12 | * - onViewComplete(viewHolder【A】)
13 | *
14 | * If sliding from page【A】 to the next page【B】, the callback will be:
15 | * - canSlideTo(SlideDirection.Next)
16 | * - onCreateViewHolder(context, inflater) (如果是首次滑动)
17 | * - onBindView(viewHolder【B】, SlideDirection.Next)
18 | * - onViewDismiss(viewHolder【A】, SlideDirection.Next)
19 | * - onViewComplete(viewHolder【B】)
20 | * - finishSlide(SlideDirection.Next)
21 | *
22 | * If sliding from page【B】 to the previous page【A】, the callback will be:
23 | * - canSlideTo(SlideDirection.Prev)
24 | * - onBindView(viewHolder【A】, SlideDirection.Prev)
25 | * - onViewDismiss(viewHolder【B】, SlideDirection.Prev)
26 | * - onViewComplete(viewHolder【A】)
27 | * - finishSlide(SlideDirection.Prev)
28 | *
29 | * If try sliding from page【A】 to page【B】, but do not have enough distance or speed,
30 | * you will rebound to page【A】, and the callback will be:
31 | * - canSlideTo(SlideDirection.Next)
32 | * - onBindView(viewHolder【B】, SlideDirection.Next)
33 | * - onViewDismiss(viewHolder【B】, SlideDirection.Next)
34 | * - finishSlide(SlideDirection.Origin)
35 | *
36 | * ——————————————————————————————————————————————————————————————————————————————
37 | * 适配 [SlidableLayout] 以及布局中滑动的 [View] 。
38 | *
39 | * 假如首次初始化页面【A】,触发的回调是:
40 | * - onCreateViewHolder(context, inflater)
41 | * - onViewComplete(viewHolder【A】)
42 | *
43 | * 假如从页面【A】滑动下一个页面【B】,触发的回调将会是:
44 | * - canSlideTo(SlideDirection.Next)
45 | * - onCreateViewHolder(context, inflater) (如果是首次滑动)
46 | * - onBindView(viewHolder【B】, SlideDirection.Next)
47 | * - onViewDismiss(viewHolder【A】, SlideDirection.Next)
48 | * - onViewComplete(viewHolder【B】)
49 | * - finishSlide(SlideDirection.Next)
50 | *
51 | * 假如再从页面【B】 滑动回上一个页面 【A】,触发的回调是:
52 | * - canSlideTo(SlideDirection.Prev)
53 | * - onBindView(viewHolder【A】, SlideDirection.Prev)
54 | * - onViewDismiss(viewHolder【B】, SlideDirection.Prev)
55 | * - onViewComplete(viewHolder【A】)
56 | * - finishSlide(SlideDirection.Prev)
57 | *
58 | * 假如从页面【A】试图滑动到页面【B】,但距离或者速度不够,所以放手后回弹到【A】,触发的回调是:
59 | * - canSlideTo(SlideDirection.Next)
60 | * - onBindView(viewHolder【B】, SlideDirection.Next)
61 | * - onViewDismiss(viewHolder【B】, SlideDirection.Next)
62 | * - finishSlide(SlideDirection.Origin)
63 | */
64 | interface SlideAdapter {
65 |
66 | /**
67 | * Whether it can slide in the direction of [direction].
68 | *
69 | * —————————————————————————————————————————————————————————————————————————
70 | * 能否向 [direction] 的方向滑动。
71 | *
72 | * @param direction 滑动的方向
73 | *
74 | * @return 返回 true 表示可以滑动, false 表示不可滑动。
75 | * 如果有嵌套其他外层滑动布局(比如下拉刷新),当且仅当返回 false 时会触发外层的嵌套滑动。
76 | */
77 | fun canSlideTo(direction: SlideDirection): Boolean
78 |
79 | /**
80 | * Called when [SlidableLayout] needs a new [ViewHolder] to represent an item.
81 | *
82 | * ————————————————————————————————————————————————————————————————————————————————
83 | * 创建持有 [View] 的 [SlideViewHolder] 。
84 | * 一般来说,该方法会在 [SlidableLayout.setAdapter] 方法调用时触发一次,创建当前显示的 [View],
85 | * 会在首次开始滑动时触发第二次,创建滑动目标的 [View]。
86 | */
87 | fun onCreateViewHolder(context: Context, parent: ViewGroup, inflater: LayoutInflater): ViewHolder
88 |
89 | /**
90 | * Called by [SlidableLayout] when view represented by [viewHolder] starts to slide to visible.
91 | * This method should update the contents of the [viewHolder] to reflect the
92 | * item at the [direction].
93 | *
94 | * ————————————————————————————————————————————————————————————————————————————————
95 | * 当 [View] 开始滑动到可见时触发,在这个方法中实现数据和 [View] 的绑定。
96 | *
97 | * @param viewHolder 持有 [View] 的 [SlideViewHolder]
98 | * @param direction 滑动的方向
99 | */
100 | fun onBindView(viewHolder: ViewHolder, direction: SlideDirection)
101 |
102 | /**
103 | * Called by [SlidableLayout] when view represented by [viewHolder] completely appears.
104 | *
105 | * ——————————————————————————————————————————————————————————————————————————————
106 | * 当 [View] 完全出现时触发。
107 | * 这个时机可能是 [SlidableLayout.setAdapter] 后 [View] 的第一次初始化,
108 | * 也可能是完成一次滑动,在 [finishSlide] 后 **而且** 滑到了一个新的 [View]。
109 | *
110 | * 也就是说,如果 [finishSlide] 的 [SlideDirection] 是 [SlideDirection.Origin] ,
111 | * 也就是滑动回弹到本来的界面上,是不会触发 [onViewComplete] 的。
112 | *
113 | * 在这个方法中实现当 [View] 第一次完全出现时才做的业务。比如开始播放视频。
114 | *
115 | * @param viewHolder 持有 [View] 的 [SlideViewHolder]
116 | */
117 | fun onViewComplete(viewHolder: ViewHolder, direction: SlideDirection) {}
118 |
119 | /**
120 | * Called by [SlidableLayout] when a view created by this
121 | * adapter has been dismissed.
122 | * Do some cleaning in this method.
123 | *
124 | * ——————————————————————————————————————————————————————————————————————————————
125 | * 当滑动完成时,离开的 [View] 会触发,在这个方法中实现对 [View] 的清理。
126 | *
127 | * @param viewHolder 持有 [View] 的 [SlideViewHolder]
128 | * @param direction 滑动的方向
129 | */
130 | fun onViewDismiss(viewHolder: ViewHolder, parent: ViewGroup, direction: SlideDirection) {}
131 |
132 | /**
133 | * Called by [SlidableLayout] when view finishes sliding.
134 | *
135 | * ————————————————————————————————————————————————————————————————————————————————
136 | * 当滑动完成时触发。
137 | *
138 | * @param direction 滑动的方向
139 | */
140 | fun finishSlide(dismissViewHolder: ViewHolder, visibleViewHolder: ViewHolder, direction: SlideDirection) {}
141 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/yy/mobile/widget/SlideDirection.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.widget
2 |
3 | /**
4 | * @author YvesCheung
5 | * 2019-10-21
6 | */
7 | enum class SlideDirection {
8 | /**
9 | * move to next
10 | * 滑到下一个
11 | */
12 | Next {
13 | override fun moveTo(index: Int): Int = index + 1
14 | },
15 | /**
16 | * move to previous
17 | * 滑到上一个
18 | */
19 | Prev {
20 | override fun moveTo(index: Int): Int = index - 1
21 | },
22 | /**
23 | * back to the origin
24 | * 回到原点
25 | */
26 | Origin {
27 | override fun moveTo(index: Int): Int = index
28 | };
29 |
30 | /**
31 | * 计算index的变化
32 | */
33 | abstract fun moveTo(index: Int): Int
34 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/yy/mobile/widget/SlideFragmentAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.widget
2 |
3 | import android.content.Context
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import android.widget.FrameLayout
7 | import androidx.core.view.ViewCompat
8 | import androidx.fragment.app.Fragment
9 | import androidx.fragment.app.FragmentManager
10 |
11 | abstract class SlideFragmentAdapter(private val fm: FragmentManager) : SlideAdapter {
12 |
13 | private val viewHolderList = mutableListOf()
14 |
15 | /**
16 | * Called by [SlidableLayout] to create the content [Fragment].
17 | *
18 | * ————————————————————————————————————————————————————————————————————————————————
19 | * 创建要显示的 [Fragment]。
20 | * 一般来说,该方法会在 [SlidableLayout.setAdapter] 调用时触发一次,创建当前显示的 [Fragment],
21 | * 会在首次开始滑动时触发第二次,创建滑动目标的 [Fragment]。
22 | */
23 | abstract fun onCreateFragment(context: Context): Fragment
24 |
25 | protected open fun onBindFragment(fragment: Fragment, direction: SlideDirection) {}
26 |
27 | protected open fun finishSlide(direction: SlideDirection) {}
28 |
29 | final override fun onCreateViewHolder(context: Context, parent: ViewGroup, inflater: LayoutInflater): FragmentViewHolder {
30 | val viewGroup = FrameLayout(context)
31 | viewGroup.id = ViewCompat.generateViewId()
32 | val fragment = onCreateFragment(context)
33 | fm.beginTransaction().add(viewGroup.id, fragment).commitAllowingStateLoss()
34 | val viewHolder = FragmentViewHolder(viewGroup, fragment)
35 | viewHolderList.add(viewHolder)
36 | return viewHolder
37 | }
38 |
39 | final override fun onBindView(viewHolder: FragmentViewHolder, direction: SlideDirection) {
40 | val fragment = viewHolder.f
41 | fm.beginTransaction().show(fragment).commitAllowingStateLoss()
42 | viewHolder.view.post {
43 | onBindFragment(fragment, direction)
44 | if (fragment is SlidableUI) {
45 | fragment.startVisible(direction)
46 | }
47 | }
48 | }
49 |
50 | final override fun onViewComplete(viewHolder: FragmentViewHolder, direction: SlideDirection) {
51 | val fragment = viewHolder.f
52 | fragment.setMenuVisibility(true)
53 | fragment.userVisibleHint = true
54 | if (fragment is SlidableUI) {
55 | fragment.completeVisible(direction)
56 | }
57 |
58 | viewHolderList.filter { it != viewHolder }.forEach {
59 | val otherFragment = it.f
60 | otherFragment.setMenuVisibility(false)
61 | otherFragment.userVisibleHint = false
62 | }
63 | }
64 |
65 | final override fun onViewDismiss(viewHolder: FragmentViewHolder, parent: ViewGroup, direction: SlideDirection) {
66 | val fragment = viewHolder.f
67 | fm.beginTransaction().hide(fragment).commitAllowingStateLoss()
68 | if (fragment is SlidableUI) {
69 | fragment.invisible(direction)
70 | }
71 | }
72 |
73 | final override fun finishSlide(dismissViewHolder: FragmentViewHolder, visibleViewHolder: FragmentViewHolder, direction: SlideDirection) {
74 | finishSlide(direction)
75 | if (dismissViewHolder.f is SlidableUI) {
76 | dismissViewHolder.f.preload(direction)
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/yy/mobile/widget/SlideViewAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.widget
2 |
3 | import android.content.Context
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 |
8 | /**
9 | * Base class for an Adapter.
10 | */
11 | abstract class SlideViewAdapter : SlideAdapter {
12 |
13 | /**
14 | * Called by [SlidableLayout] to create the view.
15 | * Generally speaking, this method will be called, once the
16 | * [SlidableLayout.setAdapter] method is called, to create the
17 | * currently [View].
18 | * The second time this method is called is when the first sliding
19 | * event occurs, to create the sliding target's [View].
20 | *
21 | * ———————————————————————————————————————————————————————————————————————————
22 | * 创建 [View] 。
23 | * 一般来说,该方法会在 [SlidableLayout.setAdapter] 方法调用时触发一次,
24 | * 创建当前显示的 [View],
25 | * 会在首次开始滑动时触发第二次,创建滑动目标的 [View]。
26 | */
27 | protected abstract fun onCreateView(context: Context, parent: ViewGroup, inflater: LayoutInflater): View
28 |
29 | /**
30 | * Called by [SlidableLayout] when [view] starts to slide to visible.
31 | * This method should update the contents of the [view] to reflect the
32 | * item at the [direction].
33 | *
34 | * ——————————————————————————————————————————————————————————————————————————————
35 | * 当 [view] 开始滑动到可见时触发,在这个方法中实现数据和 [view] 的绑定。
36 | *
37 | * @param direction 滑动的方向
38 | */
39 | protected abstract fun onBindView(view: View, direction: SlideDirection)
40 |
41 | /**
42 | * Called by [SlidableLayout] when view finishes sliding.
43 | *
44 | * ————————————————————————————————————————————————————————————————————————————————
45 | * 当滑动完成时触发。
46 | *
47 | * @param direction 滑动的方向
48 | */
49 | protected open fun finishSlide(direction: SlideDirection) {}
50 |
51 | /**
52 | * Called by [SlidableLayout] when view finishes sliding.
53 | *
54 | * ————————————————————————————————————————————————————————————————————————————————
55 | * 当滑动完成时触发。
56 | *
57 | * @param direction 滑动的方向
58 | */
59 | protected open fun finishSlide(dismissView: View, visibleView: View, direction: SlideDirection) {}
60 |
61 | /**
62 | * Called by [SlidableLayout] when a view created by this
63 | * adapter has been dismissed.
64 | * Do some cleaning in this method.
65 | *
66 | * ————————————————————————————————————————————————————————————————————————————————
67 | * 当滑动完成时,离开的 [view] 会触发,在这个方法中实现对 [view] 的清理。
68 | *
69 | * @param direction 滑动的方向
70 | */
71 | protected open fun onViewDismiss(view: View, parent: ViewGroup, direction: SlideDirection) {
72 | parent.removeView(view)
73 | }
74 |
75 | /**
76 | * Called by [SlidableLayout] when [view] completely appears.
77 | *
78 | * ——————————————————————————————————————————————————————————————————————————————————————
79 | * 当 [view] 完全出现时触发。
80 | * 这个时机可能是 [SlidableLayout.setAdapter] 后 [view] 的第一次初始化,
81 | * 也可能是完成一次滑动,在 [finishSlide] 后 **而且** 滑到了一个新的 [view]。
82 | *
83 | * 也就是说,如果 [finishSlide] 的 [SlideDirection] 是 [SlideDirection.Origin] ,
84 | * 也就是滑动回弹到本来的界面上,是不会触发 [onViewComplete] 的。
85 | *
86 | * 在这个方法中实现当 [view] 第一次完全出现时才做的业务。比如开始播放视频。
87 | */
88 | protected open fun onViewComplete(view: View, direction: SlideDirection) {}
89 |
90 | final override fun onCreateViewHolder(context: Context, parent: ViewGroup, inflater: LayoutInflater): SlideViewHolder {
91 | return SlideViewHolder(onCreateView(context, parent, inflater))
92 | }
93 |
94 | final override fun onBindView(viewHolder: SlideViewHolder, direction: SlideDirection) {
95 | val v = viewHolder.view
96 | onBindView(v, direction)
97 | if (v is SlidableUI) {
98 | v.startVisible(direction)
99 | }
100 | }
101 |
102 | final override fun onViewDismiss(viewHolder: SlideViewHolder, parent: ViewGroup, direction: SlideDirection) {
103 | val v = viewHolder.view
104 | if (v is SlidableUI) {
105 | v.invisible(direction)
106 | }
107 | onViewDismiss(v, parent, direction)
108 | }
109 |
110 | final override fun onViewComplete(viewHolder: SlideViewHolder, direction: SlideDirection) {
111 | val v = viewHolder.view
112 | onViewComplete(v, direction)
113 | if (v is SlidableUI) {
114 | v.completeVisible(direction)
115 | }
116 | }
117 |
118 | final override fun finishSlide(dismissViewHolder: SlideViewHolder, visibleViewHolder: SlideViewHolder, direction: SlideDirection) {
119 | finishSlide(direction)
120 | finishSlide(dismissViewHolder.view, visibleViewHolder.view, direction)
121 | if (dismissViewHolder.view is SlidableUI) {
122 | dismissViewHolder.view.preload(direction)
123 | }
124 | }
125 | }
--------------------------------------------------------------------------------
/lib/src/main/java/com/yy/mobile/widget/SlideViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.yy.mobile.widget
2 |
3 | import android.view.View
4 |
5 | open class SlideViewHolder(val view: View)
6 |
--------------------------------------------------------------------------------
/lib/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/lib/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | lib
3 |
4 |
--------------------------------------------------------------------------------
/material/NestedScroll.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/material/NestedScroll.gif
--------------------------------------------------------------------------------
/material/OppositeNestedScroll.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/material/OppositeNestedScroll.gif
--------------------------------------------------------------------------------
/material/SlidableLayoutHorizontal.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/material/SlidableLayoutHorizontal.gif
--------------------------------------------------------------------------------
/material/slidableLayout.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YvesCheung/SlidableLayout/d3b4f9880fc9e24006f395d484aa9cf5ffd5c25d/material/slidableLayout.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':lib'
2 |
--------------------------------------------------------------------------------