PagedView.java revision 0ff7f010f8bfd011f0915031b02739ae3bee401e
1/* 2 * Copyright (C) 2012 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.internal.policy.impl.keyguard; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.animation.PropertyValuesHolder; 23import android.animation.ValueAnimator; 24import android.content.Context; 25import android.content.res.TypedArray; 26import android.graphics.Canvas; 27import android.graphics.Rect; 28import android.os.Bundle; 29import android.os.Parcel; 30import android.os.Parcelable; 31import android.util.AttributeSet; 32import android.util.Log; 33import android.view.InputDevice; 34import android.view.KeyEvent; 35import android.view.MotionEvent; 36import android.view.VelocityTracker; 37import android.view.View; 38import android.view.ViewConfiguration; 39import android.view.ViewGroup; 40import android.view.ViewParent; 41import android.view.accessibility.AccessibilityEvent; 42import android.view.accessibility.AccessibilityManager; 43import android.view.accessibility.AccessibilityNodeInfo; 44import android.view.animation.Interpolator; 45import android.widget.Scroller; 46 47import com.android.internal.R; 48 49import java.util.ArrayList; 50 51/** 52 * An abstraction of the original Workspace which supports browsing through a 53 * sequential list of "pages" 54 */ 55public class PagedView extends ViewGroup implements ViewGroup.OnHierarchyChangeListener { 56 private static final String TAG = "WidgetPagedView"; 57 private static final boolean DEBUG = false; 58 protected static final int INVALID_PAGE = -1; 59 60 // the min drag distance for a fling to register, to prevent random page shifts 61 private static final int MIN_LENGTH_FOR_FLING = 25; 62 63 protected static final int PAGE_SNAP_ANIMATION_DURATION = 550; 64 protected static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950; 65 protected static final float NANOTIME_DIV = 1000000000.0f; 66 67 private static final float OVERSCROLL_ACCELERATE_FACTOR = 2; 68 private static final float OVERSCROLL_DAMP_FACTOR = 0.14f; 69 70 private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f; 71 // The page is moved more than halfway, automatically move to the next page on touch up. 72 private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f; 73 74 // The following constants need to be scaled based on density. The scaled versions will be 75 // assigned to the corresponding member variables below. 76 private static final int FLING_THRESHOLD_VELOCITY = 500; 77 private static final int MIN_SNAP_VELOCITY = 1500; 78 private static final int MIN_FLING_VELOCITY = 250; 79 80 static final int AUTOMATIC_PAGE_SPACING = -1; 81 82 protected int mFlingThresholdVelocity; 83 protected int mMinFlingVelocity; 84 protected int mMinSnapVelocity; 85 86 protected float mDensity; 87 protected float mSmoothingTime; 88 protected float mTouchX; 89 90 protected boolean mFirstLayout = true; 91 92 protected int mCurrentPage; 93 protected int mChildCountOnLastMeasure; 94 95 protected int mNextPage = INVALID_PAGE; 96 protected int mMaxScrollX; 97 protected Scroller mScroller; 98 private VelocityTracker mVelocityTracker; 99 100 private float mDownMotionX; 101 protected float mLastMotionX; 102 protected float mLastMotionXRemainder; 103 protected float mLastMotionY; 104 protected float mTotalMotionX; 105 private int mLastScreenCenter = -1; 106 private int[] mChildOffsets; 107 private int[] mChildRelativeOffsets; 108 private int[] mChildOffsetsWithLayoutScale; 109 110 protected final static int TOUCH_STATE_REST = 0; 111 protected final static int TOUCH_STATE_SCROLLING = 1; 112 protected final static int TOUCH_STATE_PREV_PAGE = 2; 113 protected final static int TOUCH_STATE_NEXT_PAGE = 3; 114 protected final static float ALPHA_QUANTIZE_LEVEL = 0.0001f; 115 116 protected int mTouchState = TOUCH_STATE_REST; 117 protected boolean mForceScreenScrolled = false; 118 119 protected OnLongClickListener mLongClickListener; 120 121 protected boolean mAllowLongPress = true; 122 123 protected int mTouchSlop; 124 private int mPagingTouchSlop; 125 private int mMaximumVelocity; 126 private int mMinimumWidth; 127 protected int mPageSpacing; 128 protected int mPageLayoutPaddingTop; 129 protected int mPageLayoutPaddingBottom; 130 protected int mPageLayoutPaddingLeft; 131 protected int mPageLayoutPaddingRight; 132 protected int mPageLayoutWidthGap; 133 protected int mPageLayoutHeightGap; 134 protected int mCellCountX = 0; 135 protected int mCellCountY = 0; 136 protected boolean mCenterPagesVertically; 137 protected boolean mAllowOverScroll = true; 138 protected int mUnboundedScrollX; 139 protected int[] mTempVisiblePagesRange = new int[2]; 140 protected boolean mForceDrawAllChildrenNextFrame; 141 142 // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. Otherwise 143 // it is equal to the scaled overscroll position. We use a separate value so as to prevent 144 // the screens from continuing to translate beyond the normal bounds. 145 protected int mOverScrollX; 146 147 // parameter that adjusts the layout to be optimized for pages with that scale factor 148 protected float mLayoutScale = 1.0f; 149 150 protected static final int INVALID_POINTER = -1; 151 152 protected int mActivePointerId = INVALID_POINTER; 153 154 private PageSwitchListener mPageSwitchListener; 155 156 protected ArrayList<Boolean> mDirtyPageContent; 157 158 // If true, syncPages and syncPageItems will be called to refresh pages 159 protected boolean mContentIsRefreshable = true; 160 161 // If true, modify alpha of neighboring pages as user scrolls left/right 162 protected boolean mFadeInAdjacentScreens = true; 163 164 // It true, use a different slop parameter (pagingTouchSlop = 2 * touchSlop) for deciding 165 // to switch to a new page 166 protected boolean mUsePagingTouchSlop = true; 167 168 // If true, the subclass should directly update scrollX itself in its computeScroll method 169 // (SmoothPagedView does this) 170 protected boolean mDeferScrollUpdate = false; 171 172 protected boolean mIsPageMoving = false; 173 174 // All syncs and layout passes are deferred until data is ready. 175 protected boolean mIsDataReady = true; 176 177 // Scrolling indicator 178 private ValueAnimator mScrollIndicatorAnimator; 179 private View mScrollIndicator; 180 private int mScrollIndicatorPaddingLeft; 181 private int mScrollIndicatorPaddingRight; 182 private boolean mShouldShowScrollIndicator = false; 183 private boolean mShouldShowScrollIndicatorImmediately = false; 184 protected static final int sScrollIndicatorFadeInDuration = 150; 185 protected static final int sScrollIndicatorFadeOutDuration = 650; 186 protected static final int sScrollIndicatorFlashDuration = 650; 187 188 public interface PageSwitchListener { 189 void onPageSwitch(View newPage, int newPageIndex); 190 } 191 192 public PagedView(Context context) { 193 this(context, null); 194 } 195 196 public PagedView(Context context, AttributeSet attrs) { 197 this(context, attrs, 0); 198 } 199 200 public PagedView(Context context, AttributeSet attrs, int defStyle) { 201 super(context, attrs, defStyle); 202 TypedArray a = context.obtainStyledAttributes(attrs, 203 R.styleable.PagedView, defStyle, 0); 204 setPageSpacing(a.getDimensionPixelSize(R.styleable.PagedView_pageSpacing, 0)); 205 mPageLayoutPaddingTop = a.getDimensionPixelSize( 206 R.styleable.PagedView_pageLayoutPaddingTop, 0); 207 mPageLayoutPaddingBottom = a.getDimensionPixelSize( 208 R.styleable.PagedView_pageLayoutPaddingBottom, 0); 209 mPageLayoutPaddingLeft = a.getDimensionPixelSize( 210 R.styleable.PagedView_pageLayoutPaddingLeft, 0); 211 mPageLayoutPaddingRight = a.getDimensionPixelSize( 212 R.styleable.PagedView_pageLayoutPaddingRight, 0); 213 mPageLayoutWidthGap = a.getDimensionPixelSize( 214 R.styleable.PagedView_pageLayoutWidthGap, 0); 215 mPageLayoutHeightGap = a.getDimensionPixelSize( 216 R.styleable.PagedView_pageLayoutHeightGap, 0); 217 mScrollIndicatorPaddingLeft = 218 a.getDimensionPixelSize(R.styleable.PagedView_scrollIndicatorPaddingLeft, 0); 219 mScrollIndicatorPaddingRight = 220 a.getDimensionPixelSize(R.styleable.PagedView_scrollIndicatorPaddingRight, 0); 221 a.recycle(); 222 223 setHapticFeedbackEnabled(false); 224 init(); 225 } 226 227 /** 228 * Initializes various states for this workspace. 229 */ 230 protected void init() { 231 mDirtyPageContent = new ArrayList<Boolean>(); 232 mDirtyPageContent.ensureCapacity(32); 233 mScroller = new Scroller(getContext(), new ScrollInterpolator()); 234 mCurrentPage = 0; 235 mCenterPagesVertically = true; 236 237 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 238 mTouchSlop = configuration.getScaledTouchSlop(); 239 mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); 240 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 241 mDensity = getResources().getDisplayMetrics().density; 242 243 mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); 244 mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * mDensity); 245 mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * mDensity); 246 setOnHierarchyChangeListener(this); 247 } 248 249 public void setPageSwitchListener(PageSwitchListener pageSwitchListener) { 250 mPageSwitchListener = pageSwitchListener; 251 if (mPageSwitchListener != null) { 252 mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage); 253 } 254 } 255 256 /** 257 * Called by subclasses to mark that data is ready, and that we can begin loading and laying 258 * out pages. 259 */ 260 protected void setDataIsReady() { 261 mIsDataReady = true; 262 } 263 264 protected boolean isDataReady() { 265 return mIsDataReady; 266 } 267 268 /** 269 * Returns the index of the currently displayed page. 270 * 271 * @return The index of the currently displayed page. 272 */ 273 int getCurrentPage() { 274 return mCurrentPage; 275 } 276 277 int getNextPage() { 278 return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; 279 } 280 281 int getPageCount() { 282 return getChildCount(); 283 } 284 285 View getPageAt(int index) { 286 return getChildAt(index); 287 } 288 289 protected int indexToPage(int index) { 290 return index; 291 } 292 293 /** 294 * Updates the scroll of the current page immediately to its final scroll position. We use this 295 * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of 296 * the previous tab page. 297 */ 298 protected void updateCurrentPageScroll() { 299 int offset = getChildOffset(mCurrentPage); 300 int relOffset = getRelativeChildOffset(mCurrentPage); 301 int newX = offset - relOffset; 302 scrollTo(newX, 0); 303 mScroller.setFinalX(newX); 304 mScroller.forceFinished(true); 305 } 306 307 /** 308 * Sets the current page. 309 */ 310 void setCurrentPage(int currentPage) { 311 if (!mScroller.isFinished()) { 312 mScroller.abortAnimation(); 313 } 314 // don't introduce any checks like mCurrentPage == currentPage here-- if we change the 315 // the default 316 if (getChildCount() == 0) { 317 return; 318 } 319 320 mCurrentPage = Math.max(0, Math.min(currentPage, getPageCount() - 1)); 321 updateCurrentPageScroll(); 322 updateScrollingIndicator(); 323 notifyPageSwitchListener(); 324 invalidate(); 325 } 326 327 protected void notifyPageSwitchListener() { 328 if (mPageSwitchListener != null) { 329 mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage); 330 } 331 } 332 333 protected void pageBeginMoving() { 334 if (!mIsPageMoving) { 335 mIsPageMoving = true; 336 onPageBeginMoving(); 337 } 338 } 339 340 protected void pageEndMoving() { 341 if (mIsPageMoving) { 342 mIsPageMoving = false; 343 onPageEndMoving(); 344 } 345 } 346 347 protected boolean isPageMoving() { 348 return mIsPageMoving; 349 } 350 351 // a method that subclasses can override to add behavior 352 protected void onPageBeginMoving() { 353 } 354 355 // a method that subclasses can override to add behavior 356 protected void onPageEndMoving() { 357 } 358 359 /** 360 * Registers the specified listener on each page contained in this workspace. 361 * 362 * @param l The listener used to respond to long clicks. 363 */ 364 @Override 365 public void setOnLongClickListener(OnLongClickListener l) { 366 mLongClickListener = l; 367 final int count = getPageCount(); 368 for (int i = 0; i < count; i++) { 369 getPageAt(i).setOnLongClickListener(l); 370 } 371 } 372 373 @Override 374 public void scrollBy(int x, int y) { 375 scrollTo(mUnboundedScrollX + x, getScrollY() + y); 376 } 377 378 @Override 379 public void scrollTo(int x, int y) { 380 mUnboundedScrollX = x; 381 382 if (x < 0) { 383 super.scrollTo(0, y); 384 if (mAllowOverScroll) { 385 overScroll(x); 386 } 387 } else if (x > mMaxScrollX) { 388 super.scrollTo(mMaxScrollX, y); 389 if (mAllowOverScroll) { 390 overScroll(x - mMaxScrollX); 391 } 392 } else { 393 mOverScrollX = x; 394 super.scrollTo(x, y); 395 } 396 397 mTouchX = x; 398 mSmoothingTime = System.nanoTime() / NANOTIME_DIV; 399 } 400 401 // we moved this functionality to a helper function so SmoothPagedView can reuse it 402 protected boolean computeScrollHelper() { 403 if (mScroller.computeScrollOffset()) { 404 // Don't bother scrolling if the page does not need to be moved 405 if (getScrollX() != mScroller.getCurrX() 406 || getScrollY() != mScroller.getCurrY() 407 || mOverScrollX != mScroller.getCurrX()) { 408 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 409 } 410 invalidate(); 411 return true; 412 } else if (mNextPage != INVALID_PAGE) { 413 mCurrentPage = Math.max(0, Math.min(mNextPage, getPageCount() - 1)); 414 mNextPage = INVALID_PAGE; 415 notifyPageSwitchListener(); 416 417 // We don't want to trigger a page end moving unless the page has settled 418 // and the user has stopped scrolling 419 if (mTouchState == TOUCH_STATE_REST) { 420 pageEndMoving(); 421 } 422 423 // Notify the user when the page changes 424 AccessibilityManager accessibilityManager = (AccessibilityManager) 425 getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 426 if (accessibilityManager.isEnabled()) { 427 AccessibilityEvent ev = 428 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED); 429 ev.getText().add(getCurrentPageDescription()); 430 sendAccessibilityEventUnchecked(ev); 431 } 432 return true; 433 } 434 return false; 435 } 436 437 public String getCurrentPageDescription() { 438 return ""; 439 } 440 441 @Override 442 public void computeScroll() { 443 computeScrollHelper(); 444 } 445 446 @Override 447 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 448 if (!mIsDataReady || getChildCount() == 0) { 449 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 450 return; 451 } 452 453 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 454 final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 455 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 456 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 457 458 if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) { 459 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 460 return; 461 } 462 463 // Return early if we aren't given a proper dimension 464 if (widthSize <= 0 || heightSize <= 0) { 465 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 466 return; 467 } 468 469 /* Allow the height to be set as WRAP_CONTENT. This allows the particular case 470 * of the All apps view on XLarge displays to not take up more space then it needs. Width 471 * is still not allowed to be set as WRAP_CONTENT since many parts of the code expect 472 * each page to have the same width. 473 */ 474 final int verticalPadding = getPaddingTop() + getPaddingBottom(); 475 final int horizontalPadding = getPaddingLeft() + getPaddingRight(); 476 477 // The children are given the same width and height as the workspace 478 // unless they were set to WRAP_CONTENT 479 if (DEBUG) Log.d(TAG, "PagedView.onMeasure(): " + widthSize + ", " + heightSize); 480 final int childCount = getChildCount(); 481 for (int i = 0; i < childCount; i++) { 482 // disallowing padding in paged view (just pass 0) 483 final View child = getPageAt(i); 484 485 int childWidthMode = MeasureSpec.EXACTLY; 486 int childHeightMode = MeasureSpec.EXACTLY; 487 488 final int childWidthMeasureSpec = 489 MeasureSpec.makeMeasureSpec(widthSize - horizontalPadding, childWidthMode); 490 final int childHeightMeasureSpec = 491 MeasureSpec.makeMeasureSpec(heightSize - verticalPadding, childHeightMode); 492 493 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 494 } 495 496 setMeasuredDimension(widthSize, heightSize); 497 498 // We can't call getChildOffset/getRelativeChildOffset until we set the measured dimensions. 499 // We also wait until we set the measured dimensions before flushing the cache as well, to 500 // ensure that the cache is filled with good values. 501 invalidateCachedOffsets(); 502 503 if (mChildCountOnLastMeasure != getChildCount()) { 504 setCurrentPage(mCurrentPage); 505 } 506 mChildCountOnLastMeasure = getChildCount(); 507 508 if (childCount > 0) { 509 if (DEBUG) Log.d(TAG, "getRelativeChildOffset(): " + getMeasuredWidth() + ", " 510 + getChildWidth(0)); 511 512 // Calculate the variable page spacing if necessary 513 if (mPageSpacing == AUTOMATIC_PAGE_SPACING) { 514 // The gap between pages in the PagedView should be equal to the gap from the page 515 // to the edge of the screen (so it is not visible in the current screen). To 516 // account for unequal padding on each side of the paged view, we take the maximum 517 // of the left/right gap and use that as the gap between each page. 518 int offset = getRelativeChildOffset(0); 519 int spacing = Math.max(offset, widthSize - offset - 520 getChildAt(0).getMeasuredWidth()); 521 setPageSpacing(spacing); 522 } 523 } 524 525 updateScrollingIndicatorPosition(); 526 527 if (childCount > 0) { 528 mMaxScrollX = getChildOffset(childCount - 1) - getRelativeChildOffset(childCount - 1); 529 } else { 530 mMaxScrollX = 0; 531 } 532 } 533 534 public void setPageSpacing(int pageSpacing) { 535 mPageSpacing = pageSpacing; 536 invalidateCachedOffsets(); 537 } 538 539 @Override 540 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 541 if (!mIsDataReady || getChildCount() == 0) { 542 return; 543 } 544 545 if (DEBUG) Log.d(TAG, "PagedView.onLayout()"); 546 final int verticalPadding = getPaddingTop() + getPaddingBottom(); 547 final int childCount = getChildCount(); 548 int childLeft = getRelativeChildOffset(0); 549 550 for (int i = 0; i < childCount; i++) { 551 final View child = getPageAt(i); 552 if (child.getVisibility() != View.GONE) { 553 final int childWidth = getScaledMeasuredWidth(child); 554 final int childHeight = child.getMeasuredHeight(); 555 int childTop = getPaddingTop(); 556 if (mCenterPagesVertically) { 557 childTop += ((getMeasuredHeight() - verticalPadding) - childHeight) / 2; 558 } 559 560 if (DEBUG) Log.d(TAG, "\tlayout-child" + i + ": " + childLeft + ", " + childTop); 561 child.layout(childLeft, childTop, 562 childLeft + child.getMeasuredWidth(), childTop + childHeight); 563 childLeft += childWidth + mPageSpacing; 564 } 565 } 566 567 if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { 568 setHorizontalScrollBarEnabled(false); 569 updateCurrentPageScroll(); 570 setHorizontalScrollBarEnabled(true); 571 mFirstLayout = false; 572 } 573 } 574 575 protected void screenScrolled(int screenCenter) { 576 if (isScrollingIndicatorEnabled()) { 577 updateScrollingIndicator(); 578 } 579 boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX; 580 581 if (mFadeInAdjacentScreens && !isInOverscroll) { 582 for (int i = 0; i < getChildCount(); i++) { 583 View child = getChildAt(i); 584 if (child != null) { 585 float scrollProgress = getScrollProgress(screenCenter, child, i); 586 float alpha = 1 - Math.abs(scrollProgress); 587 child.setAlpha(alpha); 588 } 589 } 590 invalidate(); 591 } 592 } 593 594 @Override 595 public void onChildViewAdded(View parent, View child) { 596 // This ensures that when children are added, they get the correct transforms / alphas 597 // in accordance with any scroll effects. 598 mForceScreenScrolled = true; 599 invalidate(); 600 invalidateCachedOffsets(); 601 } 602 603 @Override 604 public void onChildViewRemoved(View parent, View child) { 605 // TODO Auto-generated method stub 606 } 607 608 protected void invalidateCachedOffsets() { 609 int count = getChildCount(); 610 if (count == 0) { 611 mChildOffsets = null; 612 mChildRelativeOffsets = null; 613 mChildOffsetsWithLayoutScale = null; 614 return; 615 } 616 617 mChildOffsets = new int[count]; 618 mChildRelativeOffsets = new int[count]; 619 mChildOffsetsWithLayoutScale = new int[count]; 620 for (int i = 0; i < count; i++) { 621 mChildOffsets[i] = -1; 622 mChildRelativeOffsets[i] = -1; 623 mChildOffsetsWithLayoutScale[i] = -1; 624 } 625 } 626 627 protected int getChildOffset(int index) { 628 if (index < 0 || index > getChildCount() - 1) return 0; 629 630 int[] childOffsets = Float.compare(mLayoutScale, 1f) == 0 ? 631 mChildOffsets : mChildOffsetsWithLayoutScale; 632 633 if (childOffsets != null && childOffsets[index] != -1) { 634 return childOffsets[index]; 635 } else { 636 if (getChildCount() == 0) 637 return 0; 638 639 int offset = getRelativeChildOffset(0); 640 for (int i = 0; i < index; ++i) { 641 offset += getScaledMeasuredWidth(getPageAt(i)) + mPageSpacing; 642 } 643 if (childOffsets != null) { 644 childOffsets[index] = offset; 645 } 646 return offset; 647 } 648 } 649 650 protected int getRelativeChildOffset(int index) { 651 if (index < 0 || index > getChildCount() - 1) return 0; 652 653 if (mChildRelativeOffsets != null && mChildRelativeOffsets[index] != -1) { 654 return mChildRelativeOffsets[index]; 655 } else { 656 final int padding = getPaddingLeft() + getPaddingRight(); 657 final int offset = getPaddingLeft() + 658 (getMeasuredWidth() - padding - getChildWidth(index)) / 2; 659 if (mChildRelativeOffsets != null) { 660 mChildRelativeOffsets[index] = offset; 661 } 662 return offset; 663 } 664 } 665 666 protected int getScaledMeasuredWidth(View child) { 667 // This functions are called enough times that it actually makes a difference in the 668 // profiler -- so just inline the max() here 669 final int measuredWidth = child.getMeasuredWidth(); 670 final int minWidth = mMinimumWidth; 671 final int maxWidth = (minWidth > measuredWidth) ? minWidth : measuredWidth; 672 return (int) (maxWidth * mLayoutScale + 0.5f); 673 } 674 675 protected void getVisiblePages(int[] range) { 676 final int pageCount = getChildCount(); 677 678 if (pageCount > 0) { 679 final int screenWidth = getMeasuredWidth(); 680 int leftScreen = 0; 681 int rightScreen = 0; 682 View currPage = getPageAt(leftScreen); 683 while (leftScreen < pageCount - 1 && 684 currPage.getX() + currPage.getWidth() - 685 currPage.getPaddingRight() < getScrollX()) { 686 leftScreen++; 687 currPage = getPageAt(leftScreen); 688 } 689 rightScreen = leftScreen; 690 currPage = getPageAt(rightScreen + 1); 691 while (rightScreen < pageCount - 1 && 692 currPage.getX() - currPage.getPaddingLeft() < getScrollX() + screenWidth) { 693 rightScreen++; 694 currPage = getPageAt(rightScreen + 1); 695 } 696 range[0] = leftScreen; 697 range[1] = rightScreen; 698 } else { 699 range[0] = -1; 700 range[1] = -1; 701 } 702 } 703 704 protected boolean shouldDrawChild(View child) { 705 return child.getAlpha() > 0; 706 } 707 708 @Override 709 protected void dispatchDraw(Canvas canvas) { 710 int halfScreenSize = getMeasuredWidth() / 2; 711 // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. 712 // Otherwise it is equal to the scaled overscroll position. 713 int screenCenter = mOverScrollX + halfScreenSize; 714 715 if (screenCenter != mLastScreenCenter || mForceScreenScrolled) { 716 // set mForceScreenScrolled before calling screenScrolled so that screenScrolled can 717 // set it for the next frame 718 mForceScreenScrolled = false; 719 screenScrolled(screenCenter); 720 mLastScreenCenter = screenCenter; 721 } 722 723 // Find out which screens are visible; as an optimization we only call draw on them 724 final int pageCount = getChildCount(); 725 if (pageCount > 0) { 726 getVisiblePages(mTempVisiblePagesRange); 727 final int leftScreen = mTempVisiblePagesRange[0]; 728 final int rightScreen = mTempVisiblePagesRange[1]; 729 if (leftScreen != -1 && rightScreen != -1) { 730 final long drawingTime = getDrawingTime(); 731 // Clip to the bounds 732 canvas.save(); 733 canvas.clipRect(getScrollX(), getScrollY(), getScrollX() + getRight() - getLeft(), 734 getScrollY() + getBottom() - getTop()); 735 736 for (int i = getChildCount() - 1; i >= 0; i--) { 737 final View v = getPageAt(i); 738 if (mForceDrawAllChildrenNextFrame || 739 (leftScreen <= i && i <= rightScreen && shouldDrawChild(v))) { 740 drawChild(canvas, v, drawingTime); 741 } 742 } 743 mForceDrawAllChildrenNextFrame = false; 744 canvas.restore(); 745 } 746 } 747 } 748 749 @Override 750 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { 751 int page = indexToPage(indexOfChild(child)); 752 if (page != mCurrentPage || !mScroller.isFinished()) { 753 snapToPage(page); 754 return true; 755 } 756 return false; 757 } 758 759 @Override 760 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 761 int focusablePage; 762 if (mNextPage != INVALID_PAGE) { 763 focusablePage = mNextPage; 764 } else { 765 focusablePage = mCurrentPage; 766 } 767 View v = getPageAt(focusablePage); 768 if (v != null) { 769 return v.requestFocus(direction, previouslyFocusedRect); 770 } 771 return false; 772 } 773 774 @Override 775 public boolean dispatchUnhandledMove(View focused, int direction) { 776 if (direction == View.FOCUS_LEFT) { 777 if (getCurrentPage() > 0) { 778 snapToPage(getCurrentPage() - 1); 779 return true; 780 } 781 } else if (direction == View.FOCUS_RIGHT) { 782 if (getCurrentPage() < getPageCount() - 1) { 783 snapToPage(getCurrentPage() + 1); 784 return true; 785 } 786 } 787 return super.dispatchUnhandledMove(focused, direction); 788 } 789 790 @Override 791 public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { 792 if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) { 793 getPageAt(mCurrentPage).addFocusables(views, direction, focusableMode); 794 } 795 if (direction == View.FOCUS_LEFT) { 796 if (mCurrentPage > 0) { 797 getPageAt(mCurrentPage - 1).addFocusables(views, direction, focusableMode); 798 } 799 } else if (direction == View.FOCUS_RIGHT){ 800 if (mCurrentPage < getPageCount() - 1) { 801 getPageAt(mCurrentPage + 1).addFocusables(views, direction, focusableMode); 802 } 803 } 804 } 805 806 /** 807 * If one of our descendant views decides that it could be focused now, only 808 * pass that along if it's on the current page. 809 * 810 * This happens when live folders requery, and if they're off page, they 811 * end up calling requestFocus, which pulls it on page. 812 */ 813 @Override 814 public void focusableViewAvailable(View focused) { 815 View current = getPageAt(mCurrentPage); 816 View v = focused; 817 while (true) { 818 if (v == current) { 819 super.focusableViewAvailable(focused); 820 return; 821 } 822 if (v == this) { 823 return; 824 } 825 ViewParent parent = v.getParent(); 826 if (parent instanceof View) { 827 v = (View)v.getParent(); 828 } else { 829 return; 830 } 831 } 832 } 833 834 /** 835 * {@inheritDoc} 836 */ 837 @Override 838 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 839 if (disallowIntercept) { 840 // We need to make sure to cancel our long press if 841 // a scrollable widget takes over touch events 842 final View currentPage = getPageAt(mCurrentPage); 843 currentPage.cancelLongPress(); 844 } 845 super.requestDisallowInterceptTouchEvent(disallowIntercept); 846 } 847 848 /** 849 * Return true if a tap at (x, y) should trigger a flip to the previous page. 850 */ 851 protected boolean hitsPreviousPage(float x, float y) { 852 return (x < getRelativeChildOffset(mCurrentPage) - mPageSpacing); 853 } 854 855 /** 856 * Return true if a tap at (x, y) should trigger a flip to the next page. 857 */ 858 protected boolean hitsNextPage(float x, float y) { 859 return (x > (getMeasuredWidth() - getRelativeChildOffset(mCurrentPage) + mPageSpacing)); 860 } 861 862 @Override 863 public boolean onInterceptTouchEvent(MotionEvent ev) { 864 /* 865 * This method JUST determines whether we want to intercept the motion. 866 * If we return true, onTouchEvent will be called and we do the actual 867 * scrolling there. 868 */ 869 acquireVelocityTrackerAndAddMovement(ev); 870 871 // Skip touch handling if there are no pages to swipe 872 if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev); 873 874 /* 875 * Shortcut the most recurring case: the user is in the dragging 876 * state and he is moving his finger. We want to intercept this 877 * motion. 878 */ 879 final int action = ev.getAction(); 880 if ((action == MotionEvent.ACTION_MOVE) && 881 (mTouchState == TOUCH_STATE_SCROLLING)) { 882 return true; 883 } 884 885 switch (action & MotionEvent.ACTION_MASK) { 886 case MotionEvent.ACTION_MOVE: { 887 /* 888 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 889 * whether the user has moved far enough from his original down touch. 890 */ 891 if (mActivePointerId != INVALID_POINTER) { 892 determineScrollingStart(ev); 893 break; 894 } 895 // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN 896 // event. in that case, treat the first occurence of a move event as a ACTION_DOWN 897 // i.e. fall through to the next case (don't break) 898 // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events 899 // while it's small- this was causing a crash before we checked for INVALID_POINTER) 900 } 901 902 case MotionEvent.ACTION_DOWN: { 903 final float x = ev.getX(); 904 final float y = ev.getY(); 905 // Remember location of down touch 906 mDownMotionX = x; 907 mLastMotionX = x; 908 mLastMotionY = y; 909 mLastMotionXRemainder = 0; 910 mTotalMotionX = 0; 911 mActivePointerId = ev.getPointerId(0); 912 mAllowLongPress = true; 913 914 /* 915 * If being flinged and user touches the screen, initiate drag; 916 * otherwise don't. mScroller.isFinished should be false when 917 * being flinged. 918 */ 919 final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX()); 920 final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop); 921 if (finishedScrolling) { 922 mTouchState = TOUCH_STATE_REST; 923 mScroller.abortAnimation(); 924 } else { 925 mTouchState = TOUCH_STATE_SCROLLING; 926 } 927 928 // check if this can be the beginning of a tap on the side of the pages 929 // to scroll the current page 930 if (mTouchState != TOUCH_STATE_PREV_PAGE && mTouchState != TOUCH_STATE_NEXT_PAGE) { 931 if (getChildCount() > 0) { 932 if (hitsPreviousPage(x, y)) { 933 mTouchState = TOUCH_STATE_PREV_PAGE; 934 } else if (hitsNextPage(x, y)) { 935 mTouchState = TOUCH_STATE_NEXT_PAGE; 936 } 937 } 938 } 939 break; 940 } 941 942 case MotionEvent.ACTION_UP: 943 case MotionEvent.ACTION_CANCEL: 944 mTouchState = TOUCH_STATE_REST; 945 mAllowLongPress = false; 946 mActivePointerId = INVALID_POINTER; 947 releaseVelocityTracker(); 948 break; 949 950 case MotionEvent.ACTION_POINTER_UP: 951 onSecondaryPointerUp(ev); 952 releaseVelocityTracker(); 953 break; 954 } 955 956 /* 957 * The only time we want to intercept motion events is if we are in the 958 * drag mode. 959 */ 960 return mTouchState != TOUCH_STATE_REST; 961 } 962 963 protected void determineScrollingStart(MotionEvent ev) { 964 determineScrollingStart(ev, 1.0f); 965 } 966 967 /* 968 * Determines if we should change the touch state to start scrolling after the 969 * user moves their touch point too far. 970 */ 971 protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) { 972 /* 973 * Locally do absolute value. mLastMotionX is set to the y value 974 * of the down event. 975 */ 976 final int pointerIndex = ev.findPointerIndex(mActivePointerId); 977 if (pointerIndex == -1) { 978 return; 979 } 980 final float x = ev.getX(pointerIndex); 981 final float y = ev.getY(pointerIndex); 982 final int xDiff = (int) Math.abs(x - mLastMotionX); 983 final int yDiff = (int) Math.abs(y - mLastMotionY); 984 985 final int touchSlop = Math.round(touchSlopScale * mTouchSlop); 986 boolean xPaged = xDiff > mPagingTouchSlop; 987 boolean xMoved = xDiff > touchSlop; 988 boolean yMoved = yDiff > touchSlop; 989 990 if (xMoved || xPaged || yMoved) { 991 if (mUsePagingTouchSlop ? xPaged : xMoved) { 992 // Scroll if the user moved far enough along the X axis 993 mTouchState = TOUCH_STATE_SCROLLING; 994 mTotalMotionX += Math.abs(mLastMotionX - x); 995 mLastMotionX = x; 996 mLastMotionXRemainder = 0; 997 mTouchX = getScrollX(); 998 mSmoothingTime = System.nanoTime() / NANOTIME_DIV; 999 pageBeginMoving(); 1000 } 1001 // Either way, cancel any pending longpress 1002 cancelCurrentPageLongPress(); 1003 } 1004 } 1005 1006 protected void cancelCurrentPageLongPress() { 1007 if (mAllowLongPress) { 1008 mAllowLongPress = false; 1009 // Try canceling the long press. It could also have been scheduled 1010 // by a distant descendant, so use the mAllowLongPress flag to block 1011 // everything 1012 final View currentPage = getPageAt(mCurrentPage); 1013 if (currentPage != null) { 1014 currentPage.cancelLongPress(); 1015 } 1016 } 1017 } 1018 1019 protected float getScrollProgress(int screenCenter, View v, int page) { 1020 final int halfScreenSize = getMeasuredWidth() / 2; 1021 1022 int totalDistance = getScaledMeasuredWidth(v) + mPageSpacing; 1023 int delta = screenCenter - (getChildOffset(page) - 1024 getRelativeChildOffset(page) + halfScreenSize); 1025 1026 float scrollProgress = delta / (totalDistance * 1.0f); 1027 scrollProgress = Math.min(scrollProgress, 1.0f); 1028 scrollProgress = Math.max(scrollProgress, -1.0f); 1029 return scrollProgress; 1030 } 1031 1032 // This curve determines how the effect of scrolling over the limits of the page dimishes 1033 // as the user pulls further and further from the bounds 1034 private float overScrollInfluenceCurve(float f) { 1035 f -= 1.0f; 1036 return f * f * f + 1.0f; 1037 } 1038 1039 protected void acceleratedOverScroll(float amount) { 1040 int screenSize = getMeasuredWidth(); 1041 1042 // We want to reach the max over scroll effect when the user has 1043 // over scrolled half the size of the screen 1044 float f = OVERSCROLL_ACCELERATE_FACTOR * (amount / screenSize); 1045 1046 if (f == 0) return; 1047 1048 // Clamp this factor, f, to -1 < f < 1 1049 if (Math.abs(f) >= 1) { 1050 f /= Math.abs(f); 1051 } 1052 1053 int overScrollAmount = (int) Math.round(f * screenSize); 1054 if (amount < 0) { 1055 mOverScrollX = overScrollAmount; 1056 super.scrollTo(0, getScrollY()); 1057 } else { 1058 mOverScrollX = mMaxScrollX + overScrollAmount; 1059 super.scrollTo(mMaxScrollX, getScrollY()); 1060 } 1061 invalidate(); 1062 } 1063 1064 protected void dampedOverScroll(float amount) { 1065 int screenSize = getMeasuredWidth(); 1066 1067 float f = (amount / screenSize); 1068 1069 if (f == 0) return; 1070 f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); 1071 1072 // Clamp this factor, f, to -1 < f < 1 1073 if (Math.abs(f) >= 1) { 1074 f /= Math.abs(f); 1075 } 1076 1077 int overScrollAmount = (int) Math.round(OVERSCROLL_DAMP_FACTOR * f * screenSize); 1078 if (amount < 0) { 1079 mOverScrollX = overScrollAmount; 1080 super.scrollTo(0, getScrollY()); 1081 } else { 1082 mOverScrollX = mMaxScrollX + overScrollAmount; 1083 super.scrollTo(mMaxScrollX, getScrollY()); 1084 } 1085 invalidate(); 1086 } 1087 1088 protected void overScroll(float amount) { 1089 dampedOverScroll(amount); 1090 } 1091 1092 protected float maxOverScroll() { 1093 // Using the formula in overScroll, assuming that f = 1.0 (which it should generally not 1094 // exceed). Used to find out how much extra wallpaper we need for the over scroll effect 1095 float f = 1.0f; 1096 f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); 1097 return OVERSCROLL_DAMP_FACTOR * f; 1098 } 1099 1100 @Override 1101 public boolean onTouchEvent(MotionEvent ev) { 1102 // Skip touch handling if there are no pages to swipe 1103 if (getChildCount() <= 0) return super.onTouchEvent(ev); 1104 1105 acquireVelocityTrackerAndAddMovement(ev); 1106 1107 final int action = ev.getAction(); 1108 1109 switch (action & MotionEvent.ACTION_MASK) { 1110 case MotionEvent.ACTION_DOWN: 1111 /* 1112 * If being flinged and user touches, stop the fling. isFinished 1113 * will be false if being flinged. 1114 */ 1115 if (!mScroller.isFinished()) { 1116 mScroller.abortAnimation(); 1117 } 1118 1119 // Remember where the motion event started 1120 mDownMotionX = mLastMotionX = ev.getX(); 1121 mLastMotionXRemainder = 0; 1122 mTotalMotionX = 0; 1123 mActivePointerId = ev.getPointerId(0); 1124 if (mTouchState == TOUCH_STATE_SCROLLING) { 1125 pageBeginMoving(); 1126 } 1127 break; 1128 1129 case MotionEvent.ACTION_MOVE: 1130 if (mTouchState == TOUCH_STATE_SCROLLING) { 1131 // Scroll to follow the motion event 1132 final int pointerIndex = ev.findPointerIndex(mActivePointerId); 1133 final float x = ev.getX(pointerIndex); 1134 final float deltaX = mLastMotionX + mLastMotionXRemainder - x; 1135 1136 mTotalMotionX += Math.abs(deltaX); 1137 1138 // Only scroll and update mLastMotionX if we have moved some discrete amount. We 1139 // keep the remainder because we are actually testing if we've moved from the last 1140 // scrolled position (which is discrete). 1141 if (Math.abs(deltaX) >= 1.0f) { 1142 mTouchX += deltaX; 1143 mSmoothingTime = System.nanoTime() / NANOTIME_DIV; 1144 if (!mDeferScrollUpdate) { 1145 scrollBy((int) deltaX, 0); 1146 if (DEBUG) Log.d(TAG, "onTouchEvent().Scrolling: " + deltaX); 1147 } else { 1148 invalidate(); 1149 } 1150 mLastMotionX = x; 1151 mLastMotionXRemainder = deltaX - (int) deltaX; 1152 } else { 1153 awakenScrollBars(); 1154 } 1155 } else { 1156 determineScrollingStart(ev); 1157 } 1158 break; 1159 1160 case MotionEvent.ACTION_UP: 1161 if (mTouchState == TOUCH_STATE_SCROLLING) { 1162 final int activePointerId = mActivePointerId; 1163 final int pointerIndex = ev.findPointerIndex(activePointerId); 1164 final float x = ev.getX(pointerIndex); 1165 final VelocityTracker velocityTracker = mVelocityTracker; 1166 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1167 int velocityX = (int) velocityTracker.getXVelocity(activePointerId); 1168 final int deltaX = (int) (x - mDownMotionX); 1169 final int pageWidth = getScaledMeasuredWidth(getPageAt(mCurrentPage)); 1170 boolean isSignificantMove = Math.abs(deltaX) > pageWidth * 1171 SIGNIFICANT_MOVE_THRESHOLD; 1172 1173 mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x); 1174 1175 boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING && 1176 Math.abs(velocityX) > mFlingThresholdVelocity; 1177 1178 // In the case that the page is moved far to one direction and then is flung 1179 // in the opposite direction, we use a threshold to determine whether we should 1180 // just return to the starting page, or if we should skip one further. 1181 boolean returnToOriginalPage = false; 1182 if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD && 1183 Math.signum(velocityX) != Math.signum(deltaX) && isFling) { 1184 returnToOriginalPage = true; 1185 } 1186 1187 int finalPage; 1188 // We give flings precedence over large moves, which is why we short-circuit our 1189 // test for a large move if a fling has been registered. That is, a large 1190 // move to the left and fling to the right will register as a fling to the right. 1191 if (((isSignificantMove && deltaX > 0 && !isFling) || 1192 (isFling && velocityX > 0)) && mCurrentPage > 0) { 1193 finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1; 1194 snapToPageWithVelocity(finalPage, velocityX); 1195 } else if (((isSignificantMove && deltaX < 0 && !isFling) || 1196 (isFling && velocityX < 0)) && 1197 mCurrentPage < getChildCount() - 1) { 1198 finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1; 1199 snapToPageWithVelocity(finalPage, velocityX); 1200 } else { 1201 snapToDestination(); 1202 } 1203 } else if (mTouchState == TOUCH_STATE_PREV_PAGE) { 1204 // at this point we have not moved beyond the touch slop 1205 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so 1206 // we can just page 1207 int nextPage = Math.max(0, mCurrentPage - 1); 1208 if (nextPage != mCurrentPage) { 1209 snapToPage(nextPage); 1210 } else { 1211 snapToDestination(); 1212 } 1213 } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) { 1214 // at this point we have not moved beyond the touch slop 1215 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so 1216 // we can just page 1217 int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1); 1218 if (nextPage != mCurrentPage) { 1219 snapToPage(nextPage); 1220 } else { 1221 snapToDestination(); 1222 } 1223 } else { 1224 onUnhandledTap(ev); 1225 } 1226 mTouchState = TOUCH_STATE_REST; 1227 mActivePointerId = INVALID_POINTER; 1228 releaseVelocityTracker(); 1229 break; 1230 1231 case MotionEvent.ACTION_CANCEL: 1232 if (mTouchState == TOUCH_STATE_SCROLLING) { 1233 snapToDestination(); 1234 } 1235 mTouchState = TOUCH_STATE_REST; 1236 mActivePointerId = INVALID_POINTER; 1237 releaseVelocityTracker(); 1238 break; 1239 1240 case MotionEvent.ACTION_POINTER_UP: 1241 onSecondaryPointerUp(ev); 1242 break; 1243 } 1244 1245 return true; 1246 } 1247 1248 @Override 1249 public boolean onGenericMotionEvent(MotionEvent event) { 1250 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { 1251 switch (event.getAction()) { 1252 case MotionEvent.ACTION_SCROLL: { 1253 // Handle mouse (or ext. device) by shifting the page depending on the scroll 1254 final float vscroll; 1255 final float hscroll; 1256 if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) { 1257 vscroll = 0; 1258 hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); 1259 } else { 1260 vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); 1261 hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); 1262 } 1263 if (hscroll != 0 || vscroll != 0) { 1264 if (hscroll > 0 || vscroll > 0) { 1265 scrollRight(); 1266 } else { 1267 scrollLeft(); 1268 } 1269 return true; 1270 } 1271 } 1272 } 1273 } 1274 return super.onGenericMotionEvent(event); 1275 } 1276 1277 private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { 1278 if (mVelocityTracker == null) { 1279 mVelocityTracker = VelocityTracker.obtain(); 1280 } 1281 mVelocityTracker.addMovement(ev); 1282 } 1283 1284 private void releaseVelocityTracker() { 1285 if (mVelocityTracker != null) { 1286 mVelocityTracker.recycle(); 1287 mVelocityTracker = null; 1288 } 1289 } 1290 1291 private void onSecondaryPointerUp(MotionEvent ev) { 1292 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 1293 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 1294 final int pointerId = ev.getPointerId(pointerIndex); 1295 if (pointerId == mActivePointerId) { 1296 // This was our active pointer going up. Choose a new 1297 // active pointer and adjust accordingly. 1298 // TODO: Make this decision more intelligent. 1299 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 1300 mLastMotionX = mDownMotionX = ev.getX(newPointerIndex); 1301 mLastMotionY = ev.getY(newPointerIndex); 1302 mLastMotionXRemainder = 0; 1303 mActivePointerId = ev.getPointerId(newPointerIndex); 1304 if (mVelocityTracker != null) { 1305 mVelocityTracker.clear(); 1306 } 1307 } 1308 } 1309 1310 protected void onUnhandledTap(MotionEvent ev) {} 1311 1312 @Override 1313 public void requestChildFocus(View child, View focused) { 1314 super.requestChildFocus(child, focused); 1315 int page = indexToPage(indexOfChild(child)); 1316 if (page >= 0 && page != getCurrentPage() && !isInTouchMode()) { 1317 snapToPage(page); 1318 } 1319 } 1320 1321 protected int getChildIndexForRelativeOffset(int relativeOffset) { 1322 final int childCount = getChildCount(); 1323 int left; 1324 int right; 1325 for (int i = 0; i < childCount; ++i) { 1326 left = getRelativeChildOffset(i); 1327 right = (left + getScaledMeasuredWidth(getPageAt(i))); 1328 if (left <= relativeOffset && relativeOffset <= right) { 1329 return i; 1330 } 1331 } 1332 return -1; 1333 } 1334 1335 protected int getChildWidth(int index) { 1336 // This functions are called enough times that it actually makes a difference in the 1337 // profiler -- so just inline the max() here 1338 final int measuredWidth = getPageAt(index).getMeasuredWidth(); 1339 final int minWidth = mMinimumWidth; 1340 return (minWidth > measuredWidth) ? minWidth : measuredWidth; 1341 } 1342 1343 int getPageNearestToCenterOfScreen() { 1344 int minDistanceFromScreenCenter = Integer.MAX_VALUE; 1345 int minDistanceFromScreenCenterIndex = -1; 1346 int screenCenter = getScrollX() + (getMeasuredWidth() / 2); 1347 final int childCount = getChildCount(); 1348 for (int i = 0; i < childCount; ++i) { 1349 View layout = (View) getPageAt(i); 1350 int childWidth = getScaledMeasuredWidth(layout); 1351 int halfChildWidth = (childWidth / 2); 1352 int childCenter = getChildOffset(i) + halfChildWidth; 1353 int distanceFromScreenCenter = Math.abs(childCenter - screenCenter); 1354 if (distanceFromScreenCenter < minDistanceFromScreenCenter) { 1355 minDistanceFromScreenCenter = distanceFromScreenCenter; 1356 minDistanceFromScreenCenterIndex = i; 1357 } 1358 } 1359 return minDistanceFromScreenCenterIndex; 1360 } 1361 1362 protected void snapToDestination() { 1363 snapToPage(getPageNearestToCenterOfScreen(), PAGE_SNAP_ANIMATION_DURATION); 1364 } 1365 1366 private static class ScrollInterpolator implements Interpolator { 1367 public ScrollInterpolator() { 1368 } 1369 1370 public float getInterpolation(float t) { 1371 t -= 1.0f; 1372 return t*t*t*t*t + 1; 1373 } 1374 } 1375 1376 // We want the duration of the page snap animation to be influenced by the distance that 1377 // the screen has to travel, however, we don't want this duration to be effected in a 1378 // purely linear fashion. Instead, we use this method to moderate the effect that the distance 1379 // of travel has on the overall snap duration. 1380 float distanceInfluenceForSnapDuration(float f) { 1381 f -= 0.5f; // center the values about 0. 1382 f *= 0.3f * Math.PI / 2.0f; 1383 return (float) Math.sin(f); 1384 } 1385 1386 protected void snapToPageWithVelocity(int whichPage, int velocity) { 1387 whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1)); 1388 int halfScreenSize = getMeasuredWidth() / 2; 1389 1390 if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage)); 1391 if (DEBUG) Log.d(TAG, "snapToPageWithVelocity.getRelativeChildOffset(): " 1392 + getMeasuredWidth() + ", " + getChildWidth(whichPage)); 1393 final int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); 1394 int delta = newX - mUnboundedScrollX; 1395 int duration = 0; 1396 1397 if (Math.abs(velocity) < mMinFlingVelocity) { 1398 // If the velocity is low enough, then treat this more as an automatic page advance 1399 // as opposed to an apparent physical response to flinging 1400 snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); 1401 return; 1402 } 1403 1404 // Here we compute a "distance" that will be used in the computation of the overall 1405 // snap duration. This is a function of the actual distance that needs to be traveled; 1406 // we keep this value close to half screen size in order to reduce the variance in snap 1407 // duration as a function of the distance the page needs to travel. 1408 float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize)); 1409 float distance = halfScreenSize + halfScreenSize * 1410 distanceInfluenceForSnapDuration(distanceRatio); 1411 1412 velocity = Math.abs(velocity); 1413 velocity = Math.max(mMinSnapVelocity, velocity); 1414 1415 // we want the page's snap velocity to approximately match the velocity at which the 1416 // user flings, so we scale the duration by a value near to the derivative of the scroll 1417 // interpolator at zero, ie. 5. We use 4 to make it a little slower. 1418 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 1419 1420 snapToPage(whichPage, delta, duration); 1421 } 1422 1423 protected void snapToPage(int whichPage) { 1424 snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); 1425 } 1426 1427 protected void snapToPage(int whichPage, int duration) { 1428 whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1)); 1429 1430 if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage)); 1431 if (DEBUG) Log.d(TAG, "snapToPage.getRelativeChildOffset(): " + getMeasuredWidth() + ", " 1432 + getChildWidth(whichPage)); 1433 int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); 1434 final int sX = mUnboundedScrollX; 1435 final int delta = newX - sX; 1436 snapToPage(whichPage, delta, duration); 1437 } 1438 1439 protected void snapToPage(int whichPage, int delta, int duration) { 1440 mNextPage = whichPage; 1441 1442 View focusedChild = getFocusedChild(); 1443 if (focusedChild != null && whichPage != mCurrentPage && 1444 focusedChild == getPageAt(mCurrentPage)) { 1445 focusedChild.clearFocus(); 1446 } 1447 1448 pageBeginMoving(); 1449 awakenScrollBars(duration); 1450 if (duration == 0) { 1451 duration = Math.abs(delta); 1452 } 1453 1454 if (!mScroller.isFinished()) mScroller.abortAnimation(); 1455 mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration); 1456 1457 notifyPageSwitchListener(); 1458 invalidate(); 1459 } 1460 1461 public void scrollLeft() { 1462 if (mScroller.isFinished()) { 1463 if (mCurrentPage > 0) snapToPage(mCurrentPage - 1); 1464 } else { 1465 if (mNextPage > 0) snapToPage(mNextPage - 1); 1466 } 1467 } 1468 1469 public void scrollRight() { 1470 if (mScroller.isFinished()) { 1471 if (mCurrentPage < getChildCount() -1) snapToPage(mCurrentPage + 1); 1472 } else { 1473 if (mNextPage < getChildCount() -1) snapToPage(mNextPage + 1); 1474 } 1475 } 1476 1477 public int getPageForView(View v) { 1478 int result = -1; 1479 if (v != null) { 1480 ViewParent vp = v.getParent(); 1481 int count = getChildCount(); 1482 for (int i = 0; i < count; i++) { 1483 if (vp == getPageAt(i)) { 1484 return i; 1485 } 1486 } 1487 } 1488 return result; 1489 } 1490 1491 /** 1492 * @return True is long presses are still allowed for the current touch 1493 */ 1494 public boolean allowLongPress() { 1495 return mAllowLongPress; 1496 } 1497 1498 /** 1499 * Set true to allow long-press events to be triggered, usually checked by 1500 * {@link Launcher} to accept or block dpad-initiated long-presses. 1501 */ 1502 public void setAllowLongPress(boolean allowLongPress) { 1503 mAllowLongPress = allowLongPress; 1504 } 1505 1506 public static class SavedState extends BaseSavedState { 1507 int currentPage = -1; 1508 1509 SavedState(Parcelable superState) { 1510 super(superState); 1511 } 1512 1513 private SavedState(Parcel in) { 1514 super(in); 1515 currentPage = in.readInt(); 1516 } 1517 1518 @Override 1519 public void writeToParcel(Parcel out, int flags) { 1520 super.writeToParcel(out, flags); 1521 out.writeInt(currentPage); 1522 } 1523 1524 public static final Parcelable.Creator<SavedState> CREATOR = 1525 new Parcelable.Creator<SavedState>() { 1526 public SavedState createFromParcel(Parcel in) { 1527 return new SavedState(in); 1528 } 1529 1530 public SavedState[] newArray(int size) { 1531 return new SavedState[size]; 1532 } 1533 }; 1534 } 1535 1536 protected View getScrollingIndicator() { 1537 return null; 1538 } 1539 1540 protected boolean isScrollingIndicatorEnabled() { 1541 return false; 1542 } 1543 1544 Runnable hideScrollingIndicatorRunnable = new Runnable() { 1545 @Override 1546 public void run() { 1547 hideScrollingIndicator(false); 1548 } 1549 }; 1550 1551 protected void flashScrollingIndicator(boolean animated) { 1552 removeCallbacks(hideScrollingIndicatorRunnable); 1553 showScrollingIndicator(!animated); 1554 postDelayed(hideScrollingIndicatorRunnable, sScrollIndicatorFlashDuration); 1555 } 1556 1557 protected void showScrollingIndicator(boolean immediately) { 1558 mShouldShowScrollIndicator = true; 1559 mShouldShowScrollIndicatorImmediately = true; 1560 if (getChildCount() <= 1) return; 1561 if (!isScrollingIndicatorEnabled()) return; 1562 1563 mShouldShowScrollIndicator = false; 1564 getScrollingIndicator(); 1565 if (mScrollIndicator != null) { 1566 // Fade the indicator in 1567 updateScrollingIndicatorPosition(); 1568 mScrollIndicator.setVisibility(View.VISIBLE); 1569 cancelScrollingIndicatorAnimations(); 1570 if (immediately) { 1571 mScrollIndicator.setAlpha(1f); 1572 } else { 1573 mScrollIndicatorAnimator = ObjectAnimator.ofFloat(mScrollIndicator, "alpha", 1f); 1574 mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeInDuration); 1575 mScrollIndicatorAnimator.start(); 1576 } 1577 } 1578 } 1579 1580 protected void cancelScrollingIndicatorAnimations() { 1581 if (mScrollIndicatorAnimator != null) { 1582 mScrollIndicatorAnimator.cancel(); 1583 } 1584 } 1585 1586 protected void hideScrollingIndicator(boolean immediately) { 1587 if (getChildCount() <= 1) return; 1588 if (!isScrollingIndicatorEnabled()) return; 1589 1590 getScrollingIndicator(); 1591 if (mScrollIndicator != null) { 1592 // Fade the indicator out 1593 updateScrollingIndicatorPosition(); 1594 cancelScrollingIndicatorAnimations(); 1595 if (immediately) { 1596 mScrollIndicator.setVisibility(View.INVISIBLE); 1597 mScrollIndicator.setAlpha(0f); 1598 } else { 1599 mScrollIndicatorAnimator = ObjectAnimator.ofFloat(mScrollIndicator, "alpha", 0f); 1600 mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeOutDuration); 1601 mScrollIndicatorAnimator.addListener(new AnimatorListenerAdapter() { 1602 private boolean cancelled = false; 1603 @Override 1604 public void onAnimationCancel(android.animation.Animator animation) { 1605 cancelled = true; 1606 } 1607 @Override 1608 public void onAnimationEnd(Animator animation) { 1609 if (!cancelled) { 1610 mScrollIndicator.setVisibility(View.INVISIBLE); 1611 } 1612 } 1613 }); 1614 mScrollIndicatorAnimator.start(); 1615 } 1616 } 1617 } 1618 1619 /** 1620 * To be overridden by subclasses to determine whether the scroll indicator should stretch to 1621 * fill its space on the track or not. 1622 */ 1623 protected boolean hasElasticScrollIndicator() { 1624 return true; 1625 } 1626 1627 private void updateScrollingIndicator() { 1628 if (getChildCount() <= 1) return; 1629 if (!isScrollingIndicatorEnabled()) return; 1630 1631 getScrollingIndicator(); 1632 if (mScrollIndicator != null) { 1633 updateScrollingIndicatorPosition(); 1634 } 1635 if (mShouldShowScrollIndicator) { 1636 showScrollingIndicator(mShouldShowScrollIndicatorImmediately); 1637 } 1638 } 1639 1640 private void updateScrollingIndicatorPosition() { 1641 if (!isScrollingIndicatorEnabled()) return; 1642 if (mScrollIndicator == null) return; 1643 int numPages = getChildCount(); 1644 int pageWidth = getMeasuredWidth(); 1645 int lastChildIndex = Math.max(0, getChildCount() - 1); 1646 int maxScrollX = getChildOffset(lastChildIndex) - getRelativeChildOffset(lastChildIndex); 1647 int trackWidth = pageWidth - mScrollIndicatorPaddingLeft - mScrollIndicatorPaddingRight; 1648 int indicatorWidth = mScrollIndicator.getMeasuredWidth() - 1649 mScrollIndicator.getPaddingLeft() - mScrollIndicator.getPaddingRight(); 1650 1651 float offset = Math.max(0f, Math.min(1f, (float) getScrollX() / maxScrollX)); 1652 int indicatorSpace = trackWidth / numPages; 1653 int indicatorPos = (int) (offset * (trackWidth - indicatorSpace)) + mScrollIndicatorPaddingLeft; 1654 if (hasElasticScrollIndicator()) { 1655 if (mScrollIndicator.getMeasuredWidth() != indicatorSpace) { 1656 mScrollIndicator.getLayoutParams().width = indicatorSpace; 1657 mScrollIndicator.requestLayout(); 1658 } 1659 } else { 1660 int indicatorCenterOffset = indicatorSpace / 2 - indicatorWidth / 2; 1661 indicatorPos += indicatorCenterOffset; 1662 } 1663 mScrollIndicator.setTranslationX(indicatorPos); 1664 } 1665 1666 /* Accessibility */ 1667 @Override 1668 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1669 super.onInitializeAccessibilityNodeInfo(info); 1670 info.setScrollable(getPageCount() > 1); 1671 if (getCurrentPage() < getPageCount() - 1) { 1672 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); 1673 } 1674 if (getCurrentPage() > 0) { 1675 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 1676 } 1677 } 1678 1679 @Override 1680 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1681 super.onInitializeAccessibilityEvent(event); 1682 event.setScrollable(true); 1683 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { 1684 event.setFromIndex(mCurrentPage); 1685 event.setToIndex(mCurrentPage); 1686 event.setItemCount(getChildCount()); 1687 } 1688 } 1689 1690 @Override 1691 public boolean performAccessibilityAction(int action, Bundle arguments) { 1692 if (super.performAccessibilityAction(action, arguments)) { 1693 return true; 1694 } 1695 switch (action) { 1696 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 1697 if (getCurrentPage() < getPageCount() - 1) { 1698 scrollRight(); 1699 return true; 1700 } 1701 } break; 1702 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 1703 if (getCurrentPage() > 0) { 1704 scrollLeft(); 1705 return true; 1706 } 1707 } break; 1708 } 1709 return false; 1710 } 1711 1712 @Override 1713 public boolean onHoverEvent(android.view.MotionEvent event) { 1714 return true; 1715 } 1716} 1717