├── .gitignore ├── LICENSE ├── README.md ├── README_NESTEDSCROLL.md ├── build.gradle ├── gradle.properties ├── gradle ├── publish.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img ├── TopoSort.png ├── TouchEventBus.png ├── demoPreview.gif ├── dispatch.png ├── nestedScrollPreview.gif ├── stickNestedLayout.png ├── touchEventBusInYYPreview.gif └── touchOrder.png ├── jitpack.yml ├── nestedtouch ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── mobile │ │ └── yy │ │ └── com │ │ └── nestedtouch │ │ ├── StickyNestedLayout.kt │ │ └── StickyNestedLayoutException.kt │ └── res │ └── values │ ├── ids.xml │ └── strings.xml ├── nestedtouchsample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── mobile │ │ └── yy │ │ └── com │ │ └── nestedtouchsample │ │ ├── BlankFragment.kt │ │ ├── DetailFragment.kt │ │ ├── MainActivity.kt │ │ └── WebFragment.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── avatar.png │ ├── ic_launcher_background.xml │ └── mn_qrcode_download_yy.png │ ├── layout │ ├── activity_main.xml │ ├── fragment_blank.xml │ ├── moment_head_view.xml │ └── simple_title_bar.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── settings.gradle ├── toucheventbus ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── mobile │ │ └── yy │ │ └── com │ │ └── toucheventbus │ │ ├── AbstractTouchEventHandler.java │ │ ├── AttachToViewTouchEventHandler.java │ │ ├── InterceptClickHandler.java │ │ ├── TouchEventBus.java │ │ ├── TouchEventHandler.java │ │ ├── TouchEventHandlerContainer.java │ │ ├── TouchEventHandlerUtil.java │ │ └── TouchViewHolder.java │ └── res │ └── values │ └── strings.xml └── touchsample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── java └── mobile │ └── yy │ └── com │ └── touchsample │ ├── App.kt │ ├── inject │ └── Injector.kt │ ├── model │ ├── Tab.kt │ └── TabRepo.kt │ ├── presenter │ ├── MainPagePresenter.kt │ ├── MainTabPresenter.kt │ └── SubTabPresenter.kt │ ├── touch │ ├── BackgroundImageTouchHandler.kt │ ├── MenuTouchHandler.kt │ ├── SlidingTabTouchHandler.kt │ ├── TabTouchHandler.kt │ └── ZoomTextTouchHandler.kt │ ├── ui │ ├── BackgroundFragment.kt │ ├── FakeMenu.kt │ ├── MainActivity.kt │ ├── MainTabFragment.kt │ ├── SubTabFragment.kt │ └── ZoomUi.kt │ └── util │ └── FragmentEnterHelper.kt └── res ├── drawable-v24 └── ic_launcher_foreground.xml ├── drawable ├── ic_launcher_background.xml ├── main_background.jpg ├── main_background2.jpg └── main_background3.jpg ├── layout ├── activity_main.xml ├── fragment_main_tab.xml └── fragment_sub_tab.xml ├── mipmap-anydpi-v26 ├── ic_launcher.xml └── ic_launcher_round.xml ├── mipmap-hdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-mdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xhdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xxhdpi ├── ic_launcher.png └── ic_launcher_round.png ├── mipmap-xxxhdpi ├── ic_launcher.png └── ic_launcher_round.png └── values ├── colors.xml ├── strings.xml └── styles.xml /.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 | # 滑动冲突解决方案 2 | 3 | --- 4 | # 非嵌套滑动 | [嵌套滑动][1] 5 | 6 | > Android 系统的触摸事件分发总是从父布局开始分发,从最顶层的子 View 开始处理,这种特性有时候会限制了我们一些很复杂的交互设计。 7 | > 8 | > **``TouchEventBus``** 致力于解决非嵌套的滑动冲突,比如多个 **在同一层级** 的``Fragment`` 对触摸事件的处理:触摸事件会先到达顶层 ``Fragment`` 的 ``onTouch`` 方法,然后逐层判断是否消费,在都不消费的情况下才到达底层的 ``Fragment`` 。而且这些层级互不嵌套,没有形成 parent 和 child 的关系,意味着想通过 ``onInterceptTouchEvent()`` 或者 ``requestDisallowInterceptTouchEvent()`` 方法来调整事件分发都是不可能的。 :full_moon_with_face: 9 | 10 | ## 同级视图的触摸事件 11 | 12 | 下面是手机YY的开播预览页: 13 | 14 | ![YY预览页][2] 15 | 16 | 在这个页面上有很多对触摸事件的处理,包括且不限于: 17 | 18 | - 在屏幕上点击,会触发摄像头的聚焦(黄色框出现的地方) 19 | - 双指缩放,会触发摄像头的缩放 20 | - 左右滑动,可以切换 ``ViewPager`` ,从“直播”和“玩游戏”两个选项卡之间切换 21 | - “玩游戏”选项卡上的列表可以滑动 22 | - “直播”选项卡上的控件可以点击(开播按钮,添加图片…) 23 | - 由于预览页和开播页是同一个 ``Activity`` ,所以这个 ``Activity`` 上还有很多开播后的 ``Fragment``,比如公屏等等也有触摸事件 24 | 25 | 从视觉上可以判断出View Tree的层级以及对触摸处理的层级: 26 | 27 | ![处理顺序][3] 28 | 29 | 图左侧是 UI 的层级,上层是一些按钮控件和 ``ViewPager`` ,下层是视频流展示的 ``Fragment``。右边是触摸事件处理的层级,双指缩放/View点击/聚焦点击需要在 ``ViewPager``上面,否则都会被 ``ViewPager`` 消费掉,但是 ``ViewPager`` 的 UI 层级又比视频的 ``Fragment`` 要高。这就是非嵌套的滑动冲突的核心矛盾: 30 | 31 | > **业务逻辑的层级** 与 **用户看到的UI层级** 不一致 32 | 33 | ## 对触摸事件的重新分发 34 | 35 | 手机YY直播间中的 ``Fragment`` 非常多,而且因为插件化的原因,各个业务插件可以动态地往直播间添加/移除自己业务的 ``Fragment`` ,这些 ``Fragment`` 层级相同互不嵌套,有自己比较独立的业务逻辑,也会有点击/滑动等事件处理的需求。但由于业务场景复杂,``Fragment`` 的上下层级顺序也会动态改变,这就很容易导致一些 ``Fragment`` 一直收不到触摸事件或者在切换业务模板的时候触摸事件被其他业务消费。 36 | 37 | ``TouchEventBus`` 用于这种场景下对触摸事件进行重新分发,我们可以随心所欲地决定业务逻辑的层级顺序。 38 | 39 | ![TouchEventBus重新分发触摸事件][4] 40 | 41 | 每个手势的处理就是一个 ``TouchEventHandler``,比如镜头的缩放是 **CameraZoomHandler** ,镜头的聚焦点击是 **CameraClickHandler** ,``ViewPager`` 滑动是 **PreviewSlideHandler** ,然后为这些 Handler 重新排序,按照业务的需要来传递 ``MotionEvent`` 。然后是 ``TouchEventHandler`` 和ui的对应关系:通过Handler的 ``attach`` / ``dettach`` 方法来绑定/解绑对应的 ui 。而 ui 可以是一个具体的 ``Fragment``,也可以是一个抽象的接口,一个对触摸事件作出响应的业务。 42 | 43 | 比如开播预览页的聚焦点击处理,先是定义ui的接口: 44 | 45 | ```Java 46 | public interface CameraClickView { 47 | /** 48 | * 在指定位置为中心显示一个黄色矩形的聚焦框 49 | * 50 | * @param x 手指触摸坐标x 51 | * @param y 手指触摸坐标y 52 | */ 53 | void showVideoClickFocus(float x, float y); 54 | 55 | /** 56 | * 给VideoSdk传递触摸事件,让其在指定坐标进行摄像头聚焦 57 | * 58 | * @param e 触摸事件 59 | */ 60 | void onTouch(MotionEvent e); 61 | } 62 | ``` 63 | 64 | 然后是 ``TouchEventHandler`` 的定义: 65 | 66 | ```Java 67 | public class CameraClickHandler extends TouchEventHandler { 68 | 69 | private boolean performClick = false; 70 | //... 71 | 72 | @Override 73 | public boolean onTouch(@NonNull CameraClickView v, MotionEvent e, boolean hasBeenIntercepted) { 74 | super.onTouch(v, e, hasBeenIntercepted); 75 | if (!isCameraFocusEnable()) { //一些特殊业务需要禁止摄像头聚焦 76 | return false; 77 | } 78 | //通过MotionEvent判断performClick是否为true 79 | switch (e.getAction()) { 80 | case MotionEvent.ACTION_DOWN: 81 | //... 82 | break; 83 | case MotionEvent.ACTION_MOVE: 84 | //... 85 | break; 86 | case MotionEvent.ACTION_UP: 87 | //... 88 | break; 89 | default: 90 | break; 91 | } 92 | 93 | if (performClick) { //认为是点击行为,调用ui的接口 94 | v.showVideoClickFocus(e.getRawX(), e.getRawY()); 95 | v.onTouch(e); 96 | } 97 | return performClick; //点击的时候消费掉触摸事件 98 | } 99 | } 100 | ``` 101 | 102 | 最后是 ``TouchEventHandler`` 与 ui 的对应的绑定 103 | 104 | ```Java 105 | public class MobileLiveVideoComponent extends Fragment implements CameraClickView{ 106 | 107 | @Override 108 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 109 | //... 110 | //CameraClickHandler与当前Fragment绑定 111 | TouchEventBus.of(CameraClickHandler.class).attach(this); 112 | } 113 | 114 | @Override 115 | public void onDestroyView() { 116 | //... 117 | //CameraClickHandler与当前Fragment解绑 118 | TouchEventBus.of(CameraClickHandler.class).dettach(this); 119 | } 120 | 121 | @Override 122 | public void showVideoClickFocus(float x, float y) { 123 | //todo: 展示一个黄色框ui 124 | } 125 | 126 | @Override 127 | public void onTouch(MotionEvent e) { 128 | //todo: 调用SDK的摄像头聚焦 129 | } 130 | } 131 | ``` 132 | 133 | 当用户对ui的进行手势操作时,``MotionEvent`` 就会沿着 ``TouchEventBus`` 里面的顺序进行分发。如果在 **CameraClickHandler** 之前没有别的 Handler 把事件消费掉,那么就能在 ``onTouch`` 方法进行处理,然后在 ui 作出响应。 134 | 135 | ## 事件的分发顺序 136 | 137 | 多个 ``TouchEventHandler`` 之间需要定义一个分发的顺序,最先接收到触摸事件的 Handler 可以拦截后面的 Handler。在顺序的定义上,很难固定一条绝对的分发路线,因为随着直播间模版的切换,``Fragment`` 的层级可能会产生变化。 138 | 所以 ``TouchEventBus`` 使用相对的顺序定义。每个 Handler 可以决定要拦截哪些其他的 Handler。比如要把 **CameraClickHandler** 排在其他几个Handler前面: 139 | 140 | ```Java 141 | public class CameraClickHandler extends AbstractTouchEventHandler { 142 | //... 143 | 144 | @Override 145 | public boolean onTouch(@NonNull CameraClickView v, MotionEvent e, boolean hasBeenIntercepted) { 146 | //... 147 | } 148 | 149 | /** 150 | * 定义哪些Handler需要排在我的后面 151 | **/ 152 | @Override 153 | protected void defineNextHandlers(@NonNull List>>> handlers) { 154 | //下面的Handler都会在CameraClickHandler后面,但他们之间的顺序还未定义 155 | handlers.add(CameraZoomHandler.class); 156 | handlers.add(MediaMultiTouchHandler.class); 157 | handlers.add(PreviewSlideHandler.class); 158 | handlers.add(VideoControlTouchEventHandler.class); 159 | } 160 | } 161 | ``` 162 | 163 | 每个 Handler 都会指定排在自己后面的 Handler,从而形成一张图。通过拓扑排序我们就能动态地获得一条分发路径。下图的箭头指向 “A->B” 表示A需要排在B的前面: 164 | 165 | ![拓扑排序][5] 166 | 167 | 在直播间模版切换的时候,任何一个 Handler 都可以动态地添加到这个图当中,也可以从这个图中随时移除,不会影响其他业务的正常进行。 168 | 169 | ## 嵌套的视图用 Android 系统的触摸分发 170 | 171 | 互不嵌套的 ``Fragment`` 层级才需要使用 ``TouchEventBus``,``Fragment`` 内部用 Android 默认的触摸事件分发。如下图:红色箭头部分为 ``TouchEventBus`` 的分发,按 Handler 的拓扑顺序进行逐层调用。蓝色箭头部分为 ``Fragment`` 内部 ViewTree 的分发,完全依照 Android 系统的分发顺序,即从父布局向子视图分发,子视图向父布局逐层决定是否消费。 172 | 173 | ![触摸事件分发][6] 174 | 175 | ## 使用例子 176 | 177 | 运行本工程的 ***TouchSample*** 模块,是一个使用 ``TouchEventBus`` 的简单 Demo 。 178 | 179 | ![TouchSample][7] 180 | 181 | - 单指左右滑动切换选项卡 182 | - 双指缩放中间的"Tab%_subTab%"文本框 183 | - 双指左右滑动切换背景图 184 | - 滑动屏幕左侧拉出侧边面板 185 | 186 | ui的层级:Activity -> 背景图 -> 侧边面板 -> 选项卡 -> 文本框 187 | 188 | 触摸处理的顺序:侧边面板 -> 文本缩放 -> 背景图滑动 -> 底部导航点击 -> 选项卡滑动 189 | 190 | > 这里还做了一个操作是:让底部导航点击不消费触摸事件。所以你可以在底部的导航栏区域上左右滑动,切换的是一级Tab。而在背景图区域左右滑动,切换的是二级Tab。 191 | 192 | ## 配置 193 | 194 | 1. 在项目 build.gradle 添加仓库地址 195 | 196 | ```groovy 197 | allprojects { 198 | repositories { 199 | maven { url 'https://jitpack.io' } 200 | } 201 | } 202 | ``` 203 | 204 | 2. 对应模块添加依赖 205 | 206 | ```groovy 207 | dependencies { 208 | compile 'com.github.YvesCheung.TouchEventBus:toucheventbus:1.4.3' 209 | } 210 | ``` 211 | 212 | ## 许可证 213 | 214 | Copyright 2018 YvesCheung 215 | 216 | Licensed under the Apache License, Version 2.0 (the "License"); 217 | you may not use this file except in compliance with the License. 218 | You may obtain a copy of the License at 219 | 220 | http://www.apache.org/licenses/LICENSE-2.0 221 | 222 | Unless required by applicable law or agreed to in writing, software 223 | distributed under the License is distributed on an "AS IS" BASIS, 224 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 225 | See the License for the specific language governing permissions and 226 | limitations under the License. 227 | 228 | 229 | 230 | [1]: https://github.com/YvesCheung/TouchEventBus/blob/master/README_NESTEDSCROLL.md 231 | [2]: https://raw.githubusercontent.com/YvesCheung/TouchEventBus/master/img/touchEventBusInYYPreview.gif 232 | [3]: https://raw.githubusercontent.com/YvesCheung/TouchEventBus/master/img/touchOrder.png 233 | [4]: https://raw.githubusercontent.com/YvesCheung/TouchEventBus/master/img/TouchEventBus.png 234 | [5]: https://raw.githubusercontent.com/YvesCheung/TouchEventBus/master/img/TopoSort.png 235 | [6]: https://raw.githubusercontent.com/YvesCheung/TouchEventBus/master/img/dispatch.png 236 | [7]: https://raw.githubusercontent.com/YvesCheung/TouchEventBus/master/img/demoPreview.gif 237 | -------------------------------------------------------------------------------- /README_NESTEDSCROLL.md: -------------------------------------------------------------------------------- 1 | # 滑动冲突解决方案 2 | 3 | --- 4 | # [非嵌套滑动][1] | 嵌套滑动 5 | 6 | > 相比起非嵌套滑动的自定义分发事件的方案,嵌套滑动冲突有比较成熟的 Google 解决方案:**[NestedScrolling][2]** 。 7 | 8 | ## 三层嵌套的滑动冲突 9 | 10 | ![嵌套滑动][3] 11 | 12 | UI 层级如下: 13 | 14 | * 最外层(底层)是一个具有下拉刷新功能的布局 15 | * 中层是本库提供的控件 **``StickNestedLayout``** ,解决导航栏吸顶,以及内外层的滑动冲突 16 | * 最内层(上层)依次是 *headView* / *navView* / *contentView* ,对应上部的内容区域,中部的吸顶导航栏区域,下部的 ``ViewPager`` 区域 17 | * ``ViewPager`` 里面有 ``RecyclerView`` 列表 18 | 19 | ![StickNestedLayout][4] 20 | 21 | ## 使用 22 | 23 | 你可以通过 ``StickNestedLayout`` 轻松地完成这种页面。 24 | 25 | ```XML 26 | 30 | 31 | 34 | 35 | 38 | 39 | 43 | 44 | 48 | 49 | 50 | ``` 51 | 52 | > 其中 *headView* / *navView* / *contentView* 的id必须为 **stickyHeadView** / **stickyNavView** / **stickyContentView** 53 | 54 | 可以通过运行工程 *nestedtouchsample* 查看具体代码。例子中涉及的其他第三方库有下拉刷新控件 [SmartRefreshLayout][5] 和导航栏 [PagerSlidingTabStrip][6] ,部分参考 [StickNavLayout][7] 55 | 56 | ## 配置 57 | 58 | 最新版本: [![](https://jitpack.io/v/YvesCheung/TouchEventBus.svg)](https://jitpack.io/#YvesCheung/TouchEventBus) 59 | 60 | 1. 项目build.gradle添加 61 | 62 | ```Groovy 63 | allprojects { 64 | repositories { 65 | ... 66 | maven { url 'https://jitpack.io' } 67 | } 68 | } 69 | ``` 70 | 2. 对应模块添加依赖 71 | 72 | ```Groovy 73 | dependencies { 74 | compile 'com.github.YvesCheung.TouchEventBus:nestedtouch:x.y.z' 75 | } 76 | ``` 77 | 78 | ## 许可证 79 | 80 | Copyright 2018 YvesCheung 81 | 82 | Licensed under the Apache License, Version 2.0 (the "License"); 83 | you may not use this file except in compliance with the License. 84 | You may obtain a copy of the License at 85 | 86 | http://www.apache.org/licenses/LICENSE-2.0 87 | 88 | Unless required by applicable law or agreed to in writing, software 89 | distributed under the License is distributed on an "AS IS" BASIS, 90 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 91 | See the License for the specific language governing permissions and 92 | limitations under the License. 93 | 94 | 95 | [1]: https://github.com/YvesCheung/TouchEventBus/blob/master/README.md 96 | [2]: https://developer.android.com/reference/android/support/v4/view/NestedScrollingParent 97 | [3]: https://raw.githubusercontent.com/YvesCheung/TouchEventBus/master/img/nestedScrollPreview.gif 98 | [4]: https://raw.githubusercontent.com/YvesCheung/TouchEventBus/master/img/stickNestedLayout.png 99 | [5]: https://github.com/scwang90/SmartRefreshLayout 100 | [6]: https://github.com/ta893115871/PagerSlidingTabStrip 101 | [7]: https://github.com/hongyangAndroid/Android-StickyNavLayout 102 | -------------------------------------------------------------------------------- /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.30' 5 | 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:7.0.4' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | jcenter() 20 | mavenCentral() 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 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | android.enableJetifier=true 13 | android.useAndroidX=true 14 | org.gradle.jvmargs=-Xmx1536m 15 | 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true 20 | -------------------------------------------------------------------------------- /gradle/publish.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | apply plugin: 'signing' 3 | 4 | group = "io.github.yvescheung" // Maven Group ID for the artifact 5 | version = libraryVersion 6 | final String developerId = 'YvesCheung' 7 | final String developerName = 'YvesCheung' 8 | final String developerEmail = '975135274@qq.com' 9 | final String licenseName = 'The Apache License, Version 2.0' 10 | final String licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0' 11 | 12 | if (project.hasProperty("android")) { // Android libraries 13 | task sourcesJar(type: Jar) { 14 | classifier = 'sources' 15 | getArchiveBaseName().set(libraryName) 16 | from android.sourceSets.main.java.srcDirs 17 | } 18 | task androidNativeJar(type: Jar) { 19 | classifier = 'so' 20 | from(new File(buildDir, 'libs')) 21 | include("**/*.so") 22 | } 23 | ext.libraryOutputFile = "$buildDir/outputs/aar/${project.getName()}-release.aar" 24 | } else { // Java libraries 25 | task sourcesJar(type: Jar, dependsOn: classes) { 26 | classifier = 'sources' 27 | getArchiveBaseName().set(libraryName) 28 | from sourceSets.main.allSource 29 | } 30 | ext.libraryOutputFile = "$buildDir/libs/${project.getName()}-${version}.jar" 31 | } 32 | println("artifect: $libraryOutputFile") 33 | 34 | /** 35 | * ossrhUsername 36 | * ossrhPassword 37 | * signing.keyId 38 | * signing.password 39 | * signing.secretKeyRingFile 40 | * Variables defined in local.properties 41 | */ 42 | Properties properties = new Properties() 43 | File localProperties = project.rootProject.file('local.properties') 44 | if (localProperties.exists()) { 45 | properties.load(localProperties.newDataInputStream()) 46 | } 47 | 48 | afterEvaluate { 49 | publishing { 50 | if (properties['ossrhUsername'] != null) { 51 | repositories { 52 | maven { 53 | // You only need this if you want to publish snapshots, otherwise just set the URL 54 | // to the release repo directly 55 | url = version.endsWith('SNAPSHOT') 56 | ? "https://s01.oss.sonatype.org/content/repositories/snapshots/" 57 | : "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" 58 | 59 | // The username and password we've fetched earlier 60 | credentials { 61 | username properties['ossrhUsername'] 62 | password properties['ossrhPassword'] 63 | } 64 | } 65 | } 66 | } 67 | 68 | publications { 69 | MyPublication(MavenPublication) { 70 | from components.release 71 | groupId group 72 | artifactId libraryName 73 | artifact sourcesJar 74 | version libraryVersion 75 | pom { 76 | name = libraryName 77 | description = libraryDescription 78 | url = siteUrl 79 | 80 | developers { 81 | developer { 82 | id = developerId 83 | name = developerName 84 | email = developerEmail 85 | } 86 | } 87 | 88 | scm { 89 | connection = gitUrl 90 | developerConnection = gitUrl 91 | url = siteUrl 92 | } 93 | 94 | licenses { 95 | license { 96 | name = licenseName 97 | url = licenseUrl 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | if (properties['signing.keyId'] != null) { 106 | ext['signing.keyId'] = properties['signing.keyId'] 107 | ext['signing.password'] = properties['signing.password'] 108 | ext['signing.secretKeyRingFile'] = properties['signing.secretKeyRingFile'] 109 | signing { 110 | sign publishing.publications 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Mar 27 19:52:10 CST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /img/TopoSort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/img/TopoSort.png -------------------------------------------------------------------------------- /img/TouchEventBus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/img/TouchEventBus.png -------------------------------------------------------------------------------- /img/demoPreview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/img/demoPreview.gif -------------------------------------------------------------------------------- /img/dispatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/img/dispatch.png -------------------------------------------------------------------------------- /img/nestedScrollPreview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/img/nestedScrollPreview.gif -------------------------------------------------------------------------------- /img/stickNestedLayout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/img/stickNestedLayout.png -------------------------------------------------------------------------------- /img/touchEventBusInYYPreview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/img/touchEventBusInYYPreview.gif -------------------------------------------------------------------------------- /img/touchOrder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/img/touchOrder.png -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 -------------------------------------------------------------------------------- /nestedtouch/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /nestedtouch/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 30 8 | 9 | defaultConfig { 10 | minSdkVersion 14 11 | targetSdkVersion 30 12 | } 13 | 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | 21 | compileOptions { 22 | sourceCompatibility JavaVersion.VERSION_1_8 23 | targetCompatibility JavaVersion.VERSION_1_8 24 | } 25 | 26 | kotlinOptions { 27 | jvmTarget = JavaVersion.VERSION_1_8.toString() 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation 'androidx.core:core:1.5.0' 33 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 34 | } 35 | 36 | tasks.withType(JavaCompile) { 37 | options.encoding = "UTF-8" 38 | } 39 | 40 | ext { 41 | libraryName = 'nestedtouch' 42 | libraryDescription = 'A solution for NestedScrolling' 43 | siteUrl = 'https://github.com/YvesCheung/TouchEventBus' 44 | gitUrl = 'https://github.com/YvesCheung/TouchEventBus.git' 45 | libraryVersion = version 46 | } 47 | 48 | apply from: '../gradle/publish.gradle' -------------------------------------------------------------------------------- /nestedtouch/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 | -------------------------------------------------------------------------------- /nestedtouch/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /nestedtouch/src/main/java/mobile/yy/com/nestedtouch/StickyNestedLayout.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.nestedtouch 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import androidx.annotation.IdRes 6 | import androidx.annotation.MainThread 7 | import androidx.annotation.Size 8 | import androidx.annotation.StringRes 9 | import androidx.core.view.NestedScrollingChild2 10 | import androidx.core.view.NestedScrollingChildHelper 11 | import androidx.core.view.NestedScrollingParent2 12 | import androidx.core.view.NestedScrollingParentHelper 13 | import androidx.core.view.ViewCompat 14 | import androidx.core.view.ViewCompat.TYPE_NON_TOUCH 15 | import androidx.core.view.ViewCompat.TYPE_TOUCH 16 | import android.util.AttributeSet 17 | import android.util.Log 18 | import android.view.GestureDetector 19 | import android.view.MotionEvent 20 | import android.view.View 21 | import android.view.View.MeasureSpec.UNSPECIFIED 22 | import android.view.View.MeasureSpec.makeMeasureSpec 23 | import android.view.ViewConfiguration 24 | import android.view.animation.Interpolator 25 | import android.widget.LinearLayout 26 | import android.widget.Scroller 27 | import kotlin.math.abs 28 | import kotlin.math.min 29 | import kotlin.math.roundToInt 30 | 31 | /** 32 | * 滑动冲突时起承上启下的作用: 33 | * 处理外层SmartRefreshLayout和RecyclerView和自己的三层同向滑动。 34 | * 主要想法是把RecyclerView的NestedScrolling经过自己处理后,再传递给SmartRefreshLayout。 35 | * 36 | * @author YvesCheung 37 | * 2018/4/8 38 | */ 39 | @Suppress("MemberVisibilityCanBePrivate", "unused") 40 | open class StickyNestedLayout : LinearLayout, 41 | NestedScrollingChild2, 42 | NestedScrollingParent2 { 43 | 44 | companion object { 45 | private const val DEBUG = false 46 | 47 | private val sQuinticInterpolator: Interpolator = Interpolator { t -> 48 | val f = t - 1.0f 49 | f * f * f * f * f + 1.0f 50 | } 51 | } 52 | 53 | /** 54 | * 是否正在嵌套滑动 55 | */ 56 | private var isNestedScrollingStartedByChild = false 57 | 58 | /** 59 | * 是否由当前View主动发起的嵌套滑动 60 | */ 61 | private var isNestedScrollingStartedByThisView = false 62 | 63 | private lateinit var headView: View 64 | private var navView: View? = null 65 | private lateinit var contentView: View 66 | 67 | @Suppress("LeakingThis") 68 | private val childHelper = NestedScrollingChildHelper(this) 69 | 70 | @Suppress("LeakingThis") 71 | private val parentHelper = NestedScrollingParentHelper(this) 72 | 73 | constructor(context: Context) : super(context) 74 | 75 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 76 | 77 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) 78 | : super(context, attrs, defStyleAttr) 79 | 80 | private inline fun log(str: () -> Any?) { 81 | if (DEBUG) { 82 | Log.i("StickyNestedLayout", str()?.toString() ?: "null") 83 | } 84 | } 85 | 86 | private val mTouchSlop: Int 87 | 88 | init { 89 | orientation = VERTICAL 90 | isNestedScrollingEnabled = true 91 | val configuration = ViewConfiguration.get(context) 92 | mTouchSlop = configuration.scaledPagingTouchSlop 93 | } 94 | 95 | private fun string(@StringRes id: Int): String = context.getString(id) 96 | 97 | @Size(2) 98 | private val tempXY = IntArray(2) 99 | 100 | @Size(2) 101 | private fun allocateXY(): IntArray { 102 | tempXY[0] = 0 103 | tempXY[1] = 0 104 | return tempXY 105 | } 106 | 107 | // 108 | 109 | override fun onFinishInflate() { 110 | super.onFinishInflate() 111 | 112 | headView = requireChildView( 113 | R.id.stickyHeadView, R.string.stickyHeadView, "stickyHeadView" 114 | ) 115 | navView = optionalChildView( 116 | R.id.stickyNavView, R.string.stickyNavView, "stickyNavView" 117 | ) 118 | contentView = requireChildView( 119 | R.id.stickyContentView, R.string.stickyContentView, "stickyContentView" 120 | ) 121 | 122 | //让headView是可以收触摸事件的 dispatchTouchEvent才能处理滑动的事件 123 | headView.isFocusable = true 124 | headView.isClickable = true 125 | } 126 | 127 | private fun requireChildView(@IdRes id: Int, @StringRes strId: Int, msg: String): View { 128 | return optionalChildView(id, strId, msg) 129 | ?: throw StickyNestedLayoutException( 130 | "在StickyNestedLayout中必须要提供一个含有属性 android:id=\"@id/$msg\" 或者" + 131 | "android:contentDescription=\"@string/$msg\" 的子View " 132 | ) 133 | } 134 | 135 | private fun optionalChildView(@IdRes id: Int, @StringRes strId: Int, msg: String): View? { 136 | val viewOptional: View? = findViewById(id) 137 | return if (viewOptional != null) { 138 | viewOptional 139 | } else { 140 | val singleViewExpect = ArrayList(1) 141 | findViewsWithText(singleViewExpect, string(strId), FIND_VIEWS_WITH_CONTENT_DESCRIPTION) 142 | if (singleViewExpect.size > 1) { 143 | throw StickyNestedLayoutException( 144 | "在StickyNestedLayout中包含了多个含有属性 android:id=\"@id/$msg\" 或者" + 145 | "android:contentDescription=\"@string/$msg\" 的子View," + 146 | "StickyNestedLayout无法确定应该使用哪一个" 147 | ) 148 | } else { 149 | singleViewExpect.firstOrNull() 150 | } 151 | } 152 | } 153 | 154 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 155 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 156 | val wrapContent = makeMeasureSpec(0, UNSPECIFIED) 157 | measureChildWithMargins(headView, widthMeasureSpec, wrapContent) 158 | val navigationView = navView 159 | if (navigationView != null) { 160 | measureChildWithMargins(navigationView, widthMeasureSpec, wrapContent) 161 | } 162 | val expectContentHeight = makeMeasureSpec( 163 | measuredHeight - navViewHeight - stickyOffsetHeight, 164 | MeasureSpec.EXACTLY 165 | ) 166 | measureChildWithMargins(contentView, widthMeasureSpec, expectContentHeight) 167 | setMeasuredDimension(measuredWidthAndState, measuredHeightAndState) 168 | } 169 | 170 | private fun measureChildWithMargins( 171 | child: View, 172 | parentWidthMeasureSpec: Int, 173 | parentHeightMeasureSpec: Int 174 | ) { 175 | val lp = child.layoutParams as MarginLayoutParams 176 | val childWidthMeasureSpec = 177 | getChildMeasureSpec( 178 | parentWidthMeasureSpec, 179 | paddingLeft + paddingRight + lp.leftMargin + lp.rightMargin, 180 | lp.width 181 | ) 182 | val childHeightMeasureSpec = 183 | getChildMeasureSpec( 184 | parentHeightMeasureSpec, 185 | paddingTop + paddingBottom + lp.topMargin + lp.bottomMargin, 186 | lp.height 187 | ) 188 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 189 | } 190 | 191 | // 192 | 193 | // 194 | 195 | /** 196 | * 跟[scrollBy]类似,但会计算滑动后未消耗的距离。 197 | * 比如当需要滑行的距离为dy,但当滑行dy/3的距离之后,就已经滑行到顶部或者底部无法继续下去,那么有unconsumed[1]=dy*2/3。 198 | * 199 | * @param unconsumed 未消耗的距离 200 | */ 201 | private fun scrollByWithUnConsumed(dx: Int, dy: Int, unconsumed: IntArray? = null) { 202 | scrollToWithUnConsumed(scrollX + dx, scrollY + dy, unconsumed) 203 | } 204 | 205 | /** 206 | * 跟[scrollTo]类似,但会计算滑动后未消耗的距离。 207 | * 比如当需要滑行的距离为dy,但当滑行dy/3的距离之后,就已经滑行到顶部或者底部无法继续下去,那么有unconsumed[1]=dy*2/3。 208 | * 209 | * @param unconsumed 未消耗的距离 210 | */ 211 | private fun scrollToWithUnConsumed(dx: Int, dy: Int, unconsumed: IntArray? = null) { 212 | val scrollMax = headViewHeight - stickyOffsetHeight 213 | when { 214 | dy < 0 -> { 215 | scrollTo(dx, 0) 216 | unconsumed?.set(1, dy) 217 | } 218 | dy > scrollMax -> { 219 | scrollTo(dx, scrollMax) 220 | unconsumed?.set(1, dy - scrollMax) 221 | } 222 | else -> { 223 | scrollTo(dx, dy) 224 | unconsumed?.set(1, 0) 225 | } 226 | } 227 | scrollListeners.forEach { it.onScroll(this, scrollX, scrollY) } 228 | } 229 | 230 | private var mScroller = Scroller(context, sQuinticInterpolator) 231 | private var lastFlingX = 0 232 | private var lastFlingY = 0 233 | private var inStateOfFling = false 234 | 235 | override fun computeScroll() { 236 | if (mScroller.computeScrollOffset()) { 237 | if (inStateOfFling) { //fling 238 | val curY = mScroller.currY 239 | val curX = mScroller.currX 240 | var dy = curY - lastFlingX 241 | var dx = curX - lastFlingY 242 | lastFlingX = curY 243 | lastFlingY = curX 244 | 245 | val consumedByParent = allocateXY() 246 | if (dispatchNestedPreScroll(dx, dy, consumedByParent, null, TYPE_NON_TOUCH)) { 247 | dx -= consumedByParent[0] 248 | dy -= consumedByParent[1] 249 | } 250 | 251 | val consumedByUs = allocateXY() 252 | scrollByWithUnConsumed(0, dy, consumedByUs) 253 | dispatchNestedScroll( 254 | 0, dy - consumedByUs[1], 255 | dx, consumedByUs[1], null, TYPE_NON_TOUCH 256 | ) 257 | } else { //scroll 258 | scrollToWithUnConsumed(mScroller.currX, mScroller.currY, null) 259 | } 260 | 261 | if (mScroller.isFinished) { 262 | abortScrollerAnimation() 263 | } else { 264 | ViewCompat.postInvalidateOnAnimation(this) 265 | } 266 | } 267 | } 268 | 269 | private fun abortScrollerAnimation() { 270 | mScroller.abortAnimation() 271 | if (inStateOfFling) { 272 | stopNestedScroll(TYPE_NON_TOUCH, "abortScrollerAnimation") 273 | inStateOfFling = false 274 | } 275 | isNestedScrollingStartedByChild = false 276 | isNestedScrollingStartedByThisView = false 277 | } 278 | 279 | private fun fling(vx: Float, vy: Float) { 280 | log { "startFling velocityY = $vy" } 281 | mScroller.fling( 282 | 0, 0, vx.roundToInt(), vy.roundToInt(), 283 | Int.MIN_VALUE, Int.MAX_VALUE, Int.MIN_VALUE, Int.MAX_VALUE 284 | ) 285 | lastFlingX = 0 286 | lastFlingY = 0 287 | inStateOfFling = true 288 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, TYPE_NON_TOUCH, "fling") 289 | ViewCompat.postInvalidateOnAnimation(this) 290 | } 291 | 292 | // 293 | 294 | // 295 | 296 | override fun setNestedScrollingEnabled(enabled: Boolean) { 297 | childHelper.isNestedScrollingEnabled = enabled 298 | } 299 | 300 | override fun hasNestedScrollingParent() = childHelper.hasNestedScrollingParent() 301 | 302 | override fun hasNestedScrollingParent(type: Int) = childHelper.hasNestedScrollingParent(type) 303 | 304 | override fun isNestedScrollingEnabled() = childHelper.isNestedScrollingEnabled 305 | 306 | override fun startNestedScroll(axes: Int) = 307 | startNestedScroll(axes, TYPE_TOUCH, "callStartNestedScroll") 308 | 309 | override fun startNestedScroll(axes: Int, type: Int): Boolean = 310 | startNestedScroll(axes, type, "callStartNestedScroll(type)") 311 | 312 | private fun startNestedScroll(axes: Int, type: Int, reason: String): Boolean { 313 | log { "startNestedScroll $type by $reason" } 314 | return childHelper.startNestedScroll(axes, type) 315 | } 316 | 317 | override fun stopNestedScroll() = stopNestedScroll(TYPE_TOUCH, "CallStopNestedScroll") 318 | 319 | override fun stopNestedScroll(type: Int) = 320 | stopNestedScroll(TYPE_TOUCH, "CallStopNestedScroll(type)") 321 | 322 | private fun stopNestedScroll(type: Int, reason: String) { 323 | log { "stopNestedScroll $type by $reason" } 324 | childHelper.stopNestedScroll(type) 325 | } 326 | 327 | override fun dispatchNestedScroll( 328 | dxConsumed: Int, dyConsumed: Int, 329 | dxUnconsumed: Int, dyUnconsumed: Int, 330 | offsetInWindow: IntArray?, type: Int 331 | ) = childHelper.dispatchNestedScroll( 332 | dxConsumed, dyConsumed, dxUnconsumed, 333 | dyUnconsumed, offsetInWindow, type 334 | ) 335 | 336 | override fun dispatchNestedScroll( 337 | dxConsumed: Int, dyConsumed: Int, 338 | dxUnconsumed: Int, dyUnconsumed: Int, 339 | offsetInWindow: IntArray? 340 | ) = childHelper.dispatchNestedScroll( 341 | dxConsumed, dyConsumed, 342 | dxUnconsumed, dyUnconsumed, offsetInWindow 343 | ) 344 | 345 | override fun dispatchNestedPreScroll( 346 | dx: Int, dy: Int, consumed: IntArray?, 347 | offsetInWindow: IntArray?, type: Int 348 | ) = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type) 349 | 350 | override fun dispatchNestedPreScroll( 351 | dx: Int, dy: Int, consumed: IntArray?, 352 | offsetInWindow: IntArray? 353 | ) = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow) 354 | 355 | override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean) = 356 | childHelper.dispatchNestedFling(velocityX, velocityY, consumed) 357 | 358 | override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float) = 359 | childHelper.dispatchNestedPreFling(velocityX, velocityY) 360 | 361 | override fun onNestedScrollAccepted(child: View, target: View, axes: Int) = 362 | parentHelper.onNestedScrollAccepted(child, target, axes) 363 | 364 | override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) = 365 | parentHelper.onNestedScrollAccepted(child, target, axes, type) 366 | 367 | override fun getNestedScrollAxes() = parentHelper.nestedScrollAxes 368 | 369 | override fun onStopNestedScroll(child: View) = parentHelper.onStopNestedScroll(child) 370 | 371 | override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean = 372 | onStartNestedScroll(child, target, nestedScrollAxes, TYPE_TOUCH) 373 | 374 | override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) { 375 | onNestedPreScroll(target, dx, dy, consumed, TYPE_TOUCH) 376 | } 377 | 378 | override fun onNestedScroll( 379 | target: View, dxConsumed: Int, dyConsumed: Int, 380 | dxUnconsumed: Int, dyUnconsumed: Int 381 | ) { 382 | onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, TYPE_TOUCH) 383 | } 384 | 385 | override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean = 386 | dispatchNestedPreFling(velocityX, velocityY) 387 | 388 | override fun onNestedFling( 389 | target: View, 390 | velocityX: Float, 391 | velocityY: Float, 392 | consumed: Boolean 393 | ): Boolean = dispatchNestedFling(velocityX, velocityY, consumed) 394 | 395 | /** 396 | * 记录当前开始中的嵌套滑动类型 397 | * @see TYPE_TOUCH 398 | * @see TYPE_NON_TOUCH 399 | */ 400 | private val nestedScrollingType = mutableSetOf() 401 | 402 | //child告诉我要开始嵌套滑动 403 | override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean { 404 | log { "onStartNestedScroll $type" } 405 | if (axes and SCROLL_AXIS_VERTICAL != 0) { //只响应垂直方向的滑动 406 | nestedScrollingType.add(type) 407 | isNestedScrollingStartedByThisView = false 408 | isNestedScrollingStartedByChild = true 409 | //开始通知parent的嵌套滑动 410 | startNestedScroll( 411 | axes, 412 | type, 413 | "onStartNestedScroll" 414 | ) 415 | return true 416 | } 417 | return false 418 | } 419 | 420 | //child告诉我要停止嵌套滑动 421 | override fun onStopNestedScroll(target: View, type: Int) { 422 | log { "onStopNestedScroll $target, type = $type" } 423 | nestedScrollingType.remove(type) 424 | if (nestedScrollingType.isEmpty()) { 425 | isNestedScrollingStartedByThisView = false 426 | isNestedScrollingStartedByChild = false 427 | } 428 | stopNestedScroll(type, "onStopNestedScroll") //结束parent的嵌套滑动 429 | } 430 | 431 | override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) { 432 | if (isNestedScrollingStartedByChild) { 433 | log { "onNestedPreScroll dy = $dy, type = $type" } 434 | //dy > 0 上滑时处理 435 | val consumedByParent = allocateXY() 436 | dispatchNestedPreScroll(dx, dy, consumedByParent, null) //先分给parent搞事 437 | 438 | val leftY = dy - consumedByParent[1] //parent留给我的 439 | val headViewScrollDis = headViewHeight - scrollY - stickyOffsetHeight 440 | val headViewCanBeExpand = leftY > 0 && headViewScrollDis > 0 //上滑且headView能向上滚 441 | 442 | consumed[0] = consumedByParent[0] //x方向全是parent吃的 443 | if (headViewCanBeExpand) { 444 | if (leftY > headViewScrollDis) { //滑的距离超过了能滚的距离 445 | scrollByWithUnConsumed(0, headViewScrollDis) 446 | consumed[1] = headViewScrollDis + consumedByParent[1] //只消费能滚的最大距离 447 | } else { 448 | scrollByWithUnConsumed(0, leftY) //没超过滚的极限距离,那就滑多少滚多少 449 | consumed[1] = dy //把parent吃剩的全吃了 (parentConsumed[1] + leftY) 450 | } 451 | } else { //headView不能滑了 全是parent吃的 452 | consumed[1] = consumedByParent[1] 453 | } 454 | } 455 | } 456 | 457 | override fun onNestedScroll( 458 | target: View, dxConsumed: Int, dyConsumed: Int, 459 | dxUnconsumed: Int, dyUnconsumed: Int, type: Int 460 | ) { 461 | if (isNestedScrollingStartedByChild) { 462 | log { "onNestedScroll dyConsumed = $dyConsumed, dyUnconsumed = $dyUnconsumed, type = $type" } 463 | //dy < 0 下滑时处理 464 | var dyUnconsumedAfterMe = dyUnconsumed 465 | var dyConsumedAfterMe = dyConsumed 466 | val headViewScrollDis = scrollY 467 | 468 | if (dyUnconsumed < 0 && headViewScrollDis >= 0) { //下滑而且headView能向下滚 469 | if (headViewScrollDis < abs(dyUnconsumed)) { //滑动距离超过了可以滚的范围 470 | scrollByWithUnConsumed(0, -headViewScrollDis) //只滚我能滚的 471 | dyUnconsumedAfterMe = dyUnconsumed + headViewScrollDis //只消费我能滑的 472 | dyConsumedAfterMe = dyConsumed - headViewScrollDis 473 | } else { //全部都能消费掉 474 | scrollByWithUnConsumed(0, dyUnconsumed) 475 | dyUnconsumedAfterMe = 0 476 | dyConsumedAfterMe = dyConsumed + dyUnconsumed 477 | } 478 | } 479 | 480 | dispatchNestedScroll( 481 | 0, dyConsumedAfterMe, 0, 482 | dyUnconsumedAfterMe, null 483 | ) 484 | } 485 | } 486 | 487 | // 488 | 489 | 490 | // 491 | 492 | private var lastX = 0f 493 | private var lastY = 0f 494 | private var downRawY = 0f 495 | private var downRawX = 0f 496 | 497 | private val gestureHandler = object : GestureDetector.SimpleOnGestureListener() { 498 | 499 | override fun onScroll( 500 | e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float 501 | ): Boolean { 502 | if (isNestedScrollingStartedByThisView) { 503 | val scrollByHuman = (lastY - e2.y).roundToInt() //手势产生的距离 504 | log { "scroll y = ${e2.y} lastY = $lastY dy = $scrollByHuman" } 505 | //先给parent消费 506 | val consumedByParent = allocateXY() 507 | dispatchNestedPreScroll(0, scrollByHuman, consumedByParent, null) 508 | val scrollAfterParent = scrollByHuman - consumedByParent[1] //parent吃剩的 509 | val unconsumed = allocateXY() 510 | scrollByWithUnConsumed(0, scrollAfterParent, unconsumed) //自己滑 511 | val consumeY = scrollByHuman - unconsumed[1] 512 | //滑剩的再给一次parent 513 | dispatchNestedScroll(0, consumeY, 0, unconsumed[1], null) 514 | } 515 | lastX = e2.x 516 | lastY = e2.y 517 | return true 518 | } 519 | 520 | override fun onFling( 521 | e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float 522 | ): Boolean { 523 | log { "onFling velocity = $velocityY" } 524 | val vx = -velocityX 525 | val vy = -velocityY 526 | if (isNestedScrollingStartedByThisView) { 527 | //根据当前速度 进行惯性滑行 528 | //先让parent消费 529 | if (!dispatchNestedPreFling(vx, vy)) { 530 | dispatchNestedFling(vx, vy, true) 531 | 532 | fling(vx, vy) 533 | } 534 | stopNestedScroll(TYPE_TOUCH, "onFling") 535 | return true 536 | } 537 | return false 538 | } 539 | 540 | override fun onDown(e: MotionEvent): Boolean { 541 | log { "onDown $e" } 542 | abortScrollerAnimation() 543 | lastY = e.y 544 | lastX = e.x 545 | if (e.x.toInt() in headView.left..headView.right && 546 | e.y.toInt() in headView.top..headView.bottom 547 | ) { 548 | isNestedScrollingStartedByThisView = true 549 | isNestedScrollingStartedByChild = false 550 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, TYPE_TOUCH, "onDown") 551 | } 552 | return true 553 | } 554 | } 555 | private val gestureDetector by lazy { GestureDetector(context, gestureHandler) } 556 | 557 | @SuppressLint("ClickableViewAccessibility") 558 | override fun onTouchEvent(event: MotionEvent): Boolean { 559 | val action = event.action and MotionEvent.ACTION_MASK 560 | if (gestureDetector.onTouchEvent(event)) { 561 | return true 562 | } else if (action == MotionEvent.ACTION_UP || 563 | action == MotionEvent.ACTION_CANCEL 564 | ) { 565 | log { if (action == MotionEvent.ACTION_UP) "onUp" else "onCancel" } 566 | if (isNestedScrollingStartedByThisView) { 567 | stopNestedScroll( 568 | TYPE_TOUCH, 569 | if (action == MotionEvent.ACTION_UP) "onUp" else "onCancel" 570 | ) 571 | return true 572 | } 573 | return false 574 | } 575 | return false 576 | } 577 | 578 | override fun onInterceptTouchEvent(event: MotionEvent): Boolean { 579 | val action = event.action and MotionEvent.ACTION_MASK 580 | var intercept = false 581 | 582 | if (action != MotionEvent.ACTION_MOVE) { 583 | if (isNestedScrollingStartedByThisView) { 584 | intercept = true 585 | } 586 | } 587 | when (action) { 588 | MotionEvent.ACTION_DOWN -> { 589 | log { "onIntercept onDown" } 590 | abortScrollerAnimation() 591 | lastY = event.y 592 | lastX = event.x 593 | downRawY = event.rawY 594 | downRawX = event.rawX 595 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, TYPE_TOUCH, "onInterceptDown") 596 | } 597 | MotionEvent.ACTION_MOVE -> { 598 | lastY = event.y 599 | lastX = event.x 600 | if (!isNestedScrollingStartedByChild) { 601 | val dy = abs(event.rawY - downRawY) 602 | val dx = abs(event.rawX - downRawX) 603 | if (dy > mTouchSlop && dy > 2 * dx) { 604 | isNestedScrollingStartedByThisView = true 605 | log { "onInterceptTouchEvent requestDisallowIntercept" } 606 | requestDisallowParentTouchEvent() 607 | intercept = true 608 | } 609 | } 610 | } 611 | } 612 | return intercept || super.onInterceptTouchEvent(event) 613 | } 614 | 615 | private fun requestDisallowParentTouchEvent() { 616 | parent?.requestDisallowInterceptTouchEvent(true) 617 | } 618 | // 619 | 620 | // 621 | 622 | /** 623 | * 滑动到id为stickyNavView的导航栏上方。 624 | * 比如点击微博的评论按钮进入微博详情页,会直接滑动到导航区域。 625 | */ 626 | @MainThread 627 | fun scrollToNavView() { 628 | val toY = navView?.y ?: contentView.y 629 | scrollTo(0, toY.toInt()) 630 | } 631 | 632 | /** 633 | * 吸顶导航栏距离顶部的距离偏移。 634 | * 默认没有偏移。即导航栏可以滑动到顶部并吸附。 635 | */ 636 | var stickyOffsetHeight: Int = 0 637 | @MainThread 638 | set(value) { 639 | field = if (value < 0) 0 else value 640 | requestLayout() 641 | } 642 | get() = min(field, headViewHeight) 643 | 644 | /** 645 | * 获取头部区域的高度 646 | */ 647 | val headViewHeight: Int get() = headView.measuredHeight 648 | 649 | /** 650 | * 获取导航栏条的高度 651 | */ 652 | val navViewHeight: Int get() = navView?.measuredHeight ?: 0 653 | 654 | /** 655 | * 获取下部区域的高度 656 | */ 657 | val contentViewHeight: Int get() = contentView.measuredHeight 658 | 659 | private val scrollListeners = mutableListOf() 660 | 661 | /** 662 | * 添加滚动监听器(内部View的滑动不会触发) 663 | * @see removeOnScrollChangeListener 664 | */ 665 | fun addOnScrollListener(listener: OnScrollListener) { 666 | scrollListeners.add(listener) 667 | } 668 | 669 | /** 670 | * 移除滚动监听器 671 | * @see addOnScrollListener 672 | */ 673 | fun removeOnScrollChangeListener(listener: OnScrollListener) { 674 | scrollListeners.remove(listener) 675 | } 676 | 677 | interface OnScrollListener { 678 | fun onScroll(view: StickyNestedLayout, scrollX: Int, scrollY: Int) 679 | } 680 | 681 | // 682 | } -------------------------------------------------------------------------------- /nestedtouch/src/main/java/mobile/yy/com/nestedtouch/StickyNestedLayoutException.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.nestedtouch 2 | 3 | /** 4 | * @author YvesCheung 5 | * 2018/5/28 6 | */ 7 | class StickyNestedLayoutException(error: String) : RuntimeException(error) -------------------------------------------------------------------------------- /nestedtouch/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /nestedtouch/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | nestedTouch 3 | 4 | stickyHeadView 5 | stickyNavView 6 | stickyContentView 7 | 8 | -------------------------------------------------------------------------------- /nestedtouchsample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /nestedtouchsample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | android { 5 | compileSdkVersion 30 6 | 7 | defaultConfig { 8 | applicationId "mobile.yy.com.nestedtouchsample" 9 | minSdkVersion 14 10 | targetSdkVersion 30 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | 22 | compileOptions { 23 | sourceCompatibility JavaVersion.VERSION_1_8 24 | targetCompatibility JavaVersion.VERSION_1_8 25 | } 26 | 27 | kotlinOptions { 28 | jvmTarget = JavaVersion.VERSION_1_8.toString() 29 | } 30 | } 31 | 32 | dependencies { 33 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 34 | implementation 'androidx.recyclerview:recyclerview:1.1.0' 35 | implementation 'androidx.appcompat:appcompat:1.3.0' 36 | implementation 'com.scwang.smartrefresh:SmartRefreshLayout:1.0.5.1' 37 | implementation 'com.gxz.pagerslidingtabstrip:library:1.3.1' 38 | implementation project(":nestedtouch") 39 | } 40 | -------------------------------------------------------------------------------- /nestedtouchsample/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 | -------------------------------------------------------------------------------- /nestedtouchsample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /nestedtouchsample/src/main/java/mobile/yy/com/nestedtouchsample/BlankFragment.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.nestedtouchsample 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import androidx.fragment.app.Fragment 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | 10 | 11 | /** 12 | * Created by 张宇 on 2018/5/25. 13 | * E-mail: zhangyu4@yy.com 14 | * YY: 909017428 15 | */ 16 | class BlankFragment : Fragment() { 17 | 18 | @SuppressLint("InflateParams") 19 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 20 | return inflater.inflate(R.layout.fragment_blank, null) 21 | } 22 | } -------------------------------------------------------------------------------- /nestedtouchsample/src/main/java/mobile/yy/com/nestedtouchsample/DetailFragment.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.nestedtouchsample 2 | 3 | import android.content.res.Resources 4 | import android.os.Bundle 5 | import androidx.fragment.app.Fragment 6 | import androidx.recyclerview.widget.LinearLayoutManager 7 | import androidx.recyclerview.widget.RecyclerView 8 | import android.util.TypedValue 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.widget.TextView 13 | import com.scwang.smartrefresh.layout.SmartRefreshLayout 14 | import com.scwang.smartrefresh.layout.footer.ClassicsFooter 15 | 16 | /** 17 | * Created by 张宇 on 2018/4/27. 18 | * E-mail: zhangyu4@yy.com 19 | * YY: 909017428 20 | */ 21 | class DetailFragment : Fragment() { 22 | 23 | private val randomNumber get() = Array(30) { idx -> "${idx}000000" } 24 | 25 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 26 | val recyclerView = RecyclerView(inflater.context).apply { 27 | layoutManager = LinearLayoutManager(inflater.context) 28 | adapter = DetailAdapter(randomNumber.toList()) 29 | } 30 | return SmartRefreshLayout(inflater.context).apply { 31 | isEnableRefresh = false 32 | setRefreshFooter(ClassicsFooter(inflater.context)) 33 | setEnableNestedScroll(true) 34 | addView(recyclerView) 35 | } 36 | } 37 | } 38 | 39 | class DetailAdapter(private val list: List) : RecyclerView.Adapter() { 40 | 41 | private val Number.dp2px 42 | get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), 43 | Resources.getSystem().displayMetrics).toInt() 44 | 45 | private val padding = 20.dp2px 46 | 47 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DetailViewHolder { 48 | return DetailViewHolder(TextView(parent.context).apply { 49 | setPadding(padding, padding, padding, padding) 50 | }) 51 | } 52 | 53 | override fun getItemCount() = list.size 54 | 55 | override fun onBindViewHolder(holder: DetailViewHolder, position: Int) { 56 | holder.v.text = list[position] 57 | } 58 | 59 | class DetailViewHolder(val v: TextView) : RecyclerView.ViewHolder(v) 60 | } -------------------------------------------------------------------------------- /nestedtouchsample/src/main/java/mobile/yy/com/nestedtouchsample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.nestedtouchsample 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.FragmentManager 5 | import androidx.fragment.app.FragmentPagerAdapter 6 | import androidx.appcompat.app.AppCompatActivity 7 | import android.view.ViewTreeObserver 8 | import android.widget.Toast 9 | import com.scwang.smartrefresh.layout.header.ClassicsHeader 10 | import kotlinx.android.synthetic.main.activity_main.* 11 | import kotlinx.android.synthetic.main.moment_head_view.* 12 | import mobile.yy.com.nestedtouch.StickyNestedLayout 13 | 14 | class MainActivity : AppCompatActivity() { 15 | 16 | private val layoutListener = object : ViewTreeObserver.OnGlobalLayoutListener { 17 | override fun onGlobalLayout() { 18 | titleBar.viewTreeObserver.removeGlobalOnLayoutListener(this) 19 | stickyNestedLayout.stickyOffsetHeight = titleBar.height 20 | 21 | val headViewHeight = (stickyNestedLayout.headViewHeight - titleBar.height).toFloat() 22 | stickyNestedLayout.addOnScrollListener(object : StickyNestedLayout.OnScrollListener { 23 | override fun onScroll(view: StickyNestedLayout, scrollX: Int, scrollY: Int) { 24 | titleBar.alpha = 1f - (headViewHeight - scrollY) / headViewHeight 25 | } 26 | }) 27 | } 28 | } 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | setContentView(R.layout.activity_main) 33 | 34 | refreshLayout.setRefreshHeader(ClassicsHeader(this)) 35 | refreshLayout.setEnableNestedScroll(true) 36 | refreshLayout.isEnableLoadMore = false 37 | 38 | titleBar.alpha = 0f 39 | titleBar.viewTreeObserver.addOnGlobalLayoutListener(layoutListener) 40 | 41 | contentView.adapter = MainAdapter(supportFragmentManager) 42 | 43 | navView.setViewPager(contentView) 44 | 45 | momentItemQrCode.setOnClickListener { 46 | Toast.makeText(this, "click qr code", Toast.LENGTH_SHORT).show() 47 | } 48 | } 49 | } 50 | 51 | class MainAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) { 52 | 53 | override fun getItem(position: Int) = 54 | when (position) { 55 | 0 -> DetailFragment() 56 | 1 -> WebFragment() 57 | else -> BlankFragment() 58 | } 59 | 60 | override fun getCount() = 3 61 | 62 | override fun getPageTitle(position: Int) = when (position) { 63 | 0 -> "转发" 64 | 1 -> "评论" 65 | else -> "点赞" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /nestedtouchsample/src/main/java/mobile/yy/com/nestedtouchsample/WebFragment.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.nestedtouchsample 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.core.view.MotionEventCompat 8 | import androidx.core.view.NestedScrollingChild 9 | import androidx.core.view.NestedScrollingChildHelper 10 | import androidx.core.view.ViewCompat 11 | import android.util.AttributeSet 12 | import android.view.LayoutInflater 13 | import android.view.MotionEvent 14 | import android.view.View 15 | import android.view.ViewGroup 16 | import android.webkit.WebView 17 | import android.webkit.WebViewClient 18 | import kotlin.math.max 19 | 20 | 21 | /** 22 | * @author YvesCheung 23 | * 2021/1/15 24 | */ 25 | class WebFragment : Fragment() { 26 | 27 | private lateinit var webView: WebView 28 | 29 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 30 | return NestedScrollWebView(inflater.context).also { webView = it } 31 | } 32 | 33 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 34 | super.onViewCreated(view, savedInstanceState) 35 | webView.settings.apply { 36 | @SuppressLint("SetJavaScriptEnabled") 37 | javaScriptEnabled = true 38 | useWideViewPort = true 39 | loadWithOverviewMode = true 40 | setSupportZoom(true) 41 | builtInZoomControls = true 42 | displayZoomControls = false 43 | loadsImagesAutomatically = true 44 | } 45 | webView.webViewClient = object : WebViewClient() { 46 | override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { 47 | view.loadUrl(url) 48 | return true 49 | } 50 | } 51 | webView.loadUrl("https://www.yy.com/") 52 | } 53 | 54 | override fun onResume() { 55 | super.onResume() 56 | webView.onResume() 57 | } 58 | 59 | override fun onPause() { 60 | webView.onPause() 61 | super.onPause() 62 | } 63 | 64 | override fun onDestroyView() { 65 | webView.destroy() 66 | super.onDestroyView() 67 | } 68 | 69 | /** 70 | * Copy from [https://github.com/tobiasrohloff/NestedScrollWebView/blob/master/lib/src/main/java/com/tobiasrohloff/view/NestedScrollWebView.java] 71 | */ 72 | private class NestedScrollWebView @JvmOverloads constructor( 73 | context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 74 | ) : WebView(context, attrs, defStyleAttr), NestedScrollingChild { 75 | 76 | private var mLastMotionY = 0 77 | private val mScrollOffset = IntArray(2) 78 | private val mScrollConsumed = IntArray(2) 79 | private var mNestedYOffset = 0 80 | private var mChildHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this) 81 | 82 | init { 83 | isNestedScrollingEnabled = true 84 | } 85 | 86 | @SuppressLint("ClickableViewAccessibility") 87 | override fun onTouchEvent(event: MotionEvent): Boolean { 88 | var result = false 89 | val trackedEvent = MotionEvent.obtain(event) 90 | val action = MotionEventCompat.getActionMasked(event) 91 | if (action == MotionEvent.ACTION_DOWN) { 92 | mNestedYOffset = 0 93 | } 94 | val y = event.y.toInt() 95 | event.offsetLocation(0f, mNestedYOffset.toFloat()) 96 | when (action) { 97 | MotionEvent.ACTION_DOWN -> { 98 | mLastMotionY = y 99 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) 100 | result = super.onTouchEvent(event) 101 | } 102 | MotionEvent.ACTION_MOVE -> { 103 | var deltaY = mLastMotionY - y 104 | if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { 105 | deltaY -= mScrollConsumed[1] 106 | trackedEvent.offsetLocation(0f, mScrollOffset[1].toFloat()) 107 | mNestedYOffset += mScrollOffset[1] 108 | } 109 | mLastMotionY = y - mScrollOffset[1] 110 | val oldY = scrollY 111 | val newScrollY = max(0, oldY + deltaY) 112 | val dyConsumed = newScrollY - oldY 113 | val dyUnconsumed = deltaY - dyConsumed 114 | if (dispatchNestedScroll(0, dyConsumed, 0, dyUnconsumed, mScrollOffset)) { 115 | mLastMotionY -= mScrollOffset[1] 116 | trackedEvent.offsetLocation(0f, mScrollOffset[1].toFloat()) 117 | mNestedYOffset += mScrollOffset[1] 118 | } 119 | result = super.onTouchEvent(trackedEvent) 120 | trackedEvent.recycle() 121 | } 122 | MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { 123 | stopNestedScroll() 124 | result = super.onTouchEvent(event) 125 | } 126 | } 127 | return result 128 | } 129 | 130 | override fun setNestedScrollingEnabled(enabled: Boolean) { 131 | mChildHelper.isNestedScrollingEnabled = enabled 132 | } 133 | 134 | override fun isNestedScrollingEnabled(): Boolean { 135 | return mChildHelper.isNestedScrollingEnabled 136 | } 137 | 138 | override fun startNestedScroll(axes: Int): Boolean { 139 | return mChildHelper.startNestedScroll(axes) 140 | } 141 | 142 | override fun stopNestedScroll() { 143 | mChildHelper.stopNestedScroll() 144 | } 145 | 146 | override fun hasNestedScrollingParent(): Boolean { 147 | return mChildHelper.hasNestedScrollingParent() 148 | } 149 | 150 | override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?): Boolean { 151 | return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow) 152 | } 153 | 154 | override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?): Boolean { 155 | return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow) 156 | } 157 | 158 | override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean { 159 | return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed) 160 | } 161 | 162 | override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean { 163 | return mChildHelper.dispatchNestedPreFling(velocityX, velocityY) 164 | } 165 | } 166 | } -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/drawable/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/nestedtouchsample/src/main/res/drawable/avatar.png -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/drawable/mn_qrcode_download_yy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/nestedtouchsample/src/main/res/drawable/mn_qrcode_download_yy.png -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 19 | 24 | 25 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | 40 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 60 | 61 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/layout/fragment_blank.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/layout/moment_head_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 27 | 28 | 37 | 38 | 50 | 51 | 59 | -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/layout/simple_title_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/nestedtouchsample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/nestedtouchsample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/nestedtouchsample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/nestedtouchsample/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/nestedtouchsample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/nestedtouchsample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/nestedtouchsample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/nestedtouchsample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/nestedtouchsample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/nestedtouchsample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFC125 4 | #FF9045 5 | #FF7256 6 | 7 | -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | NestedTouchSample 3 | 4 | -------------------------------------------------------------------------------- /nestedtouchsample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':touchsample', ':toucheventbus', ':nestedtouch', ':nestedtouchsample' 2 | -------------------------------------------------------------------------------- /toucheventbus/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /toucheventbus/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | group = 'com.github.YvesCheung' 3 | 4 | android { 5 | compileSdkVersion 30 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | targetSdkVersion 30 10 | 11 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 12 | 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | 22 | compileOptions { 23 | sourceCompatibility JavaVersion.VERSION_1_8 24 | targetCompatibility JavaVersion.VERSION_1_8 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation 'androidx.appcompat:appcompat:1.3.0' 30 | } 31 | 32 | tasks.withType(JavaCompile) { 33 | options.encoding = "UTF-8" 34 | } 35 | 36 | ext { 37 | libraryName = 'toucheventbus' 38 | libraryDescription = 'A solution for Non-Nested Scrolling' 39 | siteUrl = 'https://github.com/YvesCheung/TouchEventBus' 40 | gitUrl = 'https://github.com/YvesCheung/TouchEventBus.git' 41 | libraryVersion = version 42 | } 43 | 44 | apply from: '../gradle/publish.gradle' -------------------------------------------------------------------------------- /toucheventbus/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 | -------------------------------------------------------------------------------- /toucheventbus/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /toucheventbus/src/main/java/mobile/yy/com/toucheventbus/AbstractTouchEventHandler.java: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.toucheventbus; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | import android.util.Log; 6 | import android.view.MotionEvent; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * @author YvesCheung 13 | * 2017/9/19 14 | *

