blob: 16116ceafbc912b9d2a16bacced186628c5b59c2 [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;
20
21import android.content.Context;
22import android.graphics.Canvas;
23import android.graphics.Rect;
24import android.graphics.drawable.Drawable;
Winson Chung80baf5a2010-08-09 16:03:15 -070025import android.os.Handler;
Winson Chung321e9ee2010-08-09 13:37:56 -070026import android.os.Parcel;
27import android.os.Parcelable;
28import android.util.AttributeSet;
29import android.util.Log;
30import 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.AlphaAnimation;
37import android.view.animation.Animation;
38import android.view.animation.AnimationUtils;
39import android.view.animation.Animation.AnimationListener;
Winson Chung321e9ee2010-08-09 13:37:56 -070040import android.widget.Scroller;
41
Winson Chung80baf5a2010-08-09 16:03:15 -070042import com.android.launcher.R;
43
Winson Chung321e9ee2010-08-09 13:37:56 -070044/**
45 * An abstraction of the original Workspace which supports browsing through a
46 * sequential list of "pages" (or PagedViewCellLayouts).
47 */
48public abstract class PagedView extends ViewGroup {
49 private static final String TAG = "PagedView";
50 private static final int INVALID_SCREEN = -1;
51
52 // the velocity at which a fling gesture will cause us to snap to the next screen
53 private static final int SNAP_VELOCITY = 500;
54
55 // the min drag distance for a fling to register, to prevent random screen shifts
56 private static final int MIN_LENGTH_FOR_FLING = 50;
57
58 private boolean mFirstLayout = true;
59
60 private int mCurrentScreen;
61 private int mNextScreen = INVALID_SCREEN;
62 private Scroller mScroller;
63 private VelocityTracker mVelocityTracker;
64
65 private float mDownMotionX;
66 private float mLastMotionX;
67 private float mLastMotionY;
68
69 private final static int TOUCH_STATE_REST = 0;
70 private final static int TOUCH_STATE_SCROLLING = 1;
71 private final static int TOUCH_STATE_PREV_PAGE = 2;
72 private final static int TOUCH_STATE_NEXT_PAGE = 3;
73
74 private int mTouchState = TOUCH_STATE_REST;
75
76 private OnLongClickListener mLongClickListener;
77
78 private boolean mAllowLongPress = true;
79
80 private int mTouchSlop;
81 private int mPagingTouchSlop;
82 private int mMaximumVelocity;
83
84 private static final int INVALID_POINTER = -1;
85
86 private int mActivePointerId = INVALID_POINTER;
87
88 private ScreenSwitchListener mScreenSwitchListener;
89
90 private boolean mDimmedPagesDirty;
91
92 public interface ScreenSwitchListener {
93 void onScreenSwitch(View newScreen, int newScreenIndex);
94 }
95
Winson Chung321e9ee2010-08-09 13:37:56 -070096 public PagedView(Context context) {
97 this(context, null);
98 }
99
100 public PagedView(Context context, AttributeSet attrs) {
101 this(context, attrs, 0);
102 }
103
104 public PagedView(Context context, AttributeSet attrs, int defStyle) {
105 super(context, attrs, defStyle);
106
107 setHapticFeedbackEnabled(false);
108 initWorkspace();
109 }
110
111 /**
112 * Initializes various states for this workspace.
113 */
114 private void initWorkspace() {
115 mScroller = new Scroller(getContext());
116 mCurrentScreen = 0;
117
118 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
119 mTouchSlop = configuration.getScaledTouchSlop();
120 mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
121 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
122 }
123
124 public void setScreenSwitchListener(ScreenSwitchListener screenSwitchListener) {
125 mScreenSwitchListener = screenSwitchListener;
126 if (mScreenSwitchListener != null) {
127 mScreenSwitchListener.onScreenSwitch(getScreenAt(mCurrentScreen), mCurrentScreen);
128 }
129 }
130
131 /**
132 * Returns the index of the currently displayed screen.
133 *
134 * @return The index of the currently displayed screen.
135 */
136 int getCurrentScreen() {
137 return mCurrentScreen;
138 }
139
140 int getScreenCount() {
141 return getChildCount();
142 }
143
144 View getScreenAt(int index) {
145 return getChildAt(index);
146 }
147
148 int getScrollWidth() {
149 return getWidth();
150 }
151
152 /**
153 * Sets the current screen.
154 *
155 * @param currentScreen
156 */
157 void setCurrentScreen(int currentScreen) {
158 if (!mScroller.isFinished()) mScroller.abortAnimation();
159 if (getChildCount() == 0) return;
160
161 mCurrentScreen = Math.max(0, Math.min(currentScreen, getScreenCount() - 1));
162 scrollTo(getChildOffset(mCurrentScreen) - getRelativeChildOffset(mCurrentScreen), 0);
Winson Chung80baf5a2010-08-09 16:03:15 -0700163
Winson Chung321e9ee2010-08-09 13:37:56 -0700164 invalidate();
165 notifyScreenSwitchListener();
166 }
167
168 private void notifyScreenSwitchListener() {
169 if (mScreenSwitchListener != null) {
170 mScreenSwitchListener.onScreenSwitch(getScreenAt(mCurrentScreen), mCurrentScreen);
171 }
172 }
173
174 /**
175 * Registers the specified listener on each screen contained in this workspace.
176 *
177 * @param l The listener used to respond to long clicks.
178 */
179 @Override
180 public void setOnLongClickListener(OnLongClickListener l) {
181 mLongClickListener = l;
182 final int count = getScreenCount();
183 for (int i = 0; i < count; i++) {
184 getScreenAt(i).setOnLongClickListener(l);
185 }
186 }
187
188 @Override
189 public void computeScroll() {
190 if (mScroller.computeScrollOffset()) {
191 scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
192 postInvalidate();
193 } else if (mNextScreen != INVALID_SCREEN) {
194 mCurrentScreen = Math.max(0, Math.min(mNextScreen, getScreenCount() - 1));
195 notifyScreenSwitchListener();
196 mNextScreen = INVALID_SCREEN;
197 }
198 }
199
200 @Override
201 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
202 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
203 final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
204 if (widthMode != MeasureSpec.EXACTLY) {
205 throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
206 }
207
208 final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
209 final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
210 if (heightMode != MeasureSpec.EXACTLY) {
211 throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
212 }
213
214 // The children are given the same width and height as the workspace
215 final int childCount = getChildCount();
216 for (int i = 0; i < childCount; i++) {
217 getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
218 }
219
220 setMeasuredDimension(widthSize, heightSize);
221
222 if (mFirstLayout) {
223 setHorizontalScrollBarEnabled(false);
224 scrollTo(mCurrentScreen * widthSize, 0);
225 setHorizontalScrollBarEnabled(true);
226 mFirstLayout = false;
227 }
228 }
229
230 @Override
231 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
232 final int childCount = getChildCount();
233 int childLeft = 0;
234 if (childCount > 0) {
235 childLeft = (getMeasuredWidth() - getChildAt(0).getMeasuredWidth()) / 2;
236 }
237
238 for (int i = 0; i < childCount; i++) {
239 final View child = getChildAt(i);
240 if (child.getVisibility() != View.GONE) {
241 final int childWidth = child.getMeasuredWidth();
242 child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
243 childLeft += childWidth;
244 }
245 }
246 }
247
248 protected void invalidateDimmedPages() {
249 mDimmedPagesDirty = true;
250 }
251
252 @Override
253 protected void dispatchDraw(Canvas canvas) {
254 if (mDimmedPagesDirty || (mTouchState == TOUCH_STATE_SCROLLING) ||
255 !mScroller.isFinished()) {
256 int screenCenter = mScrollX + (getMeasuredWidth() / 2);
257 final int childCount = getChildCount();
258 for (int i = 0; i < childCount; ++i) {
259 PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
260 int childWidth = layout.getMeasuredWidth();
261 int halfChildWidth = (childWidth / 2);
262 int childCenter = getChildOffset(i) + halfChildWidth;
263 int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
Winson Chungb3347bb2010-08-19 14:51:28 -0700264 float alpha = 0.0f;
Winson Chung321e9ee2010-08-09 13:37:56 -0700265 if (distanceFromScreenCenter < halfChildWidth) {
Winson Chungb3347bb2010-08-19 14:51:28 -0700266 alpha = 1.0f;
Winson Chung321e9ee2010-08-09 13:37:56 -0700267 } else if (distanceFromScreenCenter > childWidth) {
Winson Chungb3347bb2010-08-19 14:51:28 -0700268 alpha = 0.0f;
Winson Chung321e9ee2010-08-09 13:37:56 -0700269 } else {
Winson Chungb3347bb2010-08-19 14:51:28 -0700270 float dimAlpha = (float) (distanceFromScreenCenter - halfChildWidth) / halfChildWidth;
271 dimAlpha = Math.max(0.0f, Math.min(1.0f, (dimAlpha * dimAlpha)));
272 alpha = 1.0f - dimAlpha;
Winson Chung321e9ee2010-08-09 13:37:56 -0700273 }
Winson Chungaffd7b42010-08-20 15:11:56 -0700274 if (Float.compare(alpha, layout.getAlpha()) != 0) {
Winson Chungb3347bb2010-08-19 14:51:28 -0700275 layout.setAlpha(alpha);
Winson Chungaffd7b42010-08-20 15:11:56 -0700276 }
Winson Chung321e9ee2010-08-09 13:37:56 -0700277 }
278 }
279 super.dispatchDraw(canvas);
280 }
281
282 @Override
283 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
284 int screen = indexOfChild(child);
285 if (screen != mCurrentScreen || !mScroller.isFinished()) {
286 snapToScreen(screen);
287 return true;
288 }
289 return false;
290 }
291
292 @Override
293 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
294 int focusableScreen;
295 if (mNextScreen != INVALID_SCREEN) {
296 focusableScreen = mNextScreen;
297 } else {
298 focusableScreen = mCurrentScreen;
299 }
300 View v = getScreenAt(focusableScreen);
301 if (v != null) {
302 v.requestFocus(direction, previouslyFocusedRect);
303 }
304 return false;
305 }
306
307 @Override
308 public boolean dispatchUnhandledMove(View focused, int direction) {
309 if (direction == View.FOCUS_LEFT) {
310 if (getCurrentScreen() > 0) {
311 snapToScreen(getCurrentScreen() - 1);
312 return true;
313 }
314 } else if (direction == View.FOCUS_RIGHT) {
315 if (getCurrentScreen() < getScreenCount() - 1) {
316 snapToScreen(getCurrentScreen() + 1);
317 return true;
318 }
319 }
320 return super.dispatchUnhandledMove(focused, direction);
321 }
322
323 @Override
324 public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
325 if (mCurrentScreen >= 0 && mCurrentScreen < getScreenCount()) {
326 getScreenAt(mCurrentScreen).addFocusables(views, direction);
327 }
328 if (direction == View.FOCUS_LEFT) {
329 if (mCurrentScreen > 0) {
330 getScreenAt(mCurrentScreen - 1).addFocusables(views, direction);
331 }
332 } else if (direction == View.FOCUS_RIGHT){
333 if (mCurrentScreen < getScreenCount() - 1) {
334 getScreenAt(mCurrentScreen + 1).addFocusables(views, direction);
335 }
336 }
337 }
338
339 /**
340 * If one of our descendant views decides that it could be focused now, only
341 * pass that along if it's on the current screen.
342 *
343 * This happens when live folders requery, and if they're off screen, they
344 * end up calling requestFocus, which pulls it on screen.
345 */
346 @Override
347 public void focusableViewAvailable(View focused) {
348 View current = getScreenAt(mCurrentScreen);
349 View v = focused;
350 while (true) {
351 if (v == current) {
352 super.focusableViewAvailable(focused);
353 return;
354 }
355 if (v == this) {
356 return;
357 }
358 ViewParent parent = v.getParent();
359 if (parent instanceof View) {
360 v = (View)v.getParent();
361 } else {
362 return;
363 }
364 }
365 }
366
367 /**
368 * {@inheritDoc}
369 */
370 @Override
371 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
372 if (disallowIntercept) {
373 // We need to make sure to cancel our long press if
374 // a scrollable widget takes over touch events
375 final View currentScreen = getChildAt(mCurrentScreen);
376 currentScreen.cancelLongPress();
377 }
378 super.requestDisallowInterceptTouchEvent(disallowIntercept);
379 }
380
381 @Override
382 public boolean onInterceptTouchEvent(MotionEvent ev) {
383 /*
384 * This method JUST determines whether we want to intercept the motion.
385 * If we return true, onTouchEvent will be called and we do the actual
386 * scrolling there.
387 */
388
389 /*
390 * Shortcut the most recurring case: the user is in the dragging
391 * state and he is moving his finger. We want to intercept this
392 * motion.
393 */
394 final int action = ev.getAction();
395 if ((action == MotionEvent.ACTION_MOVE) &&
396 (mTouchState == TOUCH_STATE_SCROLLING)) {
397 return true;
398 }
399
400
401 switch (action & MotionEvent.ACTION_MASK) {
402 case MotionEvent.ACTION_MOVE: {
403 /*
404 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
405 * whether the user has moved far enough from his original down touch.
406 */
407 determineScrollingStart(ev);
408 break;
409 }
410
411 case MotionEvent.ACTION_DOWN: {
412 final float x = ev.getX();
413 final float y = ev.getY();
414 // Remember location of down touch
415 mDownMotionX = x;
416 mLastMotionX = x;
417 mLastMotionY = y;
418 mActivePointerId = ev.getPointerId(0);
419 mAllowLongPress = true;
420
421 /*
422 * If being flinged and user touches the screen, initiate drag;
423 * otherwise don't. mScroller.isFinished should be false when
424 * being flinged.
425 */
426 mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
427
428 // check if this can be the beginning of a tap on the side of the screens
429 // to scroll the current page
430 if ((mTouchState != TOUCH_STATE_PREV_PAGE) &&
431 (mTouchState != TOUCH_STATE_NEXT_PAGE)) {
432 if (getChildCount() > 0) {
433 int relativeChildLeft = getChildOffset(0);
434 int relativeChildRight = relativeChildLeft + getChildAt(0).getMeasuredWidth();
435 if (x < relativeChildLeft) {
436 mTouchState = TOUCH_STATE_PREV_PAGE;
437 } else if (x > relativeChildRight) {
438 mTouchState = TOUCH_STATE_NEXT_PAGE;
439 }
440 }
441 }
442 break;
443 }
444
445 case MotionEvent.ACTION_CANCEL:
446 case MotionEvent.ACTION_UP:
447 // Release the drag
448 mTouchState = TOUCH_STATE_REST;
449 mAllowLongPress = false;
450 mActivePointerId = INVALID_POINTER;
451
452 break;
453
454 case MotionEvent.ACTION_POINTER_UP:
455 onSecondaryPointerUp(ev);
456 break;
457 }
458
459 /*
460 * The only time we want to intercept motion events is if we are in the
461 * drag mode.
462 */
463 return mTouchState != TOUCH_STATE_REST;
464 }
465
Winson Chung80baf5a2010-08-09 16:03:15 -0700466 protected void animateClickFeedback(View v, final Runnable r) {
467 // animate the view slightly to show click feedback running some logic after it is "pressed"
468 Animation anim = AnimationUtils.loadAnimation(getContext(),
469 R.anim.paged_view_click_feedback);
470 anim.setAnimationListener(new AnimationListener() {
471 @Override
472 public void onAnimationStart(Animation animation) {}
473 @Override
474 public void onAnimationRepeat(Animation animation) {
475 r.run();
476 }
477 @Override
478 public void onAnimationEnd(Animation animation) {}
479 });
480 v.startAnimation(anim);
481 }
482
Winson Chung321e9ee2010-08-09 13:37:56 -0700483 /*
484 * Determines if we should change the touch state to start scrolling after the
485 * user moves their touch point too far.
486 */
487 private void determineScrollingStart(MotionEvent ev) {
488 /*
489 * Locally do absolute value. mLastMotionX is set to the y value
490 * of the down event.
491 */
492 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
493 final float x = ev.getX(pointerIndex);
494 final float y = ev.getY(pointerIndex);
495 final int xDiff = (int) Math.abs(x - mLastMotionX);
496 final int yDiff = (int) Math.abs(y - mLastMotionY);
497
498 final int touchSlop = mTouchSlop;
499 boolean xPaged = xDiff > mPagingTouchSlop;
500 boolean xMoved = xDiff > touchSlop;
501 boolean yMoved = yDiff > touchSlop;
502
503 if (xMoved || yMoved) {
504 if (xPaged) {
505 // Scroll if the user moved far enough along the X axis
506 mTouchState = TOUCH_STATE_SCROLLING;
507 mLastMotionX = x;
508 }
509 // Either way, cancel any pending longpress
510 if (mAllowLongPress) {
511 mAllowLongPress = false;
512 // Try canceling the long press. It could also have been scheduled
513 // by a distant descendant, so use the mAllowLongPress flag to block
514 // everything
515 final View currentScreen = getScreenAt(mCurrentScreen);
516 currentScreen.cancelLongPress();
517 }
518 }
519 }
520
521 @Override
522 public boolean onTouchEvent(MotionEvent ev) {
523 if (mVelocityTracker == null) {
524 mVelocityTracker = VelocityTracker.obtain();
525 }
526 mVelocityTracker.addMovement(ev);
527
528 final int action = ev.getAction();
529
530 switch (action & MotionEvent.ACTION_MASK) {
531 case MotionEvent.ACTION_DOWN:
532 /*
533 * If being flinged and user touches, stop the fling. isFinished
534 * will be false if being flinged.
535 */
536 if (!mScroller.isFinished()) {
537 mScroller.abortAnimation();
538 }
539
540 // Remember where the motion event started
541 mDownMotionX = mLastMotionX = ev.getX();
542 mActivePointerId = ev.getPointerId(0);
543 break;
544
545 case MotionEvent.ACTION_MOVE:
546 if (mTouchState == TOUCH_STATE_SCROLLING) {
547 // Scroll to follow the motion event
548 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
549 final float x = ev.getX(pointerIndex);
550 final int deltaX = (int) (mLastMotionX - x);
551 mLastMotionX = x;
552
553 int sx = getScrollX();
554 if (deltaX < 0) {
555 if (sx > 0) {
556 scrollBy(Math.max(-sx, deltaX), 0);
557 }
558 } else if (deltaX > 0) {
559 final int lastChildIndex = getChildCount() - 1;
560 final int availableToScroll = getChildOffset(lastChildIndex) -
561 getRelativeChildOffset(lastChildIndex) - sx;
562 if (availableToScroll > 0) {
563 scrollBy(Math.min(availableToScroll, deltaX), 0);
564 }
565 } else {
566 awakenScrollBars();
567 }
568 } else if ((mTouchState == TOUCH_STATE_PREV_PAGE) ||
569 (mTouchState == TOUCH_STATE_NEXT_PAGE)) {
570 determineScrollingStart(ev);
571 }
572 break;
573
574 case MotionEvent.ACTION_UP:
575 if (mTouchState == TOUCH_STATE_SCROLLING) {
576 final int activePointerId = mActivePointerId;
577 final int pointerIndex = ev.findPointerIndex(activePointerId);
578 final float x = ev.getX(pointerIndex);
579 final VelocityTracker velocityTracker = mVelocityTracker;
580 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
581 int velocityX = (int) velocityTracker.getXVelocity(activePointerId);
582 boolean isfling = Math.abs(mDownMotionX - x) > MIN_LENGTH_FOR_FLING;
583
584 if (isfling && velocityX > SNAP_VELOCITY && mCurrentScreen > 0) {
585 snapToScreen(mCurrentScreen - 1);
586 } else if (isfling && velocityX < -SNAP_VELOCITY &&
587 mCurrentScreen < getChildCount() - 1) {
588 snapToScreen(mCurrentScreen + 1);
589 } else {
590 snapToDestination();
591 }
592
593 if (mVelocityTracker != null) {
594 mVelocityTracker.recycle();
595 mVelocityTracker = null;
596 }
597 } else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
598 // at this point we have not moved beyond the touch slop
599 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
600 // we can just page
601 int nextScreen = Math.max(0, mCurrentScreen - 1);
602 if (nextScreen != mCurrentScreen) {
603 snapToScreen(nextScreen);
604 } else {
605 snapToDestination();
606 }
607 } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) {
608 // at this point we have not moved beyond the touch slop
609 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
610 // we can just page
611 int nextScreen = Math.min(getChildCount() - 1, mCurrentScreen + 1);
612 if (nextScreen != mCurrentScreen) {
613 snapToScreen(nextScreen);
614 } else {
615 snapToDestination();
616 }
617 }
618 mTouchState = TOUCH_STATE_REST;
619 mActivePointerId = INVALID_POINTER;
620 break;
621
622 case MotionEvent.ACTION_CANCEL:
623 mTouchState = TOUCH_STATE_REST;
624 mActivePointerId = INVALID_POINTER;
625 break;
626
627 case MotionEvent.ACTION_POINTER_UP:
628 onSecondaryPointerUp(ev);
629 break;
630 }
631
632 return true;
633 }
634
635 private void onSecondaryPointerUp(MotionEvent ev) {
636 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
637 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
638 final int pointerId = ev.getPointerId(pointerIndex);
639 if (pointerId == mActivePointerId) {
640 // This was our active pointer going up. Choose a new
641 // active pointer and adjust accordingly.
642 // TODO: Make this decision more intelligent.
643 final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
644 mLastMotionX = mDownMotionX = ev.getX(newPointerIndex);
645 mLastMotionY = ev.getY(newPointerIndex);
646 mActivePointerId = ev.getPointerId(newPointerIndex);
647 if (mVelocityTracker != null) {
648 mVelocityTracker.clear();
649 }
650 }
651 }
652
653 @Override
654 public void requestChildFocus(View child, View focused) {
655 super.requestChildFocus(child, focused);
656 int screen = indexOfChild(child);
657 if (screen >= 0 && !isInTouchMode()) {
658 snapToScreen(screen);
659 }
660 }
661
662 protected int getRelativeChildOffset(int index) {
663 return (getMeasuredWidth() - getChildAt(index).getMeasuredWidth()) / 2;
664 }
665
666 protected int getChildOffset(int index) {
667 if (getChildCount() == 0)
668 return 0;
669
670 int offset = getRelativeChildOffset(0);
671 for (int i = 0; i < index; ++i) {
672 offset += getChildAt(i).getMeasuredWidth();
673 }
674 return offset;
675 }
676
677 protected void snapToDestination() {
678 int minDistanceFromScreenCenter = getMeasuredWidth();
679 int minDistanceFromScreenCenterIndex = -1;
680 int screenCenter = mScrollX + (getMeasuredWidth() / 2);
681 final int childCount = getChildCount();
682 for (int i = 0; i < childCount; ++i) {
683 PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
684 int childWidth = layout.getMeasuredWidth();
685 int halfChildWidth = (childWidth / 2);
686 int childCenter = getChildOffset(i) + halfChildWidth;
687 int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
688 if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
689 minDistanceFromScreenCenter = distanceFromScreenCenter;
690 minDistanceFromScreenCenterIndex = i;
691 }
692 }
693 snapToScreen(minDistanceFromScreenCenterIndex, 1000);
694 }
695
696 void snapToScreen(int whichScreen) {
697 snapToScreen(whichScreen, 1000);
698 }
699
700 void snapToScreen(int whichScreen, int duration) {
701 whichScreen = Math.max(0, Math.min(whichScreen, getScreenCount() - 1));
702
703 mNextScreen = whichScreen;
704
705 int newX = getChildOffset(whichScreen) - getRelativeChildOffset(whichScreen);
706 final int sX = getScrollX();
707 final int delta = newX - sX;
708 awakenScrollBars(duration);
709 if (duration == 0) {
710 duration = Math.abs(delta);
711 }
712
713 if (!mScroller.isFinished()) mScroller.abortAnimation();
714 mScroller.startScroll(sX, 0, delta, 0, duration);
Winson Chung80baf5a2010-08-09 16:03:15 -0700715
716 // only load some associated pages
717 loadAssociatedPages(mNextScreen);
718
Winson Chung321e9ee2010-08-09 13:37:56 -0700719 invalidate();
720 }
721
722 @Override
723 protected Parcelable onSaveInstanceState() {
724 final SavedState state = new SavedState(super.onSaveInstanceState());
725 state.currentScreen = mCurrentScreen;
726 return state;
727 }
728
729 @Override
730 protected void onRestoreInstanceState(Parcelable state) {
731 SavedState savedState = (SavedState) state;
732 super.onRestoreInstanceState(savedState.getSuperState());
733 if (savedState.currentScreen != -1) {
734 mCurrentScreen = savedState.currentScreen;
735 }
736 }
737
738 public void scrollLeft() {
739 if (mScroller.isFinished()) {
740 if (mCurrentScreen > 0) snapToScreen(mCurrentScreen - 1);
741 } else {
742 if (mNextScreen > 0) snapToScreen(mNextScreen - 1);
743 }
744 }
745
746 public void scrollRight() {
747 if (mScroller.isFinished()) {
748 if (mCurrentScreen < getChildCount() -1) snapToScreen(mCurrentScreen + 1);
749 } else {
750 if (mNextScreen < getChildCount() -1) snapToScreen(mNextScreen + 1);
751 }
752 }
753
754 public int getScreenForView(View v) {
755 int result = -1;
756 if (v != null) {
757 ViewParent vp = v.getParent();
758 int count = getChildCount();
759 for (int i = 0; i < count; i++) {
760 if (vp == getChildAt(i)) {
761 return i;
762 }
763 }
764 }
765 return result;
766 }
767
768 /**
769 * @return True is long presses are still allowed for the current touch
770 */
771 public boolean allowLongPress() {
772 return mAllowLongPress;
773 }
774
775 public static class SavedState extends BaseSavedState {
776 int currentScreen = -1;
777
778 SavedState(Parcelable superState) {
779 super(superState);
780 }
781
782 private SavedState(Parcel in) {
783 super(in);
784 currentScreen = in.readInt();
785 }
786
787 @Override
788 public void writeToParcel(Parcel out, int flags) {
789 super.writeToParcel(out, flags);
790 out.writeInt(currentScreen);
791 }
792
793 public static final Parcelable.Creator<SavedState> CREATOR =
794 new Parcelable.Creator<SavedState>() {
795 public SavedState createFromParcel(Parcel in) {
796 return new SavedState(in);
797 }
798
799 public SavedState[] newArray(int size) {
800 return new SavedState[size];
801 }
802 };
803 }
804
Winson Chung80baf5a2010-08-09 16:03:15 -0700805 public void loadAssociatedPages(int screen) {
806 final int count = getChildCount();
807 if (screen < count) {
808 int lowerScreenBound = Math.max(0, screen - 1);
809 int upperScreenBound = Math.min(screen + 1, count - 1);
Winson Chung80baf5a2010-08-09 16:03:15 -0700810 for (int i = 0; i < count; ++i) {
811 if (lowerScreenBound <= i && i <= upperScreenBound) {
812 syncPageItems(i);
813 } else {
Winson Chungb3347bb2010-08-19 14:51:28 -0700814 final PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
Winson Chung80baf5a2010-08-09 16:03:15 -0700815 if (layout.getChildCount() > 0) {
816 layout.removeAllViews();
817 }
Winson Chung80baf5a2010-08-09 16:03:15 -0700818 }
819 }
Winson Chung80baf5a2010-08-09 16:03:15 -0700820 }
821 }
822
Winson Chung321e9ee2010-08-09 13:37:56 -0700823 public abstract void syncPages();
824 public abstract void syncPageItems(int page);
825 public void invalidatePageData() {
826 syncPages();
Winson Chung80baf5a2010-08-09 16:03:15 -0700827 loadAssociatedPages(mCurrentScreen);
Winson Chung321e9ee2010-08-09 13:37:56 -0700828 invalidateDimmedPages();
829 requestLayout();
830 }
831}