├── .gitignore ├── .gradle ├── 4.4 │ ├── fileChanges │ │ └── last-build.bin │ ├── fileContent │ │ └── fileContent.lock │ ├── fileHashes │ │ ├── fileHashes.bin │ │ ├── fileHashes.lock │ │ └── resourceHashesCache.bin │ ├── javaCompile │ │ ├── classAnalysis.bin │ │ ├── jarAnalysis.bin │ │ ├── javaCompile.lock │ │ ├── taskHistory.bin │ │ └── taskJars.bin │ └── taskHistory │ │ ├── taskHistory.bin │ │ └── taskHistory.lock └── buildOutputCleanup │ ├── buildOutputCleanup.lock │ ├── cache.properties │ └── outputFiles.bin ├── .idea ├── caches │ └── build_file_checksums.ser ├── codeStyles │ └── Project.xml ├── compiler.xml ├── dictionaries │ └── Glan.xml ├── encodings.xml ├── gradle.xml ├── libraries │ ├── Gradle__com_android_support_recyclerview_v7_23_2_0.xml │ ├── Gradle__com_android_support_support_annotations_27_1_1_jar.xml │ ├── Gradle__com_android_support_test_espresso_espresso_core_3_0_2.xml │ ├── Gradle__com_android_support_test_espresso_espresso_idling_resource_3_0_2.xml │ ├── Gradle__com_android_support_test_monitor_1_0_2.xml │ ├── Gradle__com_android_support_test_runner_1_0_2.xml │ ├── Gradle__com_google_code_findbugs_jsr305_2_0_1_jar.xml │ ├── Gradle__com_squareup_javawriter_2_1_1_jar.xml │ ├── Gradle__com_zhy_base_adapter_2_0_0.xml │ ├── Gradle__javax_inject_javax_inject_1_jar.xml │ ├── Gradle__junit_junit_4_12_jar.xml │ ├── Gradle__net_sf_kxml_kxml2_2_3_0_jar.xml │ ├── Gradle__org_hamcrest_hamcrest_core_1_3_jar.xml │ ├── Gradle__org_hamcrest_hamcrest_integration_1_3_jar.xml │ └── Gradle__org_hamcrest_hamcrest_library_1_3_jar.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── workspace.xml ├── GithubRep.iml ├── LICENSE ├── README.md ├── StickyHeaderViewPager.iml ├── StickyLayout.iml ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── activity │ │ └── newsreader │ │ └── netease │ │ └── com │ │ └── sticklayout │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── activity │ │ │ └── newsreader │ │ │ └── netease │ │ │ └── com │ │ │ └── sticklayout │ │ │ ├── MainActivity.java │ │ │ ├── TestActivity1.java │ │ │ ├── TestActivity2.java │ │ │ ├── TestActivity3.java │ │ │ ├── fragment │ │ │ ├── OtherTabFragment.java │ │ │ └── TabFragment.java │ │ │ └── view │ │ │ ├── IScrollable.java │ │ │ ├── NRStickyLayout.java │ │ │ ├── NRStickyLayout2.java │ │ │ ├── NestedLinearLayout.java │ │ │ ├── ScrollableViewImp.java │ │ │ └── ScrollerLayout.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_test1.xml │ │ ├── activity_test2.xml │ │ ├── activity_test3.xml │ │ ├── fragment_main.xml │ │ ├── fragment_other.xml │ │ └── item.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ids.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── activity │ └── newsreader │ └── netease │ └── com │ └── sticklayout │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.properties ├── output ├── big.gif ├── little.gif └── simple.apk └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | build/ 17 | .gradle 18 | captures/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | gradle.properties 23 | reviewlocal 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log files 28 | *.log 29 | 30 | # IDE files 31 | *.iml 32 | .idea 33 | *.DS_Store 34 | -------------------------------------------------------------------------------- /.gradle/4.4/fileChanges/last-build.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gradle/4.4/fileContent/fileContent.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffWangGithub/StickLayout/3bd346f24ec0f148fea6ec342347a5dd514e88b8/.gradle/4.4/fileContent/fileContent.lock -------------------------------------------------------------------------------- /.gradle/4.4/fileHashes/fileHashes.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffWangGithub/StickLayout/3bd346f24ec0f148fea6ec342347a5dd514e88b8/.gradle/4.4/fileHashes/fileHashes.bin -------------------------------------------------------------------------------- /.gradle/4.4/fileHashes/fileHashes.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffWangGithub/StickLayout/3bd346f24ec0f148fea6ec342347a5dd514e88b8/.gradle/4.4/fileHashes/fileHashes.lock -------------------------------------------------------------------------------- /.gradle/4.4/fileHashes/resourceHashesCache.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffWangGithub/StickLayout/3bd346f24ec0f148fea6ec342347a5dd514e88b8/.gradle/4.4/fileHashes/resourceHashesCache.bin -------------------------------------------------------------------------------- /.gradle/4.4/javaCompile/classAnalysis.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffWangGithub/StickLayout/3bd346f24ec0f148fea6ec342347a5dd514e88b8/.gradle/4.4/javaCompile/classAnalysis.bin -------------------------------------------------------------------------------- /.gradle/4.4/javaCompile/jarAnalysis.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffWangGithub/StickLayout/3bd346f24ec0f148fea6ec342347a5dd514e88b8/.gradle/4.4/javaCompile/jarAnalysis.bin -------------------------------------------------------------------------------- /.gradle/4.4/javaCompile/javaCompile.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffWangGithub/StickLayout/3bd346f24ec0f148fea6ec342347a5dd514e88b8/.gradle/4.4/javaCompile/javaCompile.lock -------------------------------------------------------------------------------- /.gradle/4.4/javaCompile/taskHistory.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffWangGithub/StickLayout/3bd346f24ec0f148fea6ec342347a5dd514e88b8/.gradle/4.4/javaCompile/taskHistory.bin -------------------------------------------------------------------------------- /.gradle/4.4/javaCompile/taskJars.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffWangGithub/StickLayout/3bd346f24ec0f148fea6ec342347a5dd514e88b8/.gradle/4.4/javaCompile/taskJars.bin -------------------------------------------------------------------------------- /.gradle/4.4/taskHistory/taskHistory.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffWangGithub/StickLayout/3bd346f24ec0f148fea6ec342347a5dd514e88b8/.gradle/4.4/taskHistory/taskHistory.bin -------------------------------------------------------------------------------- /.gradle/4.4/taskHistory/taskHistory.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffWangGithub/StickLayout/3bd346f24ec0f148fea6ec342347a5dd514e88b8/.gradle/4.4/taskHistory/taskHistory.lock -------------------------------------------------------------------------------- /.gradle/buildOutputCleanup/buildOutputCleanup.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffWangGithub/StickLayout/3bd346f24ec0f148fea6ec342347a5dd514e88b8/.gradle/buildOutputCleanup/buildOutputCleanup.lock -------------------------------------------------------------------------------- /.gradle/buildOutputCleanup/cache.properties: -------------------------------------------------------------------------------- 1 | #Thu May 24 18:38:50 CST 2018 2 | gradle.version=4.4 3 | -------------------------------------------------------------------------------- /.gradle/buildOutputCleanup/outputFiles.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffWangGithub/StickLayout/3bd346f24ec0f148fea6ec342347a5dd514e88b8/.gradle/buildOutputCleanup/outputFiles.bin -------------------------------------------------------------------------------- /.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffWangGithub/StickLayout/3bd346f24ec0f148fea6ec342347a5dd514e88b8/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | -------------------------------------------------------------------------------- /.idea/dictionaries/Glan.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__com_android_support_recyclerview_v7_23_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__com_android_support_support_annotations_27_1_1_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__com_android_support_test_espresso_espresso_core_3_0_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__com_android_support_test_espresso_espresso_idling_resource_3_0_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__com_android_support_test_monitor_1_0_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__com_android_support_test_runner_1_0_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__com_google_code_findbugs_jsr305_2_0_1_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__com_squareup_javawriter_2_1_1_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__com_zhy_base_adapter_2_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__javax_inject_javax_inject_1_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__junit_junit_4_12_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__net_sf_kxml_kxml2_2_3_0_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__org_hamcrest_hamcrest_core_1_3_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__org_hamcrest_hamcrest_integration_1_3_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/Gradle__org_hamcrest_hamcrest_library_1_3_jar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 13 | 14 | 27 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /GithubRep.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "{}" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright 2015 无舵航程 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StickLayout 2 | NestedScrolling嵌套滚动的使用 3 | 4 | [详情](http://glanwang.com/2018/05/23/Android/Android%E5%B5%8C%E5%A5%97%E6%BB%9A%E5%8A%A8%E5%85%A5%E9%97%A8/) 5 | -------------------------------------------------------------------------------- /StickyHeaderViewPager.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /StickyLayout.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | defaultConfig { 6 | applicationId "activity.newsreader.netease.com.sticklayout" 7 | minSdkVersion 14 8 | targetSdkVersion 27 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 'com.android.support:appcompat-v7:26.0.0' 24 | implementation 'com.zhy:base-adapter:2.0.0' 25 | testImplementation 'junit:junit:4.12' 26 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 27 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 28 | } 29 | -------------------------------------------------------------------------------- /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/androidTest/java/activity/newsreader/netease/com/sticklayout/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package activity.newsreader.netease.com.sticklayout; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("activity.newsreader.netease.com.sticklayout", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/activity/newsreader/netease/com/sticklayout/MainActivity.java: -------------------------------------------------------------------------------- 1 | package activity.newsreader.netease.com.sticklayout; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.app.FragmentActivity; 7 | import android.view.View; 8 | import android.widget.Button; 9 | 10 | public class MainActivity extends FragmentActivity implements View.OnClickListener { 11 | 12 | 13 | private Button mBtn1, mBtn2, mBtn3; 14 | 15 | @Override 16 | protected void onCreate(@Nullable Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | 19 | setContentView(R.layout.activity_main); 20 | 21 | mBtn1 = (Button) findViewById(R.id.btn1); 22 | mBtn2 = (Button) findViewById(R.id.btn2); 23 | mBtn3 = (Button) findViewById(R.id.btn3); 24 | 25 | mBtn1.setOnClickListener(this); 26 | mBtn2.setOnClickListener(this); 27 | mBtn3.setOnClickListener(this); 28 | 29 | } 30 | 31 | 32 | @Override 33 | public void onClick(View v) { 34 | switch (v.getId()) { 35 | case R.id.btn1: 36 | startActivity(new Intent(this, TestActivity1.class)); 37 | break; 38 | case R.id.btn2: 39 | startActivity(new Intent(this, TestActivity2.class)); 40 | break; 41 | case R.id.btn3: 42 | startActivity(new Intent(this, TestActivity3.class)); 43 | break; 44 | default: 45 | break; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/activity/newsreader/netease/com/sticklayout/TestActivity1.java: -------------------------------------------------------------------------------- 1 | package activity.newsreader.netease.com.sticklayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v4.app.FragmentActivity; 7 | import android.support.v4.app.FragmentPagerAdapter; 8 | import android.support.v4.view.ViewPager; 9 | 10 | import activity.newsreader.netease.com.sticklayout.fragment.OtherTabFragment; 11 | import activity.newsreader.netease.com.sticklayout.fragment.TabFragment; 12 | 13 | /** 14 | * @title: 测试 NRStickyLayout,嵌套滚动 15 | * @description: 16 | * @company: Netease 17 | * @author: GlanWang 18 | * @version: Created on 18/6/6. 19 | */ 20 | public class TestActivity1 extends FragmentActivity { 21 | private String[] mTitles = new String[] { "简介", "评价", "Other" }; 22 | 23 | private Fragment[] mFragments = new Fragment[mTitles.length]; 24 | 25 | private ViewPager mViewPager; 26 | private FragmentPagerAdapter mAdapter; 27 | 28 | @Override 29 | protected void onCreate(@Nullable Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | setContentView(R.layout.activity_test1); 32 | 33 | mViewPager = (ViewPager) findViewById(R.id.id_nr_stickylayout_viewpage); 34 | initData(); 35 | 36 | } 37 | 38 | private void initData() { 39 | for (int i = 0; i < mTitles.length; i++) { 40 | if (i == mTitles.length - 1) { 41 | mFragments[i] = Fragment.instantiate(this, OtherTabFragment.class.getName()); 42 | } else { 43 | mFragments[i] = Fragment.instantiate(this, TabFragment.class.getName()); 44 | } 45 | } 46 | mAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) { 47 | @Override 48 | public int getCount() { 49 | return mTitles.length; 50 | } 51 | 52 | @Override 53 | public Fragment getItem(int position) { 54 | return mFragments[position]; 55 | } 56 | }; 57 | mViewPager.setAdapter(mAdapter); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/activity/newsreader/netease/com/sticklayout/TestActivity2.java: -------------------------------------------------------------------------------- 1 | package activity.newsreader.netease.com.sticklayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.FragmentActivity; 6 | 7 | /** 8 | * @title: 测试 Scroller的用法 9 | * @description: 10 | * @company: Netease 11 | * @author: GlanWang 12 | * @version: Created on 18/6/6. 13 | */ 14 | public class TestActivity2 extends FragmentActivity { 15 | @Override 16 | protected void onCreate(@Nullable Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_test2); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/activity/newsreader/netease/com/sticklayout/TestActivity3.java: -------------------------------------------------------------------------------- 1 | package activity.newsreader.netease.com.sticklayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v4.app.FragmentActivity; 7 | import android.support.v4.app.FragmentPagerAdapter; 8 | import android.support.v4.view.ViewPager; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | 12 | import activity.newsreader.netease.com.sticklayout.fragment.OtherTabFragment; 13 | import activity.newsreader.netease.com.sticklayout.fragment.TabFragment; 14 | import activity.newsreader.netease.com.sticklayout.view.NRStickyLayout2; 15 | 16 | /** 17 | * @title: 18 | * @description: 19 | * @company: Netease 20 | * @author: GlanWang 21 | * @version: Created on 18/6/6. 22 | */ 23 | public class TestActivity3 extends FragmentActivity { 24 | 25 | private String[] mTitles = new String[] { "简介", "评价", "Other" }; 26 | 27 | private Fragment[] mFragments = new Fragment[mTitles.length]; 28 | 29 | private NRStickyLayout2 mSticktyLayout; 30 | private ViewPager mViewPager; 31 | private FragmentPagerAdapter mAdapter; 32 | 33 | @Override 34 | protected void onCreate(@Nullable Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | 37 | setContentView(R.layout.activity_test3); 38 | 39 | mSticktyLayout = (NRStickyLayout2) findViewById(R.id.sticky_view_layout); 40 | mViewPager = (ViewPager) findViewById(R.id.id_nr_stickylayout_viewpage); 41 | initData(); 42 | 43 | if (mFragments[0] instanceof TabFragment) { 44 | View scrollView = ((TabFragment) mFragments[0]).getScrollView(); 45 | mSticktyLayout.setCurrentScrollableView(scrollView); 46 | } 47 | } 48 | 49 | @Override 50 | protected void onResume() { 51 | super.onResume(); 52 | int currentItem = mViewPager.getCurrentItem(); 53 | Fragment item = mAdapter.getItem(currentItem); 54 | if (item instanceof TabFragment) { 55 | View scrollView = ((TabFragment) item).getScrollView(); 56 | mSticktyLayout.setCurrentScrollableView(scrollView); 57 | } 58 | } 59 | 60 | private void initData() { 61 | for (int i = 0; i < mTitles.length; i++) { 62 | if (i == mTitles.length - 1) { 63 | mFragments[i] = Fragment.instantiate(this, OtherTabFragment.class.getName()); 64 | } else { 65 | mFragments[i] = Fragment.instantiate(this, TabFragment.class.getName()); 66 | } 67 | } 68 | mAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) { 69 | @Override 70 | public int getCount() { 71 | return mTitles.length; 72 | } 73 | 74 | @Override 75 | public Fragment getItem(int position) { 76 | return mFragments[position]; 77 | } 78 | 79 | @Override 80 | public void setPrimaryItem(ViewGroup container, int position, Object object) { 81 | super.setPrimaryItem(container, position, object); 82 | setScrollView(); 83 | } 84 | }; 85 | mViewPager.setAdapter(mAdapter); 86 | } 87 | 88 | 89 | private void setScrollView() { 90 | View currentScrollableView = mSticktyLayout.getCurrentScrollableView(); 91 | int currentItem = mViewPager.getCurrentItem(); 92 | Fragment item = mAdapter.getItem(currentItem); 93 | if (item instanceof TabFragment) { 94 | View scrollView = ((TabFragment) item).getScrollView(); 95 | if (currentScrollableView != scrollView) { 96 | mSticktyLayout.setCurrentScrollableView(scrollView); 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/activity/newsreader/netease/com/sticklayout/fragment/OtherTabFragment.java: -------------------------------------------------------------------------------- 1 | package activity.newsreader.netease.com.sticklayout.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import activity.newsreader.netease.com.sticklayout.R; 11 | import activity.newsreader.netease.com.sticklayout.view.NestedLinearLayout; 12 | 13 | /** 14 | * @title: 15 | * @description: 16 | * @company: Netease 17 | * @author: GlanWang 18 | * @version: Created on 18/5/24. 19 | */ 20 | public class OtherTabFragment extends TabFragment { 21 | 22 | private NestedLinearLayout mNll; 23 | 24 | @Nullable 25 | @Override 26 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 27 | return inflater.inflate(R.layout.fragment_other, container, false); 28 | } 29 | 30 | @Override 31 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 32 | mNll = (NestedLinearLayout) view.findViewById(R.id.nested_lll); 33 | } 34 | 35 | 36 | @Override 37 | public View getScrollView() { 38 | return mNll; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/activity/newsreader/netease/com/sticklayout/fragment/TabFragment.java: -------------------------------------------------------------------------------- 1 | package activity.newsreader.netease.com.sticklayout.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.NonNull; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.app.Fragment; 7 | import android.support.v7.widget.LinearLayoutManager; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | 13 | import com.zhy.base.adapter.ViewHolder; 14 | import com.zhy.base.adapter.recyclerview.CommonAdapter; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | import activity.newsreader.netease.com.sticklayout.R; 20 | 21 | /** 22 | * @title: 23 | * @description: 24 | * @company: Netease 25 | * @author: GlanWang 26 | * @version: Created on 18/5/24. 27 | */ 28 | public class TabFragment extends Fragment { 29 | 30 | private RecyclerView mRecyclerView; 31 | private List mData = new ArrayList<>(); 32 | private String mTitle = ""; 33 | 34 | @Override 35 | public void onCreate(@Nullable Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | if (getArguments() != null) { 38 | mTitle = getArguments().getString("title"); 39 | } 40 | } 41 | 42 | @Nullable 43 | @Override 44 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 45 | return inflater.inflate(R.layout.fragment_main, container, false); 46 | } 47 | 48 | @Override 49 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 50 | super.onViewCreated(view, savedInstanceState); 51 | 52 | mRecyclerView = view.findViewById(R.id.recycler_view); 53 | mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); 54 | 55 | for (int i = 0; i < 50; i++) { 56 | mData.add(mTitle + "====" + i); 57 | } 58 | mRecyclerView.setAdapter(new CommonAdapter(getContext(), R.layout.item, mData){ 59 | @Override 60 | public void convert(ViewHolder holder, String s) { 61 | holder.setText(R.id.id_info, s); 62 | } 63 | }); 64 | } 65 | 66 | public View getScrollView() { 67 | return mRecyclerView; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/activity/newsreader/netease/com/sticklayout/view/IScrollable.java: -------------------------------------------------------------------------------- 1 | package activity.newsreader.netease.com.sticklayout.view; 2 | 3 | import android.view.View; 4 | 5 | /** 6 | * @title: 7 | * @description: 8 | * @company: Netease 9 | * @author: GlanWang 10 | * @version: Created on 18/6/5. 11 | */ 12 | public interface IScrollable { 13 | /** 14 | * 根据速度,距离,和事件进行平滑滚动 15 | * @param yVelocit 16 | * @param distance 17 | * @param duration 18 | */ 19 | void smoothScrollBy(int yVelocit, int distance, int duration); 20 | 21 | /** 22 | * 是否滑动到顶部 23 | */ 24 | boolean isTop(); 25 | 26 | 27 | View getScrollView(); 28 | 29 | void setScrollView(View scrollView); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/activity/newsreader/netease/com/sticklayout/view/NRStickyLayout.java: -------------------------------------------------------------------------------- 1 | package activity.newsreader.netease.com.sticklayout.view; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.view.ViewCompat; 6 | import android.support.v4.view.ViewPager; 7 | import android.util.AttributeSet; 8 | import android.util.Log; 9 | import android.view.MotionEvent; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.LinearLayout; 13 | import android.widget.Scroller; 14 | 15 | import activity.newsreader.netease.com.sticklayout.R; 16 | 17 | /** 18 | * @title: 19 | * @description: 20 | * @company: Netease 21 | * @author: GlanWang 22 | * @version: Created on 18/5/24. 23 | */ 24 | public class NRStickyLayout extends LinearLayout { 25 | 26 | private static final String TAG = "NRStickyLayout"; 27 | 28 | public static final int SCROLL_UP = 1; //向上滚动 29 | public static final int SCROLL_DOWN = 2; //向下滚动 30 | 31 | private static final int mMaxVelocity = 10000; 32 | 33 | private Scroller mScroller; 34 | private View mTopView; 35 | private View mStickyView; 36 | private ViewPager mViewPager; 37 | private int mMaxScrollY; 38 | private int mMinScrollY; 39 | private int mStickyViewMarginTop = 0; 40 | private TopViewScrollCallback mTopViewScrollCallback; 41 | private int mScrollY; 42 | private int mMaxViewPagerHeight = 0; 43 | private IScrollable mScrollable; 44 | private int mCurDirection; 45 | private boolean isPreFling = false; 46 | 47 | public NRStickyLayout(Context context) { 48 | this(context, null); 49 | } 50 | 51 | public NRStickyLayout(Context context, @Nullable AttributeSet attrs) { 52 | this(context, attrs, 0); 53 | } 54 | 55 | public NRStickyLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 56 | super(context, attrs, defStyleAttr); 57 | setOrientation(LinearLayout.VERTICAL); 58 | 59 | mScroller = new Scroller(context); 60 | mScrollable = new ScrollableViewImp(); 61 | } 62 | 63 | @Override 64 | protected void onFinishInflate() { 65 | super.onFinishInflate(); 66 | //获取 view 67 | mTopView = findViewById(R.id.id_nr_stickylayout_top_view); 68 | mStickyView = findViewById(R.id.id_nr_stickylayout_sticky_view); 69 | View viewPager = findViewById(R.id.id_nr_stickylayout_viewpage); 70 | if (!(viewPager instanceof ViewPager)) { 71 | throw new RuntimeException("id_nr_stickylayout_viewpage must be viewpager"); 72 | } 73 | mViewPager = ((ViewPager) viewPager); 74 | } 75 | 76 | @Override 77 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 78 | int newHeight = MeasureSpec.getSize(heightMeasureSpec) - mStickyViewMarginTop; 79 | int newHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.getMode(heightMeasureSpec)); 80 | super.onMeasure(widthMeasureSpec, newHeightSpec); 81 | if (mTopView != null) { 82 | //不限制顶部 view 的高度 83 | mTopView.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 84 | ViewGroup.LayoutParams params = mViewPager.getLayoutParams(); 85 | mMaxViewPagerHeight = Math.max(mMaxViewPagerHeight, (getMeasuredHeight() - mStickyView.getMeasuredHeight())); 86 | params.height = mMaxViewPagerHeight; 87 | setMeasuredDimension(getMeasuredWidth(), mTopView.getMeasuredHeight() + mStickyView.getMeasuredHeight() + mViewPager.getMeasuredHeight()); 88 | } 89 | } 90 | 91 | 92 | @Override 93 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 94 | super.onSizeChanged(w, h, oldw, oldh); 95 | mMaxScrollY = mTopView.getMeasuredHeight() - mStickyViewMarginTop; 96 | } 97 | 98 | @Override 99 | public boolean dispatchTouchEvent(MotionEvent ev) { 100 | switch (ev.getAction()) { 101 | case MotionEvent.ACTION_DOWN: 102 | mScroller.abortAnimation(); 103 | break; 104 | 105 | } 106 | return super.dispatchTouchEvent(ev); 107 | } 108 | 109 | 110 | @Override 111 | public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { 112 | Log.d(TAG, "onStartNestedScroll--" + "ViewCompat.SCROLL_AXIS_VERTICAL = " + ViewCompat.SCROLL_AXIS_VERTICAL + "; nestedScrollAxes= " + nestedScrollAxes); 113 | //拦截垂直滚动 114 | mScrollable.setScrollView(target); 115 | return (nestedScrollAxes & getNestedScrollAxes()) != 0; 116 | } 117 | 118 | @Override 119 | public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { 120 | Log.d(TAG, "onNestedScrollAccepted--"); 121 | 122 | } 123 | 124 | @Override 125 | public void onStopNestedScroll(View target) { 126 | Log.d(TAG, "onStopNestedScroll"); 127 | } 128 | 129 | @Override 130 | public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { 131 | Log.d(TAG, "onNestedScroll"); 132 | } 133 | 134 | @Override 135 | public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { 136 | Log.d(TAG, "onNestedPreScroll dy = " + dy); 137 | //上滑 138 | boolean hiddenTop = dy > 0 && getScrollY() < mMaxScrollY; 139 | //下滑动,且子 view 不可滑动了 140 | boolean showTop = dy < 0 && getScrollY() > 0 && mScrollable.isTop(); 141 | if (hiddenTop || showTop) { 142 | // 滚动自己 143 | scrollBy(0, dy); 144 | //消费掉 dy 145 | consumed[1] = dy; 146 | } 147 | } 148 | 149 | 150 | @Override 151 | public boolean onNestedPreFling(View target, float velocityX, float velocityY) { 152 | Log.d(TAG, "onNestedPreFling"); 153 | if (velocityY == 0) { 154 | return false; 155 | } 156 | mCurDirection = velocityY < 0 ? SCROLL_DOWN : SCROLL_UP; 157 | if (mScrollY > 0) { 158 | isPreFling = true; 159 | mScroller.fling(0, getScrollY(), (int)velocityX, (int)velocityY, 0, 0, 160 | -Integer.MAX_VALUE, Integer.MAX_VALUE); 161 | invalidate(); 162 | return true; 163 | } 164 | return false; 165 | } 166 | 167 | 168 | @Override 169 | public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { 170 | Log.d(TAG, "onNestedFling"); 171 | return true; 172 | } 173 | 174 | @Override 175 | public void computeScroll() { 176 | if (mScroller.computeScrollOffset()) { 177 | final int currY = mScroller.getCurrY(); 178 | if (mCurDirection == SCROLL_UP) { 179 | //手势向上 180 | if (isSticked()) { 181 | int dy = mScroller.getFinalY() - currY; 182 | if (dy > 0) { 183 | int duration = mScroller.getDuration() - mScroller.timePassed(); 184 | int currVelocity = (int) mScroller.getCurrVelocity(); 185 | currVelocity = currVelocity > mMaxVelocity ? mMaxVelocity : currVelocity; 186 | mScroller.abortAnimation(); 187 | mScrollable.smoothScrollBy(currVelocity, dy, duration); 188 | } 189 | Log.d("wgc", "upup---dy" + dy); 190 | } else { 191 | int dy = mScroller.getCurrY() - mScrollY; 192 | int toY = getScrollY() + dy; 193 | scrollTo(0, toY); 194 | invalidate(); 195 | } 196 | } else { 197 | //手势向下 198 | if (mScrollable.isTop()) { 199 | int dy = currY - mScrollY; 200 | int toY = getScrollY() + dy; 201 | scrollTo(0, toY); 202 | if (mScrollY <= mMinScrollY) { 203 | mScroller.abortAnimation(); 204 | } 205 | } else { 206 | int dy = mScroller.getCurrY() - mScrollY; 207 | int duration = mScroller.getDuration() - mScroller.timePassed(); 208 | int currVelocity = (int)mScroller.getCurrVelocity(); 209 | currVelocity = currVelocity > mMaxVelocity ? mMaxVelocity : currVelocity; 210 | if (isPreFling) { 211 | mScrollable.smoothScrollBy(-currVelocity, dy, duration); 212 | isPreFling = false; 213 | } 214 | } 215 | //刷新调用computeScroll() 方法,时时判断是否滚动 top 状态 216 | invalidate(); 217 | } 218 | } 219 | } 220 | 221 | @Override 222 | public int getNestedScrollAxes() { 223 | return ViewCompat.SCROLL_AXIS_VERTICAL; 224 | } 225 | 226 | 227 | @Override 228 | public void scrollTo(int x, int y) { 229 | y = y < 0 ? 0 : y; 230 | y = y > mMaxScrollY ? mMaxScrollY : y; 231 | if (y != getScrollY()) { 232 | super.scrollTo(x, y); 233 | if (mTopViewScrollCallback != null) { 234 | int scrollY = getScrollY(); 235 | float per = mMaxScrollY == 0 ? 0 : ((float) scrollY)/ mMaxScrollY; 236 | mTopViewScrollCallback.onTopViewScroll(scrollY, per); 237 | Log.d("aaa", "scroolY = " + scrollY + " percent = " + per); 238 | } 239 | mScrollY = getScrollY(); 240 | } else { 241 | int scrollY = getScrollY(); 242 | if (mScrollY != scrollY) { 243 | mScrollY = scrollY; 244 | if (mTopViewScrollCallback != null) { 245 | float per = mMaxScrollY == 0 ? 0 : ((float) scrollY)/ mMaxScrollY; 246 | mTopViewScrollCallback.onTopViewScroll(mScrollY, per); 247 | Log.d("aaa", "y == getScrollY" + " percent = " + per); 248 | } 249 | } 250 | } 251 | } 252 | 253 | 254 | 255 | /** 256 | * 设置 stickyView 悬浮之后距离顶部的高度 257 | * @param margintTop 258 | */ 259 | public void setStickyViewMarginTop(int margintTop) { 260 | if (margintTop < 0) { 261 | return; 262 | } 263 | mStickyViewMarginTop = margintTop; 264 | } 265 | 266 | 267 | public void setTopViewScrollCallback(TopViewScrollCallback callback) { 268 | mTopViewScrollCallback = callback; 269 | } 270 | 271 | 272 | public interface TopViewScrollCallback { 273 | void onTopViewScroll(int currentScrolledY, float scrolledYPercent); 274 | } 275 | 276 | 277 | private boolean isSticked() { 278 | return mScrollY >= mMaxScrollY; 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /app/src/main/java/activity/newsreader/netease/com/sticklayout/view/NRStickyLayout2.java: -------------------------------------------------------------------------------- 1 | package activity.newsreader.netease.com.sticklayout.view; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.view.ViewPager; 6 | import android.util.AttributeSet; 7 | import android.view.MotionEvent; 8 | import android.view.VelocityTracker; 9 | import android.view.View; 10 | import android.view.ViewConfiguration; 11 | import android.widget.LinearLayout; 12 | import android.widget.Scroller; 13 | 14 | import activity.newsreader.netease.com.sticklayout.R; 15 | 16 | /** 17 | * @title: 自己处理事件实现 viewpage 的 sticky layout 18 | * @description: 19 | * @company: Netease 20 | * @author: GlanWang 21 | * @version: Created on 18/6/4. 22 | */ 23 | public class NRStickyLayout2 extends LinearLayout{ 24 | private static final String TAG = "StickyLayout2"; 25 | 26 | public static final int SCROLL_UP = 1; //向上滚动 27 | public static final int SCROLL_DOWN = 2; //向下滚动 28 | 29 | private Scroller mScroller; 30 | private IScrollable mScrollable; 31 | private int mTouchSlop; //系统判定是 move 的最小距离 32 | private int mMaximumFlingVelocity; //最大的认定为fling 动作的最小速度 33 | private View mHeaderView; 34 | private ViewPager mViewPager; 35 | private int mHeaderHeight; 36 | private int mMaxScrollY; //当前 view 可滚动的最大值 37 | private int mMinScrollY; //当前 view 可滚动的最小值 38 | private VelocityTracker mVelocityTracker; 39 | private float mLastX,mLastY; 40 | private int mCurY; //当前滚动过的距离 41 | private int mCurDirection = 0; 42 | private int mLastScrollerY; 43 | private boolean verticalScrollFlag = false; 44 | private float mDownX; //第一次按下的x坐标 45 | private float mDownY; //第一次按下的y坐标 46 | 47 | public NRStickyLayout2(Context context) { 48 | this(context, null); 49 | } 50 | 51 | public NRStickyLayout2(Context context, @Nullable AttributeSet attrs) { 52 | this(context, attrs, 0); 53 | } 54 | 55 | public NRStickyLayout2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 56 | super(context, attrs, defStyleAttr); 57 | mScroller = new Scroller(context); 58 | mScrollable = new ScrollableViewImp(); 59 | 60 | mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 61 | 62 | mMaximumFlingVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity(); 63 | setOrientation(VERTICAL); 64 | } 65 | 66 | @Override 67 | protected void onFinishInflate() { 68 | super.onFinishInflate(); 69 | //获取 view 70 | mHeaderView = findViewById(R.id.id_nr_stickylayout_top_view); 71 | View viewPager = findViewById(R.id.id_nr_stickylayout_viewpage); 72 | if (!(viewPager instanceof ViewPager)) { 73 | throw new RuntimeException("id_nr_stickylayout_viewpage must be viewpager"); 74 | } 75 | mViewPager = ((ViewPager) viewPager); 76 | } 77 | 78 | 79 | @Override 80 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 81 | //不限定 header 的高度 82 | measureChildWithMargins(mHeaderView, widthMeasureSpec, 0, MeasureSpec.UNSPECIFIED, 0); 83 | mHeaderHeight = mHeaderView.getMeasuredHeight(); 84 | mMaxScrollY = mHeaderHeight; 85 | //调整整个 view 的高度 86 | int newHeightSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec) + mMaxScrollY, MeasureSpec.EXACTLY); 87 | super.onMeasure(widthMeasureSpec, newHeightSpec); 88 | } 89 | 90 | 91 | @Override 92 | public boolean dispatchTouchEvent(MotionEvent ev) { 93 | float currentX = ev.getX(); 94 | float currentY = ev.getY(); 95 | 96 | obtainVelocityTracker(ev); 97 | switch (ev.getAction()) { 98 | case MotionEvent.ACTION_DOWN: 99 | verticalScrollFlag = false; 100 | mDownX = currentX; 101 | mDownY = currentY; 102 | mLastX = currentX; 103 | mLastY = currentY; 104 | break; 105 | case MotionEvent.ACTION_MOVE: 106 | float deltaY = currentY - mLastY; //和上一次move相比的距离 107 | float shiftX = Math.abs(currentX - mDownX); //当前触摸位置与第一次 down 事件发生时的便宜 108 | float shiftY = Math.abs(currentY - mDownY); //当前触摸位置与第一次 down 事件发生时的便宜 109 | mLastY = currentY; 110 | //此处使用 111 | if (shiftY > shiftX && shiftY > mTouchSlop) { 112 | //垂直滚动 113 | verticalScrollFlag = true; 114 | } else if (shiftX > shiftY && shiftX > mTouchSlop){ 115 | //水平 116 | verticalScrollFlag = false; 117 | } 118 | if (verticalScrollFlag && (!isStickied() || mScrollable.isTop())) { 119 | scrollBy(0, (int) (-deltaY + 0.5)); 120 | if (mCurY > 0 && !isStickied()) { 121 | return true; 122 | } 123 | } 124 | break; 125 | case MotionEvent.ACTION_UP: 126 | if (verticalScrollFlag) { 127 | //指定速度单位为1000毫秒,表示每1000毫秒允许fling 的最大距离为mMaximumFlingVelocity 128 | mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); 129 | float yVelocity = mVelocityTracker.getYVelocity(); 130 | mCurDirection = yVelocity > 0 ? SCROLL_DOWN : SCROLL_UP; 131 | // 移动内容坐标系的方向与事件速度方向相反 132 | mScroller.fling(0, getScrollY(), 0, (int) -yVelocity, 0, 0, -Integer.MAX_VALUE, Integer.MAX_VALUE); 133 | mLastScrollerY = getScrollY(); 134 | invalidate();//重新绘制,会引起 computeScroll方法的执行 ---- 如果不刷则会卡顿一下,因为不走computeScroll方法的执行 135 | } 136 | recycleVelocityTracker(); 137 | break; 138 | case MotionEvent.ACTION_CANCEL: 139 | recycleVelocityTracker(); 140 | break; 141 | 142 | } 143 | super.dispatchTouchEvent(ev); 144 | return true; 145 | } 146 | 147 | @Override 148 | public void computeScroll() { 149 | if (mScroller.computeScrollOffset()) { 150 | //未执行完成返回 true 151 | final int currY = mScroller.getCurrY(); 152 | if (mCurDirection == SCROLL_UP) { 153 | //手势向上 154 | if (isStickied()) { 155 | //scroller.getFinalY()获取 fling 时最后的 y 156 | int dy = mScroller.getFinalY() - currY; //剩余距离 157 | int duration = mScroller.getDuration() - mScroller.timePassed();//剩余事件 158 | mScrollable.smoothScrollBy((int) mScroller.getCurrVelocity(), dy, duration); 159 | mScroller.abortAnimation(); 160 | return; 161 | } else { 162 | scrollTo(0, currY); //移动外层布局 163 | } 164 | } else { 165 | //手势向下 166 | if (mScrollable.isTop()) { 167 | //到顶部 168 | int dy = currY - mLastScrollerY; 169 | int toY = getScrollY() + dy; 170 | scrollTo(0, toY); 171 | if (mCurY <= mMinScrollY) { 172 | mScroller.abortAnimation(); 173 | return; 174 | } 175 | } 176 | //向下滑动时,初始状态可能不在顶部,所以要一直重绘,让computeScroll一直调用 177 | //确保代码能进入上面的if判断 --- key 178 | invalidate(); 179 | } 180 | mLastScrollerY = currY; 181 | } 182 | } 183 | 184 | @Override 185 | public void scrollTo(int x, int y) { 186 | if (y >= mMaxScrollY) { 187 | y = mMaxScrollY; 188 | } else if (y <= mMinScrollY) { 189 | y = mMinScrollY; 190 | } 191 | mCurY = y; 192 | super.scrollTo(x, y); 193 | } 194 | 195 | @Override 196 | public void scrollBy(int x, int y) { 197 | int scrollY = getScrollY(); 198 | int toY = scrollY + y; 199 | if (toY >= mMaxScrollY) { 200 | toY = mMaxScrollY; 201 | } else if (toY <= mMinScrollY) { 202 | toY = mMinScrollY; 203 | } 204 | y = toY - scrollY; 205 | super.scrollBy(x, y); 206 | } 207 | 208 | public void setCurrentScrollableView(View scrollableView) { 209 | mScrollable.setScrollView(scrollableView); 210 | } 211 | 212 | public View getCurrentScrollableView() { 213 | return mScrollable.getScrollView(); 214 | } 215 | 216 | 217 | private boolean isStickied() { 218 | return mCurY >= mMaxScrollY; 219 | } 220 | 221 | private VelocityTracker obtainVelocityTracker(MotionEvent ev) { 222 | if (mVelocityTracker == null) { 223 | mVelocityTracker = VelocityTracker.obtain(); 224 | } 225 | mVelocityTracker.addMovement(ev); 226 | return mVelocityTracker; 227 | } 228 | 229 | private void recycleVelocityTracker() { 230 | if (mVelocityTracker != null) { 231 | mVelocityTracker.recycle(); 232 | mVelocityTracker = null; 233 | } 234 | } 235 | 236 | } 237 | -------------------------------------------------------------------------------- /app/src/main/java/activity/newsreader/netease/com/sticklayout/view/NestedLinearLayout.java: -------------------------------------------------------------------------------- 1 | package activity.newsreader.netease.com.sticklayout.view; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.view.NestedScrollingChild; 6 | import android.support.v4.view.NestedScrollingChildHelper; 7 | import android.support.v4.view.ViewCompat; 8 | import android.support.v4.view.ViewConfigurationCompat; 9 | import android.util.AttributeSet; 10 | import android.view.MotionEvent; 11 | import android.view.ViewConfiguration; 12 | import android.widget.LinearLayout; 13 | 14 | /** 15 | * @title: 使用嵌套滚动实现 viewpage 的 sticky layout 16 | * @description: 17 | * @company: Netease 18 | * @author: GlanWang 19 | * @version: Created on 18/5/24. 20 | */ 21 | public class NestedLinearLayout extends LinearLayout implements NestedScrollingChild { 22 | private NestedScrollingChildHelper mNestedScrollHelper; 23 | private float mLastTouchX, mLastTouchY; 24 | private final int[] offset = new int[2]; //偏移量 25 | private final int[] consumed = new int[2]; //消费 26 | private int mTouchSlop; 27 | private boolean isBeingNestedScrolling;//是否正在嵌套滚动过程中 28 | 29 | 30 | public NestedLinearLayout(Context context) { 31 | this(context, null); 32 | } 33 | 34 | public NestedLinearLayout(Context context, @Nullable AttributeSet attrs) { 35 | this(context, attrs, 0); 36 | } 37 | 38 | public NestedLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 39 | super(context, attrs, defStyleAttr); 40 | mNestedScrollHelper = new NestedScrollingChildHelper(this); 41 | mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(ViewConfiguration.get(context)); 42 | setNestedScrollingEnabled(true); 43 | } 44 | 45 | @Override 46 | public boolean onInterceptTouchEvent(MotionEvent ev) { 47 | boolean isIntercept = false; 48 | if (isNestedScrollingEnabled()) { 49 | switch (ev.getAction()) { 50 | case MotionEvent.ACTION_DOWN: 51 | mLastTouchX = ev.getX(); 52 | mLastTouchY = ev.getY(); 53 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); 54 | break; 55 | case MotionEvent.ACTION_MOVE: 56 | float dx = ev.getX() - mLastTouchX; 57 | float dy = ev.getY() - mLastTouchY; 58 | if (Math.abs(dy) > Math.abs(dx) && Math.abs(dy) >= mTouchSlop) { 59 | isIntercept = true; 60 | } 61 | break; 62 | case MotionEvent.ACTION_CANCEL: 63 | case MotionEvent.ACTION_UP: 64 | stopNestedScroll(); 65 | break; 66 | } 67 | } 68 | return isIntercept; 69 | } 70 | 71 | @Override 72 | public boolean onTouchEvent(MotionEvent event) { 73 | if (isNestedScrollingEnabled()) { 74 | switch (event.getAction()) { 75 | case MotionEvent.ACTION_DOWN: { 76 | mLastTouchX = event.getX(); 77 | mLastTouchY = event.getY(); 78 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); 79 | break; 80 | } 81 | case MotionEvent.ACTION_CANCEL: 82 | case MotionEvent.ACTION_UP: { 83 | mLastTouchX = mLastTouchY = 0; 84 | stopNestedScroll(); 85 | break; 86 | } 87 | case MotionEvent.ACTION_MOVE: { 88 | final float x = event.getX(); 89 | final float y = event.getY(); 90 | float dx = mLastTouchX - x; 91 | float dy = mLastTouchY - y; 92 | if (isBeingNestedScrolling || Math.abs(dy) > Math.abs(dx) && Math.abs(dy) >= mTouchSlop) { 93 | dispatchNestedPreScroll(0, (int) dy, consumed, offset); 94 | } 95 | break; 96 | } 97 | } 98 | return true; 99 | } 100 | return super.onTouchEvent(event); 101 | } 102 | 103 | @Override 104 | public void setNestedScrollingEnabled(boolean enabled) { 105 | mNestedScrollHelper.setNestedScrollingEnabled(enabled); 106 | } 107 | 108 | @Override 109 | public boolean isNestedScrollingEnabled() { 110 | return mNestedScrollHelper.isNestedScrollingEnabled(); 111 | } 112 | 113 | @Override 114 | public boolean startNestedScroll(int axes) { 115 | return mNestedScrollHelper.startNestedScroll(axes); 116 | } 117 | 118 | @Override 119 | public void stopNestedScroll() { 120 | isBeingNestedScrolling = false; 121 | mNestedScrollHelper.stopNestedScroll(); 122 | } 123 | 124 | @Override 125 | public boolean hasNestedScrollingParent() { 126 | return mNestedScrollHelper.hasNestedScrollingParent(); 127 | } 128 | 129 | @Override 130 | public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) { 131 | return mNestedScrollHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); 132 | } 133 | 134 | @Override 135 | public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) { 136 | isBeingNestedScrolling = true; 137 | return mNestedScrollHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); 138 | } 139 | 140 | @Override 141 | public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { 142 | return mNestedScrollHelper.dispatchNestedFling(velocityX, velocityY, consumed); 143 | } 144 | 145 | @Override 146 | public boolean dispatchNestedPreFling(float velocityX, float velocityY) { 147 | return mNestedScrollHelper.dispatchNestedPreFling(velocityX, velocityY); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /app/src/main/java/activity/newsreader/netease/com/sticklayout/view/ScrollableViewImp.java: -------------------------------------------------------------------------------- 1 | package activity.newsreader.netease.com.sticklayout.view; 2 | 3 | import android.os.Build; 4 | import android.support.v7.widget.LinearLayoutManager; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.webkit.WebView; 9 | import android.widget.AbsListView; 10 | import android.widget.AdapterView; 11 | import android.widget.ScrollView; 12 | 13 | /** 14 | * @title: 15 | * @description: 16 | * @company: Netease 17 | * @author: GlanWang 18 | * @version: Created on 18/6/5. 19 | */ 20 | public class ScrollableViewImp implements IScrollable { 21 | 22 | private View mScrollView; 23 | @Override 24 | public void smoothScrollBy(int yVelocity, int distance, int duration) { 25 | View scrollableView = getScrollView(); 26 | if (scrollableView instanceof AbsListView) { 27 | AbsListView absListView = (AbsListView) scrollableView; 28 | if (Build.VERSION.SDK_INT >= 21) { 29 | absListView.fling(yVelocity); 30 | } else { 31 | absListView.smoothScrollBy(distance, duration); 32 | } 33 | } else if (scrollableView instanceof ScrollView) { 34 | ((ScrollView) scrollableView).fling(yVelocity); 35 | } else if (scrollableView instanceof RecyclerView) { 36 | ((RecyclerView) scrollableView).fling(0, yVelocity); 37 | } else if (scrollableView instanceof WebView) { 38 | ((WebView) scrollableView).flingScroll(0, yVelocity); 39 | } 40 | } 41 | 42 | @Override 43 | public boolean isTop() { 44 | View scrollableView = getScrollView(); 45 | 46 | if (scrollableView instanceof AdapterView) { 47 | return isAdapterViewTop((AdapterView) scrollableView); 48 | } 49 | if (scrollableView instanceof ScrollView) { 50 | return isScrollViewTop((ScrollView) scrollableView); 51 | } 52 | if (scrollableView instanceof RecyclerView) { 53 | return isRecyclerViewTop((RecyclerView) scrollableView); 54 | } 55 | if (scrollableView instanceof ViewGroup) { 56 | return isViewGroupTop((ViewGroup)scrollableView); 57 | } 58 | return false; 59 | } 60 | 61 | @Override 62 | public View getScrollView() { 63 | return mScrollView; 64 | } 65 | 66 | @Override 67 | public void setScrollView(View scrollView) { 68 | this.mScrollView = scrollView; 69 | } 70 | 71 | private boolean isAdapterViewTop(AdapterView adapterView) { 72 | if (adapterView != null) { 73 | int firstVisiblePosition = adapterView.getFirstVisiblePosition(); 74 | View childAt = adapterView.getChildAt(0); 75 | if (childAt == null || (firstVisiblePosition == 0 && childAt.getTop() == 0)) { 76 | return true; 77 | } 78 | } 79 | return false; 80 | } 81 | 82 | private boolean isScrollViewTop(ScrollView scrollView) { 83 | if (scrollView != null) { 84 | int scrollViewY = scrollView.getScrollY(); 85 | return scrollViewY <= 0; 86 | } 87 | return false; 88 | } 89 | 90 | 91 | private boolean isRecyclerViewTop(RecyclerView recyclerView) { 92 | if (recyclerView != null) { 93 | RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); 94 | if (layoutManager instanceof LinearLayoutManager) { 95 | int firstVisibleItemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); 96 | View childAt = recyclerView.getChildAt(0); 97 | if (childAt == null || (firstVisibleItemPosition == 0 && childAt.getTop() == 0)) { 98 | return true; 99 | } 100 | } 101 | } 102 | return false; 103 | } 104 | 105 | private boolean isViewGroupTop(ViewGroup scrollableView) { 106 | if (scrollableView != null) { 107 | int scrollViewY = scrollableView.getScrollY(); 108 | return scrollViewY <= 0; 109 | } 110 | return false; 111 | } 112 | 113 | 114 | } 115 | -------------------------------------------------------------------------------- /app/src/main/java/activity/newsreader/netease/com/sticklayout/view/ScrollerLayout.java: -------------------------------------------------------------------------------- 1 | package activity.newsreader.netease.com.sticklayout.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.MotionEvent; 6 | import android.view.View; 7 | import android.view.ViewConfiguration; 8 | import android.view.ViewGroup; 9 | import android.widget.Scroller; 10 | 11 | /** 12 | * @title: 13 | * @description: scroller 使用 14 | * 1. new 对象 15 | * 2. mScroller.startScroll()触发滚动 16 | * 3. 复写computeScroll,判断是否需要继续 17 | * @company: Netease 18 | * @author: GlanWang 19 | * @version: Created on 18/6/4. 20 | */ 21 | public class ScrollerLayout extends ViewGroup { 22 | 23 | private Scroller mScroller; 24 | private int mTouchSlop; 25 | private int leftBorder; 26 | private int rightBorder; 27 | private float mXDown, mXLastMove, mXMove; 28 | 29 | public ScrollerLayout(Context context) { 30 | this(context, null); 31 | } 32 | 33 | public ScrollerLayout(Context context, AttributeSet attrs) { 34 | this(context, attrs, 0); 35 | } 36 | 37 | public ScrollerLayout(Context context, AttributeSet attrs, int defStyleAttr) { 38 | super(context, attrs, defStyleAttr); 39 | // 第一步, new Scoller() 40 | mScroller = new Scroller(context); 41 | ViewConfiguration viewConfiguration = ViewConfiguration.get(context); 42 | mTouchSlop = viewConfiguration.getScaledTouchSlop(); 43 | } 44 | 45 | @Override 46 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 47 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 48 | int childCount = getChildCount(); 49 | for (int i = 0; i < childCount; i++) { 50 | View childView = getChildAt(i); 51 | measureChild(childView, widthMeasureSpec, heightMeasureSpec); 52 | } 53 | } 54 | 55 | @Override 56 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 57 | if (changed) { 58 | int childCount = getChildCount(); 59 | for (int i = 0; i < childCount; i++) { 60 | View childView = getChildAt(i); 61 | childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight()); 62 | } 63 | //初始化左右边界 64 | leftBorder = getChildAt(0).getLeft(); 65 | rightBorder = getChildAt(getChildCount() - 1).getRight(); 66 | } 67 | } 68 | 69 | @Override 70 | public boolean onInterceptTouchEvent(MotionEvent ev) { 71 | switch (ev.getAction()) { 72 | case MotionEvent.ACTION_DOWN: 73 | mXDown = ev.getRawX(); 74 | mXLastMove = mXDown; 75 | break; 76 | case MotionEvent.ACTION_MOVE: 77 | mXMove = ev.getRawX(); 78 | float diff = Math.abs(mXMove - mXDown); 79 | mXLastMove = mXMove; 80 | if (diff > mTouchSlop) { 81 | return true; 82 | } 83 | break; 84 | } 85 | return super.onInterceptTouchEvent(ev); 86 | } 87 | 88 | 89 | @Override 90 | public boolean onTouchEvent(MotionEvent event) { 91 | switch (event.getAction()) { 92 | case MotionEvent.ACTION_MOVE: 93 | mXMove = event.getRawX(); 94 | float scrooledX = mXLastMove - mXMove; 95 | if (getScrollX() + scrooledX < leftBorder) { 96 | scrollTo(leftBorder, 0); 97 | return true; 98 | } else if (getScrollX() + getWidth() + scrooledX > rightBorder) { 99 | scrollTo(rightBorder - getWidth(), 0); 100 | return true; 101 | } 102 | scrollBy((int) scrooledX, 0); 103 | mXLastMove = mXMove; 104 | break; 105 | case MotionEvent.ACTION_UP: 106 | int targetIndex = (getScrollX() + getWidth()/2)/getWidth(); 107 | int dx = targetIndex * getWidth() - getScrollX(); 108 | 109 | // 第二步, 调用 startScroll()方法初始化滚动数据并刷新界面 110 | // param1滚动开始时 X 的左边;param2滚动开始时 Y 的坐标; 111 | // param3 横向滚动距离,正值向左; param4纵向滚动距离,正值向上 112 | mScroller.startScroll(getScrollX(), 0, dx, 0); 113 | invalidate(); 114 | break; 115 | } 116 | 117 | return super.onTouchEvent(event); 118 | } 119 | 120 | @Override 121 | public void computeScroll() { 122 | // 第三步, 重新 computeScroll方法 123 | if (mScroller.computeScrollOffset()) { 124 | //true 表示滚动尚未完成; 125 | scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 126 | invalidate(); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /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_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 |