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