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