├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml └── markdown-navigator.xml ├── LICENSE ├── README.md ├── YCSlideLib ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── yc │ │ └── slide │ │ ├── LoggerUtils.java │ │ ├── SlideAnimLayout.java │ │ ├── SlideLayout.java │ │ └── VerticalScrollView.java │ └── res │ └── values │ ├── attrs.xml │ └── strings.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── yc │ │ └── ycshopdetaillayout │ │ ├── FiveActivity.java │ │ ├── MainActivity.java │ │ ├── VerticalRecyclerView.java │ │ ├── VerticalWebView.java │ │ ├── first │ │ ├── FirstActivity.java │ │ ├── ShopDetailFragment.java │ │ └── ShopMainFragment.java │ │ ├── four │ │ └── FourActivity.java │ │ ├── second │ │ ├── SecondActivity.java │ │ └── ShopMain1Fragment.java │ │ └── third │ │ ├── NoScrollWebView.java │ │ └── ThirdActivity.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_first.xml │ ├── activity_four.xml │ ├── activity_main.xml │ ├── activity_second.xml │ ├── activity_third.xml │ ├── include_goods_details_bar.xml │ ├── include_shop_detail.xml │ ├── include_shop_detail3.xml │ ├── include_shop_main.xml │ └── shop_main.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 │ ├── icon_detail_page.png │ ├── icon_detail_page_100_true.png │ ├── icon_detail_page_network_payment.png │ ├── icon_detail_page_one_to_ten.png │ └── icon_details_page_down_loading.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── image ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── app-debug.apk └── slide.gif └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.ap_ 3 | 4 | # Files for the ART/Dalvik VM 5 | *.dex 6 | 7 | # Java class files 8 | *.class 9 | 10 | # Generated files 11 | bin/ 12 | gen/ 13 | out/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Android Studio Navigation editor temp files 29 | .navigation/ 30 | 31 | # Android Studio captures folder 32 | captures/ 33 | 34 | # IntelliJ 35 | *.iml 36 | .idea/workspace.xml 37 | .idea/tasks.xml 38 | .idea/gradle.xml 39 | .idea/assetWizardSettings.xml 40 | .idea/dictionaries 41 | .idea/libraries 42 | .idea/caches 43 | 44 | # Keystore files 45 | # Uncomment the following line if you do not want to check your keystore files in. 46 | #*.jks 47 | 48 | # External native build folder generated in Android Studio 2.2 and later 49 | .externalNativeBuild 50 | 51 | # Google Services (e.g. APIs or Firebase) 52 | google-services.json 53 | 54 | # Freeline 55 | freeline.py 56 | freeline/ 57 | freeline_project_description.json 58 | 59 | # fastlane 60 | fastlane/report.xml 61 | fastlane/Preview.html 62 | fastlane/screenshots 63 | fastlane/test_output 64 | fastlane/readme.md 65 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 36 | 37 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /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 | - 01.该库介绍 4 | - 02.效果展示 5 | - 03.如何使用 6 | - 04.注意要点 7 | - 05.优化问题 8 | - 06.部分代码逻辑 9 | - 07.参考案例 10 | 11 | 12 | ### 01.该库介绍 13 | - 模仿淘宝、京东、考拉等商品详情页分页加载的UI效果。可以嵌套RecyclerView、WebView、ViewPager、ScrollView等等。 14 | - 项目地址:https://github.com/yangchong211/YCShopDetailLayout 15 | - [apk下载链接](https://github.com/yangchong211/YCShopDetailLayout/tree/master/image) 16 | 17 | 18 | ### 02.效果展示 19 | - ![slide.gif](https://upload-images.jianshu.io/upload_images/4432347-43e7e30096b6e322.gif?imageMogr2/auto-orient/strip) 20 | 21 | 22 | #### 2.1 使用SlideLayout效果 23 | - ![image](https://upload-images.jianshu.io/upload_images/4432347-cae4a780c2c0a988.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 24 | 25 | 26 | 27 | #### 2.2 使用SlideAnimLayout带有加载动画效果 28 | - ![image](https://upload-images.jianshu.io/upload_images/4432347-d4f966b7750d1ece.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 29 | - ![image](https://upload-images.jianshu.io/upload_images/4432347-f7a0e3647aad672e.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 30 | 31 | 32 | 33 | ### 03.如何使用 34 | #### 3.0 如何引入到项目中 35 | - 如下所示 36 | ``` java 37 | implementation 'com.github.yangchong211:YCSlideLayout:1.1.6' 38 | ``` 39 | 40 | #### 3.1 第一种,直接上拉加载分页【SlideLayout有两个子ChildView】 41 | - SlideDetailsLayout有两个子ChildView:一个是商品页layout,一个是详情页layout 42 | - 在布局中 43 | ``` 44 | 51 | 52 | 53 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ``` 64 | - 在代码中 65 | ``` 66 | mSlideDetailsLayout.setOnSlideDetailsListener(new SlideLayout.OnSlideDetailsListener() { 67 | @Override 68 | public void onStatusChanged(SlideLayout.Status status) { 69 | if (status == SlideLayout.Status.OPEN) { 70 | //当前为图文详情页 71 | Log.e("FirstActivity","下拉回到商品详情"); 72 | } else { 73 | //当前为商品详情页 74 | Log.e("FirstActivity","继续上拉,查看图文详情"); 75 | } 76 | } 77 | }); 78 | 79 | //关闭商详页 80 | mSlideDetailsLayout.smoothClose(true); 81 | //打开详情页 82 | mSlideDetailsLayout.smoothOpen(true); 83 | ``` 84 | 85 | 86 | #### 3.2 第一种,上拉加载有动画效果,然后展示分页【SlideAnimLayout有三个子ChildView】 87 | - SlideAnimLayout有三个子ChildView:一个是商品页layout,一个是上拉加载动画layout,一个是详情页layout 88 | - 在布局中 89 | ``` 90 | 98 | 99 | 100 | 104 | 105 | 106 | 112 | 119 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | ``` 136 | - 在代码中 137 | ``` 138 | mSlideDetailsLayout.setScrollStatusListener(new SlideAnimLayout.onScrollStatusListener() { 139 | @Override 140 | public void onStatusChanged(SlideAnimLayout.Status mNowStatus, boolean isHalf) { 141 | if(mNowStatus== SlideAnimLayout.Status.CLOSE){ 142 | //打开 143 | if(isHalf){ 144 | mTvMoreText.setText("释放,查看图文详情"); 145 | mIvMoreImg.animate().rotation(0); 146 | LoggerUtils.i("onStatusChanged---CLOSE---释放"+isHalf); 147 | }else{//关闭 148 | mTvMoreText.setText("继续上拉,查看图文详情"); 149 | mIvMoreImg.animate().rotation(180); 150 | LoggerUtils.i("onStatusChanged---CLOSE---继续上拉"+isHalf); 151 | } 152 | }else{ 153 | //打开 154 | if(isHalf){ 155 | mTvMoreText.setText("下拉回到商品详情"); 156 | mIvMoreImg.animate().rotation(0); 157 | LoggerUtils.i("onStatusChanged---OPEN---下拉回到商品详情"+isHalf); 158 | }else{//关闭 159 | mTvMoreText.setText("释放回到商品详情"); 160 | mIvMoreImg.animate().rotation(180); 161 | LoggerUtils.i("onStatusChanged---OPEN---释放回到商品详情"+isHalf); 162 | } 163 | } 164 | } 165 | }); 166 | 167 | //关闭商详页 168 | mSlideDetailsLayout.smoothClose(true); 169 | //打开详情页 170 | mSlideDetailsLayout.smoothOpen(true); 171 | ``` 172 | 173 | 174 | 175 | ### 04.注意要点 176 | - 针对SlideDetailsLayout仅获取子节点中的前两个View 177 | - 其中第一个作为Front,即商品页;第二个作为Behind,即图文详情WebView页面。具体看代码: 178 | ``` 179 | @Override 180 | protected void onFinishInflate() { 181 | super.onFinishInflate(); 182 | final int childCount = getChildCount(); 183 | if (1 >= childCount) { 184 | throw new RuntimeException("SlideDetailsLayout only accept child more than 1!!"); 185 | } 186 | mFrontView = getChildAt(0); 187 | mBehindView = getChildAt(1); 188 | if(mDefaultPanel == 1){ 189 | post(new Runnable() { 190 | @Override 191 | public void run() { 192 | //默认是关闭状态的 193 | smoothOpen(false); 194 | } 195 | }); 196 | } 197 | } 198 | ``` 199 | - 针对SlideAnimLayout仅获取子节点中三个View,且第二个为动画节点View 200 | - 其中第一个作为Front,即商品页;第二个作为anim,即上拉动画view。第三个作为Behind,即图文详情WebView页面。具体看代码: 201 | ``` 202 | @Override 203 | protected void onFinishInflate() { 204 | super.onFinishInflate(); 205 | final int childCount = getChildCount(); 206 | if (1 >= childCount) { 207 | throw new RuntimeException("SlideDetailsLayout only accept childs more than 1!!"); 208 | } 209 | mFrontView = getChildAt(0); 210 | mAnimView = getChildAt(1); 211 | mBehindView = getChildAt(2); 212 | mAnimView.post(new Runnable() { 213 | @Override 214 | public void run() { 215 | animHeight = mAnimView.getHeight(); 216 | LoggerUtils.i("获取控件高度"+animHeight); 217 | } 218 | }); 219 | if(mDefaultPanel == 1){ 220 | post(new Runnable() { 221 | @Override 222 | public void run() { 223 | //默认是关闭状态的 224 | smoothOpen(false); 225 | } 226 | }); 227 | } 228 | } 229 | ``` 230 | 231 | 232 | 233 | ### 05.优化问题 234 | - 异常情况保存状态 235 | ``` 236 | @Override 237 | protected Parcelable onSaveInstanceState() { 238 | SavedState ss = new SavedState(super.onSaveInstanceState()); 239 | ss.offset = mSlideOffset; 240 | ss.status = mStatus.ordinal(); 241 | return ss; 242 | } 243 | 244 | @Override 245 | protected void onRestoreInstanceState(Parcelable state) { 246 | SavedState ss = (SavedState) state; 247 | super.onRestoreInstanceState(ss.getSuperState()); 248 | mSlideOffset = ss.offset; 249 | mStatus = Status.valueOf(ss.status); 250 | if (mStatus == Status.OPEN) { 251 | mBehindView.setVisibility(VISIBLE); 252 | } 253 | requestLayout(); 254 | } 255 | ``` 256 | - 当页面销毁的时候,移除listener监听,移除动画资源 257 | ``` 258 | @Override 259 | protected void onDetachedFromWindow() { 260 | super.onDetachedFromWindow(); 261 | setScrollStatusListener(null); 262 | setOnSlideStatusListener(null); 263 | if (animator!=null){ 264 | animator.cancel(); 265 | animator = null; 266 | } 267 | } 268 | ``` 269 | 270 | 271 | ### 06.部分代码逻辑 272 | #### 6.1 如何实现ScrollView在最顶部或者最底部的时候,不消费事件 273 | - 具体逻辑在dispatchTouchEvent分发事件中,当滑动到顶部或者底部的时候,则直接让父View消费事件。其他情况是自己是将事件会向上返还给View的父节点。 274 | ``` 275 | @Override 276 | public boolean dispatchTouchEvent(MotionEvent ev) { 277 | switch (ev.getAction()) { 278 | case MotionEvent.ACTION_DOWN: 279 | downX = ev.getX(); 280 | downY = ev.getY(); 281 | //如果滑动到了最底部,就允许继续向上滑动加载下一页,否者不允许 282 | //如果子节点不希望父进程拦截触摸事件,则为true。 283 | getParent().requestDisallowInterceptTouchEvent(true); 284 | break; 285 | case MotionEvent.ACTION_MOVE: 286 | float dx = ev.getX() - downX; 287 | float dy = ev.getY() - downY; 288 | boolean allowParentTouchEvent; 289 | if (Math.abs(dy) > Math.abs(dx)) { 290 | if (dy > 0) { 291 | //位于顶部时下拉,让父View消费事件 292 | allowParentTouchEvent = isTop(); 293 | } else { 294 | //位于底部时上拉,让父View消费事件 295 | allowParentTouchEvent = isBottom(); 296 | } 297 | } else { 298 | //水平方向滑动 299 | allowParentTouchEvent = true; 300 | } 301 | getParent().requestDisallowInterceptTouchEvent(!allowParentTouchEvent); 302 | break; 303 | default: 304 | break; 305 | } 306 | return super.dispatchTouchEvent(ev); 307 | } 308 | ``` 309 | 310 | 311 | #### 6.2 如何实现商品页和详情页之间的滑动,如何处理上拉加载控件的动画效果 312 | - SlideAnimLayout有三个子ChildView:一个是商品页layout,一个是上拉加载动画layout,一个是详情页layout 313 | - 通过onInterceptTouchEvent进行事件拦截后,在onTouchEvent方法中对触摸信息做进一步处理可以实现竖直方向的滑动 314 | - 当商品页ScrollView滑动到底部时,则直接让父View消费事件,该父View也就是SlideAnimLayout 315 | - 在onInterceptTouchEvent中,当打开详情页后(也就是CLOSE状态),向下拉动,当y轴滑动位移绝对值大于触摸移动的像素距离,并且当y轴滑动位移大于0,则拦截事件分发自己消费事件 316 | - 在onInterceptTouchEvent中,当关闭详情页后(也就是OPEN状态),向上拉动,当y轴滑动位移绝对值大于触摸移动的像素距离,并且当y轴滑动位移小于0,则拦截事件分发自己消费事件 317 | - 当处在商品页时,向上拉动;或者处于详情页时,向下拉动,在拉动过程中去改变mSlideOffset值,并且调用requestLayout()方法去绘制 318 | - 在屏幕区域滑动两个面板只需要改变两个面板在y轴方向的位移(有正负方向)即可。滑动的标尺是控件相对于Top的移动,且所有的位移计算都是基于该标尺。在切换面板时只需要知道对应的offset值即可…… 319 | - 如何处理上拉加载控件的动画效果 320 | - 添加一个listener监听,可以监听到状态,以及是否达到一半距离,主要是和offset比较,当到达一半距离的时候,这个时候用属性动画将箭头view旋转180度即可实现。 321 | - 既然要监听滑动距离,则首先要获取该加载控件的高度animHeight,那么在哪里获取比较合适呢?可以在onFinishInflate()方法中,用post形式获取控件高度。 322 | - 那么如何使滑动生效,并且看上去比较连贯 323 | - 自定义布局中有非常重要的两个环节onMeasure(测量)和onLayout(布局)。测量决定了View的所占的大小,布局决定了View所处的位置。实现滑动的关键思路就在这里,我们在onLayout方法中根据通过onInterceptTouchEvent、onTouchEvent得到的滑动信息进行计算而得到布局的位置信息,并把这个位置信息设置到子View上面即可实现滑动。 324 | - 滑动后松开手指如何实现滚动效果 325 | - 也就是说,当处在商品页时,向上拉动,拉动位移大于一半时,松开手指,则直接滑动到下一页详情页页面 326 | - 具体逻辑在finishTouchEvent方法中,它主要是记录offset值,以及close或open状态下视图的高度,还有是否发生切换变化 327 | - 最后开启动画,在动画过程中添加动画update的监听,在该方法中去requestLayout()控件,这样就达到滚动效果了。动画滚动结束后,如果是open状态并且是第一次显示,则设置详情页控件可见。 328 | - 如何使滚动效果比较自然,或者如何调整滚动时长 329 | - 可以自定义设置时间,直接在布局中设置…… 330 | 331 | 332 | 333 | ### 07.参考案例 334 | - 感谢下面大佬的开源案例 335 | - https://github.com/jeasonlzy/VerticalSlideView 336 | - https://github.com/hexianqiao3755/GoodsInfoPage 337 | - https://github.com/cnbleu/SlideDetailsLayout 338 | 339 | 340 | 341 | ### 08.其他更多 342 | #### 01.关于博客汇总链接 343 | - 1.[技术博客汇总](https://www.jianshu.com/p/614cb839182c) 344 | - 2.[开源项目汇总](https://blog.csdn.net/m0_37700275/article/details/80863574) 345 | - 3.[生活博客汇总](https://blog.csdn.net/m0_37700275/article/details/79832978) 346 | - 4.[喜马拉雅音频汇总](https://www.jianshu.com/p/f665de16d1eb) 347 | - 5.[其他汇总](https://www.jianshu.com/p/53017c3fc75d) 348 | 349 | 350 | 351 | #### 02.关于我的博客 352 | - 我的个人站点:www.yczbj.org,www.ycbjie.cn 353 | - github:https://github.com/yangchong211 354 | - 知乎:https://www.zhihu.com/people/yczbj/activities 355 | - 简书:http://www.jianshu.com/u/b7b2c6ed9284 356 | - csdn:http://my.csdn.net/m0_37700275 357 | - 喜马拉雅听书:http://www.ximalaya.com/zhubo/71989305/ 358 | - 开源中国:https://my.oschina.net/zbj1618/blog 359 | - 泡在网上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1 360 | - 邮箱:yangchong211@163.com 361 | - 阿里云博客:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV 362 | - segmentfault头条:https://segmentfault.com/u/xiangjianyu/articles 363 | - 掘金:https://juejin.im/user/5939433efe88c2006afa0c6e 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | -------------------------------------------------------------------------------- /YCSlideLib/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /YCSlideLib/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | //迁移到jitpack 3 | apply plugin: 'com.github.dcendents.android-maven' 4 | 5 | android { 6 | compileSdkVersion 29 7 | 8 | defaultConfig { 9 | minSdkVersion 16 10 | targetSdkVersion 29 11 | versionCode 12 12 | versionName "1.1.2" 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | 22 | } 23 | 24 | dependencies { 25 | implementation fileTree(dir: 'libs', include: ['*.jar']) 26 | implementation 'androidx.appcompat:appcompat:1.3.0' 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /YCSlideLib/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 | -------------------------------------------------------------------------------- /YCSlideLib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /YCSlideLib/src/main/java/com/yc/slide/LoggerUtils.java: -------------------------------------------------------------------------------- 1 | package com.yc.slide; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | *
 7 |  *     @author yangchong
 8 |  *     blog  : https://github.com/yangchong211/YCShopDetailLayout
 9 |  *     time  : 2018/6/6
