) {
146 | val tempData = currentList.toMutableList()
147 | val children = TreeViewModel.getChildren(clickedNode)
148 | tempData.removeAll(children.toSet())
149 | TreeViewModel.remove(clickedNode, clickedNode.child)
150 | clickedNode.isExpand = false
151 | submitList(tempData)
152 | }
153 |
154 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/FileTree/src/main/java/com/rk/filetree/widget/DiagonalScrollView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Based on AOSP's HorizontalScrollView, here it's Copyright:
3 | *
4 | * Copyright (C) 2009 The Android Open Source Project
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 | package com.rk.filetree.widget;
20 |
21 | import android.content.Context;
22 | import android.graphics.Rect;
23 | import android.os.Build;
24 | import android.os.Parcel;
25 | import android.os.Parcelable;
26 | import android.util.AttributeSet;
27 | import android.view.FocusFinder;
28 | import android.view.InputDevice;
29 | import android.view.MotionEvent;
30 | import android.view.VelocityTracker;
31 | import android.view.View;
32 | import android.view.ViewConfiguration;
33 | import android.view.ViewDebug;
34 | import android.view.ViewGroup;
35 | import android.view.ViewParent;
36 | import android.view.accessibility.AccessibilityEvent;
37 | import android.view.accessibility.AccessibilityNodeInfo;
38 | import android.view.animation.AnimationUtils;
39 | import android.widget.FrameLayout;
40 | import android.widget.OverScroller;
41 |
42 | import java.util.ArrayList;
43 | import java.util.List;
44 |
45 | /**
46 | * Layout container for a view hierarchy that can be scrolled by the user,
47 | * allowing it to be larger than the physical display. A DiagonalScrollView
48 | * is a {@link FrameLayout}, meaning you should place one child in it
49 | * containing the entire contents to scroll; this child may itself be a layout
50 | * manager with a complex hierarchy of objects. A child that is often used
51 | * is a {@link android.widget.TableLayout}.
52 | * You should never use a DiagonalScrollView with a {@link android.widget.ListView}, because
53 | * ListView takes care of its own scrolling. Most importantly, doing this
54 | * defeats all of the important optimizations in ListView for dealing with
55 | * large lists, since it effectively forces the ListView to display its entire
56 | * list of items to fill up the infinite container supplied by DiagonalScrollView.
57 | */
58 | public class DiagonalScrollView extends FrameLayout {
59 | private static final int ANIMATED_SCROLL_GAP = 250;
60 |
61 | private static final float MAX_SCROLL_FACTOR = 0.5f;
62 | /**
63 | * Sentinel value for no current active pointer.
64 | * Used by {@link #mActivePointerId}.
65 | */
66 | private static final int INVALID_POINTER = -1;
67 | private final Rect mTempRect = new Rect();
68 | private final List listeners = new ArrayList<>();
69 | private long mLastScroll;
70 | private OverScroller mScroller;
71 | /**
72 | * Position of the last motion event.
73 | */
74 | private float mLastMotionX;
75 | private float mLastMotionY;
76 | /**
77 | * True when the layout has changed but the traversal has not come through yet.
78 | * Ideally the view hierarchy would keep track of this for us.
79 | */
80 | private boolean mIsLayoutDirty = true;
81 | /**
82 | * The child to give focus to in the event that a child has requested focus while the
83 | * layout is dirty. This prevents the scroll from being wrong if the child has not been
84 | * laid out before requesting focus.
85 | */
86 | private View mChildToScrollTo = null;
87 | /**
88 | * True if the user is currently dragging this ScrollView around. This is
89 | * not the same as 'is being flinged', which can be checked by
90 | * mScroller.isFinished() (flinging begins when the user lifts his finger).
91 | */
92 | private boolean mIsBeingDragged = false;
93 | /**
94 | * Determines speed during touch scrolling
95 | */
96 | private VelocityTracker mVelocityTracker;
97 | /**
98 | * When set to true, the scroll view measure its child to make it fill the currently
99 | * visible area.
100 | */
101 | @ViewDebug.ExportedProperty(category = "layout")
102 | private boolean mFillViewport;
103 | private int mTouchSlop;
104 | private int mMinimumVelocity;
105 | private int mMaximumVelocity;
106 | private int mOverscrollDistance;
107 | private int mOverflingDistance;
108 | /**
109 | * ID of the active pointer. This is used to retain consistency during
110 | * drags/flings if multiple pointers are used.
111 | */
112 | private int mActivePointerId = INVALID_POINTER;
113 | private SavedState mSavedState;
114 |
115 | public DiagonalScrollView(Context context) {
116 | this(context, null);
117 | }
118 |
119 | public DiagonalScrollView(Context context, AttributeSet attrs) {
120 | this(context, attrs, 0);
121 | }
122 |
123 | public DiagonalScrollView(Context context, AttributeSet attrs, int defStyle) {
124 | super(context, attrs, defStyle);
125 | initScrollView();
126 | }
127 |
128 | @Override
129 | public boolean shouldDelayChildPressedState() {
130 | return true;
131 | }
132 |
133 | private void initScrollView() {
134 | mScroller = new OverScroller(getContext());
135 | setFocusable(true);
136 | setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
137 | setWillNotDraw(false);
138 | final ViewConfiguration configuration = ViewConfiguration.get(getContext());
139 | mTouchSlop = configuration.getScaledTouchSlop();
140 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
141 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
142 | mOverscrollDistance = configuration.getScaledOverscrollDistance();
143 | mOverflingDistance = configuration.getScaledOverflingDistance();
144 | }
145 |
146 | @Override
147 | public void addView(View child) {
148 | if (getChildCount() > 0) {
149 | throw new IllegalStateException("ScrollView can host only one direct child");
150 | }
151 |
152 | super.addView(child);
153 | }
154 |
155 | @Override
156 | public void addView(View child, int index) {
157 | if (getChildCount() > 0) {
158 | throw new IllegalStateException("ScrollView can host only one direct child");
159 | }
160 |
161 | super.addView(child, index);
162 | }
163 |
164 | @Override
165 | public void addView(View child, ViewGroup.LayoutParams params) {
166 | if (getChildCount() > 0) {
167 | throw new IllegalStateException("ScrollView can host only one direct child");
168 | }
169 |
170 | super.addView(child, params);
171 | }
172 |
173 | @Override
174 | public void addView(View child, int index, ViewGroup.LayoutParams params) {
175 | if (getChildCount() > 0) {
176 | throw new IllegalStateException("ScrollView can host only one direct child");
177 | }
178 |
179 | super.addView(child, index, params);
180 | }
181 |
182 | /**
183 | * Indicates whether this ScrollView's content is stretched to fill the viewport.
184 | *
185 | * @return True if the content fills the viewport, false otherwise.
186 | */
187 | public boolean isFillViewport() {
188 | return mFillViewport;
189 | }
190 |
191 | /**
192 | * Indicates this ScrollView whether it should stretch its content height to fill
193 | * the viewport or not.
194 | *
195 | * @param fillViewport True to stretch the content's height to the viewport's
196 | * boundaries, false otherwise.
197 | */
198 | public void setFillViewport(boolean fillViewport) {
199 | if (fillViewport != mFillViewport) {
200 | mFillViewport = fillViewport;
201 | requestLayout();
202 | }
203 | }
204 |
205 | @Override
206 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
207 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
208 |
209 | if (!mFillViewport) {
210 | return;
211 | }
212 |
213 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
214 | if (heightMode == MeasureSpec.UNSPECIFIED) {
215 | return;
216 | }
217 |
218 | if (getChildCount() > 0) {
219 | final View child = getChildAt(0);
220 | int width = getMeasuredWidth();
221 | int height = getMeasuredHeight();
222 | if (child.getMeasuredHeight() < height || child.getMeasuredWidth() < width) {
223 | width -= getPaddingLeft();
224 | width -= getPaddingRight();
225 | int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
226 | height -= getPaddingTop();
227 | height -= getPaddingBottom();
228 | int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
229 |
230 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
231 | }
232 | }
233 | }
234 |
235 | public void addOnScrollListener(final OnScrollListener listener) {
236 | this.listeners.add(listener);
237 | }
238 |
239 | public boolean removeOnScrollListener(final OnScrollListener listener) {
240 | return this.listeners.remove(listener);
241 | }
242 |
243 | @Override
244 | protected void onScrollChanged(int l, int t, int oldl, int oldt) {
245 | super.onScrollChanged(l, t, oldl, oldt);
246 | for (OnScrollListener listener : listeners) {
247 | listener.onScrollChanged(l, t, oldl, oldt);
248 | }
249 | }
250 |
251 | private boolean inChild(int x, int y) {
252 | if (getChildCount() > 0) {
253 | final int scrollX = getScrollX();
254 | final int scrollY = getScrollY();
255 | final View child = getChildAt(0);
256 | return !(y < child.getTop() - scrollY
257 | || y >= child.getBottom() - scrollY
258 | || x < child.getLeft() - scrollX
259 | || x >= child.getRight() - scrollX);
260 | }
261 | return false;
262 | }
263 |
264 | private void initOrResetVelocityTracker() {
265 | if (mVelocityTracker == null) {
266 | mVelocityTracker = VelocityTracker.obtain();
267 | } else {
268 | mVelocityTracker.clear();
269 | }
270 | }
271 |
272 | private void initVelocityTrackerIfNotExists() {
273 | if (mVelocityTracker == null) {
274 | mVelocityTracker = VelocityTracker.obtain();
275 | }
276 | }
277 |
278 | private void recycleVelocityTracker() {
279 | if (mVelocityTracker != null) {
280 | mVelocityTracker.recycle();
281 | mVelocityTracker = null;
282 | }
283 | }
284 |
285 | @Override
286 | public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
287 | if (disallowIntercept) {
288 | recycleVelocityTracker();
289 | }
290 | super.requestDisallowInterceptTouchEvent(disallowIntercept);
291 | }
292 |
293 |
294 | @Override
295 | public boolean onInterceptTouchEvent(MotionEvent ev) {
296 | /*
297 | * This method JUST determines whether we want to intercept the motion.
298 | * If we return true, onMotionEvent will be called and we do the actual
299 | * scrolling there.
300 | */
301 |
302 | /*
303 | * Shortcut the most recurring case: the user is in the dragging
304 | * state and he is moving his finger. We want to intercept this
305 | * motion.
306 | */
307 | final int action = ev.getAction();
308 | if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
309 | return true;
310 | }
311 |
312 | switch (action & MotionEvent.ACTION_MASK) {
313 | case MotionEvent.ACTION_MOVE: {
314 | /*
315 | * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
316 | * whether the user has moved far enough from his original down touch.
317 | */
318 |
319 | /*
320 | * Locally do absolute value. mLastMotionY is set to the y value
321 | * of the down event.
322 | */
323 | final int activePointerId = mActivePointerId;
324 | if (activePointerId == INVALID_POINTER) {
325 | // If we don't have a valid id, the touch down wasn't on content.
326 | break;
327 | }
328 |
329 | final int pointerIndex = ev.findPointerIndex(activePointerId);
330 | final float x = ev.getX(pointerIndex);
331 | final float y = ev.getY(pointerIndex);
332 | final int xDiff = (int) Math.abs(x - mLastMotionX);
333 | final int yDiff = (int) Math.abs(y - mLastMotionY);
334 | if ((xDiff + yDiff) / 2 > mTouchSlop) {
335 | mIsBeingDragged = true;
336 | mLastMotionX = x;
337 | mLastMotionY = y;
338 | initVelocityTrackerIfNotExists();
339 | mVelocityTracker.addMovement(ev);
340 | }
341 | break;
342 | }
343 |
344 | case MotionEvent.ACTION_DOWN: {
345 | final float x = ev.getX();
346 | final float y = ev.getY();
347 | if (!inChild((int) x, (int) y)) {
348 | mIsBeingDragged = false;
349 | recycleVelocityTracker();
350 | break;
351 | }
352 |
353 | /*
354 | * Remember location of down touch.
355 | * ACTION_DOWN always refers to pointer index 0.
356 | */
357 | mLastMotionX = x;
358 | mLastMotionY = y;
359 | mActivePointerId = ev.getPointerId(0);
360 |
361 | initOrResetVelocityTracker();
362 | mVelocityTracker.addMovement(ev);
363 | /*
364 | * If being flinged and user touches the screen, initiate drag;
365 | * otherwise don't. mScroller.isFinished should be false when
366 | * being flinged.
367 | */
368 | mIsBeingDragged = !mScroller.isFinished();
369 | break;
370 | }
371 |
372 | case MotionEvent.ACTION_CANCEL:
373 | case MotionEvent.ACTION_UP:
374 | /* Release the drag */
375 | mIsBeingDragged = false;
376 | mActivePointerId = INVALID_POINTER;
377 | recycleVelocityTracker();
378 | if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, getScrollRangeY())) {
379 | invalidate();
380 | }
381 | break;
382 | case MotionEvent.ACTION_POINTER_UP:
383 | onSecondaryPointerUp(ev);
384 | break;
385 | }
386 |
387 | /*
388 | * The only time we want to intercept motion events is if we are in the
389 | * drag mode.
390 | */
391 | return mIsBeingDragged;
392 | }
393 |
394 | @Override
395 | public boolean onTouchEvent(MotionEvent ev) {
396 | initVelocityTrackerIfNotExists();
397 | mVelocityTracker.addMovement(ev);
398 |
399 | final int action = ev.getAction();
400 |
401 | switch (action & MotionEvent.ACTION_MASK) {
402 | case MotionEvent.ACTION_DOWN: {
403 | mIsBeingDragged = getChildCount() != 0;
404 | if (!mIsBeingDragged) {
405 | return false;
406 | }
407 |
408 | /*
409 | * If being flinged and user touches, stop the fling. isFinished
410 | * will be false if being flinged.
411 | */
412 | if (!mScroller.isFinished()) {
413 | mScroller.abortAnimation();
414 | }
415 |
416 | // Remember where the motion event started
417 | mLastMotionX = ev.getX();
418 | mLastMotionY = ev.getY();
419 | mActivePointerId = ev.getPointerId(0);
420 | break;
421 | }
422 | case MotionEvent.ACTION_MOVE:
423 | if (mIsBeingDragged) {
424 | // Scroll to follow the motion event
425 | final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
426 | final float x = ev.getX(activePointerIndex);
427 | final float y = ev.getY(activePointerIndex);
428 | final int deltaX = (int) (mLastMotionX - x);
429 | final int deltaY = (int) (mLastMotionY - y);
430 | mLastMotionX = x;
431 | mLastMotionY = y;
432 |
433 | final int oldX = getScrollX();
434 | final int oldY = getScrollY();
435 | final int rangeX = getScrollRangeX();
436 | final int rangeY = getScrollRangeY();
437 |
438 | overScrollBy(deltaX, deltaY, getScrollX(), getScrollY(),
439 | rangeX, rangeY, mOverscrollDistance, mOverscrollDistance, true);
440 | onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
441 | }
442 | break;
443 | case MotionEvent.ACTION_UP:
444 | if (mIsBeingDragged) {
445 | final VelocityTracker velocityTracker = mVelocityTracker;
446 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
447 | int initialVelocityX = (int) velocityTracker.getXVelocity(mActivePointerId);
448 | int initialVelocityY = (int) velocityTracker.getYVelocity(mActivePointerId);
449 |
450 | if (getChildCount() > 0) {
451 | if (((Math.abs(initialVelocityX) + Math.abs(initialVelocityY)) / 2 > mMinimumVelocity)) {
452 | fling(-initialVelocityX, -initialVelocityY);
453 | } else {
454 | if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0,
455 | getScrollRangeY())) {
456 | invalidate();
457 | }
458 | }
459 | }
460 |
461 | mActivePointerId = INVALID_POINTER;
462 | endDrag();
463 | }
464 | break;
465 | case MotionEvent.ACTION_CANCEL:
466 | if (mIsBeingDragged && getChildCount() > 0) {
467 | if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, getScrollRangeY())) {
468 | invalidate();
469 | }
470 | mActivePointerId = INVALID_POINTER;
471 | endDrag();
472 | }
473 | break;
474 | case MotionEvent.ACTION_POINTER_DOWN: {
475 | final int index = ev.getActionIndex();
476 | mLastMotionX = ev.getX(index);
477 | mLastMotionY = ev.getY(index);
478 | mActivePointerId = ev.getPointerId(index);
479 | break;
480 | }
481 | case MotionEvent.ACTION_POINTER_UP:
482 | onSecondaryPointerUp(ev);
483 | mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId));
484 | mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId));
485 | break;
486 | }
487 | return true;
488 | }
489 |
490 | private void onSecondaryPointerUp(MotionEvent ev) {
491 | final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
492 | MotionEvent.ACTION_POINTER_INDEX_SHIFT;
493 | final int pointerId = ev.getPointerId(pointerIndex);
494 | if (pointerId == mActivePointerId) {
495 | // This was our active pointer going up. Choose a new
496 | // active pointer and adjust accordingly.
497 | // TODO: Make this decision more intelligent.
498 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
499 | mLastMotionX = ev.getX(newPointerIndex);
500 | mLastMotionY = ev.getY(newPointerIndex);
501 | mActivePointerId = ev.getPointerId(newPointerIndex);
502 | if (mVelocityTracker != null) {
503 | mVelocityTracker.clear();
504 | }
505 | }
506 | }
507 |
508 | @Override
509 | public boolean onGenericMotionEvent(MotionEvent event) {
510 | if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
511 | switch (event.getAction()) {
512 | case MotionEvent.ACTION_SCROLL: {
513 | if (!mIsBeingDragged) {
514 | final float hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
515 | final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
516 | if (hscroll != 0 || vscroll != 0) {
517 | final int deltaX = (int) (hscroll * MAX_SCROLL_FACTOR);
518 | final int rangeX = getScrollRangeX();
519 | final int deltaY = (int) (vscroll * MAX_SCROLL_FACTOR);
520 | final int rangeY = getScrollRangeY();
521 | int oldScrollX = getScrollX();
522 | int newScrollX = oldScrollX - deltaX;
523 | int oldScrollY = getScrollY();
524 | int newScrollY = oldScrollY - deltaY;
525 | if (newScrollX < 0) {
526 | newScrollX = 0;
527 | } else if (newScrollX > rangeX) {
528 | newScrollX = rangeX;
529 | }
530 | if (newScrollY < 0) {
531 | newScrollY = 0;
532 | } else if (newScrollY > rangeY) {
533 | newScrollY = rangeY;
534 | }
535 | if (newScrollX != oldScrollX || newScrollY != oldScrollY) {
536 | super.scrollTo(newScrollX, newScrollY);
537 | return true;
538 | }
539 | }
540 | }
541 | }
542 | }
543 | }
544 | return super.onGenericMotionEvent(event);
545 | }
546 |
547 | @Override
548 | protected void onOverScrolled(int scrollX, int scrollY,
549 | boolean clampedX, boolean clampedY) {
550 | // Treat animating scrolls differently; see #computeScroll() for why.
551 | if (mScroller.isFinished()) {
552 | super.scrollTo(scrollX, scrollY);
553 | } else {
554 | setScrollX(scrollX);
555 | setScrollY(scrollY);
556 | invalidate();
557 | }
558 | awakenScrollBars();
559 | }
560 |
561 | @Override
562 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
563 | super.onInitializeAccessibilityNodeInfo(info);
564 | info.setScrollable(getScrollRangeX() > 0 || getScrollRangeY() > 0);
565 | }
566 |
567 | @Override
568 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
569 | super.onInitializeAccessibilityEvent(event);
570 | final boolean scrollable = getScrollRangeX() > 0 || getScrollRangeY() > 0;
571 | event.setScrollable(scrollable);
572 | event.setScrollX(getScrollX());
573 | event.setScrollY(getScrollY());
574 | }
575 |
576 | private int getScrollRangeY() {
577 | int scrollRange = 0;
578 | if (getChildCount() > 0) {
579 | View child = getChildAt(0);
580 | scrollRange = Math.max(0,
581 | child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop()));
582 | }
583 | return scrollRange;
584 | }
585 |
586 | private int getScrollRangeX() {
587 | int scrollRange = 0;
588 | if (getChildCount() > 0) {
589 | View child = getChildAt(0);
590 | scrollRange = Math.max(0,
591 | child.getWidth() - (getWidth() - getPaddingLeft() - getPaddingRight()));
592 | }
593 | return scrollRange;
594 | }
595 |
596 | /**
597 | * @return whether the descendant of this scroll view is scrolled off
598 | * screen.
599 | */
600 | private boolean isOffScreen(View descendant) {
601 | return !isWithinDeltaOfScreen(descendant, getWidth(), getHeight());
602 | }
603 |
604 | /**
605 | * @return whether the descendant of this scroll view is within delta
606 | * pixels of being on the screen.
607 | */
608 | private boolean isWithinDeltaOfScreen(View descendant, int width, int height) {
609 | descendant.getDrawingRect(mTempRect);
610 | offsetDescendantRectToMyCoords(descendant, mTempRect);
611 |
612 | return mTempRect.bottom >= getScrollY() && mTempRect.top <= (getScrollY() + height) &&
613 | mTempRect.right >= getScrollX() && mTempRect.left <= (getScrollX() + width);
614 | }
615 |
616 | /**
617 | * Smooth scroll by a delta
618 | *
619 | * @param deltaX the number of pixels to scroll by on the X axis
620 | * @param deltaY the number of pixels to scroll by on the Y axis
621 | */
622 | private void doScroll(int deltaX, int deltaY) {
623 | if (deltaX != 0 && deltaY != 0) {
624 | smoothScrollBy(deltaX, deltaY);
625 | }
626 | }
627 |
628 | /**
629 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
630 | *
631 | * @param dx the number of pixels to scroll by on the X axis
632 | * @param dy the number of pixels to scroll by on the Y axis
633 | */
634 | private void smoothScrollBy(int dx, int dy) {
635 | if (getChildCount() == 0) {
636 | // Nothing to do.
637 | return;
638 | }
639 | long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
640 | if (duration > ANIMATED_SCROLL_GAP) {
641 | final int width = getWidth() - getPaddingRight() - getPaddingLeft();
642 | final int height = getHeight() - getPaddingBottom() - getPaddingTop();
643 | final int right = getChildAt(0).getWidth();
644 | final int bottom = getChildAt(0).getHeight();
645 | final int maxX = Math.max(0, right - width);
646 | final int maxY = Math.max(0, bottom - height);
647 | final int scrollX = getScrollX();
648 | final int scrollY = getScrollY();
649 | dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
650 | dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY;
651 |
652 | mScroller.startScroll(scrollX, scrollY, dx, dy);
653 | invalidate();
654 | } else {
655 | if (!mScroller.isFinished()) {
656 | mScroller.abortAnimation();
657 | }
658 | scrollBy(dx, dy);
659 | }
660 | mLastScroll = AnimationUtils.currentAnimationTimeMillis();
661 | }
662 |
663 | /**
664 | * The scroll range of a scroll view is the overall height of all of its
665 | * children.
666 | */
667 | @Override
668 | protected int computeVerticalScrollRange() {
669 | final int count = getChildCount();
670 | final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop();
671 | if (count == 0) {
672 | return contentHeight;
673 | }
674 |
675 | int scrollRange = getChildAt(0).getBottom();
676 | final int scrollY = getScrollY();
677 | final int overscrollBottom = Math.max(0, scrollRange - contentHeight);
678 | if (scrollY < 0) {
679 | scrollRange -= scrollY;
680 | } else if (scrollY > overscrollBottom) {
681 | scrollRange += scrollY - overscrollBottom;
682 | }
683 |
684 | return scrollRange;
685 | }
686 |
687 | @Override
688 | protected int computeHorizontalScrollRange() {
689 | final int count = getChildCount();
690 | final int contentWidth = getWidth() - getPaddingRight() - getPaddingLeft();
691 | if (count == 0) {
692 | return contentWidth;
693 | }
694 |
695 | int scrollRange = getChildAt(0).getRight();
696 | final int scrollX = getScrollX();
697 | final int overscrollRight = Math.max(0, scrollRange - contentWidth);
698 | if (scrollX < 0) {
699 | scrollRange -= scrollX;
700 | } else if (scrollX > overscrollRight) {
701 | scrollRange += scrollX - overscrollRight;
702 | }
703 |
704 | return scrollRange;
705 | }
706 |
707 | @Override
708 | protected int computeVerticalScrollOffset() {
709 | return Math.max(0, super.computeVerticalScrollOffset());
710 | }
711 |
712 | @Override
713 | protected int computeHorizontalScrollOffset() {
714 | return Math.max(0, super.computeHorizontalScrollOffset());
715 | }
716 |
717 | @Override
718 | protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
719 | child.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
720 | MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
721 | }
722 |
723 | @Override
724 | protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
725 | int parentHeightMeasureSpec, int heightUsed) {
726 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
727 |
728 | final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
729 | lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
730 | final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
731 | lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
732 |
733 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
734 | }
735 |
736 | @Override
737 | public void computeScroll() {
738 | if (mScroller.computeScrollOffset()) {
739 | // This is called at drawing time by ViewGroup. We don't want to
740 | // re-show the scrollbars at this point, which scrollTo will do,
741 | // so we replicate most of scrollTo here.
742 | //
743 | // It's a little odd to call onScrollChanged from inside the drawing.
744 | //
745 | // It is, except when you remember that computeScroll() is used to
746 | // animate scrolling. So unless we want to defer the onScrollChanged()
747 | // until the end of the animated scrolling, we don't really have a
748 | // choice here.
749 | //
750 | // I agree. The alternative, which I think would be worse, is to post
751 | // something and tell the subclasses later. This is bad because there
752 | // will be a window where getScrollX()/Y is different from what the app
753 | // thinks it is.
754 | //
755 | int oldX = getScrollX();
756 | int oldY = getScrollY();
757 | int x = mScroller.getCurrX();
758 | int y = mScroller.getCurrY();
759 |
760 | if (oldX != x || oldY != y) {
761 | final int rangeX = getScrollRangeX();
762 | final int rangeY = getScrollRangeY();
763 |
764 | overScrollBy(x - oldX, y - oldY, oldX, oldY, rangeX, rangeY,
765 | mOverflingDistance, mOverflingDistance, false);
766 | onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
767 | }
768 |
769 | awakenScrollBars();
770 |
771 | // Keep on drawing until the animation has finished.
772 | postInvalidate();
773 | }
774 | }
775 |
776 | /**
777 | * Scrolls the view to the given child.
778 | *
779 | * @param child the View to scroll to
780 | */
781 | private void scrollToChild(View child) {
782 | child.getDrawingRect(mTempRect);
783 |
784 | /* Offset from child's local coordinates to ScrollView coordinates */
785 | offsetDescendantRectToMyCoords(child, mTempRect);
786 |
787 | int scrollDeltaX = computeScrollDeltaXToGetChildRectOnScreen(mTempRect);
788 | int scrollDeltaY = computeScrollDeltaYToGetChildRectOnScreen(mTempRect);
789 |
790 | if (scrollDeltaX != 0 || scrollDeltaY != 0) {
791 | scrollBy(scrollDeltaX, scrollDeltaY);
792 | }
793 | }
794 |
795 | /**
796 | * If rect is off screen, scroll just enough to get it (or at least the
797 | * first screen size chunk of it) on screen.
798 | *
799 | * @param rect The rectangle.
800 | * @param immediate True to scroll immediately without animation
801 | * @return true if scrolling was performed
802 | */
803 | private boolean scrollToChildRect(Rect rect, boolean immediate) {
804 | final int deltaX = computeScrollDeltaXToGetChildRectOnScreen(rect);
805 | final int deltaY = computeScrollDeltaYToGetChildRectOnScreen(rect);
806 | final boolean scroll = deltaX != 0 || deltaY != 0;
807 | if (scroll) {
808 | if (immediate) {
809 | scrollBy(deltaX, deltaY);
810 | } else {
811 | smoothScrollBy(deltaX, deltaY);
812 | }
813 | }
814 | return scroll;
815 | }
816 |
817 | /**
818 | * Compute the amount to scroll in the X direction in order to get
819 | * a rectangle completely on the screen (or, if taller than the screen,
820 | * at least the first screen size chunk of it).
821 | *
822 | * @param rect The rect.
823 | * @return The scroll delta.
824 | */
825 | private int computeScrollDeltaXToGetChildRectOnScreen(Rect rect) {
826 | if (getChildCount() == 0) return 0;
827 |
828 | int width = getWidth();
829 | int screenLeft = getScrollX();
830 | int screenRight = screenLeft + width;
831 |
832 | int fadingEdge = getHorizontalFadingEdgeLength();
833 |
834 | // leave room for top fading edge as long as rect isn't at very top
835 | if (rect.left > 0) {
836 | screenLeft += fadingEdge;
837 | }
838 |
839 | // leave room for bottom fading edge as long as rect isn't at very bottom
840 | if (rect.right < getChildAt(0).getWidth()) {
841 | screenRight -= fadingEdge;
842 | }
843 |
844 | int scrollXDelta = 0;
845 |
846 | if (rect.right > screenRight && rect.left > screenLeft) {
847 | // need to move down to get it in view: move down just enough so
848 | // that the entire rectangle is in view (or at least the first
849 | // screen size chunk).
850 |
851 | if (rect.width() > width) {
852 | // just enough to get screen size chunk on
853 | scrollXDelta += (rect.left - screenLeft);
854 | } else {
855 | // get entire rect at bottom of screen
856 | scrollXDelta += (rect.right - screenRight);
857 | }
858 |
859 | // make sure we aren't scrolling beyond the end of our content
860 | int right = getChildAt(0).getRight();
861 | int distanceToRight = right - screenRight;
862 | scrollXDelta = Math.min(scrollXDelta, distanceToRight);
863 |
864 | } else if (rect.left < screenLeft && rect.right < screenRight) {
865 | // need to move up to get it in view: move up just enough so that
866 | // entire rectangle is in view (or at least the first screen
867 | // size chunk of it).
868 |
869 | if (rect.width() > width) {
870 | // screen size chunk
871 | scrollXDelta -= (screenRight - rect.right);
872 | } else {
873 | // entire rect at top
874 | scrollXDelta -= (screenLeft - rect.left);
875 | }
876 |
877 | // make sure we aren't scrolling any further than the top our content
878 | scrollXDelta = Math.max(scrollXDelta, -getScrollX());
879 | }
880 | return scrollXDelta;
881 | }
882 |
883 | /**
884 | * Compute the amount to scroll in the Y direction in order to get
885 | * a rectangle completely on the screen (or, if taller than the screen,
886 | * at least the first screen size chunk of it).
887 | *
888 | * @param rect The rect.
889 | * @return The scroll delta.
890 | */
891 | private int computeScrollDeltaYToGetChildRectOnScreen(Rect rect) {
892 | if (getChildCount() == 0) return 0;
893 |
894 | int height = getHeight();
895 | int screenTop = getScrollY();
896 | int screenBottom = screenTop + height;
897 |
898 | int fadingEdge = getVerticalFadingEdgeLength();
899 |
900 | // leave room for top fading edge as long as rect isn't at very top
901 | if (rect.top > 0) {
902 | screenTop += fadingEdge;
903 | }
904 |
905 | // leave room for bottom fading edge as long as rect isn't at very bottom
906 | if (rect.bottom < getChildAt(0).getHeight()) {
907 | screenBottom -= fadingEdge;
908 | }
909 |
910 | int scrollYDelta = 0;
911 |
912 | if (rect.bottom > screenBottom && rect.top > screenTop) {
913 | // need to move down to get it in view: move down just enough so
914 | // that the entire rectangle is in view (or at least the first
915 | // screen size chunk).
916 |
917 | if (rect.height() > height) {
918 | // just enough to get screen size chunk on
919 | scrollYDelta += (rect.top - screenTop);
920 | } else {
921 | // get entire rect at bottom of screen
922 | scrollYDelta += (rect.bottom - screenBottom);
923 | }
924 |
925 | // make sure we aren't scrolling beyond the end of our content
926 | int bottom = getChildAt(0).getBottom();
927 | int distanceToBottom = bottom - screenBottom;
928 | scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
929 |
930 | } else if (rect.top < screenTop && rect.bottom < screenBottom) {
931 | // need to move up to get it in view: move up just enough so that
932 | // entire rectangle is in view (or at least the first screen
933 | // size chunk of it).
934 |
935 | if (rect.height() > height) {
936 | // screen size chunk
937 | scrollYDelta -= (screenBottom - rect.bottom);
938 | } else {
939 | // entire rect at top
940 | scrollYDelta -= (screenTop - rect.top);
941 | }
942 |
943 | // make sure we aren't scrolling any further than the top our content
944 | scrollYDelta = Math.max(scrollYDelta, -getScrollY());
945 | }
946 | return scrollYDelta;
947 | }
948 |
949 | @Override
950 | public void requestChildFocus(View child, View focused) {
951 | if (!mIsLayoutDirty) {
952 | scrollToChild(focused);
953 | } else {
954 | // The child may not be laid out yet, we can't compute the scroll yet
955 | mChildToScrollTo = focused;
956 | }
957 | super.requestChildFocus(child, focused);
958 | }
959 |
960 |
961 | /**
962 | * When looking for focus in children of a scroll view, need to be a little
963 | * more careful not to give focus to something that is scrolled off screen.
964 | *
965 | * This is more expensive than the default {@link ViewGroup}
966 | * implementation, otherwise this behavior might have been made the default.
967 | */
968 | @Override
969 | protected boolean onRequestFocusInDescendants(int direction,
970 | Rect previouslyFocusedRect) {
971 |
972 | // convert from forward / backward notation to up / down / left / right
973 | // (ugh).
974 | if (direction == View.FOCUS_FORWARD) {
975 | direction = View.FOCUS_DOWN;
976 | } else if (direction == View.FOCUS_BACKWARD) {
977 | direction = View.FOCUS_UP;
978 | }
979 |
980 | final View nextFocus = previouslyFocusedRect == null ?
981 | FocusFinder.getInstance().findNextFocus(this, null, direction) :
982 | FocusFinder.getInstance().findNextFocusFromRect(this,
983 | previouslyFocusedRect, direction);
984 |
985 | return nextFocus != null && !isOffScreen(nextFocus) && nextFocus.requestFocus(direction, previouslyFocusedRect);
986 |
987 | }
988 |
989 | @Override
990 | public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
991 | boolean immediate) {
992 | // offset into coordinate space of this scroll view
993 | rectangle.offset(child.getLeft() - child.getScrollX(),
994 | child.getTop() - child.getScrollY());
995 |
996 | return scrollToChildRect(rectangle, immediate);
997 | }
998 |
999 | @Override
1000 | public void requestLayout() {
1001 | mIsLayoutDirty = true;
1002 | super.requestLayout();
1003 | }
1004 |
1005 | @Override
1006 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
1007 | super.onLayout(changed, l, t, r, b);
1008 | mIsLayoutDirty = false;
1009 | // Give a child focus if it needs it
1010 | if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
1011 | scrollToChild(mChildToScrollTo);
1012 | }
1013 | mChildToScrollTo = null;
1014 |
1015 | if (mSavedState != null) {
1016 | setScrollX(mSavedState.scrollXOffsetFromStart);
1017 | setScrollY(mSavedState.scrollYOffsetFromStart);
1018 | mSavedState = null;
1019 | }
1020 | // Don't forget to clamp
1021 | if (getScrollX() > getScrollRangeX()) {
1022 | setScrollX(getScrollRangeX());
1023 | } else if (getScrollX() < 0) {
1024 | setScrollX(0);
1025 | }
1026 | if (getScrollY() > getScrollRangeY()) {
1027 | setScrollY(getScrollRangeY());
1028 | } else if (getScrollY() < 0) {
1029 | setScrollY(0);
1030 | }
1031 |
1032 | // Calling this with the present values causes it to re-clam them
1033 | scrollTo(getScrollX(), getScrollY());
1034 | }
1035 |
1036 | @Override
1037 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1038 | super.onSizeChanged(w, h, oldw, oldh);
1039 |
1040 | View currentFocused = findFocus();
1041 | if (null == currentFocused || this == currentFocused)
1042 | return;
1043 |
1044 | // If the currently-focused view was visible on the screen when the
1045 | // screen was at the old height, then scroll the screen to make that
1046 | // view visible with the new screen height.
1047 | if (isWithinDeltaOfScreen(currentFocused, oldw, oldh)) {
1048 | currentFocused.getDrawingRect(mTempRect);
1049 | offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1050 | int scrollDeltaX = computeScrollDeltaXToGetChildRectOnScreen(mTempRect);
1051 | int scrollDeltaY = computeScrollDeltaYToGetChildRectOnScreen(mTempRect);
1052 | doScroll(scrollDeltaX, scrollDeltaY);
1053 | }
1054 | }
1055 |
1056 | /**
1057 | * Return true if child is an descendant of parent, (or equal to the parent).
1058 | */
1059 | private boolean isViewDescendantOf(View child, View parent) {
1060 | if (child == parent) {
1061 | return true;
1062 | }
1063 |
1064 | final ViewParent theParent = child.getParent();
1065 | return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
1066 | }
1067 |
1068 | /**
1069 | * Fling the scroll view
1070 | *
1071 | * @param velocityX The initial velocity in the X direction. Positive
1072 | * numbers mean that the finger/cursor is moving right the screen,
1073 | * which means we want to scroll towards the left.
1074 | * @param velocityY The initial velocity in the Y direction. Positive
1075 | * numbers mean that the finger/cursor is moving down the screen,
1076 | * which means we want to scroll towards the top.
1077 | */
1078 | private void fling(int velocityX, int velocityY) {
1079 | if (getChildCount() > 0) {
1080 | int width = getWidth() - getPaddingRight() - getPaddingLeft();
1081 | int right = getChildAt(0).getWidth();
1082 | int height = getHeight() - getPaddingBottom() - getPaddingTop();
1083 | int bottom = getChildAt(0).getHeight();
1084 |
1085 | mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, Math.max(0, right - width), 0,
1086 | Math.max(0, bottom - height), width / 2, height / 2);
1087 |
1088 | invalidate();
1089 | }
1090 | }
1091 |
1092 | private void endDrag() {
1093 | mIsBeingDragged = false;
1094 |
1095 | recycleVelocityTracker();
1096 | }
1097 |
1098 | /**
1099 | * {@inheritDoc}
1100 | *
1101 | *
This version also clamps the scrolling to the bounds of our child.
1102 | */
1103 | @Override
1104 | public void scrollTo(int x, int y) {
1105 | // we rely on the fact the View.scrollBy calls scrollTo.
1106 | if (getChildCount() > 0) {
1107 | View child = getChildAt(0);
1108 | x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
1109 | y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
1110 | if (x != getScrollX() || y != getScrollY()) {
1111 | super.scrollTo(x, y);
1112 | }
1113 | }
1114 | }
1115 |
1116 | private int clamp(int n, int my, int child) {
1117 | if (my >= child || n < 0) {
1118 | /* my >= child is this case:
1119 | * |--------------- me ---------------|
1120 | * |------ child ------|
1121 | * or
1122 | * |--------------- me ---------------|
1123 | * |------ child ------|
1124 | * or
1125 | * |--------------- me ---------------|
1126 | * |------ child ------|
1127 | *
1128 | * n < 0 is this case:
1129 | * |------ me ------|
1130 | * |-------- child --------|
1131 | * |-- getScrollX() --|
1132 | */
1133 | return 0;
1134 | }
1135 | if ((my + n) > child) {
1136 | /* this case:
1137 | * |------ me ------|
1138 | * |------ child ------|
1139 | * |-- getScrollX() --|
1140 | */
1141 | return child - my;
1142 | }
1143 | return n;
1144 | }
1145 |
1146 | @Override
1147 | protected void onRestoreInstanceState(Parcelable state) {
1148 | if (getContext().getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
1149 | // Some old apps reused IDs in ways they shouldn't have.
1150 | // Don't break them, but they don't get scroll state restoration.
1151 | super.onRestoreInstanceState(state);
1152 | return;
1153 | }
1154 | SavedState ss = (SavedState) state;
1155 | super.onRestoreInstanceState(ss.getSuperState());
1156 | mSavedState = ss;
1157 | requestLayout();
1158 | }
1159 |
1160 | @Override
1161 | protected Parcelable onSaveInstanceState() {
1162 | if (getContext().getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
1163 | // Some old apps reused IDs in ways they shouldn't have.
1164 | // Don't break them, but they don't get scroll state restoration.
1165 | return super.onSaveInstanceState();
1166 | }
1167 | Parcelable superState = super.onSaveInstanceState();
1168 | SavedState ss = new SavedState(superState);
1169 | ss.scrollXOffsetFromStart = getScrollX();
1170 | ss.scrollYOffsetFromStart = getScrollY();
1171 | return ss;
1172 | }
1173 |
1174 | public interface OnScrollListener {
1175 | void onScrollChanged(int left, int top, int oldLeft, int oldTop);
1176 | }
1177 |
1178 | static class SavedState extends BaseSavedState {
1179 | public static final Creator CREATOR
1180 | = new Creator() {
1181 | public SavedState createFromParcel(Parcel in) {
1182 | return new SavedState(in);
1183 | }
1184 |
1185 | public SavedState[] newArray(int size) {
1186 | return new SavedState[size];
1187 | }
1188 | };
1189 | int scrollXOffsetFromStart;
1190 | int scrollYOffsetFromStart;
1191 |
1192 | SavedState(Parcelable superState) {
1193 | super(superState);
1194 | }
1195 |
1196 | SavedState(Parcel source) {
1197 | super(source);
1198 | scrollXOffsetFromStart = source.readInt();
1199 | scrollYOffsetFromStart = source.readInt();
1200 | }
1201 |
1202 | @Override
1203 | public void writeToParcel(Parcel dest, int flags) {
1204 | super.writeToParcel(dest, flags);
1205 | dest.writeInt(scrollXOffsetFromStart);
1206 | dest.writeInt(scrollYOffsetFromStart);
1207 | }
1208 |
1209 | @Override
1210 | public String toString() {
1211 | return "HorizontalScrollView.SavedState{"
1212 | + Integer.toHexString(System.identityHashCode(this))
1213 | + " scrollXPosition=" + scrollXOffsetFromStart
1214 | + " scrollYPosition=" + scrollYOffsetFromStart
1215 | + "}";
1216 | }
1217 | }
1218 | }
--------------------------------------------------------------------------------