├── .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 | [![License](https://img.shields.io/badge/License%20-Apache%202-337ab7.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![](https://jitpack.io/v/YvesCheung/SlidableLayout.svg)](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 | [![License](https://img.shields.io/badge/License%20-Apache%202-337ab7.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![](https://jitpack.io/v/YvesCheung/SlidableLayout.svg)](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 | --------------------------------------------------------------------------------