├── .classpath
├── .gitignore
├── .project
├── AndroidManifest.xml
├── README.md
├── default.properties
├── proguard.cfg
├── res
├── drawable-hdpi
│ └── icon.png
├── drawable-ldpi
│ └── icon.png
├── drawable-mdpi
│ └── icon.png
├── layout
│ └── main.xml
└── values
│ └── strings.xml
└── src
└── com
└── grantlandchew
├── example
└── verticalpager
│ └── TestActivity.java
└── view
└── VerticalPager.java
/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # built application files
2 | *.apk
3 | *.ap_
4 |
5 | # files for the dex VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # generated GUI files
12 | *R.java
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | verticalpager-example
4 |
5 |
6 |
7 |
8 |
9 | com.android.ide.eclipse.adt.ResourceManagerBuilder
10 |
11 |
12 |
13 |
14 | com.android.ide.eclipse.adt.PreCompilerBuilder
15 |
16 |
17 |
18 |
19 | org.eclipse.jdt.core.javabuilder
20 |
21 |
22 |
23 |
24 | com.android.ide.eclipse.adt.ApkBuilder
25 |
26 |
27 |
28 |
29 |
30 | com.android.ide.eclipse.adt.AndroidNature
31 | org.eclipse.jdt.core.javanature
32 |
33 |
34 |
--------------------------------------------------------------------------------
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## VerticalPager
2 |
3 | Intended as a ViewGroup that mimics the UIScrollView vertical paging functionality of iOS.
4 |
5 | Supports:
6 |
7 | Each child inherits the width and height of the VerticalPager. Swipe to page up
8 | and down, or invoke scrollUp() and scrollDown() methods to accomplish the same.
9 | OnScrollListener will report scroll and scroll-finished events (use to implement
10 | a "current page number/position" view, for example). Much of the code for this
11 | class was adapted from the Workspace class from the official Launcher app (AOSP).
12 |
13 | modified from [http://code.google.com/p/deezapps-widgets/](http://code.google.com/p/deezapps-widgets/)
--------------------------------------------------------------------------------
/default.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system use,
7 | # "build.properties", and override values to adapt the script to your
8 | # project structure.
9 |
10 | # Project target.
11 | target=android-4
12 |
--------------------------------------------------------------------------------
/proguard.cfg:
--------------------------------------------------------------------------------
1 | -optimizationpasses 5
2 | -dontusemixedcaseclassnames
3 | -dontskipnonpubliclibraryclasses
4 | -dontpreverify
5 | -verbose
6 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
7 |
8 | -keep public class * extends android.app.Activity
9 | -keep public class * extends android.app.Application
10 | -keep public class * extends android.app.Service
11 | -keep public class * extends android.content.BroadcastReceiver
12 | -keep public class * extends android.content.ContentProvider
13 | -keep public class * extends android.app.backup.BackupAgentHelper
14 | -keep public class * extends android.preference.Preference
15 | -keep public class com.android.vending.licensing.ILicensingService
16 |
17 | -keepclasseswithmembernames class * {
18 | native ;
19 | }
20 |
21 | -keepclasseswithmembernames class * {
22 | public (android.content.Context, android.util.AttributeSet);
23 | }
24 |
25 | -keepclasseswithmembernames class * {
26 | public (android.content.Context, android.util.AttributeSet, int);
27 | }
28 |
29 | -keepclassmembers enum * {
30 | public static **[] values();
31 | public static ** valueOf(java.lang.String);
32 | }
33 |
34 | -keep class * implements android.os.Parcelable {
35 | public static final android.os.Parcelable$Creator *;
36 | }
37 |
--------------------------------------------------------------------------------
/res/drawable-hdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grantland/android-verticalpager/be106911947e3dddb6522dbadb4029cea32bd91e/res/drawable-hdpi/icon.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grantland/android-verticalpager/be106911947e3dddb6522dbadb4029cea32bd91e/res/drawable-ldpi/icon.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grantland/android-verticalpager/be106911947e3dddb6522dbadb4029cea32bd91e/res/drawable-mdpi/icon.png
--------------------------------------------------------------------------------
/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
14 |
22 |
28 |
29 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hello World, TestActivity!
4 | VerticalPager Example
5 |
6 |
--------------------------------------------------------------------------------
/src/com/grantlandchew/example/verticalpager/TestActivity.java:
--------------------------------------------------------------------------------
1 | package com.grantlandchew.example.verticalpager;
2 |
3 | /*
4 | * Copyright (C) 2011 Grantland Chew
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 | *
20 | * Based on http://code.google.com/p/deezapps-widgets/
21 | *
22 | * Copyright (C) 2010 Deez Apps!
23 | *
24 | * Licensed under the Apache License, Version 2.0 (the "License");
25 | * you may not use this file except in compliance with the License.
26 | * You may obtain a copy of the License at
27 | *
28 | * http://www.apache.org/licenses/LICENSE-2.0
29 | *
30 | * Unless required by applicable law or agreed to in writing, software
31 | * distributed under the License is distributed on an "AS IS" BASIS,
32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33 | * See the License for the specific language governing permissions and
34 | * limitations under the License.
35 | *
36 | * --
37 | *
38 | * Based on http://android.git.kernel.org/?p=platform/packages/apps/Launcher.git;a=blob;f=src/com/android/launcher/Workspace.java
39 | *
40 | * Copyright (C) 2008 The Android Open Source Project
41 | *
42 | * Licensed under the Apache License, Version 2.0 (the "License");
43 | * you may not use this file except in compliance with the License.
44 | * You may obtain a copy of the License at
45 | *
46 | * http://www.apache.org/licenses/LICENSE-2.0
47 | *
48 | * Unless required by applicable law or agreed to in writing, software
49 | * distributed under the License is distributed on an "AS IS" BASIS,
50 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
51 | * See the License for the specific language governing permissions and
52 | * limitations under the License.
53 | */
54 |
55 | import android.app.Activity;
56 | import android.os.Bundle;
57 | import android.widget.LinearLayout;
58 | import android.widget.TextView;
59 |
60 | import com.grantlandchew.view.VerticalPager;
61 |
62 | /**
63 | * @author Grantland Chew
64 | * @since Feb 13, 2011
65 | */
66 | public class TestActivity extends Activity {
67 | /**
68 | * Called when the activity is first created.
69 | */
70 | @Override
71 | public void onCreate(Bundle savedInstanceState) {
72 | super.onCreate(savedInstanceState);
73 | setContentView(R.layout.main);
74 |
75 | final VerticalPager pager = (VerticalPager) findViewById(R.id.pager);
76 | final LinearLayout list = (LinearLayout) findViewById(R.id.list);
77 |
78 | TextView text;
79 |
80 | for(int i = 0; i < 30; i++ ) {
81 | text = new TextView(this);
82 | text.setText("test: "+i);
83 | text.setTextSize(30);
84 | list.addView(text);
85 | }
86 |
87 | pager.addOnScrollListener(new VerticalPager.OnScrollListener() {
88 | public void onScroll(int scrollX) {
89 | //Log.d("TestActivity", "scrollX=" + scrollX);
90 | }
91 |
92 | public void onViewScrollFinished(int currentPage) {
93 | //Log.d("TestActivity", "viewIndex=" + currentPage);
94 | }
95 | });
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/com/grantlandchew/view/VerticalPager.java:
--------------------------------------------------------------------------------
1 | package com.grantlandchew.view;
2 |
3 | /*
4 | * Copyright (C) 2011 Grantland Chew
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 | *
20 | * Based on http://code.google.com/p/deezapps-widgets/
21 | *
22 | * Copyright (C) 2010 Deez Apps!
23 | *
24 | * Licensed under the Apache License, Version 2.0 (the "License");
25 | * you may not use this file except in compliance with the License.
26 | * You may obtain a copy of the License at
27 | *
28 | * http://www.apache.org/licenses/LICENSE-2.0
29 | *
30 | * Unless required by applicable law or agreed to in writing, software
31 | * distributed under the License is distributed on an "AS IS" BASIS,
32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33 | * See the License for the specific language governing permissions and
34 | * limitations under the License.
35 | *
36 | * --
37 | *
38 | * Based on http://android.git.kernel.org/?p=platform/packages/apps/Launcher.git;a=blob;f=src/com/android/launcher/Workspace.java
39 | *
40 | * Copyright (C) 2008 The Android Open Source Project
41 | *
42 | * Licensed under the Apache License, Version 2.0 (the "License");
43 | * you may not use this file except in compliance with the License.
44 | * You may obtain a copy of the License at
45 | *
46 | * http://www.apache.org/licenses/LICENSE-2.0
47 | *
48 | * Unless required by applicable law or agreed to in writing, software
49 | * distributed under the License is distributed on an "AS IS" BASIS,
50 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
51 | * See the License for the specific language governing permissions and
52 | * limitations under the License.
53 | */
54 |
55 | import java.util.ArrayList;
56 | import java.util.HashSet;
57 | import java.util.Set;
58 |
59 | import android.content.Context;
60 | import android.graphics.Canvas;
61 | import android.graphics.Rect;
62 | import android.os.Parcel;
63 | import android.os.Parcelable;
64 | import android.util.AttributeSet;
65 | import android.view.MotionEvent;
66 | import android.view.VelocityTracker;
67 | import android.view.View;
68 | import android.view.ViewConfiguration;
69 | import android.view.ViewGroup;
70 | import android.view.ViewParent;
71 | import android.view.animation.DecelerateInterpolator;
72 | import android.widget.Scroller;
73 |
74 | /**
75 | * @author Grantland Chew
76 | * @since Feb 13, 2011
77 | */
78 | public class VerticalPager extends ViewGroup {
79 | public static final String TAG = "VerticalPager";
80 |
81 | private static final int INVALID_SCREEN = -1;
82 | public static final int SPEC_UNDEFINED = -1;
83 | private static final int TOP = 0;
84 | private static final int BOTTOM = 1;
85 |
86 | /**
87 | * The velocity at which a fling gesture will cause us to snap to the next screen
88 | */
89 | private static final int SNAP_VELOCITY = 1000;
90 |
91 | private int pageHeight;
92 | private int measuredHeight;
93 |
94 | private boolean mFirstLayout = true;
95 |
96 | private int mCurrentPage;
97 | private int mNextPage = INVALID_SCREEN;
98 |
99 | private Scroller mScroller;
100 | private VelocityTracker mVelocityTracker;
101 |
102 | private int mTouchSlop;
103 | private int mMaximumVelocity;
104 |
105 | private float mLastMotionY;
106 | private float mLastMotionX;
107 |
108 | private final static int TOUCH_STATE_REST = 0;
109 | private final static int TOUCH_STATE_SCROLLING = 1;
110 |
111 | private int mTouchState = TOUCH_STATE_REST;
112 |
113 | private boolean mAllowLongPress;
114 |
115 | private Set mListeners = new HashSet();
116 |
117 | /**
118 | * Used to inflate the Workspace from XML.
119 | *
120 | * @param context The application's context.
121 | * @param attrs The attribtues set containing the Workspace's customization values.
122 | */
123 | public VerticalPager(Context context, AttributeSet attrs) {
124 | this(context, attrs, 0);
125 | }
126 |
127 | /**
128 | * Used to inflate the Workspace from XML.
129 | *
130 | * @param context The application's context.
131 | * @param attrs The attribtues set containing the Workspace's customization values.
132 | * @param defStyle Unused.
133 | */
134 | public VerticalPager(Context context, AttributeSet attrs, int defStyle) {
135 | super(context, attrs, defStyle);
136 |
137 | //TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.com_deezapps_widget_HorizontalPager);
138 | //pageHeightSpec = a.getDimensionPixelSize(R.styleable.com_deezapps_widget_HorizontalPager_pageWidth, SPEC_UNDEFINED);
139 | //a.recycle();
140 |
141 | init(context);
142 | }
143 |
144 | /**
145 | * Initializes various states for this workspace.
146 | */
147 | private void init(Context context) {
148 | mScroller = new Scroller(getContext(), new DecelerateInterpolator());
149 | mCurrentPage = 0;
150 |
151 | final ViewConfiguration configuration = ViewConfiguration.get(getContext());
152 | mTouchSlop = configuration.getScaledTouchSlop();
153 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
154 | }
155 |
156 | /**
157 | * Returns the index of the currently displayed page.
158 | *
159 | * @return The index of the currently displayed page.
160 | */
161 | int getCurrentPage() {
162 | return mCurrentPage;
163 | }
164 |
165 | /**
166 | * Sets the current page.
167 | *
168 | * @param currentPage
169 | */
170 | public void setCurrentPage(int currentPage) {
171 | mCurrentPage = Math.max(0, Math.min(currentPage, getChildCount()));
172 | scrollTo(getScrollYForPage(mCurrentPage), 0);
173 | invalidate();
174 | }
175 |
176 | public int getPageHeight() {
177 | return pageHeight;
178 | }
179 |
180 | //public void setPageHeight(int pageHeight) {
181 | // this.pageHeightSpec = pageHeight;
182 | //}
183 |
184 | /**
185 | * Gets the value that getScrollX() should return if the specified page is the current page (and no other scrolling is occurring).
186 | * Use this to pass a value to scrollTo(), for example.
187 | * @param whichPage
188 | * @return
189 | */
190 | private int getScrollYForPage(int whichPage) {
191 | int height = 0;
192 | for(int i = 0; i < whichPage; i++) {
193 | final View child = getChildAt(i);
194 | if (child.getVisibility() != View.GONE) {
195 | height += child.getHeight();
196 | }
197 | }
198 | return height - pageHeightPadding();
199 | }
200 |
201 | @Override
202 | public void computeScroll() {
203 | if (mScroller.computeScrollOffset()) {
204 | scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
205 | postInvalidate();
206 | } else if (mNextPage != INVALID_SCREEN) {
207 | mCurrentPage = mNextPage;
208 | mNextPage = INVALID_SCREEN;
209 | clearChildrenCache();
210 | }
211 | }
212 |
213 | @Override
214 | protected void dispatchDraw(Canvas canvas) {
215 |
216 | // ViewGroup.dispatchDraw() supports many features we don't need:
217 | // clip to padding, layout animation, animation listener, disappearing
218 | // children, etc. The following implementation attempts to fast-track
219 | // the drawing dispatch by drawing only what we know needs to be drawn.
220 |
221 | final long drawingTime = getDrawingTime();
222 | // todo be smarter about which children need drawing
223 | final int count = getChildCount();
224 | for (int i = 0; i < count; i++) {
225 | drawChild(canvas, getChildAt(i), drawingTime);
226 | }
227 |
228 | for (OnScrollListener mListener : mListeners) {
229 | int adjustedScrollY = getScrollY() + pageHeightPadding();
230 | mListener.onScroll(adjustedScrollY);
231 | if (adjustedScrollY % pageHeight == 0) {
232 | mListener.onViewScrollFinished(adjustedScrollY / pageHeight);
233 | }
234 | }
235 | }
236 |
237 | int pageHeightPadding() {
238 | return ((getMeasuredHeight() - pageHeight) / 2);
239 | }
240 |
241 | @Override
242 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
243 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
244 |
245 | pageHeight = getMeasuredHeight();
246 |
247 | final int count = getChildCount();
248 | for (int i = 0; i < count; i++) {
249 | getChildAt(i).measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
250 | MeasureSpec.makeMeasureSpec(pageHeight, MeasureSpec.UNSPECIFIED));
251 | }
252 |
253 | if (mFirstLayout) {
254 | scrollTo(getScrollYForPage(mCurrentPage), 0);
255 | mFirstLayout = false;
256 | }
257 | }
258 |
259 | @Override
260 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
261 | measuredHeight = 0;
262 |
263 | final int count = getChildCount();
264 | int height;
265 | for (int i = 0; i < count; i++) {
266 | final View child = getChildAt(i);
267 | if (child.getVisibility() != View.GONE) {
268 | if(i == 0) {
269 | child.getHeight();
270 | child.layout(0, measuredHeight, right - left, measuredHeight + (int)(pageHeight*.96));
271 | measuredHeight += (pageHeight*.96);
272 | } else {
273 | height = pageHeight * (int)Math.ceil((double)child.getMeasuredHeight()/(double)pageHeight);
274 | height = Math.max(pageHeight, height);
275 | child.layout(0, measuredHeight, right - left, measuredHeight + height);
276 | measuredHeight += height;
277 | }
278 | }
279 | }
280 | }
281 |
282 | @Override
283 | public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
284 | int screen = indexOfChild(child);
285 | if (screen != mCurrentPage || !mScroller.isFinished()) {
286 | return true;
287 | }
288 | return false;
289 | }
290 |
291 | @Override
292 | protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
293 | int focusableScreen;
294 | if (mNextPage != INVALID_SCREEN) {
295 | focusableScreen = mNextPage;
296 | } else {
297 | focusableScreen = mCurrentPage;
298 | }
299 | getChildAt(focusableScreen).requestFocus(direction, previouslyFocusedRect);
300 | return false;
301 | }
302 |
303 | @Override
304 | public boolean dispatchUnhandledMove(View focused, int direction) {
305 | if (direction == View.FOCUS_LEFT) {
306 | if (getCurrentPage() > 0) {
307 | snapToPage(getCurrentPage() - 1);
308 | return true;
309 | }
310 | } else if (direction == View.FOCUS_RIGHT) {
311 | if (getCurrentPage() < getChildCount() - 1) {
312 | snapToPage(getCurrentPage() + 1);
313 | return true;
314 | }
315 | }
316 | return super.dispatchUnhandledMove(focused, direction);
317 | }
318 |
319 | @Override
320 | public void addFocusables(ArrayList views, int direction) {
321 | getChildAt(mCurrentPage).addFocusables(views, direction);
322 | if (direction == View.FOCUS_LEFT) {
323 | if (mCurrentPage > 0) {
324 | getChildAt(mCurrentPage - 1).addFocusables(views, direction);
325 | }
326 | } else if (direction == View.FOCUS_RIGHT){
327 | if (mCurrentPage < getChildCount() - 1) {
328 | getChildAt(mCurrentPage + 1).addFocusables(views, direction);
329 | }
330 | }
331 | }
332 |
333 | @Override
334 | public boolean onInterceptTouchEvent(MotionEvent ev) {
335 | //Log.d(TAG, "onInterceptTouchEvent::action=" + ev.getAction());
336 |
337 | /*
338 | * This method JUST determines whether we want to intercept the motion.
339 | * If we return true, onTouchEvent will be called and we do the actual
340 | * scrolling there.
341 | */
342 |
343 | /*
344 | * Shortcut the most recurring case: the user is in the dragging
345 | * state and he is moving his finger. We want to intercept this
346 | * motion.
347 | */
348 | final int action = ev.getAction();
349 | if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) {
350 | //Log.d(TAG, "onInterceptTouchEvent::shortcut=true");
351 | return true;
352 | }
353 |
354 | final float y = ev.getY();
355 | final float x = ev.getX();
356 |
357 | switch (action) {
358 | case MotionEvent.ACTION_MOVE:
359 | /*
360 | * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
361 | * whether the user has moved far enough from his original down touch.
362 | */
363 | if (mTouchState == TOUCH_STATE_REST) {
364 | checkStartScroll(x, y);
365 | }
366 |
367 | break;
368 |
369 | case MotionEvent.ACTION_DOWN:
370 | // Remember location of down touch
371 | mLastMotionX = x;
372 | mLastMotionY = y;
373 | mAllowLongPress = true;
374 |
375 | /*
376 | * If being flinged and user touches the screen, initiate drag;
377 | * otherwise don't. mScroller.isFinished should be false when
378 | * being flinged.
379 | */
380 | mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
381 | break;
382 |
383 | case MotionEvent.ACTION_CANCEL:
384 | case MotionEvent.ACTION_UP:
385 | // Release the drag
386 | clearChildrenCache();
387 | mTouchState = TOUCH_STATE_REST;
388 | break;
389 | }
390 |
391 | /*
392 | * The only time we want to intercept motion events is if we are in the
393 | * drag mode.
394 | */
395 | return mTouchState != TOUCH_STATE_REST;
396 | }
397 |
398 | private void checkStartScroll(float x, float y) {
399 | /*
400 | * Locally do absolute value. mLastMotionX is set to the y value
401 | * of the down event.
402 | */
403 | final int xDiff = (int) Math.abs(x - mLastMotionX);
404 | final int yDiff = (int) Math.abs(y - mLastMotionY);
405 |
406 | boolean xMoved = xDiff > mTouchSlop;
407 | boolean yMoved = yDiff > mTouchSlop;
408 |
409 | if (xMoved || yMoved) {
410 |
411 | if (yMoved) {
412 | // Scroll if the user moved far enough along the X axis
413 | mTouchState = TOUCH_STATE_SCROLLING;
414 | enableChildrenCache();
415 | }
416 | // Either way, cancel any pending longpress
417 | if (mAllowLongPress) {
418 | mAllowLongPress = false;
419 | // Try canceling the long press. It could also have been scheduled
420 | // by a distant descendant, so use the mAllowLongPress flag to block
421 | // everything
422 | final View currentScreen = getChildAt(mCurrentPage);
423 | currentScreen.cancelLongPress();
424 | }
425 | }
426 | }
427 |
428 | void enableChildrenCache() {
429 | setChildrenDrawingCacheEnabled(true);
430 | setChildrenDrawnWithCacheEnabled(true);
431 | }
432 |
433 | void clearChildrenCache() {
434 | setChildrenDrawnWithCacheEnabled(false);
435 | }
436 |
437 | @Override
438 | public boolean onTouchEvent(MotionEvent ev) {
439 | if (mVelocityTracker == null) {
440 | mVelocityTracker = VelocityTracker.obtain();
441 | }
442 | mVelocityTracker.addMovement(ev);
443 |
444 | final int action = ev.getAction();
445 | final float x = ev.getX();
446 | final float y = ev.getY();
447 |
448 | switch (action) {
449 | case MotionEvent.ACTION_DOWN:
450 | /*
451 | * If being flinged and user touches, stop the fling. isFinished
452 | * will be false if being flinged.
453 | */
454 | if (!mScroller.isFinished()) {
455 | mScroller.abortAnimation();
456 | }
457 |
458 | // Remember where the motion event started
459 | mLastMotionY = y;
460 | break;
461 | case MotionEvent.ACTION_MOVE:
462 | if (mTouchState == TOUCH_STATE_REST) {
463 | checkStartScroll(y, x);
464 | } else if (mTouchState == TOUCH_STATE_SCROLLING) {
465 | // Scroll to follow the motion event
466 | int deltaY = (int) (mLastMotionY - y);
467 | mLastMotionY = y;
468 |
469 | // Apply friction to scrolling past boundaries.
470 | final int count = getChildCount();
471 | if (getScrollY() < 0 || getScrollY() + pageHeight > getChildAt(count - 1).getBottom()) {
472 | deltaY /= 2;
473 | }
474 |
475 | scrollBy(0, deltaY);
476 | }
477 | break;
478 | case MotionEvent.ACTION_UP:
479 | if (mTouchState == TOUCH_STATE_SCROLLING) {
480 | final VelocityTracker velocityTracker = mVelocityTracker;
481 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
482 | int velocityY = (int) velocityTracker.getYVelocity();
483 |
484 | final int count = getChildCount();
485 |
486 | // check scrolling past first or last page?
487 | if(getScrollY() < 0) {
488 | snapToPage(0);
489 | } else if(getScrollY() > measuredHeight - pageHeight) {
490 | snapToPage(count - 1, BOTTOM);
491 | } else {
492 | for(int i = 0; i < count; i++) {
493 | final View child = getChildAt(i);
494 | if(child.getTop() < getScrollY() &&
495 | child.getBottom() > getScrollY() + pageHeight) {
496 | // we're inside a page, fling that bitch
497 | mNextPage = i;
498 | mScroller.fling(getScrollX(), getScrollY(), 0, -velocityY, 0, 0, child.getTop(), child.getBottom() - getHeight());
499 | invalidate();
500 | break;
501 | } else if(child.getBottom() > getScrollY() && child.getBottom() < getScrollY() + getHeight()) {
502 | // stuck in between pages, oh snap!
503 | if(velocityY < -SNAP_VELOCITY) {
504 | snapToPage(i + 1);
505 | } else if(velocityY > SNAP_VELOCITY) {
506 | snapToPage(i, BOTTOM);
507 | } else if(getScrollY() + pageHeight/2 > child.getBottom()) {
508 | snapToPage(i + 1);
509 | } else {
510 | snapToPage(i, BOTTOM);
511 | }
512 | break;
513 | }
514 | }
515 | }
516 |
517 | if (mVelocityTracker != null) {
518 | mVelocityTracker.recycle();
519 | mVelocityTracker = null;
520 | }
521 | }
522 | mTouchState = TOUCH_STATE_REST;
523 | break;
524 | case MotionEvent.ACTION_CANCEL:
525 | mTouchState = TOUCH_STATE_REST;
526 | }
527 |
528 | return true;
529 | }
530 |
531 | private void snapToPage(final int whichPage, final int where) {
532 | enableChildrenCache();
533 |
534 | boolean changingPages = whichPage != mCurrentPage;
535 |
536 | mNextPage = whichPage;
537 |
538 | View focusedChild = getFocusedChild();
539 | if (focusedChild != null && changingPages && focusedChild == getChildAt(mCurrentPage)) {
540 | focusedChild.clearFocus();
541 | }
542 |
543 | final int delta;
544 | if(getChildAt(whichPage).getHeight() <= pageHeight || where == TOP) {
545 | delta = getChildAt(whichPage).getTop() - getScrollY();
546 | } else {
547 | delta = getChildAt(whichPage).getBottom() - pageHeight - getScrollY();
548 | }
549 |
550 | mScroller.startScroll(0, getScrollY(), 0, delta, 400);
551 | invalidate();
552 | }
553 |
554 | public void snapToPage(final int whichPage) {
555 | snapToPage(whichPage, TOP);
556 | }
557 |
558 | @Override
559 | protected Parcelable onSaveInstanceState() {
560 | final SavedState state = new SavedState(super.onSaveInstanceState());
561 | state.currentScreen = mCurrentPage;
562 | return state;
563 | }
564 |
565 | @Override
566 | protected void onRestoreInstanceState(Parcelable state) {
567 | SavedState savedState = (SavedState) state;
568 | super.onRestoreInstanceState(savedState.getSuperState());
569 | if (savedState.currentScreen != INVALID_SCREEN) {
570 | mCurrentPage = savedState.currentScreen;
571 | }
572 | }
573 |
574 | public void scrollUp() {
575 | if (mNextPage == INVALID_SCREEN && mCurrentPage > 0 && mScroller.isFinished()) {
576 | snapToPage(mCurrentPage - 1);
577 | }
578 | }
579 |
580 | public void scrollDown() {
581 | if (mNextPage == INVALID_SCREEN && mCurrentPage < getChildCount() - 1 && mScroller.isFinished()) {
582 | snapToPage(mCurrentPage + 1);
583 | }
584 | }
585 |
586 | public int getScreenForView(View v) {
587 | int result = -1;
588 | if (v != null) {
589 | ViewParent vp = v.getParent();
590 | int count = getChildCount();
591 | for (int i = 0; i < count; i++) {
592 | if (vp == getChildAt(i)) {
593 | return i;
594 | }
595 | }
596 | }
597 | return result;
598 | }
599 |
600 | /**
601 | * @return True is long presses are still allowed for the current touch
602 | */
603 | public boolean allowLongPress() {
604 | return mAllowLongPress;
605 | }
606 |
607 | public static class SavedState extends BaseSavedState {
608 | int currentScreen = -1;
609 |
610 | SavedState(Parcelable superState) {
611 | super(superState);
612 | }
613 |
614 | private SavedState(Parcel in) {
615 | super(in);
616 | currentScreen = in.readInt();
617 | }
618 |
619 | @Override
620 | public void writeToParcel(Parcel out, int flags) {
621 | super.writeToParcel(out, flags);
622 | out.writeInt(currentScreen);
623 | }
624 |
625 | public static final Parcelable.Creator CREATOR =
626 | new Parcelable.Creator() {
627 | public SavedState createFromParcel(Parcel in) {
628 | return new SavedState(in);
629 | }
630 |
631 | public SavedState[] newArray(int size) {
632 | return new SavedState[size];
633 | }
634 | };
635 | }
636 |
637 | public void addOnScrollListener(OnScrollListener listener) {
638 | mListeners.add(listener);
639 | }
640 |
641 | public void removeOnScrollListener(OnScrollListener listener) {
642 | mListeners.remove(listener);
643 | }
644 |
645 | /**
646 | * Implement to receive events on scroll position and page snaps.
647 | */
648 | public static interface OnScrollListener {
649 | /**
650 | * Receives the current scroll X value. This value will be adjusted to assume the left edge of the first
651 | * page has a scroll position of 0. Note that values less than 0 and greater than the right edge of the
652 | * last page are possible due to touch events scrolling beyond the edges.
653 | * @param scrollX Scroll X value
654 | */
655 | void onScroll(int scrollX);
656 |
657 | /**
658 | * Invoked when scrolling is finished (settled on a page, centered).
659 | * @param currentPage The current page
660 | */
661 | void onViewScrollFinished(int currentPage);
662 | }
663 | }
664 |
--------------------------------------------------------------------------------