├── .gitignore
├── LICENSE
├── README.md
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── library
├── .gitignore
├── build.gradle
├── gradle-mvn-push.gradle
├── proguard-rules.pro
├── publication.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── jrummyapps
│ └── android
│ └── widget
│ └── TwoDScrollView.java
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | ########### Specifies intentionally untracked files to ignore ###########
2 |
3 | ### Gradle
4 | .gradle/
5 | build/
6 |
7 | ### IntelliJ IDEA
8 | /.idea
9 | *.iml
10 | *.iws
11 | captures/
12 | .navigation/
13 | local.properties
14 | bin/
15 | gen/
16 | out/
17 | *.apk
18 | *.ap_
19 |
20 | ### Android
21 | *.jks
22 | *.dex
23 |
24 | ### Java
25 | *.class
26 | hs_err_pid*
27 |
28 | ### Windows
29 | Desktop.ini
30 | Thumbs.db
31 | ehthumbs.db
32 |
33 | ### OSX
34 | .DS_Store
35 |
36 | ### Linux
37 | *~
38 | .fuse_hidden*
39 | .directory
40 | .Trash-*
41 |
42 | ### Logs
43 | *.log
44 |
45 | ### Crashlytics
46 | crashlytics.properties
47 | fabric.properties
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2016 Jared Rummler
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TwoDScrollView
2 |
3 | [](https://maven-badges.herokuapp.com/maven-central/com.jaredrummler/twodscrollview) [](LICENSE.txt) [](https://android-arsenal.com/api?level=7)
4 |
5 | Two-dimensional scroll view for Android
6 |
7 | Download
8 | --------
9 |
10 | ```groovy
11 | compile 'com.jaredrummler:twodscrollview:{latest_version}'
12 | ```
13 |
14 | Or simply copy the [TwoDScrollView](https://raw.githubusercontent.com/jaredrummler/TwoDScrollView/master/library/src/main/java/com/jaredrummler/android/widget/TwoDScrollView.java) class intro your project, update the package declaration, and you are good to go.
15 |
16 | Acknowledgements
17 | ----------------
18 |
19 | TwoDScrollView was originally posted in 2010 by Matt Clark on http://GORGES.us (see [here](http://web.archive.org/web/20110625064025/http://blog.gorges.us/2010/06/android-two-dimensional-scrollview)).
20 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | jcenter()
4 | }
5 | dependencies {
6 | classpath 'com.android.tools.build:gradle:2.1.2'
7 | classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4.1.1"
8 | }
9 | }
10 |
11 | allprojects {
12 | repositories {
13 | jcenter()
14 | }
15 | }
16 |
17 | task clean(type: Delete) {
18 | delete rootProject.buildDir
19 | }
20 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2016 Jared Rummler
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 | #
17 |
18 | VERSION_NAME=1.0.1
19 | VERSION_CODE=1
20 | GROUP=com.jaredrummler
21 |
22 | POM_NAME=TwoDScrollView
23 | POM_ARTIFACT_ID=twodscrollview
24 | POM_PACKAGING=aar
25 |
26 | POM_DESCRIPTION=two-dimensional scroll view
27 | POM_URL=https://github.com/jaredrummler/TwoDScrollView
28 | POM_SCM_URL=https://github.com/jaredrummler/TwoDScrollView
29 | POM_SCM_CONNECTION=scm:git@github.com:jaredrummler/TwoDScrollView.git
30 | POM_SCM_DEV_CONNECTION=scm:git@github.com:jaredrummler/TwoDScrollView.git
31 | POM_LICENCE_NAME=The Apache Software License, Version 2.0
32 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
33 | POM_LICENCE_DIST=repo
34 | POM_DEVELOPER_ID=jaredrummler
35 | POM_DEVELOPER_NAME=Jared Rummler
36 |
37 | SNAPSHOT_REPOSITORY_URL=https://oss.sonatype.org/content/repositories/snapshots
38 | RELEASE_REPOSITORY_URL=https://oss.sonatype.org/service/local/staging/deploy/maven2
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredrummler/TwoDScrollView/1a02477a0865193d67175adb0fc89a5df1e9e2ad/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Oct 21 11:34:03 PDT 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 24
5 | buildToolsVersion "24.0.3"
6 |
7 | defaultConfig {
8 | minSdkVersion 7
9 | targetSdkVersion 24
10 | }
11 | }
12 |
13 | //apply from: 'publication.gradle'
14 | //apply from: 'gradle-mvn-push.gradle'
15 |
16 | //afterEvaluate {
17 | // androidJavadocs.classpath += project.android.libraryVariants.toList().first().javaCompile.classpath
18 | //}
--------------------------------------------------------------------------------
/library/gradle-mvn-push.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven'
2 | apply plugin: 'signing'
3 |
4 | def isReleaseBuild() {
5 | return VERSION_NAME.contains("SNAPSHOT") == false
6 | }
7 |
8 | def getReleaseRepositoryUrl() {
9 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
10 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
11 | }
12 |
13 | def getSnapshotRepositoryUrl() {
14 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
15 | : "https://oss.sonatype.org/content/repositories/snapshots/"
16 | }
17 |
18 | def getRepositoryUsername() {
19 | return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : ""
20 | }
21 |
22 | def getRepositoryPassword() {
23 | return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : ""
24 | }
25 |
26 | afterEvaluate { project ->
27 | uploadArchives {
28 | repositories {
29 | mavenDeployer {
30 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
31 |
32 | pom.groupId = GROUP
33 | pom.artifactId = POM_ARTIFACT_ID
34 | pom.version = VERSION_NAME
35 |
36 | repository(url: getReleaseRepositoryUrl()) {
37 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
38 | }
39 | snapshotRepository(url: getSnapshotRepositoryUrl()) {
40 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
41 | }
42 |
43 | pom.project {
44 | name POM_NAME
45 | packaging POM_PACKAGING
46 | description POM_DESCRIPTION
47 | url POM_URL
48 |
49 | scm {
50 | url POM_SCM_URL
51 | connection POM_SCM_CONNECTION
52 | developerConnection POM_SCM_DEV_CONNECTION
53 | }
54 |
55 | licenses {
56 | license {
57 | name POM_LICENCE_NAME
58 | url POM_LICENCE_URL
59 | distribution POM_LICENCE_DIST
60 | }
61 | }
62 |
63 | developers {
64 | developer {
65 | id POM_DEVELOPER_ID
66 | name POM_DEVELOPER_NAME
67 | }
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
74 | signing {
75 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
76 | sign configurations.archives
77 | }
78 |
79 | task androidJavadocs(type: Javadoc) {
80 | failOnError = false
81 | source = android.sourceSets.main.java.srcDirs
82 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
83 | }
84 |
85 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
86 | classifier = 'javadoc'
87 | from androidJavadocs.destinationDir
88 | }
89 |
90 | task androidSourcesJar(type: Jar) {
91 | classifier = 'sources'
92 | from android.sourceSets.main.java.sourceFiles
93 | }
94 |
95 | artifacts {
96 | archives androidSourcesJar
97 | archives androidJavadocsJar
98 | }
99 | }
--------------------------------------------------------------------------------
/library/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in C:\android-sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/library/publication.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.jfrog.artifactory'
2 | apply plugin: 'maven-publish'
3 |
4 | def libraryGroupId = 'com.jrummyapps'
5 | def libraryArtifactId = 'twodscrollview'
6 | def libraryVersion = '1.0.0'
7 |
8 | publishing {
9 | publications {
10 | aar(MavenPublication) {
11 | groupId libraryGroupId
12 | version libraryVersion
13 | artifactId libraryArtifactId
14 |
15 | artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
16 | artifact androidJavadocsJar
17 | artifact androidSourcesJar
18 | pom.withXml {
19 | def dependencies = asNode().appendNode('dependencies')
20 | configurations.getByName("_releaseCompile").getResolvedConfiguration().getFirstLevelModuleDependencies().each {
21 | def dependency = dependencies.appendNode('dependency')
22 | dependency.appendNode('groupId', it.moduleGroup)
23 | dependency.appendNode('artifactId', it.moduleName)
24 | dependency.appendNode('version', it.moduleVersion)
25 | }
26 | }
27 | }
28 | }
29 | }
30 |
31 | artifactory {
32 | contextUrl = "${artifactory_context_url}"
33 | publish {
34 | repository {
35 | repoKey = 'libs-release-local'
36 |
37 | username = "${artifactory_username}"
38 | password = "${artifactory_password}"
39 | }
40 | defaults {
41 | publications('aar')
42 | publishArtifacts = true
43 |
44 | properties = ['qa.level': 'basic', 'q.os': 'android', 'dev.team': 'core']
45 | publishPom = true
46 | }
47 | }
48 | }
49 |
50 | task androidJavadocs(type: Javadoc) {
51 | source = android.sourceSets.main.java.srcDirs
52 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
53 | if (JavaVersion.current().isJava8Compatible()) {
54 | options.addStringOption('Xdoclint:none', '-quiet')
55 | }
56 | failOnError false
57 | }
58 |
59 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
60 | classifier = 'javadoc'
61 | from androidJavadocs.destinationDir
62 | }
63 |
64 | task androidSourcesJar(type: Jar) {
65 | classifier = 'sources'
66 | from android.sourceSets.main.java.srcDirs
67 | }
68 |
69 | artifactoryPublish.dependsOn('clean',
70 | 'assembleRelease',
71 | 'generatePomFileForAarPublication',
72 | 'androidSourcesJar',
73 | 'androidJavadocsJar',
74 | 'androidJavadocs')
75 |
76 | artifactoryPublish.mustRunAfter('clean')
77 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/library/src/main/java/com/jrummyapps/android/widget/TwoDScrollView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Jared Rummler
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 | */
17 |
18 | package com.jrummyapps.android.widget;
19 |
20 | import android.content.Context;
21 | import android.graphics.Rect;
22 | import android.util.AttributeSet;
23 | import android.view.FocusFinder;
24 | import android.view.KeyEvent;
25 | import android.view.MotionEvent;
26 | import android.view.VelocityTracker;
27 | import android.view.View;
28 | import android.view.ViewConfiguration;
29 | import android.view.ViewDebug;
30 | import android.view.ViewGroup;
31 | import android.view.ViewParent;
32 | import android.view.animation.AnimationUtils;
33 | import android.widget.FrameLayout;
34 | import android.widget.LinearLayout;
35 | import android.widget.Scroller;
36 | import android.widget.TextView;
37 |
38 | import java.util.List;
39 |
40 | /**
41 | * Layout container for a view hierarchy that can be scrolled by the user, allowing it to be larger than the
42 | * physical display. A TwoDScrollView is a {@link FrameLayout}, meaning you should place one child in it containing
43 | * the entire contents to scroll; this child may itself be a layout manager with a complex hierarchy of objects. A
44 | * child that is often used is a {@link LinearLayout} in a vertical orientation, presenting a vertical array of
45 | * top-level items that the user can scroll through.
46 | *
47 | * The {@link TextView} class also takes care of its own scrolling, so does not require a TwoDScrollView, but
48 | * using the two together is possible to achieve the effect of a text view within a larger container.
49 | *
50 | * Original source code from Matt Clark at:
51 | * http://web.archive.org/web/20110625064025/http://blog.gorges.us/2010/06/android-two-dimensional-scrollview
52 | */
53 | public class TwoDScrollView extends FrameLayout {
54 |
55 | static final int ANIMATED_SCROLL_GAP = 250;
56 | static final float MAX_SCROLL_FACTOR = 0.5f;
57 |
58 | private final Rect tempRect = new Rect();
59 |
60 | private long lastScroll;
61 |
62 | private Scroller scroller;
63 |
64 | private ScrollChangeListener scrollChangeListener;
65 |
66 | /**
67 | * When set to true, the scroll view measure its child to make it fill the currently visible
68 | * area.
69 | */
70 | @ViewDebug.ExportedProperty(category = "layout")
71 | private boolean fillViewport;
72 |
73 | /**
74 | * Flag to indicate that we are moving focus ourselves. This is so the code that watches for
75 | * focus changes initiated outside this TwoDScrollView knows that it does not have to do
76 | * anything.
77 | */
78 | private boolean twoDScrollViewMovedFocus;
79 |
80 | /**
81 | * Position of the last motion event.
82 | */
83 | private float lastMotionY;
84 |
85 | private float lastMotionX;
86 |
87 | /**
88 | * True when the layout has changed but the traversal has not come through yet. Ideally the view
89 | * hierarchy would keep track of this for us.
90 | */
91 | private boolean isLayoutDirty = true;
92 |
93 | /**
94 | * The child to give focus to in the event that a child has requested focus while the layout is
95 | * dirty. This prevents the scroll from being wrong if the child has not been laid out before
96 | * requesting focus.
97 | */
98 | private View childToScrollTo = null;
99 |
100 | /**
101 | * True if the user is currently dragging this TwoDScrollView around. This is not the same as
102 | * 'is being flinged', which can be checked by scroller.isFinished() (flinging begins when the
103 | * user lifts his finger).
104 | */
105 | private boolean isBeingDragged = false;
106 |
107 | /**
108 | * Determines speed during touch scrolling
109 | */
110 | private VelocityTracker velocityTracker;
111 |
112 | /**
113 | * Whether arrow scrolling is animated.
114 | */
115 | private int touchSlop;
116 |
117 | private int minimumVelocity;
118 |
119 | private int maximumVelocity;
120 |
121 | public TwoDScrollView(Context context) {
122 | this(context, null);
123 | }
124 |
125 | public TwoDScrollView(Context context, AttributeSet attrs) {
126 | super(context, attrs);
127 | initTwoDScrollView(context, attrs);
128 | }
129 |
130 | public TwoDScrollView(Context context, AttributeSet attrs, int defStyle) {
131 | super(context, attrs, defStyle);
132 | initTwoDScrollView(context, attrs);
133 | }
134 |
135 | public void setScrollChangeListner(ScrollChangeListener listener) {
136 | scrollChangeListener = listener;
137 | }
138 |
139 | /**
140 | * Indicates whether this ScrollView's content is stretched to fill the viewport.
141 | *
142 | * @return True if the content fills the viewport, false otherwise.
143 | * @attr ref android.R.styleable#ScrollView_fillViewport
144 | */
145 | public boolean isFillViewport() {
146 | return fillViewport;
147 | }
148 |
149 | /**
150 | * Indicates this ScrollView whether it should stretch its content height to fill the viewport or not.
151 | *
152 | * @param fillViewport
153 | * True to stretch the content's height to the viewport's boundaries, false otherwise.
154 | * @attr ref android.R.styleable#ScrollView_fillViewport
155 | */
156 | public void setFillViewport(boolean fillViewport) {
157 | if (fillViewport != this.fillViewport) {
158 | this.fillViewport = fillViewport;
159 | requestLayout();
160 | }
161 | }
162 |
163 | @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
164 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
165 | if (!fillViewport) {
166 | return;
167 | }
168 | int heightMode = MeasureSpec.getMode(heightMeasureSpec);
169 | if (heightMode == MeasureSpec.UNSPECIFIED) {
170 | return;
171 | }
172 | if (getChildCount() > 0) {
173 | View child = getChildAt(0);
174 | int height = getMeasuredHeight();
175 | if (child.getMeasuredHeight() < height) {
176 | LayoutParams lp = (LayoutParams) child.getLayoutParams();
177 | int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft()
178 | + getPaddingRight(), lp.width);
179 | height -= getPaddingTop();
180 | height -= getPaddingBottom();
181 | int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
182 | MeasureSpec.EXACTLY);
183 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
184 | }
185 | }
186 | }
187 |
188 | @Override protected void onLayout(boolean changed, int l, int t, int r, int b) {
189 | super.onLayout(changed, l, t, r, b);
190 | isLayoutDirty = false;
191 | // Give a child focus if it needs it
192 | if (childToScrollTo != null && isViewDescendantOf(childToScrollTo, this)) {
193 | scrollToChild(childToScrollTo);
194 | }
195 | childToScrollTo = null;
196 |
197 | // Calling this with the present values causes it to re-clam them
198 | scrollTo(getScrollX(), getScrollY());
199 | }
200 |
201 | /**
202 | * @return The maximum amount this scroll view will scroll in response to an arrow event.
203 | */
204 | public int getMaxScrollAmountVertical() {
205 | return (int) (MAX_SCROLL_FACTOR * getHeight());
206 | }
207 |
208 | public int getMaxScrollAmountHorizontal() {
209 | return (int) (MAX_SCROLL_FACTOR * getWidth());
210 | }
211 |
212 | private void initTwoDScrollView(Context context, AttributeSet attrs) {
213 | scroller = new Scroller(getContext());
214 | setFocusable(true);
215 | setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
216 | setWillNotDraw(false);
217 | ViewConfiguration configuration = ViewConfiguration.get(getContext());
218 | touchSlop = configuration.getScaledTouchSlop();
219 | minimumVelocity = configuration.getScaledMinimumFlingVelocity();
220 | maximumVelocity = configuration.getScaledMaximumFlingVelocity();
221 | }
222 |
223 | /**
224 | * @return Returns true this TwoDScrollView can be scrolled
225 | */
226 | private boolean canScroll() {
227 | View child = getChildAt(0);
228 | if (child != null) {
229 | int childHeight = child.getHeight();
230 | int childWidth = child.getWidth();
231 | return (getHeight() < childHeight + getPaddingTop() + getPaddingBottom())
232 | || (getWidth() < childWidth + getPaddingLeft() + getPaddingRight());
233 | }
234 | return false;
235 | }
236 |
237 | /**
238 | * You can call this function yourself to have the scroll view perform scrolling from a key
239 | * event, just as if the event had been dispatched to it by the view hierarchy.
240 | *
241 | * @param event
242 | * The key event to execute.
243 | * @return Return true if the event was handled, else false.
244 | */
245 | public boolean executeKeyEvent(KeyEvent event) {
246 | tempRect.setEmpty();
247 | if (!canScroll()) {
248 | if (isFocused()) {
249 | View currentFocused = findFocus();
250 | if (currentFocused == this) {
251 | currentFocused = null;
252 | }
253 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,
254 | View.FOCUS_DOWN);
255 | return nextFocused != null && nextFocused != this
256 | && nextFocused.requestFocus(View.FOCUS_DOWN);
257 | }
258 | return false;
259 | }
260 | boolean handled = false;
261 | if (event.getAction() == KeyEvent.ACTION_DOWN) {
262 | switch (event.getKeyCode()) {
263 | case KeyEvent.KEYCODE_DPAD_UP:
264 | if (!event.isAltPressed()) {
265 | handled = arrowScroll(View.FOCUS_UP, false);
266 | } else {
267 | handled = fullScroll(View.FOCUS_UP, false);
268 | }
269 | break;
270 | case KeyEvent.KEYCODE_DPAD_DOWN:
271 | if (!event.isAltPressed()) {
272 | handled = arrowScroll(View.FOCUS_DOWN, false);
273 | } else {
274 | handled = fullScroll(View.FOCUS_DOWN, false);
275 | }
276 | break;
277 | case KeyEvent.KEYCODE_DPAD_LEFT:
278 | if (!event.isAltPressed()) {
279 | handled = arrowScroll(View.FOCUS_LEFT, true);
280 | } else {
281 | handled = fullScroll(View.FOCUS_LEFT, true);
282 | }
283 | break;
284 | case KeyEvent.KEYCODE_DPAD_RIGHT:
285 | if (!event.isAltPressed()) {
286 | handled = arrowScroll(View.FOCUS_RIGHT, true);
287 | } else {
288 | handled = fullScroll(View.FOCUS_RIGHT, true);
289 | }
290 | break;
291 | }
292 | }
293 | return handled;
294 | }
295 |
296 | @Override public boolean onTouchEvent(MotionEvent ev) {
297 |
298 | if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
299 | // Don't handle edge touches immediately -- they may actually belong to one of our
300 | // descendants.
301 | return false;
302 | }
303 |
304 | if (!canScroll()) {
305 | return false;
306 | }
307 |
308 | if (velocityTracker == null) {
309 | velocityTracker = VelocityTracker.obtain();
310 | }
311 | velocityTracker.addMovement(ev);
312 |
313 | int action = ev.getAction();
314 | float y = ev.getY();
315 | float x = ev.getX();
316 |
317 | switch (action) {
318 | case MotionEvent.ACTION_DOWN:
319 |
320 | // If being flinged and user touches, stop the fling.
321 | // isFinished will be false if being flinged.
322 |
323 | if (!scroller.isFinished()) {
324 | scroller.abortAnimation();
325 | }
326 |
327 | // Remember where the motion event started
328 | lastMotionY = y;
329 | lastMotionX = x;
330 | break;
331 | case MotionEvent.ACTION_MOVE:
332 | // Scroll to follow the motion event
333 | int deltaX = (int) (lastMotionX - x);
334 | int deltaY = (int) (lastMotionY - y);
335 | lastMotionX = x;
336 | lastMotionY = y;
337 |
338 | if (deltaX < 0) {
339 | if (getScrollX() < 0) {
340 | deltaX = 0;
341 | }
342 | } else if (deltaX > 0) {
343 | int rightEdge = getWidth() - getPaddingRight();
344 | int availableToScroll = getChildAt(0).getRight() - getScrollX() - rightEdge;
345 | if (availableToScroll > 0) {
346 | deltaX = Math.min(availableToScroll, deltaX);
347 | } else {
348 | deltaX = 0;
349 | }
350 | }
351 | if (deltaY < 0) {
352 | if (getScrollY() < 0) {
353 | deltaY = 0;
354 | }
355 | } else if (deltaY > 0) {
356 | int bottomEdge = getHeight() - getPaddingBottom();
357 | int availableToScroll = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
358 | if (availableToScroll > 0) {
359 | deltaY = Math.min(availableToScroll, deltaY);
360 | } else {
361 | deltaY = 0;
362 | }
363 | }
364 | if (deltaY != 0 || deltaX != 0) {
365 | scrollBy(deltaX, deltaY);
366 | }
367 | break;
368 | case MotionEvent.ACTION_UP:
369 | VelocityTracker velocityTracker = this.velocityTracker;
370 | velocityTracker.computeCurrentVelocity(1000, maximumVelocity);
371 | int initialXVelocity = (int) velocityTracker.getXVelocity();
372 | int initialYVelocity = (int) velocityTracker.getYVelocity();
373 | if ((Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > minimumVelocity)
374 | && getChildCount() > 0) {
375 | fling(-initialXVelocity, -initialYVelocity);
376 | }
377 | if (this.velocityTracker != null) {
378 | this.velocityTracker.recycle();
379 | this.velocityTracker = null;
380 | }
381 | }
382 | return true;
383 | }
384 |
385 | @Override protected void onScrollChanged(int x, int y, int oldx, int oldy) {
386 | super.onScrollChanged(x, y, oldx, oldy);
387 | if (scrollChangeListener != null) {
388 | scrollChangeListener.onScrollChanged(this, x, y, oldx, oldy);
389 | }
390 | }
391 |
392 | @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {
393 | super.onSizeChanged(w, h, oldw, oldh);
394 |
395 | View currentFocused = findFocus();
396 | if (null == currentFocused || this == currentFocused) {
397 | return;
398 | }
399 |
400 | // If the currently-focused view was visible on the screen when the
401 | // screen was at the old height, then scroll the screen to make that
402 | // view visible with the new screen height.
403 | currentFocused.getDrawingRect(tempRect);
404 | offsetDescendantRectToMyCoords(currentFocused, tempRect);
405 | int scrollDeltaX = computeScrollDeltaToGetChildRectOnScreen(tempRect);
406 | int scrollDeltaY = computeScrollDeltaToGetChildRectOnScreen(tempRect);
407 | doScroll(scrollDeltaX, scrollDeltaY);
408 | }
409 |
410 | /**
411 | * {@inheritDoc}
412 | *
413 | * This version also clamps the scrolling to the bounds of our child.
414 | */
415 | @Override
416 | public void scrollTo(int x, int y) {
417 | // we rely on the fact the View.scrollBy calls scrollTo.
418 | if (getChildCount() > 0) {
419 | View child = getChildAt(0);
420 | x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
421 | y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
422 | if (x != getScrollX() || y != getScrollY()) {
423 | super.scrollTo(x, y);
424 | }
425 | }
426 | }
427 |
428 | @Override public void computeScroll() {
429 | if (scroller.computeScrollOffset()) {
430 | // This is called at drawing time by ViewGroup. We don't want to
431 | // re-show the scrollbars at this point, which scrollTo will do,
432 | // so we replicate most of scrollTo here.
433 | //
434 | // It's a little odd to call onScrollChanged from inside the drawing.
435 | //
436 | // It is, except when you remember that computeScroll() is used to
437 | // animate scrolling. So unless we want to defer the onScrollChanged()
438 | // until the end of the animated scrolling, we don't really have a
439 | // choice here.
440 | //
441 | // I agree. The alternative, which I think would be worse, is to post
442 | // something and tell the subclasses later. This is bad because there
443 | // will be a window where mScrollX/Y is different from what the app
444 | // thinks it is.
445 | //
446 | int oldX = getScrollX();
447 | int oldY = getScrollY();
448 | int x = scroller.getCurrX();
449 | int y = scroller.getCurrY();
450 | if (getChildCount() > 0) {
451 | View child = getChildAt(0);
452 | scrollTo(
453 | clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(),
454 | child.getWidth()),
455 | clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(),
456 | child.getHeight()));
457 | } else {
458 | scrollTo(x, y);
459 | }
460 | if (oldX != getScrollX() || oldY != getScrollY()) {
461 | onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
462 | }
463 |
464 | // Keep on drawing until the animation has finished.
465 | postInvalidate();
466 | }
467 | }
468 |
469 | @Override protected float getTopFadingEdgeStrength() {
470 | if (getChildCount() == 0) {
471 | return 0.0f;
472 | }
473 | int length = getVerticalFadingEdgeLength();
474 | if (getScrollY() < length) {
475 | return getScrollY() / (float) length;
476 | }
477 | return 1.0f;
478 | }
479 |
480 | @Override protected float getBottomFadingEdgeStrength() {
481 | if (getChildCount() == 0) {
482 | return 0.0f;
483 | }
484 | int length = getVerticalFadingEdgeLength();
485 | int bottomEdge = getHeight() - getPaddingBottom();
486 | int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
487 | if (span < length) {
488 | return span / (float) length;
489 | }
490 | return 1.0f;
491 | }
492 |
493 | @Override protected float getLeftFadingEdgeStrength() {
494 | if (getChildCount() == 0) {
495 | return 0.0f;
496 | }
497 | int length = getHorizontalFadingEdgeLength();
498 | if (getScrollX() < length) {
499 | return getScrollX() / (float) length;
500 | }
501 | return 1.0f;
502 | }
503 |
504 | @Override protected float getRightFadingEdgeStrength() {
505 | if (getChildCount() == 0) {
506 | return 0.0f;
507 | }
508 | int length = getHorizontalFadingEdgeLength();
509 | int rightEdge = getWidth() - getPaddingRight();
510 | int span = getChildAt(0).getRight() - getScrollX() - rightEdge;
511 | if (span < length) {
512 | return span / (float) length;
513 | }
514 | return 1.0f;
515 | }
516 |
517 | @Override protected int computeHorizontalScrollRange() {
518 | int count = getChildCount();
519 | return count == 0 ? getWidth() : (getChildAt(0)).getRight();
520 | }
521 |
522 | /**
523 | * The scroll range of a scroll view is the overall height of all of its children.
524 | */
525 | @Override protected int computeVerticalScrollRange() {
526 | int count = getChildCount();
527 | return count == 0 ? getHeight() : (getChildAt(0)).getBottom();
528 | }
529 |
530 | @Override public void requestLayout() {
531 | isLayoutDirty = true;
532 | super.requestLayout();
533 | }
534 |
535 | /**
536 | * Finds the next focusable component that fits in this View's bounds (excluding fading edges)
537 | * pretending that this View's top is located at the parameter top.
538 | *
539 | * @param topFocus
540 | * look for a candidate is the one at the top of the bounds if topFocus is true, or at
541 | * the bottom of the bounds if topFocus is false
542 | * @param top
543 | * the top offset of the bounds in which a focusable must be found (the fading edge is
544 | * assumed to start at this position)
545 | * @param preferredFocusable
546 | * the View that has highest priority and will be returned if it is within my bounds
547 | * (null is valid)
548 | * @return the next focusable component in the bounds or null if none can be found
549 | */
550 | private View findFocusableViewInMyBounds(boolean topFocus, int top, boolean leftFocus,
551 | int left, View preferredFocusable) {
552 | // The fading edge's transparent side should be considered for focus since it's mostly
553 | // visible, so we divide the actual fading edge length by 2.
554 |
555 | int verticalFadingEdgeLength = getVerticalFadingEdgeLength() / 2;
556 | int topWithoutFadingEdge = top + verticalFadingEdgeLength;
557 | int bottomWithoutFadingEdge = top + getHeight() - verticalFadingEdgeLength;
558 | int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
559 | int leftWithoutFadingEdge = left + horizontalFadingEdgeLength;
560 | int rightWithoutFadingEdge = left + getWidth() - horizontalFadingEdgeLength;
561 |
562 | if ((preferredFocusable != null) && (preferredFocusable.getTop() < bottomWithoutFadingEdge)
563 | && (preferredFocusable.getBottom() > topWithoutFadingEdge)
564 | && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
565 | && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
566 | return preferredFocusable;
567 | }
568 | return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge,
569 | leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge);
570 | }
571 |
572 | /**
573 | * Finds the next focusable component that fits in the specified bounds.
574 | *
575 | * @param topFocus
576 | * look for a candidate is the one at the top of the bounds if topFocus is true, or at
577 | * the bottom of the bounds if topFocus is false
578 | * @param top
579 | * the top offset of the bounds in which a focusable must be found
580 | * @param bottom
581 | * the bottom offset of the bounds in which a focusable must be found
582 | * @return the next focusable component in the bounds or null if none can be found
583 | */
584 | private View findFocusableViewInBounds(boolean topFocus, int top, int bottom,
585 | boolean leftFocus, int left, int right) {
586 | List focusables = getFocusables(View.FOCUS_FORWARD);
587 | View focusCandidate = null;
588 |
589 | // A fully contained focusable is one where its top is below the bound's top, and its bottom
590 | // is above the bound's bottom. A partially contained focusable is one where some part of it
591 | // is within the bounds, but it also has some part that is not within bounds. A fully
592 | // contained focusable is preferred to a partially contained focusable.
593 |
594 | boolean foundFullyContainedFocusable = false;
595 |
596 | int count = focusables.size();
597 | for (int i = 0; i < count; i++) {
598 | View view = focusables.get(i);
599 | int viewTop = view.getTop();
600 | int viewBottom = view.getBottom();
601 | int viewLeft = view.getLeft();
602 | int viewRight = view.getRight();
603 |
604 | if (top < viewBottom && viewTop < bottom && left < viewRight && viewLeft < right) {
605 | // the focusable is in the target area, it is a candidate for focusing
606 | boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom)
607 | && (left < viewLeft) && (viewRight < right);
608 | if (focusCandidate == null) {
609 | // No candidate, take this one
610 | focusCandidate = view;
611 | foundFullyContainedFocusable = viewIsFullyContained;
612 | } else {
613 | boolean viewIsCloserToVerticalBoundary = (topFocus && viewTop < focusCandidate
614 | .getTop()) || (!topFocus && viewBottom > focusCandidate.getBottom());
615 | boolean viewIsCloserToHorizontalBoundary =
616 | (leftFocus && viewLeft < focusCandidate
617 | .getLeft()) ||
618 | (!leftFocus && viewRight > focusCandidate.getRight());
619 | if (foundFullyContainedFocusable) {
620 | if (viewIsFullyContained && viewIsCloserToVerticalBoundary
621 | && viewIsCloserToHorizontalBoundary) {
622 | // We're dealing with only fully contained views, so it has to be closer to the
623 | // boundary to beat our candidate
624 | focusCandidate = view;
625 | }
626 | } else {
627 | if (viewIsFullyContained) {
628 | // Any fully contained view beats a partially contained view */
629 | focusCandidate = view;
630 | foundFullyContainedFocusable = true;
631 | } else if (viewIsCloserToVerticalBoundary
632 | && viewIsCloserToHorizontalBoundary) {
633 | // Partially contained view beats another partially contained view if it's closer
634 | focusCandidate = view;
635 | }
636 | }
637 | }
638 | }
639 | }
640 | return focusCandidate;
641 | }
642 |
643 | /**
644 | * Handles scrolling in response to a "home/end" shortcut press. This method will scroll the
645 | * view to the top or bottom and give the focus to the topmost/bottommost component in the new
646 | * visible area. If no component is a good candidate for focus, this scrollview reclaims the
647 | * focus.
648 | *
649 | * @param direction
650 | * the scroll direction: {@link View#FOCUS_UP} to go the top of the view or
651 | * {@link View#FOCUS_DOWN} to go the bottom
652 | * @return true if the key event is consumed by this method, false otherwise
653 | */
654 | public boolean fullScroll(int direction, boolean horizontal) {
655 | if (!horizontal) {
656 | boolean down = direction == View.FOCUS_DOWN;
657 | int height = getHeight();
658 | tempRect.top = 0;
659 | tempRect.bottom = height;
660 | if (down) {
661 | int count = getChildCount();
662 | if (count > 0) {
663 | View view = getChildAt(count - 1);
664 | tempRect.bottom = view.getBottom();
665 | tempRect.top = tempRect.bottom - height;
666 | }
667 | }
668 | return scrollAndFocus(direction, tempRect.top, tempRect.bottom, 0, 0, 0);
669 | } else {
670 | boolean right = direction == View.FOCUS_DOWN;
671 | int width = getWidth();
672 | tempRect.left = 0;
673 | tempRect.right = width;
674 | if (right) {
675 | int count = getChildCount();
676 | if (count > 0) {
677 | View view = getChildAt(count - 1);
678 | tempRect.right = view.getBottom();
679 | tempRect.left = tempRect.right - width;
680 | }
681 | }
682 | return scrollAndFocus(0, 0, 0, direction, tempRect.top, tempRect.bottom);
683 | }
684 | }
685 |
686 | /**
687 | * Scrolls the view to make the area defined by top
and bottom
688 | * visible. This method attempts to give the focus to a component visible in this area. If no
689 | * component can be focused in the new visible area, the focus is reclaimed by this scrollview.
690 | *
691 | *
692 | * @param directionY
693 | * the scroll direction: {@link View#FOCUS_UP} to go upward {@link
694 | * View#FOCUS_DOWN} to downward
695 | * @param top
696 | * the top offset of the new area to be made visible
697 | * @param bottom
698 | * the bottom offset of the new area to be made visible
699 | * @return true if the key event is consumed by this method, false otherwise
700 | */
701 | private boolean scrollAndFocus(int directionY, int top, int bottom, int directionX, int left,
702 | int right) {
703 | boolean handled = true;
704 | int height = getHeight();
705 | int containerTop = getScrollY();
706 | int containerBottom = containerTop + height;
707 | boolean up = directionY == View.FOCUS_UP;
708 | int width = getWidth();
709 | int containerLeft = getScrollX();
710 | int containerRight = containerLeft + width;
711 | boolean leftwards = directionX == View.FOCUS_UP;
712 | View newFocused = findFocusableViewInBounds(up, top, bottom, leftwards, left, right);
713 | if (newFocused == null) {
714 | newFocused = this;
715 | }
716 | if ((top >= containerTop && bottom <= containerBottom)
717 | || (left >= containerLeft && right <= containerRight)) {
718 | handled = false;
719 | } else {
720 | int deltaY = up ? (top - containerTop) : (bottom - containerBottom);
721 | int deltaX = leftwards ? (left - containerLeft) : (right - containerRight);
722 | doScroll(deltaX, deltaY);
723 | }
724 | if (newFocused != findFocus() && newFocused.requestFocus(directionY)) {
725 | twoDScrollViewMovedFocus = true;
726 | twoDScrollViewMovedFocus = false;
727 | }
728 | return handled;
729 | }
730 |
731 | /**
732 | * Handle scrolling in response to an up or down arrow click.
733 | *
734 | * @param direction
735 | * The direction corresponding to the arrow key that was pressed
736 | * @return True if we consumed the event, false otherwise
737 | */
738 | public boolean arrowScroll(int direction, boolean horizontal) {
739 | View currentFocused = findFocus();
740 | if (currentFocused == this) {
741 | currentFocused = null;
742 | }
743 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
744 | int maxJump = horizontal ? getMaxScrollAmountHorizontal() : getMaxScrollAmountVertical();
745 |
746 | if (!horizontal) {
747 | if (nextFocused != null) {
748 | nextFocused.getDrawingRect(tempRect);
749 | offsetDescendantRectToMyCoords(nextFocused, tempRect);
750 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(tempRect);
751 | doScroll(0, scrollDelta);
752 | nextFocused.requestFocus(direction);
753 | } else {
754 | // no new focus
755 | int scrollDelta = maxJump;
756 | if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
757 | scrollDelta = getScrollY();
758 | } else if (direction == View.FOCUS_DOWN) {
759 | if (getChildCount() > 0) {
760 | int daBottom = getChildAt(0).getBottom();
761 | int screenBottom = getScrollY() + getHeight();
762 | if (daBottom - screenBottom < maxJump) {
763 | scrollDelta = daBottom - screenBottom;
764 | }
765 | }
766 | }
767 | if (scrollDelta == 0) {
768 | return false;
769 | }
770 | doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
771 | }
772 | } else {
773 | if (nextFocused != null) {
774 | nextFocused.getDrawingRect(tempRect);
775 | offsetDescendantRectToMyCoords(nextFocused, tempRect);
776 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(tempRect);
777 | doScroll(scrollDelta, 0);
778 | nextFocused.requestFocus(direction);
779 | } else {
780 | // no new focus
781 | int scrollDelta = maxJump;
782 | if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
783 | scrollDelta = getScrollY();
784 | } else if (direction == View.FOCUS_DOWN) {
785 | if (getChildCount() > 0) {
786 | int daBottom = getChildAt(0).getBottom();
787 | int screenBottom = getScrollY() + getHeight();
788 | if (daBottom - screenBottom < maxJump) {
789 | scrollDelta = daBottom - screenBottom;
790 | }
791 | }
792 | }
793 | if (scrollDelta == 0) {
794 | return false;
795 | }
796 | doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0);
797 | }
798 | }
799 | return true;
800 | }
801 |
802 | /**
803 | * Smooth scroll by a Y delta
804 | *
805 | * @param deltaY
806 | * the number of pixels to scroll by on the Y axis
807 | */
808 | private void doScroll(int deltaX, int deltaY) {
809 | if (deltaX != 0 || deltaY != 0) {
810 | smoothScrollBy(deltaX, deltaY);
811 | }
812 | }
813 |
814 | /**
815 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
816 | *
817 | * @param dx
818 | * the number of pixels to scroll by on the X axis
819 | * @param dy
820 | * the number of pixels to scroll by on the Y axis
821 | */
822 | public void smoothScrollBy(int dx, int dy) {
823 | long duration = AnimationUtils.currentAnimationTimeMillis() - lastScroll;
824 | if (duration > ANIMATED_SCROLL_GAP) {
825 | scroller.startScroll(getScrollX(), getScrollY(), dx, dy);
826 | awakenScrollBars(scroller.getDuration());
827 | invalidate();
828 | } else {
829 | if (!scroller.isFinished()) {
830 | scroller.abortAnimation();
831 | }
832 | scrollBy(dx, dy);
833 | }
834 | lastScroll = AnimationUtils.currentAnimationTimeMillis();
835 | }
836 |
837 | /**
838 | * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
839 | *
840 | * @param x
841 | * the position where to scroll on the X axis
842 | * @param y
843 | * the position where to scroll on the Y axis
844 | */
845 | public void smoothScrollTo(int x, int y) {
846 | smoothScrollBy(x - getScrollX(), y - getScrollY());
847 | }
848 |
849 | /**
850 | * Scrolls the view to the given child.
851 | *
852 | * @param child
853 | * the View to scroll to
854 | */
855 | private void scrollToChild(View child) {
856 | child.getDrawingRect(tempRect);
857 | /* Offset from child's local coordinates to TwoDScrollView coordinates */
858 | offsetDescendantRectToMyCoords(child, tempRect);
859 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(tempRect);
860 | if (scrollDelta != 0) {
861 | scrollBy(0, scrollDelta);
862 | }
863 | }
864 |
865 | /**
866 | * If rect is off screen, scroll just enough to get it (or at least the first screen size chunk
867 | * of it) on screen.
868 | *
869 | * @param rect
870 | * The rectangle.
871 | * @param immediate
872 | * True to scroll immediately without animation
873 | * @return true if scrolling was performed
874 | */
875 | private boolean scrollToChildRect(Rect rect, boolean immediate) {
876 | int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
877 | boolean scroll = delta != 0;
878 | if (scroll) {
879 | if (immediate) {
880 | scrollBy(0, delta);
881 | } else {
882 | smoothScrollBy(0, delta);
883 | }
884 | }
885 | return scroll;
886 | }
887 |
888 | /**
889 | * Compute the amount to scroll in the Y direction in order to get a rectangle completely on the
890 | * screen (or, if taller than the screen, at least the first screen size chunk of it).
891 | *
892 | * @param rect
893 | * The rect.
894 | * @return The scroll delta.
895 | */
896 | protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
897 | if (getChildCount() == 0) {
898 | return 0;
899 | }
900 | int height = getHeight();
901 | int screenTop = getScrollY();
902 | int screenBottom = screenTop + height;
903 | int fadingEdge = getVerticalFadingEdgeLength();
904 | // leave room for top fading edge as long as rect isn't at very top
905 | if (rect.top > 0) {
906 | screenTop += fadingEdge;
907 | }
908 |
909 | // leave room for bottom fading edge as long as rect isn't at very bottom
910 | if (rect.bottom < getChildAt(0).getHeight()) {
911 | screenBottom -= fadingEdge;
912 | }
913 | int scrollYDelta = 0;
914 | if (rect.bottom > screenBottom && rect.top > screenTop) {
915 | // need to move down to get it in view: move down just enough so
916 | // that the entire rectangle is in view (or at least the first
917 | // screen size chunk).
918 | if (rect.height() > height) {
919 | // just enough to get screen size chunk on
920 | scrollYDelta += (rect.top - screenTop);
921 | } else {
922 | // get entire rect at bottom of screen
923 | scrollYDelta += (rect.bottom - screenBottom);
924 | }
925 |
926 | // make sure we aren't scrolling beyond the end of our content
927 | int bottom = getChildAt(0).getBottom();
928 | int distanceToBottom = bottom - screenBottom;
929 | scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
930 |
931 | } else if (rect.top < screenTop && rect.bottom < screenBottom) {
932 | // need to move up to get it in view: move up just enough so that
933 | // entire rectangle is in view (or at least the first screen
934 | // size chunk of it).
935 |
936 | if (rect.height() > height) {
937 | // screen size chunk
938 | scrollYDelta -= (screenBottom - rect.bottom);
939 | } else {
940 | // entire rect at top
941 | scrollYDelta -= (screenTop - rect.top);
942 | }
943 |
944 | // make sure we aren't scrolling any further than the top our content
945 | scrollYDelta = Math.max(scrollYDelta, -getScrollY());
946 | }
947 | return scrollYDelta;
948 | }
949 |
950 | @Override public void requestChildFocus(View child, View focused) {
951 | if (!twoDScrollViewMovedFocus) {
952 | if (!isLayoutDirty) {
953 | scrollToChild(focused);
954 | } else {
955 | // The child may not be laid out yet, we can't compute the scroll yet
956 | childToScrollTo = focused;
957 | }
958 | }
959 | super.requestChildFocus(child, focused);
960 | }
961 |
962 | @Override public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
963 | // offset into coordinate space of this scroll view
964 | rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY());
965 | return scrollToChildRect(rectangle, immediate);
966 | }
967 |
968 | @Override public boolean dispatchKeyEvent(KeyEvent event) {
969 | // Let the focused view and/or our descendants get the key first
970 | boolean handled = super.dispatchKeyEvent(event);
971 | return handled || executeKeyEvent(event);
972 | }
973 |
974 | @Override public boolean onInterceptTouchEvent(MotionEvent ev) {
975 |
976 | // This method JUST determines whether we want to intercept the motion. If we return true,
977 | // onMotionEvent will be called and we do the actual scrolling there.
978 |
979 | // Shortcut the most recurring case: the user is in the dragging state and he is moving his
980 | // finger. We want to intercept this motion.
981 |
982 | int action = ev.getAction();
983 | if ((action == MotionEvent.ACTION_MOVE) && (isBeingDragged)) {
984 | return true;
985 | }
986 | if (!canScroll()) {
987 | isBeingDragged = false;
988 | return false;
989 | }
990 | float y = ev.getY();
991 | float x = ev.getX();
992 | switch (action) {
993 | case MotionEvent.ACTION_MOVE:
994 |
995 | // isBeingDragged == false, otherwise the shortcut would have caught it. Check whether
996 | // the user has moved far enough from his original down touch.
997 |
998 | // Locally do absolute value. lastMotionY is set to the y value of the down event.
999 |
1000 | int yDiff = (int) Math.abs(y - lastMotionY);
1001 | int xDiff = (int) Math.abs(x - lastMotionX);
1002 | if (yDiff > touchSlop || xDiff > touchSlop) {
1003 | isBeingDragged = true;
1004 | }
1005 | break;
1006 |
1007 | case MotionEvent.ACTION_DOWN:
1008 | // Remember location of down touch
1009 | lastMotionY = y;
1010 | lastMotionX = x;
1011 |
1012 | // If being flinged and user touches the screen, initiate drag; otherwise don't.
1013 | // scroller.isFinished should be false when being flinged.
1014 |
1015 | isBeingDragged = !scroller.isFinished();
1016 | break;
1017 |
1018 | case MotionEvent.ACTION_CANCEL:
1019 | case MotionEvent.ACTION_UP:
1020 | // Release the drag
1021 | isBeingDragged = false;
1022 | break;
1023 | }
1024 |
1025 | // The only time we want to intercept motion events is if we are in the drag mode.
1026 |
1027 | return isBeingDragged;
1028 | }
1029 |
1030 | /**
1031 | * When looking for focus in children of a scroll view, need to be a little more careful not to
1032 | * give focus to something that is scrolled off screen.
1033 | *
1034 | * This is more expensive than the default {@link ViewGroup} implementation,
1035 | * otherwise this behavior might have been made the default.
1036 | */
1037 | @Override protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
1038 | // convert from forward / backward notation to up / down / left / right
1039 | if (direction == View.FOCUS_FORWARD) {
1040 | direction = View.FOCUS_DOWN;
1041 | } else if (direction == View.FOCUS_BACKWARD) {
1042 | direction = View.FOCUS_UP;
1043 | }
1044 |
1045 | View nextFocus = previouslyFocusedRect == null ? FocusFinder.getInstance().findNextFocus(this, null, direction) :
1046 | FocusFinder.getInstance().findNextFocusFromRect(this, previouslyFocusedRect, direction);
1047 | return nextFocus != null && nextFocus.requestFocus(direction, previouslyFocusedRect);
1048 | }
1049 |
1050 | @Override public void addView(View child) {
1051 | if (getChildCount() > 0) {
1052 | throw new IllegalStateException("TwoDScrollView can host only one direct child");
1053 | }
1054 | super.addView(child);
1055 | }
1056 |
1057 | @Override public void addView(View child, int index) {
1058 | if (getChildCount() > 0) {
1059 | throw new IllegalStateException("TwoDScrollView can host only one direct child");
1060 | }
1061 | super.addView(child, index);
1062 | }
1063 |
1064 | @Override public void addView(View child, ViewGroup.LayoutParams params) {
1065 | if (getChildCount() > 0) {
1066 | throw new IllegalStateException("TwoDScrollView can host only one direct child");
1067 | }
1068 | super.addView(child, params);
1069 | }
1070 |
1071 | @Override public void addView(View child, int index, ViewGroup.LayoutParams params) {
1072 | if (getChildCount() > 0) {
1073 | throw new IllegalStateException("TwoDScrollView can host only one direct child");
1074 | }
1075 | super.addView(child, index, params);
1076 | }
1077 |
1078 | @Override protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
1079 | ViewGroup.LayoutParams lp = child.getLayoutParams();
1080 | int childWidthMeasureSpec;
1081 | int childHeightMeasureSpec;
1082 | childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width);
1083 | childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
1084 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1085 | }
1086 |
1087 | @Override protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
1088 | int parentHeightMeasureSpec, int heightUsed) {
1089 | MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
1090 | int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
1091 | int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
1092 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1093 | }
1094 |
1095 | /**
1096 | * Return true if child is an descendant of parent, (or equal to the parent).
1097 | */
1098 | private boolean isViewDescendantOf(View child, View parent) {
1099 | if (child == parent) {
1100 | return true;
1101 | }
1102 |
1103 | ViewParent theParent = child.getParent();
1104 | return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
1105 | }
1106 |
1107 | /**
1108 | * Fling the scroll view
1109 | *
1110 | * @param velocityY
1111 | * The initial velocity in the Y direction. Positive numbers mean that the finger/curor
1112 | * is moving down the screen, which means we want to scroll towards the top.
1113 | */
1114 | public void fling(int velocityX, int velocityY) {
1115 | if (getChildCount() > 0) {
1116 | int height = getHeight() - getPaddingBottom() - getPaddingTop();
1117 | int bottom = getChildAt(0).getHeight();
1118 | int width = getWidth() - getPaddingRight() - getPaddingLeft();
1119 | int right = getChildAt(0).getWidth();
1120 |
1121 | scroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0,
1122 | bottom - height);
1123 |
1124 | boolean movingDown = velocityY > 0;
1125 | boolean movingRight = velocityX > 0;
1126 |
1127 | View newFocused = findFocusableViewInMyBounds(movingRight, scroller.getFinalX(),
1128 | movingDown, scroller.getFinalY(), findFocus());
1129 | if (newFocused == null) {
1130 | newFocused = this;
1131 | }
1132 |
1133 | if (newFocused != findFocus()
1134 | && newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP)) {
1135 | twoDScrollViewMovedFocus = true;
1136 | twoDScrollViewMovedFocus = false;
1137 | }
1138 |
1139 | awakenScrollBars(scroller.getDuration());
1140 | invalidate();
1141 | }
1142 | }
1143 |
1144 | private int clamp(int n, int my, int child) {
1145 | if (my >= child || n < 0) {
1146 | return 0;
1147 | }
1148 | if ((my + n) > child) {
1149 | return child - my;
1150 | }
1151 | return n;
1152 | }
1153 |
1154 | /**
1155 | * Interface for listening to scroll changes.
1156 | */
1157 | public interface ScrollChangeListener {
1158 |
1159 | /**
1160 | * This is called in response to an internal scroll in this view (i.e., the view scrolled its own contents).
1161 | * This is typically as a result of {@link View#scrollBy(int, int)} or {@link View#scrollTo(int, int)} having
1162 | * been called.
1163 | *
1164 | * @param view
1165 | * the view being scrolled.
1166 | * @param x
1167 | * Current horizontal scroll origin.
1168 | * @param y
1169 | * Current vertical scroll origin.
1170 | * @param oldx
1171 | * Previous horizontal scroll origin.
1172 | * @param oldy
1173 | * Previous vertical scroll origin.
1174 | */
1175 | void onScrollChanged(View view, int x, int y, int oldx, int oldy);
1176 | }
1177 |
1178 | }
1179 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 Jared Rummler
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 | */
17 |
18 | include ':library'
19 |
--------------------------------------------------------------------------------