blob: 03a4d0c03b33fc3d5a4f5ed6eba8ac5f19d3eafb [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 Chungb3347bb2010-08-19 14:51:28 -0700274 if (Float.compare(alpha, layout.getAlpha()) != 0)
275 layout.setAlpha(alpha);
Winson Chung321e9ee2010-08-09 13:37:56 -0700276 }
277 }
278 super.dispatchDraw(canvas);
279 }
280
281 @Override
282 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
283 int screen = indexOfChild(child);
284 if (screen != mCurrentScreen || !mScroller.isFinished()) {
285 snapToScreen(screen);
286 return true;
287 }
288 return false;
289 }
290
291 @Override
292 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
293 int focusableScreen;
294 if (mNextScreen != INVALID_SCREEN) {
295 focusableScreen = mNextScreen;
296 } else {
297 focusableScreen = mCurrentScreen;
298 }
299 View v = getScreenAt(focusableScreen);
300 if (v != null) {
301 v.requestFocus(direction, previouslyFocusedRect);
302 }
303 return false;
304 }
305
306 @Override
307 public boolean dispatchUnhandledMove(View focused, int direction) {
308 if (direction == View.FOCUS_LEFT) {
309 if (getCurrentScreen() > 0) {
310 snapToScreen(getCurrentScreen() - 1);
311 return true;
312 }
313 } else if (direction == View.FOCUS_RIGHT) {
314 if (getCurrentScreen() < getScreenCount() - 1) {
315 snapToScreen(getCurrentScreen() + 1);
316 return true;
317 }
318 }
319 return super.dispatchUnhandledMove(focused, direction);
320 }
321
322 @Override
323 public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
324 if (mCurrentScreen >= 0 && mCurrentScreen < getScreenCount()) {
325 getScreenAt(mCurrentScreen).addFocusables(views, direction);
326 }
327 if (direction == View.FOCUS_LEFT) {
328 if (mCurrentScreen > 0) {
329 getScreenAt(mCurrentScreen - 1).addFocusables(views, direction);
330 }
331 } else if (direction == View.FOCUS_RIGHT){
332 if (mCurrentScreen < getScreenCount() - 1) {
333 getScreenAt(mCurrentScreen + 1).addFocusables(views, direction);
334 }
335 }
336 }
337
338 /**
339 * If one of our descendant views decides that it could be focused now, only
340 * pass that along if it's on the current screen.
341 *
342 * This happens when live folders requery, and if they're off screen, they
343 * end up calling requestFocus, which pulls it on screen.
344 */
345 @Override
346 public void focusableViewAvailable(View focused) {
347 View current = getScreenAt(mCurrentScreen);
348 View v = focused;
349 while (true) {
350 if (v == current) {
351 super.focusableViewAvailable(focused);
352 return;
353 }
354 if (v == this) {
355 return;
356 }
357 ViewParent parent = v.getParent();
358 if (parent instanceof View) {
359 v = (View)v.getParent();
360 } else {
361 return;
362 }
363 }
364 }
365
366 /**
367 * {@inheritDoc}
368 */
369 @Override
370 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
371 if (disallowIntercept) {
372 // We need to make sure to cancel our long press if
373 // a scrollable widget takes over touch events
374 final View currentScreen = getChildAt(mCurrentScreen);
375 currentScreen.cancelLongPress();
376 }
377 super.requestDisallowInterceptTouchEvent(disallowIntercept);
378 }
379
380 @Override
381 public boolean onInterceptTouchEvent(MotionEvent ev) {
382 /*
383 * This method JUST determines whether we want to intercept the motion.
384 * If we return true, onTouchEvent will be called and we do the actual
385 * scrolling there.
386 */
387
388 /*
389 * Shortcut the most recurring case: the user is in the dragging
390 * state and he is moving his finger. We want to intercept this
391 * motion.
392 */
393 final int action = ev.getAction();
394 if ((action == MotionEvent.ACTION_MOVE) &&
395 (mTouchState == TOUCH_STATE_SCROLLING)) {
396 return true;
397 }
398
399
400 switch (action & MotionEvent.ACTION_MASK) {
401 case MotionEvent.ACTION_MOVE: {
402 /*
403 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
404 * whether the user has moved far enough from his original down touch.
405 */
406 determineScrollingStart(ev);
407 break;
408 }
409
410 case MotionEvent.ACTION_DOWN: {
411 final float x = ev.getX();
412 final float y = ev.getY();
413 // Remember location of down touch
414 mDownMotionX = x;
415 mLastMotionX = x;
416 mLastMotionY = y;
417 mActivePointerId = ev.getPointerId(0);
418 mAllowLongPress = true;
419
420 /*
421 * If being flinged and user touches the screen, initiate drag;
422 * otherwise don't. mScroller.isFinished should be false when
423 * being flinged.
424 */
425 mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
426
427 // check if this can be the beginning of a tap on the side of the screens
428 // to scroll the current page
429 if ((mTouchState != TOUCH_STATE_PREV_PAGE) &&
430 (mTouchState != TOUCH_STATE_NEXT_PAGE)) {
431 if (getChildCount() > 0) {
432 int relativeChildLeft = getChildOffset(0);
433 int relativeChildRight = relativeChildLeft + getChildAt(0).getMeasuredWidth();
434 if (x < relativeChildLeft) {
435 mTouchState = TOUCH_STATE_PREV_PAGE;
436 } else if (x > relativeChildRight) {
437 mTouchState = TOUCH_STATE_NEXT_PAGE;
438 }
439 }
440 }
441 break;
442 }
443
444 case MotionEvent.ACTION_CANCEL:
445 case MotionEvent.ACTION_UP:
446 // Release the drag
447 mTouchState = TOUCH_STATE_REST;
448 mAllowLongPress = false;
449 mActivePointerId = INVALID_POINTER;
450
451 break;
452
453 case MotionEvent.ACTION_POINTER_UP:
454 onSecondaryPointerUp(ev);
455 break;
456 }
457
458 /*
459 * The only time we want to intercept motion events is if we are in the
460 * drag mode.
461 */
462 return mTouchState != TOUCH_STATE_REST;
463 }
464
Winson Chung80baf5a2010-08-09 16:03:15 -0700465 protected void animateClickFeedback(View v, final Runnable r) {
466 // animate the view slightly to show click feedback running some logic after it is "pressed"
467 Animation anim = AnimationUtils.loadAnimation(getContext(),
468 R.anim.paged_view_click_feedback);
469 anim.setAnimationListener(new AnimationListener() {
470 @Override
471 public void onAnimationStart(Animation animation) {}
472 @Override
473 public void onAnimationRepeat(Animation animation) {
474 r.run();
475 }
476 @Override
477 public void onAnimationEnd(Animation animation) {}
478 });
479 v.startAnimation(anim);
480 }
481
Winson Chung321e9ee2010-08-09 13:37:56 -0700482 /*
483 * Determines if we should change the touch state to start scrolling after the
484 * user moves their touch point too far.
485 */
486 private void determineScrollingStart(MotionEvent ev) {
487 /*
488 * Locally do absolute value. mLastMotionX is set to the y value
489 * of the down event.
490 */
491 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
492 final float x = ev.getX(pointerIndex);
493 final float y = ev.getY(pointerIndex);
494 final int xDiff = (int) Math.abs(x - mLastMotionX);
495 final int yDiff = (int) Math.abs(y - mLastMotionY);
496
497 final int touchSlop = mTouchSlop;
498 boolean xPaged = xDiff > mPagingTouchSlop;
499 boolean xMoved = xDiff > touchSlop;
500 boolean yMoved = yDiff > touchSlop;
501
502 if (xMoved || yMoved) {
503 if (xPaged) {
504 // Scroll if the user moved far enough along the X axis
505 mTouchState = TOUCH_STATE_SCROLLING;
506 mLastMotionX = x;
507 }
508 // Either way, cancel any pending longpress
509 if (mAllowLongPress) {
510 mAllowLongPress = false;
511 // Try canceling the long press. It could also have been scheduled
512 // by a distant descendant, so use the mAllowLongPress flag to block
513 // everything
514 final View currentScreen = getScreenAt(mCurrentScreen);
515 currentScreen.cancelLongPress();
516 }
517 }
518 }
519
520 @Override
521 public boolean onTouchEvent(MotionEvent ev) {
522 if (mVelocityTracker == null) {
523 mVelocityTracker = VelocityTracker.obtain();
524 }
525 mVelocityTracker.addMovement(ev);
526
527 final int action = ev.getAction();
528
529 switch (action & MotionEvent.ACTION_MASK) {
530 case MotionEvent.ACTION_DOWN:
531 /*
532 * If being flinged and user touches, stop the fling. isFinished
533 * will be false if being flinged.
534 */
535 if (!mScroller.isFinished()) {
536 mScroller.abortAnimation();
537 }
538
539 // Remember where the motion event started
540 mDownMotionX = mLastMotionX = ev.getX();
541 mActivePointerId = ev.getPointerId(0);
542 break;
543
544 case MotionEvent.ACTION_MOVE:
545 if (mTouchState == TOUCH_STATE_SCROLLING) {
546 // Scroll to follow the motion event
547 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
548 final float x = ev.getX(pointerIndex);
549 final int deltaX = (int) (mLastMotionX - x);
550 mLastMotionX = x;
551
552 int sx = getScrollX();
553 if (deltaX < 0) {
554 if (sx > 0) {
555 scrollBy(Math.max(-sx, deltaX), 0);
556 }
557 } else if (deltaX > 0) {
558 final int lastChildIndex = getChildCount() - 1;
559 final int availableToScroll = getChildOffset(lastChildIndex) -
560 getRelativeChildOffset(lastChildIndex) - sx;
561 if (availableToScroll > 0) {
562 scrollBy(Math.min(availableToScroll, deltaX), 0);
563 }
564 } else {
565 awakenScrollBars();
566 }
567 } else if ((mTouchState == TOUCH_STATE_PREV_PAGE) ||
568 (mTouchState == TOUCH_STATE_NEXT_PAGE)) {
569 determineScrollingStart(ev);
570 }
571 break;
572
573 case MotionEvent.ACTION_UP:
574 if (mTouchState == TOUCH_STATE_SCROLLING) {
575 final int activePointerId = mActivePointerId;
576 final int pointerIndex = ev.findPointerIndex(activePointerId);
577 final float x = ev.getX(pointerIndex);
578 final VelocityTracker velocityTracker = mVelocityTracker;
579 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
580 int velocityX = (int) velocityTracker.getXVelocity(activePointerId);
581 boolean isfling = Math.abs(mDownMotionX - x) > MIN_LENGTH_FOR_FLING;
582
583 if (isfling && velocityX > SNAP_VELOCITY && mCurrentScreen > 0) {
584 snapToScreen(mCurrentScreen - 1);
585 } else if (isfling && velocityX < -SNAP_VELOCITY &&
586 mCurrentScreen < getChildCount() - 1) {
587 snapToScreen(mCurrentScreen + 1);
588 } else {
589 snapToDestination();
590 }
591
592 if (mVelocityTracker != null) {
593 mVelocityTracker.recycle();
594 mVelocityTracker = null;
595 }
596 } else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
597 // at this point we have not moved beyond the touch slop
598 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
599 // we can just page
600 int nextScreen = Math.max(0, mCurrentScreen - 1);
601 if (nextScreen != mCurrentScreen) {
602 snapToScreen(nextScreen);
603 } else {
604 snapToDestination();
605 }
606 } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) {
607 // at this point we have not moved beyond the touch slop
608 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
609 // we can just page
610 int nextScreen = Math.min(getChildCount() - 1, mCurrentScreen + 1);
611 if (nextScreen != mCurrentScreen) {
612 snapToScreen(nextScreen);
613 } else {
614 snapToDestination();
615 }
616 }
617 mTouchState = TOUCH_STATE_REST;
618 mActivePointerId = INVALID_POINTER;
619 break;
620
621 case MotionEvent.ACTION_CANCEL:
622 mTouchState = TOUCH_STATE_REST;
623 mActivePointerId = INVALID_POINTER;
624 break;
625
626 case MotionEvent.ACTION_POINTER_UP:
627 onSecondaryPointerUp(ev);
628 break;
629 }
630
631 return true;
632 }
633
634 private void onSecondaryPointerUp(MotionEvent ev) {
635 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
636 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
637 final int pointerId = ev.getPointerId(pointerIndex);
638 if (pointerId == mActivePointerId) {
639 // This was our active pointer going up. Choose a new
640 // active pointer and adjust accordingly.
641 // TODO: Make this decision more intelligent.
642 final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
643 mLastMotionX = mDownMotionX = ev.getX(newPointerIndex);
644 mLastMotionY = ev.getY(newPointerIndex);
645 mActivePointerId = ev.getPointerId(newPointerIndex);
646 if (mVelocityTracker != null) {
647 mVelocityTracker.clear();
648 }
649 }
650 }
651
652 @Override
653 public void requestChildFocus(View child, View focused) {
654 super.requestChildFocus(child, focused);
655 int screen = indexOfChild(child);
656 if (screen >= 0 && !isInTouchMode()) {
657 snapToScreen(screen);
658 }
659 }
660
661 protected int getRelativeChildOffset(int index) {
662 return (getMeasuredWidth() - getChildAt(index).getMeasuredWidth()) / 2;
663 }
664
665 protected int getChildOffset(int index) {
666 if (getChildCount() == 0)
667 return 0;
668
669 int offset = getRelativeChildOffset(0);
670 for (int i = 0; i < index; ++i) {
671 offset += getChildAt(i).getMeasuredWidth();
672 }
673 return offset;
674 }
675
676 protected void snapToDestination() {
677 int minDistanceFromScreenCenter = getMeasuredWidth();
678 int minDistanceFromScreenCenterIndex = -1;
679 int screenCenter = mScrollX + (getMeasuredWidth() / 2);
680 final int childCount = getChildCount();
681 for (int i = 0; i < childCount; ++i) {
682 PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
683 int childWidth = layout.getMeasuredWidth();
684 int halfChildWidth = (childWidth / 2);
685 int childCenter = getChildOffset(i) + halfChildWidth;
686 int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
687 if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
688 minDistanceFromScreenCenter = distanceFromScreenCenter;
689 minDistanceFromScreenCenterIndex = i;
690 }
691 }
692 snapToScreen(minDistanceFromScreenCenterIndex, 1000);
693 }
694
695 void snapToScreen(int whichScreen) {
696 snapToScreen(whichScreen, 1000);
697 }
698
699 void snapToScreen(int whichScreen, int duration) {
700 whichScreen = Math.max(0, Math.min(whichScreen, getScreenCount() - 1));
701
702 mNextScreen = whichScreen;
703
704 int newX = getChildOffset(whichScreen) - getRelativeChildOffset(whichScreen);
705 final int sX = getScrollX();
706 final int delta = newX - sX;
707 awakenScrollBars(duration);
708 if (duration == 0) {
709 duration = Math.abs(delta);
710 }
711
712 if (!mScroller.isFinished()) mScroller.abortAnimation();
713 mScroller.startScroll(sX, 0, delta, 0, duration);
Winson Chung80baf5a2010-08-09 16:03:15 -0700714
715 // only load some associated pages
716 loadAssociatedPages(mNextScreen);
717
Winson Chung321e9ee2010-08-09 13:37:56 -0700718 invalidate();
719 }
720
721 @Override
722 protected Parcelable onSaveInstanceState() {
723 final SavedState state = new SavedState(super.onSaveInstanceState());
724 state.currentScreen = mCurrentScreen;
725 return state;
726 }
727
728 @Override
729 protected void onRestoreInstanceState(Parcelable state) {
730 SavedState savedState = (SavedState) state;
731 super.onRestoreInstanceState(savedState.getSuperState());
732 if (savedState.currentScreen != -1) {
733 mCurrentScreen = savedState.currentScreen;
734 }
735 }
736
737 public void scrollLeft() {
738 if (mScroller.isFinished()) {
739 if (mCurrentScreen > 0) snapToScreen(mCurrentScreen - 1);
740 } else {
741 if (mNextScreen > 0) snapToScreen(mNextScreen - 1);
742 }
743 }
744
745 public void scrollRight() {
746 if (mScroller.isFinished()) {
747 if (mCurrentScreen < getChildCount() -1) snapToScreen(mCurrentScreen + 1);
748 } else {
749 if (mNextScreen < getChildCount() -1) snapToScreen(mNextScreen + 1);
750 }
751 }
752
753 public int getScreenForView(View v) {
754 int result = -1;
755 if (v != null) {
756 ViewParent vp = v.getParent();
757 int count = getChildCount();
758 for (int i = 0; i < count; i++) {
759 if (vp == getChildAt(i)) {
760 return i;
761 }
762 }
763 }
764 return result;
765 }
766
767 /**
768 * @return True is long presses are still allowed for the current touch
769 */
770 public boolean allowLongPress() {
771 return mAllowLongPress;
772 }
773
774 public static class SavedState extends BaseSavedState {
775 int currentScreen = -1;
776
777 SavedState(Parcelable superState) {
778 super(superState);
779 }
780
781 private SavedState(Parcel in) {
782 super(in);
783 currentScreen = in.readInt();
784 }
785
786 @Override
787 public void writeToParcel(Parcel out, int flags) {
788 super.writeToParcel(out, flags);
789 out.writeInt(currentScreen);
790 }
791
792 public static final Parcelable.Creator<SavedState> CREATOR =
793 new Parcelable.Creator<SavedState>() {
794 public SavedState createFromParcel(Parcel in) {
795 return new SavedState(in);
796 }
797
798 public SavedState[] newArray(int size) {
799 return new SavedState[size];
800 }
801 };
802 }
803
Winson Chung80baf5a2010-08-09 16:03:15 -0700804 public void loadAssociatedPages(int screen) {
805 final int count = getChildCount();
806 if (screen < count) {
807 int lowerScreenBound = Math.max(0, screen - 1);
808 int upperScreenBound = Math.min(screen + 1, count - 1);
Winson Chung80baf5a2010-08-09 16:03:15 -0700809 for (int i = 0; i < count; ++i) {
810 if (lowerScreenBound <= i && i <= upperScreenBound) {
811 syncPageItems(i);
812 } else {
Winson Chungb3347bb2010-08-19 14:51:28 -0700813 final PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
Winson Chung80baf5a2010-08-09 16:03:15 -0700814 if (layout.getChildCount() > 0) {
815 layout.removeAllViews();
816 }
Winson Chung80baf5a2010-08-09 16:03:15 -0700817 }
818 }
Winson Chung80baf5a2010-08-09 16:03:15 -0700819 }
820 }
821
Winson Chung321e9ee2010-08-09 13:37:56 -0700822 public abstract void syncPages();
823 public abstract void syncPageItems(int page);
824 public void invalidatePageData() {
825 syncPages();
Winson Chung80baf5a2010-08-09 16:03:15 -0700826 loadAssociatedPages(mCurrentScreen);
Winson Chung321e9ee2010-08-09 13:37:56 -0700827 invalidateDimmedPages();
828 requestLayout();
829 }
830}