├── .gitignore ├── AndroidManifest.xml ├── LICENSE ├── README.md ├── ScreenShots └── TwoWayNestedScrollView.gif ├── ic_launcher-web.png ├── libs └── android-support-v4.jar ├── proguard-project.txt ├── project.properties ├── res ├── drawable-hdpi │ └── ic_launcher.png ├── drawable-mdpi │ └── ic_launcher.png ├── drawable-xhdpi │ └── ic_launcher.png ├── drawable-xxhdpi │ └── ic_launcher.png ├── layout │ └── activity_main.xml ├── values-w820dp │ └── dimens.xml └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── src └── com └── peerless2012 └── twowaynestedscrollview ├── MainActivity.java └── TwoWayNestedScrollView.java /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /gen/ 3 | /.classpath 4 | /.project 5 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /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 peerless2012 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 | #TwoWayNestedScrollView 2 | TwoWayNestedScrollView 是一个ScrollView参照NestedScrollView,实现普通ScrollView垂直滚动和HorizontalScrollView水平滚动的ScrollView,由于是基于support包的NestedScrollView,所以也支持Android5.0上实现的nested功能,和parent实现混合滚动效果 3 | 4 | ![效果图] (https://raw.githubusercontent.com/peerless2012/TwoWayNestedScrollView/master/ScreenShots/TwoWayNestedScrollView.gif) 5 | 6 | Developed By 7 | ------------ 8 | 9 | * peerless2012 - 10 | 11 | 12 | License 13 | -------- 14 | 15 | Copyright 2015 peerless2012. 16 | 17 | Licensed under the Apache License, Version 2.0 (the "License"); 18 | you may not use this file except in compliance with the License. 19 | You may obtain a copy of the License at 20 | 21 | http://www.apache.org/licenses/LICENSE-2.0 22 | 23 | Unless required by applicable law or agreed to in writing, software 24 | distributed under the License is distributed on an "AS IS" BASIS, 25 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 | See the License for the specific language governing permissions and 27 | limitations under the License. 28 | 29 | -------------------------------------------------------------------------------- /ScreenShots/TwoWayNestedScrollView.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerless2012/TwoWayNestedScrollView/3347e93fbd570c9c6e3b00cb0ade2005b9b5c79d/ScreenShots/TwoWayNestedScrollView.gif -------------------------------------------------------------------------------- /ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerless2012/TwoWayNestedScrollView/3347e93fbd570c9c6e3b00cb0ade2005b9b5c79d/ic_launcher-web.png -------------------------------------------------------------------------------- /libs/android-support-v4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerless2012/TwoWayNestedScrollView/3347e93fbd570c9c6e3b00cb0ade2005b9b5c79d/libs/android-support-v4.jar -------------------------------------------------------------------------------- /proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-19 15 | -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerless2012/TwoWayNestedScrollView/3347e93fbd570c9c6e3b00cb0ade2005b9b5c79d/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerless2012/TwoWayNestedScrollView/3347e93fbd570c9c6e3b00cb0ade2005b9b5c79d/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerless2012/TwoWayNestedScrollView/3347e93fbd570c9c6e3b00cb0ade2005b9b5c79d/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerless2012/TwoWayNestedScrollView/3347e93fbd570c9c6e3b00cb0ade2005b9b5c79d/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 64dp 9 | 10 | 11 | -------------------------------------------------------------------------------- /res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16dp 5 | 16dp 6 | 7 | 8 | -------------------------------------------------------------------------------- /res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TwoWayNestedScrollView 5 | Hello world! 6 | 乍眼一看可能不明白这个效果是怎么完成的。我们先仔细看看上面的边就会发现,白色的边的宽度不断从右边往左边延伸,而一条稍微延时的边紧跟着一起移动。每条边都有这样的做法。看起来就像上面的边经过拐角移动到了左边,并以此类推。 7 | \n不用SVG也能完成这样的效果,甚至只用伪元素。但是我们想探索一下怎样用CSS而不是JavaScript来控制SVG。\n 8 | 现在,来思考一下要怎么创建出这样的效果。我们可以改变矩形的troke-dashoffset或者直接画线。我们尝试不用JavaScript的解决方案。我们发现,CSS过渡stroke-dashoffset 和 stroke-dasharray的值会触发很多BUG。所以我们要尝试不同的解决方案,利用线条和它们的动画,这在CSS里很容易理解和实现。这也给我们更多机会去探索不同的动画效果。\n 9 | 我们将要使用的线的特别之处是,它们在这个动画里将有三种状态。它们是方盒边长的三倍,其中中间一截是与边等长的间隙。我们将通过设置stroke-dashoffset的值来实现与方盒的边等长。现在,这个动画实现的关键在于线的位置转换:自从Twitter推出 Bootstrap 以来,它的推广程度就像火箭发射一样节节攀升。这个广受欢迎的CSS框架为众多网站提供了响应式网格系统,预定义样式的组件与 JavaScript插件 10 | 。 Bootstrap 的设计初衷之一就是实用。当你要新建网站时, Bootstrap 绝对是一个节约时转载自 http://dwz.cn/JcXMM 你了解 CSS 吗?在六个月前,我提供了一个在线 免费 CSS 测试 系统。测试结果表明很多一线开发者并没有如他们所想的那样了解 CSS。目前有超过 3,000 人参加了该项测试,平均成绩只有 55 分。 但是,嘿,平均分本身并没有什么意思乍眼一看可能不明白这个效果是怎么完成的。我们先仔细看看上面的边就会发现,白色的边的宽度不断从右边往左边延伸,而一条稍微延时的边紧跟着一起移动。每条边都有这样的做法。看起来就像上面的边经过拐角移动到了左边,并以此类推。 11 | \n不用SVG也能完成这样的效果,甚至只用伪元素。但是我们想探索一下怎样用CSS而不是JavaScript来控制SVG。\n 12 | 现在,来思考一下要怎么创建出这样的效果。我们可以改变矩形的troke-dashoffset或者直接画线。我们尝试不用JavaScript的解决方案。我们发现,CSS过渡stroke-dashoffset 和 stroke-dasharray的值会触发很多BUG。所以我们要尝试不同的解决方案,利用线条和它们的动画,这在CSS里很容易理解和实现。这也给我们更多机会去探索不同的动画效果。\n 13 | 我们将要使用的线的特别之处是,它们在这个动画里将有三种状态。它们是方盒边长的三倍,其中中间一截是与边等长的间隙。我们将通过设置stroke-dashoffset的值来实现与方盒的边等长。现在,这个动画实现的关键在于线的位置转换:自从Twitter推出 Bootstrap 以来,它的推广程度就像火箭发射一样节节攀升。这个广受欢迎的CSS框架为众多网站提供了响应式网格系统,预定义样式的组件与 JavaScript插件 14 | 。 Bootstrap 的设计初衷之一就是实用。当你要新建网站时, Bootstrap 绝对是一个节约时转载自 http://dwz.cn/JcXMM 你了解 CSS 吗?在六个月前,我提供了一乍眼一看可能不明白这个效果是怎么完成的。我们先仔细看看上面的边就会发现,白色的边的宽度不断从右边往左边延伸,而一条稍微延时的边紧跟着一起移动。每条边都有这样的做法。看起来就像上面的边经过拐角移动到了左边,并以此类推。 15 | \n不用SVG也能完成这样的效果,甚至只用伪元素。但是我们想探索一下怎样用CSS而不是JavaScript来控制SVG。\n 16 | 现在,来思考一下要怎么创建出这样的效果。我们可以改变矩形的troke-dashoffset或者直接画线。我们尝试不用JavaScript的解决方案。我们发现,CSS过渡stroke-dashoffset 和 stroke-dasharray的值会触发很多BUG。所以我们要尝试不同的解决方案,利用线条和它们的动画,这在CSS里很容易理解和实现。这也给我们更多机会去探索不同的动画效果。\n 17 | 我们将要使用的线的特别之处是,它们在这个动画里将有三种状态。它们是方盒边长的三倍,其中中间一截是与边等长的间隙。我们将通过设置stroke-dashoffset的值来实现与方盒的边等长。现在,这个动画实现的关键在于线的位置转换:自从Twitter推出 Bootstrap 以来,它的推广程度就像火箭发射一样节节攀升。这个广受欢迎的CSS框架为众多网站提供了响应式网格系统,预定义样式的组件与 JavaScript插件 18 | 。 Bootstrap 的设计初衷之一就是实用。当你要新建网站时, Bootstrap 绝对是一个节约时转载自 http://dwz.cn/JcXMM 你了解 CSS 吗?在六个月前,我提供了一个在线 免费 CSS 测试 系统。测试结果表明很多一线开发者并没有如他们所想的那样了解 CSS。目前有超过 3,000 人参加了该项测试,平均成绩只有 55 分。 但是,嘿,平均分本身并没有什么意思乍眼一看可能不明白这个效果是怎么完成的。我们先仔细看看上面的边就会发现,白色的边的宽度不断从右边往左边延伸,而一条稍微延时的边紧跟着一起移动。每条边都有这样的做法。看起来就像上面的边经过拐角移动到了左边,并以此类推。 19 | \n不用SVG也能完成这样的效果,甚至只用伪元素。但是我们想探索一下怎样用CSS而不是JavaScript来控制SVG。\n 20 | 现在,来思考一下要怎么创建出这样的效果。我们可以改变矩形的troke-dashoffset或者直接画线。我们尝试不用JavaScript的解决方案。我们发现,CSS过渡stroke-dashoffset 和 stroke-dasharray的值会触发很多BUG。所以我们要尝试不同的解决方案,利用线条和它们的动画,这在CSS里很容易理解和实现。这也给我们更多机会去探索不同的动画效果。\n 21 | 我们将要使用的线的特别之处是,它们在这个动画里将有三种状态。它们是方盒边长的三倍,其中中间一截是与边等长的间隙。我们将通过设置stroke-dashoffset的值来实现与方盒的边等长。现在,这个动画实现的关键在于线的位置转换:自从Twitter推出 Bootstrap 以来,它的推广程度就像火箭发射一样节节攀升。这个广受欢迎的CSS框架为众多网站提供了响应式网格系统,预定义样式的组件与 JavaScript插件 22 | 。 Bootstrap 的设计初衷之一就是实用。当你要新建网站时, Bootstrap 绝对是一个节约时转载自 http://dwz.cn/JcXMM 你了解 CSS 吗?在六个月前,我提供了一乍眼一看可能不明白这个效果是怎么完成的。我们先仔细看看上面的边就会发现,白色的边的宽度不断从右边往左边延伸,而一条稍微延时的边紧跟着一起移动。每条边都有这样的做法。看起来就像上面的边经过拐角移动到了左边,并以此类推。 23 | \n不用SVG也能完成这样的效果,甚至只用伪元素。但是我们想探索一下怎样用CSS而不是JavaScript来控制SVG。\n 24 | 现在,来思考一下要怎么创建出这样的效果。我们可以改变矩形的troke-dashoffset或者直接画线。我们尝试不用JavaScript的解决方案。我们发现,CSS过渡stroke-dashoffset 和 stroke-dasharray的值会触发很多BUG。所以我们要尝试不同的解决方案,利用线条和它们的动画,这在CSS里很容易理解和实现。这也给我们更多机会去探索不同的动画效果。\n 25 | 我们将要使用的线的特别之处是,它们在这个动画里将有三种状态。它们是方盒边长的三倍,其中中间一截是与边等长的间隙。我们将通过设置stroke-dashoffset的值来实现与方盒的边等长。现在,这个动画实现的关键在于线的位置转换:自从Twitter推出 Bootstrap 以来,它的推广程度就像火箭发射一样节节攀升。这个广受欢迎的CSS框架为众多网站提供了响应式网格系统,预定义样式的组件与 JavaScript插件 26 | 。 Bootstrap 的设计初衷之一就是实用。当你要新建网站时, Bootstrap 绝对是一个节约时转载自 http://dwz.cn/JcXMM 你了解 CSS 吗?在六个月前,我提供了一个在线 免费 CSS 测试 系统。测试结果表明很多一线开发者并没有如他们所想的那样了解 CSS。目前有超过 3,000 人参加了该项测试,平均成绩只有 55 分。 但是,嘿,平均分本身并没有什么意思乍眼一看可能不明白这个效果是怎么完成的。我们先仔细看看上面的边就会发现,白色的边的宽度不断从右边往左边延伸,而一条稍微延时的边紧跟着一起移动。每条边都有这样的做法。看起来就像上面的边经过拐角移动到了左边,并以此类推。 27 | \n不用SVG也能完成这样的效果,甚至只用伪元素。但是我们想探索一下怎样用CSS而不是JavaScript来控制SVG。\n 28 | 现在,来思考一下要怎么创建出这样的效果。我们可以改变矩形的troke-dashoffset或者直接画线。我们尝试不用JavaScript的解决方案。我们发现,CSS过渡stroke-dashoffset 和 stroke-dasharray的值会触发很多BUG。所以我们要尝试不同的解决方案,利用线条和它们的动画,这在CSS里很容易理解和实现。这也给我们更多机会去探索不同的动画效果。\n 29 | 我们将要使用的线的特别之处是,它们在这个动画里将有三种状态。它们是方盒边长的三倍,其中中间一截是与边等长的间隙。我们将通过设置stroke-dashoffset的值来实现与方盒的边等长。现在,这个动画实现的关键在于线的位置转换:自从Twitter推出 Bootstrap 以来,它的推广程度就像火箭发射一样节节攀升。这个广受欢迎的CSS框架为众多网站提供了响应式网格系统,预定义样式的组件与 JavaScript插件 30 | 。 Bootstrap 的设计初衷之一就是实用。当你要新建网站时, Bootstrap 绝对是一个节约时转载自 http://dwz.cn/JcXMM 你了解 CSS 吗?在六个月前,我提供了一乍眼一看可能不明白这个效果是怎么完成的。我们先仔细看看上面的边就会发现,白色的边的宽度不断从右边往左边延伸,而一条稍微延时的边紧跟着一起移动。每条边都有这样的做法。看起来就像上面的边经过拐角移动到了左边,并以此类推。 31 | \n不用SVG也能完成这样的效果,甚至只用伪元素。但是我们想探索一下怎样用CSS而不是JavaScript来控制SVG。\n 32 | 现在,来思考一下要怎么创建出这样的效果。我们可以改变矩形的troke-dashoffset或者直接画线。我们尝试不用JavaScript的解决方案。我们发现,CSS过渡stroke-dashoffset 和 stroke-dasharray的值会触发很多BUG。所以我们要尝试不同的解决方案,利用线条和它们的动画,这在CSS里很容易理解和实现。这也给我们更多机会去探索不同的动画效果。\n 33 | 我们将要使用的线的特别之处是,它们在这个动画里将有三种状态。它们是方盒边长的三倍,其中中间一截是与边等长的间隙。我们将通过设置stroke-dashoffset的值来实现与方盒的边等长。现在,这个动画实现的关键在于线的位置转换:自从Twitter推出 Bootstrap 以来,它的推广程度就像火箭发射一样节节攀升。这个广受欢迎的CSS框架为众多网站提供了响应式网格系统,预定义样式的组件与 JavaScript插件 34 | 。 Bootstrap 的设计初衷之一就是实用。当你要新建网站时, Bootstrap 绝对是一个节约时转载自 http://dwz.cn/JcXMM 你了解 CSS 吗?在六个月前,我提供了一个在线 免费 CSS 测试 系统。测试结果表明很多一线开发者并没有如他们所想的那样了解 CSS。目前有超过 3,000 人参加了该项测试,平均成绩只有 55 分。 但是,嘿,平均分本身并没有什么意思乍眼一看可能不明白这个效果是怎么完成的。我们先仔细看看上面的边就会发现,白色的边的宽度不断从右边往左边延伸,而一条稍微延时的边紧跟着一起移动。每条边都有这样的做法。看起来就像上面的边经过拐角移动到了左边,并以此类推。 35 | \n不用SVG也能完成这样的效果,甚至只用伪元素。但是我们想探索一下怎样用CSS而不是JavaScript来控制SVG。\n 36 | 现在,来思考一下要怎么创建出这样的效果。我们可以改变矩形的troke-dashoffset或者直接画线。我们尝试不用JavaScript的解决方案。我们发现,CSS过渡stroke-dashoffset 和 stroke-dasharray的值会触发很多BUG。所以我们要尝试不同的解决方案,利用线条和它们的动画,这在CSS里很容易理解和实现。这也给我们更多机会去探索不同的动画效果。\n 37 | 我们将要使用的线的特别之处是,它们在这个动画里将有三种状态。它们是方盒边长的三倍,其中中间一截是与边等长的间隙。我们将通过设置stroke-dashoffset的值来实现与方盒的边等长。现在,这个动画实现的关键在于线的位置转换:自从Twitter推出 Bootstrap 以来,它的推广程度就像火箭发射一样节节攀升。这个广受欢迎的CSS框架为众多网站提供了响应式网格系统,预定义样式的组件与 JavaScript插件 38 | 。 Bootstrap 的设计初衷之一就是实用。当你要新建网站时, Bootstrap 绝对是一个节约时转载自 http://dwz.cn/JcXMM 你了解 CSS 吗?在六个月前,我提供了一 39 | 40 | -------------------------------------------------------------------------------- /res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 14 | 15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/com/peerless2012/twowaynestedscrollview/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.peerless2012.twowaynestedscrollview; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | 6 | public class MainActivity extends Activity{ 7 | @Override 8 | protected void onCreate(Bundle savedInstanceState) { 9 | super.onCreate(savedInstanceState); 10 | setContentView(R.layout.activity_main); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/com/peerless2012/twowaynestedscrollview/TwoWayNestedScrollView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.peerless2012.twowaynestedscrollview; 17 | 18 | import java.util.List; 19 | 20 | import android.annotation.SuppressLint; 21 | import android.content.Context; 22 | import android.content.res.TypedArray; 23 | import android.graphics.Canvas; 24 | import android.graphics.Color; 25 | import android.graphics.Rect; 26 | import android.os.Bundle; 27 | import android.os.Parcel; 28 | import android.os.Parcelable; 29 | import android.support.v4.view.AccessibilityDelegateCompat; 30 | import android.support.v4.view.InputDeviceCompat; 31 | import android.support.v4.view.MotionEventCompat; 32 | import android.support.v4.view.NestedScrollingChild; 33 | import android.support.v4.view.NestedScrollingChildHelper; 34 | import android.support.v4.view.NestedScrollingParent; 35 | import android.support.v4.view.NestedScrollingParentHelper; 36 | import android.support.v4.view.VelocityTrackerCompat; 37 | import android.support.v4.view.ViewCompat; 38 | import android.support.v4.view.accessibility.AccessibilityEventCompat; 39 | import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 40 | import android.support.v4.view.accessibility.AccessibilityRecordCompat; 41 | import android.support.v4.widget.EdgeEffectCompat; 42 | import android.support.v4.widget.ScrollerCompat; 43 | import android.util.AttributeSet; 44 | import android.util.Log; 45 | import android.util.TypedValue; 46 | import android.view.FocusFinder; 47 | import android.view.KeyEvent; 48 | import android.view.MotionEvent; 49 | import android.view.VelocityTracker; 50 | import android.view.View; 51 | import android.view.ViewConfiguration; 52 | import android.view.ViewGroup; 53 | import android.view.ViewParent; 54 | import android.view.accessibility.AccessibilityEvent; 55 | import android.view.animation.AnimationUtils; 56 | import android.widget.FrameLayout; 57 | import android.widget.ScrollView; 58 | 59 | /** 60 | * @author wangzhiming Describe: 2015年8月24日 下午5:36:19 61 | */ 62 | @SuppressLint("Override") 63 | public class TwoWayNestedScrollView extends FrameLayout implements 64 | NestedScrollingParent, NestedScrollingChild { 65 | static final int ANIMATED_SCROLL_GAP = 250; 66 | 67 | static final float MAX_SCROLL_FACTOR = 0.5f; 68 | 69 | private static final String TAG = "NestedScrollView"; 70 | 71 | private long mLastScroll; 72 | 73 | private final Rect mTempRect = new Rect(); 74 | private ScrollerCompat mScroller; 75 | private EdgeEffectCompat mEdgeGlowLeft; 76 | private EdgeEffectCompat mEdgeGlowRight; 77 | private EdgeEffectCompat mEdgeGlowTop; 78 | private EdgeEffectCompat mEdgeGlowBottom; 79 | 80 | /** 81 | * Position of the last motion event. 82 | */ 83 | private int mLastMotionY; 84 | private int mLastMotionX; 85 | 86 | /** 87 | * True when the layout has changed but the traversal has not come through 88 | * yet. Ideally the view hierarchy would keep track of this for us. 89 | */ 90 | private boolean mIsLayoutDirty = true; 91 | private boolean mIsLaidOut = false; 92 | 93 | /** 94 | * The child to give focus to in the event that a child has requested focus 95 | * while the layout is dirty. This prevents the scroll from being wrong if 96 | * the child has not been laid out before requesting focus. 97 | */ 98 | private View mChildToScrollTo = null; 99 | 100 | /** 101 | * True if the user is currently dragging this ScrollView around. This is 102 | * not the same as 'is being flinged', which can be checked by 103 | * mScroller.isFinished() (flinging begins when the user lifts his finger). 104 | */ 105 | private boolean mIsBeingDragged = false; 106 | 107 | private static final int DIRECTION_INVALIDATE = 0; 108 | private static final int DIRECTION_VERTICAL = 1; 109 | private static final int DIRECTION_HORIZONTAL = 2; 110 | private int scrollDirection = DIRECTION_INVALIDATE; 111 | 112 | /** 113 | * Determines speed during touch scrolling 114 | */ 115 | private VelocityTracker mVelocityTracker; 116 | 117 | /** 118 | * When set to true, the scroll view measure its child to make it fill the 119 | * currently visible area. 120 | */ 121 | private boolean mFillViewport = true; 122 | 123 | /** 124 | * Whether arrow scrolling is animated. 125 | */ 126 | private boolean mSmoothScrollingEnabled = true; 127 | 128 | private int mTouchSlop; 129 | private int mMinimumVelocity; 130 | private int mMaximumVelocity; 131 | 132 | /** 133 | * ID of the active pointer. This is used to retain consistency during 134 | * drags/flings if multiple pointers are used. 135 | */ 136 | private int mActivePointerId = INVALID_POINTER; 137 | 138 | /** 139 | * Used during scrolling to retrieve the new offset within the window. 140 | */ 141 | private final int[] mScrollOffset = new int[2]; 142 | private final int[] mScrollConsumed = new int[2]; 143 | private int mNestedYOffset; 144 | private int mNestedXOffset; 145 | 146 | /** 147 | * Sentinel value for no current active pointer. Used by 148 | * {@link #mActivePointerId}. 149 | */ 150 | private static final int INVALID_POINTER = -1; 151 | 152 | private SavedState mSavedState; 153 | 154 | private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); 155 | 156 | private static final int[] SCROLLVIEW_STYLEABLE = new int[] { android.R.attr.fillViewport }; 157 | 158 | private final NestedScrollingParentHelper mParentHelper; 159 | private final NestedScrollingChildHelper mChildHelper; 160 | 161 | private float mVerticalScrollFactor; 162 | 163 | public TwoWayNestedScrollView(Context context) { 164 | this(context, null); 165 | } 166 | 167 | public TwoWayNestedScrollView(Context context, AttributeSet attrs) { 168 | this(context, attrs, 0); 169 | } 170 | 171 | public TwoWayNestedScrollView(Context context, AttributeSet attrs, 172 | int defStyleAttr) { 173 | super(context, attrs, defStyleAttr); 174 | 175 | initScrollView(); 176 | 177 | final TypedArray a = context.obtainStyledAttributes(attrs, 178 | SCROLLVIEW_STYLEABLE, defStyleAttr, 0); 179 | 180 | setFillViewport(a.getBoolean(0, false)); 181 | 182 | a.recycle(); 183 | 184 | mParentHelper = new NestedScrollingParentHelper(this); 185 | mChildHelper = new NestedScrollingChildHelper(this); 186 | 187 | // ...because why else would you be using this widget? 188 | setNestedScrollingEnabled(true); 189 | 190 | ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); 191 | } 192 | 193 | // NestedScrollingChild 194 | 195 | @Override 196 | public void setNestedScrollingEnabled(boolean enabled) { 197 | mChildHelper.setNestedScrollingEnabled(enabled); 198 | } 199 | 200 | @Override 201 | public boolean isNestedScrollingEnabled() { 202 | return mChildHelper.isNestedScrollingEnabled(); 203 | } 204 | 205 | @Override 206 | public boolean startNestedScroll(int axes) { 207 | return mChildHelper.startNestedScroll(axes); 208 | } 209 | 210 | @Override 211 | public void stopNestedScroll() { 212 | mChildHelper.stopNestedScroll(); 213 | } 214 | 215 | @Override 216 | public boolean hasNestedScrollingParent() { 217 | return mChildHelper.hasNestedScrollingParent(); 218 | } 219 | 220 | @Override 221 | public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, 222 | int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { 223 | return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, 224 | dxUnconsumed, dyUnconsumed, offsetInWindow); 225 | } 226 | 227 | @Override 228 | public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, 229 | int[] offsetInWindow) { 230 | return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, 231 | offsetInWindow); 232 | } 233 | 234 | @Override 235 | public boolean dispatchNestedFling(float velocityX, float velocityY, 236 | boolean consumed) { 237 | return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); 238 | } 239 | 240 | @Override 241 | public boolean dispatchNestedPreFling(float velocityX, float velocityY) { 242 | return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); 243 | } 244 | 245 | // NestedScrollingParent 246 | 247 | @Override 248 | public boolean onStartNestedScroll(View child, View target, 249 | int nestedScrollAxes) { 250 | return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; 251 | } 252 | 253 | @Override 254 | public void onNestedScrollAccepted(View child, View target, 255 | int nestedScrollAxes) { 256 | mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); 257 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); 258 | } 259 | 260 | @Override 261 | public void onStopNestedScroll(View target) { 262 | stopNestedScroll(); 263 | } 264 | 265 | @Override 266 | public void onNestedScroll(View target, int dxConsumed, int dyConsumed, 267 | int dxUnconsumed, int dyUnconsumed) { 268 | final int oldScrollY = getScrollY(); 269 | scrollBy(0, dyUnconsumed); 270 | final int myConsumed = getScrollY() - oldScrollY; 271 | final int myUnconsumed = dyUnconsumed - myConsumed; 272 | dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); 273 | } 274 | 275 | @Override 276 | public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { 277 | // Do nothing 278 | } 279 | 280 | @Override 281 | public boolean onNestedFling(View target, float velocityX, float velocityY, 282 | boolean consumed) { 283 | if (!consumed) { 284 | if (velocityY > velocityX) { 285 | flingWithNestedDispatchVertical((int) velocityY); 286 | }else { 287 | flingWithNestedDispatchHorizontal((int) velocityX); 288 | } 289 | return true; 290 | } 291 | return false; 292 | } 293 | 294 | @Override 295 | public boolean onNestedPreFling(View target, float velocityX, 296 | float velocityY) { 297 | // Do nothing 298 | return false; 299 | } 300 | 301 | @Override 302 | public int getNestedScrollAxes() { 303 | return mParentHelper.getNestedScrollAxes(); 304 | } 305 | 306 | // ScrollView import 307 | 308 | public boolean shouldDelayChildPressedState() { 309 | return true; 310 | } 311 | 312 | @Override 313 | protected float getTopFadingEdgeStrength() { 314 | if (getChildCount() == 0) { 315 | return 0.0f; 316 | } 317 | 318 | final int length = getVerticalFadingEdgeLength(); 319 | final int scrollY = getScrollY(); 320 | if (scrollY < length) { 321 | return scrollY / (float) length; 322 | } 323 | 324 | return 1.0f; 325 | } 326 | 327 | @Override 328 | protected float getBottomFadingEdgeStrength() { 329 | if (getChildCount() == 0) { 330 | return 0.0f; 331 | } 332 | 333 | final int length = getVerticalFadingEdgeLength(); 334 | final int bottomEdge = getHeight() - getPaddingBottom(); 335 | final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; 336 | if (span < length) { 337 | return span / (float) length; 338 | } 339 | 340 | return 1.0f; 341 | } 342 | @Override 343 | protected float getLeftFadingEdgeStrength() { 344 | if (getChildCount() == 0) { 345 | return 0.0f; 346 | } 347 | 348 | final int length = getHorizontalFadingEdgeLength(); 349 | final int scrollX = getScrollX(); 350 | if (scrollX < length) { 351 | return scrollX / (float) length; 352 | } 353 | 354 | return 1.0f; 355 | } 356 | 357 | @Override 358 | protected float getRightFadingEdgeStrength() { 359 | if (getChildCount() == 0) { 360 | return 0.0f; 361 | } 362 | 363 | final int length = getHorizontalFadingEdgeLength(); 364 | final int rightEdge = getWidth() - getPaddingRight(); 365 | final int span = getChildAt(0).getRight() - getScrollX() - rightEdge; 366 | if (span < length) { 367 | return span / (float) length; 368 | } 369 | 370 | return 1.0f; 371 | } 372 | 373 | /** 374 | * @return The maximum amount this scroll view will scroll in response to an 375 | * arrow event. 376 | */ 377 | public int getMaxScrollYAmount() { 378 | return (int) (MAX_SCROLL_FACTOR * getHeight()); 379 | } 380 | 381 | public int getMaxScrollXAmount() { 382 | return (int) (MAX_SCROLL_FACTOR * getWidth()); 383 | } 384 | 385 | private void initScrollView() { 386 | mScroller = ScrollerCompat.create(getContext(), null); 387 | setFocusable(true); 388 | setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 389 | setWillNotDraw(false); 390 | final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 391 | mTouchSlop = configuration.getScaledTouchSlop(); 392 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 393 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 394 | } 395 | 396 | @Override 397 | public void addView(View child) { 398 | if (getChildCount() > 0) { 399 | throw new IllegalStateException( 400 | "ScrollView can host only one direct child"); 401 | } 402 | 403 | super.addView(child); 404 | } 405 | 406 | @Override 407 | public void addView(View child, int index) { 408 | if (getChildCount() > 0) { 409 | throw new IllegalStateException( 410 | "ScrollView can host only one direct child"); 411 | } 412 | 413 | super.addView(child, index); 414 | } 415 | 416 | @Override 417 | public void addView(View child, ViewGroup.LayoutParams params) { 418 | if (getChildCount() > 0) { 419 | throw new IllegalStateException( 420 | "ScrollView can host only one direct child"); 421 | } 422 | 423 | super.addView(child, params); 424 | } 425 | 426 | @Override 427 | public void addView(View child, int index, ViewGroup.LayoutParams params) { 428 | if (getChildCount() > 0) { 429 | throw new IllegalStateException( 430 | "ScrollView can host only one direct child"); 431 | } 432 | 433 | super.addView(child, index, params); 434 | } 435 | 436 | /** 437 | * @return Returns true this ScrollView can be scrolled in vertical direction 438 | */ 439 | private boolean canVerticalScroll() { 440 | View child = getChildAt(0); 441 | if (child != null) { 442 | int childHeight = child.getHeight(); 443 | return getHeight() < childHeight + getPaddingTop() 444 | + getPaddingBottom(); 445 | } 446 | return false; 447 | } 448 | 449 | /** 450 | * @return Returns true this ScrollView can be scrolled in horizontal direction 451 | */ 452 | private boolean canHorizontalScroll() { 453 | View child = getChildAt(0); 454 | if (child != null) { 455 | int childHeight = child.getWidth(); 456 | return getHeight() < childHeight + getPaddingTop() 457 | + getPaddingBottom(); 458 | } 459 | return false; 460 | } 461 | 462 | /** 463 | * Indicates whether this ScrollView's content is stretched to fill the 464 | * viewport. 465 | * 466 | * @return True if the content fills the viewport, false otherwise. 467 | * 468 | * @attr ref android.R.styleable#ScrollView_fillViewport 469 | */ 470 | public boolean isFillViewport() { 471 | return mFillViewport; 472 | } 473 | 474 | /** 475 | * Indicates this ScrollView whether it should stretch its content height to 476 | * fill the viewport or not. 477 | * 478 | * @param fillViewport 479 | * True to stretch the content's height to the viewport's 480 | * boundaries, false otherwise. 481 | * 482 | * @attr ref android.R.styleable#ScrollView_fillViewport 483 | */ 484 | public void setFillViewport(boolean fillViewport) { 485 | if (fillViewport != mFillViewport) { 486 | mFillViewport = fillViewport; 487 | requestLayout(); 488 | } 489 | } 490 | 491 | /** 492 | * @return Whether arrow scrolling will animate its transition. 493 | */ 494 | public boolean isSmoothScrollingEnabled() { 495 | return mSmoothScrollingEnabled; 496 | } 497 | 498 | /** 499 | * Set whether arrow scrolling will animate its transition. 500 | * 501 | * @param smoothScrollingEnabled 502 | * whether arrow scrolling will animate its transition 503 | */ 504 | public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { 505 | mSmoothScrollingEnabled = smoothScrollingEnabled; 506 | } 507 | 508 | @Override 509 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 510 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 511 | 512 | if (!mFillViewport) { 513 | return; 514 | } 515 | 516 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 517 | if (heightMode == MeasureSpec.UNSPECIFIED) { 518 | return; 519 | } 520 | 521 | if (getChildCount() > 0) { 522 | final View child = getChildAt(0); 523 | int height = getMeasuredHeight(); 524 | if (child.getMeasuredHeight() < height) { 525 | final FrameLayout.LayoutParams lp = (LayoutParams) child 526 | .getLayoutParams(); 527 | 528 | int childWidthMeasureSpec = getChildMeasureSpec( 529 | widthMeasureSpec, getPaddingLeft() + getPaddingRight(), 530 | lp.width); 531 | height -= getPaddingTop(); 532 | height -= getPaddingBottom(); 533 | int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( 534 | height, MeasureSpec.EXACTLY); 535 | 536 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 537 | } 538 | } 539 | } 540 | 541 | @Override 542 | public boolean dispatchKeyEvent(KeyEvent event) { 543 | // Let the focused view and/or our descendants get the key first 544 | return super.dispatchKeyEvent(event) || executeKeyEvent(event); 545 | } 546 | 547 | /** 548 | * You can call this function yourself to have the scroll view perform 549 | * scrolling from a key event, just as if the event had been dispatched to 550 | * it by the view hierarchy. 551 | * 552 | * @param event 553 | * The key event to execute. 554 | * @return Return true if the event was handled, else false. 555 | */ 556 | public boolean executeKeyEvent(KeyEvent event) { 557 | mTempRect.setEmpty(); 558 | 559 | if (!canVerticalScroll()) { 560 | if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { 561 | View currentFocused = findFocus(); 562 | if (currentFocused == this) 563 | currentFocused = null; 564 | View nextFocused = FocusFinder.getInstance().findNextFocus( 565 | this, currentFocused, View.FOCUS_DOWN); 566 | return nextFocused != null && nextFocused != this 567 | && nextFocused.requestFocus(View.FOCUS_DOWN); 568 | } 569 | return false; 570 | } 571 | if (!canHorizontalScroll()) { 572 | if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { 573 | View currentFocused = findFocus(); 574 | if (currentFocused == this) 575 | currentFocused = null; 576 | View nextFocused = FocusFinder.getInstance().findNextFocus( 577 | this, currentFocused, View.FOCUS_DOWN); 578 | return nextFocused != null && nextFocused != this 579 | && nextFocused.requestFocus(View.FOCUS_DOWN); 580 | } 581 | return false; 582 | } 583 | 584 | boolean handled = false; 585 | if (event.getAction() == KeyEvent.ACTION_DOWN) { 586 | switch (event.getKeyCode()) { 587 | case KeyEvent.KEYCODE_DPAD_UP: 588 | if (!event.isAltPressed()) { 589 | handled = arrowScroll(View.FOCUS_UP); 590 | } else { 591 | handled = fullScroll(View.FOCUS_UP); 592 | } 593 | break; 594 | case KeyEvent.KEYCODE_DPAD_DOWN: 595 | if (!event.isAltPressed()) { 596 | handled = arrowScroll(View.FOCUS_DOWN); 597 | } else { 598 | handled = fullScroll(View.FOCUS_DOWN); 599 | } 600 | break; 601 | case KeyEvent.KEYCODE_SPACE: 602 | pageScroll(event.isShiftPressed() ? View.FOCUS_UP 603 | : View.FOCUS_DOWN); 604 | break; 605 | } 606 | } 607 | 608 | return handled; 609 | } 610 | 611 | private boolean inChild(int x, int y) { 612 | if (getChildCount() > 0) { 613 | final int scrollY = getScrollY(); 614 | final View child = getChildAt(0); 615 | return !(y < child.getTop() - scrollY 616 | || y >= child.getBottom() - scrollY || x < child.getLeft() || x >= child 617 | .getRight()); 618 | } 619 | return false; 620 | } 621 | 622 | private void initOrResetVelocityTracker() { 623 | if (mVelocityTracker == null) { 624 | mVelocityTracker = VelocityTracker.obtain(); 625 | } else { 626 | mVelocityTracker.clear(); 627 | } 628 | } 629 | 630 | private void initVelocityTrackerIfNotExists() { 631 | if (mVelocityTracker == null) { 632 | mVelocityTracker = VelocityTracker.obtain(); 633 | } 634 | } 635 | 636 | private void recycleVelocityTracker() { 637 | if (mVelocityTracker != null) { 638 | mVelocityTracker.recycle(); 639 | mVelocityTracker = null; 640 | } 641 | } 642 | 643 | @Override 644 | public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 645 | if (disallowIntercept) { 646 | recycleVelocityTracker(); 647 | } 648 | super.requestDisallowInterceptTouchEvent(disallowIntercept); 649 | } 650 | 651 | @Override 652 | public boolean onInterceptTouchEvent(MotionEvent ev) { 653 | /* 654 | * This method JUST determines whether we want to intercept the motion. 655 | * If we return true, onMotionEvent will be called and we do the actual 656 | * scrolling there. 657 | */ 658 | 659 | /* 660 | * Shortcut the most recurring case: the user is in the dragging state 661 | * and he is moving his finger. We want to intercept this motion. 662 | */ 663 | final int action = ev.getAction(); 664 | if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 665 | return true; 666 | } 667 | 668 | /* 669 | * Don't try to intercept touch if we can't scroll anyway. 670 | */ 671 | if (getScrollY() == 0 && !ViewCompat.canScrollVertically(this, 1)) { 672 | return false; 673 | } 674 | 675 | switch (action & MotionEventCompat.ACTION_MASK) { 676 | case MotionEvent.ACTION_MOVE: { 677 | /* 678 | * mIsBeingDragged == false, otherwise the shortcut would have 679 | * caught it. Check whether the user has moved far enough from his 680 | * original down touch. 681 | */ 682 | 683 | /* 684 | * Locally do absolute value. mLastMotionY is set to the y value of 685 | * the down event. 686 | */ 687 | final int activePointerId = mActivePointerId; 688 | if (activePointerId == INVALID_POINTER) { 689 | // If we don't have a valid id, the touch down wasn't on 690 | // content. 691 | break; 692 | } 693 | 694 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, 695 | activePointerId); 696 | if (pointerIndex == -1) { 697 | Log.e(TAG, "Invalid pointerId=" + activePointerId 698 | + " in onInterceptTouchEvent"); 699 | break; 700 | } 701 | 702 | final int x = (int) MotionEventCompat.getX(ev, pointerIndex); 703 | final int xDiff = Math.abs(x - mLastMotionX); 704 | final int y = (int) MotionEventCompat.getY(ev, pointerIndex); 705 | final int yDiff = Math.abs(y - mLastMotionY); 706 | 707 | if (yDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { 708 | mIsBeingDragged = true; 709 | mLastMotionY = y; 710 | initVelocityTrackerIfNotExists(); 711 | mVelocityTracker.addMovement(ev); 712 | mNestedYOffset = 0; 713 | final ViewParent parent = getParent(); 714 | if (parent != null) { 715 | parent.requestDisallowInterceptTouchEvent(true); 716 | } 717 | }else if (xDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { 718 | mIsBeingDragged = true; 719 | mLastMotionX = x; 720 | initVelocityTrackerIfNotExists(); 721 | mVelocityTracker.addMovement(ev); 722 | mNestedXOffset = 0; 723 | final ViewParent parent = getParent(); 724 | if (parent != null) { 725 | parent.requestDisallowInterceptTouchEvent(true); 726 | } 727 | } 728 | break; 729 | } 730 | 731 | case MotionEvent.ACTION_DOWN: { 732 | final int y = (int) ev.getY(); 733 | final int x = (int) ev.getX(); 734 | if (!inChild(x, y)) { 735 | mIsBeingDragged = false; 736 | scrollDirection = DIRECTION_INVALIDATE; 737 | recycleVelocityTracker(); 738 | break; 739 | } 740 | 741 | /* 742 | * Remember location of down touch. ACTION_DOWN always refers to 743 | * pointer index 0. 744 | */ 745 | mLastMotionY = y; 746 | mLastMotionX = x; 747 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 748 | 749 | initOrResetVelocityTracker(); 750 | mVelocityTracker.addMovement(ev); 751 | /* 752 | * If being flinged and user touches the screen, initiate drag; 753 | * otherwise don't. mScroller.isFinished should be false when being 754 | * flinged. 755 | */ 756 | mIsBeingDragged = !mScroller.isFinished(); 757 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); 758 | break; 759 | } 760 | 761 | case MotionEvent.ACTION_CANCEL: 762 | case MotionEvent.ACTION_UP: 763 | /* Release the drag */ 764 | mIsBeingDragged = false; 765 | mActivePointerId = INVALID_POINTER; 766 | recycleVelocityTracker(); 767 | stopNestedScroll(); 768 | break; 769 | case MotionEventCompat.ACTION_POINTER_UP: 770 | onSecondaryPointerUp(ev); 771 | break; 772 | } 773 | 774 | /* 775 | * The only time we want to intercept motion events is if we are in the 776 | * drag mode. 777 | */ 778 | return mIsBeingDragged; 779 | } 780 | 781 | @Override 782 | public boolean onTouchEvent(MotionEvent ev) { 783 | initVelocityTrackerIfNotExists(); 784 | 785 | MotionEvent vtev = MotionEvent.obtain(ev); 786 | 787 | final int actionMasked = MotionEventCompat.getActionMasked(ev); 788 | 789 | if (actionMasked == MotionEvent.ACTION_DOWN) { 790 | mNestedYOffset = 0; 791 | mNestedXOffset = 0; 792 | } 793 | vtev.offsetLocation(mNestedXOffset, mNestedYOffset); 794 | 795 | switch (actionMasked) { 796 | case MotionEvent.ACTION_DOWN: { 797 | if (getChildCount() == 0) { 798 | return false; 799 | } 800 | if ((mIsBeingDragged = !mScroller.isFinished())) { 801 | final ViewParent parent = getParent(); 802 | if (parent != null) { 803 | parent.requestDisallowInterceptTouchEvent(true); 804 | } 805 | } 806 | 807 | /* 808 | * If being flinged and user touches, stop the fling. isFinished 809 | * will be false if being flinged. 810 | */ 811 | if (!mScroller.isFinished()) { 812 | mScroller.abortAnimation(); 813 | } 814 | // Remember where the motion event started 815 | mLastMotionX = (int) ev.getX(); 816 | mLastMotionY = (int) ev.getY(); 817 | mIsBeingDragged = false; 818 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 819 | startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); 820 | break; 821 | } 822 | case MotionEvent.ACTION_MOVE: 823 | final int activePointerIndex = MotionEventCompat.findPointerIndex( 824 | ev, mActivePointerId); 825 | if (activePointerIndex == -1) { 826 | Log.e(TAG, "Invalid pointerId=" + mActivePointerId 827 | + " in onTouchEvent"); 828 | break; 829 | } 830 | 831 | final int x = (int) MotionEventCompat.getX(ev, activePointerIndex); 832 | int deltaX = mLastMotionX - x; 833 | final int y = (int) MotionEventCompat.getY(ev, activePointerIndex); 834 | int deltaY = mLastMotionY - y; 835 | if (dispatchNestedPreScroll(deltaX, deltaY, mScrollConsumed, mScrollOffset)) { 836 | deltaX -= mScrollConsumed[0]; 837 | deltaY -= mScrollConsumed[1]; 838 | vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); 839 | mNestedXOffset += mScrollOffset[0]; 840 | mNestedYOffset += mScrollOffset[1]; 841 | } 842 | 843 | if (!mIsBeingDragged) { 844 | if (Math.abs(deltaY) > mTouchSlop) { 845 | final ViewParent parent = getParent(); 846 | if (parent != null) { 847 | parent.requestDisallowInterceptTouchEvent(true); 848 | } 849 | mIsBeingDragged = true; 850 | if (deltaY > 0) { 851 | deltaY -= mTouchSlop; 852 | } else { 853 | deltaY += mTouchSlop; 854 | } 855 | scrollDirection = DIRECTION_VERTICAL; 856 | }else if (Math.abs(deltaX) > mTouchSlop) { 857 | final ViewParent parent = getParent(); 858 | if (parent != null) { 859 | parent.requestDisallowInterceptTouchEvent(true); 860 | } 861 | mIsBeingDragged = true; 862 | if (deltaX > 0) { 863 | deltaX -= mTouchSlop; 864 | } else { 865 | deltaX += mTouchSlop; 866 | } 867 | scrollDirection = DIRECTION_HORIZONTAL; 868 | }else { 869 | scrollDirection = DIRECTION_INVALIDATE; 870 | } 871 | } 872 | if (mIsBeingDragged && scrollDirection != DIRECTION_INVALIDATE) { 873 | if (scrollDirection == DIRECTION_VERTICAL) { 874 | // Scroll to follow the motion event 875 | mLastMotionY = y - mScrollOffset[1]; 876 | 877 | final int oldY = getScrollY(); 878 | final int horizontalRange = getHorizontalScrollRange(); 879 | final int verticalRange = getVerticalScrollRange(); 880 | final int overscrollMode = ViewCompat.getOverScrollMode(this); 881 | boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS 882 | || (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && verticalRange > 0); 883 | 884 | // Calling overScrollByCompat will call onOverScrolled, which 885 | // calls onScrollChanged if applicable. 886 | if (overScrollByCompat(0, deltaY, getScrollX(), getScrollY(), horizontalRange, verticalRange, 0, 887 | 0, true) && !hasNestedScrollingParent()) { 888 | // Break our velocity if we hit a scroll barrier. 889 | mVelocityTracker.clear(); 890 | } 891 | 892 | final int scrolledDeltaY = getScrollY() - oldY; 893 | final int unconsumedY = deltaY - scrolledDeltaY; 894 | if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, 895 | mScrollOffset)) { 896 | mLastMotionY -= mScrollOffset[1]; 897 | vtev.offsetLocation(0, mScrollOffset[1]); 898 | mNestedYOffset += mScrollOffset[1]; 899 | } else if (canOverscroll) { 900 | ensureGlows(); 901 | final int pulledToY = oldY + deltaY; 902 | if (pulledToY < 0) { 903 | mEdgeGlowTop.onPull((float) deltaY / getHeight(), 904 | MotionEventCompat.getX(ev, activePointerIndex) 905 | / getWidth()); 906 | if (!mEdgeGlowBottom.isFinished()) { 907 | mEdgeGlowBottom.onRelease(); 908 | } 909 | } else if (pulledToY > verticalRange) { 910 | mEdgeGlowBottom.onPull( 911 | (float) deltaY / getHeight(), 912 | 1.f 913 | - MotionEventCompat.getX(ev, 914 | activePointerIndex) 915 | / getWidth()); 916 | if (!mEdgeGlowTop.isFinished()) { 917 | mEdgeGlowTop.onRelease(); 918 | } 919 | } 920 | if (mEdgeGlowTop != null 921 | && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom 922 | .isFinished())) { 923 | ViewCompat.postInvalidateOnAnimation(this); 924 | } 925 | } 926 | }else if(scrollDirection == DIRECTION_HORIZONTAL){ 927 | // Scroll to follow the motion event 928 | mLastMotionX = x - mScrollOffset[0]; 929 | 930 | final int oldX = getScrollX(); 931 | final int horizontalRange = getHorizontalScrollRange(); 932 | final int verticalRange = getVerticalScrollRange(); 933 | final int overscrollMode = ViewCompat.getOverScrollMode(this); 934 | boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS 935 | || (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && horizontalRange > 0); 936 | 937 | // Calling overScrollByCompat will call onOverScrolled, which 938 | // calls onScrollChanged if applicable. 939 | if (overScrollByCompat(deltaX, 0, getScrollX(), getScrollY(), horizontalRange, verticalRange, 0, 940 | 0, true) && !hasNestedScrollingParent()) { 941 | // Break our velocity if we hit a scroll barrier. 942 | mVelocityTracker.clear(); 943 | } 944 | 945 | final int scrolledDeltaX = getScrollX() - oldX; 946 | final int unconsumedX = deltaX - scrolledDeltaX; 947 | if (dispatchNestedScroll(scrolledDeltaX, 0, unconsumedX, 0, 948 | mScrollOffset)) { 949 | mLastMotionX -= mScrollOffset[0]; 950 | vtev.offsetLocation(mScrollOffset[0], 0); 951 | mNestedXOffset += mScrollOffset[0]; 952 | } else if (canOverscroll) { 953 | ensureGlows(); 954 | final int pulledToX = oldX + deltaX; 955 | if (pulledToX < 0) { 956 | mEdgeGlowLeft.onPull((float) deltaX / getWidth(), 957 | MotionEventCompat.getX(ev, activePointerIndex) 958 | / getHeight()); 959 | if (!mEdgeGlowRight.isFinished()) { 960 | mEdgeGlowRight.onRelease(); 961 | } 962 | } else if (pulledToX > horizontalRange) { 963 | mEdgeGlowRight.onPull( 964 | (float) deltaX / getWidth(), 965 | 1.f 966 | - MotionEventCompat.getX(ev, 967 | activePointerIndex) 968 | / getHeight()); 969 | if (!mEdgeGlowLeft.isFinished()) { 970 | mEdgeGlowLeft.onRelease(); 971 | } 972 | } 973 | if (mEdgeGlowLeft != null && (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight 974 | .isFinished())) { 975 | ViewCompat.postInvalidateOnAnimation(this); 976 | } 977 | } 978 | } 979 | 980 | } 981 | break; 982 | case MotionEvent.ACTION_UP: 983 | if (mIsBeingDragged) { 984 | final VelocityTracker velocityTracker = mVelocityTracker; 985 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 986 | int verticalInitialVelocity = (int) VelocityTrackerCompat.getYVelocity( 987 | velocityTracker, mActivePointerId); 988 | int horizontalInitialVelocity = (int) VelocityTrackerCompat.getXVelocity( 989 | velocityTracker, mActivePointerId); 990 | 991 | if (scrollDirection == DIRECTION_VERTICAL) { 992 | if ((Math.abs(verticalInitialVelocity) > mMinimumVelocity)) { 993 | flingWithNestedDispatchVertical(-verticalInitialVelocity); 994 | } 995 | }else if (scrollDirection == DIRECTION_HORIZONTAL){ 996 | if ((Math.abs(horizontalInitialVelocity) > mMinimumVelocity)) { 997 | flingWithNestedDispatchHorizontal(-horizontalInitialVelocity); 998 | } 999 | } 1000 | mActivePointerId = INVALID_POINTER; 1001 | endDrag(); 1002 | } 1003 | break; 1004 | case MotionEvent.ACTION_CANCEL: 1005 | if (mIsBeingDragged && getChildCount() > 0) { 1006 | mActivePointerId = INVALID_POINTER; 1007 | endDrag(); 1008 | } 1009 | break; 1010 | case MotionEventCompat.ACTION_POINTER_DOWN: { 1011 | final int index = MotionEventCompat.getActionIndex(ev); 1012 | mLastMotionX = (int) MotionEventCompat.getX(ev, index); 1013 | mLastMotionY = (int) MotionEventCompat.getY(ev, index); 1014 | mActivePointerId = MotionEventCompat.getPointerId(ev, index); 1015 | break; 1016 | } 1017 | case MotionEventCompat.ACTION_POINTER_UP: 1018 | onSecondaryPointerUp(ev); 1019 | mLastMotionX = (int) MotionEventCompat.getX(ev, 1020 | MotionEventCompat.findPointerIndex(ev, mActivePointerId)); 1021 | mLastMotionY = (int) MotionEventCompat.getY(ev, 1022 | MotionEventCompat.findPointerIndex(ev, mActivePointerId)); 1023 | break; 1024 | } 1025 | 1026 | if (mVelocityTracker != null) { 1027 | mVelocityTracker.addMovement(vtev); 1028 | } 1029 | vtev.recycle(); 1030 | return true; 1031 | } 1032 | 1033 | private void onSecondaryPointerUp(MotionEvent ev) { 1034 | final int pointerIndex = (ev.getAction() & MotionEventCompat.ACTION_POINTER_INDEX_MASK) >> MotionEventCompat.ACTION_POINTER_INDEX_SHIFT; 1035 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 1036 | if (pointerId == mActivePointerId) { 1037 | // This was our active pointer going up. Choose a new 1038 | // active pointer and adjust accordingly. 1039 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 1040 | mLastMotionX = (int) MotionEventCompat.getX(ev, newPointerIndex); 1041 | mLastMotionY = (int) MotionEventCompat.getY(ev, newPointerIndex); 1042 | mActivePointerId = MotionEventCompat.getPointerId(ev, 1043 | newPointerIndex); 1044 | if (mVelocityTracker != null) { 1045 | mVelocityTracker.clear(); 1046 | } 1047 | } 1048 | } 1049 | 1050 | public boolean onGenericMotionEvent(MotionEvent event) { 1051 | if ((MotionEventCompat.getSource(event) & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { 1052 | switch (event.getAction()) { 1053 | case MotionEventCompat.ACTION_SCROLL: { 1054 | if (!mIsBeingDragged) { 1055 | final float vscroll = MotionEventCompat.getAxisValue(event, 1056 | MotionEventCompat.AXIS_VSCROLL); 1057 | if (vscroll != 0) { 1058 | final int delta = (int) (vscroll * getVerticalScrollFactorCompat()); 1059 | final int range = getVerticalScrollRange(); 1060 | int oldScrollY = getScrollY(); 1061 | int newScrollY = oldScrollY - delta; 1062 | if (newScrollY < 0) { 1063 | newScrollY = 0; 1064 | } else if (newScrollY > range) { 1065 | newScrollY = range; 1066 | } 1067 | if (newScrollY != oldScrollY) { 1068 | super.scrollTo(getScrollX(), newScrollY); 1069 | return true; 1070 | } 1071 | } 1072 | } 1073 | } 1074 | } 1075 | } 1076 | return false; 1077 | } 1078 | 1079 | private float getVerticalScrollFactorCompat() { 1080 | if (mVerticalScrollFactor == 0) { 1081 | TypedValue outValue = new TypedValue(); 1082 | final Context context = getContext(); 1083 | if (!context.getTheme().resolveAttribute( 1084 | android.R.attr.listPreferredItemHeight, outValue, true)) { 1085 | throw new IllegalStateException( 1086 | "Expected theme to define listPreferredItemHeight."); 1087 | } 1088 | mVerticalScrollFactor = outValue.getDimension(context 1089 | .getResources().getDisplayMetrics()); 1090 | } 1091 | return mVerticalScrollFactor; 1092 | } 1093 | 1094 | protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, 1095 | boolean clampedY) { 1096 | super.scrollTo(scrollX, scrollY); 1097 | // getChildAt(0).scrollTo(scrollX, scrollY); 1098 | } 1099 | 1100 | boolean overScrollByCompat(int deltaX, int deltaY, int scrollX, 1101 | int scrollY, int scrollRangeX, int scrollRangeY, 1102 | int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { 1103 | final int overScrollMode = ViewCompat.getOverScrollMode(this); 1104 | final boolean canScrollHorizontal = computeHorizontalScrollRange() > computeHorizontalScrollExtent(); 1105 | final boolean canScrollVertical = computeVerticalScrollRange() > computeVerticalScrollExtent(); 1106 | final boolean overScrollHorizontal = overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS 1107 | || (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); 1108 | final boolean overScrollVertical = overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS 1109 | || (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); 1110 | 1111 | int newScrollX = scrollX + deltaX; 1112 | if (!overScrollHorizontal) { 1113 | maxOverScrollX = 0; 1114 | } 1115 | 1116 | int newScrollY = scrollY + deltaY; 1117 | if (!overScrollVertical) { 1118 | maxOverScrollY = 0; 1119 | } 1120 | 1121 | // Clamp values if at the limits and record 1122 | final int left = -maxOverScrollX; 1123 | final int right = maxOverScrollX + scrollRangeX; 1124 | final int top = -maxOverScrollY; 1125 | final int bottom = maxOverScrollY + scrollRangeY; 1126 | 1127 | boolean clampedX = false; 1128 | if (newScrollX > right) { 1129 | newScrollX = right; 1130 | clampedX = true; 1131 | } else if (newScrollX < left) { 1132 | newScrollX = left; 1133 | clampedX = true; 1134 | } 1135 | 1136 | boolean clampedY = false; 1137 | if (newScrollY > bottom) { 1138 | newScrollY = bottom; 1139 | clampedY = true; 1140 | } else if (newScrollY < top) { 1141 | newScrollY = top; 1142 | clampedY = true; 1143 | } 1144 | 1145 | onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); 1146 | 1147 | return clampedX || clampedY; 1148 | } 1149 | 1150 | private int getVerticalScrollRange() { 1151 | int scrollRange = 0; 1152 | if (getChildCount() > 0) { 1153 | View child = getChildAt(0); 1154 | scrollRange = Math.max(0, child.getHeight() 1155 | - (getHeight() - getPaddingBottom() - getPaddingTop())); 1156 | } 1157 | return scrollRange; 1158 | } 1159 | 1160 | private int getHorizontalScrollRange() { 1161 | int scrollRange = 0; 1162 | if (getChildCount() > 0) { 1163 | View child = getChildAt(0); 1164 | scrollRange = Math.max(0, child.getWidth() 1165 | - (getWidth() - getPaddingLeft() - getPaddingRight())); 1166 | } 1167 | return scrollRange; 1168 | } 1169 | 1170 | /** 1171 | *

