blob: d56e7acdf70600ceebc9354f41bc9207095dbeb9 [file] [log] [blame]
Winson Chung321e9ee2010-08-09 13:37:56 -07001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.launcher2;
18
19import java.util.ArrayList;
Winson Chung86f77532010-08-24 11:08:22 -070020import java.util.Arrays;
Winson Chung321e9ee2010-08-09 13:37:56 -070021
22import android.content.Context;
23import android.graphics.Canvas;
24import android.graphics.Rect;
Winson Chung321e9ee2010-08-09 13:37:56 -070025import android.os.Parcel;
26import android.os.Parcelable;
27import android.util.AttributeSet;
Winson Chung321e9ee2010-08-09 13:37:56 -070028import android.view.MotionEvent;
29import android.view.VelocityTracker;
30import android.view.View;
31import android.view.ViewConfiguration;
32import android.view.ViewGroup;
33import android.view.ViewParent;
Winson Chung80baf5a2010-08-09 16:03:15 -070034import android.view.animation.Animation;
Winson Chung80baf5a2010-08-09 16:03:15 -070035import android.view.animation.Animation.AnimationListener;
Winson Chung86f77532010-08-24 11:08:22 -070036import android.view.animation.AnimationUtils;
Winson Chung321e9ee2010-08-09 13:37:56 -070037import android.widget.Scroller;
38
Winson Chung80baf5a2010-08-09 16:03:15 -070039import com.android.launcher.R;
40
Winson Chung321e9ee2010-08-09 13:37:56 -070041/**
42 * An abstraction of the original Workspace which supports browsing through a
43 * sequential list of "pages" (or PagedViewCellLayouts).
44 */
45public abstract class PagedView extends ViewGroup {
46 private static final String TAG = "PagedView";
Winson Chung86f77532010-08-24 11:08:22 -070047 private static final int INVALID_PAGE = -1;
Winson Chung321e9ee2010-08-09 13:37:56 -070048
Winson Chung86f77532010-08-24 11:08:22 -070049 // the velocity at which a fling gesture will cause us to snap to the next page
Winson Chung321e9ee2010-08-09 13:37:56 -070050 private static final int SNAP_VELOCITY = 500;
51
Winson Chung86f77532010-08-24 11:08:22 -070052 // the min drag distance for a fling to register, to prevent random page shifts
Winson Chung321e9ee2010-08-09 13:37:56 -070053 private static final int MIN_LENGTH_FOR_FLING = 50;
54
55 private boolean mFirstLayout = true;
56
Winson Chung86f77532010-08-24 11:08:22 -070057 private int mCurrentPage;
58 private int mNextPage = INVALID_PAGE;
Winson Chung321e9ee2010-08-09 13:37:56 -070059 private Scroller mScroller;
60 private VelocityTracker mVelocityTracker;
61
62 private float mDownMotionX;
63 private float mLastMotionX;
64 private float mLastMotionY;
65
66 private final static int TOUCH_STATE_REST = 0;
67 private final static int TOUCH_STATE_SCROLLING = 1;
68 private final static int TOUCH_STATE_PREV_PAGE = 2;
69 private final static int TOUCH_STATE_NEXT_PAGE = 3;
70
71 private int mTouchState = TOUCH_STATE_REST;
72
73 private OnLongClickListener mLongClickListener;
74
75 private boolean mAllowLongPress = true;
76
77 private int mTouchSlop;
78 private int mPagingTouchSlop;
79 private int mMaximumVelocity;
80
81 private static final int INVALID_POINTER = -1;
82
83 private int mActivePointerId = INVALID_POINTER;
84
Winson Chung86f77532010-08-24 11:08:22 -070085 private PageSwitchListener mPageSwitchListener;
Winson Chung321e9ee2010-08-09 13:37:56 -070086
Winson Chung86f77532010-08-24 11:08:22 -070087 private ArrayList<Boolean> mDirtyPageContent;
88 private boolean mDirtyPageAlpha;
Winson Chung321e9ee2010-08-09 13:37:56 -070089
Winson Chung86f77532010-08-24 11:08:22 -070090 public interface PageSwitchListener {
91 void onPageSwitch(View newPage, int newPageIndex);
Winson Chung321e9ee2010-08-09 13:37:56 -070092 }
93
Winson Chung321e9ee2010-08-09 13:37:56 -070094 public PagedView(Context context) {
95 this(context, null);
96 }
97
98 public PagedView(Context context, AttributeSet attrs) {
99 this(context, attrs, 0);
100 }
101
102 public PagedView(Context context, AttributeSet attrs, int defStyle) {
103 super(context, attrs, defStyle);
104
105 setHapticFeedbackEnabled(false);
106 initWorkspace();
107 }
108
109 /**
110 * Initializes various states for this workspace.
111 */
112 private void initWorkspace() {
Winson Chung86f77532010-08-24 11:08:22 -0700113 mDirtyPageContent = new ArrayList<Boolean>();
114 mDirtyPageContent.ensureCapacity(32);
Winson Chung321e9ee2010-08-09 13:37:56 -0700115 mScroller = new Scroller(getContext());
Winson Chung86f77532010-08-24 11:08:22 -0700116 mCurrentPage = 0;
Winson Chung321e9ee2010-08-09 13:37:56 -0700117
118 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
119 mTouchSlop = configuration.getScaledTouchSlop();
120 mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
121 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
122 }
123
Winson Chung86f77532010-08-24 11:08:22 -0700124 public void setPageSwitchListener(PageSwitchListener pageSwitchListener) {
125 mPageSwitchListener = pageSwitchListener;
126 if (mPageSwitchListener != null) {
127 mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700128 }
129 }
130
131 /**
Winson Chung86f77532010-08-24 11:08:22 -0700132 * Returns the index of the currently displayed page.
Winson Chung321e9ee2010-08-09 13:37:56 -0700133 *
Winson Chung86f77532010-08-24 11:08:22 -0700134 * @return The index of the currently displayed page.
Winson Chung321e9ee2010-08-09 13:37:56 -0700135 */
Winson Chung86f77532010-08-24 11:08:22 -0700136 int getCurrentPage() {
137 return mCurrentPage;
Winson Chung321e9ee2010-08-09 13:37:56 -0700138 }
139
Winson Chung86f77532010-08-24 11:08:22 -0700140 int getPageCount() {
Winson Chung321e9ee2010-08-09 13:37:56 -0700141 return getChildCount();
142 }
143
Winson Chung86f77532010-08-24 11:08:22 -0700144 View getPageAt(int index) {
Winson Chung321e9ee2010-08-09 13:37:56 -0700145 return getChildAt(index);
146 }
147
148 int getScrollWidth() {
149 return getWidth();
150 }
151
152 /**
Winson Chung86f77532010-08-24 11:08:22 -0700153 * Sets the current page.
Winson Chung321e9ee2010-08-09 13:37:56 -0700154 */
Winson Chung86f77532010-08-24 11:08:22 -0700155 void setCurrentPage(int currentPage) {
Winson Chung321e9ee2010-08-09 13:37:56 -0700156 if (!mScroller.isFinished()) mScroller.abortAnimation();
157 if (getChildCount() == 0) return;
158
Winson Chung86f77532010-08-24 11:08:22 -0700159 mCurrentPage = Math.max(0, Math.min(currentPage, getPageCount() - 1));
160 scrollTo(getChildOffset(mCurrentPage) - getRelativeChildOffset(mCurrentPage), 0);
Winson Chung80baf5a2010-08-09 16:03:15 -0700161
Winson Chung321e9ee2010-08-09 13:37:56 -0700162 invalidate();
Winson Chung86f77532010-08-24 11:08:22 -0700163 notifyPageSwitchListener();
Winson Chung321e9ee2010-08-09 13:37:56 -0700164 }
165
Winson Chung86f77532010-08-24 11:08:22 -0700166 private void notifyPageSwitchListener() {
167 if (mPageSwitchListener != null) {
168 mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700169 }
170 }
171
172 /**
Winson Chung86f77532010-08-24 11:08:22 -0700173 * Registers the specified listener on each page contained in this workspace.
Winson Chung321e9ee2010-08-09 13:37:56 -0700174 *
175 * @param l The listener used to respond to long clicks.
176 */
177 @Override
178 public void setOnLongClickListener(OnLongClickListener l) {
179 mLongClickListener = l;
Winson Chung86f77532010-08-24 11:08:22 -0700180 final int count = getPageCount();
Winson Chung321e9ee2010-08-09 13:37:56 -0700181 for (int i = 0; i < count; i++) {
Winson Chung86f77532010-08-24 11:08:22 -0700182 getPageAt(i).setOnLongClickListener(l);
Winson Chung321e9ee2010-08-09 13:37:56 -0700183 }
184 }
185
186 @Override
187 public void computeScroll() {
188 if (mScroller.computeScrollOffset()) {
189 scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
190 postInvalidate();
Winson Chung86f77532010-08-24 11:08:22 -0700191 } else if (mNextPage != INVALID_PAGE) {
192 mCurrentPage = Math.max(0, Math.min(mNextPage, getPageCount() - 1));
193 notifyPageSwitchListener();
194 mNextPage = INVALID_PAGE;
Winson Chung321e9ee2010-08-09 13:37:56 -0700195 }
196 }
197
198 @Override
199 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
200 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
201 final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
202 if (widthMode != MeasureSpec.EXACTLY) {
203 throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
204 }
205
206 final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
207 final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
208 if (heightMode != MeasureSpec.EXACTLY) {
209 throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
210 }
211
212 // The children are given the same width and height as the workspace
213 final int childCount = getChildCount();
214 for (int i = 0; i < childCount; i++) {
215 getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
216 }
217
218 setMeasuredDimension(widthSize, heightSize);
219
220 if (mFirstLayout) {
221 setHorizontalScrollBarEnabled(false);
Winson Chung86f77532010-08-24 11:08:22 -0700222 scrollTo(mCurrentPage * widthSize, 0);
Winson Chung321e9ee2010-08-09 13:37:56 -0700223 setHorizontalScrollBarEnabled(true);
224 mFirstLayout = false;
225 }
226 }
227
228 @Override
229 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
230 final int childCount = getChildCount();
231 int childLeft = 0;
232 if (childCount > 0) {
233 childLeft = (getMeasuredWidth() - getChildAt(0).getMeasuredWidth()) / 2;
234 }
235
236 for (int i = 0; i < childCount; i++) {
237 final View child = getChildAt(i);
238 if (child.getVisibility() != View.GONE) {
239 final int childWidth = child.getMeasuredWidth();
240 child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
241 childLeft += childWidth;
242 }
243 }
244 }
245
Winson Chung321e9ee2010-08-09 13:37:56 -0700246 @Override
247 protected void dispatchDraw(Canvas canvas) {
Winson Chung86f77532010-08-24 11:08:22 -0700248 if (mDirtyPageAlpha || (mTouchState == TOUCH_STATE_SCROLLING) || !mScroller.isFinished()) {
Winson Chung321e9ee2010-08-09 13:37:56 -0700249 int screenCenter = mScrollX + (getMeasuredWidth() / 2);
250 final int childCount = getChildCount();
251 for (int i = 0; i < childCount; ++i) {
252 PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
253 int childWidth = layout.getMeasuredWidth();
254 int halfChildWidth = (childWidth / 2);
255 int childCenter = getChildOffset(i) + halfChildWidth;
256 int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
Winson Chungb3347bb2010-08-19 14:51:28 -0700257 float alpha = 0.0f;
Winson Chung321e9ee2010-08-09 13:37:56 -0700258 if (distanceFromScreenCenter < halfChildWidth) {
Winson Chungb3347bb2010-08-19 14:51:28 -0700259 alpha = 1.0f;
Winson Chung321e9ee2010-08-09 13:37:56 -0700260 } else if (distanceFromScreenCenter > childWidth) {
Winson Chungb3347bb2010-08-19 14:51:28 -0700261 alpha = 0.0f;
Winson Chung321e9ee2010-08-09 13:37:56 -0700262 } else {
Winson Chungb3347bb2010-08-19 14:51:28 -0700263 float dimAlpha = (float) (distanceFromScreenCenter - halfChildWidth) / halfChildWidth;
264 dimAlpha = Math.max(0.0f, Math.min(1.0f, (dimAlpha * dimAlpha)));
265 alpha = 1.0f - dimAlpha;
Winson Chung321e9ee2010-08-09 13:37:56 -0700266 }
Winson Chungaffd7b42010-08-20 15:11:56 -0700267 if (Float.compare(alpha, layout.getAlpha()) != 0) {
Winson Chungb3347bb2010-08-19 14:51:28 -0700268 layout.setAlpha(alpha);
Winson Chungaffd7b42010-08-20 15:11:56 -0700269 }
Winson Chung321e9ee2010-08-09 13:37:56 -0700270 }
Winson Chung86f77532010-08-24 11:08:22 -0700271 mDirtyPageAlpha = false;
Winson Chung321e9ee2010-08-09 13:37:56 -0700272 }
273 super.dispatchDraw(canvas);
274 }
275
276 @Override
277 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
Winson Chung86f77532010-08-24 11:08:22 -0700278 int page = indexOfChild(child);
279 if (page != mCurrentPage || !mScroller.isFinished()) {
280 snapToPage(page);
Winson Chung321e9ee2010-08-09 13:37:56 -0700281 return true;
282 }
283 return false;
284 }
285
286 @Override
287 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
Winson Chung86f77532010-08-24 11:08:22 -0700288 int focusablePage;
289 if (mNextPage != INVALID_PAGE) {
290 focusablePage = mNextPage;
Winson Chung321e9ee2010-08-09 13:37:56 -0700291 } else {
Winson Chung86f77532010-08-24 11:08:22 -0700292 focusablePage = mCurrentPage;
Winson Chung321e9ee2010-08-09 13:37:56 -0700293 }
Winson Chung86f77532010-08-24 11:08:22 -0700294 View v = getPageAt(focusablePage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700295 if (v != null) {
296 v.requestFocus(direction, previouslyFocusedRect);
297 }
298 return false;
299 }
300
301 @Override
302 public boolean dispatchUnhandledMove(View focused, int direction) {
303 if (direction == View.FOCUS_LEFT) {
Winson Chung86f77532010-08-24 11:08:22 -0700304 if (getCurrentPage() > 0) {
305 snapToPage(getCurrentPage() - 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700306 return true;
307 }
308 } else if (direction == View.FOCUS_RIGHT) {
Winson Chung86f77532010-08-24 11:08:22 -0700309 if (getCurrentPage() < getPageCount() - 1) {
310 snapToPage(getCurrentPage() + 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700311 return true;
312 }
313 }
314 return super.dispatchUnhandledMove(focused, direction);
315 }
316
317 @Override
318 public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
Winson Chung86f77532010-08-24 11:08:22 -0700319 if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) {
320 getPageAt(mCurrentPage).addFocusables(views, direction);
Winson Chung321e9ee2010-08-09 13:37:56 -0700321 }
322 if (direction == View.FOCUS_LEFT) {
Winson Chung86f77532010-08-24 11:08:22 -0700323 if (mCurrentPage > 0) {
324 getPageAt(mCurrentPage - 1).addFocusables(views, direction);
Winson Chung321e9ee2010-08-09 13:37:56 -0700325 }
326 } else if (direction == View.FOCUS_RIGHT){
Winson Chung86f77532010-08-24 11:08:22 -0700327 if (mCurrentPage < getPageCount() - 1) {
328 getPageAt(mCurrentPage + 1).addFocusables(views, direction);
Winson Chung321e9ee2010-08-09 13:37:56 -0700329 }
330 }
331 }
332
333 /**
334 * If one of our descendant views decides that it could be focused now, only
Winson Chung86f77532010-08-24 11:08:22 -0700335 * pass that along if it's on the current page.
Winson Chung321e9ee2010-08-09 13:37:56 -0700336 *
Winson Chung86f77532010-08-24 11:08:22 -0700337 * This happens when live folders requery, and if they're off page, they
338 * end up calling requestFocus, which pulls it on page.
Winson Chung321e9ee2010-08-09 13:37:56 -0700339 */
340 @Override
341 public void focusableViewAvailable(View focused) {
Winson Chung86f77532010-08-24 11:08:22 -0700342 View current = getPageAt(mCurrentPage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700343 View v = focused;
344 while (true) {
345 if (v == current) {
346 super.focusableViewAvailable(focused);
347 return;
348 }
349 if (v == this) {
350 return;
351 }
352 ViewParent parent = v.getParent();
353 if (parent instanceof View) {
354 v = (View)v.getParent();
355 } else {
356 return;
357 }
358 }
359 }
360
361 /**
362 * {@inheritDoc}
363 */
364 @Override
365 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
366 if (disallowIntercept) {
367 // We need to make sure to cancel our long press if
368 // a scrollable widget takes over touch events
Winson Chung86f77532010-08-24 11:08:22 -0700369 final View currentPage = getChildAt(mCurrentPage);
370 currentPage.cancelLongPress();
Winson Chung321e9ee2010-08-09 13:37:56 -0700371 }
372 super.requestDisallowInterceptTouchEvent(disallowIntercept);
373 }
374
375 @Override
376 public boolean onInterceptTouchEvent(MotionEvent ev) {
377 /*
378 * This method JUST determines whether we want to intercept the motion.
379 * If we return true, onTouchEvent will be called and we do the actual
380 * scrolling there.
381 */
382
383 /*
384 * Shortcut the most recurring case: the user is in the dragging
385 * state and he is moving his finger. We want to intercept this
386 * motion.
387 */
388 final int action = ev.getAction();
389 if ((action == MotionEvent.ACTION_MOVE) &&
390 (mTouchState == TOUCH_STATE_SCROLLING)) {
391 return true;
392 }
393
394
395 switch (action & MotionEvent.ACTION_MASK) {
396 case MotionEvent.ACTION_MOVE: {
397 /*
398 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
399 * whether the user has moved far enough from his original down touch.
400 */
401 determineScrollingStart(ev);
402 break;
403 }
404
405 case MotionEvent.ACTION_DOWN: {
406 final float x = ev.getX();
407 final float y = ev.getY();
408 // Remember location of down touch
409 mDownMotionX = x;
410 mLastMotionX = x;
411 mLastMotionY = y;
412 mActivePointerId = ev.getPointerId(0);
413 mAllowLongPress = true;
414
415 /*
416 * If being flinged and user touches the screen, initiate drag;
417 * otherwise don't. mScroller.isFinished should be false when
418 * being flinged.
419 */
420 mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
421
Winson Chung86f77532010-08-24 11:08:22 -0700422 // check if this can be the beginning of a tap on the side of the pages
Winson Chung321e9ee2010-08-09 13:37:56 -0700423 // to scroll the current page
424 if ((mTouchState != TOUCH_STATE_PREV_PAGE) &&
425 (mTouchState != TOUCH_STATE_NEXT_PAGE)) {
426 if (getChildCount() > 0) {
427 int relativeChildLeft = getChildOffset(0);
428 int relativeChildRight = relativeChildLeft + getChildAt(0).getMeasuredWidth();
429 if (x < relativeChildLeft) {
430 mTouchState = TOUCH_STATE_PREV_PAGE;
431 } else if (x > relativeChildRight) {
432 mTouchState = TOUCH_STATE_NEXT_PAGE;
433 }
434 }
435 }
436 break;
437 }
438
439 case MotionEvent.ACTION_CANCEL:
440 case MotionEvent.ACTION_UP:
441 // Release the drag
442 mTouchState = TOUCH_STATE_REST;
443 mAllowLongPress = false;
444 mActivePointerId = INVALID_POINTER;
445
446 break;
447
448 case MotionEvent.ACTION_POINTER_UP:
449 onSecondaryPointerUp(ev);
450 break;
451 }
452
453 /*
454 * The only time we want to intercept motion events is if we are in the
455 * drag mode.
456 */
457 return mTouchState != TOUCH_STATE_REST;
458 }
459
Winson Chung80baf5a2010-08-09 16:03:15 -0700460 protected void animateClickFeedback(View v, final Runnable r) {
461 // animate the view slightly to show click feedback running some logic after it is "pressed"
462 Animation anim = AnimationUtils.loadAnimation(getContext(),
463 R.anim.paged_view_click_feedback);
464 anim.setAnimationListener(new AnimationListener() {
465 @Override
466 public void onAnimationStart(Animation animation) {}
467 @Override
468 public void onAnimationRepeat(Animation animation) {
469 r.run();
470 }
471 @Override
472 public void onAnimationEnd(Animation animation) {}
473 });
474 v.startAnimation(anim);
475 }
476
Winson Chung321e9ee2010-08-09 13:37:56 -0700477 /*
478 * Determines if we should change the touch state to start scrolling after the
479 * user moves their touch point too far.
480 */
481 private void determineScrollingStart(MotionEvent ev) {
482 /*
483 * Locally do absolute value. mLastMotionX is set to the y value
484 * of the down event.
485 */
486 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
487 final float x = ev.getX(pointerIndex);
488 final float y = ev.getY(pointerIndex);
489 final int xDiff = (int) Math.abs(x - mLastMotionX);
490 final int yDiff = (int) Math.abs(y - mLastMotionY);
491
492 final int touchSlop = mTouchSlop;
493 boolean xPaged = xDiff > mPagingTouchSlop;
494 boolean xMoved = xDiff > touchSlop;
495 boolean yMoved = yDiff > touchSlop;
496
497 if (xMoved || yMoved) {
498 if (xPaged) {
499 // Scroll if the user moved far enough along the X axis
500 mTouchState = TOUCH_STATE_SCROLLING;
501 mLastMotionX = x;
502 }
503 // Either way, cancel any pending longpress
504 if (mAllowLongPress) {
505 mAllowLongPress = false;
506 // Try canceling the long press. It could also have been scheduled
507 // by a distant descendant, so use the mAllowLongPress flag to block
508 // everything
Winson Chung86f77532010-08-24 11:08:22 -0700509 final View currentPage = getPageAt(mCurrentPage);
510 currentPage.cancelLongPress();
Winson Chung321e9ee2010-08-09 13:37:56 -0700511 }
512 }
513 }
514
515 @Override
516 public boolean onTouchEvent(MotionEvent ev) {
517 if (mVelocityTracker == null) {
518 mVelocityTracker = VelocityTracker.obtain();
519 }
520 mVelocityTracker.addMovement(ev);
521
522 final int action = ev.getAction();
523
524 switch (action & MotionEvent.ACTION_MASK) {
525 case MotionEvent.ACTION_DOWN:
526 /*
527 * If being flinged and user touches, stop the fling. isFinished
528 * will be false if being flinged.
529 */
530 if (!mScroller.isFinished()) {
531 mScroller.abortAnimation();
532 }
533
534 // Remember where the motion event started
535 mDownMotionX = mLastMotionX = ev.getX();
536 mActivePointerId = ev.getPointerId(0);
537 break;
538
539 case MotionEvent.ACTION_MOVE:
540 if (mTouchState == TOUCH_STATE_SCROLLING) {
541 // Scroll to follow the motion event
542 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
543 final float x = ev.getX(pointerIndex);
544 final int deltaX = (int) (mLastMotionX - x);
545 mLastMotionX = x;
546
547 int sx = getScrollX();
548 if (deltaX < 0) {
549 if (sx > 0) {
550 scrollBy(Math.max(-sx, deltaX), 0);
551 }
552 } else if (deltaX > 0) {
553 final int lastChildIndex = getChildCount() - 1;
554 final int availableToScroll = getChildOffset(lastChildIndex) -
555 getRelativeChildOffset(lastChildIndex) - sx;
556 if (availableToScroll > 0) {
557 scrollBy(Math.min(availableToScroll, deltaX), 0);
558 }
559 } else {
560 awakenScrollBars();
561 }
562 } else if ((mTouchState == TOUCH_STATE_PREV_PAGE) ||
563 (mTouchState == TOUCH_STATE_NEXT_PAGE)) {
564 determineScrollingStart(ev);
565 }
566 break;
567
568 case MotionEvent.ACTION_UP:
569 if (mTouchState == TOUCH_STATE_SCROLLING) {
570 final int activePointerId = mActivePointerId;
571 final int pointerIndex = ev.findPointerIndex(activePointerId);
572 final float x = ev.getX(pointerIndex);
573 final VelocityTracker velocityTracker = mVelocityTracker;
574 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
575 int velocityX = (int) velocityTracker.getXVelocity(activePointerId);
576 boolean isfling = Math.abs(mDownMotionX - x) > MIN_LENGTH_FOR_FLING;
577
Winson Chung86f77532010-08-24 11:08:22 -0700578 if (isfling && velocityX > SNAP_VELOCITY && mCurrentPage > 0) {
579 snapToPage(mCurrentPage - 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700580 } else if (isfling && velocityX < -SNAP_VELOCITY &&
Winson Chung86f77532010-08-24 11:08:22 -0700581 mCurrentPage < getChildCount() - 1) {
582 snapToPage(mCurrentPage + 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700583 } else {
584 snapToDestination();
585 }
586
587 if (mVelocityTracker != null) {
588 mVelocityTracker.recycle();
589 mVelocityTracker = null;
590 }
591 } else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
592 // at this point we have not moved beyond the touch slop
593 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
594 // we can just page
Winson Chung86f77532010-08-24 11:08:22 -0700595 int nextPage = Math.max(0, mCurrentPage - 1);
596 if (nextPage != mCurrentPage) {
597 snapToPage(nextPage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700598 } else {
599 snapToDestination();
600 }
601 } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) {
602 // at this point we have not moved beyond the touch slop
603 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
604 // we can just page
Winson Chung86f77532010-08-24 11:08:22 -0700605 int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1);
606 if (nextPage != mCurrentPage) {
607 snapToPage(nextPage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700608 } else {
609 snapToDestination();
610 }
611 }
612 mTouchState = TOUCH_STATE_REST;
613 mActivePointerId = INVALID_POINTER;
614 break;
615
616 case MotionEvent.ACTION_CANCEL:
617 mTouchState = TOUCH_STATE_REST;
618 mActivePointerId = INVALID_POINTER;
619 break;
620
621 case MotionEvent.ACTION_POINTER_UP:
622 onSecondaryPointerUp(ev);
623 break;
624 }
625
626 return true;
627 }
628
629 private void onSecondaryPointerUp(MotionEvent ev) {
630 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
631 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
632 final int pointerId = ev.getPointerId(pointerIndex);
633 if (pointerId == mActivePointerId) {
634 // This was our active pointer going up. Choose a new
635 // active pointer and adjust accordingly.
636 // TODO: Make this decision more intelligent.
637 final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
638 mLastMotionX = mDownMotionX = ev.getX(newPointerIndex);
639 mLastMotionY = ev.getY(newPointerIndex);
640 mActivePointerId = ev.getPointerId(newPointerIndex);
641 if (mVelocityTracker != null) {
642 mVelocityTracker.clear();
643 }
644 }
645 }
646
647 @Override
648 public void requestChildFocus(View child, View focused) {
649 super.requestChildFocus(child, focused);
Winson Chung86f77532010-08-24 11:08:22 -0700650 int page = indexOfChild(child);
651 if (page >= 0 && !isInTouchMode()) {
652 snapToPage(page);
Winson Chung321e9ee2010-08-09 13:37:56 -0700653 }
654 }
655
656 protected int getRelativeChildOffset(int index) {
657 return (getMeasuredWidth() - getChildAt(index).getMeasuredWidth()) / 2;
658 }
659
660 protected int getChildOffset(int index) {
661 if (getChildCount() == 0)
662 return 0;
663
664 int offset = getRelativeChildOffset(0);
665 for (int i = 0; i < index; ++i) {
666 offset += getChildAt(i).getMeasuredWidth();
667 }
668 return offset;
669 }
670
671 protected void snapToDestination() {
672 int minDistanceFromScreenCenter = getMeasuredWidth();
673 int minDistanceFromScreenCenterIndex = -1;
674 int screenCenter = mScrollX + (getMeasuredWidth() / 2);
675 final int childCount = getChildCount();
676 for (int i = 0; i < childCount; ++i) {
677 PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
678 int childWidth = layout.getMeasuredWidth();
679 int halfChildWidth = (childWidth / 2);
680 int childCenter = getChildOffset(i) + halfChildWidth;
681 int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
682 if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
683 minDistanceFromScreenCenter = distanceFromScreenCenter;
684 minDistanceFromScreenCenterIndex = i;
685 }
686 }
Winson Chung86f77532010-08-24 11:08:22 -0700687 snapToPage(minDistanceFromScreenCenterIndex, 1000);
Winson Chung321e9ee2010-08-09 13:37:56 -0700688 }
689
Winson Chung86f77532010-08-24 11:08:22 -0700690 void snapToPage(int whichPage) {
691 snapToPage(whichPage, 1000);
Winson Chung321e9ee2010-08-09 13:37:56 -0700692 }
693
Winson Chung86f77532010-08-24 11:08:22 -0700694 void snapToPage(int whichPage, int duration) {
695 whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1));
Winson Chung321e9ee2010-08-09 13:37:56 -0700696
Winson Chung86f77532010-08-24 11:08:22 -0700697 mNextPage = whichPage;
Winson Chung321e9ee2010-08-09 13:37:56 -0700698
Winson Chung86f77532010-08-24 11:08:22 -0700699 int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700700 final int sX = getScrollX();
701 final int delta = newX - sX;
702 awakenScrollBars(duration);
703 if (duration == 0) {
704 duration = Math.abs(delta);
705 }
706
707 if (!mScroller.isFinished()) mScroller.abortAnimation();
708 mScroller.startScroll(sX, 0, delta, 0, duration);
Winson Chung80baf5a2010-08-09 16:03:15 -0700709
710 // only load some associated pages
Winson Chung86f77532010-08-24 11:08:22 -0700711 loadAssociatedPages(mNextPage);
Winson Chung80baf5a2010-08-09 16:03:15 -0700712
Winson Chung321e9ee2010-08-09 13:37:56 -0700713 invalidate();
714 }
715
716 @Override
717 protected Parcelable onSaveInstanceState() {
718 final SavedState state = new SavedState(super.onSaveInstanceState());
Winson Chung86f77532010-08-24 11:08:22 -0700719 state.currentPage = mCurrentPage;
Winson Chung321e9ee2010-08-09 13:37:56 -0700720 return state;
721 }
722
723 @Override
724 protected void onRestoreInstanceState(Parcelable state) {
725 SavedState savedState = (SavedState) state;
726 super.onRestoreInstanceState(savedState.getSuperState());
Winson Chung86f77532010-08-24 11:08:22 -0700727 if (savedState.currentPage != -1) {
728 mCurrentPage = savedState.currentPage;
Winson Chung321e9ee2010-08-09 13:37:56 -0700729 }
730 }
731
732 public void scrollLeft() {
733 if (mScroller.isFinished()) {
Winson Chung86f77532010-08-24 11:08:22 -0700734 if (mCurrentPage > 0) snapToPage(mCurrentPage - 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700735 } else {
Winson Chung86f77532010-08-24 11:08:22 -0700736 if (mNextPage > 0) snapToPage(mNextPage - 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700737 }
738 }
739
740 public void scrollRight() {
741 if (mScroller.isFinished()) {
Winson Chung86f77532010-08-24 11:08:22 -0700742 if (mCurrentPage < getChildCount() -1) snapToPage(mCurrentPage + 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700743 } else {
Winson Chung86f77532010-08-24 11:08:22 -0700744 if (mNextPage < getChildCount() -1) snapToPage(mNextPage + 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700745 }
746 }
747
Winson Chung86f77532010-08-24 11:08:22 -0700748 public int getPageForView(View v) {
Winson Chung321e9ee2010-08-09 13:37:56 -0700749 int result = -1;
750 if (v != null) {
751 ViewParent vp = v.getParent();
752 int count = getChildCount();
753 for (int i = 0; i < count; i++) {
754 if (vp == getChildAt(i)) {
755 return i;
756 }
757 }
758 }
759 return result;
760 }
761
762 /**
763 * @return True is long presses are still allowed for the current touch
764 */
765 public boolean allowLongPress() {
766 return mAllowLongPress;
767 }
768
769 public static class SavedState extends BaseSavedState {
Winson Chung86f77532010-08-24 11:08:22 -0700770 int currentPage = -1;
Winson Chung321e9ee2010-08-09 13:37:56 -0700771
772 SavedState(Parcelable superState) {
773 super(superState);
774 }
775
776 private SavedState(Parcel in) {
777 super(in);
Winson Chung86f77532010-08-24 11:08:22 -0700778 currentPage = in.readInt();
Winson Chung321e9ee2010-08-09 13:37:56 -0700779 }
780
781 @Override
782 public void writeToParcel(Parcel out, int flags) {
783 super.writeToParcel(out, flags);
Winson Chung86f77532010-08-24 11:08:22 -0700784 out.writeInt(currentPage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700785 }
786
787 public static final Parcelable.Creator<SavedState> CREATOR =
788 new Parcelable.Creator<SavedState>() {
789 public SavedState createFromParcel(Parcel in) {
790 return new SavedState(in);
791 }
792
793 public SavedState[] newArray(int size) {
794 return new SavedState[size];
795 }
796 };
797 }
798
Winson Chung86f77532010-08-24 11:08:22 -0700799 public void loadAssociatedPages(int page) {
Winson Chung80baf5a2010-08-09 16:03:15 -0700800 final int count = getChildCount();
Winson Chung86f77532010-08-24 11:08:22 -0700801 if (page < count) {
802 int lowerPageBound = Math.max(0, page - 1);
803 int upperPageBound = Math.min(page + 1, count - 1);
Winson Chung80baf5a2010-08-09 16:03:15 -0700804 for (int i = 0; i < count; ++i) {
Winson Chung86f77532010-08-24 11:08:22 -0700805 final PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
806 final int childCount = layout.getChildCount();
807 if (lowerPageBound <= i && i <= upperPageBound) {
808 if (mDirtyPageContent.get(i)) {
809 syncPageItems(i);
810 mDirtyPageContent.set(i, false);
811 }
Winson Chung80baf5a2010-08-09 16:03:15 -0700812 } else {
Winson Chung86f77532010-08-24 11:08:22 -0700813 if (childCount > 0) {
Winson Chung80baf5a2010-08-09 16:03:15 -0700814 layout.removeAllViews();
815 }
Winson Chung86f77532010-08-24 11:08:22 -0700816 mDirtyPageContent.set(i, true);
Winson Chung80baf5a2010-08-09 16:03:15 -0700817 }
818 }
Winson Chung80baf5a2010-08-09 16:03:15 -0700819 }
820 }
821
Winson Chung86f77532010-08-24 11:08:22 -0700822 /**
823 * This method is called ONLY to synchronize the number of pages that the paged view has.
824 * To actually fill the pages with information, implement syncPageItems() below. It is
825 * guaranteed that syncPageItems() will be called for a particular page before it is shown,
826 * and therefore, individual page items do not need to be updated in this method.
827 */
Winson Chung321e9ee2010-08-09 13:37:56 -0700828 public abstract void syncPages();
Winson Chung86f77532010-08-24 11:08:22 -0700829
830 /**
831 * This method is called to synchronize the items that are on a particular page. If views on
832 * the page can be reused, then they should be updated within this method.
833 */
Winson Chung321e9ee2010-08-09 13:37:56 -0700834 public abstract void syncPageItems(int page);
Winson Chung86f77532010-08-24 11:08:22 -0700835
Winson Chung321e9ee2010-08-09 13:37:56 -0700836 public void invalidatePageData() {
Winson Chung86f77532010-08-24 11:08:22 -0700837 // Update all the pages
Winson Chung321e9ee2010-08-09 13:37:56 -0700838 syncPages();
Winson Chung86f77532010-08-24 11:08:22 -0700839
840 // Mark each of the pages as dirty
841 final int count = getChildCount();
842 mDirtyPageContent.clear();
843 for (int i = 0; i < count; ++i) {
844 mDirtyPageContent.add(true);
845 }
846
847 // Load any pages that are necessary for the current window of views
848 loadAssociatedPages(mCurrentPage);
849 mDirtyPageAlpha = true;
Winson Chung321e9ee2010-08-09 13:37:56 -0700850 requestLayout();
851 }
852}