15 | * 基本触摸事件处理骨架 绑定指定类型的{@link TouchViewHolder} viewHolder和 {@link TouchEventHandler} nextHandler 16 | */ 17 | @SuppressWarnings("WeakerAccess") 18 | public abstract class AbstractTouchEventHandler implements TouchEventHandler> { 19 | 20 | private List>>> mHandlers 21 | = new ArrayList<>(); 22 | 23 | private TouchViewHolder mViewHolder = new TouchViewHolder<>(); 24 | 25 | public AbstractTouchEventHandler() { 26 | defineNextHandlers(mHandlers); 27 | } 28 | 29 | @Nullable 30 | @Override 31 | public List>>> nextHandler() { 32 | return mHandlers; 33 | } 34 | 35 | @NonNull 36 | @Override 37 | public TouchViewHolder getViewHolder() { 38 | return mViewHolder; 39 | } 40 | 41 | @Override 42 | public boolean forceMonitor() { 43 | return false; 44 | } 45 | 46 | @Override 47 | public boolean onTouch(@NonNull T t, @NonNull MotionEvent e, boolean hasBeenIntercepted) { 48 | switch (e.getAction()) { 49 | case MotionEvent.ACTION_DOWN: 50 | case MotionEvent.ACTION_UP: 51 | Log.i("TouchEventHandler", name() + " intercepted = " + 52 | hasBeenIntercepted + " event = " + e); 53 | break; 54 | default: 55 | Log.v("TouchEventHandler", name() + " intercepted = " + 56 | hasBeenIntercepted + " event = " + e); 57 | break; 58 | } 59 | return false; 60 | } 61 | 62 | /** 63 | * 返回当前Handler的名字,用于标志和打印日志 64 | * 65 | * @return 当前Handler名字,一般是类名 66 | */ 67 | @NonNull 68 | protected abstract String name(); 69 | 70 | /** 71 | * {@link #nextHandler()}的另一种实现方式,可以选择这个方法来实现 72 | * 73 | * @param handlers 一个可变的空列表,可以把下层的Handler添加到列表中 74 | * @see #nextHandler() 75 | */ 76 | protected void defineNextHandlers( 77 | @NonNull List>>> handlers) { 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /toucheventbus/src/main/java/mobile/yy/com/toucheventbus/AttachToViewTouchEventHandler.java: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.toucheventbus; 2 | 3 | import androidx.annotation.NonNull; 4 | import android.view.MotionEvent; 5 | import android.view.View; 6 | 7 | /** 8 | * @author YvesCheung 9 | * 2017/9/6 10 | *

11 | * 处理要依附在View上的触摸事件,可以判断事件是否发生在View上,并根据View的位置纠正点击事件的x y坐标。 12 | */ 13 | public abstract class AttachToViewTouchEventHandler extends AbstractTouchEventHandler { 14 | @Override 15 | public boolean onTouch(@NonNull VIEW v, @NonNull MotionEvent e, boolean hasBeenIntercepted) { 16 | final boolean sup = super.onTouch(v, e, hasBeenIntercepted); 17 | TouchEventHandlerUtil.reviseToView(v, e); 18 | if (TouchEventHandlerUtil.isOnView(e, v)) { 19 | return onTouch(v, e, hasBeenIntercepted, true); 20 | } else if (forceMonitor()) { 21 | return onTouch(v, e, hasBeenIntercepted, false); 22 | } 23 | return sup; 24 | } 25 | 26 | /** 27 | * @param v 接受触摸事件的ui表示 28 | * @param e 触摸事件 29 | * @param hasBeenIntercepted 是否已经被前面的Handler拦截。只有当{@link #forceMonitor()}返回true时该值才有意义 30 | * @param insideView 是否触摸在View的范围内。只有当{@link #forceMonitor()}返回true时, 31 | * 才能收到当前View范围以外的触摸事件 32 | * @return 是否已经消费这个触摸事件 33 | */ 34 | public abstract boolean onTouch(@NonNull VIEW v, MotionEvent e, boolean hasBeenIntercepted, boolean insideView); 35 | } 36 | -------------------------------------------------------------------------------- /toucheventbus/src/main/java/mobile/yy/com/toucheventbus/InterceptClickHandler.java: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.toucheventbus; 2 | 3 | import androidx.annotation.NonNull; 4 | import android.util.Log; 5 | import android.view.MotionEvent; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | /** 10 | * @author YvesCheung 11 | * 2017/9/6 12 | *

13 | * 你可以实现一个子类继承于{@link InterceptClickHandler},然后拦截所有clickable或longClickable的View的触摸事件。 14 | * 注意这个Handler只会"拦截"点击事件,但不会处理点击事件,也就是不会触摸onClickListener之类的响应。 15 | */ 16 | @SuppressWarnings("WeakerAccess") 17 | public abstract class InterceptClickHandler extends AttachToViewTouchEventHandler { 18 | private static final String TAG = "TouchEventHandler"; 19 | 20 | @Override 21 | public boolean onTouch(@NonNull ViewGroup v, MotionEvent e, boolean hasBeenIntercepted, boolean insideView) { 22 | return hasBeenIntercepted 23 | || insideView 24 | && e.getAction() == MotionEvent.ACTION_UP 25 | && performClick(v, e); 26 | } 27 | 28 | /** 29 | * 判断当前触摸事件是否击中了可点击的view 30 | * 31 | * @param vg 需要判断的view范围 32 | * @param e 触摸事件 33 | * @return true如果击中了范围内的某个可点击的子view 34 | */ 35 | protected boolean performClick(ViewGroup vg, MotionEvent e) { 36 | for (int i = 0; i < vg.getChildCount(); i++) { 37 | View v = vg.getChildAt(i); 38 | if (TouchEventHandlerUtil.isOnView(e, v)) { 39 | if (v.isClickable() || v.isLongClickable()) { 40 | Log.i(TAG, name() + " hit v = " + v); 41 | return true; 42 | } else if (v instanceof ViewGroup) { 43 | if (performClick((ViewGroup) v, e)) { 44 | return true; 45 | } 46 | } 47 | } 48 | } 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /toucheventbus/src/main/java/mobile/yy/com/toucheventbus/TouchEventBus.java: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.toucheventbus; 2 | 3 | import android.app.Activity; 4 | import androidx.annotation.Nullable; 5 | import androidx.annotation.UiThread; 6 | import androidx.fragment.app.Fragment; 7 | import android.util.Log; 8 | import android.view.MotionEvent; 9 | import android.view.View; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.Iterator; 14 | import java.util.List; 15 | 16 | /** 17 | * @author YvesCheung 18 | * 2017/9/1 19 | *

20 | * 开播端直播间的触摸消息总线,负责把消息按顺序分发给TouchEventHandler 21 | */ 22 | @SuppressWarnings("unused") 23 | public class TouchEventBus { 24 | private static final String TAG = "TouchEventHandler"; 25 | private final static TouchEventBus mBus = new TouchEventBus(); 26 | private static List>> orderList = Collections.emptyList(); 27 | private final TouchEventHandlerContainer mContainer; 28 | 29 | private TouchEventBus() { 30 | mContainer = new TouchEventHandlerContainer(); 31 | } 32 | 33 | private static TouchEventBus instance() { 34 | return mBus; 35 | } 36 | 37 | /** 38 | * 分发触摸事件的起点。 39 | * 一般在Activity的dispatchEvent开始分发。一个Activity上只能有一个起点。 40 | * 41 | * @param event 触摸事件 42 | * @param parentView 起点接收触摸事件的view。用于对触摸事件的发生坐标进行调整。 43 | */ 44 | @UiThread 45 | public static boolean dispatchTouchEvent(MotionEvent event, @Nullable View parentView) { 46 | if (parentView == null) { 47 | throw new IllegalArgumentException("parent view can't be null"); 48 | } 49 | if (event == null) { 50 | return false; 51 | } 52 | MotionEvent e = MotionEvent.obtain(event); 53 | //开始一串触摸事件 54 | if (e.getAction() == MotionEvent.ACTION_DOWN) { //获取ViewTree的顺序列表 55 | orderList = new ArrayList<>(instance().mContainer.getOrderTouchEventHandler()); 56 | } 57 | //根据parentView修正触摸事件的坐标,处理parentView本身不是全屏的情况 58 | e.offsetLocation(parentView.getScrollX() + e.getRawX() - e.getX(), 59 | parentView.getScrollY() + e.getRawY() - e.getY()); 60 | boolean intercepted = false; 61 | Iterator>> itr = orderList.iterator(); 62 | while (itr.hasNext()) { 63 | final TouchEventHandler> handler = itr.next(); 64 | //为了性能这里只在每个Handler开始时copy一次MotionEvent 65 | //同一个ViewHolder前面的View如果在onTouch的时候很卑鄙地修改了MotionEvent的属性,会导致后面的View很懵逼 66 | //如果确实担心这种情况,就需要在ViewHolder每次遍历时再copy一次 67 | //如果完全不担心上层Handler修改MotionEvent,可以把下面这句obtain也去掉进一步提高性能 68 | MotionEvent copyEvent = MotionEvent.obtain(e); 69 | try { 70 | if (!intercepted || handler.forceMonitor()) { 71 | //上层没拦截 或者 自己死活都要触摸事件 72 | intercepted = dispatchInner(handler, intercepted, copyEvent); 73 | } else { 74 | //已经被拦截,发一个Cancel事件,而且不再参与接下来同一串的触摸事件 75 | copyEvent.setAction(MotionEvent.ACTION_CANCEL); 76 | dispatchInner(handler, true, copyEvent); 77 | itr.remove(); 78 | } 79 | } finally { 80 | copyEvent.recycle(); 81 | } 82 | } 83 | //结束触摸后,清掉orderList防止内存泄漏 84 | if (e.getAction() == MotionEvent.ACTION_UP 85 | || e.getAction() == MotionEvent.ACTION_CANCEL) { 86 | orderList = Collections.emptyList(); 87 | } 88 | e.recycle(); 89 | return intercepted; 90 | } 91 | 92 | /** 93 | * @see #dispatchTouchEvent(MotionEvent, View) 94 | */ 95 | @UiThread 96 | public static boolean dispatchTouchEvent(MotionEvent event, Activity entry) { 97 | return dispatchTouchEvent(event, entry.getWindow().getDecorView() 98 | .findViewById(android.R.id.content)); 99 | } 100 | 101 | /** 102 | * @see #dispatchTouchEvent(MotionEvent, View) 103 | */ 104 | @UiThread 105 | public static boolean dispatchTouchEvent(MotionEvent event, Fragment entry) { 106 | return dispatchTouchEvent(event, entry.getView()); 107 | } 108 | 109 | /** 110 | * @see #dispatchTouchEvent(MotionEvent, View) 111 | */ 112 | @UiThread 113 | public static boolean dispatchTouchEvent(MotionEvent event, android.app.Fragment entry) { 114 | return dispatchTouchEvent(event, entry.getView()); 115 | } 116 | 117 | @SuppressWarnings("unchecked") 118 | private static boolean dispatchInner( 119 | TouchEventHandler> handler, 120 | boolean intercepted, MotionEvent e) { 121 | final TouchEventHandler> h = 122 | (TouchEventHandler>) handler; 123 | boolean interceptChild = intercepted; 124 | TouchViewHolder vh = h.getViewHolder(); 125 | for (VIEW v : vh.getView()) { 126 | interceptChild = h.onTouch(v, e, intercepted) || interceptChild; 127 | } 128 | return interceptChild; 129 | } 130 | 131 | /** 132 | * 获取对应的触摸事件处理器 133 | * 134 | * @param ui的类型,可以是具体的View或者抽象的接口 135 | * @param cls {@link TouchEventHandler} 的class 136 | * @return 该 {@link TouchEventHandler} 的 {@link TouchViewHolder} 137 | */ 138 | @SuppressWarnings("unchecked") 139 | @UiThread 140 | public static TouchViewHolder of(Class>> cls) { 141 | TouchEventHandler handler = instance().mContainer.getHandler(cls); 142 | if (handler == null) { 143 | try { 144 | handler = cls.newInstance(); 145 | instance().mContainer.put(handler); 146 | } catch (InstantiationException e) { 147 | Log.e(TAG, e.getMessage()); 148 | } catch (IllegalAccessException e) { 149 | Log.e(TAG, e.getMessage()); 150 | } 151 | } 152 | if (handler == null) { 153 | throw new IllegalStateException(cls + "需要一个无参的构造函数"); 154 | } 155 | return handler.getViewHolder(); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /toucheventbus/src/main/java/mobile/yy/com/toucheventbus/TouchEventHandler.java: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.toucheventbus; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | import android.view.MotionEvent; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author YvesCheung 11 | * 2017/9/4 12 | * 13 | *

触摸处理器

14 | *

所有需要触摸事件的业务都应该是一个独立的{@link TouchEventHandler}。比如上下滑切换直播间是一个Handler, 15 | * 左滑出现菜单是一个Handler,开播的摄像头手势缩放或者点击对焦是一个Handler,插件玩法需要触摸事件处理也是一 16 | * 个Handler等等。

17 | *

每个Handler自己定义需要拦截哪些Handler。当触摸事件发生时,TouchEventBus并不是根据View树的顺序来分发事件, 18 | * 而是根据业务的需要,逐个Handler分发。通过{@link #nextHandler()}方法给出在此业务下方的Handler列表。那么 19 | * 当前Handler就会先于它们接收到触摸事件,从而判断是否需要拦截或者放开。

20 | *

在{@link #onTouch(Object, MotionEvent, boolean)}方法写对触摸事件处理的业务。

21 | *

如果当前Handler非常的霸道,无论前面的Handler是否已经拦截处理了触摸事件,自己都还是要收到触摸事件,那么就让 22 | * {@link #forceMonitor()}返回true。

23 | */ 24 | public interface TouchEventHandler> { 25 | 26 | /** 27 | * @param view 需要对触摸事件作出反应的ui 28 | * @param e 触摸事件 29 | * @param hasBeenIntercepted 是否已经被前面的Handler处理过。当且仅当{@link #forceMonitor()}返回true时, 30 | * 该字段才有意义 31 | * @return 已经对触摸事件作出处理返回true 32 | */ 33 | boolean onTouch(@NonNull VIEW view, @NonNull MotionEvent e, boolean hasBeenIntercepted); 34 | 35 | /** 36 | * @return 返回在自己后面的Handler列表 37 | */ 38 | @Nullable 39 | List>>> nextHandler(); 40 | 41 | /** 42 | * 所有的ui都会存放在ViewHolder中。一个Handler会处理ViewHolder中所有View的事件。 43 | * 理论上一个Handler只会对应一个ui类。 44 | * ViewHolder是为了存放一个View可能会在同一时间内出现的多个实例。 45 | * 比如后一个Fragment实例B在前一个实例A的onDestroy之前先onCreate。 46 | * 47 | * @return 获取当前Handler的 {@link TouchViewHolder} 48 | */ 49 | @NonNull 50 | HOLDER getViewHolder(); 51 | 52 | /** 53 | * 是否强制监听触摸事件 54 | * 55 | * @return 返回true的话,无论前面的Handler是否已经处理过触摸事件,{@link #onTouch(Object, MotionEvent, boolean)} 56 | * 还是能收到触摸事件,但hasBeenIntercepted字段会为true。返回false的话,不会再收到触摸事件除非前面的Handler全 57 | * 都不处理 58 | */ 59 | boolean forceMonitor(); 60 | } 61 | -------------------------------------------------------------------------------- /toucheventbus/src/main/java/mobile/yy/com/toucheventbus/TouchEventHandlerContainer.java: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.toucheventbus; 2 | 3 | import android.util.Log; 4 | 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.Iterator; 8 | import java.util.LinkedHashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | /** 13 | * @author YvesCheung 14 | * 2017/9/5 15 | *

16 | * 用于对{@link TouchEventHandler}根据 {@link TouchEventHandler#nextHandler()}来进行排序 17 | */ 18 | class TouchEventHandlerContainer { 19 | private static final String TAG = "TouchEventHandler"; 20 | private Map, TouchEventHandler>> 21 | mHandlers = new LinkedHashMap<>(); 22 | private List>> mContainer = new ArrayList<>(); 23 | 24 | /** 25 | * 拓扑排序 26 | */ 27 | @SuppressWarnings("unchecked") 28 | void put(TouchEventHandler> handler) { 29 | if (handler == null) { 30 | return; 31 | } 32 | 33 | Iterator, TouchEventHandler>>> 34 | it = mHandlers.entrySet().iterator(); 35 | while (it.hasNext()) { 36 | Map.Entry, TouchEventHandler>> 37 | entry = it.next(); 38 | if (entry.getValue().getViewHolder().getView().isEmpty()) { 39 | Log.i(TAG, "remove " + entry.getValue()); 40 | mContainer.remove(entry.getValue()); 41 | it.remove(); 42 | } 43 | } 44 | 45 | Map, Integer> handlerCnt = new HashMap<>(); 46 | mHandlers.put(handler.getClass(), handler); 47 | mContainer.add(handler); 48 | for (TouchEventHandler> h : mContainer) { 49 | handlerCnt.put(h.getClass(), 0); 50 | } 51 | for (TouchEventHandler> h : mContainer) { 52 | List>>> clz = h.nextHandler(); 53 | if (clz != null) { 54 | for (Class cls : clz) { 55 | if (cls != null && mHandlers.containsKey(cls)) { 56 | int cnt = handlerCnt.get(cls); 57 | handlerCnt.put(cls, cnt + 1); 58 | } 59 | } 60 | } 61 | Log.i(TAG, "handlerCnt = " + handlerCnt); 62 | } 63 | mContainer = new ArrayList<>(); 64 | while (handlerCnt.size() > 0) { 65 | Class hasKey = null; 66 | List>>> nextKey = null; 67 | for (Map.Entry, Integer> e : handlerCnt.entrySet()) { 68 | if (e.getValue() <= 0) { 69 | hasKey = e.getKey(); 70 | TouchEventHandler> h = mHandlers.get(hasKey); 71 | if (h != null) { 72 | mContainer.add(h); 73 | nextKey = h.nextHandler(); 74 | } 75 | break; 76 | } 77 | } 78 | 79 | if (hasKey != null) { 80 | handlerCnt.remove(hasKey); 81 | } else { 82 | //Log.e(TAG, "TouchHandler的事件分发存在环路,请检查nextHandler"); 83 | throw new IllegalStateException("TouchHandler的事件分发存在环路,请检查nextHandler"); 84 | } 85 | 86 | if (nextKey != null) { 87 | for (Class nk : nextKey) { 88 | if (nk != null && mHandlers.containsKey(nk)) { 89 | int cnt = handlerCnt.get(nk); 90 | handlerCnt.put(nk, cnt - 1); 91 | } 92 | } 93 | } 94 | } 95 | Log.i(TAG, "order list = " + mContainer); 96 | } 97 | 98 | public void remove(TouchEventHandler> handler) { 99 | if (handler != null) { 100 | // mContainer.remove(handler); 101 | mHandlers.remove(handler.getClass()); 102 | } 103 | } 104 | 105 | List>> getOrderTouchEventHandler() { 106 | return mContainer; 107 | } 108 | 109 | TouchEventHandler getHandler(Class cls) { 110 | return mHandlers.get(cls); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /toucheventbus/src/main/java/mobile/yy/com/toucheventbus/TouchEventHandlerUtil.java: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.toucheventbus; 2 | 3 | import android.graphics.Rect; 4 | import android.view.MotionEvent; 5 | import android.view.View; 6 | 7 | import static android.view.MotionEvent.ACTION_BUTTON_PRESS; 8 | import static android.view.MotionEvent.ACTION_BUTTON_RELEASE; 9 | import static android.view.MotionEvent.ACTION_CANCEL; 10 | import static android.view.MotionEvent.ACTION_DOWN; 11 | import static android.view.MotionEvent.ACTION_HOVER_ENTER; 12 | import static android.view.MotionEvent.ACTION_HOVER_EXIT; 13 | import static android.view.MotionEvent.ACTION_HOVER_MOVE; 14 | import static android.view.MotionEvent.ACTION_MASK; 15 | import static android.view.MotionEvent.ACTION_MOVE; 16 | import static android.view.MotionEvent.ACTION_OUTSIDE; 17 | import static android.view.MotionEvent.ACTION_POINTER_DOWN; 18 | import static android.view.MotionEvent.ACTION_POINTER_INDEX_MASK; 19 | import static android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT; 20 | import static android.view.MotionEvent.ACTION_POINTER_UP; 21 | import static android.view.MotionEvent.ACTION_SCROLL; 22 | import static android.view.MotionEvent.ACTION_UP; 23 | 24 | /** 25 | * @author YvesCheung 26 | * 2017/9/6 27 | */ 28 | @SuppressWarnings("WeakerAccess") 29 | public class TouchEventHandlerUtil { 30 | private TouchEventHandlerUtil() { 31 | } 32 | 33 | /** 34 | * 把触摸事件修正到对应的view上。使{@link MotionEvent#getX()}和{@link MotionEvent#getY()}的相对位置 35 | * 是在view上的相对位置 36 | * 37 | * @param v 对应的View 38 | * @param event 需要修正的触摸事件 39 | */ 40 | public static void reviseToView(View v, MotionEvent event) { 41 | int[] location = new int[2]; 42 | v.getLocationOnScreen(location); 43 | event.offsetLocation(-location[0], -location[1]); 44 | } 45 | 46 | /** 47 | * 判断触摸事件是否在对应的view上 48 | * 49 | * @param e 触摸事件 50 | * @param v 对应的View 51 | * @return true 当且仅当触摸事件e是发生在v上面 52 | */ 53 | public static boolean isOnView(MotionEvent e, View v) { 54 | Rect r = new Rect(); 55 | v.getGlobalVisibleRect(r); 56 | return r.left <= e.getRawX() && e.getRawX() <= r.right 57 | && 58 | r.top <= e.getRawY() && e.getRawY() <= r.bottom; 59 | } 60 | 61 | /** 62 | * 两点之间的距离 63 | * 64 | * @param x1 点A的x坐标 65 | * @param y1 点A的y坐标 66 | * @param x2 点B的x坐标 67 | * @param y2 点B的y坐标 68 | * @return 点A和点B之间的距离 69 | */ 70 | public static float distance(float x1, float y1, float x2, float y2) { 71 | float x = x1 - x2; 72 | float y = y1 - y2; 73 | return (float) Math.sqrt(x * x + y * y); 74 | } 75 | 76 | /** 77 | * 计算MotionEvent事件二点间的距离 78 | * 79 | * @param event 触摸事件 80 | * @return 二点间的距离。-1表示该触摸只有一个手指 81 | */ 82 | public static float spacing(MotionEvent event) { 83 | if (event.getPointerCount() >= 2) { 84 | return distance(event.getX(0), event.getY(0), event.getX(1), event.getY(1)); 85 | } 86 | return -1; 87 | } 88 | 89 | /** 90 | * 如果是多点触摸,只保留一只手指的触摸事件。用于想用多指操作来触发Android设计给单指使用的View 91 | * 92 | * @param origin 原触摸事件 93 | * @param newEvent 只保留一只手指的新触摸事件回调 94 | */ 95 | public static void removePointers(MotionEvent origin, Consumer newEvent) { 96 | int action; 97 | switch (origin.getActionMasked()) { 98 | case ACTION_POINTER_DOWN: 99 | action = ACTION_DOWN; 100 | break; 101 | case ACTION_POINTER_UP: 102 | action = ACTION_UP; 103 | break; 104 | default: 105 | action = origin.getAction(); 106 | break; 107 | } 108 | final MotionEvent copy = MotionEvent.obtain(origin.getDownTime(), origin.getEventTime(), action, 109 | origin.getX(), origin.getY(), origin.getMetaState()); 110 | newEvent.accept(copy); 111 | copy.recycle(); 112 | } 113 | 114 | /** 115 | * copy from {@link MotionEvent} 116 | * Returns a string that represents the symbolic name of the specified unmasked action 117 | * such as "ACTION_DOWN", "ACTION_POINTER_DOWN(3)" or an equivalent numeric constant 118 | * such as "35" if unknown. 119 | * 120 | * @param action The unmasked action. 121 | * @return The symbolic name of the specified action. 122 | */ 123 | public static String actionToString(int action) { 124 | switch (action) { 125 | case ACTION_DOWN: 126 | return "ACTION_DOWN"; 127 | case ACTION_UP: 128 | return "ACTION_UP"; 129 | case ACTION_CANCEL: 130 | return "ACTION_CANCEL"; 131 | case ACTION_OUTSIDE: 132 | return "ACTION_OUTSIDE"; 133 | case ACTION_MOVE: 134 | return "ACTION_MOVE"; 135 | case ACTION_HOVER_MOVE: 136 | return "ACTION_HOVER_MOVE"; 137 | case ACTION_SCROLL: 138 | return "ACTION_SCROLL"; 139 | case ACTION_HOVER_ENTER: 140 | return "ACTION_HOVER_ENTER"; 141 | case ACTION_HOVER_EXIT: 142 | return "ACTION_HOVER_EXIT"; 143 | case ACTION_BUTTON_PRESS: 144 | return "ACTION_BUTTON_PRESS"; 145 | case ACTION_BUTTON_RELEASE: 146 | return "ACTION_BUTTON_RELEASE"; 147 | } 148 | int index = (action & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT; 149 | switch (action & ACTION_MASK) { 150 | case ACTION_POINTER_DOWN: 151 | return "ACTION_POINTER_DOWN(" + index + ")"; 152 | case ACTION_POINTER_UP: 153 | return "ACTION_POINTER_UP(" + index + ")"; 154 | default: 155 | return Integer.toString(action); 156 | } 157 | } 158 | 159 | public interface Consumer { 160 | void accept(T a); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /toucheventbus/src/main/java/mobile/yy/com/toucheventbus/TouchViewHolder.java: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.toucheventbus; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | /** 9 | * @author YvesCheung 10 | * 2017/9/8 11 | */ 12 | public class TouchViewHolder { 13 | private final Set set = new HashSet<>(); 14 | 15 | @NonNull 16 | Set getView() { 17 | return set; 18 | } 19 | 20 | /** 21 | * 把TouchEventHandler绑定到指定的ui上 22 | * 23 | * @param v ui:它具体可能并不是一个View,可以是一个接口,或者对触摸事件作出反应的对象。 24 | * @see #dettach(Object) 25 | */ 26 | public void attach(VIEW v) { 27 | if (v != null) { 28 | set.add(v); 29 | } 30 | } 31 | 32 | /** 33 | * 当不再需要触摸事件时,需要解除绑定 34 | * 35 | * @param v 通过{@link #attach(Object)}方法绑定的表示层 36 | * @see #attach(Object) 37 | */ 38 | public void dettach(VIEW v) { 39 | if (v != null) { 40 | set.remove(v); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /toucheventbus/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TouchEventBus 3 | 4 | -------------------------------------------------------------------------------- /touchsample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /touchsample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 30 7 | defaultConfig { 8 | applicationId "mobile.yy.com.touchsample" 9 | minSdkVersion 14 10 | targetSdkVersion 30 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | 22 | compileOptions { 23 | sourceCompatibility JavaVersion.VERSION_1_8 24 | targetCompatibility JavaVersion.VERSION_1_8 25 | } 26 | 27 | kotlinOptions { 28 | jvmTarget = JavaVersion.VERSION_1_8.toString() 29 | } 30 | } 31 | 32 | dependencies { 33 | implementation 'androidx.appcompat:appcompat:1.3.0' 34 | implementation project(":toucheventbus") 35 | implementation 'com.gxz.pagerslidingtabstrip:library:1.3.1' 36 | implementation 'io.reactivex.rxjava2:rxjava:2.1.13' 37 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' 38 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 39 | } -------------------------------------------------------------------------------- /touchsample/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 | -------------------------------------------------------------------------------- /touchsample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/App.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample 2 | 3 | import android.app.Application 4 | import mobile.yy.com.touchsample.inject.Injector 5 | 6 | /** 7 | * @author YvesCheung 8 | * 2018/4/25 9 | */ 10 | class App : Application() { 11 | 12 | companion object { 13 | val injector = Injector() 14 | 15 | fun instance() = this 16 | } 17 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/inject/Injector.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.inject 2 | 3 | import mobile.yy.com.touchsample.model.TabRepo 4 | import mobile.yy.com.touchsample.presenter.MainPagePresenter 5 | import mobile.yy.com.touchsample.presenter.MainTabPresenter 6 | import mobile.yy.com.touchsample.presenter.SubTabPresenter 7 | 8 | /** 9 | * @author YvesCheung 10 | * 2018/4/25 11 | */ 12 | class Injector { 13 | 14 | private val tabRepo = TabRepo() 15 | 16 | fun getMainTabPresenter(bizId: Int) = MainTabPresenter(bizId, tabRepo) 17 | 18 | fun getMainPagePresenter() = MainPagePresenter(tabRepo) 19 | 20 | fun getSubTabPresenter(subBizId: Int) = SubTabPresenter(subBizId, tabRepo) 21 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/model/Tab.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.model 2 | 3 | import androidx.annotation.ColorInt 4 | 5 | /** 6 | * @author YvesCheung 7 | * 2018/4/25 8 | */ 9 | data class MainTab( 10 | val bizId: Int, 11 | val tabName: String, 12 | @ColorInt val backgroundColor: Int, 13 | val subTab: List 14 | ) 15 | 16 | data class SubTab( 17 | val bizId: Int, 18 | val subBizId: Int, 19 | val tabName: String, 20 | var textSize: Float 21 | ) -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/model/TabRepo.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.model 2 | 3 | import android.util.SparseArray 4 | import io.reactivex.Observable 5 | import io.reactivex.Single 6 | import java.util.* 7 | 8 | /** 9 | * @author YvesCheung 10 | * 2018/4/25 11 | */ 12 | @Suppress("MemberVisibilityCanBePrivate") 13 | class TabRepo { 14 | 15 | private val random = Random() 16 | 17 | private val mainTabStore = SparseArray() 18 | 19 | private val subTabStore = SparseArray() 20 | 21 | private val randomColor 22 | get() = (0x40 shl 24) or random.nextInt(0xffffff) 23 | 24 | private fun getBizId() = listOf(0, 1, 2, 3, 4) 25 | 26 | fun getTabs(): Single> { 27 | return Observable.fromIterable(getBizId()) 28 | .flatMapSingle { bizId -> getTab(bizId) } 29 | .reduce(mutableListOf()) { list: MutableList, tab: MainTab -> 30 | list.apply { add(tab) } 31 | } 32 | } 33 | 34 | fun getTab(bizId: Int): Single { 35 | return getSubTabs(bizId) 36 | .map { MainTab(bizId, "Tab$bizId", randomColor, it) } 37 | .doOnSuccess { mainTabStore.put(bizId, it) } 38 | } 39 | 40 | fun getSubTabs(bizId: Int): Single> { 41 | return Observable.fromArray(0, 1, 2) 42 | .map { idx -> SubTab(bizId, bizId * 10 + idx, "subTab$idx", 60f) } 43 | .doOnNext { tab -> subTabStore.put(tab.subBizId, tab) } 44 | .reduce(mutableListOf()) { list: MutableList, tab: SubTab -> 45 | list.apply { add(tab) } 46 | } 47 | } 48 | 49 | fun getCacheTab(bizId: Int): MainTab { 50 | return mainTabStore[bizId]!! 51 | } 52 | 53 | fun getCacheSubTab(subBizId: Int): SubTab { 54 | return subTabStore[subBizId]!! 55 | } 56 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/presenter/MainPagePresenter.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.presenter 2 | 3 | import io.reactivex.Single 4 | import mobile.yy.com.touchsample.model.MainTab 5 | import mobile.yy.com.touchsample.model.TabRepo 6 | 7 | /** 8 | * @author YvesCheung 9 | * 2018/4/25 10 | */ 11 | class MainPagePresenter(private val repo: TabRepo) { 12 | 13 | fun getBizTab(): Single> = repo.getTabs() 14 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/presenter/MainTabPresenter.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.presenter 2 | 3 | import mobile.yy.com.touchsample.model.TabRepo 4 | 5 | /** 6 | * @author YvesCheung 7 | * 2018/4/25 8 | */ 9 | class MainTabPresenter(private val bizId: Int, private val repo: TabRepo) { 10 | 11 | fun getTab() = repo.getCacheTab(bizId) 12 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/presenter/SubTabPresenter.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.presenter 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.subjects.BehaviorSubject 5 | import mobile.yy.com.touchsample.model.TabRepo 6 | 7 | /** 8 | * @author YvesCheung 9 | * 2018/4/25 10 | */ 11 | class SubTabPresenter(subTabBiz: Int, repo: TabRepo) { 12 | 13 | private val subTab = repo.getCacheSubTab(subTabBiz) 14 | 15 | private val mainTab = repo.getCacheTab(subTab.bizId) 16 | 17 | private val onTextSizeChange = BehaviorSubject.createDefault(subTab.textSize) 18 | 19 | fun getContent() = "${mainTab.tabName}_${subTab.tabName}" 20 | 21 | fun getTextSize(): Observable = onTextSizeChange.hide() 22 | 23 | fun setTextSize(textSize: Float) { 24 | var size = textSize 25 | if (size > 200f) size = 200f 26 | else if (size < 30f) size = 30f 27 | subTab.textSize = size 28 | onTextSizeChange.onNext(size) 29 | } 30 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/touch/BackgroundImageTouchHandler.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.touch 2 | 3 | import androidx.viewpager.widget.ViewPager 4 | import android.view.MotionEvent 5 | import mobile.yy.com.toucheventbus.AttachToViewTouchEventHandler 6 | import mobile.yy.com.toucheventbus.TouchEventHandler 7 | import mobile.yy.com.toucheventbus.TouchEventHandlerUtil.removePointers 8 | import mobile.yy.com.toucheventbus.TouchViewHolder 9 | 10 | /** 11 | * @author YvesCheung 12 | * 2018/4/25 13 | * 14 | * 双指左右滑动的时候,使背景图的ViewPager发生滑动。 15 | * 1,在onTouch方法中判断是否双指滑动,双指的话就给背景图dispatch触摸。如果不是双指的move,就不处理。 16 | * 2,在defineNextHandler方法中拦截选项卡滑动的处理。 17 | */ 18 | class BackgroundImageTouchHandler : AttachToViewTouchEventHandler() { 19 | 20 | private var twoPointer = false 21 | 22 | override fun onTouch(v: ViewPager, e: MotionEvent, hasBeenIntercepted: Boolean, insideView: Boolean): Boolean { 23 | //针对双指滑动的情况,全部触摸事件交由背景图处理 24 | if (e.pointerCount == 2) { 25 | twoPointer = true 26 | } 27 | //down up cancel比较特殊,都要传递下去 28 | if (twoPointer 29 | || e.action == MotionEvent.ACTION_DOWN 30 | || e.action == MotionEvent.ACTION_UP 31 | || e.action == MotionEvent.ACTION_CANCEL) { 32 | //双指滑动在ViewPager上会很不流畅,所以先把它模拟成单指传递下去 33 | removePointers(e) { event -> v.dispatchTouchEvent(event) } 34 | } 35 | //结束双指滑动 36 | if (e.action == MotionEvent.ACTION_UP 37 | || e.action == MotionEvent.ACTION_CANCEL) { 38 | twoPointer = false 39 | } 40 | return twoPointer 41 | } 42 | 43 | /** 44 | * 背景滑动的优先级要比选项卡滑动高 45 | */ 46 | override fun defineNextHandlers(handlers: MutableList>>>) { 47 | handlers.add(TabTouchHandler::class.java) 48 | } 49 | 50 | override fun name() = "BackgroundImageTouchHandler" 51 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/touch/MenuTouchHandler.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.touch 2 | 3 | import android.view.MotionEvent 4 | import android.view.VelocityTracker 5 | import mobile.yy.com.toucheventbus.AbstractTouchEventHandler 6 | import mobile.yy.com.toucheventbus.TouchEventHandler 7 | import mobile.yy.com.toucheventbus.TouchViewHolder 8 | import mobile.yy.com.touchsample.ui.FakeMenu 9 | 10 | /** 11 | * @author YvesCheung 12 | * 2018/4/26 13 | */ 14 | class MenuTouchHandler : AbstractTouchEventHandler() { 15 | 16 | private var touchForMenu = false 17 | private var lastX = 0f 18 | private var velocityTracker: VelocityTracker? = null 19 | 20 | override fun onTouch(ui: FakeMenu, e: MotionEvent, hasBeenIntercepted: Boolean): Boolean { 21 | super.onTouch(ui, e, hasBeenIntercepted) 22 | when (e.action) { 23 | MotionEvent.ACTION_DOWN -> { 24 | if (e.x < 100 || ui.isOpenOrOpening()) { 25 | touchForMenu = true 26 | lastX = e.x 27 | ui.down() 28 | velocityTracker = VelocityTracker.obtain() 29 | } else { 30 | touchForMenu = false 31 | } 32 | } 33 | MotionEvent.ACTION_MOVE -> { 34 | if (touchForMenu) { 35 | velocityTracker?.addMovement(e) 36 | ui.move(e.x - lastX) 37 | lastX = e.x 38 | } 39 | } 40 | MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { 41 | if (touchForMenu) { 42 | velocityTracker?.computeCurrentVelocity(1) 43 | ui.up(velocityTracker?.xVelocity ?: 0f) 44 | velocityTracker?.recycle() 45 | } 46 | } 47 | } 48 | return touchForMenu 49 | } 50 | 51 | override fun defineNextHandlers(handlers: MutableList>>>) { 52 | handlers.add(BackgroundImageTouchHandler::class.java) 53 | handlers.add(TabTouchHandler::class.java) 54 | handlers.add(ZoomTextTouchHandler::class.java) 55 | } 56 | 57 | override fun name() = "MenuTouchHandler" 58 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/touch/SlidingTabTouchHandler.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.touch 2 | 3 | import android.view.MotionEvent 4 | import com.gxz.PagerSlidingTabStrip 5 | import mobile.yy.com.toucheventbus.AttachToViewTouchEventHandler 6 | 7 | /** 8 | * @author YvesCheung 9 | * 2018/4/25 10 | * 11 | * PagerSlidingTabStrip的点击事件 12 | */ 13 | class SlidingTabTouchHandler : AttachToViewTouchEventHandler() { 14 | 15 | override fun onTouch(v: PagerSlidingTabStrip, e: MotionEvent, hasBeenIntercepted: Boolean, insideView: Boolean): Boolean { 16 | v.dispatchTouchEvent(e) 17 | return false 18 | } 19 | 20 | override fun nextHandler() = listOf(TabTouchHandler::class.java) 21 | 22 | override fun name() = "SlidingTabTouchHandler" 23 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/touch/TabTouchHandler.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.touch 2 | 3 | import androidx.viewpager.widget.ViewPager 4 | import android.view.MotionEvent 5 | import mobile.yy.com.toucheventbus.AttachToViewTouchEventHandler 6 | 7 | /** 8 | * @author YvesCheung 9 | * 2018/4/25 10 | */ 11 | class TabTouchHandler : AttachToViewTouchEventHandler() { 12 | 13 | override fun onTouch(v: ViewPager, e: MotionEvent, hasBeenIntercepted: Boolean, insideView: Boolean): Boolean { 14 | return hasBeenIntercepted || v.dispatchTouchEvent(e) 15 | } 16 | 17 | //强行接收触摸事件 即使不是在ViewPager上的滑动也能收到 18 | //所以在下面的导航栏上滑动可以切换一级选项卡 19 | override fun forceMonitor() = true 20 | 21 | override fun name() = "TabTouchEventBus" 22 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/touch/ZoomTextTouchHandler.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.touch 2 | 3 | import android.view.MotionEvent 4 | import mobile.yy.com.toucheventbus.AbstractTouchEventHandler 5 | import mobile.yy.com.toucheventbus.TouchEventHandlerUtil.spacing 6 | import mobile.yy.com.touchsample.ui.ZoomUi 7 | 8 | /** 9 | * @author YvesCheung 10 | * 2018/4/26 11 | * 12 | * 双指缩放字体的触摸处理 13 | */ 14 | class ZoomTextTouchHandler : AbstractTouchEventHandler() { 15 | 16 | companion object { 17 | //判断为缩放的变化距离阈值 18 | private const val ZOOM_MIN_VALUE = 60f 19 | //缩放距离转化缩放百分比 20 | private const val ZOOM_REGULAR = 400f 21 | } 22 | 23 | private var space = 0f 24 | private var zoom = false 25 | 26 | override fun onTouch(ui: ZoomUi, e: MotionEvent, hasBeenIntercepted: Boolean): Boolean { 27 | super.onTouch(ui, e, hasBeenIntercepted) 28 | when (e.actionMasked) { 29 | MotionEvent.ACTION_POINTER_DOWN -> { 30 | //两只手指都按到屏幕开始 31 | space = spacing(e) 32 | } 33 | MotionEvent.ACTION_MOVE -> { 34 | val current = spacing(e) 35 | val distance = current - space 36 | if (zoom) { 37 | ui.resize(percentage = distance / ZOOM_REGULAR) 38 | space = current 39 | } else if (Math.abs(distance) > ZOOM_MIN_VALUE) { 40 | zoom = true 41 | } 42 | } 43 | MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_CANCEL -> { 44 | //双指离开屏幕时结束缩放 45 | zoom = false 46 | space = 0f 47 | } 48 | } 49 | return zoom 50 | } 51 | 52 | override fun nextHandler() = listOf( 53 | SlidingTabTouchHandler::class.java, 54 | TabTouchHandler::class.java, 55 | BackgroundImageTouchHandler::class.java 56 | ) 57 | 58 | override fun name() = "ZoomTextTouchHandler" 59 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/ui/BackgroundFragment.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.ui 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.viewpager.widget.PagerAdapter 8 | import androidx.viewpager.widget.ViewPager 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.widget.ImageView 13 | import mobile.yy.com.toucheventbus.TouchEventBus 14 | import mobile.yy.com.touchsample.R 15 | import mobile.yy.com.touchsample.touch.BackgroundImageTouchHandler 16 | 17 | /** 18 | * @author YvesCheung 19 | * 2018/4/25 20 | */ 21 | class BackgroundFragment : Fragment() { 22 | 23 | private lateinit var viewPager: ViewPager 24 | 25 | @SuppressLint("InflateParams") 26 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { 27 | return ViewPager(inflater.context).also { viewPager = it } 28 | } 29 | 30 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 31 | super.onViewCreated(view, savedInstanceState) 32 | viewPager.adapter = BackgroundAdapter(view.context) 33 | TouchEventBus.of(BackgroundImageTouchHandler::class.java).attach(viewPager) 34 | } 35 | 36 | override fun onDestroyView() { 37 | TouchEventBus.of(BackgroundImageTouchHandler::class.java).dettach(viewPager) 38 | super.onDestroyView() 39 | } 40 | } 41 | 42 | class BackgroundAdapter(private val context: Context) : PagerAdapter() { 43 | 44 | private val gallery = Array(3) { idx -> 45 | ImageView(context).apply { 46 | scaleType = ImageView.ScaleType.CENTER_CROP 47 | setImageResource(when (idx) { 48 | 0 -> R.drawable.main_background 49 | 1 -> R.drawable.main_background2 50 | else -> R.drawable.main_background3 51 | }) 52 | } 53 | } 54 | 55 | override fun instantiateItem(container: ViewGroup, position: Int): Any { 56 | val imageView = gallery[position] 57 | if (imageView.parent == null) { 58 | container.addView(imageView) 59 | } 60 | return imageView 61 | } 62 | 63 | override fun destroyItem(container: ViewGroup, position: Int, imageView: Any) { 64 | if (imageView is View) { 65 | container.removeView(imageView) 66 | } 67 | } 68 | 69 | override fun isViewFromObject(view: View, imageView: Any) = view == imageView 70 | 71 | override fun getCount() = gallery.size 72 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/ui/FakeMenu.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.ui 2 | 3 | import android.animation.ValueAnimator 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.graphics.Typeface 7 | import android.os.Build 8 | import android.util.AttributeSet 9 | import android.view.Gravity 10 | import androidx.appcompat.widget.AppCompatTextView 11 | import mobile.yy.com.toucheventbus.TouchEventBus 12 | import mobile.yy.com.touchsample.R 13 | import mobile.yy.com.touchsample.touch.MenuTouchHandler 14 | 15 | /** 16 | * @author YvesCheung 17 | * 2018/4/26 18 | */ 19 | class FakeMenu @JvmOverloads constructor( 20 | context: Context, 21 | attrs: AttributeSet? = null, 22 | defStyleAttr: Int = 0 23 | ) : AppCompatTextView(context, attrs, defStyleAttr) { 24 | 25 | companion object { 26 | const val DEFAULT_VELOCITY = 6f 27 | } 28 | 29 | init { 30 | text = "PANEL" 31 | setTextColor(Color.parseColor("#ff3399")) 32 | textSize = 25f 33 | gravity = Gravity.CENTER 34 | setBackgroundColor(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 35 | resources.getColor(R.color.colorPrimary, null) 36 | } else { 37 | resources.getColor(R.color.colorPrimary) 38 | }) 39 | typeface = Typeface.DEFAULT_BOLD 40 | } 41 | 42 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 43 | super.onSizeChanged(w, h, oldw, oldh) 44 | x = -w.toFloat() 45 | } 46 | 47 | override fun onFinishInflate() { 48 | super.onFinishInflate() 49 | TouchEventBus.of(MenuTouchHandler::class.java).attach(this) 50 | } 51 | 52 | override fun onDetachedFromWindow() { 53 | TouchEventBus.of(MenuTouchHandler::class.java).dettach(this) 54 | super.onDetachedFromWindow() 55 | } 56 | 57 | private 58 | var animator = ValueAnimator.ofFloat(1f) 59 | 60 | fun down() { 61 | animator.cancel() 62 | } 63 | 64 | fun move(dx: Float) { 65 | x = Math.min(Math.max(dx + x, -measuredWidth.toFloat()), 0f) 66 | } 67 | 68 | fun up(velocity: Float) { 69 | val startX = x 70 | fun animate(velocity: Float, dis: Float) { 71 | val duration = Math.abs(dis / velocity) 72 | animator = ValueAnimator.ofFloat(1f).apply { 73 | this.duration = duration.toLong() 74 | addUpdateListener { 75 | val percentage = it.animatedFraction 76 | x = startX + dis * percentage 77 | } 78 | start() 79 | } 80 | } 81 | 82 | fun open(velocity: Float) { 83 | val dis = -startX 84 | animate(velocity, dis) 85 | } 86 | 87 | fun close(velocity: Float) { 88 | val dis = startX + measuredWidth 89 | animate(velocity, -dis) 90 | } 91 | 92 | if (velocity > DEFAULT_VELOCITY) { 93 | open(velocity) 94 | } else if (velocity < -DEFAULT_VELOCITY) { 95 | close(velocity) 96 | } else if (startX > -measuredWidth / 2f && startX < 0f) { 97 | open(DEFAULT_VELOCITY) 98 | } else if (startX <= -measuredWidth / 2f && startX > -measuredWidth) { 99 | close(DEFAULT_VELOCITY) 100 | } 101 | } 102 | 103 | fun isOpenOrOpening() = x > -measuredWidth 104 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.ui 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import androidx.fragment.app.FragmentManager 6 | import androidx.fragment.app.FragmentPagerAdapter 7 | import androidx.appcompat.app.AppCompatActivity 8 | import android.view.MotionEvent 9 | import kotlinx.android.synthetic.main.activity_main.* 10 | import mobile.yy.com.toucheventbus.TouchEventBus 11 | import mobile.yy.com.touchsample.App 12 | import mobile.yy.com.touchsample.R 13 | import mobile.yy.com.touchsample.model.MainTab 14 | import mobile.yy.com.touchsample.touch.SlidingTabTouchHandler 15 | import mobile.yy.com.touchsample.touch.TabTouchHandler 16 | 17 | class MainActivity : AppCompatActivity() { 18 | 19 | private val presenter by lazy { App.injector.getMainPagePresenter() } 20 | 21 | @SuppressLint("CheckResult") 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContentView(R.layout.activity_main) 25 | 26 | presenter.getBizTab().subscribe { tabs -> 27 | mainViewPager.adapter = MainPagerAdapter(supportFragmentManager, tabs) 28 | mainPagerTabStrip.setViewPager(mainViewPager) 29 | } 30 | 31 | TouchEventBus.of(TabTouchHandler::class.java).attach(mainViewPager) 32 | TouchEventBus.of(SlidingTabTouchHandler::class.java).attach(mainPagerTabStrip) 33 | } 34 | 35 | override fun onDestroy() { 36 | TouchEventBus.of(SlidingTabTouchHandler::class.java).dettach(mainPagerTabStrip) 37 | TouchEventBus.of(TabTouchHandler::class.java).dettach(mainViewPager) 38 | super.onDestroy() 39 | } 40 | 41 | override fun dispatchTouchEvent(ev: MotionEvent): Boolean { 42 | if (TouchEventBus.dispatchTouchEvent(ev, this)) { 43 | return true 44 | } 45 | return super.dispatchTouchEvent(ev) 46 | } 47 | } 48 | 49 | private class MainPagerAdapter(fm: FragmentManager, val tabs: List) : FragmentPagerAdapter(fm) { 50 | 51 | override fun getItem(position: Int) = MainTabFragment.newInstance(tabs[position].bizId) 52 | 53 | override fun getCount() = tabs.size 54 | 55 | override fun getPageTitle(position: Int) = tabs[position].tabName 56 | } 57 | -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/ui/MainTabFragment.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.ui 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import androidx.fragment.app.FragmentManager 6 | import androidx.fragment.app.FragmentPagerAdapter 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import kotlinx.android.synthetic.main.fragment_main_tab.* 11 | import mobile.yy.com.touchsample.App 12 | import mobile.yy.com.touchsample.R 13 | import mobile.yy.com.touchsample.model.SubTab 14 | import mobile.yy.com.touchsample.util.OnVisibleChangeFragment 15 | 16 | /** 17 | * @author YvesCheung 18 | * 2018/4/25 19 | */ 20 | class MainTabFragment : OnVisibleChangeFragment() { 21 | 22 | companion object { 23 | fun newInstance(bizId: Int) = MainTabFragment().apply { 24 | arguments = Bundle().apply { 25 | putInt("bizId", bizId) 26 | } 27 | } 28 | } 29 | 30 | private var bizId = 0 31 | 32 | private val presenter by lazy { App.injector.getMainTabPresenter(bizId) } 33 | 34 | private val mainTab by lazy { presenter.getTab() } 35 | 36 | private val mAdapter by lazy { SubTabAdapter(childFragmentManager, mainTab.subTab) } 37 | 38 | @SuppressLint("InflateParams") 39 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 40 | bizId = arguments?.getInt("bizId", bizId) ?: 0 41 | return inflater.inflate(R.layout.fragment_main_tab, null).apply { 42 | setBackgroundColor(mainTab.backgroundColor) 43 | } 44 | } 45 | 46 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 47 | super.onViewCreated(view, savedInstanceState) 48 | subViewPager.adapter = mAdapter 49 | subTabStrip.setViewPager(subViewPager) 50 | } 51 | 52 | override fun onFragmentVisibleChange(visible: Boolean) { 53 | mAdapter.onVisibleChange(visible) 54 | } 55 | } 56 | 57 | class SubTabAdapter(fm: FragmentManager, private val tabs: List) : FragmentPagerAdapter(fm) { 58 | 59 | private val fragments = Array(tabs.size) { pos -> 60 | SubTabFragment.newInstance(tabs[pos].subBizId) 61 | } 62 | 63 | override fun getCount() = tabs.size 64 | 65 | override fun getItem(position: Int) = fragments[position] 66 | 67 | override fun getPageTitle(position: Int) = tabs[position].tabName 68 | 69 | fun onVisibleChange(visible: Boolean) { 70 | fragments.forEach { it.setParentFragmentVisible(visible) } 71 | } 72 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/ui/SubTabFragment.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.ui 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.util.TypedValue 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import kotlinx.android.synthetic.main.fragment_sub_tab.* 10 | import mobile.yy.com.toucheventbus.TouchEventBus 11 | import mobile.yy.com.touchsample.App 12 | import mobile.yy.com.touchsample.R 13 | import mobile.yy.com.touchsample.touch.ZoomTextTouchHandler 14 | import mobile.yy.com.touchsample.util.OnVisibleChangeFragment 15 | 16 | /** 17 | * @author YvesCheung 18 | * 2018/4/25 19 | */ 20 | class SubTabFragment : OnVisibleChangeFragment(), ZoomUi { 21 | 22 | companion object { 23 | fun newInstance(subBizId: Int) = SubTabFragment().apply { 24 | arguments = Bundle().apply { 25 | putInt("subBizId", subBizId) 26 | } 27 | } 28 | } 29 | 30 | private var subBizId = 0 31 | 32 | private val presenter by lazy { App.injector.getSubTabPresenter(subBizId) } 33 | 34 | @SuppressLint("InflateParams") 35 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 36 | subBizId = arguments?.getInt("subBizId") ?: 0 37 | return inflater.inflate(R.layout.fragment_sub_tab, null) 38 | } 39 | 40 | @SuppressLint("CheckResult") 41 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 42 | super.onViewCreated(view, savedInstanceState) 43 | subTextView.text = presenter.getContent() 44 | presenter.getTextSize().subscribe { textSize -> 45 | subTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) 46 | } 47 | } 48 | 49 | override fun onFragmentVisibleChange(visible: Boolean) { 50 | if (visible) { 51 | TouchEventBus.of(ZoomTextTouchHandler::class.java).attach(this) 52 | } else { 53 | TouchEventBus.of(ZoomTextTouchHandler::class.java).dettach(this) 54 | } 55 | } 56 | 57 | override fun resize(percentage: Float) { 58 | val textSize = subTextView.textSize 59 | presenter.setTextSize((percentage + 1f) * textSize) 60 | } 61 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/ui/ZoomUi.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.ui 2 | 3 | /** 4 | * @author YvesCheung 5 | * 2018/4/26 6 | */ 7 | interface ZoomUi { 8 | 9 | fun resize(percentage: Float) 10 | } -------------------------------------------------------------------------------- /touchsample/src/main/java/mobile/yy/com/touchsample/util/FragmentEnterHelper.kt: -------------------------------------------------------------------------------- 1 | package mobile.yy.com.touchsample.util 2 | 3 | import android.os.Bundle 4 | import androidx.annotation.CallSuper 5 | import androidx.fragment.app.Fragment 6 | import kotlin.properties.Delegates 7 | import kotlin.properties.ObservableProperty 8 | import kotlin.reflect.KProperty 9 | 10 | /** 11 | * @author YvesCheung 12 | * 2018/2/25 13 | * 14 | * 判断 [android.support.v4.app.Fragment] 可见性的辅助工具。 15 | * 使用时请重写 [android.support.v4.app.Fragment] 的以下几个方法: 16 | * [android.support.v4.app.Fragment.onCreate] 17 | * [android.support.v4.app.Fragment.onStart], 18 | * [android.support.v4.app.Fragment.onStop], 19 | * [android.support.v4.app.Fragment.setUserVisibleHint], 20 | * 并在这些方法中调用: 21 | * [EnterFragmentHelper.onCreate] 22 | * [EnterFragmentHelper.onStart] 23 | * [EnterFragmentHelper.onStop] 24 | * [EnterFragmentHelper.userVisibleHint]。 25 | * 如果有嵌套的情况,比如首页的多个一级Tab嵌套多个二级Tab,那么还需要提供父Fragment的可见性。 26 | * 在父Fragment可见性发生变化时调用[EnterFragmentHelper.onParentFragmentVisibleChange]方法。 27 | * 28 | * @see OnVisibleChangeFragment 29 | */ 30 | class EnterFragmentHelper( 31 | /** 32 | * 当前Fragment可见性改变时的回调 33 | */ 34 | private val onVisibleChange: (visible: Boolean) -> Unit, 35 | /** 36 | * 当前Fragment在应用打开后,第一次可见时的回调 37 | */ 38 | private val onFirstVisible: () -> Unit 39 | ) { 40 | 41 | private var parentFragmentVisible = false 42 | 43 | companion object { 44 | private var firstTime = true 45 | } 46 | 47 | private var fragmentVisible by VisibleCheck() 48 | 49 | /** 50 | * 当前Fragment是否可见 51 | */ 52 | var isThisFragmentVisible: Boolean = false 53 | private set(value) { 54 | field = value 55 | } 56 | 57 | var userVisibleHint by Delegates.observable(false) { _, _, newValue -> 58 | fragmentVisible = newValue 59 | } 60 | 61 | fun onParentFragmentVisibleChange(visible: Boolean) { 62 | parentFragmentVisible = visible 63 | fragmentVisible = visible 64 | } 65 | 66 | fun onCreate(parentFragment: Fragment?) { 67 | if (parentFragment == null) { 68 | parentFragmentVisible = true 69 | } 70 | } 71 | 72 | fun onStop() { 73 | fragmentVisible = false 74 | } 75 | 76 | fun onStart() { 77 | fragmentVisible = true 78 | } 79 | 80 | private inner class VisibleCheck : ObservableProperty(initialValue = false) { 81 | //false 就不要新值 82 | override fun beforeChange(property: KProperty<*>, oldValue: Boolean, newValue: Boolean): Boolean { 83 | if (newValue) { 84 | if (!userVisibleHint || !parentFragmentVisible) { 85 | return false 86 | } 87 | } 88 | return true 89 | } 90 | 91 | override fun afterChange(property: KProperty<*>, oldValue: Boolean, newValue: Boolean) { 92 | if (oldValue != newValue) { 93 | isThisFragmentVisible = newValue 94 | onVisibleChange(newValue) 95 | 96 | if (newValue && firstTime) { 97 | firstTime = false 98 | onFirstVisible() 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | /** 106 | * Fragment判断是否第一次可见。 107 | * 可以继承并重写[onFragmentFirstVisible]和[onFragmentFirstVisible]来获取可见性改变的回调。 108 | * 如果当前Fragment是嵌套Fragment,比如首页一级Tab里面的二级Tab,则需要提供父Fragment的可见性变化, 109 | * 在父Fragment可见性变化时主动调用[setParentFragmentVisible]方法。 110 | */ 111 | abstract class OnVisibleChangeFragment : Fragment() { 112 | 113 | private val helper = EnterFragmentHelper(::onFragmentVisibleChange, ::onFragmentFirstVisible) 114 | 115 | protected val isThisFragmentVisible get() = helper.isThisFragmentVisible 116 | 117 | @CallSuper 118 | override fun setUserVisibleHint(isVisibleToUser: Boolean) { 119 | super.setUserVisibleHint(isVisibleToUser) 120 | helper.userVisibleHint = isVisibleToUser 121 | } 122 | 123 | /** 124 | * 在父Fragment可见性发生变化时请调用这个方法。 125 | */ 126 | fun setParentFragmentVisible(isParentVisible: Boolean) { 127 | helper.onParentFragmentVisibleChange(isParentVisible) 128 | } 129 | 130 | @CallSuper 131 | override fun onCreate(savedInstanceState: Bundle?) { 132 | super.onCreate(savedInstanceState) 133 | helper.onCreate(parentFragment) 134 | } 135 | 136 | @CallSuper 137 | override fun onStart() { 138 | super.onStart() 139 | helper.onStart() 140 | } 141 | 142 | @CallSuper 143 | override fun onStop() { 144 | super.onStop() 145 | helper.onStop() 146 | } 147 | 148 | /** 149 | * 当前Fragment在onCreate之后可见性发生变化时 150 | */ 151 | open fun onFragmentVisibleChange(visible: Boolean) {} 152 | 153 | /** 154 | * 当前Fragment在onCreate之后首次可见时 155 | */ 156 | open fun onFragmentFirstVisible() {} 157 | } -------------------------------------------------------------------------------- /touchsample/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /touchsample/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /touchsample/src/main/res/drawable/main_background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/touchsample/src/main/res/drawable/main_background.jpg -------------------------------------------------------------------------------- /touchsample/src/main/res/drawable/main_background2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/touchsample/src/main/res/drawable/main_background2.jpg -------------------------------------------------------------------------------- /touchsample/src/main/res/drawable/main_background3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/touchsample/src/main/res/drawable/main_background3.jpg -------------------------------------------------------------------------------- /touchsample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 28 | 29 | 36 | 37 | 44 | 45 | 51 | 52 | -------------------------------------------------------------------------------- /touchsample/src/main/res/layout/fragment_main_tab.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /touchsample/src/main/res/layout/fragment_sub_tab.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | -------------------------------------------------------------------------------- /touchsample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /touchsample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /touchsample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/touchsample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /touchsample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/touchsample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /touchsample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/touchsample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /touchsample/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/touchsample/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /touchsample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/touchsample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /touchsample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/touchsample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /touchsample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/touchsample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /touchsample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/touchsample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /touchsample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/touchsample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /touchsample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YvesCheung/TouchEventBus/cf2d610e988e943fc1faa33087d645324883254c/touchsample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /touchsample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFF68F 4 | #FFC125 5 | #FF7256 6 | 7 | -------------------------------------------------------------------------------- /touchsample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TouchSample 3 | 4 | -------------------------------------------------------------------------------- /touchsample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | --------------------------------------------------------------------------------