1172 | * Finds the next focusable component that fits in the specified bounds. 1173 | *

1174 | * 1175 | * @param topFocus 1176 | * look for a candidate is the one at the top of the bounds if 1177 | * topFocus is true, or at the bottom of the bounds if topFocus 1178 | * is false 1179 | * @param top 1180 | * the top offset of the bounds in which a focusable must be 1181 | * found 1182 | * @param bottom 1183 | * the bottom offset of the bounds in which a focusable must be 1184 | * found 1185 | * @return the next focusable component in the bounds or null if none can be 1186 | * found 1187 | */ 1188 | private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { 1189 | 1190 | List focusables = getFocusables(View.FOCUS_FORWARD); 1191 | View focusCandidate = null; 1192 | 1193 | /* 1194 | * A fully contained focusable is one where its top is below the bound's 1195 | * top, and its bottom is above the bound's bottom. A partially 1196 | * contained focusable is one where some part of it is within the 1197 | * bounds, but it also has some part that is not within bounds. A fully 1198 | * contained focusable is preferred to a partially contained focusable. 1199 | */ 1200 | boolean foundFullyContainedFocusable = false; 1201 | 1202 | int count = focusables.size(); 1203 | for (int i = 0; i < count; i++) { 1204 | View view = focusables.get(i); 1205 | int viewTop = view.getTop(); 1206 | int viewBottom = view.getBottom(); 1207 | 1208 | if (top < viewBottom && viewTop < bottom) { 1209 | /* 1210 | * the focusable is in the target area, it is a candidate for 1211 | * focusing 1212 | */ 1213 | 1214 | final boolean viewIsFullyContained = (top < viewTop) 1215 | && (viewBottom < bottom); 1216 | 1217 | if (focusCandidate == null) { 1218 | /* No candidate, take this one */ 1219 | focusCandidate = view; 1220 | foundFullyContainedFocusable = viewIsFullyContained; 1221 | } else { 1222 | final boolean viewIsCloserToBoundary = (topFocus && viewTop < focusCandidate 1223 | .getTop()) 1224 | || (!topFocus && viewBottom > focusCandidate 1225 | .getBottom()); 1226 | 1227 | if (foundFullyContainedFocusable) { 1228 | if (viewIsFullyContained && viewIsCloserToBoundary) { 1229 | /* 1230 | * We're dealing with only fully contained views, so 1231 | * it has to be closer to the boundary to beat our 1232 | * candidate 1233 | */ 1234 | focusCandidate = view; 1235 | } 1236 | } else { 1237 | if (viewIsFullyContained) { 1238 | /* 1239 | * Any fully contained view beats a partially 1240 | * contained view 1241 | */ 1242 | focusCandidate = view; 1243 | foundFullyContainedFocusable = true; 1244 | } else if (viewIsCloserToBoundary) { 1245 | /* 1246 | * Partially contained view beats another partially 1247 | * contained view if it's closer 1248 | */ 1249 | focusCandidate = view; 1250 | } 1251 | } 1252 | } 1253 | } 1254 | } 1255 | 1256 | return focusCandidate; 1257 | } 1258 | 1259 | /** 1260 | *

1261 | * Handles scrolling in response to a "page up/down" shortcut press. This 1262 | * method will scroll the view by one page up or down and give the focus to 1263 | * the topmost/bottommost component in the new visible area. If no component 1264 | * is a good candidate for focus, this scrollview reclaims the focus. 1265 | *

1266 | * 1267 | * @param direction 1268 | * the scroll direction: {@link android.view.View#FOCUS_UP} to go 1269 | * one page up or {@link android.view.View#FOCUS_DOWN} to go one 1270 | * page down 1271 | * @return true if the key event is consumed by this method, false otherwise 1272 | */ 1273 | public boolean pageScroll(int direction) { 1274 | boolean down = direction == View.FOCUS_DOWN; 1275 | int height = getHeight(); 1276 | 1277 | if (down) { 1278 | mTempRect.top = getScrollY() + height; 1279 | int count = getChildCount(); 1280 | if (count > 0) { 1281 | View view = getChildAt(count - 1); 1282 | if (mTempRect.top + height > view.getBottom()) { 1283 | mTempRect.top = view.getBottom() - height; 1284 | } 1285 | } 1286 | } else { 1287 | mTempRect.top = getScrollY() - height; 1288 | if (mTempRect.top < 0) { 1289 | mTempRect.top = 0; 1290 | } 1291 | } 1292 | mTempRect.bottom = mTempRect.top + height; 1293 | 1294 | return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); 1295 | } 1296 | 1297 | /** 1298 | *

1299 | * Handles scrolling in response to a "home/end" shortcut press. This method 1300 | * will scroll the view to the top or bottom and give the focus to the 1301 | * topmost/bottommost component in the new visible area. If no component is 1302 | * a good candidate for focus, this scrollview reclaims the focus. 1303 | *

1304 | * 1305 | * @param direction 1306 | * the scroll direction: {@link android.view.View#FOCUS_UP} to go 1307 | * the top of the view or {@link android.view.View#FOCUS_DOWN} to 1308 | * go the bottom 1309 | * @return true if the key event is consumed by this method, false otherwise 1310 | */ 1311 | public boolean fullScroll(int direction) { 1312 | boolean down = direction == View.FOCUS_DOWN; 1313 | int height = getHeight(); 1314 | 1315 | mTempRect.top = 0; 1316 | mTempRect.bottom = height; 1317 | 1318 | if (down) { 1319 | int count = getChildCount(); 1320 | if (count > 0) { 1321 | View view = getChildAt(count - 1); 1322 | mTempRect.bottom = view.getBottom() + getPaddingBottom(); 1323 | mTempRect.top = mTempRect.bottom - height; 1324 | } 1325 | } 1326 | 1327 | return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); 1328 | } 1329 | 1330 | /** 1331 | *

1332 | * Scrolls the view to make the area defined by top and 1333 | * bottom visible. This method attempts to give the focus to a 1334 | * component visible in this area. If no component can be focused in the new 1335 | * visible area, the focus is reclaimed by this ScrollView. 1336 | *

1337 | * 1338 | * @param direction 1339 | * the scroll direction: {@link android.view.View#FOCUS_UP} to go 1340 | * upward, {@link android.view.View#FOCUS_DOWN} to downward 1341 | * @param top 1342 | * the top offset of the new area to be made visible 1343 | * @param bottom 1344 | * the bottom offset of the new area to be made visible 1345 | * @return true if the key event is consumed by this method, false otherwise 1346 | */ 1347 | private boolean scrollAndFocus(int direction, int top, int bottom) { 1348 | boolean handled = true; 1349 | 1350 | int height = getHeight(); 1351 | int containerTop = getScrollY(); 1352 | int containerBottom = containerTop + height; 1353 | boolean up = direction == View.FOCUS_UP; 1354 | 1355 | View newFocused = findFocusableViewInBounds(up, top, bottom); 1356 | if (newFocused == null) { 1357 | newFocused = this; 1358 | } 1359 | 1360 | if (top >= containerTop && bottom <= containerBottom) { 1361 | handled = false; 1362 | } else { 1363 | int delta = up ? (top - containerTop) : (bottom - containerBottom); 1364 | doScrollY(delta); 1365 | } 1366 | 1367 | if (newFocused != findFocus()) 1368 | newFocused.requestFocus(direction); 1369 | 1370 | return handled; 1371 | } 1372 | 1373 | /** 1374 | * Handle scrolling in response to an up or down arrow click. 1375 | * 1376 | * @param direction 1377 | * The direction corresponding to the arrow key that was pressed 1378 | * @return True if we consumed the event, false otherwise 1379 | */ 1380 | public boolean arrowScroll(int direction) { 1381 | 1382 | View currentFocused = findFocus(); 1383 | if (currentFocused == this) 1384 | currentFocused = null; 1385 | 1386 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, 1387 | currentFocused, direction); 1388 | 1389 | final int maxJump = getMaxScrollYAmount(); 1390 | 1391 | if (nextFocused != null 1392 | && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) { 1393 | nextFocused.getDrawingRect(mTempRect); 1394 | offsetDescendantRectToMyCoords(nextFocused, mTempRect); 1395 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1396 | doScrollY(scrollDelta); 1397 | nextFocused.requestFocus(direction); 1398 | } else { 1399 | // no new focus 1400 | int scrollDelta = maxJump; 1401 | 1402 | if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { 1403 | scrollDelta = getScrollY(); 1404 | } else if (direction == View.FOCUS_DOWN) { 1405 | if (getChildCount() > 0) { 1406 | int daBottom = getChildAt(0).getBottom(); 1407 | int screenBottom = getScrollY() + getHeight() 1408 | - getPaddingBottom(); 1409 | if (daBottom - screenBottom < maxJump) { 1410 | scrollDelta = daBottom - screenBottom; 1411 | } 1412 | } 1413 | } 1414 | if (scrollDelta == 0) { 1415 | return false; 1416 | } 1417 | doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); 1418 | } 1419 | 1420 | if (currentFocused != null && currentFocused.isFocused() 1421 | && isOffScreen(currentFocused)) { 1422 | // previously focused item still has focus and is off screen, give 1423 | // it up (take it back to ourselves) 1424 | // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we 1425 | // are 1426 | // sure to 1427 | // get it) 1428 | final int descendantFocusability = getDescendantFocusability(); // save 1429 | setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 1430 | requestFocus(); 1431 | setDescendantFocusability(descendantFocusability); // restore 1432 | } 1433 | return true; 1434 | } 1435 | 1436 | /** 1437 | * @return whether the descendant of this scroll view is scrolled off 1438 | * screen. 1439 | */ 1440 | private boolean isOffScreen(View descendant) { 1441 | return !isWithinDeltaOfScreen(descendant, 0, getHeight()); 1442 | } 1443 | 1444 | /** 1445 | * @return whether the descendant of this scroll view is within delta pixels 1446 | * of being on the screen. 1447 | */ 1448 | private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) { 1449 | descendant.getDrawingRect(mTempRect); 1450 | offsetDescendantRectToMyCoords(descendant, mTempRect); 1451 | 1452 | return (mTempRect.bottom + delta) >= getScrollY() 1453 | && (mTempRect.top - delta) <= (getScrollY() + height); 1454 | } 1455 | 1456 | /** 1457 | * Smooth scroll by a Y delta 1458 | * 1459 | * @param delta 1460 | * the number of pixels to scroll by on the Y axis 1461 | */ 1462 | private void doScrollY(int delta) { 1463 | if (delta != 0) { 1464 | if (mSmoothScrollingEnabled) { 1465 | smoothScrollBy(0, delta); 1466 | } else { 1467 | scrollBy(0, delta); 1468 | } 1469 | } 1470 | } 1471 | 1472 | /** 1473 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 1474 | * 1475 | * @param dx 1476 | * the number of pixels to scroll by on the X axis 1477 | * @param dy 1478 | * the number of pixels to scroll by on the Y axis 1479 | */ 1480 | public final void smoothScrollBy(int dx, int dy) { 1481 | if (getChildCount() == 0) { 1482 | // Nothing to do. 1483 | return; 1484 | } 1485 | long duration = AnimationUtils.currentAnimationTimeMillis() 1486 | - mLastScroll; 1487 | if (duration > ANIMATED_SCROLL_GAP) { 1488 | if (scrollDirection == DIRECTION_VERTICAL) { 1489 | final int height = getHeight() - getPaddingBottom() 1490 | - getPaddingTop(); 1491 | final int bottom = getChildAt(0).getHeight(); 1492 | final int maxY = Math.max(0, bottom - height); 1493 | final int scrollY = getScrollY(); 1494 | dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY; 1495 | 1496 | mScroller.startScroll(getScrollX(), scrollY, 0, dy); 1497 | ViewCompat.postInvalidateOnAnimation(this); 1498 | }else if (scrollDirection == DIRECTION_HORIZONTAL){ 1499 | final int width = getWidth() - getPaddingLeft() 1500 | - getPaddingRight(); 1501 | final int right = getChildAt(0).getHeight(); 1502 | final int maxX = Math.max(0, right - width); 1503 | final int scrollX = getScrollX(); 1504 | dx = Math.max(Math.min(scrollX + dx, maxX),0) - scrollX; 1505 | 1506 | mScroller.startScroll(scrollX, getScrollY(), dx, 0); 1507 | ViewCompat.postInvalidateOnAnimation(this); 1508 | } 1509 | } else { 1510 | if (!mScroller.isFinished()) { 1511 | mScroller.abortAnimation(); 1512 | } 1513 | scrollBy(dx, dy); 1514 | } 1515 | mLastScroll = AnimationUtils.currentAnimationTimeMillis(); 1516 | } 1517 | 1518 | /** 1519 | * Like {@link #scrollTo}, but scroll smoothly instead of immediately. 1520 | * 1521 | * @param x 1522 | * the position where to scroll on the X axis 1523 | * @param y 1524 | * the position where to scroll on the Y axis 1525 | */ 1526 | public final void smoothScrollTo(int x, int y) { 1527 | smoothScrollBy(x - getScrollX(), y - getScrollY()); 1528 | } 1529 | 1530 | protected int computeHorizontalScrollRange() { 1531 | final int count = getChildCount(); 1532 | final int contentWidth = getWidth() - getPaddingLeft() - getPaddingRight(); 1533 | if (count == 0) { 1534 | return contentWidth; 1535 | } 1536 | 1537 | int scrollRange = getChildAt(0).getRight(); 1538 | final int scrollX = getScrollX(); 1539 | final int overscrollRight = Math.max(0, scrollRange - contentWidth); 1540 | if (scrollX < 0) { 1541 | scrollRange -= scrollX; 1542 | } else if (scrollX > overscrollRight) { 1543 | scrollRange += scrollX - overscrollRight; 1544 | } 1545 | 1546 | return scrollRange; 1547 | } 1548 | 1549 | /** 1550 | *

1551 | * The scroll range of a scroll view is the overall height of all of its 1552 | * children. 1553 | *

1554 | */ 1555 | @Override 1556 | protected int computeVerticalScrollRange() { 1557 | final int count = getChildCount(); 1558 | final int contentHeight = getHeight() - getPaddingBottom() 1559 | - getPaddingTop(); 1560 | if (count == 0) { 1561 | return contentHeight; 1562 | } 1563 | 1564 | int scrollRange = getChildAt(0).getBottom(); 1565 | final int scrollY = getScrollY(); 1566 | final int overscrollBottom = Math.max(0, scrollRange - contentHeight); 1567 | if (scrollY < 0) { 1568 | scrollRange -= scrollY; 1569 | } else if (scrollY > overscrollBottom) { 1570 | scrollRange += scrollY - overscrollBottom; 1571 | } 1572 | 1573 | return scrollRange; 1574 | } 1575 | 1576 | @Override 1577 | protected int computeVerticalScrollOffset() { 1578 | return Math.max(0, super.computeVerticalScrollOffset()); 1579 | } 1580 | 1581 | @Override 1582 | protected void measureChild(View child, int parentWidthMeasureSpec, 1583 | int parentHeightMeasureSpec) { 1584 | ViewGroup.LayoutParams lp = child.getLayoutParams(); 1585 | 1586 | int childWidthMeasureSpec; 1587 | int childHeightMeasureSpec; 1588 | 1589 | childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 1590 | getPaddingLeft() + getPaddingRight(), lp.width); 1591 | 1592 | childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, 1593 | MeasureSpec.UNSPECIFIED); 1594 | 1595 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1596 | } 1597 | 1598 | @Override 1599 | protected void measureChildWithMargins(View child, 1600 | int parentWidthMeasureSpec, int widthUsed, 1601 | int parentHeightMeasureSpec, int heightUsed) { 1602 | final MarginLayoutParams lp = (MarginLayoutParams) child 1603 | .getLayoutParams(); 1604 | 1605 | final int childWidthMeasureSpec = getChildMeasureSpec( 1606 | parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight() 1607 | + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); 1608 | final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( 1609 | lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); 1610 | 1611 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1612 | } 1613 | 1614 | @Override 1615 | public void computeScroll() { 1616 | int measuredHeight = getMeasuredHeight(); 1617 | int measuredWidth = getMeasuredHeight(); 1618 | if (mScroller.computeScrollOffset()) { 1619 | int oldX = getScrollX(); 1620 | int oldY = getScrollY(); 1621 | int x = mScroller.getCurrX(); 1622 | int y = mScroller.getCurrY(); 1623 | if (oldX != x || oldY != y) { 1624 | final int horizontalRange = getHorizontalScrollRange(); 1625 | final int verticalRange = getVerticalScrollRange(); 1626 | final int overscrollMode = ViewCompat.getOverScrollMode(this); 1627 | final boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS 1628 | || (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && (verticalRange > 0 || horizontalRange > 0)); 1629 | 1630 | overScrollByCompat(x - oldX, y - oldY, oldX, oldY, horizontalRange, verticalRange, 0, 1631 | 0, false); 1632 | 1633 | if (canOverscroll) { 1634 | ensureGlows(); 1635 | if (y <= 0 && oldY > 0) { 1636 | mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); 1637 | } else if (y >= verticalRange && oldY < verticalRange) { 1638 | mEdgeGlowBottom.onAbsorb((int) mScroller 1639 | .getCurrVelocity()); 1640 | } 1641 | if (x <= 0 && oldX > 0) { 1642 | mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity()); 1643 | } else if (x >= verticalRange && oldX < horizontalRange) { 1644 | mEdgeGlowRight.onAbsorb((int) mScroller 1645 | .getCurrVelocity()); 1646 | } 1647 | } 1648 | } 1649 | } 1650 | } 1651 | 1652 | /** 1653 | * Scrolls the view to the given child. 1654 | * 1655 | * @param child 1656 | * the View to scroll to 1657 | */ 1658 | private void scrollToChild(View child) { 1659 | child.getDrawingRect(mTempRect); 1660 | 1661 | /* Offset from child's local coordinates to ScrollView coordinates */ 1662 | offsetDescendantRectToMyCoords(child, mTempRect); 1663 | 1664 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1665 | 1666 | if (scrollDelta != 0) { 1667 | scrollBy(0, scrollDelta); 1668 | } 1669 | } 1670 | 1671 | /** 1672 | * If rect is off screen, scroll just enough to get it (or at least the 1673 | * first screen size chunk of it) on screen. 1674 | * 1675 | * @param rect 1676 | * The rectangle. 1677 | * @param immediate 1678 | * True to scroll immediately without animation 1679 | * @return true if scrolling was performed 1680 | */ 1681 | private boolean scrollToChildRect(Rect rect, boolean immediate) { 1682 | final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); 1683 | final boolean scroll = delta != 0; 1684 | if (scroll) { 1685 | if (immediate) { 1686 | scrollBy(0, delta); 1687 | } else { 1688 | smoothScrollBy(0, delta); 1689 | } 1690 | } 1691 | return scroll; 1692 | } 1693 | 1694 | /** 1695 | * Compute the amount to scroll in the Y direction in order to get a 1696 | * rectangle completely on the screen (or, if taller than the screen, at 1697 | * least the first screen size chunk of it). 1698 | * 1699 | * @param rect 1700 | * The rect. 1701 | * @return The scroll delta. 1702 | */ 1703 | protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { 1704 | if (getChildCount() == 0) 1705 | return 0; 1706 | 1707 | int height = getHeight(); 1708 | int screenTop = getScrollY(); 1709 | int screenBottom = screenTop + height; 1710 | 1711 | int fadingEdge = getVerticalFadingEdgeLength(); 1712 | 1713 | // leave room for top fading edge as long as rect isn't at very top 1714 | if (rect.top > 0) { 1715 | screenTop += fadingEdge; 1716 | } 1717 | 1718 | // leave room for bottom fading edge as long as rect isn't at very 1719 | // bottom 1720 | if (rect.bottom < getChildAt(0).getHeight()) { 1721 | screenBottom -= fadingEdge; 1722 | } 1723 | 1724 | int scrollYDelta = 0; 1725 | 1726 | if (rect.bottom > screenBottom && rect.top > screenTop) { 1727 | // need to move down to get it in view: move down just enough so 1728 | // that the entire rectangle is in view (or at least the first 1729 | // screen size chunk). 1730 | 1731 | if (rect.height() > height) { 1732 | // just enough to get screen size chunk on 1733 | scrollYDelta += (rect.top - screenTop); 1734 | } else { 1735 | // get entire rect at bottom of screen 1736 | scrollYDelta += (rect.bottom - screenBottom); 1737 | } 1738 | 1739 | // make sure we aren't scrolling beyond the end of our content 1740 | int bottom = getChildAt(0).getBottom(); 1741 | int distanceToBottom = bottom - screenBottom; 1742 | scrollYDelta = Math.min(scrollYDelta, distanceToBottom); 1743 | 1744 | } else if (rect.top < screenTop && rect.bottom < screenBottom) { 1745 | // need to move up to get it in view: move up just enough so that 1746 | // entire rectangle is in view (or at least the first screen 1747 | // size chunk of it). 1748 | 1749 | if (rect.height() > height) { 1750 | // screen size chunk 1751 | scrollYDelta -= (screenBottom - rect.bottom); 1752 | } else { 1753 | // entire rect at top 1754 | scrollYDelta -= (screenTop - rect.top); 1755 | } 1756 | 1757 | // make sure we aren't scrolling any further than the top our 1758 | // content 1759 | scrollYDelta = Math.max(scrollYDelta, -getScrollY()); 1760 | } 1761 | return scrollYDelta; 1762 | } 1763 | 1764 | @Override 1765 | public void requestChildFocus(View child, View focused) { 1766 | if (!mIsLayoutDirty) { 1767 | scrollToChild(focused); 1768 | } else { 1769 | // The child may not be laid out yet, we can't compute the scroll 1770 | // yet 1771 | mChildToScrollTo = focused; 1772 | } 1773 | super.requestChildFocus(child, focused); 1774 | } 1775 | 1776 | /** 1777 | * When looking for focus in children of a scroll view, need to be a little 1778 | * more careful not to give focus to something that is scrolled off screen. 1779 | * 1780 | * This is more expensive than the default {@link android.view.ViewGroup} 1781 | * implementation, otherwise this behavior might have been made the default. 1782 | */ 1783 | @Override 1784 | protected boolean onRequestFocusInDescendants(int direction, 1785 | Rect previouslyFocusedRect) { 1786 | 1787 | // convert from forward / backward notation to up / down / left / right 1788 | // (ugh). 1789 | if (direction == View.FOCUS_FORWARD) { 1790 | direction = View.FOCUS_DOWN; 1791 | } else if (direction == View.FOCUS_BACKWARD) { 1792 | direction = View.FOCUS_UP; 1793 | } 1794 | 1795 | final View nextFocus = previouslyFocusedRect == null ? FocusFinder 1796 | .getInstance().findNextFocus(this, null, direction) 1797 | : FocusFinder.getInstance().findNextFocusFromRect(this, 1798 | previouslyFocusedRect, direction); 1799 | 1800 | if (nextFocus == null) { 1801 | return false; 1802 | } 1803 | 1804 | if (isOffScreen(nextFocus)) { 1805 | return false; 1806 | } 1807 | 1808 | return nextFocus.requestFocus(direction, previouslyFocusedRect); 1809 | } 1810 | 1811 | @Override 1812 | public boolean requestChildRectangleOnScreen(View child, Rect rectangle, 1813 | boolean immediate) { 1814 | // offset into coordinate space of this scroll view 1815 | rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() 1816 | - child.getScrollY()); 1817 | 1818 | return scrollToChildRect(rectangle, immediate); 1819 | } 1820 | 1821 | @Override 1822 | public void requestLayout() { 1823 | mIsLayoutDirty = true; 1824 | super.requestLayout(); 1825 | } 1826 | 1827 | @Override 1828 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 1829 | super.onLayout(changed, l, t, r, b); 1830 | mIsLayoutDirty = false; 1831 | // Give a child focus if it needs it 1832 | if (mChildToScrollTo != null 1833 | && isViewDescendantOf(mChildToScrollTo, this)) { 1834 | scrollToChild(mChildToScrollTo); 1835 | } 1836 | mChildToScrollTo = null; 1837 | 1838 | if (!mIsLaidOut) { 1839 | if (mSavedState != null) { 1840 | scrollTo(getScrollX(), mSavedState.scrollPosition); 1841 | mSavedState = null; 1842 | } // mScrollY default value is "0" 1843 | 1844 | final int childHeight = (getChildCount() > 0) ? getChildAt(0) 1845 | .getMeasuredHeight() : 0; 1846 | final int scrollRange = Math.max(0, childHeight 1847 | - (b - t - getPaddingBottom() - getPaddingTop())); 1848 | 1849 | // Don't forget to clamp 1850 | if (getScrollY() > scrollRange) { 1851 | scrollTo(getScrollX(), scrollRange); 1852 | } else if (getScrollY() < 0) { 1853 | scrollTo(getScrollX(), 0); 1854 | } 1855 | } 1856 | 1857 | // Calling this with the present values causes it to re-claim them 1858 | scrollTo(getScrollX(), getScrollY()); 1859 | mIsLaidOut = true; 1860 | } 1861 | 1862 | @Override 1863 | public void onAttachedToWindow() { 1864 | mIsLaidOut = false; 1865 | } 1866 | 1867 | @Override 1868 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1869 | super.onSizeChanged(w, h, oldw, oldh); 1870 | 1871 | View currentFocused = findFocus(); 1872 | if (null == currentFocused || this == currentFocused) 1873 | return; 1874 | 1875 | // If the currently-focused view was visible on the screen when the 1876 | // screen was at the old height, then scroll the screen to make that 1877 | // view visible with the new screen height. 1878 | if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) { 1879 | currentFocused.getDrawingRect(mTempRect); 1880 | offsetDescendantRectToMyCoords(currentFocused, mTempRect); 1881 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1882 | doScrollY(scrollDelta); 1883 | } 1884 | } 1885 | 1886 | /** 1887 | * Return true if child is a descendant of parent, (or equal to the parent). 1888 | */ 1889 | private static boolean isViewDescendantOf(View child, View parent) { 1890 | if (child == parent) { 1891 | return true; 1892 | } 1893 | 1894 | final ViewParent theParent = child.getParent(); 1895 | return (theParent instanceof ViewGroup) 1896 | && isViewDescendantOf((View) theParent, parent); 1897 | } 1898 | 1899 | /** 1900 | * Fling the scroll view 1901 | * 1902 | * @param velocityY 1903 | * The initial velocity in the Y direction. Positive numbers mean 1904 | * that the finger/cursor is moving down the screen, which means 1905 | * we want to scroll towards the top. 1906 | */ 1907 | public void flingVertical(int velocityY) { 1908 | if (getChildCount() > 0) { 1909 | int width = getWidth() - getPaddingRight() - getPaddingLeft(); 1910 | int height = getHeight() - getPaddingBottom() - getPaddingTop(); 1911 | int bottom = getChildAt(0).getHeight(); 1912 | 1913 | int scrollX = getScrollX(); 1914 | mScroller.fling(scrollX, getScrollY(), 0, velocityY, scrollX, scrollX, 0,Math.max(0, bottom - height),0,height / 2); 1915 | 1916 | ViewCompat.postInvalidateOnAnimation(this); 1917 | } 1918 | } 1919 | /** 1920 | * void android.support.v4.widget.ScrollerCompat.fling 1921 | * (int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) 1922 | * @param velocityX 1923 | */ 1924 | public void flingHorizontal(int velocityX) { 1925 | if (getChildCount() > 0) { 1926 | int width = getWidth() - getPaddingRight() - getPaddingLeft(); 1927 | int height = getHeight() - getPaddingBottom() - getPaddingTop(); 1928 | int right = getChildAt(0).getWidth(); 1929 | 1930 | int scrollY = getScrollY(); 1931 | mScroller.fling(getScrollX(), scrollY, velocityX, 0,0, Math.max(0, right - width), scrollY, scrollY,width/2,0); 1932 | ViewCompat.postInvalidateOnAnimation(this); 1933 | } 1934 | } 1935 | 1936 | private void flingWithNestedDispatchVertical(int velocityY) { 1937 | final int scrollY = getScrollY(); 1938 | final boolean canFling = (scrollY > 0 || velocityY > 0) 1939 | && (scrollY < getVerticalScrollRange() || velocityY < 0); 1940 | if (!dispatchNestedPreFling(0, velocityY)) { 1941 | dispatchNestedFling(0, velocityY, canFling); 1942 | if (canFling) { 1943 | flingVertical(velocityY); 1944 | } 1945 | } 1946 | } 1947 | private void flingWithNestedDispatchHorizontal(int velocityX) { 1948 | final int scrollX = getScrollX(); 1949 | final boolean canFling = (scrollX > 0 || velocityX > 0) 1950 | && (scrollX < getHorizontalScrollRange() || velocityX < 0); 1951 | if (!dispatchNestedPreFling(velocityX, 0)) { 1952 | dispatchNestedFling(velocityX, 0, canFling); 1953 | if (canFling) { 1954 | flingHorizontal(velocityX); 1955 | } 1956 | } 1957 | } 1958 | 1959 | private void endDrag() { 1960 | mIsBeingDragged = false; 1961 | 1962 | recycleVelocityTracker(); 1963 | 1964 | if (mEdgeGlowTop != null) { 1965 | mEdgeGlowLeft.onRelease(); 1966 | mEdgeGlowRight.onRelease(); 1967 | mEdgeGlowTop.onRelease(); 1968 | mEdgeGlowBottom.onRelease(); 1969 | } 1970 | } 1971 | 1972 | /** 1973 | * {@inheritDoc} 1974 | * 1975 | *

1976 | * This version also clamps the scrolling to the bounds of our child. 1977 | */ 1978 | @Override 1979 | public void scrollTo(int x, int y) { 1980 | // we rely on the fact the View.scrollBy calls scrollTo. 1981 | if (getChildCount() > 0) { 1982 | View child = getChildAt(0); 1983 | x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), 1984 | child.getWidth()); 1985 | y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), 1986 | child.getHeight()); 1987 | if (x != getScrollX() || y != getScrollY()) { 1988 | super.scrollTo(x, y); 1989 | } 1990 | } 1991 | } 1992 | 1993 | private void ensureGlows() { 1994 | if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) { 1995 | if (mEdgeGlowTop == null) { 1996 | Context context = getContext(); 1997 | mEdgeGlowTop = new EdgeEffectCompat(context); 1998 | mEdgeGlowBottom = new EdgeEffectCompat(context); 1999 | mEdgeGlowLeft = new EdgeEffectCompat(context); 2000 | mEdgeGlowRight = new EdgeEffectCompat(context); 2001 | } 2002 | } else { 2003 | mEdgeGlowTop = null; 2004 | mEdgeGlowBottom = null; 2005 | mEdgeGlowLeft = null; 2006 | mEdgeGlowRight = null; 2007 | } 2008 | } 2009 | @Override 2010 | public int getSolidColor() { 2011 | return Color.BLUE; 2012 | } 2013 | @Override 2014 | public void draw(Canvas canvas) { 2015 | super.draw(canvas); 2016 | if (mEdgeGlowTop != null) { 2017 | final int scrollX = getScrollX(); 2018 | final int scrollY = getScrollY(); 2019 | if (scrollDirection == DIRECTION_HORIZONTAL) { 2020 | // if (!mEdgeGlowLeft.isFinished()) { 2021 | // Log.i("NestedScrollView", "draw 水平 左侧"); 2022 | // final int restoreCount = canvas.save(); 2023 | // final int height = getHeight() - getPaddingTop() 2024 | // - getPaddingBottom(); 2025 | // 2026 | // canvas.translate(getPaddingLeft(), height); 2027 | // canvas.rotate(-90); 2028 | // mEdgeGlowLeft.setSize(getWidth(), height); 2029 | // if (mEdgeGlowLeft.draw(canvas)) { 2030 | // ViewCompat.postInvalidateOnAnimation(this); 2031 | // } 2032 | // canvas.restoreToCount(restoreCount); 2033 | // } 2034 | // if (!mEdgeGlowRight.isFinished()) { 2035 | // Log.i("NestedScrollView", "draw 水平 右侧"); 2036 | // final int restoreCount = canvas.save(); 2037 | // final int height = getHeight() - getPaddingTop() 2038 | // - getPaddingBottom(); 2039 | // 2040 | // canvas.translate(getPaddingLeft() + getWidth(),height); 2041 | // canvas.rotate(90); 2042 | // mEdgeGlowRight.setSize(getWidth(), height); 2043 | // if (mEdgeGlowRight.draw(canvas)) { 2044 | // ViewCompat.postInvalidateOnAnimation(this); 2045 | // } 2046 | // canvas.restoreToCount(restoreCount); 2047 | // } 2048 | 2049 | if (!mEdgeGlowLeft.isFinished()) { 2050 | final int restoreCount = canvas.save(); 2051 | final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 2052 | 2053 | canvas.rotate(270); 2054 | canvas.translate(-height + getPaddingTop() - scrollY, Math.min(0, scrollX)); 2055 | mEdgeGlowLeft.setSize(height, getWidth()); 2056 | if (mEdgeGlowLeft.draw(canvas)) { 2057 | invalidate(); 2058 | } 2059 | canvas.restoreToCount(restoreCount); 2060 | } 2061 | if (!mEdgeGlowRight.isFinished()) { 2062 | final int restoreCount = canvas.save(); 2063 | final int width = getWidth(); 2064 | final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 2065 | 2066 | canvas.rotate(90); 2067 | canvas.translate(-getPaddingTop() + scrollY, 2068 | -(Math.max(getHorizontalScrollRange(), scrollX) + width)); 2069 | mEdgeGlowRight.setSize(height, width); 2070 | if (mEdgeGlowRight.draw(canvas)) { 2071 | invalidate(); 2072 | } 2073 | canvas.restoreToCount(restoreCount); 2074 | } 2075 | }else if (scrollDirection == DIRECTION_VERTICAL) { 2076 | if (!mEdgeGlowTop.isFinished()) { 2077 | final int restoreCount = canvas.save(); 2078 | final int width = getWidth() - getPaddingLeft() 2079 | - getPaddingRight(); 2080 | 2081 | canvas.translate(scrollX + getPaddingLeft(), Math.min(0, scrollY)); 2082 | mEdgeGlowTop.setSize(width, getHeight()); 2083 | if (mEdgeGlowTop.draw(canvas)) { 2084 | ViewCompat.postInvalidateOnAnimation(this); 2085 | } 2086 | canvas.restoreToCount(restoreCount); 2087 | } 2088 | if (!mEdgeGlowBottom.isFinished()) { 2089 | final int restoreCount = canvas.save(); 2090 | final int width = getWidth() - getPaddingLeft() 2091 | - getPaddingRight(); 2092 | final int height = getHeight(); 2093 | 2094 | canvas.translate(scrollX - width + getPaddingLeft(), 2095 | Math.max(getVerticalScrollRange(), scrollY) + height); 2096 | canvas.rotate(180, width, 0); 2097 | mEdgeGlowBottom.setSize(width, height); 2098 | if (mEdgeGlowBottom.draw(canvas)) { 2099 | ViewCompat.postInvalidateOnAnimation(this); 2100 | } 2101 | canvas.restoreToCount(restoreCount); 2102 | } 2103 | }else { 2104 | //do nothing 2105 | } 2106 | 2107 | 2108 | } 2109 | } 2110 | 2111 | private static int clamp(int n, int my, int child) { 2112 | if (my >= child || n < 0) { 2113 | /* 2114 | * my >= child is this case: |--------------- me ---------------| 2115 | * |------ child ------| or |--------------- me ---------------| 2116 | * |------ child ------| or |--------------- me ---------------| 2117 | * |------ child ------| 2118 | * 2119 | * n < 0 is this case: |------ me ------| |-------- child --------| 2120 | * |-- mScrollX --| 2121 | */ 2122 | return 0; 2123 | } 2124 | if ((my + n) > child) { 2125 | /* 2126 | * this case: |------ me ------| |------ child ------| |-- mScrollX 2127 | * --| 2128 | */ 2129 | return child - my; 2130 | } 2131 | return n; 2132 | } 2133 | 2134 | @Override 2135 | protected void onRestoreInstanceState(Parcelable state) { 2136 | SavedState ss = (SavedState) state; 2137 | super.onRestoreInstanceState(ss.getSuperState()); 2138 | mSavedState = ss; 2139 | requestLayout(); 2140 | } 2141 | 2142 | @Override 2143 | protected Parcelable onSaveInstanceState() { 2144 | Parcelable superState = super.onSaveInstanceState(); 2145 | SavedState ss = new SavedState(superState); 2146 | ss.scrollPosition = getScrollY(); 2147 | return ss; 2148 | } 2149 | 2150 | static class SavedState extends BaseSavedState { 2151 | public int scrollPosition; 2152 | 2153 | SavedState(Parcelable superState) { 2154 | super(superState); 2155 | } 2156 | 2157 | public SavedState(Parcel source) { 2158 | super(source); 2159 | scrollPosition = source.readInt(); 2160 | } 2161 | 2162 | @Override 2163 | public void writeToParcel(Parcel dest, int flags) { 2164 | super.writeToParcel(dest, flags); 2165 | dest.writeInt(scrollPosition); 2166 | } 2167 | 2168 | @Override 2169 | public String toString() { 2170 | return "HorizontalScrollView.SavedState{" 2171 | + Integer.toHexString(System.identityHashCode(this)) 2172 | + " scrollPosition=" + scrollPosition + "}"; 2173 | } 2174 | 2175 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 2176 | public SavedState createFromParcel(Parcel in) { 2177 | return new SavedState(in); 2178 | } 2179 | 2180 | public SavedState[] newArray(int size) { 2181 | return new SavedState[size]; 2182 | } 2183 | }; 2184 | } 2185 | 2186 | static class AccessibilityDelegate extends AccessibilityDelegateCompat { 2187 | @Override 2188 | public boolean performAccessibilityAction(View host, int action, 2189 | Bundle arguments) { 2190 | if (super.performAccessibilityAction(host, action, arguments)) { 2191 | return true; 2192 | } 2193 | final TwoWayNestedScrollView nsvHost = (TwoWayNestedScrollView) host; 2194 | if (!nsvHost.isEnabled()) { 2195 | return false; 2196 | } 2197 | switch (action) { 2198 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { 2199 | final int viewportWidth = nsvHost.getWidth() 2200 | - nsvHost.getPaddingLeft() - nsvHost.getPaddingRight(); 2201 | final int targetScrollX = Math.min(nsvHost.getScrollX() 2202 | + viewportWidth, nsvHost.getHorizontalScrollRange()); 2203 | final int viewportHeight = nsvHost.getHeight() 2204 | - nsvHost.getPaddingBottom() - nsvHost.getPaddingTop(); 2205 | final int targetScrollY = Math.min(nsvHost.getScrollY() 2206 | + viewportHeight, nsvHost.getVerticalScrollRange()); 2207 | 2208 | if (targetScrollY != nsvHost.getScrollY()) { 2209 | nsvHost.smoothScrollTo(targetScrollX, targetScrollY); 2210 | return true; 2211 | } 2212 | } 2213 | return false; 2214 | case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { 2215 | final int viewportWidth = nsvHost.getWidth() 2216 | - nsvHost.getPaddingLeft() - nsvHost.getPaddingRight(); 2217 | final int targetScrollX = Math.max(nsvHost.getScrollY() 2218 | - viewportWidth, 0); 2219 | final int viewportHeight = nsvHost.getHeight() 2220 | - nsvHost.getPaddingBottom() - nsvHost.getPaddingTop(); 2221 | final int targetScrollY = Math.max(nsvHost.getScrollY() 2222 | - viewportHeight, 0); 2223 | if (targetScrollY != nsvHost.getScrollY()) { 2224 | nsvHost.smoothScrollTo(targetScrollX, targetScrollY); 2225 | return true; 2226 | } 2227 | } 2228 | return false; 2229 | } 2230 | return false; 2231 | } 2232 | 2233 | @Override 2234 | public void onInitializeAccessibilityNodeInfo(View host, 2235 | AccessibilityNodeInfoCompat info) { 2236 | super.onInitializeAccessibilityNodeInfo(host, info); 2237 | final TwoWayNestedScrollView nsvHost = (TwoWayNestedScrollView) host; 2238 | info.setClassName(ScrollView.class.getName()); 2239 | if (nsvHost.isEnabled()) { 2240 | final int verticalScrollRange = nsvHost.getVerticalScrollRange(); 2241 | final int horizontalScrollRange = nsvHost.getHorizontalScrollRange(); 2242 | if (verticalScrollRange > 0) { 2243 | info.setScrollable(true); 2244 | if (nsvHost.getScrollY() > 0) { 2245 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); 2246 | } 2247 | if (nsvHost.getScrollY() < verticalScrollRange) { 2248 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); 2249 | } 2250 | } 2251 | if (horizontalScrollRange > 0) { 2252 | if (nsvHost.getScrollX() > 0) { 2253 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); 2254 | } 2255 | if (nsvHost.getScrollX() < horizontalScrollRange) { 2256 | info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); 2257 | } 2258 | } 2259 | } 2260 | } 2261 | 2262 | @Override 2263 | public void onInitializeAccessibilityEvent(View host, 2264 | AccessibilityEvent event) { 2265 | super.onInitializeAccessibilityEvent(host, event); 2266 | final TwoWayNestedScrollView nsvHost = (TwoWayNestedScrollView) host; 2267 | event.setClassName(ScrollView.class.getName()); 2268 | final AccessibilityRecordCompat record = AccessibilityEventCompat 2269 | .asRecord(event); 2270 | final boolean scrollable = nsvHost.getVerticalScrollRange() > 0 || nsvHost.getHorizontalScrollRange() > 0; 2271 | record.setScrollable(scrollable); 2272 | record.setScrollX(nsvHost.getScrollX()); 2273 | record.setScrollY(nsvHost.getScrollY()); 2274 | record.setMaxScrollX(nsvHost.getHorizontalScrollRange()); 2275 | record.setMaxScrollY(nsvHost.getVerticalScrollRange()); 2276 | } 2277 | } 2278 | } 2279 | --------------------------------------------------------------------------------