blob: 6154947972d8e9e0a68dabcdcf9df6d1177a7307 [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 Chung241c3b42010-08-25 16:53:03 -070021import java.util.HashMap;
Winson Chung321e9ee2010-08-09 13:37:56 -070022
23import android.content.Context;
Winson Chung241c3b42010-08-25 16:53:03 -070024import android.graphics.Bitmap;
Winson Chung321e9ee2010-08-09 13:37:56 -070025import android.graphics.Canvas;
26import android.graphics.Rect;
Winson Chung321e9ee2010-08-09 13:37:56 -070027import android.os.Parcel;
28import android.os.Parcelable;
29import android.util.AttributeSet;
Winson Chung321e9ee2010-08-09 13:37:56 -070030import android.view.MotionEvent;
31import android.view.VelocityTracker;
32import android.view.View;
33import android.view.ViewConfiguration;
34import android.view.ViewGroup;
35import android.view.ViewParent;
Winson Chung80baf5a2010-08-09 16:03:15 -070036import android.view.animation.Animation;
Winson Chung80baf5a2010-08-09 16:03:15 -070037import android.view.animation.Animation.AnimationListener;
Winson Chung86f77532010-08-24 11:08:22 -070038import android.view.animation.AnimationUtils;
Winson Chung321e9ee2010-08-09 13:37:56 -070039import android.widget.Scroller;
40
Winson Chung80baf5a2010-08-09 16:03:15 -070041import com.android.launcher.R;
42
Winson Chung321e9ee2010-08-09 13:37:56 -070043/**
44 * An abstraction of the original Workspace which supports browsing through a
45 * sequential list of "pages" (or PagedViewCellLayouts).
46 */
47public abstract class PagedView extends ViewGroup {
48 private static final String TAG = "PagedView";
Winson Chung86f77532010-08-24 11:08:22 -070049 private static final int INVALID_PAGE = -1;
Winson Chung321e9ee2010-08-09 13:37:56 -070050
Winson Chung86f77532010-08-24 11:08:22 -070051 // the velocity at which a fling gesture will cause us to snap to the next page
Winson Chung321e9ee2010-08-09 13:37:56 -070052 private static final int SNAP_VELOCITY = 500;
53
Winson Chung86f77532010-08-24 11:08:22 -070054 // the min drag distance for a fling to register, to prevent random page shifts
Winson Chung321e9ee2010-08-09 13:37:56 -070055 private static final int MIN_LENGTH_FOR_FLING = 50;
56
57 private boolean mFirstLayout = true;
58
Winson Chung86f77532010-08-24 11:08:22 -070059 private int mCurrentPage;
60 private int mNextPage = INVALID_PAGE;
Winson Chung321e9ee2010-08-09 13:37:56 -070061 private Scroller mScroller;
62 private VelocityTracker mVelocityTracker;
63
64 private float mDownMotionX;
65 private float mLastMotionX;
66 private float mLastMotionY;
67
68 private final static int TOUCH_STATE_REST = 0;
69 private final static int TOUCH_STATE_SCROLLING = 1;
70 private final static int TOUCH_STATE_PREV_PAGE = 2;
71 private final static int TOUCH_STATE_NEXT_PAGE = 3;
72
73 private int mTouchState = TOUCH_STATE_REST;
74
75 private OnLongClickListener mLongClickListener;
76
77 private boolean mAllowLongPress = true;
78
79 private int mTouchSlop;
80 private int mPagingTouchSlop;
81 private int mMaximumVelocity;
82
83 private static final int INVALID_POINTER = -1;
84
85 private int mActivePointerId = INVALID_POINTER;
86
Winson Chung86f77532010-08-24 11:08:22 -070087 private PageSwitchListener mPageSwitchListener;
Winson Chung321e9ee2010-08-09 13:37:56 -070088
Winson Chung86f77532010-08-24 11:08:22 -070089 private ArrayList<Boolean> mDirtyPageContent;
90 private boolean mDirtyPageAlpha;
Winson Chung321e9ee2010-08-09 13:37:56 -070091
Winson Chung241c3b42010-08-25 16:53:03 -070092 protected PagedViewIconCache mPageViewIconCache;
93
94 /**
95 * Simple cache mechanism for PagedViewIcon outlines.
96 */
97 class PagedViewIconCache {
98 private final HashMap<Object, Bitmap> iconOutlineCache = new HashMap<Object, Bitmap>();
99
100 public void clear() {
101 iconOutlineCache.clear();
102 }
103 public void addOutline(Object key, Bitmap b) {
104 iconOutlineCache.put(key, b);
105 }
106 public void removeOutline(Object key) {
107 if (iconOutlineCache.containsKey(key)) {
108 iconOutlineCache.remove(key);
109 }
110 }
111 public Bitmap getOutline(Object key) {
112 return iconOutlineCache.get(key);
113 }
114 }
115
Winson Chung86f77532010-08-24 11:08:22 -0700116 public interface PageSwitchListener {
117 void onPageSwitch(View newPage, int newPageIndex);
Winson Chung321e9ee2010-08-09 13:37:56 -0700118 }
119
Winson Chung321e9ee2010-08-09 13:37:56 -0700120 public PagedView(Context context) {
121 this(context, null);
122 }
123
124 public PagedView(Context context, AttributeSet attrs) {
125 this(context, attrs, 0);
126 }
127
128 public PagedView(Context context, AttributeSet attrs, int defStyle) {
129 super(context, attrs, defStyle);
130
131 setHapticFeedbackEnabled(false);
132 initWorkspace();
133 }
134
135 /**
136 * Initializes various states for this workspace.
137 */
138 private void initWorkspace() {
Winson Chung86f77532010-08-24 11:08:22 -0700139 mDirtyPageContent = new ArrayList<Boolean>();
140 mDirtyPageContent.ensureCapacity(32);
Winson Chung241c3b42010-08-25 16:53:03 -0700141 mPageViewIconCache = new PagedViewIconCache();
Winson Chung321e9ee2010-08-09 13:37:56 -0700142 mScroller = new Scroller(getContext());
Winson Chung86f77532010-08-24 11:08:22 -0700143 mCurrentPage = 0;
Winson Chung321e9ee2010-08-09 13:37:56 -0700144
145 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
146 mTouchSlop = configuration.getScaledTouchSlop();
147 mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
148 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
149 }
150
Winson Chung86f77532010-08-24 11:08:22 -0700151 public void setPageSwitchListener(PageSwitchListener pageSwitchListener) {
152 mPageSwitchListener = pageSwitchListener;
153 if (mPageSwitchListener != null) {
154 mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700155 }
156 }
157
158 /**
Winson Chung86f77532010-08-24 11:08:22 -0700159 * Returns the index of the currently displayed page.
Winson Chung321e9ee2010-08-09 13:37:56 -0700160 *
Winson Chung86f77532010-08-24 11:08:22 -0700161 * @return The index of the currently displayed page.
Winson Chung321e9ee2010-08-09 13:37:56 -0700162 */
Winson Chung86f77532010-08-24 11:08:22 -0700163 int getCurrentPage() {
164 return mCurrentPage;
Winson Chung321e9ee2010-08-09 13:37:56 -0700165 }
166
Winson Chung86f77532010-08-24 11:08:22 -0700167 int getPageCount() {
Winson Chung321e9ee2010-08-09 13:37:56 -0700168 return getChildCount();
169 }
170
Winson Chung86f77532010-08-24 11:08:22 -0700171 View getPageAt(int index) {
Winson Chung321e9ee2010-08-09 13:37:56 -0700172 return getChildAt(index);
173 }
174
175 int getScrollWidth() {
176 return getWidth();
177 }
178
179 /**
Winson Chung86f77532010-08-24 11:08:22 -0700180 * Sets the current page.
Winson Chung321e9ee2010-08-09 13:37:56 -0700181 */
Winson Chung86f77532010-08-24 11:08:22 -0700182 void setCurrentPage(int currentPage) {
Winson Chung321e9ee2010-08-09 13:37:56 -0700183 if (!mScroller.isFinished()) mScroller.abortAnimation();
184 if (getChildCount() == 0) return;
185
Winson Chung86f77532010-08-24 11:08:22 -0700186 mCurrentPage = Math.max(0, Math.min(currentPage, getPageCount() - 1));
187 scrollTo(getChildOffset(mCurrentPage) - getRelativeChildOffset(mCurrentPage), 0);
Winson Chung80baf5a2010-08-09 16:03:15 -0700188
Winson Chung321e9ee2010-08-09 13:37:56 -0700189 invalidate();
Winson Chung86f77532010-08-24 11:08:22 -0700190 notifyPageSwitchListener();
Winson Chung321e9ee2010-08-09 13:37:56 -0700191 }
192
Winson Chung86f77532010-08-24 11:08:22 -0700193 private void notifyPageSwitchListener() {
194 if (mPageSwitchListener != null) {
195 mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700196 }
197 }
198
199 /**
Winson Chung86f77532010-08-24 11:08:22 -0700200 * Registers the specified listener on each page contained in this workspace.
Winson Chung321e9ee2010-08-09 13:37:56 -0700201 *
202 * @param l The listener used to respond to long clicks.
203 */
204 @Override
205 public void setOnLongClickListener(OnLongClickListener l) {
206 mLongClickListener = l;
Winson Chung86f77532010-08-24 11:08:22 -0700207 final int count = getPageCount();
Winson Chung321e9ee2010-08-09 13:37:56 -0700208 for (int i = 0; i < count; i++) {
Winson Chung86f77532010-08-24 11:08:22 -0700209 getPageAt(i).setOnLongClickListener(l);
Winson Chung321e9ee2010-08-09 13:37:56 -0700210 }
211 }
212
213 @Override
214 public void computeScroll() {
215 if (mScroller.computeScrollOffset()) {
216 scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
217 postInvalidate();
Winson Chung86f77532010-08-24 11:08:22 -0700218 } else if (mNextPage != INVALID_PAGE) {
219 mCurrentPage = Math.max(0, Math.min(mNextPage, getPageCount() - 1));
220 notifyPageSwitchListener();
221 mNextPage = INVALID_PAGE;
Winson Chung321e9ee2010-08-09 13:37:56 -0700222 }
223 }
224
225 @Override
226 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
227 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
228 final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
229 if (widthMode != MeasureSpec.EXACTLY) {
230 throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
231 }
232
233 final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
234 final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
235 if (heightMode != MeasureSpec.EXACTLY) {
236 throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
237 }
238
239 // The children are given the same width and height as the workspace
240 final int childCount = getChildCount();
241 for (int i = 0; i < childCount; i++) {
242 getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
243 }
244
245 setMeasuredDimension(widthSize, heightSize);
246
247 if (mFirstLayout) {
248 setHorizontalScrollBarEnabled(false);
Winson Chung86f77532010-08-24 11:08:22 -0700249 scrollTo(mCurrentPage * widthSize, 0);
Winson Chung321e9ee2010-08-09 13:37:56 -0700250 setHorizontalScrollBarEnabled(true);
251 mFirstLayout = false;
252 }
253 }
254
255 @Override
256 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
257 final int childCount = getChildCount();
258 int childLeft = 0;
259 if (childCount > 0) {
260 childLeft = (getMeasuredWidth() - getChildAt(0).getMeasuredWidth()) / 2;
261 }
262
263 for (int i = 0; i < childCount; i++) {
264 final View child = getChildAt(i);
265 if (child.getVisibility() != View.GONE) {
266 final int childWidth = child.getMeasuredWidth();
267 child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
268 childLeft += childWidth;
269 }
270 }
271 }
272
Winson Chung321e9ee2010-08-09 13:37:56 -0700273 @Override
274 protected void dispatchDraw(Canvas canvas) {
Winson Chung86f77532010-08-24 11:08:22 -0700275 if (mDirtyPageAlpha || (mTouchState == TOUCH_STATE_SCROLLING) || !mScroller.isFinished()) {
Winson Chung321e9ee2010-08-09 13:37:56 -0700276 int screenCenter = mScrollX + (getMeasuredWidth() / 2);
277 final int childCount = getChildCount();
278 for (int i = 0; i < childCount; ++i) {
279 PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
280 int childWidth = layout.getMeasuredWidth();
281 int halfChildWidth = (childWidth / 2);
282 int childCenter = getChildOffset(i) + halfChildWidth;
283 int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
Winson Chungb3347bb2010-08-19 14:51:28 -0700284 float alpha = 0.0f;
Winson Chung321e9ee2010-08-09 13:37:56 -0700285 if (distanceFromScreenCenter < halfChildWidth) {
Winson Chungb3347bb2010-08-19 14:51:28 -0700286 alpha = 1.0f;
Winson Chung321e9ee2010-08-09 13:37:56 -0700287 } else if (distanceFromScreenCenter > childWidth) {
Winson Chungb3347bb2010-08-19 14:51:28 -0700288 alpha = 0.0f;
Winson Chung321e9ee2010-08-09 13:37:56 -0700289 } else {
Winson Chungb3347bb2010-08-19 14:51:28 -0700290 float dimAlpha = (float) (distanceFromScreenCenter - halfChildWidth) / halfChildWidth;
291 dimAlpha = Math.max(0.0f, Math.min(1.0f, (dimAlpha * dimAlpha)));
292 alpha = 1.0f - dimAlpha;
Winson Chung321e9ee2010-08-09 13:37:56 -0700293 }
Winson Chungaffd7b42010-08-20 15:11:56 -0700294 if (Float.compare(alpha, layout.getAlpha()) != 0) {
Winson Chungb3347bb2010-08-19 14:51:28 -0700295 layout.setAlpha(alpha);
Winson Chungaffd7b42010-08-20 15:11:56 -0700296 }
Winson Chung321e9ee2010-08-09 13:37:56 -0700297 }
Winson Chung86f77532010-08-24 11:08:22 -0700298 mDirtyPageAlpha = false;
Winson Chung321e9ee2010-08-09 13:37:56 -0700299 }
300 super.dispatchDraw(canvas);
301 }
302
303 @Override
304 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
Winson Chung86f77532010-08-24 11:08:22 -0700305 int page = indexOfChild(child);
306 if (page != mCurrentPage || !mScroller.isFinished()) {
307 snapToPage(page);
Winson Chung321e9ee2010-08-09 13:37:56 -0700308 return true;
309 }
310 return false;
311 }
312
313 @Override
314 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
Winson Chung86f77532010-08-24 11:08:22 -0700315 int focusablePage;
316 if (mNextPage != INVALID_PAGE) {
317 focusablePage = mNextPage;
Winson Chung321e9ee2010-08-09 13:37:56 -0700318 } else {
Winson Chung86f77532010-08-24 11:08:22 -0700319 focusablePage = mCurrentPage;
Winson Chung321e9ee2010-08-09 13:37:56 -0700320 }
Winson Chung86f77532010-08-24 11:08:22 -0700321 View v = getPageAt(focusablePage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700322 if (v != null) {
323 v.requestFocus(direction, previouslyFocusedRect);
324 }
325 return false;
326 }
327
328 @Override
329 public boolean dispatchUnhandledMove(View focused, int direction) {
330 if (direction == View.FOCUS_LEFT) {
Winson Chung86f77532010-08-24 11:08:22 -0700331 if (getCurrentPage() > 0) {
332 snapToPage(getCurrentPage() - 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700333 return true;
334 }
335 } else if (direction == View.FOCUS_RIGHT) {
Winson Chung86f77532010-08-24 11:08:22 -0700336 if (getCurrentPage() < getPageCount() - 1) {
337 snapToPage(getCurrentPage() + 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700338 return true;
339 }
340 }
341 return super.dispatchUnhandledMove(focused, direction);
342 }
343
344 @Override
345 public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
Winson Chung86f77532010-08-24 11:08:22 -0700346 if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) {
347 getPageAt(mCurrentPage).addFocusables(views, direction);
Winson Chung321e9ee2010-08-09 13:37:56 -0700348 }
349 if (direction == View.FOCUS_LEFT) {
Winson Chung86f77532010-08-24 11:08:22 -0700350 if (mCurrentPage > 0) {
351 getPageAt(mCurrentPage - 1).addFocusables(views, direction);
Winson Chung321e9ee2010-08-09 13:37:56 -0700352 }
353 } else if (direction == View.FOCUS_RIGHT){
Winson Chung86f77532010-08-24 11:08:22 -0700354 if (mCurrentPage < getPageCount() - 1) {
355 getPageAt(mCurrentPage + 1).addFocusables(views, direction);
Winson Chung321e9ee2010-08-09 13:37:56 -0700356 }
357 }
358 }
359
360 /**
361 * If one of our descendant views decides that it could be focused now, only
Winson Chung86f77532010-08-24 11:08:22 -0700362 * pass that along if it's on the current page.
Winson Chung321e9ee2010-08-09 13:37:56 -0700363 *
Winson Chung86f77532010-08-24 11:08:22 -0700364 * This happens when live folders requery, and if they're off page, they
365 * end up calling requestFocus, which pulls it on page.
Winson Chung321e9ee2010-08-09 13:37:56 -0700366 */
367 @Override
368 public void focusableViewAvailable(View focused) {
Winson Chung86f77532010-08-24 11:08:22 -0700369 View current = getPageAt(mCurrentPage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700370 View v = focused;
371 while (true) {
372 if (v == current) {
373 super.focusableViewAvailable(focused);
374 return;
375 }
376 if (v == this) {
377 return;
378 }
379 ViewParent parent = v.getParent();
380 if (parent instanceof View) {
381 v = (View)v.getParent();
382 } else {
383 return;
384 }
385 }
386 }
387
388 /**
389 * {@inheritDoc}
390 */
391 @Override
392 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
393 if (disallowIntercept) {
394 // We need to make sure to cancel our long press if
395 // a scrollable widget takes over touch events
Winson Chung86f77532010-08-24 11:08:22 -0700396 final View currentPage = getChildAt(mCurrentPage);
397 currentPage.cancelLongPress();
Winson Chung321e9ee2010-08-09 13:37:56 -0700398 }
399 super.requestDisallowInterceptTouchEvent(disallowIntercept);
400 }
401
402 @Override
403 public boolean onInterceptTouchEvent(MotionEvent ev) {
404 /*
405 * This method JUST determines whether we want to intercept the motion.
406 * If we return true, onTouchEvent will be called and we do the actual
407 * scrolling there.
408 */
409
410 /*
411 * Shortcut the most recurring case: the user is in the dragging
412 * state and he is moving his finger. We want to intercept this
413 * motion.
414 */
415 final int action = ev.getAction();
416 if ((action == MotionEvent.ACTION_MOVE) &&
417 (mTouchState == TOUCH_STATE_SCROLLING)) {
418 return true;
419 }
420
421
422 switch (action & MotionEvent.ACTION_MASK) {
423 case MotionEvent.ACTION_MOVE: {
424 /*
425 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
426 * whether the user has moved far enough from his original down touch.
427 */
428 determineScrollingStart(ev);
429 break;
430 }
431
432 case MotionEvent.ACTION_DOWN: {
433 final float x = ev.getX();
434 final float y = ev.getY();
435 // Remember location of down touch
436 mDownMotionX = x;
437 mLastMotionX = x;
438 mLastMotionY = y;
439 mActivePointerId = ev.getPointerId(0);
440 mAllowLongPress = true;
441
442 /*
443 * If being flinged and user touches the screen, initiate drag;
444 * otherwise don't. mScroller.isFinished should be false when
445 * being flinged.
446 */
447 mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
448
Winson Chung86f77532010-08-24 11:08:22 -0700449 // check if this can be the beginning of a tap on the side of the pages
Winson Chung321e9ee2010-08-09 13:37:56 -0700450 // to scroll the current page
451 if ((mTouchState != TOUCH_STATE_PREV_PAGE) &&
452 (mTouchState != TOUCH_STATE_NEXT_PAGE)) {
453 if (getChildCount() > 0) {
454 int relativeChildLeft = getChildOffset(0);
455 int relativeChildRight = relativeChildLeft + getChildAt(0).getMeasuredWidth();
456 if (x < relativeChildLeft) {
457 mTouchState = TOUCH_STATE_PREV_PAGE;
458 } else if (x > relativeChildRight) {
459 mTouchState = TOUCH_STATE_NEXT_PAGE;
460 }
461 }
462 }
463 break;
464 }
465
466 case MotionEvent.ACTION_CANCEL:
467 case MotionEvent.ACTION_UP:
468 // Release the drag
469 mTouchState = TOUCH_STATE_REST;
470 mAllowLongPress = false;
471 mActivePointerId = INVALID_POINTER;
472
473 break;
474
475 case MotionEvent.ACTION_POINTER_UP:
476 onSecondaryPointerUp(ev);
477 break;
478 }
479
480 /*
481 * The only time we want to intercept motion events is if we are in the
482 * drag mode.
483 */
484 return mTouchState != TOUCH_STATE_REST;
485 }
486
Winson Chung80baf5a2010-08-09 16:03:15 -0700487 protected void animateClickFeedback(View v, final Runnable r) {
488 // animate the view slightly to show click feedback running some logic after it is "pressed"
489 Animation anim = AnimationUtils.loadAnimation(getContext(),
490 R.anim.paged_view_click_feedback);
491 anim.setAnimationListener(new AnimationListener() {
492 @Override
493 public void onAnimationStart(Animation animation) {}
494 @Override
495 public void onAnimationRepeat(Animation animation) {
496 r.run();
497 }
498 @Override
499 public void onAnimationEnd(Animation animation) {}
500 });
501 v.startAnimation(anim);
502 }
503
Winson Chung321e9ee2010-08-09 13:37:56 -0700504 /*
505 * Determines if we should change the touch state to start scrolling after the
506 * user moves their touch point too far.
507 */
508 private void determineScrollingStart(MotionEvent ev) {
509 /*
510 * Locally do absolute value. mLastMotionX is set to the y value
511 * of the down event.
512 */
513 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
514 final float x = ev.getX(pointerIndex);
515 final float y = ev.getY(pointerIndex);
516 final int xDiff = (int) Math.abs(x - mLastMotionX);
517 final int yDiff = (int) Math.abs(y - mLastMotionY);
518
519 final int touchSlop = mTouchSlop;
520 boolean xPaged = xDiff > mPagingTouchSlop;
521 boolean xMoved = xDiff > touchSlop;
522 boolean yMoved = yDiff > touchSlop;
523
524 if (xMoved || yMoved) {
525 if (xPaged) {
526 // Scroll if the user moved far enough along the X axis
527 mTouchState = TOUCH_STATE_SCROLLING;
528 mLastMotionX = x;
529 }
530 // Either way, cancel any pending longpress
531 if (mAllowLongPress) {
532 mAllowLongPress = false;
533 // Try canceling the long press. It could also have been scheduled
534 // by a distant descendant, so use the mAllowLongPress flag to block
535 // everything
Winson Chung86f77532010-08-24 11:08:22 -0700536 final View currentPage = getPageAt(mCurrentPage);
537 currentPage.cancelLongPress();
Winson Chung321e9ee2010-08-09 13:37:56 -0700538 }
539 }
540 }
541
542 @Override
543 public boolean onTouchEvent(MotionEvent ev) {
544 if (mVelocityTracker == null) {
545 mVelocityTracker = VelocityTracker.obtain();
546 }
547 mVelocityTracker.addMovement(ev);
548
549 final int action = ev.getAction();
550
551 switch (action & MotionEvent.ACTION_MASK) {
552 case MotionEvent.ACTION_DOWN:
553 /*
554 * If being flinged and user touches, stop the fling. isFinished
555 * will be false if being flinged.
556 */
557 if (!mScroller.isFinished()) {
558 mScroller.abortAnimation();
559 }
560
561 // Remember where the motion event started
562 mDownMotionX = mLastMotionX = ev.getX();
563 mActivePointerId = ev.getPointerId(0);
564 break;
565
566 case MotionEvent.ACTION_MOVE:
567 if (mTouchState == TOUCH_STATE_SCROLLING) {
568 // Scroll to follow the motion event
569 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
570 final float x = ev.getX(pointerIndex);
571 final int deltaX = (int) (mLastMotionX - x);
572 mLastMotionX = x;
573
574 int sx = getScrollX();
575 if (deltaX < 0) {
576 if (sx > 0) {
577 scrollBy(Math.max(-sx, deltaX), 0);
578 }
579 } else if (deltaX > 0) {
580 final int lastChildIndex = getChildCount() - 1;
581 final int availableToScroll = getChildOffset(lastChildIndex) -
582 getRelativeChildOffset(lastChildIndex) - sx;
583 if (availableToScroll > 0) {
584 scrollBy(Math.min(availableToScroll, deltaX), 0);
585 }
586 } else {
587 awakenScrollBars();
588 }
589 } else if ((mTouchState == TOUCH_STATE_PREV_PAGE) ||
590 (mTouchState == TOUCH_STATE_NEXT_PAGE)) {
591 determineScrollingStart(ev);
592 }
593 break;
594
595 case MotionEvent.ACTION_UP:
596 if (mTouchState == TOUCH_STATE_SCROLLING) {
597 final int activePointerId = mActivePointerId;
598 final int pointerIndex = ev.findPointerIndex(activePointerId);
599 final float x = ev.getX(pointerIndex);
600 final VelocityTracker velocityTracker = mVelocityTracker;
601 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
602 int velocityX = (int) velocityTracker.getXVelocity(activePointerId);
603 boolean isfling = Math.abs(mDownMotionX - x) > MIN_LENGTH_FOR_FLING;
604
Winson Chung86f77532010-08-24 11:08:22 -0700605 if (isfling && velocityX > SNAP_VELOCITY && mCurrentPage > 0) {
606 snapToPage(mCurrentPage - 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700607 } else if (isfling && velocityX < -SNAP_VELOCITY &&
Winson Chung86f77532010-08-24 11:08:22 -0700608 mCurrentPage < getChildCount() - 1) {
609 snapToPage(mCurrentPage + 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700610 } else {
611 snapToDestination();
612 }
613
614 if (mVelocityTracker != null) {
615 mVelocityTracker.recycle();
616 mVelocityTracker = null;
617 }
618 } else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
619 // at this point we have not moved beyond the touch slop
620 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
621 // we can just page
Winson Chung86f77532010-08-24 11:08:22 -0700622 int nextPage = Math.max(0, mCurrentPage - 1);
623 if (nextPage != mCurrentPage) {
624 snapToPage(nextPage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700625 } else {
626 snapToDestination();
627 }
628 } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) {
629 // at this point we have not moved beyond the touch slop
630 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
631 // we can just page
Winson Chung86f77532010-08-24 11:08:22 -0700632 int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1);
633 if (nextPage != mCurrentPage) {
634 snapToPage(nextPage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700635 } else {
636 snapToDestination();
637 }
638 }
639 mTouchState = TOUCH_STATE_REST;
640 mActivePointerId = INVALID_POINTER;
641 break;
642
643 case MotionEvent.ACTION_CANCEL:
644 mTouchState = TOUCH_STATE_REST;
645 mActivePointerId = INVALID_POINTER;
646 break;
647
648 case MotionEvent.ACTION_POINTER_UP:
649 onSecondaryPointerUp(ev);
650 break;
651 }
652
653 return true;
654 }
655
656 private void onSecondaryPointerUp(MotionEvent ev) {
657 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
658 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
659 final int pointerId = ev.getPointerId(pointerIndex);
660 if (pointerId == mActivePointerId) {
661 // This was our active pointer going up. Choose a new
662 // active pointer and adjust accordingly.
663 // TODO: Make this decision more intelligent.
664 final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
665 mLastMotionX = mDownMotionX = ev.getX(newPointerIndex);
666 mLastMotionY = ev.getY(newPointerIndex);
667 mActivePointerId = ev.getPointerId(newPointerIndex);
668 if (mVelocityTracker != null) {
669 mVelocityTracker.clear();
670 }
671 }
672 }
673
674 @Override
675 public void requestChildFocus(View child, View focused) {
676 super.requestChildFocus(child, focused);
Winson Chung86f77532010-08-24 11:08:22 -0700677 int page = indexOfChild(child);
678 if (page >= 0 && !isInTouchMode()) {
679 snapToPage(page);
Winson Chung321e9ee2010-08-09 13:37:56 -0700680 }
681 }
682
683 protected int getRelativeChildOffset(int index) {
684 return (getMeasuredWidth() - getChildAt(index).getMeasuredWidth()) / 2;
685 }
686
687 protected int getChildOffset(int index) {
688 if (getChildCount() == 0)
689 return 0;
690
691 int offset = getRelativeChildOffset(0);
692 for (int i = 0; i < index; ++i) {
693 offset += getChildAt(i).getMeasuredWidth();
694 }
695 return offset;
696 }
697
698 protected void snapToDestination() {
699 int minDistanceFromScreenCenter = getMeasuredWidth();
700 int minDistanceFromScreenCenterIndex = -1;
701 int screenCenter = mScrollX + (getMeasuredWidth() / 2);
702 final int childCount = getChildCount();
703 for (int i = 0; i < childCount; ++i) {
704 PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
705 int childWidth = layout.getMeasuredWidth();
706 int halfChildWidth = (childWidth / 2);
707 int childCenter = getChildOffset(i) + halfChildWidth;
708 int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
709 if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
710 minDistanceFromScreenCenter = distanceFromScreenCenter;
711 minDistanceFromScreenCenterIndex = i;
712 }
713 }
Winson Chung86f77532010-08-24 11:08:22 -0700714 snapToPage(minDistanceFromScreenCenterIndex, 1000);
Winson Chung321e9ee2010-08-09 13:37:56 -0700715 }
716
Winson Chung86f77532010-08-24 11:08:22 -0700717 void snapToPage(int whichPage) {
718 snapToPage(whichPage, 1000);
Winson Chung321e9ee2010-08-09 13:37:56 -0700719 }
720
Winson Chung86f77532010-08-24 11:08:22 -0700721 void snapToPage(int whichPage, int duration) {
722 whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1));
Winson Chung321e9ee2010-08-09 13:37:56 -0700723
Winson Chung86f77532010-08-24 11:08:22 -0700724 mNextPage = whichPage;
Winson Chung321e9ee2010-08-09 13:37:56 -0700725
Winson Chung86f77532010-08-24 11:08:22 -0700726 int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700727 final int sX = getScrollX();
728 final int delta = newX - sX;
729 awakenScrollBars(duration);
730 if (duration == 0) {
731 duration = Math.abs(delta);
732 }
733
734 if (!mScroller.isFinished()) mScroller.abortAnimation();
735 mScroller.startScroll(sX, 0, delta, 0, duration);
Winson Chung80baf5a2010-08-09 16:03:15 -0700736
737 // only load some associated pages
Winson Chung86f77532010-08-24 11:08:22 -0700738 loadAssociatedPages(mNextPage);
Winson Chung80baf5a2010-08-09 16:03:15 -0700739
Winson Chung321e9ee2010-08-09 13:37:56 -0700740 invalidate();
741 }
742
743 @Override
744 protected Parcelable onSaveInstanceState() {
745 final SavedState state = new SavedState(super.onSaveInstanceState());
Winson Chung86f77532010-08-24 11:08:22 -0700746 state.currentPage = mCurrentPage;
Winson Chung321e9ee2010-08-09 13:37:56 -0700747 return state;
748 }
749
750 @Override
751 protected void onRestoreInstanceState(Parcelable state) {
752 SavedState savedState = (SavedState) state;
753 super.onRestoreInstanceState(savedState.getSuperState());
Winson Chung86f77532010-08-24 11:08:22 -0700754 if (savedState.currentPage != -1) {
755 mCurrentPage = savedState.currentPage;
Winson Chung321e9ee2010-08-09 13:37:56 -0700756 }
757 }
758
759 public void scrollLeft() {
760 if (mScroller.isFinished()) {
Winson Chung86f77532010-08-24 11:08:22 -0700761 if (mCurrentPage > 0) snapToPage(mCurrentPage - 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700762 } else {
Winson Chung86f77532010-08-24 11:08:22 -0700763 if (mNextPage > 0) snapToPage(mNextPage - 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700764 }
765 }
766
767 public void scrollRight() {
768 if (mScroller.isFinished()) {
Winson Chung86f77532010-08-24 11:08:22 -0700769 if (mCurrentPage < getChildCount() -1) snapToPage(mCurrentPage + 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700770 } else {
Winson Chung86f77532010-08-24 11:08:22 -0700771 if (mNextPage < getChildCount() -1) snapToPage(mNextPage + 1);
Winson Chung321e9ee2010-08-09 13:37:56 -0700772 }
773 }
774
Winson Chung86f77532010-08-24 11:08:22 -0700775 public int getPageForView(View v) {
Winson Chung321e9ee2010-08-09 13:37:56 -0700776 int result = -1;
777 if (v != null) {
778 ViewParent vp = v.getParent();
779 int count = getChildCount();
780 for (int i = 0; i < count; i++) {
781 if (vp == getChildAt(i)) {
782 return i;
783 }
784 }
785 }
786 return result;
787 }
788
789 /**
790 * @return True is long presses are still allowed for the current touch
791 */
792 public boolean allowLongPress() {
793 return mAllowLongPress;
794 }
795
796 public static class SavedState extends BaseSavedState {
Winson Chung86f77532010-08-24 11:08:22 -0700797 int currentPage = -1;
Winson Chung321e9ee2010-08-09 13:37:56 -0700798
799 SavedState(Parcelable superState) {
800 super(superState);
801 }
802
803 private SavedState(Parcel in) {
804 super(in);
Winson Chung86f77532010-08-24 11:08:22 -0700805 currentPage = in.readInt();
Winson Chung321e9ee2010-08-09 13:37:56 -0700806 }
807
808 @Override
809 public void writeToParcel(Parcel out, int flags) {
810 super.writeToParcel(out, flags);
Winson Chung86f77532010-08-24 11:08:22 -0700811 out.writeInt(currentPage);
Winson Chung321e9ee2010-08-09 13:37:56 -0700812 }
813
814 public static final Parcelable.Creator<SavedState> CREATOR =
815 new Parcelable.Creator<SavedState>() {
816 public SavedState createFromParcel(Parcel in) {
817 return new SavedState(in);
818 }
819
820 public SavedState[] newArray(int size) {
821 return new SavedState[size];
822 }
823 };
824 }
825
Winson Chung86f77532010-08-24 11:08:22 -0700826 public void loadAssociatedPages(int page) {
Winson Chung80baf5a2010-08-09 16:03:15 -0700827 final int count = getChildCount();
Winson Chung86f77532010-08-24 11:08:22 -0700828 if (page < count) {
829 int lowerPageBound = Math.max(0, page - 1);
830 int upperPageBound = Math.min(page + 1, count - 1);
Winson Chung80baf5a2010-08-09 16:03:15 -0700831 for (int i = 0; i < count; ++i) {
Winson Chung86f77532010-08-24 11:08:22 -0700832 final PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
833 final int childCount = layout.getChildCount();
834 if (lowerPageBound <= i && i <= upperPageBound) {
835 if (mDirtyPageContent.get(i)) {
836 syncPageItems(i);
837 mDirtyPageContent.set(i, false);
838 }
Winson Chung80baf5a2010-08-09 16:03:15 -0700839 } else {
Winson Chung86f77532010-08-24 11:08:22 -0700840 if (childCount > 0) {
Winson Chung80baf5a2010-08-09 16:03:15 -0700841 layout.removeAllViews();
842 }
Winson Chung86f77532010-08-24 11:08:22 -0700843 mDirtyPageContent.set(i, true);
Winson Chung80baf5a2010-08-09 16:03:15 -0700844 }
845 }
Winson Chung80baf5a2010-08-09 16:03:15 -0700846 }
847 }
848
Winson Chung86f77532010-08-24 11:08:22 -0700849 /**
850 * This method is called ONLY to synchronize the number of pages that the paged view has.
851 * To actually fill the pages with information, implement syncPageItems() below. It is
852 * guaranteed that syncPageItems() will be called for a particular page before it is shown,
853 * and therefore, individual page items do not need to be updated in this method.
854 */
Winson Chung321e9ee2010-08-09 13:37:56 -0700855 public abstract void syncPages();
Winson Chung86f77532010-08-24 11:08:22 -0700856
857 /**
858 * This method is called to synchronize the items that are on a particular page. If views on
859 * the page can be reused, then they should be updated within this method.
860 */
Winson Chung321e9ee2010-08-09 13:37:56 -0700861 public abstract void syncPageItems(int page);
Winson Chung86f77532010-08-24 11:08:22 -0700862
Winson Chung321e9ee2010-08-09 13:37:56 -0700863 public void invalidatePageData() {
Winson Chung86f77532010-08-24 11:08:22 -0700864 // Update all the pages
Winson Chung321e9ee2010-08-09 13:37:56 -0700865 syncPages();
Winson Chung86f77532010-08-24 11:08:22 -0700866
867 // Mark each of the pages as dirty
868 final int count = getChildCount();
869 mDirtyPageContent.clear();
870 for (int i = 0; i < count; ++i) {
871 mDirtyPageContent.add(true);
872 }
873
874 // Load any pages that are necessary for the current window of views
875 loadAssociatedPages(mCurrentPage);
876 mDirtyPageAlpha = true;
Winson Chung321e9ee2010-08-09 13:37:56 -0700877 requestLayout();
878 }
879}