├── .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 | --------------------------------------------------------------------------------