PagedView.java revision 28750fba6a2d141eb9a1e566718c17236030b815
1/* 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; 20import java.util.HashMap; 21 22import android.content.Context; 23import android.graphics.Bitmap; 24import android.graphics.Canvas; 25import android.graphics.Rect; 26import android.os.Parcel; 27import android.os.Parcelable; 28import android.util.AttributeSet; 29import android.util.Log; 30import android.view.ActionMode; 31import android.view.MotionEvent; 32import android.view.VelocityTracker; 33import android.view.View; 34import android.view.ViewConfiguration; 35import android.view.ViewGroup; 36import android.view.ViewParent; 37import android.view.animation.Animation; 38import android.view.animation.Animation.AnimationListener; 39import android.view.animation.AnimationUtils; 40import android.widget.Checkable; 41import android.widget.LinearLayout; 42import android.widget.Scroller; 43 44import com.android.launcher.R; 45 46/** 47 * An abstraction of the original Workspace which supports browsing through a 48 * sequential list of "pages" 49 */ 50public abstract class PagedView extends ViewGroup { 51 private static final String TAG = "PagedView"; 52 protected static final int INVALID_PAGE = -1; 53 54 // the min drag distance for a fling to register, to prevent random page shifts 55 private static final int MIN_LENGTH_FOR_FLING = 50; 56 57 private static final int PAGE_SNAP_ANIMATION_DURATION = 1000; 58 protected static final float NANOTIME_DIV = 1000000000.0f; 59 60 // the velocity at which a fling gesture will cause us to snap to the next page 61 protected int mSnapVelocity = 500; 62 63 protected float mSmoothingTime; 64 protected float mTouchX; 65 66 protected boolean mFirstLayout = true; 67 68 protected int mCurrentPage; 69 protected int mNextPage = INVALID_PAGE; 70 protected Scroller mScroller; 71 private VelocityTracker mVelocityTracker; 72 73 private float mDownMotionX; 74 private float mLastMotionX; 75 private float mLastMotionY; 76 77 protected final static int TOUCH_STATE_REST = 0; 78 protected final static int TOUCH_STATE_SCROLLING = 1; 79 protected final static int TOUCH_STATE_PREV_PAGE = 2; 80 protected final static int TOUCH_STATE_NEXT_PAGE = 3; 81 82 protected int mTouchState = TOUCH_STATE_REST; 83 84 protected OnLongClickListener mLongClickListener; 85 86 private boolean mAllowLongPress = true; 87 88 private int mTouchSlop; 89 private int mPagingTouchSlop; 90 private int mMaximumVelocity; 91 92 protected static final int INVALID_POINTER = -1; 93 94 protected int mActivePointerId = INVALID_POINTER; 95 96 private PageSwitchListener mPageSwitchListener; 97 98 private ArrayList<Boolean> mDirtyPageContent; 99 private boolean mDirtyPageAlpha; 100 101 // choice modes 102 protected static final int CHOICE_MODE_NONE = 0; 103 protected static final int CHOICE_MODE_SINGLE = 1; 104 // Multiple selection mode is not supported by all Launcher actions atm 105 protected static final int CHOICE_MODE_MULTIPLE = 2; 106 107 private int mChoiceMode; 108 private ActionMode mActionMode; 109 110 protected PagedViewIconCache mPageViewIconCache; 111 112 // If true, syncPages and syncPageItems will be called to refresh pages 113 protected boolean mContentIsRefreshable = true; 114 115 // If true, modify alpha of neighboring pages as user scrolls left/right 116 protected boolean mFadeInAdjacentScreens = true; 117 118 // It true, use a different slop parameter (pagingTouchSlop = 2 * touchSlop) for deciding 119 // to switch to a new page 120 protected boolean mUsePagingTouchSlop = true; 121 122 // If true, the subclass should directly update mScrollX itself in its computeScroll method 123 // (SmoothPagedView does this) 124 protected boolean mDeferScrollUpdate = false; 125 126 /** 127 * Simple cache mechanism for PagedViewIcon outlines. 128 */ 129 class PagedViewIconCache { 130 private final HashMap<Object, Bitmap> iconOutlineCache = new HashMap<Object, Bitmap>(); 131 132 public void clear() { 133 iconOutlineCache.clear(); 134 } 135 public void addOutline(Object key, Bitmap b) { 136 iconOutlineCache.put(key, b); 137 } 138 public void removeOutline(Object key) { 139 if (iconOutlineCache.containsKey(key)) { 140 iconOutlineCache.remove(key); 141 } 142 } 143 public Bitmap getOutline(Object key) { 144 return iconOutlineCache.get(key); 145 } 146 } 147 148 public interface PageSwitchListener { 149 void onPageSwitch(View newPage, int newPageIndex); 150 } 151 152 public interface PageMovingListener { 153 void onPageBeginMoving(); 154 void onPageEndMoving(); 155 } 156 157 public PagedView(Context context) { 158 this(context, null); 159 } 160 161 public PagedView(Context context, AttributeSet attrs) { 162 this(context, attrs, 0); 163 } 164 165 public PagedView(Context context, AttributeSet attrs, int defStyle) { 166 super(context, attrs, defStyle); 167 mChoiceMode = CHOICE_MODE_NONE; 168 169 setHapticFeedbackEnabled(false); 170 init(); 171 } 172 173 /** 174 * Initializes various states for this workspace. 175 */ 176 protected void init() { 177 mDirtyPageContent = new ArrayList<Boolean>(); 178 mDirtyPageContent.ensureCapacity(32); 179 mPageViewIconCache = new PagedViewIconCache(); 180 mScroller = new Scroller(getContext()); 181 mCurrentPage = 0; 182 183 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 184 mTouchSlop = configuration.getScaledTouchSlop(); 185 mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); 186 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 187 } 188 189 public void setPageSwitchListener(PageSwitchListener pageSwitchListener) { 190 mPageSwitchListener = pageSwitchListener; 191 if (mPageSwitchListener != null) { 192 mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage); 193 } 194 } 195 196 /** 197 * Returns the index of the currently displayed page. 198 * 199 * @return The index of the currently displayed page. 200 */ 201 int getCurrentPage() { 202 return mCurrentPage; 203 } 204 205 int getPageCount() { 206 return getChildCount(); 207 } 208 209 View getPageAt(int index) { 210 return getChildAt(index); 211 } 212 213 int getScrollWidth() { 214 return getWidth(); 215 } 216 217 /** 218 * Sets the current page. 219 */ 220 void setCurrentPage(int currentPage) { 221 if (!mScroller.isFinished()) mScroller.abortAnimation(); 222 if (getChildCount() == 0) return; 223 224 mCurrentPage = Math.max(0, Math.min(currentPage, getPageCount() - 1)); 225 scrollTo(getChildOffset(mCurrentPage) - getRelativeChildOffset(mCurrentPage), 0); 226 227 invalidate(); 228 notifyPageSwitchListener(); 229 } 230 231 protected void notifyPageSwitchListener() { 232 if (mPageSwitchListener != null) { 233 mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage); 234 } 235 } 236 237 // a method that subclasses can override to add behavior 238 protected void pageBeginMoving() { 239 } 240 241 // a method that subclasses can override to add behavior 242 protected void pageEndMoving() { 243 } 244 245 /** 246 * Registers the specified listener on each page contained in this workspace. 247 * 248 * @param l The listener used to respond to long clicks. 249 */ 250 @Override 251 public void setOnLongClickListener(OnLongClickListener l) { 252 mLongClickListener = l; 253 final int count = getPageCount(); 254 for (int i = 0; i < count; i++) { 255 getPageAt(i).setOnLongClickListener(l); 256 } 257 } 258 259 @Override 260 public void scrollTo(int x, int y) { 261 super.scrollTo(x, y); 262 mTouchX = x; 263 mSmoothingTime = System.nanoTime() / NANOTIME_DIV; 264 } 265 266 // we moved this functionality to a helper function so SmoothPagedView can reuse it 267 protected boolean computeScrollHelper() { 268 if (mScroller.computeScrollOffset()) { 269 mDirtyPageAlpha = true; 270 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 271 invalidate(); 272 return true; 273 } else if (mNextPage != INVALID_PAGE) { 274 mDirtyPageAlpha = true; 275 mCurrentPage = Math.max(0, Math.min(mNextPage, getPageCount() - 1)); 276 mNextPage = INVALID_PAGE; 277 notifyPageSwitchListener(); 278 pageEndMoving(); 279 return true; 280 } 281 return false; 282 } 283 284 @Override 285 public void computeScroll() { 286 computeScrollHelper(); 287 } 288 289 @Override 290 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 291 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 292 final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 293 if (widthMode != MeasureSpec.EXACTLY) { 294 throw new IllegalStateException("Workspace can only be used in EXACTLY mode."); 295 } 296 297 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 298 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 299 if (heightMode != MeasureSpec.EXACTLY) { 300 throw new IllegalStateException("Workspace can only be used in EXACTLY mode."); 301 } 302 303 // The children are given the same width and height as the workspace 304 // unless they were set to WRAP_CONTENT 305 final int childCount = getChildCount(); 306 for (int i = 0; i < childCount; i++) { 307 // disallowing padding in paged view (just pass 0) 308 final View child = getChildAt(i); 309 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 310 311 int childWidthMode; 312 if (lp.width == LayoutParams.WRAP_CONTENT) { 313 childWidthMode = MeasureSpec.AT_MOST; 314 } else { 315 childWidthMode = MeasureSpec.EXACTLY; 316 } 317 318 int childHeightMode; 319 if (lp.height == LayoutParams.WRAP_CONTENT) { 320 childHeightMode = MeasureSpec.AT_MOST; 321 } else { 322 childHeightMode = MeasureSpec.EXACTLY; 323 } 324 325 final int childWidthMeasureSpec = 326 MeasureSpec.makeMeasureSpec(widthSize, childWidthMode); 327 final int childHeightMeasureSpec = 328 MeasureSpec.makeMeasureSpec(heightSize, childHeightMode); 329 330 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 331 } 332 333 setMeasuredDimension(widthSize, heightSize); 334 } 335 336 @Override 337 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 338 if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { 339 setHorizontalScrollBarEnabled(false); 340 int newX = getChildOffset(mCurrentPage) - getRelativeChildOffset(mCurrentPage); 341 scrollTo(newX, 0); 342 mScroller.setFinalX(newX); 343 setHorizontalScrollBarEnabled(true); 344 mFirstLayout = false; 345 } 346 347 final int childCount = getChildCount(); 348 int childLeft = 0; 349 if (childCount > 0) { 350 childLeft = getRelativeChildOffset(0); 351 } 352 353 for (int i = 0; i < childCount; i++) { 354 final View child = getChildAt(i); 355 if (child.getVisibility() != View.GONE) { 356 final int childWidth = child.getMeasuredWidth(); 357 final int childHeight = (getMeasuredHeight() - child.getMeasuredHeight()) / 2; 358 child.layout(childLeft, childHeight, 359 childLeft + childWidth, childHeight + child.getMeasuredHeight()); 360 childLeft += childWidth; 361 } 362 } 363 } 364 365 protected void updateAdjacentPagesAlpha() { 366 if (mFadeInAdjacentScreens) { 367 if (mDirtyPageAlpha || (mTouchState == TOUCH_STATE_SCROLLING) || !mScroller.isFinished()) { 368 int halfScreenSize = getMeasuredWidth() / 2; 369 int screenCenter = mScrollX + halfScreenSize; 370 371 final int childCount = getChildCount(); 372 for (int i = 0; i < childCount; ++i) { 373 View layout = (View) getChildAt(i); 374 int childWidth = layout.getMeasuredWidth(); 375 int halfChildWidth = (childWidth / 2); 376 int childCenter = getChildOffset(i) + halfChildWidth; 377 378 int d = halfChildWidth; 379 int distanceFromScreenCenter = childCenter - screenCenter; 380 if (distanceFromScreenCenter > 0) { 381 if (i > 0) { 382 d += getChildAt(i - 1).getMeasuredWidth() / 2; 383 } 384 } else { 385 if (i < childCount - 1) { 386 d += getChildAt(i + 1).getMeasuredWidth() / 2; 387 } 388 } 389 390 float dimAlpha = (float) (Math.abs(distanceFromScreenCenter)) / d; 391 dimAlpha = Math.max(0.0f, Math.min(1.0f, (dimAlpha * dimAlpha))); 392 float alpha = 1.0f - dimAlpha; 393 394 if (Float.compare(alpha, layout.getAlpha()) != 0) { 395 layout.setAlpha(alpha); 396 } 397 } 398 mDirtyPageAlpha = false; 399 } 400 } 401 } 402 403 @Override 404 protected void dispatchDraw(Canvas canvas) { 405 updateAdjacentPagesAlpha(); 406 407 // Find out which screens are visible; as an optimization we only call draw on them 408 // As an optimization, this code assumes that all pages have the same width as the 0th 409 // page. 410 final int pageCount = getChildCount(); 411 if (pageCount > 0) { 412 final int pageWidth = getChildAt(0).getMeasuredWidth(); 413 final int screenWidth = getMeasuredWidth(); 414 int x = getRelativeChildOffset(0) + pageWidth; 415 int leftScreen = 0; 416 int rightScreen = 0; 417 while (x <= mScrollX) { 418 leftScreen++; 419 x += pageWidth; 420 // replace above line with this if you don't assume all pages have same width as 0th 421 // page: 422 // x += getChildAt(leftScreen).getMeasuredWidth(); 423 } 424 rightScreen = leftScreen; 425 while (x < mScrollX + screenWidth) { 426 rightScreen++; 427 x += pageWidth; 428 // replace above line with this if you don't assume all pages have same width as 0th 429 // page: 430 //if (rightScreen < pageCount) { 431 // x += getChildAt(rightScreen).getMeasuredWidth(); 432 //} 433 } 434 rightScreen = Math.min(getChildCount() - 1, rightScreen); 435 436 final long drawingTime = getDrawingTime(); 437 for (int i = leftScreen; i <= rightScreen; i++) { 438 drawChild(canvas, getChildAt(i), drawingTime); 439 } 440 } 441 } 442 443 @Override 444 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { 445 int page = indexOfChild(child); 446 if (page != mCurrentPage || !mScroller.isFinished()) { 447 snapToPage(page); 448 return true; 449 } 450 return false; 451 } 452 453 @Override 454 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 455 int focusablePage; 456 if (mNextPage != INVALID_PAGE) { 457 focusablePage = mNextPage; 458 } else { 459 focusablePage = mCurrentPage; 460 } 461 View v = getPageAt(focusablePage); 462 if (v != null) { 463 v.requestFocus(direction, previouslyFocusedRect); 464 } 465 return false; 466 } 467 468 @Override 469 public boolean dispatchUnhandledMove(View focused, int direction) { 470 if (direction == View.FOCUS_LEFT) { 471 if (getCurrentPage() > 0) { 472 snapToPage(getCurrentPage() - 1); 473 return true; 474 } 475 } else if (direction == View.FOCUS_RIGHT) { 476 if (getCurrentPage() < getPageCount() - 1) { 477 snapToPage(getCurrentPage() + 1); 478 return true; 479 } 480 } 481 return super.dispatchUnhandledMove(focused, direction); 482 } 483 484 @Override 485 public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { 486 if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) { 487 getPageAt(mCurrentPage).addFocusables(views, direction); 488 } 489 if (direction == View.FOCUS_LEFT) { 490 if (mCurrentPage > 0) { 491 getPageAt(mCurrentPage - 1).addFocusables(views, direction); 492 } 493 } else if (direction == View.FOCUS_RIGHT){ 494 if (mCurrentPage < getPageCount() - 1) { 495 getPageAt(mCurrentPage + 1).addFocusables(views, direction); 496 } 497 } 498 } 499 500 /** 501 * If one of our descendant views decides that it could be focused now, only 502 * pass that along if it's on the current page. 503 * 504 * This happens when live folders requery, and if they're off page, they 505 * end up calling requestFocus, which pulls it on page. 506 */ 507 @Override 508 public void focusableViewAvailable(View focused) { 509 View current = getPageAt(mCurrentPage); 510 View v = focused; 511 while (true) { 512 if (v == current) { 513 super.focusableViewAvailable(focused); 514 return; 515 } 516 if (v == this) { 517 return; 518 } 519 ViewParent parent = v.getParent(); 520 if (parent instanceof View) { 521 v = (View)v.getParent(); 522 } else { 523 return; 524 } 525 } 526 } 527 528 /** 529 * {@inheritDoc} 530 */ 531 @Override 532 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 533 if (disallowIntercept) { 534 // We need to make sure to cancel our long press if 535 // a scrollable widget takes over touch events 536 final View currentPage = getChildAt(mCurrentPage); 537 currentPage.cancelLongPress(); 538 } 539 super.requestDisallowInterceptTouchEvent(disallowIntercept); 540 } 541 542 @Override 543 public boolean onInterceptTouchEvent(MotionEvent ev) { 544 /* 545 * This method JUST determines whether we want to intercept the motion. 546 * If we return true, onTouchEvent will be called and we do the actual 547 * scrolling there. 548 */ 549 550 /* 551 * Shortcut the most recurring case: the user is in the dragging 552 * state and he is moving his finger. We want to intercept this 553 * motion. 554 */ 555 final int action = ev.getAction(); 556 if ((action == MotionEvent.ACTION_MOVE) && 557 (mTouchState == TOUCH_STATE_SCROLLING)) { 558 return true; 559 } 560 561 switch (action & MotionEvent.ACTION_MASK) { 562 case MotionEvent.ACTION_MOVE: { 563 /* 564 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 565 * whether the user has moved far enough from his original down touch. 566 */ 567 if (mActivePointerId != INVALID_POINTER) { 568 determineScrollingStart(ev); 569 break; 570 } 571 // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN 572 // event. in that case, treat the first occurence of a move event as a ACTION_DOWN 573 // i.e. fall through to the next case (don't break) 574 // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events 575 // while it's small- this was causing a crash before we checked for INVALID_POINTER) 576 } 577 578 case MotionEvent.ACTION_DOWN: { 579 final float x = ev.getX(); 580 final float y = ev.getY(); 581 // Remember location of down touch 582 mDownMotionX = x; 583 mLastMotionX = x; 584 mLastMotionY = y; 585 mActivePointerId = ev.getPointerId(0); 586 mAllowLongPress = true; 587 588 /* 589 * If being flinged and user touches the screen, initiate drag; 590 * otherwise don't. mScroller.isFinished should be false when 591 * being flinged. 592 */ 593 final int xDist = (mScroller.getFinalX() - mScroller.getCurrX()); 594 final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop); 595 if (finishedScrolling) { 596 mTouchState = TOUCH_STATE_REST; 597 mScroller.abortAnimation(); 598 } else { 599 mTouchState = TOUCH_STATE_SCROLLING; 600 } 601 602 // check if this can be the beginning of a tap on the side of the pages 603 // to scroll the current page 604 if ((mTouchState != TOUCH_STATE_PREV_PAGE) && 605 (mTouchState != TOUCH_STATE_NEXT_PAGE)) { 606 if (getChildCount() > 0) { 607 int width = getMeasuredWidth(); 608 int offset = getRelativeChildOffset(mCurrentPage); 609 if (x < offset) { 610 mTouchState = TOUCH_STATE_PREV_PAGE; 611 } else if (x > (width - offset)) { 612 mTouchState = TOUCH_STATE_NEXT_PAGE; 613 } 614 } 615 } 616 break; 617 } 618 619 case MotionEvent.ACTION_CANCEL: 620 case MotionEvent.ACTION_UP: 621 // Release the drag 622 pageEndMoving(); 623 mTouchState = TOUCH_STATE_REST; 624 mAllowLongPress = false; 625 mActivePointerId = INVALID_POINTER; 626 627 break; 628 629 case MotionEvent.ACTION_POINTER_UP: 630 onSecondaryPointerUp(ev); 631 break; 632 } 633 634 /* 635 * The only time we want to intercept motion events is if we are in the 636 * drag mode. 637 */ 638 return mTouchState != TOUCH_STATE_REST; 639 } 640 641 protected void animateClickFeedback(View v, final Runnable r) { 642 // animate the view slightly to show click feedback running some logic after it is "pressed" 643 Animation anim = AnimationUtils.loadAnimation(getContext(), 644 R.anim.paged_view_click_feedback); 645 anim.setAnimationListener(new AnimationListener() { 646 @Override 647 public void onAnimationStart(Animation animation) {} 648 @Override 649 public void onAnimationRepeat(Animation animation) { 650 r.run(); 651 } 652 @Override 653 public void onAnimationEnd(Animation animation) {} 654 }); 655 v.startAnimation(anim); 656 } 657 658 /* 659 * Determines if we should change the touch state to start scrolling after the 660 * user moves their touch point too far. 661 */ 662 private void determineScrollingStart(MotionEvent ev) { 663 /* 664 * Locally do absolute value. mLastMotionX is set to the y value 665 * of the down event. 666 */ 667 final int pointerIndex = ev.findPointerIndex(mActivePointerId); 668 final float x = ev.getX(pointerIndex); 669 final float y = ev.getY(pointerIndex); 670 final int xDiff = (int) Math.abs(x - mLastMotionX); 671 final int yDiff = (int) Math.abs(y - mLastMotionY); 672 673 final int touchSlop = mTouchSlop; 674 boolean xPaged = xDiff > mPagingTouchSlop; 675 boolean xMoved = xDiff > touchSlop; 676 boolean yMoved = yDiff > touchSlop; 677 678 if (xMoved || yMoved) { 679 if (mUsePagingTouchSlop ? xPaged : xMoved) { 680 // Scroll if the user moved far enough along the X axis 681 mTouchState = TOUCH_STATE_SCROLLING; 682 mLastMotionX = x; 683 mTouchX = mScrollX; 684 mSmoothingTime = System.nanoTime() / NANOTIME_DIV; 685 pageBeginMoving(); 686 } 687 // Either way, cancel any pending longpress 688 if (mAllowLongPress) { 689 mAllowLongPress = false; 690 // Try canceling the long press. It could also have been scheduled 691 // by a distant descendant, so use the mAllowLongPress flag to block 692 // everything 693 final View currentPage = getPageAt(mCurrentPage); 694 currentPage.cancelLongPress(); 695 } 696 } 697 } 698 699 @Override 700 public boolean onTouchEvent(MotionEvent ev) { 701 if (mVelocityTracker == null) { 702 mVelocityTracker = VelocityTracker.obtain(); 703 } 704 mVelocityTracker.addMovement(ev); 705 706 final int action = ev.getAction(); 707 708 switch (action & MotionEvent.ACTION_MASK) { 709 case MotionEvent.ACTION_DOWN: 710 /* 711 * If being flinged and user touches, stop the fling. isFinished 712 * will be false if being flinged. 713 */ 714 if (!mScroller.isFinished()) { 715 mScroller.abortAnimation(); 716 } 717 718 // Remember where the motion event started 719 mDownMotionX = mLastMotionX = ev.getX(); 720 mActivePointerId = ev.getPointerId(0); 721 if (mTouchState == TOUCH_STATE_SCROLLING) { 722 pageBeginMoving(); 723 } 724 break; 725 726 case MotionEvent.ACTION_MOVE: 727 if (mTouchState == TOUCH_STATE_SCROLLING) { 728 // Scroll to follow the motion event 729 final int pointerIndex = ev.findPointerIndex(mActivePointerId); 730 final float x = ev.getX(pointerIndex); 731 final int deltaX = (int) (mLastMotionX - x); 732 mLastMotionX = x; 733 734 int sx = getScrollX(); 735 if (deltaX < 0) { 736 if (sx > 0) { 737 mTouchX += Math.max(-mTouchX, deltaX); 738 mSmoothingTime = System.nanoTime() / NANOTIME_DIV; 739 if (!mDeferScrollUpdate) { 740 scrollBy(Math.max(-sx, deltaX), 0); 741 } else { 742 // This will trigger a call to computeScroll() on next drawChild() call 743 invalidate(); 744 } 745 } 746 } else if (deltaX > 0) { 747 final int lastChildIndex = getChildCount() - 1; 748 final int availableToScroll = getChildOffset(lastChildIndex) - 749 getRelativeChildOffset(lastChildIndex) - sx; 750 if (availableToScroll > 0) { 751 mTouchX += Math.min(availableToScroll, deltaX); 752 mSmoothingTime = System.nanoTime() / NANOTIME_DIV; 753 if (!mDeferScrollUpdate) { 754 scrollBy(Math.min(availableToScroll, deltaX), 0); 755 } else { 756 // This will trigger a call to computeScroll() on next drawChild() call 757 invalidate(); 758 } 759 } 760 } else { 761 awakenScrollBars(); 762 } 763 } else if ((mTouchState == TOUCH_STATE_PREV_PAGE) || 764 (mTouchState == TOUCH_STATE_NEXT_PAGE)) { 765 determineScrollingStart(ev); 766 } 767 break; 768 769 case MotionEvent.ACTION_UP: 770 if (mTouchState == TOUCH_STATE_SCROLLING) { 771 final int activePointerId = mActivePointerId; 772 final int pointerIndex = ev.findPointerIndex(activePointerId); 773 final float x = ev.getX(pointerIndex); 774 final VelocityTracker velocityTracker = mVelocityTracker; 775 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 776 int velocityX = (int) velocityTracker.getXVelocity(activePointerId); 777 boolean isfling = Math.abs(mDownMotionX - x) > MIN_LENGTH_FOR_FLING; 778 779 final int snapVelocity = mSnapVelocity; 780 if (isfling && velocityX > snapVelocity && mCurrentPage > 0) { 781 snapToPageWithVelocity(mCurrentPage - 1, velocityX); 782 } else if (isfling && velocityX < -snapVelocity && 783 mCurrentPage < getChildCount() - 1) { 784 snapToPageWithVelocity(mCurrentPage + 1, velocityX); 785 } else { 786 snapToDestination(); 787 } 788 789 if (mVelocityTracker != null) { 790 mVelocityTracker.recycle(); 791 mVelocityTracker = null; 792 } 793 } else if (mTouchState == TOUCH_STATE_PREV_PAGE) { 794 // at this point we have not moved beyond the touch slop 795 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so 796 // we can just page 797 int nextPage = Math.max(0, mCurrentPage - 1); 798 if (nextPage != mCurrentPage) { 799 snapToPage(nextPage); 800 } else { 801 snapToDestination(); 802 } 803 } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) { 804 // at this point we have not moved beyond the touch slop 805 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so 806 // we can just page 807 int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1); 808 if (nextPage != mCurrentPage) { 809 snapToPage(nextPage); 810 } else { 811 snapToDestination(); 812 } 813 } 814 mTouchState = TOUCH_STATE_REST; 815 mActivePointerId = INVALID_POINTER; 816 break; 817 818 case MotionEvent.ACTION_CANCEL: 819 mTouchState = TOUCH_STATE_REST; 820 mActivePointerId = INVALID_POINTER; 821 break; 822 823 case MotionEvent.ACTION_POINTER_UP: 824 onSecondaryPointerUp(ev); 825 break; 826 } 827 828 return true; 829 } 830 831 private void onSecondaryPointerUp(MotionEvent ev) { 832 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 833 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 834 final int pointerId = ev.getPointerId(pointerIndex); 835 if (pointerId == mActivePointerId) { 836 // This was our active pointer going up. Choose a new 837 // active pointer and adjust accordingly. 838 // TODO: Make this decision more intelligent. 839 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 840 mLastMotionX = mDownMotionX = ev.getX(newPointerIndex); 841 mLastMotionY = ev.getY(newPointerIndex); 842 mActivePointerId = ev.getPointerId(newPointerIndex); 843 if (mVelocityTracker != null) { 844 mVelocityTracker.clear(); 845 } 846 } 847 } 848 849 @Override 850 public void requestChildFocus(View child, View focused) { 851 super.requestChildFocus(child, focused); 852 int page = indexOfChild(child); 853 if (page >= 0 && !isInTouchMode()) { 854 snapToPage(page); 855 } 856 } 857 858 protected int getChildIndexForRelativeOffset(int relativeOffset) { 859 final int childCount = getChildCount(); 860 int left = getRelativeChildOffset(0); 861 for (int i = 0; i < childCount; ++i) { 862 final int right = (left + getChildAt(i).getMeasuredWidth()); 863 if (left <= relativeOffset && relativeOffset <= right) { 864 return i; 865 } 866 left = right; 867 } 868 return -1; 869 } 870 871 protected int getRelativeChildOffset(int index) { 872 return (getMeasuredWidth() - getChildAt(index).getMeasuredWidth()) / 2; 873 } 874 875 protected int getChildOffset(int index) { 876 if (getChildCount() == 0) 877 return 0; 878 879 int offset = getRelativeChildOffset(0); 880 for (int i = 0; i < index; ++i) { 881 offset += getChildAt(i).getMeasuredWidth(); 882 } 883 return offset; 884 } 885 886 int getPageNearestToCenterOfScreen() { 887 int minDistanceFromScreenCenter = getMeasuredWidth(); 888 int minDistanceFromScreenCenterIndex = -1; 889 int screenCenter = mScrollX + (getMeasuredWidth() / 2); 890 final int childCount = getChildCount(); 891 for (int i = 0; i < childCount; ++i) { 892 View layout = (View) getChildAt(i); 893 int childWidth = layout.getMeasuredWidth(); 894 int halfChildWidth = (childWidth / 2); 895 int childCenter = getChildOffset(i) + halfChildWidth; 896 int distanceFromScreenCenter = Math.abs(childCenter - screenCenter); 897 if (distanceFromScreenCenter < minDistanceFromScreenCenter) { 898 minDistanceFromScreenCenter = distanceFromScreenCenter; 899 minDistanceFromScreenCenterIndex = i; 900 } 901 } 902 return minDistanceFromScreenCenterIndex; 903 } 904 905 protected void snapToDestination() { 906 snapToPage(getPageNearestToCenterOfScreen(), PAGE_SNAP_ANIMATION_DURATION); 907 } 908 909 protected void snapToPageWithVelocity(int whichPage, int velocity) { 910 // We ignore velocity in this implementation, but children (e.g. SmoothPagedView) 911 // can use it 912 snapToPage(whichPage); 913 } 914 915 protected void snapToPage(int whichPage) { 916 snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); 917 } 918 919 protected void snapToPage(int whichPage, int duration) { 920 whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1)); 921 922 int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); 923 final int sX = getScrollX(); 924 final int delta = newX - sX; 925 snapToPage(whichPage, delta, duration); 926 } 927 928 protected void snapToPage(int whichPage, int delta, int duration) { 929 mNextPage = whichPage; 930 931 View focusedChild = getFocusedChild(); 932 if (focusedChild != null && whichPage != mCurrentPage && 933 focusedChild == getChildAt(mCurrentPage)) { 934 focusedChild.clearFocus(); 935 } 936 937 pageBeginMoving(); 938 awakenScrollBars(duration); 939 if (duration == 0) { 940 duration = Math.abs(delta); 941 } 942 943 if (!mScroller.isFinished()) mScroller.abortAnimation(); 944 mScroller.startScroll(getScrollX(), 0, delta, 0, duration); 945 946 // only load some associated pages 947 loadAssociatedPages(mNextPage); 948 notifyPageSwitchListener(); 949 invalidate(); 950 } 951 952 @Override 953 protected Parcelable onSaveInstanceState() { 954 final SavedState state = new SavedState(super.onSaveInstanceState()); 955 state.currentPage = mCurrentPage; 956 return state; 957 } 958 959 @Override 960 protected void onRestoreInstanceState(Parcelable state) { 961 SavedState savedState = (SavedState) state; 962 super.onRestoreInstanceState(savedState.getSuperState()); 963 if (savedState.currentPage != -1) { 964 mCurrentPage = savedState.currentPage; 965 } 966 } 967 968 public void scrollLeft() { 969 if (mScroller.isFinished()) { 970 if (mCurrentPage > 0) snapToPage(mCurrentPage - 1); 971 } else { 972 if (mNextPage > 0) snapToPage(mNextPage - 1); 973 } 974 } 975 976 public void scrollRight() { 977 if (mScroller.isFinished()) { 978 if (mCurrentPage < getChildCount() -1) snapToPage(mCurrentPage + 1); 979 } else { 980 if (mNextPage < getChildCount() -1) snapToPage(mNextPage + 1); 981 } 982 } 983 984 public int getPageForView(View v) { 985 int result = -1; 986 if (v != null) { 987 ViewParent vp = v.getParent(); 988 int count = getChildCount(); 989 for (int i = 0; i < count; i++) { 990 if (vp == getChildAt(i)) { 991 return i; 992 } 993 } 994 } 995 return result; 996 } 997 998 /** 999 * @return True is long presses are still allowed for the current touch 1000 */ 1001 public boolean allowLongPress() { 1002 return mAllowLongPress; 1003 } 1004 1005 /** 1006 * Set true to allow long-press events to be triggered, usually checked by 1007 * {@link Launcher} to accept or block dpad-initiated long-presses. 1008 */ 1009 public void setAllowLongPress(boolean allowLongPress) { 1010 mAllowLongPress = allowLongPress; 1011 } 1012 1013 public static class SavedState extends BaseSavedState { 1014 int currentPage = -1; 1015 1016 SavedState(Parcelable superState) { 1017 super(superState); 1018 } 1019 1020 private SavedState(Parcel in) { 1021 super(in); 1022 currentPage = in.readInt(); 1023 } 1024 1025 @Override 1026 public void writeToParcel(Parcel out, int flags) { 1027 super.writeToParcel(out, flags); 1028 out.writeInt(currentPage); 1029 } 1030 1031 public static final Parcelable.Creator<SavedState> CREATOR = 1032 new Parcelable.Creator<SavedState>() { 1033 public SavedState createFromParcel(Parcel in) { 1034 return new SavedState(in); 1035 } 1036 1037 public SavedState[] newArray(int size) { 1038 return new SavedState[size]; 1039 } 1040 }; 1041 } 1042 1043 public void loadAssociatedPages(int page) { 1044 if (mContentIsRefreshable) { 1045 final int count = getChildCount(); 1046 if (page < count) { 1047 int lowerPageBound = getAssociatedLowerPageBound(page); 1048 int upperPageBound = getAssociatedUpperPageBound(page); 1049 for (int i = 0; i < count; ++i) { 1050 final ViewGroup layout = (ViewGroup) getChildAt(i); 1051 final int childCount = layout.getChildCount(); 1052 if (lowerPageBound <= i && i <= upperPageBound) { 1053 if (mDirtyPageContent.get(i)) { 1054 syncPageItems(i); 1055 mDirtyPageContent.set(i, false); 1056 } 1057 } else { 1058 if (childCount > 0) { 1059 layout.removeAllViews(); 1060 } 1061 mDirtyPageContent.set(i, true); 1062 } 1063 } 1064 } 1065 } 1066 } 1067 1068 protected int getAssociatedLowerPageBound(int page) { 1069 return Math.max(0, page - 1); 1070 } 1071 protected int getAssociatedUpperPageBound(int page) { 1072 final int count = getChildCount(); 1073 return Math.min(page + 1, count - 1); 1074 } 1075 1076 protected void startChoiceMode(int mode, ActionMode.Callback callback) { 1077 if (isChoiceMode(CHOICE_MODE_NONE)) { 1078 mChoiceMode = mode; 1079 mActionMode = startActionMode(callback); 1080 } 1081 } 1082 1083 public void endChoiceMode() { 1084 if (!isChoiceMode(CHOICE_MODE_NONE)) { 1085 mChoiceMode = CHOICE_MODE_NONE; 1086 resetCheckedGrandchildren(); 1087 mActionMode.finish(); 1088 mActionMode = null; 1089 } 1090 } 1091 1092 protected boolean isChoiceMode(int mode) { 1093 return mChoiceMode == mode; 1094 } 1095 1096 protected ArrayList<Checkable> getCheckedGrandchildren() { 1097 ArrayList<Checkable> checked = new ArrayList<Checkable>(); 1098 final int childCount = getChildCount(); 1099 for (int i = 0; i < childCount; ++i) { 1100 final PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i); 1101 final int grandChildCount = layout.getChildCount(); 1102 for (int j = 0; j < grandChildCount; ++j) { 1103 final View v = layout.getChildAt(j); 1104 if (v instanceof Checkable && ((Checkable) v).isChecked()) { 1105 checked.add((Checkable) v); 1106 } 1107 } 1108 } 1109 return checked; 1110 } 1111 1112 /** 1113 * If in CHOICE_MODE_SINGLE and an item is checked, returns that item. 1114 * Otherwise, returns null. 1115 */ 1116 protected Checkable getSingleCheckedGrandchild() { 1117 if (mChoiceMode == CHOICE_MODE_SINGLE) { 1118 final int childCount = getChildCount(); 1119 for (int i = 0; i < childCount; ++i) { 1120 final PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i); 1121 final int grandChildCount = layout.getChildCount(); 1122 for (int j = 0; j < grandChildCount; ++j) { 1123 final View v = layout.getChildAt(j); 1124 if (v instanceof Checkable && ((Checkable) v).isChecked()) { 1125 return (Checkable) v; 1126 } 1127 } 1128 } 1129 } 1130 return null; 1131 } 1132 1133 public Object getChosenItem() { 1134 View checkedView = (View) getSingleCheckedGrandchild(); 1135 if (checkedView != null) { 1136 return checkedView.getTag(); 1137 } 1138 return null; 1139 } 1140 1141 protected void resetCheckedGrandchildren() { 1142 // loop through children, and set all of their children to _not_ be checked 1143 final ArrayList<Checkable> checked = getCheckedGrandchildren(); 1144 for (int i = 0; i < checked.size(); ++i) { 1145 final Checkable c = checked.get(i); 1146 c.setChecked(false); 1147 } 1148 } 1149 1150 /** 1151 * This method is called ONLY to synchronize the number of pages that the paged view has. 1152 * To actually fill the pages with information, implement syncPageItems() below. It is 1153 * guaranteed that syncPageItems() will be called for a particular page before it is shown, 1154 * and therefore, individual page items do not need to be updated in this method. 1155 */ 1156 public abstract void syncPages(); 1157 1158 /** 1159 * This method is called to synchronize the items that are on a particular page. If views on 1160 * the page can be reused, then they should be updated within this method. 1161 */ 1162 public abstract void syncPageItems(int page); 1163 1164 public void invalidatePageData() { 1165 if (mContentIsRefreshable) { 1166 // Update all the pages 1167 syncPages(); 1168 1169 // Mark each of the pages as dirty 1170 final int count = getChildCount(); 1171 mDirtyPageContent.clear(); 1172 for (int i = 0; i < count; ++i) { 1173 mDirtyPageContent.add(true); 1174 } 1175 1176 // Load any pages that are necessary for the current window of views 1177 loadAssociatedPages(mCurrentPage); 1178 mDirtyPageAlpha = true; 1179 requestLayout(); 1180 } 1181 } 1182} 1183