10 |  *     desc  : 日志
11 |  *     revise:
12 |  * 
13 | */ 14 | public class LoggerUtils { 15 | 16 | private static boolean isLog = false; 17 | 18 | public static void setIsLog(boolean isLog) { 19 | LoggerUtils.isLog = isLog; 20 | } 21 | 22 | public static void i(String string){ 23 | if (isLog){ 24 | Log.i("LoggerUtils",string); 25 | } 26 | } 27 | 28 | 29 | public static void d(String string){ 30 | if (isLog){ 31 | Log.d("LoggerUtils",string); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /YCSlideLib/src/main/java/com/yc/slide/SlideAnimLayout.java: -------------------------------------------------------------------------------- 1 | package com.yc.slide; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ValueAnimator; 6 | import android.annotation.SuppressLint; 7 | import android.content.Context; 8 | import android.content.res.TypedArray; 9 | import android.os.Parcel; 10 | import android.os.Parcelable; 11 | import android.util.AttributeSet; 12 | import android.view.MotionEvent; 13 | import android.view.View; 14 | import android.view.ViewConfiguration; 15 | import android.view.ViewGroup; 16 | import android.widget.AbsListView; 17 | import android.widget.FrameLayout; 18 | import android.widget.LinearLayout; 19 | import android.widget.RelativeLayout; 20 | import androidx.core.view.ViewCompat; 21 | 22 | /** 23 | *
 24 |  *     @author yangchong
 25 |  *     blog  : https://github.com/yangchong211/YCShopDetailLayout
 26 |  *     time  : 2018/6/6
 27 |  *     desc  : SlideAnimLayout
 28 |  *     revise:
 29 |  * 
30 | */ 31 | public class SlideAnimLayout extends ViewGroup { 32 | 33 | private ValueAnimator animator; 34 | 35 | public enum Status { 36 | /** 37 | * 关闭 38 | */ 39 | CLOSE, 40 | /** 41 | * 打开 42 | */ 43 | OPEN; 44 | public static Status valueOf(int stats) { 45 | if (0 == stats) { 46 | return CLOSE; 47 | } else if (1 == stats) { 48 | return OPEN; 49 | } else { 50 | return CLOSE; 51 | } 52 | } 53 | } 54 | 55 | private static final int DEFAULT_DURATION = 300; 56 | private View mFrontView; 57 | private View mAnimView; 58 | private View mBehindView; 59 | 60 | private float mTouchSlop; 61 | private float mInitMotionY; 62 | private float mInitMotionX; 63 | 64 | 65 | private View mTarget; 66 | private float mSlideOffset; 67 | private Status mStatus = Status.CLOSE; 68 | private boolean isFirstShowBehindView = true; 69 | private long mDuration = DEFAULT_DURATION; 70 | private int mDefaultPanel = 0; 71 | private int animHeight; 72 | 73 | 74 | public SlideAnimLayout(Context context) { 75 | this(context, null); 76 | } 77 | 78 | public SlideAnimLayout(Context context, AttributeSet attrs) { 79 | this(context, attrs, 0); 80 | } 81 | 82 | public SlideAnimLayout(Context context, AttributeSet attrs, int defStyleAttr) { 83 | super(context, attrs, defStyleAttr); 84 | @SuppressLint("CustomViewStyleable") 85 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlideLayout, defStyleAttr, 0); 86 | mDuration = a.getInt(R.styleable.SlideLayout_duration, DEFAULT_DURATION); 87 | mDefaultPanel = a.getInt(R.styleable.SlideLayout_default_panel, 0); 88 | a.recycle(); 89 | //在我们认为用户正在滚动之前,触摸可以移动的像素距离 90 | mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 91 | } 92 | 93 | 94 | /** 95 | * 打开商详页 96 | * @param smooth 97 | */ 98 | public void smoothOpen(boolean smooth) { 99 | if (mStatus != Status.OPEN) { 100 | mStatus = Status.OPEN; 101 | //控件的高度+动画布局 102 | final float height = -getMeasuredHeight() - animHeight; 103 | LoggerUtils.i("SlideLayout---smoothOpen---"+height); 104 | animatorSwitch(0, height, true, smooth ? mDuration : 0); 105 | } 106 | } 107 | 108 | 109 | /** 110 | * 关闭商详页 111 | * @param smooth 112 | */ 113 | public void smoothClose(boolean smooth) { 114 | if (mStatus != Status.CLOSE) { 115 | mStatus = Status.CLOSE; 116 | final float height = -getMeasuredHeight(); 117 | LoggerUtils.i("SlideLayout---smoothClose---"+height); 118 | animatorSwitch(height, 0, true, smooth ? mDuration : 0); 119 | } 120 | } 121 | 122 | 123 | /** 124 | * 这个方法是在结束inflate之后才会执行 125 | */ 126 | @Override 127 | protected void onFinishInflate() { 128 | super.onFinishInflate(); 129 | final int childCount = getChildCount(); 130 | if (1 >= childCount) { 131 | throw new RuntimeException("SlideDetailsLayout only accept childs more than 1!!"); 132 | } 133 | LoggerUtils.i("获取子节点的个数"+childCount); 134 | mFrontView = getChildAt(0); 135 | mAnimView = getChildAt(1); 136 | mBehindView = getChildAt(2); 137 | mAnimView.post(new Runnable() { 138 | @Override 139 | public void run() { 140 | animHeight = mAnimView.getHeight(); 141 | LoggerUtils.i("获取控件高度"+animHeight); 142 | } 143 | }); 144 | if(mDefaultPanel == 1){ 145 | post(new Runnable() { 146 | @Override 147 | public void run() { 148 | //默认效果 149 | smoothOpen(false); 150 | } 151 | }); 152 | } 153 | } 154 | 155 | @Override 156 | protected void onDetachedFromWindow() { 157 | super.onDetachedFromWindow(); 158 | setScrollStatusListener(null); 159 | setOnSlideStatusListener(null); 160 | if (animator!=null){ 161 | animator.cancel(); 162 | animator = null; 163 | } 164 | } 165 | 166 | @Override 167 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 168 | //super.onMeasure(widthMeasureSpec, heightMeasureSpec); 169 | //获取宽高 170 | final int pWidth = MeasureSpec.getSize(widthMeasureSpec); 171 | final int pHeight = MeasureSpec.getSize(heightMeasureSpec); 172 | final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(pWidth, MeasureSpec.EXACTLY); 173 | final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(pHeight, MeasureSpec.EXACTLY); 174 | View child; 175 | for (int i = 0; i < getChildCount(); i++) { 176 | child = getChildAt(i); 177 | //当控件是Gone的时候,不进行测量 178 | if (child.getVisibility() == View.GONE) { 179 | continue; 180 | } 181 | //当孩子控件是动画控件时,则特殊处理 182 | if(getChildAt(i) == mAnimView){ 183 | child.measure(0,0); 184 | int measuredHeight = child.getMeasuredHeight(); 185 | int makeMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY); 186 | LoggerUtils.i("onMeasure获取控件高度"+measuredHeight); 187 | measureChild(child, childWidthMeasureSpec, makeMeasureSpec); 188 | } else{ 189 | measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec); 190 | } 191 | } 192 | setMeasuredDimension(pWidth, pHeight); 193 | } 194 | 195 | @Override 196 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 197 | int top; 198 | int bottom; 199 | final int offset = (int) mSlideOffset; 200 | View child; 201 | for (int i = 0; i < getChildCount(); i++) { 202 | child = getChildAt(i); 203 | if (child.getVisibility() == GONE) { 204 | continue; 205 | } 206 | LoggerUtils.i("onLayout,offset---"+offset); 207 | int measuredHeight = getChildAt(1).getMeasuredHeight(); 208 | if (child == mBehindView) { 209 | top = b + offset + measuredHeight ; 210 | bottom = top + b - t + measuredHeight; 211 | LoggerUtils.i("onLayout,mBehindView---"+top+"-----"+bottom); 212 | }else if(child == mAnimView){ 213 | top = b + offset; 214 | bottom = top - t + child.getMeasuredHeight(); 215 | LoggerUtils.i("onLayout,mAnimView---"+top+"-----"+bottom); 216 | } else { 217 | top = t + offset; 218 | bottom = b + offset; 219 | LoggerUtils.i("onLayout,other---"+top+"-----"+bottom); 220 | } 221 | child.layout(l, top, r, bottom); 222 | } 223 | } 224 | 225 | 226 | /** 227 | * 分发事件:事件分发的对象是事件。注意,事件分发是向下传递的,也就是父到子的顺序。 228 | * 根据内部拦截状态,向其child或者自己分发事件 229 | * 230 | * @param ev 231 | * @return 232 | */ 233 | @Override 234 | public boolean onInterceptTouchEvent(MotionEvent ev) { 235 | ensureTarget(); 236 | if (null == mTarget) { 237 | return false; 238 | } 239 | if (!isEnabled()) { 240 | return false; 241 | } 242 | final int action = ev.getAction(); 243 | boolean shouldIntercept = false; 244 | switch (action) { 245 | case MotionEvent.ACTION_DOWN: 246 | mInitMotionX = ev.getX(); 247 | mInitMotionY = ev.getY(); 248 | shouldIntercept = false; 249 | break; 250 | case MotionEvent.ACTION_MOVE: 251 | final float x = ev.getX(); 252 | final float y = ev.getY(); 253 | final float xDiff = x - mInitMotionX; 254 | final float yDiff = y - mInitMotionY; 255 | boolean close = mStatus == Status.CLOSE && yDiff > 0; 256 | boolean open = mStatus == Status.OPEN && yDiff < 0; 257 | if (!canChildScrollVertically((int) yDiff)) { 258 | final float xDiffers = Math.abs(xDiff); 259 | final float yDiffers = Math.abs(yDiff); 260 | if (yDiffers > mTouchSlop && yDiffers >= xDiffers && !(close || open)) { 261 | shouldIntercept = true; 262 | } 263 | } 264 | break; 265 | case MotionEvent.ACTION_UP: 266 | case MotionEvent.ACTION_CANCEL: 267 | break; 268 | default: 269 | break; 270 | } 271 | return shouldIntercept; 272 | } 273 | 274 | @Override 275 | public boolean onTouchEvent(MotionEvent ev) { 276 | ensureTarget(); 277 | if (null == mTarget) { 278 | return false; 279 | } 280 | if (!isEnabled()) { 281 | return false; 282 | } 283 | boolean wantTouch = true; 284 | final int action = ev.getAction(); 285 | switch (action) { 286 | case MotionEvent.ACTION_DOWN: 287 | break; 288 | case MotionEvent.ACTION_MOVE: { 289 | //获取滑动点y轴的位移 290 | final float y = ev.getY(); 291 | final float yDiff = y - mInitMotionY; 292 | boolean childScrollVertically = canChildScrollVertically(((int) yDiff)); 293 | //在关闭状态并且滑动位移小于等于0时 294 | boolean isDiffZero = yDiff<=0 && Status.OPEN == mStatus; 295 | boolean isAnimOpen = Status.OPEN == mStatus && yDiff>=animHeight; 296 | boolean isAnimClose = Status.CLOSE == mStatus && Math.abs(yDiff)>=animHeight; 297 | if (childScrollVertically || isDiffZero) { 298 | wantTouch = false; 299 | }else if(isAnimOpen|| isAnimClose){ 300 | wantTouch = true; 301 | } else { 302 | processTouchEvent(yDiff); 303 | wantTouch = true; 304 | } 305 | break; 306 | } 307 | case MotionEvent.ACTION_UP: 308 | case MotionEvent.ACTION_CANCEL: { 309 | finishTouchEvent(); 310 | wantTouch = false; 311 | break; 312 | } 313 | default: 314 | break; 315 | } 316 | return wantTouch; 317 | } 318 | 319 | 320 | /** 321 | * 设置方法是触摸滑动的时候 322 | * @param offset offset 323 | */ 324 | private void processTouchEvent(final float offset) { 325 | if (Math.abs(offset) < mTouchSlop) { 326 | return; 327 | } 328 | final float oldOffset = mSlideOffset; 329 | if (mStatus == Status.CLOSE) { 330 | if (offset >= 0) { 331 | mSlideOffset = 0; 332 | } else { 333 | mSlideOffset = offset; 334 | } 335 | if (mSlideOffset == oldOffset) { 336 | return; 337 | } 338 | } else if (mStatus == Status.OPEN) { 339 | final float pHeight = -getMeasuredHeight(); 340 | if (offset <= 0) { 341 | mSlideOffset = pHeight; 342 | } else { 343 | mSlideOffset = pHeight- animHeight + offset; 344 | } 345 | if (mSlideOffset == oldOffset) { 346 | return; 347 | } 348 | } 349 | 350 | if (Status.CLOSE == mStatus) { 351 | if (offset <= -animHeight/2) { 352 | LoggerUtils.i("准备翻下页,已超过一半"); 353 | if(listener!=null){ 354 | listener.onStatusChanged(mStatus, true); 355 | } 356 | } else { 357 | LoggerUtils.i("准备翻下页,不超过一半"); 358 | if(listener!=null){ 359 | listener.onStatusChanged(mStatus, false); 360 | } 361 | } 362 | } else if (Status.OPEN == mStatus) { 363 | if ((offset ) >= animHeight/2) { 364 | if(listener!=null){ 365 | listener.onStatusChanged(mStatus, false); 366 | } 367 | LoggerUtils.i("准备翻上页,已超过一半:offset:"+offset+"--->pHeight:"+"--->:"+animHeight); 368 | } else { 369 | if(listener!=null){ 370 | listener.onStatusChanged(mStatus, true); 371 | } 372 | LoggerUtils.i("准备翻上页,不超过一半"+offset+"--->pHeight:"+"--->:"+animHeight); 373 | } 374 | } 375 | requestLayout(); 376 | } 377 | 378 | 379 | /** 380 | * 结束触摸 381 | */ 382 | private void finishTouchEvent() { 383 | final int pHeight = getMeasuredHeight(); 384 | LoggerUtils.i("finishTouchEvent------pHeight---"+pHeight); 385 | final float offset = mSlideOffset; 386 | boolean changed = false; 387 | if (Status.CLOSE == mStatus) { 388 | if (offset <= -animHeight /2) { 389 | mSlideOffset = -pHeight - animHeight; 390 | mStatus = Status.OPEN; 391 | changed = true; 392 | } else { 393 | mSlideOffset = 0; 394 | } 395 | LoggerUtils.i("finishTouchEvent----CLOSE--mSlideOffset---"+mSlideOffset); 396 | } else if (Status.OPEN == mStatus) { 397 | if ((offset + pHeight) >= -animHeight/2) { 398 | mSlideOffset = 0; 399 | mStatus = Status.CLOSE; 400 | changed = true; 401 | } else { 402 | mSlideOffset = -pHeight - animHeight; 403 | } 404 | LoggerUtils.i("finishTouchEvent----OPEN-----"+mSlideOffset); 405 | } 406 | animatorSwitch(offset, mSlideOffset, changed); 407 | } 408 | 409 | 410 | /** 411 | * 共同调用的方法 412 | */ 413 | private void animatorSwitch(final float start, final float end, final boolean changed) { 414 | animatorSwitch(start, end, changed, mDuration); 415 | } 416 | 417 | private void animatorSwitch(final float start, final float end, 418 | final boolean changed, final long duration) { 419 | animator = ValueAnimator.ofFloat(start, end); 420 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 421 | @Override 422 | public void onAnimationUpdate(ValueAnimator animation) { 423 | mSlideOffset = (float) animation.getAnimatedValue(); 424 | LoggerUtils.i("animatorSwitch----onAnimationUpdate-----"+mSlideOffset); 425 | requestLayout(); 426 | } 427 | }); 428 | animator.addListener(new AnimatorListenerAdapter() { 429 | @Override 430 | public void onAnimationEnd(Animator animation) { 431 | super.onAnimationEnd(animation); 432 | if (changed) { 433 | if (mStatus == Status.OPEN && isFirstShowBehindView) { 434 | isFirstShowBehindView = false; 435 | mBehindView.setVisibility(VISIBLE); 436 | } 437 | if (onSlideStatusListener!=null){ 438 | onSlideStatusListener.onStatusChanged(mStatus); 439 | } 440 | } 441 | } 442 | }); 443 | animator.setDuration(duration); 444 | animator.start(); 445 | } 446 | 447 | 448 | private void ensureTarget() { 449 | if (mStatus == Status.CLOSE) { 450 | mTarget = mFrontView; 451 | } else { 452 | mTarget = mBehindView; 453 | } 454 | } 455 | 456 | 457 | /** 458 | * 是否可以滑动,direction为负数时表示向下滑动,反之表示向上滑动。 459 | * @param direction direction 460 | * @return 461 | */ 462 | protected boolean canChildScrollVertically(int direction) { 463 | if (mTarget instanceof AbsListView) { 464 | return canListViewScroll((AbsListView) mTarget); 465 | } else if (mTarget instanceof FrameLayout || mTarget instanceof RelativeLayout || 466 | mTarget instanceof LinearLayout) { 467 | View child; 468 | for (int i = 0; i < ((ViewGroup) mTarget).getChildCount(); i++) { 469 | child = ((ViewGroup) mTarget).getChildAt(i); 470 | if (child instanceof AbsListView) { 471 | return canListViewScroll((AbsListView) child); 472 | } 473 | } 474 | } 475 | return ViewCompat.canScrollVertically(mTarget, -direction); 476 | } 477 | 478 | 479 | protected boolean canListViewScroll(AbsListView absListView) { 480 | if (mStatus == Status.OPEN) { 481 | return absListView.getChildCount() > 0 482 | && (absListView.getFirstVisiblePosition() > 0 483 | || absListView.getChildAt(0).getTop() < absListView.getPaddingTop()); 484 | } else { 485 | final int count = absListView.getChildCount(); 486 | return count > 0 && (absListView.getLastVisiblePosition() < count - 1 487 | || absListView.getChildAt(count - 1).getBottom() > absListView.getMeasuredHeight()); 488 | } 489 | } 490 | 491 | 492 | @Override 493 | protected Parcelable onSaveInstanceState() { 494 | SavedState ss = new SavedState(super.onSaveInstanceState()); 495 | ss.offset = mSlideOffset; 496 | ss.status = mStatus.ordinal(); 497 | return ss; 498 | } 499 | 500 | @Override 501 | protected void onRestoreInstanceState(Parcelable state) { 502 | SavedState ss = (SavedState) state; 503 | super.onRestoreInstanceState(ss.getSuperState()); 504 | mSlideOffset = ss.offset; 505 | mStatus = Status.valueOf(ss.status); 506 | if (mStatus == Status.OPEN) { 507 | mBehindView.setVisibility(VISIBLE); 508 | } 509 | requestLayout(); 510 | } 511 | 512 | private static class SavedState extends BaseSavedState { 513 | 514 | private float offset; 515 | private int status; 516 | 517 | SavedState(Parcel source) { 518 | super(source); 519 | offset = source.readFloat(); 520 | status = source.readInt(); 521 | } 522 | 523 | SavedState(Parcelable superState) { 524 | super(superState); 525 | } 526 | 527 | @Override 528 | public void writeToParcel(Parcel out, int flags) { 529 | super.writeToParcel(out, flags); 530 | out.writeFloat(offset); 531 | out.writeInt(status); 532 | } 533 | 534 | public static final Creator CREATOR = new Creator() { 535 | @Override 536 | public SlideAnimLayout.SavedState createFromParcel(Parcel in) { 537 | return new SlideAnimLayout.SavedState(in); 538 | } 539 | 540 | @Override 541 | public SlideAnimLayout.SavedState[] newArray(int size) { 542 | return new SlideAnimLayout.SavedState[size]; 543 | } 544 | }; 545 | } 546 | 547 | public interface onScrollStatusListener{ 548 | /** 549 | * 监听方法 550 | * @param status 状态 551 | * @param isHalf 是否是一半距离 552 | */ 553 | void onStatusChanged(Status status, boolean isHalf); 554 | } 555 | 556 | private onScrollStatusListener listener; 557 | 558 | public void setScrollStatusListener(onScrollStatusListener listener){ 559 | this.listener = listener; 560 | } 561 | 562 | private OnSlideStatusListener onSlideStatusListener; 563 | public interface OnSlideStatusListener { 564 | void onStatusChanged(Status status); 565 | } 566 | public void setOnSlideStatusListener(OnSlideStatusListener listener){ 567 | this.onSlideStatusListener = listener; 568 | } 569 | 570 | } 571 | -------------------------------------------------------------------------------- /YCSlideLib/src/main/java/com/yc/slide/SlideLayout.java: -------------------------------------------------------------------------------- 1 | package com.yc.slide; 2 | 3 | 4 | import android.animation.Animator; 5 | import android.animation.AnimatorListenerAdapter; 6 | import android.animation.ValueAnimator; 7 | import android.content.Context; 8 | import android.content.res.TypedArray; 9 | import android.graphics.Canvas; 10 | import android.os.Parcel; 11 | import android.os.Parcelable; 12 | import android.util.AttributeSet; 13 | import android.view.MotionEvent; 14 | import android.view.View; 15 | import android.view.ViewConfiguration; 16 | import android.view.ViewGroup; 17 | import android.widget.AbsListView; 18 | import android.widget.FrameLayout; 19 | import android.widget.LinearLayout; 20 | import android.widget.RelativeLayout; 21 | 22 | import androidx.core.view.ViewCompat; 23 | 24 | 25 | 26 | /** 27 | *
 28 |  *     @author yangchong
 29 |  *     blog  : https://github.com/yangchong211/YCShopDetailLayout
 30 |  *     time  : 2018/6/6
 31 |  *     desc  : SlideLayout
 32 |  *     revise:
 33 |  * 
34 | */ 35 | public class SlideLayout extends ViewGroup { 36 | 37 | 38 | private static final float DEFAULT_PERCENT = 0.2f; 39 | private static final int DEFAULT_DURATION = 300; 40 | 41 | private View mFrontView; 42 | private View mBehindView; 43 | private View mTarget; 44 | private float mTouchSlop; 45 | private float mInitMotionY; 46 | private float mInitMotionX; 47 | private float mSlideOffset; 48 | private Status mStatus = Status.CLOSE; 49 | private boolean isFirstShowBehindView = true; 50 | private float mPercent = DEFAULT_PERCENT; 51 | private long mDuration = DEFAULT_DURATION; 52 | private int mDefaultPanel = 0; 53 | 54 | /** 55 | * 状态,使用枚举 56 | */ 57 | public enum Status { 58 | CLOSE, 59 | OPEN; 60 | public static Status valueOf(int stats) { 61 | if (0 == stats) { 62 | return CLOSE; 63 | } else if (1 == stats) { 64 | return OPEN; 65 | } else { 66 | return CLOSE; 67 | } 68 | } 69 | } 70 | 71 | public SlideLayout(Context context) { 72 | this(context, null); 73 | } 74 | 75 | public SlideLayout(Context context, AttributeSet attrs) { 76 | this(context, attrs, 0); 77 | } 78 | 79 | public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) { 80 | super(context, attrs, defStyleAttr); 81 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlideLayout, defStyleAttr, 0); 82 | mPercent = a.getFloat(R.styleable.SlideLayout_percent, DEFAULT_PERCENT); 83 | mDuration = a.getInt(R.styleable.SlideLayout_duration, DEFAULT_DURATION); 84 | mDefaultPanel = a.getInt(R.styleable.SlideLayout_default_panel, 0); 85 | a.recycle(); 86 | mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 87 | } 88 | 89 | @Override 90 | protected void onFinishInflate() { 91 | super.onFinishInflate(); 92 | final int childCount = getChildCount(); 93 | if (1 >= childCount) { 94 | throw new RuntimeException("SlideDetailsLayout only accept child more than 1!!"); 95 | } 96 | mFrontView = getChildAt(0); 97 | mBehindView = getChildAt(1); 98 | if(mDefaultPanel == 1){ 99 | this.post(new Runnable() { 100 | @Override 101 | public void run() { 102 | smoothOpen(false); 103 | } 104 | }); 105 | } 106 | } 107 | 108 | /** 109 | * 测量 110 | */ 111 | @Override 112 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 113 | final int pWidth = MeasureSpec.getSize(widthMeasureSpec); 114 | final int pHeight = MeasureSpec.getSize(heightMeasureSpec); 115 | final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(pWidth, MeasureSpec.EXACTLY); 116 | final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(pHeight, MeasureSpec.EXACTLY); 117 | View child; 118 | for (int i = 0; i < getChildCount(); i++) { 119 | child = getChildAt(i); 120 | if (child.getVisibility() == GONE) { 121 | continue; 122 | } 123 | measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec); 124 | } 125 | setMeasuredDimension(pWidth, pHeight); 126 | } 127 | 128 | /** 129 | * 绘制 130 | */ 131 | @Override 132 | protected void onDraw(Canvas canvas) { 133 | super.onDraw(canvas); 134 | } 135 | 136 | /** 137 | * 布局 138 | */ 139 | @Override 140 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 141 | int top; 142 | int bottom; 143 | final int offset = (int) mSlideOffset; 144 | View child; 145 | for (int i = 0; i < getChildCount(); i++) { 146 | child = getChildAt(i); 147 | if (child.getVisibility() == GONE) { 148 | continue; 149 | } 150 | if (child == mBehindView) { 151 | top = b + offset; 152 | bottom = top + b - t; 153 | } else { 154 | top = t + offset; 155 | bottom = b + offset; 156 | } 157 | child.layout(l, top, r, bottom); 158 | } 159 | } 160 | 161 | /** 162 | * 拦截事件 163 | * @param ev ev 164 | * @return 165 | */ 166 | @Override 167 | public boolean onInterceptTouchEvent(MotionEvent ev) { 168 | ensureTarget(); 169 | if (null == mTarget) { 170 | return false; 171 | } 172 | if (!isEnabled()) { 173 | return false; 174 | } 175 | final int action = ev.getAction(); 176 | boolean shouldIntercept = false; 177 | switch (action) { 178 | case MotionEvent.ACTION_DOWN: { 179 | mInitMotionX = ev.getX(); 180 | mInitMotionY = ev.getY(); 181 | shouldIntercept = false; 182 | break; 183 | } 184 | case MotionEvent.ACTION_MOVE: { 185 | final float x = ev.getX(); 186 | final float y = ev.getY(); 187 | final float xDiff = x - mInitMotionX; 188 | final float yDiff = y - mInitMotionY; 189 | boolean close = mStatus == SlideLayout.Status.CLOSE && yDiff > 0; 190 | boolean open = mStatus == SlideLayout.Status.OPEN && yDiff < 0; 191 | if (!canChildScrollVertically((int) yDiff)) { 192 | final float xDiffers = Math.abs(xDiff); 193 | final float yDiffers = Math.abs(yDiff); 194 | if (yDiffers > mTouchSlop && yDiffers >= xDiffers && !(close || open)) { 195 | shouldIntercept = true; 196 | } 197 | } 198 | break; 199 | } 200 | case MotionEvent.ACTION_UP: 201 | case MotionEvent.ACTION_CANCEL: { 202 | shouldIntercept = false; 203 | break; 204 | } 205 | default: 206 | break; 207 | } 208 | return shouldIntercept; 209 | } 210 | 211 | /** 212 | * 触摸事件 213 | * @param ev ev 214 | * @return true表示自己处理,false表示自己处理不了 215 | */ 216 | @Override 217 | public boolean onTouchEvent(MotionEvent ev) { 218 | ensureTarget(); 219 | if (null == mTarget) { 220 | return false; 221 | } 222 | if (!isEnabled()) { 223 | return false; 224 | } 225 | boolean wantTouch = true; 226 | final int action = ev.getAction(); 227 | switch (action) { 228 | case MotionEvent.ACTION_DOWN: 229 | if (mTarget instanceof View) { 230 | wantTouch = true; 231 | } 232 | break; 233 | case MotionEvent.ACTION_MOVE: 234 | final float y = ev.getY(); 235 | final float yDiff = y - mInitMotionY; 236 | if (canChildScrollVertically(((int) yDiff))) { 237 | wantTouch = false; 238 | } else { 239 | processTouchEvent(yDiff); 240 | wantTouch = true; 241 | } 242 | break; 243 | case MotionEvent.ACTION_UP: 244 | case MotionEvent.ACTION_CANCEL: 245 | finishTouchEvent(); 246 | wantTouch = false; 247 | break; 248 | default: 249 | break; 250 | } 251 | return wantTouch; 252 | } 253 | 254 | 255 | private void processTouchEvent(final float offset) { 256 | if (Math.abs(offset) < mTouchSlop) { 257 | return; 258 | } 259 | final float oldOffset = mSlideOffset; 260 | if (mStatus == Status.CLOSE) { 261 | if (offset >= 0) { 262 | mSlideOffset = 0; 263 | } else { 264 | mSlideOffset = offset; 265 | } 266 | if (mSlideOffset == oldOffset) { 267 | return; 268 | } 269 | } else if (mStatus == Status.OPEN) { 270 | final float pHeight = -getMeasuredHeight(); 271 | if (offset <= 0) { 272 | mSlideOffset = pHeight; 273 | } else { 274 | final float newOffset = pHeight + offset; 275 | mSlideOffset = newOffset; 276 | } 277 | if (mSlideOffset == oldOffset) { 278 | return; 279 | } 280 | } 281 | requestLayout(); 282 | } 283 | 284 | 285 | private void finishTouchEvent() { 286 | final int pHeight = getMeasuredHeight(); 287 | final int percent = (int) (pHeight * mPercent); 288 | final float offset = mSlideOffset; 289 | boolean changed = false; 290 | if (Status.CLOSE == mStatus) { 291 | if (offset <= -percent) { 292 | mSlideOffset = -pHeight; 293 | mStatus = Status.OPEN; 294 | changed = true; 295 | } else { 296 | mSlideOffset = 0; 297 | } 298 | } else if (Status.OPEN == mStatus) { 299 | if ((offset + pHeight) >= percent) { 300 | mSlideOffset = 0; 301 | mStatus = Status.CLOSE; 302 | changed = true; 303 | } else { 304 | mSlideOffset = -pHeight; 305 | } 306 | } 307 | animatorSwitch(offset, mSlideOffset, changed); 308 | } 309 | 310 | 311 | private void animatorSwitch(final float start, final float end, final boolean changed) { 312 | animatorSwitch(start, end, changed, mDuration); 313 | } 314 | 315 | private void animatorSwitch(final float start, final float end, final boolean changed, final long duration) { 316 | ValueAnimator animator = ValueAnimator.ofFloat(start, end); 317 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 318 | @Override 319 | public void onAnimationUpdate(ValueAnimator animation) { 320 | mSlideOffset = (float) animation.getAnimatedValue(); 321 | requestLayout(); 322 | } 323 | }); 324 | animator.addListener(new AnimatorListenerAdapter() { 325 | @Override 326 | public void onAnimationEnd(Animator animation) { 327 | super.onAnimationEnd(animation); 328 | if (changed) { 329 | if (mStatus == Status.OPEN) { 330 | checkAndFirstOpenPanel(); 331 | } 332 | 333 | if (null != mOnSlideDetailsListener) { 334 | mOnSlideDetailsListener.onStatusChanged(mStatus); 335 | } 336 | } 337 | } 338 | }); 339 | animator.setDuration(duration); 340 | animator.start(); 341 | } 342 | 343 | private void checkAndFirstOpenPanel() { 344 | if (isFirstShowBehindView) { 345 | isFirstShowBehindView = false; 346 | mBehindView.setVisibility(VISIBLE); 347 | } 348 | } 349 | 350 | private void ensureTarget() { 351 | if (mStatus == Status.CLOSE) { 352 | mTarget = mFrontView; 353 | } else { 354 | mTarget = mBehindView; 355 | } 356 | } 357 | 358 | 359 | 360 | protected boolean canChildScrollVertically(int direction) { 361 | if (mTarget instanceof AbsListView) { 362 | return canListViewScroll((AbsListView) mTarget); 363 | } else if (mTarget instanceof FrameLayout || mTarget instanceof RelativeLayout || 364 | mTarget instanceof LinearLayout) { 365 | View child; 366 | for (int i = 0; i < ((ViewGroup) mTarget).getChildCount(); i++) { 367 | child = ((ViewGroup) mTarget).getChildAt(i); 368 | if (child instanceof AbsListView) { 369 | return canListViewScroll((AbsListView) child); 370 | } 371 | } 372 | } 373 | return ViewCompat.canScrollVertically(mTarget, -direction); 374 | } 375 | 376 | protected boolean canListViewScroll(AbsListView absListView) { 377 | if (mStatus == Status.OPEN) { 378 | return absListView.getChildCount() > 0 379 | && (absListView.getFirstVisiblePosition() > 0 380 | || absListView.getChildAt(0).getTop() < absListView.getPaddingTop()); 381 | } else { 382 | final int count = absListView.getChildCount(); 383 | return count > 0 384 | && (absListView.getLastVisiblePosition() < count - 1 385 | || absListView.getChildAt(count - 1).getBottom() > absListView.getMeasuredHeight()); 386 | } 387 | } 388 | 389 | @Override 390 | protected Parcelable onSaveInstanceState() { 391 | SavedState ss = new SavedState(super.onSaveInstanceState()); 392 | ss.offset = mSlideOffset; 393 | ss.status = mStatus.ordinal(); 394 | return ss; 395 | } 396 | 397 | @Override 398 | protected void onRestoreInstanceState(Parcelable state) { 399 | SavedState ss = (SavedState) state; 400 | super.onRestoreInstanceState(ss.getSuperState()); 401 | mSlideOffset = ss.offset; 402 | mStatus = Status.valueOf(ss.status); 403 | if (mStatus == Status.OPEN) { 404 | mBehindView.setVisibility(VISIBLE); 405 | } 406 | requestLayout(); 407 | } 408 | 409 | static class SavedState extends BaseSavedState { 410 | 411 | private float offset; 412 | private int status; 413 | 414 | SavedState(Parcel source) { 415 | super(source); 416 | offset = source.readFloat(); 417 | status = source.readInt(); 418 | } 419 | 420 | SavedState(Parcelable superState) { 421 | super(superState); 422 | } 423 | 424 | @Override 425 | public void writeToParcel(Parcel out, int flags) { 426 | super.writeToParcel(out, flags); 427 | out.writeFloat(offset); 428 | out.writeInt(status); 429 | } 430 | 431 | public static final Creator CREATOR = 432 | new Creator() { 433 | @Override 434 | public SavedState createFromParcel(Parcel in) { 435 | return new SavedState(in); 436 | } 437 | 438 | @Override 439 | public SavedState[] newArray(int size) { 440 | return new SavedState[size]; 441 | } 442 | }; 443 | } 444 | 445 | /*------------------------------------回调接口------------------------------------------------*/ 446 | private OnSlideDetailsListener mOnSlideDetailsListener; 447 | public interface OnSlideDetailsListener { 448 | void onStatusChanged(Status status); 449 | } 450 | public void setOnSlideDetailsListener(OnSlideDetailsListener listener) { 451 | this.mOnSlideDetailsListener = listener; 452 | } 453 | 454 | 455 | /*------------------------------------相关方法------------------------------------------------*/ 456 | public void smoothOpen(boolean smooth) { 457 | if (mStatus != Status.OPEN) { 458 | mStatus = Status.OPEN; 459 | final float height = -getMeasuredHeight(); 460 | animatorSwitch(0, height, true, smooth ? mDuration : 0); 461 | } 462 | } 463 | 464 | 465 | public void smoothClose(boolean smooth) { 466 | if (mStatus != Status.CLOSE) { 467 | mStatus = Status.CLOSE; 468 | final float height = -getMeasuredHeight(); 469 | animatorSwitch(height, 0, true, smooth ? mDuration : 0); 470 | } 471 | } 472 | 473 | public void setPercent(float percent) { 474 | this.mPercent = percent; 475 | } 476 | 477 | } 478 | -------------------------------------------------------------------------------- /YCSlideLib/src/main/java/com/yc/slide/VerticalScrollView.java: -------------------------------------------------------------------------------- 1 | package com.yc.slide; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.util.AttributeSet; 6 | import android.view.MotionEvent; 7 | import android.widget.ScrollView; 8 | 9 | 10 | /** 11 | *
12 |  *     @author yangchong
13 |  *     blog  : https://github.com/yangchong211/YCShopDetailLayout
14 |  *     time  : 2018/6/6
15 |  *     desc  : 当ScrollView在最顶部或者最底部的时候,不消费事件
16 |  *     revise:
17 |  * 
18 | */ 19 | public class VerticalScrollView extends ScrollView { 20 | 21 | private float downX; 22 | private float downY; 23 | 24 | public VerticalScrollView(Context context) { 25 | this(context, null); 26 | } 27 | 28 | public VerticalScrollView(Context context, AttributeSet attrs) { 29 | this(context, attrs, android.R.attr.scrollViewStyle); 30 | } 31 | 32 | public VerticalScrollView(Context context, AttributeSet attrs, int defStyleAttr) { 33 | super(context, attrs, defStyleAttr); 34 | } 35 | 36 | @Override 37 | public boolean dispatchTouchEvent(MotionEvent ev) { 38 | switch (ev.getAction()) { 39 | case MotionEvent.ACTION_DOWN: 40 | downX = ev.getX(); 41 | downY = ev.getY(); 42 | //如果滑动到了最底部,就允许继续向上滑动加载下一页,否者不允许 43 | //如果子节点不希望父进程拦截触摸事件,则为true。 44 | getParent().requestDisallowInterceptTouchEvent(true); 45 | break; 46 | case MotionEvent.ACTION_MOVE: 47 | float dx = ev.getX() - downX; 48 | float dy = ev.getY() - downY; 49 | boolean allowParentTouchEvent; 50 | if (Math.abs(dy) > Math.abs(dx)) { 51 | if (dy > 0) { 52 | //位于顶部时下拉,让父View消费事件 53 | allowParentTouchEvent = isTop(); 54 | } else { 55 | //位于底部时上拉,让父View消费事件 56 | allowParentTouchEvent = isBottom(); 57 | } 58 | } else { 59 | //水平方向滑动 60 | allowParentTouchEvent = true; 61 | } 62 | getParent().requestDisallowInterceptTouchEvent(!allowParentTouchEvent); 63 | break; 64 | default: 65 | break; 66 | } 67 | return super.dispatchTouchEvent(ev); 68 | } 69 | 70 | private boolean isTop() { 71 | return !canScrollVertically(-1); 72 | } 73 | 74 | private boolean isBottom() { 75 | return !canScrollVertically(1); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /YCSlideLib/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /YCSlideLib/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | YCShopDetailLayoutLib 3 | 4 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 29 5 | defaultConfig { 6 | applicationId "com.ycbjie.ycshopdetaillayout" 7 | minSdkVersion 17 8 | targetSdkVersion 29 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'androidx.appcompat:appcompat:1.3.0' 24 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 25 | // implementation project(path: ':YCSlideLib') 26 | implementation 'com.github.yangchong211:YCSlideLayout:1.1.6' 27 | } 28 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/yc/ycshopdetaillayout/FiveActivity.java: -------------------------------------------------------------------------------- 1 | package com.yc.ycshopdetaillayout; 2 | 3 | import android.os.Bundle; 4 | import android.view.View; 5 | import android.widget.ScrollView; 6 | 7 | import androidx.annotation.Nullable; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | 10 | import com.yc.slide.VerticalScrollView; 11 | import com.ycbjie.ycshopdetaillayout.R; 12 | 13 | public class FiveActivity extends AppCompatActivity { 14 | 15 | private VerticalScrollView scrollView; 16 | 17 | @Override 18 | protected void onCreate(@Nullable Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.shop_main); 21 | scrollView = findViewById(R.id.scrollView); 22 | findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() { 23 | @Override 24 | public void onClick(View v) { 25 | //scrollView.scrollTo(0,0); 26 | scrollView.post(new Runnable() { 27 | @Override 28 | public void run() { 29 | //ScrollView滑动到顶部 30 | scrollView.fullScroll(ScrollView.FOCUS_UP); 31 | } 32 | }); 33 | } 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/yc/ycshopdetaillayout/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.yc.ycshopdetaillayout; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | 7 | import androidx.appcompat.app.AppCompatActivity; 8 | 9 | import com.yc.slide.LoggerUtils; 10 | import com.yc.ycshopdetaillayout.first.FirstActivity; 11 | import com.yc.ycshopdetaillayout.second.SecondActivity; 12 | import com.yc.ycshopdetaillayout.third.ThirdActivity; 13 | import com.ycbjie.ycshopdetaillayout.R; 14 | import com.yc.ycshopdetaillayout.four.FourActivity; 15 | 16 | public class MainActivity extends AppCompatActivity { 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_main); 22 | LoggerUtils.setIsLog(true); 23 | 24 | findViewById(R.id.tv_1).setOnClickListener(new View.OnClickListener() { 25 | @Override 26 | public void onClick(View v) { 27 | startActivity(new Intent(MainActivity.this, FirstActivity.class)); 28 | } 29 | }); 30 | findViewById(R.id.tv_2).setOnClickListener(new View.OnClickListener() { 31 | @Override 32 | public void onClick(View v) { 33 | startActivity(new Intent(MainActivity.this, SecondActivity.class)); 34 | } 35 | }); 36 | findViewById(R.id.tv_3).setOnClickListener(new View.OnClickListener() { 37 | @Override 38 | public void onClick(View v) { 39 | startActivity(new Intent(MainActivity.this, ThirdActivity.class)); 40 | } 41 | }); 42 | findViewById(R.id.tv_4).setOnClickListener(new View.OnClickListener() { 43 | @Override 44 | public void onClick(View v) { 45 | startActivity(new Intent(MainActivity.this,FourActivity.class)); 46 | } 47 | }); 48 | findViewById(R.id.tv_5).setOnClickListener(new View.OnClickListener() { 49 | @Override 50 | public void onClick(View v) { 51 | startActivity(new Intent(MainActivity.this,FiveActivity.class)); 52 | } 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/yc/ycshopdetaillayout/VerticalRecyclerView.java: -------------------------------------------------------------------------------- 1 | package com.yc.ycshopdetaillayout; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.MotionEvent; 6 | 7 | import androidx.recyclerview.widget.GridLayoutManager; 8 | import androidx.recyclerview.widget.LinearLayoutManager; 9 | import androidx.recyclerview.widget.RecyclerView; 10 | import androidx.recyclerview.widget.StaggeredGridLayoutManager; 11 | 12 | /** 13 | *
 14 |  *     @author yangchong
 15 |  *     blog  :
 16 |  *     time  : 2018/6/6
 17 |  *     desc  : 当WebView在最顶部或者最底部的时候,不消费事件
 18 |  *     revise:
 19 |  * 
20 | */ 21 | public class VerticalRecyclerView extends RecyclerView { 22 | 23 | private float downX; 24 | private float downY; 25 | /** 第一个可见的item的位置 */ 26 | private int firstVisibleItemPosition; 27 | /** 第一个的位置 */ 28 | private int[] firstPositions; 29 | /** 最后一个可见的item的位置 */ 30 | private int lastVisibleItemPosition; 31 | /** 最后一个的位置 */ 32 | private int[] lastPositions; 33 | private boolean isTop; 34 | private boolean isBottom; 35 | 36 | public VerticalRecyclerView(Context context) { 37 | this(context, null); 38 | } 39 | 40 | public VerticalRecyclerView(Context context, AttributeSet attrs) { 41 | this(context, attrs, 0); 42 | } 43 | 44 | public VerticalRecyclerView(Context context, AttributeSet attrs, int defStyle) { 45 | super(context, attrs, defStyle); 46 | } 47 | 48 | @Override 49 | public boolean dispatchTouchEvent(MotionEvent ev) { 50 | RecyclerView.LayoutManager layoutManager = getLayoutManager(); 51 | if (layoutManager != null) { 52 | if (layoutManager instanceof GridLayoutManager) { 53 | lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); 54 | firstVisibleItemPosition = ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition(); 55 | } else if (layoutManager instanceof LinearLayoutManager) { 56 | lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); 57 | firstVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); 58 | } else if (layoutManager instanceof StaggeredGridLayoutManager) { 59 | StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager; 60 | if (lastPositions == null) { 61 | lastPositions = new int[staggeredGridLayoutManager.getSpanCount()]; 62 | firstPositions = new int[staggeredGridLayoutManager.getSpanCount()]; 63 | } 64 | staggeredGridLayoutManager.findLastVisibleItemPositions(lastPositions); 65 | staggeredGridLayoutManager.findFirstVisibleItemPositions(firstPositions); 66 | lastVisibleItemPosition = findMax(lastPositions); 67 | firstVisibleItemPosition = findMin(firstPositions); 68 | } 69 | } else { 70 | throw new RuntimeException("Unsupported LayoutManager used. Valid ones are LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager"); 71 | } 72 | 73 | switch (ev.getAction()) { 74 | case MotionEvent.ACTION_DOWN: 75 | downX = ev.getX(); 76 | downY = ev.getY(); 77 | //如果滑动到了最底部,就允许继续向上滑动加载下一页,否者不允许 78 | getParent().requestDisallowInterceptTouchEvent(true); 79 | break; 80 | case MotionEvent.ACTION_MOVE: 81 | float dx = ev.getX() - downX; 82 | float dy = ev.getY() - downY; 83 | boolean allowParentTouchEvent; 84 | if (Math.abs(dy) > Math.abs(dx)) { 85 | if (dy > 0) { 86 | //位于顶部时下拉,让父View消费事件 87 | allowParentTouchEvent = isTop = firstVisibleItemPosition == 0 && getChildAt(0).getTop() >= 0; 88 | } else { 89 | //位于底部时上拉,让父View消费事件 90 | int visibleItemCount = layoutManager.getChildCount(); 91 | int totalItemCount = layoutManager.getItemCount(); 92 | allowParentTouchEvent = isBottom = visibleItemCount > 0 && (lastVisibleItemPosition) >= totalItemCount - 1 && getChildAt(getChildCount() - 1).getBottom() <= getHeight(); 93 | } 94 | } else { 95 | //水平方向滑动 96 | allowParentTouchEvent = true; 97 | } 98 | getParent().requestDisallowInterceptTouchEvent(!allowParentTouchEvent); 99 | break; 100 | default: 101 | break; 102 | } 103 | return super.dispatchTouchEvent(ev); 104 | 105 | } 106 | 107 | private int findMax(int[] lastPositions) { 108 | int max = lastPositions[0]; 109 | for (int value : lastPositions) { 110 | if (value >= max) { 111 | max = value; 112 | } 113 | } 114 | return max; 115 | } 116 | 117 | private int findMin(int[] firstPositions) { 118 | int min = firstPositions[0]; 119 | for (int value : firstPositions) { 120 | if (value < min) { 121 | min = value; 122 | } 123 | } 124 | return min; 125 | } 126 | 127 | public boolean isTop() { 128 | return isTop; 129 | } 130 | 131 | public boolean isBottom() { 132 | return isBottom; 133 | } 134 | 135 | public void goTop() { 136 | scrollToPosition(0); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/java/com/yc/ycshopdetaillayout/VerticalWebView.java: -------------------------------------------------------------------------------- 1 | package com.yc.ycshopdetaillayout; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.MotionEvent; 6 | import android.webkit.WebView; 7 | 8 | 9 | /** 10 | *
11 |  *     @author yangchong
12 |  *     blog  :
13 |  *     time  : 2018/6/6
14 |  *     desc  : 当WebView在最顶部或者最底部的时候,不消费事件
15 |  *     revise:
16 |  * 
17 | */ 18 | public class VerticalWebView extends WebView { 19 | 20 | private float downX; 21 | private float downY; 22 | 23 | public VerticalWebView(Context context) { 24 | this(context, null); 25 | } 26 | 27 | public VerticalWebView(Context context, AttributeSet attrs) { 28 | this(context, attrs, android.R.attr.webViewStyle); 29 | } 30 | 31 | public VerticalWebView(Context context, AttributeSet attrs, int defStyleAttr) { 32 | super(context, attrs, defStyleAttr); 33 | } 34 | 35 | @Override 36 | public boolean dispatchTouchEvent(MotionEvent ev) { 37 | switch (ev.getAction()) { 38 | case MotionEvent.ACTION_DOWN: 39 | downX = ev.getX(); 40 | downY = ev.getY(); 41 | //如果滑动到了最底部,就允许继续向上滑动加载下一页,否者不允许 42 | getParent().requestDisallowInterceptTouchEvent(true); 43 | break; 44 | case MotionEvent.ACTION_MOVE: 45 | float dx = ev.getX() - downX; 46 | float dy = ev.getY() - downY; 47 | boolean allowParentTouchEvent; 48 | if (Math.abs(dy) > Math.abs(dx)) { 49 | if (dy > 0) { 50 | //位于顶部时下拉,让父View消费事件 51 | allowParentTouchEvent = isTop(); 52 | } else { 53 | //位于底部时上拉,让父View消费事件 54 | allowParentTouchEvent = isBottom(); 55 | } 56 | } else { 57 | //水平方向滑动 58 | allowParentTouchEvent = true; 59 | } 60 | getParent().requestDisallowInterceptTouchEvent(!allowParentTouchEvent); 61 | break; 62 | default: 63 | break; 64 | } 65 | return super.dispatchTouchEvent(ev); 66 | } 67 | 68 | private boolean isTop() { 69 | return getScrollY() <= 0; 70 | } 71 | 72 | private boolean isBottom() { 73 | return getHeight() + getScrollY() >= getContentHeight() * getScale(); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/yc/ycshopdetaillayout/first/FirstActivity.java: -------------------------------------------------------------------------------- 1 | package com.yc.ycshopdetaillayout.first; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.Build; 5 | import android.os.Bundle; 6 | import android.util.Log; 7 | import android.webkit.WebSettings; 8 | import android.webkit.WebView; 9 | import android.webkit.WebViewClient; 10 | import android.widget.LinearLayout; 11 | 12 | import androidx.appcompat.app.AppCompatActivity; 13 | import androidx.fragment.app.FragmentManager; 14 | import androidx.fragment.app.FragmentTransaction; 15 | 16 | import com.yc.slide.SlideLayout; 17 | import com.ycbjie.ycshopdetaillayout.R; 18 | 19 | public class FirstActivity extends AppCompatActivity { 20 | 21 | private SlideLayout mSlideDetailsLayout; 22 | private ShopMainFragment shopMainFragment; 23 | private WebView webView; 24 | private LinearLayout mLlDetail; 25 | 26 | 27 | @Override 28 | protected void onCreate(Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | setContentView(R.layout.activity_first); 31 | 32 | initView(); 33 | initShopMainFragment(); 34 | initSlideDetailsLayout(); 35 | initWebView(); 36 | } 37 | 38 | private void initView() { 39 | mSlideDetailsLayout = findViewById(R.id.slideDetailsLayout); 40 | webView = findViewById(R.id.wb_view); 41 | mLlDetail = findViewById(R.id.ll_detail); 42 | } 43 | 44 | 45 | private void initShopMainFragment() { 46 | FragmentManager fm = getSupportFragmentManager(); 47 | FragmentTransaction fragmentTransaction = fm.beginTransaction(); 48 | if(shopMainFragment==null){ 49 | shopMainFragment = new ShopMainFragment(); 50 | fragmentTransaction 51 | .replace(R.id.fl_shop_main, shopMainFragment) 52 | .commit(); 53 | }else { 54 | fragmentTransaction.show(shopMainFragment); 55 | } 56 | } 57 | 58 | private void initSlideDetailsLayout() { 59 | mSlideDetailsLayout.setOnSlideDetailsListener(new SlideLayout.OnSlideDetailsListener() { 60 | @Override 61 | public void onStatusChanged(SlideLayout.Status status) { 62 | if (status == SlideLayout.Status.OPEN) { 63 | //当前为图文详情页 64 | Log.e("FirstActivity","下拉回到商品详情"); 65 | shopMainFragment.changBottomView(true); 66 | } else { 67 | //当前为商品详情页 68 | Log.e("FirstActivity","继续上拉,查看图文详情"); 69 | shopMainFragment.changBottomView(false); 70 | } 71 | } 72 | }); 73 | } 74 | 75 | 76 | @SuppressLint({"ObsoleteSdkInt", "SetJavaScriptEnabled"}) 77 | private void initWebView() { 78 | final WebSettings settings = webView.getSettings(); 79 | settings.setJavaScriptEnabled(true); 80 | settings.setSupportZoom(true); 81 | settings.setBuiltInZoomControls(true); 82 | settings.setUseWideViewPort(true); 83 | settings.setDomStorageEnabled(true); 84 | webView.setWebViewClient(new WebViewClient() { 85 | 86 | @Override 87 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 88 | view.loadUrl(url); 89 | return true; 90 | } 91 | }); 92 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR_MR1) { 93 | new Object() { 94 | void setLoadWithOverviewMode(boolean overview) { 95 | settings.setLoadWithOverviewMode(overview); 96 | } 97 | }.setLoadWithOverviewMode(true); 98 | } 99 | 100 | settings.setCacheMode(WebSettings.LOAD_DEFAULT); 101 | 102 | getWindow().getDecorView().post(new Runnable() { 103 | @Override 104 | public void run() { 105 | webView.loadUrl("https://www.jianshu.com/p/d745ea0cb5bd"); 106 | } 107 | }); 108 | } 109 | 110 | 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/com/yc/ycshopdetaillayout/first/ShopDetailFragment.java: -------------------------------------------------------------------------------- 1 | package com.yc.ycshopdetaillayout.first; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.Build; 5 | import android.os.Bundle; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.webkit.WebSettings; 10 | import android.webkit.WebView; 11 | import android.webkit.WebViewClient; 12 | 13 | import androidx.annotation.NonNull; 14 | import androidx.annotation.Nullable; 15 | import androidx.fragment.app.Fragment; 16 | 17 | import com.ycbjie.ycshopdetaillayout.R; 18 | 19 | 20 | public class ShopDetailFragment extends Fragment { 21 | 22 | 23 | private WebView webView; 24 | 25 | @Nullable 26 | @Override 27 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 28 | return inflater.inflate(getContentView(), container , false); 29 | } 30 | 31 | @Override 32 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 33 | super.onViewCreated(view, savedInstanceState); 34 | initView(view); 35 | initWebView(); 36 | } 37 | 38 | private int getContentView() { 39 | return R.layout.include_shop_detail; 40 | } 41 | 42 | private void initView(View view) { 43 | webView = view.findViewById(R.id.wb_view); 44 | } 45 | 46 | @SuppressLint("ObsoleteSdkInt") 47 | private void initWebView() { 48 | final WebSettings settings = webView.getSettings(); 49 | settings.setJavaScriptEnabled(true); 50 | settings.setSupportZoom(true); 51 | settings.setBuiltInZoomControls(true); 52 | settings.setUseWideViewPort(true); 53 | settings.setDomStorageEnabled(true); 54 | webView.setWebViewClient(new WebViewClient() { 55 | 56 | @Override 57 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 58 | view.loadUrl(url); 59 | return true; 60 | } 61 | }); 62 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR_MR1) { 63 | new Object() { 64 | void setLoadWithOverviewMode(boolean overview) { 65 | settings.setLoadWithOverviewMode(overview); 66 | } 67 | }.setLoadWithOverviewMode(true); 68 | } 69 | 70 | settings.setCacheMode(WebSettings.LOAD_DEFAULT); 71 | 72 | webView.loadUrl("https://www.jianshu.com/p/d745ea0cb5bd"); 73 | } 74 | 75 | 76 | 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/yc/ycshopdetaillayout/first/ShopMainFragment.java: -------------------------------------------------------------------------------- 1 | package com.yc.ycshopdetaillayout.first; 2 | 3 | import android.os.Bundle; 4 | import androidx.annotation.NonNull; 5 | import androidx.annotation.Nullable; 6 | import androidx.fragment.app.Fragment; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.ImageView; 11 | import android.widget.LinearLayout; 12 | import android.widget.ScrollView; 13 | import android.widget.TextView; 14 | 15 | import com.ycbjie.ycshopdetaillayout.R; 16 | 17 | 18 | public class ShopMainFragment extends Fragment { 19 | 20 | private ScrollView mScrollView; 21 | private TextView mTvGoodsTitle; 22 | private TextView mTvNewPrice; 23 | private TextView mTvOldPrice; 24 | private LinearLayout mLlActivity; 25 | private LinearLayout mLlCurrentGoods; 26 | private TextView mTvCurrentGoods; 27 | private ImageView mIvEnsure; 28 | private LinearLayout mLlComment; 29 | private TextView mTvCommentCount; 30 | private TextView mTvGoodComment; 31 | private ImageView mIvCommentRight; 32 | private LinearLayout mLlEmptyComment; 33 | private LinearLayout mLlRecommend; 34 | private TextView mTvBottomView; 35 | 36 | @Nullable 37 | @Override 38 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 39 | return inflater.inflate(getContentView(), container , false); 40 | } 41 | 42 | @Override 43 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 44 | super.onViewCreated(view, savedInstanceState); 45 | initView(view); 46 | } 47 | 48 | private int getContentView() { 49 | return R.layout.include_shop_main; 50 | } 51 | 52 | 53 | private void initView(View view) { 54 | mScrollView = view.findViewById(R.id.scrollView); 55 | mTvGoodsTitle = view.findViewById(R.id.tv_goods_title); 56 | mTvNewPrice = view.findViewById(R.id.tv_new_price); 57 | mTvOldPrice = view.findViewById(R.id.tv_old_price); 58 | mLlCurrentGoods = view.findViewById(R.id.ll_current_goods); 59 | mTvCurrentGoods = view.findViewById(R.id.tv_current_goods); 60 | mIvEnsure = view.findViewById(R.id.iv_ensure); 61 | mLlComment = view.findViewById(R.id.ll_comment); 62 | mTvCommentCount = view.findViewById(R.id.tv_comment_count); 63 | mTvGoodComment = view.findViewById(R.id.tv_good_comment); 64 | mIvCommentRight = view.findViewById(R.id.iv_comment_right); 65 | mLlEmptyComment = view.findViewById(R.id.ll_empty_comment); 66 | mLlRecommend = view.findViewById(R.id.ll_recommend); 67 | mTvBottomView = view.findViewById(R.id.tv_bottom_view); 68 | 69 | } 70 | 71 | 72 | public void changBottomView(boolean isDetail) { 73 | if(isDetail){ 74 | mTvBottomView.setText("下拉回到商品详情"); 75 | }else { 76 | mTvBottomView.setText("继续上拉,查看图文详情"); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/yc/ycshopdetaillayout/four/FourActivity.java: -------------------------------------------------------------------------------- 1 | package com.yc.ycshopdetaillayout.four; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.graphics.Color; 6 | import android.os.Build; 7 | import android.os.Bundle; 8 | import android.view.View; 9 | import android.webkit.WebSettings; 10 | import android.webkit.WebView; 11 | import android.webkit.WebViewClient; 12 | import android.widget.FrameLayout; 13 | import android.widget.ImageView; 14 | import android.widget.LinearLayout; 15 | import android.widget.ScrollView; 16 | import android.widget.TextView; 17 | 18 | import androidx.appcompat.app.AppCompatActivity; 19 | import androidx.core.widget.NestedScrollView; 20 | import androidx.fragment.app.FragmentManager; 21 | import androidx.fragment.app.FragmentTransaction; 22 | 23 | import com.yc.slide.LoggerUtils; 24 | import com.yc.slide.SlideAnimLayout; 25 | import com.yc.ycshopdetaillayout.second.ShopMain1Fragment; 26 | import com.ycbjie.ycshopdetaillayout.R; 27 | 28 | 29 | public class FourActivity extends AppCompatActivity { 30 | 31 | private SlideAnimLayout mSlideDetailsLayout; 32 | private ShopMain1Fragment shopMainFragment; 33 | private WebView webView; 34 | private LinearLayout ll_page_more; 35 | private ImageView mIvMoreImg; 36 | private TextView mTvMoreText; 37 | private TextView tvBarGoods; 38 | private TextView tvBarDetail; 39 | private LinearLayout root; 40 | private NestedScrollView scrollView; 41 | 42 | @Override 43 | protected void onCreate(Bundle savedInstanceState) { 44 | super.onCreate(savedInstanceState); 45 | setContentView(R.layout.activity_four); 46 | 47 | initView(); 48 | initListener(); 49 | initShopMainFragment(); 50 | initSlideDetailsLayout(); 51 | initWebView(); 52 | } 53 | 54 | private void initView() { 55 | root = findViewById(R.id.root); 56 | mSlideDetailsLayout = findViewById(R.id.slideDetailsLayout); 57 | ll_page_more = findViewById(R.id.ll_page_more); 58 | webView = findViewById(R.id.wb_view); 59 | mIvMoreImg = findViewById(R.id.iv_more_img); 60 | mTvMoreText = findViewById(R.id.tv_more_text); 61 | tvBarGoods = findViewById(R.id.tv_bar_goods); 62 | tvBarDetail = findViewById(R.id.tv_bar_detail); 63 | scrollView = findViewById(R.id.scrollView); 64 | } 65 | 66 | private void initListener() { 67 | View.OnClickListener onClickListener = new View.OnClickListener() { 68 | @Override 69 | public void onClick(View v) { 70 | switch (v.getId()){ 71 | case R.id.tv_bar_goods: 72 | tvBarGoods.setTextColor(Color.RED); 73 | tvBarDetail.setTextColor(Color.BLACK); 74 | mSlideDetailsLayout.smoothClose(true); 75 | break; 76 | case R.id.tv_bar_detail: 77 | tvBarGoods.setTextColor(Color.BLACK); 78 | tvBarDetail.setTextColor(Color.RED); 79 | mSlideDetailsLayout.smoothOpen(true); 80 | break; 81 | default: 82 | break; 83 | } 84 | } 85 | }; 86 | tvBarGoods.setOnClickListener(onClickListener); 87 | tvBarDetail.setOnClickListener(onClickListener); 88 | } 89 | 90 | 91 | private void initShopMainFragment() { 92 | FragmentManager fm = getSupportFragmentManager(); 93 | FragmentTransaction fragmentTransaction = fm.beginTransaction(); 94 | if(shopMainFragment==null){ 95 | shopMainFragment = new ShopMain1Fragment(); 96 | fragmentTransaction 97 | .replace(R.id.fl_shop_main2, shopMainFragment) 98 | .commit(); 99 | }else { 100 | fragmentTransaction.show(shopMainFragment); 101 | } 102 | } 103 | 104 | private void initSlideDetailsLayout() { 105 | mSlideDetailsLayout.setScrollStatusListener(new SlideAnimLayout.onScrollStatusListener() { 106 | @Override 107 | public void onStatusChanged(SlideAnimLayout.Status mNowStatus, boolean isHalf) { 108 | if(mNowStatus== SlideAnimLayout.Status.CLOSE){ 109 | //打开 110 | if(isHalf){ 111 | mTvMoreText.setText("释放,查看图文详情"); 112 | mIvMoreImg.animate().rotation(0); 113 | LoggerUtils.i("onStatusChanged---CLOSE---释放"+isHalf); 114 | }else{//关闭 115 | mTvMoreText.setText("继续上拉,查看图文详情"); 116 | mIvMoreImg.animate().rotation(180); 117 | LoggerUtils.i("onStatusChanged---CLOSE---继续上拉"+isHalf); 118 | } 119 | }else{ 120 | //打开 121 | if(isHalf){ 122 | mTvMoreText.setText("下拉回到商品详情"); 123 | mIvMoreImg.animate().rotation(0); 124 | LoggerUtils.i("onStatusChanged---OPEN---下拉回到商品详情"+isHalf); 125 | }else{//关闭 126 | mTvMoreText.setText("释放回到商品详情"); 127 | mIvMoreImg.animate().rotation(180); 128 | LoggerUtils.i("onStatusChanged---OPEN---释放回到商品详情"+isHalf); 129 | } 130 | } 131 | } 132 | }); 133 | mSlideDetailsLayout.setOnSlideStatusListener(new SlideAnimLayout.OnSlideStatusListener() { 134 | @Override 135 | public void onStatusChanged(SlideAnimLayout.Status status) { 136 | FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) 137 | root.getLayoutParams(); 138 | if (status == SlideAnimLayout.Status.OPEN) { 139 | layoutParams.topMargin = dip2px(FourActivity.this,44.0f); 140 | root.setLayoutParams(layoutParams); 141 | LoggerUtils.i("setOnSlideStatusListener---OPEN---下拉回到商品详情"); 142 | //scrollView.scrollTo(0,0); 143 | scrollView.post(new Runnable() { 144 | @Override 145 | public void run() { 146 | //ScrollView滑动到顶部 147 | scrollView.fullScroll(ScrollView.FOCUS_UP); 148 | } 149 | }); 150 | } else { 151 | layoutParams.topMargin = dip2px(FourActivity.this,0); 152 | root.setLayoutParams(layoutParams); 153 | LoggerUtils.i("setOnSlideStatusListener---CLOSE---上拉查看图文详情"); 154 | } 155 | } 156 | }); 157 | } 158 | 159 | 160 | @SuppressLint({"ObsoleteSdkInt", "SetJavaScriptEnabled"}) 161 | private void initWebView() { 162 | final WebSettings settings = webView.getSettings(); 163 | settings.setJavaScriptEnabled(true); 164 | settings.setSupportZoom(true); 165 | settings.setBuiltInZoomControls(true); 166 | settings.setUseWideViewPort(true); 167 | settings.setDomStorageEnabled(true); 168 | webView.setWebViewClient(new WebViewClient() { 169 | @Override 170 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 171 | view.loadUrl(url); 172 | return true; 173 | } 174 | }); 175 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR_MR1) { 176 | new Object() { 177 | void setLoadWithOverviewMode(boolean overview) { 178 | settings.setLoadWithOverviewMode(overview); 179 | } 180 | }.setLoadWithOverviewMode(true); 181 | } 182 | 183 | settings.setCacheMode(WebSettings.LOAD_DEFAULT); 184 | 185 | getWindow().getDecorView().post(new Runnable() { 186 | @Override 187 | public void run() { 188 | webView.loadUrl("https://www.jianshu.com/p/d745ea0cb5bd"); 189 | } 190 | }); 191 | } 192 | 193 | 194 | 195 | 196 | public static int dip2px(Context context, float dpValue) { 197 | final float scale = context.getResources().getDisplayMetrics().density; 198 | return (int) (dpValue * scale + 0.5f); 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /app/src/main/java/com/yc/ycshopdetaillayout/second/SecondActivity.java: -------------------------------------------------------------------------------- 1 | package com.yc.ycshopdetaillayout.second; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.Build; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.webkit.WebSettings; 8 | import android.webkit.WebView; 9 | import android.webkit.WebViewClient; 10 | import android.widget.ImageView; 11 | import android.widget.LinearLayout; 12 | import android.widget.TextView; 13 | 14 | import androidx.appcompat.app.AppCompatActivity; 15 | import androidx.fragment.app.FragmentManager; 16 | import androidx.fragment.app.FragmentTransaction; 17 | 18 | import com.yc.slide.SlideAnimLayout; 19 | import com.ycbjie.ycshopdetaillayout.R; 20 | 21 | 22 | public class SecondActivity extends AppCompatActivity { 23 | 24 | private SlideAnimLayout mSlideDetailsLayout; 25 | private ShopMain1Fragment shopMainFragment; 26 | private WebView webView; 27 | private LinearLayout ll_page_more; 28 | private ImageView mIvMoreImg; 29 | private TextView mTvMoreText; 30 | private boolean isBtn = true; 31 | 32 | @Override 33 | protected void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_second); 36 | 37 | initView(); 38 | initShopMainFragment(); 39 | initSlideDetailsLayout(); 40 | initWebView(); 41 | findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() { 42 | @Override 43 | public void onClick(View v) { 44 | if (isBtn){ 45 | isBtn = false; 46 | mSlideDetailsLayout.smoothOpen(true); 47 | }else { 48 | isBtn = true; 49 | mSlideDetailsLayout.smoothClose(true); 50 | } 51 | } 52 | }); 53 | } 54 | 55 | private void initView() { 56 | mSlideDetailsLayout = findViewById(R.id.slideDetailsLayout); 57 | ll_page_more = findViewById(R.id.ll_page_more); 58 | webView = findViewById(R.id.wb_view); 59 | mIvMoreImg = findViewById(R.id.iv_more_img); 60 | mTvMoreText = findViewById(R.id.tv_more_text); 61 | 62 | } 63 | 64 | 65 | private void initShopMainFragment() { 66 | FragmentManager fm = getSupportFragmentManager(); 67 | FragmentTransaction fragmentTransaction = fm.beginTransaction(); 68 | if(shopMainFragment==null){ 69 | shopMainFragment = new ShopMain1Fragment(); 70 | fragmentTransaction 71 | .replace(R.id.fl_shop_main2, shopMainFragment) 72 | .commit(); 73 | }else { 74 | fragmentTransaction.show(shopMainFragment); 75 | } 76 | } 77 | 78 | private void initSlideDetailsLayout() { 79 | mSlideDetailsLayout.setScrollStatusListener(new SlideAnimLayout.onScrollStatusListener() { 80 | @Override 81 | public void onStatusChanged(SlideAnimLayout.Status mNowStatus,boolean isHalf) { 82 | if(mNowStatus==SlideAnimLayout.Status.CLOSE){ 83 | if(isHalf){//打开 84 | mTvMoreText.setText("释放,查看图文详情"); 85 | mIvMoreImg.animate().rotation(0); 86 | }else{//关闭 87 | mTvMoreText.setText("继续上拉,查看图文详情"); 88 | mIvMoreImg.animate().rotation(180); 89 | } 90 | }else{ 91 | if(isHalf){//打开 92 | mTvMoreText.setText("下拉回到商品详情"); 93 | mIvMoreImg.animate().rotation(0); 94 | }else{//关闭 95 | mTvMoreText.setText("释放回到商品详情"); 96 | mIvMoreImg.animate().rotation(180); 97 | } 98 | } 99 | } 100 | }); 101 | } 102 | 103 | 104 | @SuppressLint({"ObsoleteSdkInt", "SetJavaScriptEnabled"}) 105 | private void initWebView() { 106 | final WebSettings settings = webView.getSettings(); 107 | settings.setJavaScriptEnabled(true); 108 | settings.setSupportZoom(true); 109 | settings.setBuiltInZoomControls(true); 110 | settings.setUseWideViewPort(true); 111 | settings.setDomStorageEnabled(true); 112 | webView.setWebViewClient(new WebViewClient() { 113 | 114 | @Override 115 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 116 | view.loadUrl(url); 117 | return true; 118 | } 119 | }); 120 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR_MR1) { 121 | new Object() { 122 | void setLoadWithOverviewMode(boolean overview) { 123 | settings.setLoadWithOverviewMode(overview); 124 | } 125 | }.setLoadWithOverviewMode(true); 126 | } 127 | 128 | settings.setCacheMode(WebSettings.LOAD_DEFAULT); 129 | 130 | getWindow().getDecorView().post(new Runnable() { 131 | @Override 132 | public void run() { 133 | webView.loadUrl("https://www.jianshu.com/p/d745ea0cb5bd"); 134 | } 135 | }); 136 | } 137 | 138 | 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/com/yc/ycshopdetaillayout/second/ShopMain1Fragment.java: -------------------------------------------------------------------------------- 1 | package com.yc.ycshopdetaillayout.second; 2 | 3 | import android.os.Bundle; 4 | import androidx.annotation.NonNull; 5 | import androidx.annotation.Nullable; 6 | import androidx.fragment.app.Fragment; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.ImageView; 11 | import android.widget.LinearLayout; 12 | import android.widget.ScrollView; 13 | import android.widget.TextView; 14 | 15 | import com.ycbjie.ycshopdetaillayout.R; 16 | 17 | 18 | public class ShopMain1Fragment extends Fragment { 19 | 20 | private ScrollView mScrollView; 21 | private TextView mTvGoodsTitle; 22 | private TextView mTvNewPrice; 23 | private TextView mTvOldPrice; 24 | private LinearLayout mLlActivity; 25 | private LinearLayout mLlCurrentGoods; 26 | private TextView mTvCurrentGoods; 27 | private ImageView mIvEnsure; 28 | private LinearLayout mLlComment; 29 | private TextView mTvCommentCount; 30 | private TextView mTvGoodComment; 31 | private ImageView mIvCommentRight; 32 | private LinearLayout mLlEmptyComment; 33 | private LinearLayout mLlRecommend; 34 | private TextView mTvBottomView; 35 | 36 | @Nullable 37 | @Override 38 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 39 | return inflater.inflate(getContentView(), container , false); 40 | } 41 | 42 | @Override 43 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 44 | super.onViewCreated(view, savedInstanceState); 45 | initView(view); 46 | } 47 | 48 | private int getContentView() { 49 | return R.layout.include_shop_main; 50 | } 51 | 52 | 53 | private void initView(View view) { 54 | mScrollView = view.findViewById(R.id.scrollView); 55 | mTvGoodsTitle = view.findViewById(R.id.tv_goods_title); 56 | mTvNewPrice = view.findViewById(R.id.tv_new_price); 57 | mTvOldPrice = view.findViewById(R.id.tv_old_price); 58 | mLlCurrentGoods = view.findViewById(R.id.ll_current_goods); 59 | mTvCurrentGoods = view.findViewById(R.id.tv_current_goods); 60 | mIvEnsure = view.findViewById(R.id.iv_ensure); 61 | mLlComment = view.findViewById(R.id.ll_comment); 62 | mTvCommentCount = view.findViewById(R.id.tv_comment_count); 63 | mTvGoodComment = view.findViewById(R.id.tv_good_comment); 64 | mIvCommentRight = view.findViewById(R.id.iv_comment_right); 65 | mLlEmptyComment = view.findViewById(R.id.ll_empty_comment); 66 | mLlRecommend = view.findViewById(R.id.ll_recommend); 67 | mTvBottomView = view.findViewById(R.id.tv_bottom_view); 68 | } 69 | 70 | 71 | public void changBottomView(boolean isDetail) { 72 | if(isDetail){ 73 | mTvBottomView.setText("下拉回到商品详情"); 74 | }else { 75 | mTvBottomView.setText("继续上拉,查看图文详情"); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/yc/ycshopdetaillayout/third/NoScrollWebView.java: -------------------------------------------------------------------------------- 1 | package com.yc.ycshopdetaillayout.third; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | import android.webkit.WebView; 7 | 8 | 9 | 10 | public class NoScrollWebView extends WebView { 11 | 12 | public NoScrollWebView(Context context) { 13 | super(context); 14 | initView(); 15 | } 16 | 17 | public NoScrollWebView(Context context, AttributeSet attrs) { 18 | super(context, attrs); 19 | initView(); 20 | } 21 | 22 | 23 | public NoScrollWebView(Context context, AttributeSet attrs, int defStyleAttr) { 24 | super(context, attrs, defStyleAttr); 25 | initView(); 26 | } 27 | 28 | private void initView() { 29 | 30 | } 31 | 32 | @Override 33 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 34 | int mExpandSpec = View.MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, View.MeasureSpec.AT_MOST); 35 | super.onMeasure(widthMeasureSpec, mExpandSpec); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/yc/ycshopdetaillayout/third/ThirdActivity.java: -------------------------------------------------------------------------------- 1 | package com.yc.ycshopdetaillayout.third; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.Build; 5 | import android.os.Bundle; 6 | import android.util.Log; 7 | import android.webkit.WebSettings; 8 | import android.webkit.WebView; 9 | import android.webkit.WebViewClient; 10 | import android.widget.LinearLayout; 11 | 12 | import androidx.appcompat.app.AppCompatActivity; 13 | import androidx.fragment.app.FragmentManager; 14 | import androidx.fragment.app.FragmentTransaction; 15 | 16 | import com.yc.slide.SlideLayout; 17 | import com.ycbjie.ycshopdetaillayout.R; 18 | import com.yc.ycshopdetaillayout.first.ShopMainFragment; 19 | 20 | /** 21 | * Created by yc on 2018/7/23. 22 | */ 23 | 24 | public class ThirdActivity extends AppCompatActivity { 25 | 26 | private SlideLayout mSlideDetailsLayout; 27 | private ShopMainFragment shopMainFragment; 28 | private WebView webView; 29 | private LinearLayout mLlDetail; 30 | 31 | 32 | @Override 33 | protected void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_third); 36 | 37 | initView(); 38 | initShopMainFragment(); 39 | initSlideDetailsLayout(); 40 | initWebView(); 41 | } 42 | 43 | private void initView() { 44 | mSlideDetailsLayout = findViewById(R.id.slideDetailsLayout); 45 | webView = findViewById(R.id.wb_view); 46 | mLlDetail = findViewById(R.id.ll_detail); 47 | } 48 | 49 | 50 | private void initShopMainFragment() { 51 | FragmentManager fm = getSupportFragmentManager(); 52 | FragmentTransaction fragmentTransaction = fm.beginTransaction(); 53 | if(shopMainFragment==null){ 54 | shopMainFragment = new ShopMainFragment(); 55 | fragmentTransaction 56 | .replace(R.id.fl_shop_main, shopMainFragment) 57 | .commit(); 58 | }else { 59 | fragmentTransaction.show(shopMainFragment); 60 | } 61 | } 62 | 63 | private void initSlideDetailsLayout() { 64 | mSlideDetailsLayout.setOnSlideDetailsListener(new SlideLayout.OnSlideDetailsListener() { 65 | @Override 66 | public void onStatusChanged(SlideLayout.Status status) { 67 | if (status == SlideLayout.Status.OPEN) { 68 | //当前为图文详情页 69 | Log.e("FirstActivity","图文详情页"); 70 | shopMainFragment.changBottomView(true); 71 | } else { 72 | //当前为商品详情页 73 | Log.e("FirstActivity","商品详情页"); 74 | shopMainFragment.changBottomView(false); 75 | } 76 | } 77 | }); 78 | } 79 | 80 | 81 | @SuppressLint({"ObsoleteSdkInt", "SetJavaScriptEnabled"}) 82 | private void initWebView() { 83 | final WebSettings settings = webView.getSettings(); 84 | settings.setJavaScriptEnabled(true); 85 | settings.setSupportZoom(true); 86 | settings.setBuiltInZoomControls(true); 87 | settings.setUseWideViewPort(true); 88 | settings.setDomStorageEnabled(true); 89 | webView.setWebViewClient(new WebViewClient() { 90 | 91 | @Override 92 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 93 | view.loadUrl(url); 94 | return true; 95 | } 96 | }); 97 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR_MR1) { 98 | new Object() { 99 | void setLoadWithOverviewMode(boolean overview) { 100 | settings.setLoadWithOverviewMode(overview); 101 | } 102 | }.setLoadWithOverviewMode(true); 103 | } 104 | 105 | settings.setCacheMode(WebSettings.LOAD_DEFAULT); 106 | 107 | getWindow().getDecorView().post(new Runnable() { 108 | @Override 109 | public void run() { 110 | webView.loadUrl("https://www.jianshu.com/p/d745ea0cb5bd"); 111 | } 112 | }); 113 | } 114 | 115 | 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_first.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_four.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 18 | 19 | 20 | 28 | 29 | 30 | 34 | 35 | 36 | 42 | 49 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 | 20 | 28 | 29 | 30 | 38 | 39 | 40 | 48 | 49 | 50 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_second.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 |