PagedView.java revision 0abb36f6920733f813e22ad984bb7d48f0924698
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.launcher3; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.AnimatorSet; 22import android.animation.LayoutTransition; 23import android.animation.ObjectAnimator; 24import android.animation.TimeInterpolator; 25import android.annotation.TargetApi; 26import android.content.Context; 27import android.content.res.TypedArray; 28import android.graphics.Canvas; 29import android.graphics.Matrix; 30import android.graphics.Rect; 31import android.os.Build; 32import android.os.Bundle; 33import android.os.Parcel; 34import android.os.Parcelable; 35import android.util.AttributeSet; 36import android.util.DisplayMetrics; 37import android.util.Log; 38import android.view.InputDevice; 39import android.view.KeyEvent; 40import android.view.MotionEvent; 41import android.view.VelocityTracker; 42import android.view.View; 43import android.view.ViewConfiguration; 44import android.view.ViewGroup; 45import android.view.ViewParent; 46import android.view.accessibility.AccessibilityEvent; 47import android.view.accessibility.AccessibilityManager; 48import android.view.accessibility.AccessibilityNodeInfo; 49import android.view.animation.Interpolator; 50 51import com.android.launcher3.util.LauncherEdgeEffect; 52import com.android.launcher3.util.Thunk; 53 54import java.util.ArrayList; 55 56/** 57 * An abstraction of the original Workspace which supports browsing through a 58 * sequential list of "pages" 59 */ 60public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarchyChangeListener { 61 private static final String TAG = "PagedView"; 62 private static final boolean DEBUG = false; 63 protected static final int INVALID_PAGE = -1; 64 65 // the min drag distance for a fling to register, to prevent random page shifts 66 private static final int MIN_LENGTH_FOR_FLING = 25; 67 68 protected static final int PAGE_SNAP_ANIMATION_DURATION = 750; 69 protected static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950; 70 protected static final float NANOTIME_DIV = 1000000000.0f; 71 72 private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f; 73 // The page is moved more than halfway, automatically move to the next page on touch up. 74 private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f; 75 76 private static final float MAX_SCROLL_PROGRESS = 1.0f; 77 78 // The following constants need to be scaled based on density. The scaled versions will be 79 // assigned to the corresponding member variables below. 80 private static final int FLING_THRESHOLD_VELOCITY = 500; 81 private static final int MIN_SNAP_VELOCITY = 1500; 82 private static final int MIN_FLING_VELOCITY = 250; 83 84 public static final int INVALID_RESTORE_PAGE = -1001; 85 86 private boolean mFreeScroll = false; 87 private int mFreeScrollMinScrollX = -1; 88 private int mFreeScrollMaxScrollX = -1; 89 90 static final int AUTOMATIC_PAGE_SPACING = -1; 91 92 protected int mFlingThresholdVelocity; 93 protected int mMinFlingVelocity; 94 protected int mMinSnapVelocity; 95 96 protected float mDensity; 97 protected float mSmoothingTime; 98 protected float mTouchX; 99 100 protected boolean mFirstLayout = true; 101 private int mNormalChildHeight; 102 103 protected int mCurrentPage; 104 protected int mRestorePage = INVALID_RESTORE_PAGE; 105 protected int mChildCountOnLastLayout; 106 107 protected int mNextPage = INVALID_PAGE; 108 protected int mMaxScrollX; 109 protected LauncherScroller mScroller; 110 private Interpolator mDefaultInterpolator; 111 private VelocityTracker mVelocityTracker; 112 @Thunk int mPageSpacing = 0; 113 114 private float mParentDownMotionX; 115 private float mParentDownMotionY; 116 private float mDownMotionX; 117 private float mDownMotionY; 118 private float mDownScrollX; 119 private float mDragViewBaselineLeft; 120 protected float mLastMotionX; 121 protected float mLastMotionXRemainder; 122 protected float mLastMotionY; 123 protected float mTotalMotionX; 124 private int mLastScreenCenter = -1; 125 126 private boolean mCancelTap; 127 128 private int[] mPageScrolls; 129 130 protected final static int TOUCH_STATE_REST = 0; 131 protected final static int TOUCH_STATE_SCROLLING = 1; 132 protected final static int TOUCH_STATE_PREV_PAGE = 2; 133 protected final static int TOUCH_STATE_NEXT_PAGE = 3; 134 protected final static int TOUCH_STATE_REORDERING = 4; 135 136 protected final static float ALPHA_QUANTIZE_LEVEL = 0.0001f; 137 138 protected int mTouchState = TOUCH_STATE_REST; 139 protected boolean mForceScreenScrolled = false; 140 141 protected OnLongClickListener mLongClickListener; 142 143 protected int mTouchSlop; 144 private int mMaximumVelocity; 145 protected int mPageLayoutWidthGap; 146 protected int mPageLayoutHeightGap; 147 protected int mCellCountX = 0; 148 protected int mCellCountY = 0; 149 protected boolean mCenterPagesVertically; 150 protected boolean mAllowOverScroll = true; 151 protected int[] mTempVisiblePagesRange = new int[2]; 152 protected boolean mForceDrawAllChildrenNextFrame; 153 154 protected static final int INVALID_POINTER = -1; 155 156 protected int mActivePointerId = INVALID_POINTER; 157 158 private PageSwitchListener mPageSwitchListener; 159 160 // If true, modify alpha of neighboring pages as user scrolls left/right 161 protected boolean mFadeInAdjacentScreens = false; 162 163 protected boolean mIsPageMoving = false; 164 165 private boolean mWasInOverscroll = false; 166 167 // Page Indicator 168 @Thunk int mPageIndicatorViewId; 169 @Thunk PageIndicator mPageIndicator; 170 // The viewport whether the pages are to be contained (the actual view may be larger than the 171 // viewport) 172 private Rect mViewport = new Rect(); 173 174 // Reordering 175 // We use the min scale to determine how much to expand the actually PagedView measured 176 // dimensions such that when we are zoomed out, the view is not clipped 177 private static int REORDERING_DROP_REPOSITION_DURATION = 200; 178 @Thunk static int REORDERING_REORDER_REPOSITION_DURATION = 300; 179 private static int REORDERING_SIDE_PAGE_HOVER_TIMEOUT = 80; 180 181 private float mMinScale = 1f; 182 private boolean mUseMinScale = false; 183 protected View mDragView; 184 private Runnable mSidePageHoverRunnable; 185 @Thunk int mSidePageHoverIndex = -1; 186 // This variable's scope is only for the duration of startReordering() and endReordering() 187 private boolean mReorderingStarted = false; 188 // This variable's scope is for the duration of startReordering() and after the zoomIn() 189 // animation after endReordering() 190 private boolean mIsReordering; 191 // The runnable that settles the page after snapToPage and animateDragViewToOriginalPosition 192 private int NUM_ANIMATIONS_RUNNING_BEFORE_ZOOM_OUT = 2; 193 private int mPostReorderingPreZoomInRemainingAnimationCount; 194 private Runnable mPostReorderingPreZoomInRunnable; 195 196 // Convenience/caching 197 private static final Matrix sTmpInvMatrix = new Matrix(); 198 private static final float[] sTmpPoint = new float[2]; 199 private static final int[] sTmpIntPoint = new int[2]; 200 private static final Rect sTmpRect = new Rect(); 201 202 protected final Rect mInsets = new Rect(); 203 protected final boolean mIsRtl; 204 205 // When set to true, full screen content and overscroll effect is shited inside by right inset. 206 protected boolean mIgnoreRightInset; 207 208 // Edge effect 209 private final LauncherEdgeEffect mEdgeGlowLeft = new LauncherEdgeEffect(); 210 private final LauncherEdgeEffect mEdgeGlowRight = new LauncherEdgeEffect(); 211 212 public interface PageSwitchListener { 213 void onPageSwitch(View newPage, int newPageIndex); 214 } 215 216 public PagedView(Context context) { 217 this(context, null); 218 } 219 220 public PagedView(Context context, AttributeSet attrs) { 221 this(context, attrs, 0); 222 } 223 224 public PagedView(Context context, AttributeSet attrs, int defStyle) { 225 super(context, attrs, defStyle); 226 227 TypedArray a = context.obtainStyledAttributes(attrs, 228 R.styleable.PagedView, defStyle, 0); 229 230 mPageLayoutWidthGap = a.getDimensionPixelSize( 231 R.styleable.PagedView_pageLayoutWidthGap, 0); 232 mPageLayoutHeightGap = a.getDimensionPixelSize( 233 R.styleable.PagedView_pageLayoutHeightGap, 0); 234 mPageIndicatorViewId = a.getResourceId(R.styleable.PagedView_pageIndicator, -1); 235 a.recycle(); 236 237 setHapticFeedbackEnabled(false); 238 mIsRtl = Utilities.isRtl(getResources()); 239 init(); 240 } 241 242 /** 243 * Initializes various states for this workspace. 244 */ 245 protected void init() { 246 mScroller = new LauncherScroller(getContext()); 247 setDefaultInterpolator(new ScrollInterpolator()); 248 mCurrentPage = 0; 249 mCenterPagesVertically = true; 250 251 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 252 mTouchSlop = configuration.getScaledPagingTouchSlop(); 253 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 254 mDensity = getResources().getDisplayMetrics().density; 255 256 mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); 257 mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * mDensity); 258 mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * mDensity); 259 setOnHierarchyChangeListener(this); 260 setWillNotDraw(false); 261 } 262 263 protected void setEdgeGlowColor(int color) { 264 mEdgeGlowLeft.setColor(color); 265 mEdgeGlowRight.setColor(color); 266 } 267 268 protected void setDefaultInterpolator(Interpolator interpolator) { 269 mDefaultInterpolator = interpolator; 270 mScroller.setInterpolator(mDefaultInterpolator); 271 } 272 273 protected void onAttachedToWindow() { 274 super.onAttachedToWindow(); 275 276 // Hook up the page indicator 277 ViewGroup parent = (ViewGroup) getParent(); 278 ViewGroup grandParent = (ViewGroup) parent.getParent(); 279 if (mPageIndicator == null && mPageIndicatorViewId > -1) { 280 mPageIndicator = (PageIndicator) grandParent.findViewById(mPageIndicatorViewId); 281 mPageIndicator.removeAllMarkers(true); 282 283 ArrayList<PageIndicator.PageMarkerResources> markers = 284 new ArrayList<PageIndicator.PageMarkerResources>(); 285 for (int i = 0; i < getChildCount(); ++i) { 286 markers.add(getPageIndicatorMarker(i)); 287 } 288 289 mPageIndicator.addMarkers(markers, true); 290 291 OnClickListener listener = getPageIndicatorClickListener(); 292 if (listener != null) { 293 mPageIndicator.setOnClickListener(listener); 294 } 295 mPageIndicator.setContentDescription(getPageIndicatorDescription()); 296 } 297 } 298 299 protected String getPageIndicatorDescription() { 300 return getCurrentPageDescription(); 301 } 302 303 protected OnClickListener getPageIndicatorClickListener() { 304 return null; 305 } 306 307 @Override 308 protected void onDetachedFromWindow() { 309 super.onDetachedFromWindow(); 310 // Unhook the page indicator 311 mPageIndicator = null; 312 } 313 314 // Convenience methods to map points from self to parent and vice versa 315 private float[] mapPointFromViewToParent(View v, float x, float y) { 316 sTmpPoint[0] = x; 317 sTmpPoint[1] = y; 318 v.getMatrix().mapPoints(sTmpPoint); 319 sTmpPoint[0] += v.getLeft(); 320 sTmpPoint[1] += v.getTop(); 321 return sTmpPoint; 322 } 323 private float[] mapPointFromParentToView(View v, float x, float y) { 324 sTmpPoint[0] = x - v.getLeft(); 325 sTmpPoint[1] = y - v.getTop(); 326 v.getMatrix().invert(sTmpInvMatrix); 327 sTmpInvMatrix.mapPoints(sTmpPoint); 328 return sTmpPoint; 329 } 330 331 private void updateDragViewTranslationDuringDrag() { 332 if (mDragView != null) { 333 float x = (mLastMotionX - mDownMotionX) + (getScrollX() - mDownScrollX) + 334 (mDragViewBaselineLeft - mDragView.getLeft()); 335 float y = mLastMotionY - mDownMotionY; 336 mDragView.setTranslationX(x); 337 mDragView.setTranslationY(y); 338 339 if (DEBUG) Log.d(TAG, "PagedView.updateDragViewTranslationDuringDrag(): " 340 + x + ", " + y); 341 } 342 } 343 344 public void setMinScale(float f) { 345 mMinScale = f; 346 mUseMinScale = true; 347 requestLayout(); 348 } 349 350 @Override 351 public void setScaleX(float scaleX) { 352 super.setScaleX(scaleX); 353 if (isReordering(true)) { 354 float[] p = mapPointFromParentToView(this, mParentDownMotionX, mParentDownMotionY); 355 mLastMotionX = p[0]; 356 mLastMotionY = p[1]; 357 updateDragViewTranslationDuringDrag(); 358 } 359 } 360 361 // Convenience methods to get the actual width/height of the PagedView (since it is measured 362 // to be larger to account for the minimum possible scale) 363 int getViewportWidth() { 364 return mViewport.width(); 365 } 366 int getViewportHeight() { 367 return mViewport.height(); 368 } 369 370 // Convenience methods to get the offset ASSUMING that we are centering the pages in the 371 // PagedView both horizontally and vertically 372 int getViewportOffsetX() { 373 return (getMeasuredWidth() - getViewportWidth()) / 2; 374 } 375 376 int getViewportOffsetY() { 377 return (getMeasuredHeight() - getViewportHeight()) / 2; 378 } 379 380 PageIndicator getPageIndicator() { 381 return mPageIndicator; 382 } 383 protected PageIndicator.PageMarkerResources getPageIndicatorMarker(int pageIndex) { 384 return new PageIndicator.PageMarkerResources(); 385 } 386 387 /** 388 * Add a page change listener which will be called when a page is _finished_ listening. 389 * 390 */ 391 public void setPageSwitchListener(PageSwitchListener pageSwitchListener) { 392 mPageSwitchListener = pageSwitchListener; 393 if (mPageSwitchListener != null) { 394 mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage); 395 } 396 } 397 398 /** 399 * Returns the index of the currently displayed page. 400 */ 401 public int getCurrentPage() { 402 return mCurrentPage; 403 } 404 405 /** 406 * Returns the index of page to be shown immediately afterwards. 407 */ 408 int getNextPage() { 409 return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; 410 } 411 412 int getPageCount() { 413 return getChildCount(); 414 } 415 416 public View getPageAt(int index) { 417 return getChildAt(index); 418 } 419 420 protected int indexToPage(int index) { 421 return index; 422 } 423 424 /** 425 * Updates the scroll of the current page immediately to its final scroll position. We use this 426 * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of 427 * the previous tab page. 428 */ 429 protected void updateCurrentPageScroll() { 430 // If the current page is invalid, just reset the scroll position to zero 431 int newX = 0; 432 if (0 <= mCurrentPage && mCurrentPage < getPageCount()) { 433 newX = getScrollForPage(mCurrentPage); 434 } 435 scrollTo(newX, 0); 436 mScroller.setFinalX(newX); 437 forceFinishScroller(); 438 } 439 440 private void abortScrollerAnimation(boolean resetNextPage) { 441 mScroller.abortAnimation(); 442 // We need to clean up the next page here to avoid computeScrollHelper from 443 // updating current page on the pass. 444 if (resetNextPage) { 445 mNextPage = INVALID_PAGE; 446 } 447 } 448 449 private void forceFinishScroller() { 450 mScroller.forceFinished(true); 451 // We need to clean up the next page here to avoid computeScrollHelper from 452 // updating current page on the pass. 453 mNextPage = INVALID_PAGE; 454 } 455 456 private int validateNewPage(int newPage) { 457 int validatedPage = newPage; 458 // When in free scroll mode, we need to clamp to the free scroll page range. 459 if (mFreeScroll) { 460 getFreeScrollPageRange(mTempVisiblePagesRange); 461 validatedPage = Math.max(mTempVisiblePagesRange[0], 462 Math.min(newPage, mTempVisiblePagesRange[1])); 463 } 464 // Ensure that it is clamped by the actual set of children in all cases 465 validatedPage = Math.max(0, Math.min(validatedPage, getPageCount() - 1)); 466 return validatedPage; 467 } 468 469 /** 470 * Sets the current page. 471 */ 472 public void setCurrentPage(int currentPage) { 473 if (!mScroller.isFinished()) { 474 abortScrollerAnimation(true); 475 } 476 // don't introduce any checks like mCurrentPage == currentPage here-- if we change the 477 // the default 478 if (getChildCount() == 0) { 479 return; 480 } 481 mForceScreenScrolled = true; 482 mCurrentPage = validateNewPage(currentPage); 483 updateCurrentPageScroll(); 484 notifyPageSwitchListener(); 485 invalidate(); 486 } 487 488 /** 489 * The restore page will be set in place of the current page at the next (likely first) 490 * layout. 491 */ 492 void setRestorePage(int restorePage) { 493 mRestorePage = restorePage; 494 } 495 int getRestorePage() { 496 return mRestorePage; 497 } 498 499 /** 500 * Should be called whenever the page changes. In the case of a scroll, we wait until the page 501 * has settled. 502 */ 503 protected void notifyPageSwitchListener() { 504 if (mPageSwitchListener != null) { 505 mPageSwitchListener.onPageSwitch(getPageAt(getNextPage()), getNextPage()); 506 } 507 508 updatePageIndicator(); 509 } 510 511 private void updatePageIndicator() { 512 // Update the page indicator (when we aren't reordering) 513 if (mPageIndicator != null) { 514 mPageIndicator.setContentDescription(getPageIndicatorDescription()); 515 if (!isReordering(false)) { 516 mPageIndicator.setActiveMarker(getNextPage()); 517 } 518 } 519 } 520 protected void pageBeginMoving() { 521 if (!mIsPageMoving) { 522 mIsPageMoving = true; 523 onPageBeginMoving(); 524 } 525 } 526 527 protected void pageEndMoving() { 528 if (mIsPageMoving) { 529 mIsPageMoving = false; 530 onPageEndMoving(); 531 } 532 } 533 534 protected boolean isPageMoving() { 535 return mIsPageMoving; 536 } 537 538 // a method that subclasses can override to add behavior 539 protected void onPageBeginMoving() { 540 } 541 542 // a method that subclasses can override to add behavior 543 protected void onPageEndMoving() { 544 mWasInOverscroll = false; 545 } 546 547 /** 548 * Registers the specified listener on each page contained in this workspace. 549 * 550 * @param l The listener used to respond to long clicks. 551 */ 552 @Override 553 public void setOnLongClickListener(OnLongClickListener l) { 554 mLongClickListener = l; 555 final int count = getPageCount(); 556 for (int i = 0; i < count; i++) { 557 getPageAt(i).setOnLongClickListener(l); 558 } 559 super.setOnLongClickListener(l); 560 } 561 562 @Override 563 public void scrollBy(int x, int y) { 564 scrollTo(getScrollX() + x, getScrollY() + y); 565 } 566 567 @Override 568 public void scrollTo(int x, int y) { 569 // In free scroll mode, we clamp the scrollX 570 if (mFreeScroll) { 571 // If the scroller is trying to move to a location beyond the maximum allowed 572 // in the free scroll mode, we make sure to end the scroll operation. 573 if (!mScroller.isFinished() && 574 (x > mFreeScrollMaxScrollX || x < mFreeScrollMinScrollX)) { 575 forceFinishScroller(); 576 } 577 578 x = Math.min(x, mFreeScrollMaxScrollX); 579 x = Math.max(x, mFreeScrollMinScrollX); 580 } 581 582 boolean isXBeforeFirstPage = mIsRtl ? (x > mMaxScrollX) : (x < 0); 583 boolean isXAfterLastPage = mIsRtl ? (x < 0) : (x > mMaxScrollX); 584 if (isXBeforeFirstPage) { 585 super.scrollTo(mIsRtl ? mMaxScrollX : 0, y); 586 if (mAllowOverScroll) { 587 mWasInOverscroll = true; 588 if (mIsRtl) { 589 overScroll(x - mMaxScrollX); 590 } else { 591 overScroll(x); 592 } 593 } 594 } else if (isXAfterLastPage) { 595 super.scrollTo(mIsRtl ? 0 : mMaxScrollX, y); 596 if (mAllowOverScroll) { 597 mWasInOverscroll = true; 598 if (mIsRtl) { 599 overScroll(x); 600 } else { 601 overScroll(x - mMaxScrollX); 602 } 603 } 604 } else { 605 if (mWasInOverscroll) { 606 overScroll(0); 607 mWasInOverscroll = false; 608 } 609 super.scrollTo(x, y); 610 } 611 612 mTouchX = x; 613 mSmoothingTime = System.nanoTime() / NANOTIME_DIV; 614 615 // Update the last motion events when scrolling 616 if (isReordering(true)) { 617 float[] p = mapPointFromParentToView(this, mParentDownMotionX, mParentDownMotionY); 618 mLastMotionX = p[0]; 619 mLastMotionY = p[1]; 620 updateDragViewTranslationDuringDrag(); 621 } 622 } 623 624 private void sendScrollAccessibilityEvent() { 625 AccessibilityManager am = 626 (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 627 if (am.isEnabled()) { 628 if (mCurrentPage != getNextPage()) { 629 AccessibilityEvent ev = 630 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED); 631 ev.setScrollable(true); 632 ev.setScrollX(getScrollX()); 633 ev.setScrollY(getScrollY()); 634 ev.setMaxScrollX(mMaxScrollX); 635 ev.setMaxScrollY(0); 636 637 sendAccessibilityEventUnchecked(ev); 638 } 639 } 640 } 641 642 // we moved this functionality to a helper function so SmoothPagedView can reuse it 643 protected boolean computeScrollHelper() { 644 if (mScroller.computeScrollOffset()) { 645 // Don't bother scrolling if the page does not need to be moved 646 if (getScrollX() != mScroller.getCurrX() 647 || getScrollY() != mScroller.getCurrY()) { 648 float scaleX = mFreeScroll ? getScaleX() : 1f; 649 int scrollX = (int) (mScroller.getCurrX() * (1 / scaleX)); 650 scrollTo(scrollX, mScroller.getCurrY()); 651 } 652 invalidate(); 653 return true; 654 } else if (mNextPage != INVALID_PAGE) { 655 sendScrollAccessibilityEvent(); 656 657 mCurrentPage = validateNewPage(mNextPage); 658 mNextPage = INVALID_PAGE; 659 notifyPageSwitchListener(); 660 661 // We don't want to trigger a page end moving unless the page has settled 662 // and the user has stopped scrolling 663 if (mTouchState == TOUCH_STATE_REST) { 664 pageEndMoving(); 665 } 666 667 onPostReorderingAnimationCompleted(); 668 AccessibilityManager am = (AccessibilityManager) 669 getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 670 if (am.isEnabled()) { 671 // Notify the user when the page changes 672 announceForAccessibility(getCurrentPageDescription()); 673 } 674 return true; 675 } 676 return false; 677 } 678 679 @Override 680 public void computeScroll() { 681 computeScrollHelper(); 682 } 683 684 public static class LayoutParams extends ViewGroup.LayoutParams { 685 public boolean isFullScreenPage = false; 686 687 /** 688 * {@inheritDoc} 689 */ 690 public LayoutParams(int width, int height) { 691 super(width, height); 692 } 693 694 public LayoutParams(Context context, AttributeSet attrs) { 695 super(context, attrs); 696 } 697 698 public LayoutParams(ViewGroup.LayoutParams source) { 699 super(source); 700 } 701 } 702 703 @Override 704 public LayoutParams generateLayoutParams(AttributeSet attrs) { 705 return new LayoutParams(getContext(), attrs); 706 } 707 708 @Override 709 protected LayoutParams generateDefaultLayoutParams() { 710 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 711 } 712 713 @Override 714 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 715 return new LayoutParams(p); 716 } 717 718 @Override 719 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 720 return p instanceof LayoutParams; 721 } 722 723 public void addFullScreenPage(View page) { 724 LayoutParams lp = generateDefaultLayoutParams(); 725 lp.isFullScreenPage = true; 726 super.addView(page, 0, lp); 727 } 728 729 public int getNormalChildHeight() { 730 return mNormalChildHeight; 731 } 732 733 @Override 734 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 735 if (getChildCount() == 0) { 736 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 737 return; 738 } 739 740 // We measure the dimensions of the PagedView to be larger than the pages so that when we 741 // zoom out (and scale down), the view is still contained in the parent 742 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 743 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 744 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 745 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 746 // NOTE: We multiply by 2f to account for the fact that depending on the offset of the 747 // viewport, we can be at most one and a half screens offset once we scale down 748 DisplayMetrics dm = getResources().getDisplayMetrics(); 749 int maxSize = Math.max(dm.widthPixels + mInsets.left + mInsets.right, 750 dm.heightPixels + mInsets.top + mInsets.bottom); 751 752 int parentWidthSize = (int) (2f * maxSize); 753 int parentHeightSize = (int) (2f * maxSize); 754 int scaledWidthSize, scaledHeightSize; 755 if (mUseMinScale) { 756 scaledWidthSize = (int) (parentWidthSize / mMinScale); 757 scaledHeightSize = (int) (parentHeightSize / mMinScale); 758 } else { 759 scaledWidthSize = widthSize; 760 scaledHeightSize = heightSize; 761 } 762 mViewport.set(0, 0, widthSize, heightSize); 763 764 if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) { 765 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 766 return; 767 } 768 769 // Return early if we aren't given a proper dimension 770 if (widthSize <= 0 || heightSize <= 0) { 771 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 772 return; 773 } 774 775 /* Allow the height to be set as WRAP_CONTENT. This allows the particular case 776 * of the All apps view on XLarge displays to not take up more space then it needs. Width 777 * is still not allowed to be set as WRAP_CONTENT since many parts of the code expect 778 * each page to have the same width. 779 */ 780 final int verticalPadding = getPaddingTop() + getPaddingBottom(); 781 final int horizontalPadding = getPaddingLeft() + getPaddingRight(); 782 783 int referenceChildWidth = 0; 784 // The children are given the same width and height as the workspace 785 // unless they were set to WRAP_CONTENT 786 if (DEBUG) Log.d(TAG, "PagedView.onMeasure(): " + widthSize + ", " + heightSize); 787 if (DEBUG) Log.d(TAG, "PagedView.scaledSize: " + scaledWidthSize + ", " + scaledHeightSize); 788 if (DEBUG) Log.d(TAG, "PagedView.parentSize: " + parentWidthSize + ", " + parentHeightSize); 789 if (DEBUG) Log.d(TAG, "PagedView.horizontalPadding: " + horizontalPadding); 790 if (DEBUG) Log.d(TAG, "PagedView.verticalPadding: " + verticalPadding); 791 final int childCount = getChildCount(); 792 for (int i = 0; i < childCount; i++) { 793 // disallowing padding in paged view (just pass 0) 794 final View child = getPageAt(i); 795 if (child.getVisibility() != GONE) { 796 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 797 798 int childWidthMode; 799 int childHeightMode; 800 int childWidth; 801 int childHeight; 802 803 if (!lp.isFullScreenPage) { 804 if (lp.width == LayoutParams.WRAP_CONTENT) { 805 childWidthMode = MeasureSpec.AT_MOST; 806 } else { 807 childWidthMode = MeasureSpec.EXACTLY; 808 } 809 810 if (lp.height == LayoutParams.WRAP_CONTENT) { 811 childHeightMode = MeasureSpec.AT_MOST; 812 } else { 813 childHeightMode = MeasureSpec.EXACTLY; 814 } 815 816 childWidth = getViewportWidth() - horizontalPadding 817 - mInsets.left - mInsets.right; 818 childHeight = getViewportHeight() - verticalPadding 819 - mInsets.top - mInsets.bottom; 820 mNormalChildHeight = childHeight; 821 } else { 822 childWidthMode = MeasureSpec.EXACTLY; 823 childHeightMode = MeasureSpec.EXACTLY; 824 825 childWidth = getViewportWidth() - mInsets.left 826 - (mIgnoreRightInset ? mInsets.right : 0); 827 childHeight = getViewportHeight(); 828 } 829 if (referenceChildWidth == 0) { 830 referenceChildWidth = childWidth; 831 } 832 833 final int childWidthMeasureSpec = 834 MeasureSpec.makeMeasureSpec(childWidth, childWidthMode); 835 final int childHeightMeasureSpec = 836 MeasureSpec.makeMeasureSpec(childHeight, childHeightMode); 837 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 838 } 839 } 840 setMeasuredDimension(scaledWidthSize, scaledHeightSize); 841 } 842 843 @Override 844 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 845 if (getChildCount() == 0) { 846 return; 847 } 848 849 if (DEBUG) Log.d(TAG, "PagedView.onLayout()"); 850 final int childCount = getChildCount(); 851 852 int offsetX = getViewportOffsetX(); 853 int offsetY = getViewportOffsetY(); 854 855 // Update the viewport offsets 856 mViewport.offset(offsetX, offsetY); 857 858 final int startIndex = mIsRtl ? childCount - 1 : 0; 859 final int endIndex = mIsRtl ? -1 : childCount; 860 final int delta = mIsRtl ? -1 : 1; 861 862 int verticalPadding = getPaddingTop() + getPaddingBottom(); 863 864 LayoutParams lp = (LayoutParams) getChildAt(startIndex).getLayoutParams(); 865 LayoutParams nextLp; 866 867 int childLeft = offsetX + (lp.isFullScreenPage ? 0 : getPaddingLeft()); 868 if (mPageScrolls == null || childCount != mChildCountOnLastLayout) { 869 mPageScrolls = new int[childCount]; 870 } 871 872 for (int i = startIndex; i != endIndex; i += delta) { 873 final View child = getPageAt(i); 874 if (child.getVisibility() != View.GONE) { 875 lp = (LayoutParams) child.getLayoutParams(); 876 int childTop; 877 if (lp.isFullScreenPage) { 878 childTop = offsetY; 879 } else { 880 childTop = offsetY + getPaddingTop() + mInsets.top; 881 if (mCenterPagesVertically) { 882 childTop += (getViewportHeight() - mInsets.top - mInsets.bottom - verticalPadding - child.getMeasuredHeight()) / 2; 883 } 884 } 885 886 final int childWidth = child.getMeasuredWidth(); 887 final int childHeight = child.getMeasuredHeight(); 888 889 if (DEBUG) Log.d(TAG, "\tlayout-child" + i + ": " + childLeft + ", " + childTop); 890 child.layout(childLeft, childTop, 891 childLeft + child.getMeasuredWidth(), childTop + childHeight); 892 893 int scrollOffsetLeft = lp.isFullScreenPage ? 0 : getPaddingLeft(); 894 mPageScrolls[i] = childLeft - scrollOffsetLeft - offsetX; 895 896 int pageGap = mPageSpacing; 897 int next = i + delta; 898 if (next != endIndex) { 899 nextLp = (LayoutParams) getPageAt(next).getLayoutParams(); 900 } else { 901 nextLp = null; 902 } 903 904 // Prevent full screen pages from showing in the viewport 905 // when they are not the current page. 906 if (lp.isFullScreenPage) { 907 pageGap = getPaddingLeft(); 908 } else if (nextLp != null && nextLp.isFullScreenPage) { 909 pageGap = getPaddingRight(); 910 } 911 912 childLeft += childWidth + pageGap + getChildGap(); 913 } 914 } 915 916 final LayoutTransition transition = getLayoutTransition(); 917 // If the transition is running defer updating max scroll, as some empty pages could 918 // still be present, and a max scroll change could cause sudden jumps in scroll. 919 if (transition != null && transition.isRunning()) { 920 transition.addTransitionListener(new LayoutTransition.TransitionListener() { 921 922 @Override 923 public void startTransition(LayoutTransition transition, ViewGroup container, 924 View view, int transitionType) { } 925 926 @Override 927 public void endTransition(LayoutTransition transition, ViewGroup container, 928 View view, int transitionType) { 929 // Wait until all transitions are complete. 930 if (!transition.isRunning()) { 931 transition.removeTransitionListener(this); 932 updateMaxScrollX(); 933 } 934 } 935 }); 936 } else { 937 updateMaxScrollX(); 938 } 939 940 if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < childCount) { 941 updateCurrentPageScroll(); 942 mFirstLayout = false; 943 } 944 945 if (mScroller.isFinished() && mChildCountOnLastLayout != childCount) { 946 if (mRestorePage != INVALID_RESTORE_PAGE) { 947 setCurrentPage(mRestorePage); 948 mRestorePage = INVALID_RESTORE_PAGE; 949 } else { 950 setCurrentPage(getNextPage()); 951 } 952 } 953 mChildCountOnLastLayout = childCount; 954 955 if (isReordering(true)) { 956 updateDragViewTranslationDuringDrag(); 957 } 958 } 959 960 protected int getChildGap() { 961 return 0; 962 } 963 964 @Thunk void updateMaxScrollX() { 965 int childCount = getChildCount(); 966 if (childCount > 0) { 967 final int index = mIsRtl ? 0 : childCount - 1; 968 mMaxScrollX = getScrollForPage(index); 969 } else { 970 mMaxScrollX = 0; 971 } 972 } 973 974 public void setPageSpacing(int pageSpacing) { 975 mPageSpacing = pageSpacing; 976 requestLayout(); 977 } 978 979 /** 980 * Called when the center screen changes during scrolling. 981 */ 982 protected void screenScrolled(int screenCenter) { } 983 984 @Override 985 public void onChildViewAdded(View parent, View child) { 986 // Update the page indicator, we don't update the page indicator as we 987 // add/remove pages 988 if (mPageIndicator != null && !isReordering(false)) { 989 int pageIndex = indexOfChild(child); 990 mPageIndicator.addMarker(pageIndex, 991 getPageIndicatorMarker(pageIndex), 992 true); 993 } 994 995 // This ensures that when children are added, they get the correct transforms / alphas 996 // in accordance with any scroll effects. 997 mForceScreenScrolled = true; 998 updateFreescrollBounds(); 999 invalidate(); 1000 } 1001 1002 @Override 1003 public void onChildViewRemoved(View parent, View child) { 1004 mForceScreenScrolled = true; 1005 updateFreescrollBounds(); 1006 invalidate(); 1007 } 1008 1009 private void removeMarkerForView(int index) { 1010 // Update the page indicator, we don't update the page indicator as we 1011 // add/remove pages 1012 if (mPageIndicator != null && !isReordering(false)) { 1013 mPageIndicator.removeMarker(index, true); 1014 } 1015 } 1016 1017 @Override 1018 public void removeView(View v) { 1019 // XXX: We should find a better way to hook into this before the view 1020 // gets removed form its parent... 1021 removeMarkerForView(indexOfChild(v)); 1022 super.removeView(v); 1023 } 1024 @Override 1025 public void removeViewInLayout(View v) { 1026 // XXX: We should find a better way to hook into this before the view 1027 // gets removed form its parent... 1028 removeMarkerForView(indexOfChild(v)); 1029 super.removeViewInLayout(v); 1030 } 1031 @Override 1032 public void removeViewAt(int index) { 1033 // XXX: We should find a better way to hook into this before the view 1034 // gets removed form its parent... 1035 removeMarkerForView(index); 1036 super.removeViewAt(index); 1037 } 1038 @Override 1039 public void removeAllViewsInLayout() { 1040 // Update the page indicator, we don't update the page indicator as we 1041 // add/remove pages 1042 if (mPageIndicator != null) { 1043 mPageIndicator.removeAllMarkers(true); 1044 } 1045 1046 super.removeAllViewsInLayout(); 1047 } 1048 1049 protected int getChildOffset(int index) { 1050 if (index < 0 || index > getChildCount() - 1) return 0; 1051 1052 int offset = getPageAt(index).getLeft() - getViewportOffsetX(); 1053 1054 return offset; 1055 } 1056 1057 protected void getFreeScrollPageRange(int[] range) { 1058 range[0] = 0; 1059 range[1] = Math.max(0, getChildCount() - 1); 1060 } 1061 1062 protected void getVisiblePages(int[] range) { 1063 final int pageCount = getChildCount(); 1064 sTmpIntPoint[0] = sTmpIntPoint[1] = 0; 1065 1066 range[0] = -1; 1067 range[1] = -1; 1068 1069 if (pageCount > 0) { 1070 int viewportWidth = getViewportWidth(); 1071 int curScreen = 0; 1072 1073 int count = getChildCount(); 1074 for (int i = 0; i < count; i++) { 1075 View currPage = getPageAt(i); 1076 1077 sTmpIntPoint[0] = 0; 1078 Utilities.getDescendantCoordRelativeToParent(currPage, this, sTmpIntPoint, false); 1079 if (sTmpIntPoint[0] > viewportWidth) { 1080 if (range[0] == -1) { 1081 continue; 1082 } else { 1083 break; 1084 } 1085 } 1086 1087 sTmpIntPoint[0] = currPage.getMeasuredWidth(); 1088 Utilities.getDescendantCoordRelativeToParent(currPage, this, sTmpIntPoint, false); 1089 if (sTmpIntPoint[0] < 0) { 1090 if (range[0] == -1) { 1091 continue; 1092 } else { 1093 break; 1094 } 1095 } 1096 curScreen = i; 1097 if (range[0] < 0) { 1098 range[0] = curScreen; 1099 } 1100 } 1101 1102 range[1] = curScreen; 1103 } else { 1104 range[0] = -1; 1105 range[1] = -1; 1106 } 1107 } 1108 1109 protected boolean shouldDrawChild(View child) { 1110 return child.getVisibility() == VISIBLE; 1111 } 1112 1113 @Override 1114 protected void dispatchDraw(Canvas canvas) { 1115 // Find out which screens are visible; as an optimization we only call draw on them 1116 final int pageCount = getChildCount(); 1117 if (pageCount > 0) { 1118 int halfScreenSize = getViewportWidth() / 2; 1119 int screenCenter = getScrollX() + halfScreenSize; 1120 1121 if (screenCenter != mLastScreenCenter || mForceScreenScrolled) { 1122 // set mForceScreenScrolled before calling screenScrolled so that screenScrolled can 1123 // set it for the next frame 1124 mForceScreenScrolled = false; 1125 screenScrolled(screenCenter); 1126 mLastScreenCenter = screenCenter; 1127 } 1128 1129 getVisiblePages(mTempVisiblePagesRange); 1130 final int leftScreen = mTempVisiblePagesRange[0]; 1131 final int rightScreen = mTempVisiblePagesRange[1]; 1132 if (leftScreen != -1 && rightScreen != -1) { 1133 final long drawingTime = getDrawingTime(); 1134 // Clip to the bounds 1135 canvas.save(); 1136 canvas.clipRect(getScrollX(), getScrollY(), getScrollX() + getRight() - getLeft(), 1137 getScrollY() + getBottom() - getTop()); 1138 1139 // Draw all the children, leaving the drag view for last 1140 for (int i = pageCount - 1; i >= 0; i--) { 1141 final View v = getPageAt(i); 1142 if (v == mDragView) continue; 1143 if (mForceDrawAllChildrenNextFrame || 1144 (leftScreen <= i && i <= rightScreen && shouldDrawChild(v))) { 1145 drawChild(canvas, v, drawingTime); 1146 } 1147 } 1148 // Draw the drag view on top (if there is one) 1149 if (mDragView != null) { 1150 drawChild(canvas, mDragView, drawingTime); 1151 } 1152 1153 mForceDrawAllChildrenNextFrame = false; 1154 canvas.restore(); 1155 } 1156 } 1157 } 1158 1159 @Override 1160 public void draw(Canvas canvas) { 1161 super.draw(canvas); 1162 if (getPageCount() > 0) { 1163 if (!mEdgeGlowLeft.isFinished()) { 1164 final int restoreCount = canvas.save(); 1165 Rect display = mViewport; 1166 canvas.translate(display.left, display.top); 1167 canvas.rotate(270); 1168 1169 getEdgeVerticalPostion(sTmpIntPoint); 1170 canvas.translate(display.top - sTmpIntPoint[1], 0); 1171 mEdgeGlowLeft.setSize(sTmpIntPoint[1] - sTmpIntPoint[0], display.width()); 1172 if (mEdgeGlowLeft.draw(canvas)) { 1173 postInvalidateOnAnimation(); 1174 } 1175 canvas.restoreToCount(restoreCount); 1176 } 1177 if (!mEdgeGlowRight.isFinished()) { 1178 final int restoreCount = canvas.save(); 1179 Rect display = mViewport; 1180 canvas.translate(display.left + mPageScrolls[mIsRtl ? 0 : (getPageCount() - 1)], display.top); 1181 canvas.rotate(90); 1182 1183 getEdgeVerticalPostion(sTmpIntPoint); 1184 1185 int width = mIgnoreRightInset ? (display.width() - mInsets.right) : display.width(); 1186 canvas.translate(sTmpIntPoint[0] - display.top, -width); 1187 mEdgeGlowRight.setSize(sTmpIntPoint[1] - sTmpIntPoint[0], width); 1188 if (mEdgeGlowRight.draw(canvas)) { 1189 postInvalidateOnAnimation(); 1190 } 1191 canvas.restoreToCount(restoreCount); 1192 } 1193 } 1194 } 1195 1196 /** 1197 * Returns the top and bottom position for the edge effect. 1198 */ 1199 protected abstract void getEdgeVerticalPostion(int[] pos); 1200 1201 @Override 1202 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { 1203 int page = indexToPage(indexOfChild(child)); 1204 if (page != mCurrentPage || !mScroller.isFinished()) { 1205 snapToPage(page); 1206 return true; 1207 } 1208 return false; 1209 } 1210 1211 @Override 1212 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 1213 int focusablePage; 1214 if (mNextPage != INVALID_PAGE) { 1215 focusablePage = mNextPage; 1216 } else { 1217 focusablePage = mCurrentPage; 1218 } 1219 View v = getPageAt(focusablePage); 1220 if (v != null) { 1221 return v.requestFocus(direction, previouslyFocusedRect); 1222 } 1223 return false; 1224 } 1225 1226 @Override 1227 public boolean dispatchUnhandledMove(View focused, int direction) { 1228 // XXX-RTL: This will be fixed in a future CL 1229 if (direction == View.FOCUS_LEFT) { 1230 if (getCurrentPage() > 0) { 1231 snapToPage(getCurrentPage() - 1); 1232 return true; 1233 } 1234 } else if (direction == View.FOCUS_RIGHT) { 1235 if (getCurrentPage() < getPageCount() - 1) { 1236 snapToPage(getCurrentPage() + 1); 1237 return true; 1238 } 1239 } 1240 return super.dispatchUnhandledMove(focused, direction); 1241 } 1242 1243 @Override 1244 public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { 1245 // XXX-RTL: This will be fixed in a future CL 1246 if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) { 1247 getPageAt(mCurrentPage).addFocusables(views, direction, focusableMode); 1248 } 1249 if (direction == View.FOCUS_LEFT) { 1250 if (mCurrentPage > 0) { 1251 getPageAt(mCurrentPage - 1).addFocusables(views, direction, focusableMode); 1252 } 1253 } else if (direction == View.FOCUS_RIGHT){ 1254 if (mCurrentPage < getPageCount() - 1) { 1255 getPageAt(mCurrentPage + 1).addFocusables(views, direction, focusableMode); 1256 } 1257 } 1258 } 1259 1260 /** 1261 * If one of our descendant views decides that it could be focused now, only 1262 * pass that along if it's on the current page. 1263 * 1264 * This happens when live folders requery, and if they're off page, they 1265 * end up calling requestFocus, which pulls it on page. 1266 */ 1267 @Override 1268 public void focusableViewAvailable(View focused) { 1269 View current = getPageAt(mCurrentPage); 1270 View v = focused; 1271 while (true) { 1272 if (v == current) { 1273 super.focusableViewAvailable(focused); 1274 return; 1275 } 1276 if (v == this) { 1277 return; 1278 } 1279 ViewParent parent = v.getParent(); 1280 if (parent instanceof View) { 1281 v = (View)v.getParent(); 1282 } else { 1283 return; 1284 } 1285 } 1286 } 1287 1288 /** 1289 * {@inheritDoc} 1290 */ 1291 @Override 1292 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 1293 if (disallowIntercept) { 1294 // We need to make sure to cancel our long press if 1295 // a scrollable widget takes over touch events 1296 final View currentPage = getPageAt(mCurrentPage); 1297 currentPage.cancelLongPress(); 1298 } 1299 super.requestDisallowInterceptTouchEvent(disallowIntercept); 1300 } 1301 1302 /** 1303 * Return true if a tap at (x, y) should trigger a flip to the previous page. 1304 */ 1305 protected boolean hitsPreviousPage(float x, float y) { 1306 if (mIsRtl) { 1307 return (x > (getViewportOffsetX() + getViewportWidth() - 1308 getPaddingRight() - mPageSpacing)); 1309 } 1310 return (x < getViewportOffsetX() + getPaddingLeft() + mPageSpacing); 1311 } 1312 1313 /** 1314 * Return true if a tap at (x, y) should trigger a flip to the next page. 1315 */ 1316 protected boolean hitsNextPage(float x, float y) { 1317 if (mIsRtl) { 1318 return (x < getViewportOffsetX() + getPaddingLeft() + mPageSpacing); 1319 } 1320 return (x > (getViewportOffsetX() + getViewportWidth() - 1321 getPaddingRight() - mPageSpacing)); 1322 } 1323 1324 /** Returns whether x and y originated within the buffered viewport */ 1325 private boolean isTouchPointInViewportWithBuffer(int x, int y) { 1326 sTmpRect.set(mViewport.left - mViewport.width() / 2, mViewport.top, 1327 mViewport.right + mViewport.width() / 2, mViewport.bottom); 1328 return sTmpRect.contains(x, y); 1329 } 1330 1331 @Override 1332 public boolean onInterceptTouchEvent(MotionEvent ev) { 1333 /* 1334 * This method JUST determines whether we want to intercept the motion. 1335 * If we return true, onTouchEvent will be called and we do the actual 1336 * scrolling there. 1337 */ 1338 acquireVelocityTrackerAndAddMovement(ev); 1339 1340 // Skip touch handling if there are no pages to swipe 1341 if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev); 1342 1343 /* 1344 * Shortcut the most recurring case: the user is in the dragging 1345 * state and he is moving his finger. We want to intercept this 1346 * motion. 1347 */ 1348 final int action = ev.getAction(); 1349 if ((action == MotionEvent.ACTION_MOVE) && 1350 (mTouchState == TOUCH_STATE_SCROLLING)) { 1351 return true; 1352 } 1353 1354 switch (action & MotionEvent.ACTION_MASK) { 1355 case MotionEvent.ACTION_MOVE: { 1356 /* 1357 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1358 * whether the user has moved far enough from his original down touch. 1359 */ 1360 if (mActivePointerId != INVALID_POINTER) { 1361 determineScrollingStart(ev); 1362 } 1363 // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN 1364 // event. in that case, treat the first occurence of a move event as a ACTION_DOWN 1365 // i.e. fall through to the next case (don't break) 1366 // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events 1367 // while it's small- this was causing a crash before we checked for INVALID_POINTER) 1368 break; 1369 } 1370 1371 case MotionEvent.ACTION_DOWN: { 1372 final float x = ev.getX(); 1373 final float y = ev.getY(); 1374 // Remember location of down touch 1375 mDownMotionX = x; 1376 mDownMotionY = y; 1377 mDownScrollX = getScrollX(); 1378 mLastMotionX = x; 1379 mLastMotionY = y; 1380 float[] p = mapPointFromViewToParent(this, x, y); 1381 mParentDownMotionX = p[0]; 1382 mParentDownMotionY = p[1]; 1383 mLastMotionXRemainder = 0; 1384 mTotalMotionX = 0; 1385 mActivePointerId = ev.getPointerId(0); 1386 1387 /* 1388 * If being flinged and user touches the screen, initiate drag; 1389 * otherwise don't. mScroller.isFinished should be false when 1390 * being flinged. 1391 */ 1392 final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX()); 1393 final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop / 3); 1394 1395 if (finishedScrolling) { 1396 mTouchState = TOUCH_STATE_REST; 1397 if (!mScroller.isFinished() && !mFreeScroll) { 1398 setCurrentPage(getNextPage()); 1399 pageEndMoving(); 1400 } 1401 } else { 1402 if (isTouchPointInViewportWithBuffer((int) mDownMotionX, (int) mDownMotionY)) { 1403 mTouchState = TOUCH_STATE_SCROLLING; 1404 } else { 1405 mTouchState = TOUCH_STATE_REST; 1406 } 1407 } 1408 1409 break; 1410 } 1411 1412 case MotionEvent.ACTION_UP: 1413 case MotionEvent.ACTION_CANCEL: 1414 resetTouchState(); 1415 break; 1416 1417 case MotionEvent.ACTION_POINTER_UP: 1418 onSecondaryPointerUp(ev); 1419 releaseVelocityTracker(); 1420 break; 1421 } 1422 1423 /* 1424 * The only time we want to intercept motion events is if we are in the 1425 * drag mode. 1426 */ 1427 return mTouchState != TOUCH_STATE_REST; 1428 } 1429 1430 protected void determineScrollingStart(MotionEvent ev) { 1431 determineScrollingStart(ev, 1.0f); 1432 } 1433 1434 /* 1435 * Determines if we should change the touch state to start scrolling after the 1436 * user moves their touch point too far. 1437 */ 1438 protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) { 1439 // Disallow scrolling if we don't have a valid pointer index 1440 final int pointerIndex = ev.findPointerIndex(mActivePointerId); 1441 if (pointerIndex == -1) return; 1442 1443 // Disallow scrolling if we started the gesture from outside the viewport 1444 final float x = ev.getX(pointerIndex); 1445 final float y = ev.getY(pointerIndex); 1446 if (!isTouchPointInViewportWithBuffer((int) x, (int) y)) return; 1447 1448 final int xDiff = (int) Math.abs(x - mLastMotionX); 1449 1450 final int touchSlop = Math.round(touchSlopScale * mTouchSlop); 1451 boolean xMoved = xDiff > touchSlop; 1452 1453 if (xMoved) { 1454 // Scroll if the user moved far enough along the X axis 1455 mTouchState = TOUCH_STATE_SCROLLING; 1456 mTotalMotionX += Math.abs(mLastMotionX - x); 1457 mLastMotionX = x; 1458 mLastMotionXRemainder = 0; 1459 mTouchX = getViewportOffsetX() + getScrollX(); 1460 mSmoothingTime = System.nanoTime() / NANOTIME_DIV; 1461 onScrollInteractionBegin(); 1462 pageBeginMoving(); 1463 } 1464 } 1465 1466 protected void cancelCurrentPageLongPress() { 1467 // Try canceling the long press. It could also have been scheduled 1468 // by a distant descendant, so use the mAllowLongPress flag to block 1469 // everything 1470 final View currentPage = getPageAt(mCurrentPage); 1471 if (currentPage != null) { 1472 currentPage.cancelLongPress(); 1473 } 1474 } 1475 1476 protected float getScrollProgress(int screenCenter, View v, int page) { 1477 final int halfScreenSize = getViewportWidth() / 2; 1478 1479 int delta = screenCenter - (getScrollForPage(page) + halfScreenSize); 1480 int count = getChildCount(); 1481 1482 final int totalDistance; 1483 1484 int adjacentPage = page + 1; 1485 if ((delta < 0 && !mIsRtl) || (delta > 0 && mIsRtl)) { 1486 adjacentPage = page - 1; 1487 } 1488 1489 if (adjacentPage < 0 || adjacentPage > count - 1) { 1490 totalDistance = v.getMeasuredWidth() + mPageSpacing; 1491 } else { 1492 totalDistance = Math.abs(getScrollForPage(adjacentPage) - getScrollForPage(page)); 1493 } 1494 1495 float scrollProgress = delta / (totalDistance * 1.0f); 1496 scrollProgress = Math.min(scrollProgress, MAX_SCROLL_PROGRESS); 1497 scrollProgress = Math.max(scrollProgress, - MAX_SCROLL_PROGRESS); 1498 return scrollProgress; 1499 } 1500 1501 public int getScrollForPage(int index) { 1502 if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) { 1503 return 0; 1504 } else { 1505 return mPageScrolls[index]; 1506 } 1507 } 1508 1509 // While layout transitions are occurring, a child's position may stray from its baseline 1510 // position. This method returns the magnitude of this stray at any given time. 1511 public int getLayoutTransitionOffsetForPage(int index) { 1512 if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) { 1513 return 0; 1514 } else { 1515 View child = getChildAt(index); 1516 1517 int scrollOffset = 0; 1518 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1519 if (!lp.isFullScreenPage) { 1520 scrollOffset = mIsRtl ? getPaddingRight() : getPaddingLeft(); 1521 } 1522 1523 int baselineX = mPageScrolls[index] + scrollOffset + getViewportOffsetX(); 1524 return (int) (child.getX() - baselineX); 1525 } 1526 } 1527 1528 protected void dampedOverScroll(float amount) { 1529 int screenSize = getViewportWidth(); 1530 float f = (amount / screenSize); 1531 if (f < 0) { 1532 mEdgeGlowLeft.onPull(-f); 1533 } else if (f > 0) { 1534 mEdgeGlowRight.onPull(f); 1535 } else { 1536 return; 1537 } 1538 invalidate(); 1539 } 1540 1541 protected void overScroll(float amount) { 1542 dampedOverScroll(amount); 1543 } 1544 1545 public void enableFreeScroll() { 1546 setEnableFreeScroll(true); 1547 } 1548 1549 public void disableFreeScroll() { 1550 setEnableFreeScroll(false); 1551 } 1552 1553 void updateFreescrollBounds() { 1554 getFreeScrollPageRange(mTempVisiblePagesRange); 1555 if (mIsRtl) { 1556 mFreeScrollMinScrollX = getScrollForPage(mTempVisiblePagesRange[1]); 1557 mFreeScrollMaxScrollX = getScrollForPage(mTempVisiblePagesRange[0]); 1558 } else { 1559 mFreeScrollMinScrollX = getScrollForPage(mTempVisiblePagesRange[0]); 1560 mFreeScrollMaxScrollX = getScrollForPage(mTempVisiblePagesRange[1]); 1561 } 1562 } 1563 1564 private void setEnableFreeScroll(boolean freeScroll) { 1565 mFreeScroll = freeScroll; 1566 1567 if (mFreeScroll) { 1568 updateFreescrollBounds(); 1569 getFreeScrollPageRange(mTempVisiblePagesRange); 1570 if (getCurrentPage() < mTempVisiblePagesRange[0]) { 1571 setCurrentPage(mTempVisiblePagesRange[0]); 1572 } else if (getCurrentPage() > mTempVisiblePagesRange[1]) { 1573 setCurrentPage(mTempVisiblePagesRange[1]); 1574 } 1575 } 1576 1577 setEnableOverscroll(!freeScroll); 1578 } 1579 1580 protected void setEnableOverscroll(boolean enable) { 1581 mAllowOverScroll = enable; 1582 } 1583 1584 private int getNearestHoverOverPageIndex() { 1585 if (mDragView != null) { 1586 int dragX = (int) (mDragView.getLeft() + (mDragView.getMeasuredWidth() / 2) 1587 + mDragView.getTranslationX()); 1588 getFreeScrollPageRange(mTempVisiblePagesRange); 1589 int minDistance = Integer.MAX_VALUE; 1590 int minIndex = indexOfChild(mDragView); 1591 for (int i = mTempVisiblePagesRange[0]; i <= mTempVisiblePagesRange[1]; i++) { 1592 View page = getPageAt(i); 1593 int pageX = (int) (page.getLeft() + page.getMeasuredWidth() / 2); 1594 int d = Math.abs(dragX - pageX); 1595 if (d < minDistance) { 1596 minIndex = i; 1597 minDistance = d; 1598 } 1599 } 1600 return minIndex; 1601 } 1602 return -1; 1603 } 1604 1605 @Override 1606 public boolean onTouchEvent(MotionEvent ev) { 1607 super.onTouchEvent(ev); 1608 1609 // Skip touch handling if there are no pages to swipe 1610 if (getChildCount() <= 0) return super.onTouchEvent(ev); 1611 1612 acquireVelocityTrackerAndAddMovement(ev); 1613 1614 final int action = ev.getAction(); 1615 1616 switch (action & MotionEvent.ACTION_MASK) { 1617 case MotionEvent.ACTION_DOWN: 1618 /* 1619 * If being flinged and user touches, stop the fling. isFinished 1620 * will be false if being flinged. 1621 */ 1622 if (!mScroller.isFinished()) { 1623 abortScrollerAnimation(false); 1624 } 1625 1626 // Remember where the motion event started 1627 mDownMotionX = mLastMotionX = ev.getX(); 1628 mDownMotionY = mLastMotionY = ev.getY(); 1629 mDownScrollX = getScrollX(); 1630 float[] p = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY); 1631 mParentDownMotionX = p[0]; 1632 mParentDownMotionY = p[1]; 1633 mLastMotionXRemainder = 0; 1634 mTotalMotionX = 0; 1635 mActivePointerId = ev.getPointerId(0); 1636 1637 if (mTouchState == TOUCH_STATE_SCROLLING) { 1638 onScrollInteractionBegin(); 1639 pageBeginMoving(); 1640 } 1641 break; 1642 1643 case MotionEvent.ACTION_MOVE: 1644 if (mTouchState == TOUCH_STATE_SCROLLING) { 1645 // Scroll to follow the motion event 1646 final int pointerIndex = ev.findPointerIndex(mActivePointerId); 1647 1648 if (pointerIndex == -1) return true; 1649 1650 final float x = ev.getX(pointerIndex); 1651 final float deltaX = mLastMotionX + mLastMotionXRemainder - x; 1652 1653 mTotalMotionX += Math.abs(deltaX); 1654 1655 // Only scroll and update mLastMotionX if we have moved some discrete amount. We 1656 // keep the remainder because we are actually testing if we've moved from the last 1657 // scrolled position (which is discrete). 1658 if (Math.abs(deltaX) >= 1.0f) { 1659 mTouchX += deltaX; 1660 mSmoothingTime = System.nanoTime() / NANOTIME_DIV; 1661 scrollBy((int) deltaX, 0); 1662 mLastMotionX = x; 1663 mLastMotionXRemainder = deltaX - (int) deltaX; 1664 } else { 1665 awakenScrollBars(); 1666 } 1667 } else if (mTouchState == TOUCH_STATE_REORDERING) { 1668 // Update the last motion position 1669 mLastMotionX = ev.getX(); 1670 mLastMotionY = ev.getY(); 1671 1672 // Update the parent down so that our zoom animations take this new movement into 1673 // account 1674 float[] pt = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY); 1675 mParentDownMotionX = pt[0]; 1676 mParentDownMotionY = pt[1]; 1677 updateDragViewTranslationDuringDrag(); 1678 1679 // Find the closest page to the touch point 1680 final int dragViewIndex = indexOfChild(mDragView); 1681 1682 if (DEBUG) Log.d(TAG, "mLastMotionX: " + mLastMotionX); 1683 if (DEBUG) Log.d(TAG, "mLastMotionY: " + mLastMotionY); 1684 if (DEBUG) Log.d(TAG, "mParentDownMotionX: " + mParentDownMotionX); 1685 if (DEBUG) Log.d(TAG, "mParentDownMotionY: " + mParentDownMotionY); 1686 1687 final int pageUnderPointIndex = getNearestHoverOverPageIndex(); 1688 if (pageUnderPointIndex > -1 && pageUnderPointIndex != indexOfChild(mDragView)) { 1689 mTempVisiblePagesRange[0] = 0; 1690 mTempVisiblePagesRange[1] = getPageCount() - 1; 1691 getFreeScrollPageRange(mTempVisiblePagesRange); 1692 if (mTempVisiblePagesRange[0] <= pageUnderPointIndex && 1693 pageUnderPointIndex <= mTempVisiblePagesRange[1] && 1694 pageUnderPointIndex != mSidePageHoverIndex && mScroller.isFinished()) { 1695 mSidePageHoverIndex = pageUnderPointIndex; 1696 mSidePageHoverRunnable = new Runnable() { 1697 @Override 1698 public void run() { 1699 // Setup the scroll to the correct page before we swap the views 1700 snapToPage(pageUnderPointIndex); 1701 1702 // For each of the pages between the paged view and the drag view, 1703 // animate them from the previous position to the new position in 1704 // the layout (as a result of the drag view moving in the layout) 1705 int shiftDelta = (dragViewIndex < pageUnderPointIndex) ? -1 : 1; 1706 int lowerIndex = (dragViewIndex < pageUnderPointIndex) ? 1707 dragViewIndex + 1 : pageUnderPointIndex; 1708 int upperIndex = (dragViewIndex > pageUnderPointIndex) ? 1709 dragViewIndex - 1 : pageUnderPointIndex; 1710 for (int i = lowerIndex; i <= upperIndex; ++i) { 1711 View v = getChildAt(i); 1712 // dragViewIndex < pageUnderPointIndex, so after we remove the 1713 // drag view all subsequent views to pageUnderPointIndex will 1714 // shift down. 1715 int oldX = getViewportOffsetX() + getChildOffset(i); 1716 int newX = getViewportOffsetX() + getChildOffset(i + shiftDelta); 1717 1718 // Animate the view translation from its old position to its new 1719 // position 1720 AnimatorSet anim = (AnimatorSet) v.getTag(ANIM_TAG_KEY); 1721 if (anim != null) { 1722 anim.cancel(); 1723 } 1724 1725 v.setTranslationX(oldX - newX); 1726 anim = new AnimatorSet(); 1727 anim.setDuration(REORDERING_REORDER_REPOSITION_DURATION); 1728 anim.playTogether( 1729 ObjectAnimator.ofFloat(v, "translationX", 0f)); 1730 anim.start(); 1731 v.setTag(anim); 1732 } 1733 1734 removeView(mDragView); 1735 addView(mDragView, pageUnderPointIndex); 1736 mSidePageHoverIndex = -1; 1737 if (mPageIndicator != null) { 1738 mPageIndicator.setActiveMarker(getNextPage()); 1739 } 1740 } 1741 }; 1742 postDelayed(mSidePageHoverRunnable, REORDERING_SIDE_PAGE_HOVER_TIMEOUT); 1743 } 1744 } else { 1745 removeCallbacks(mSidePageHoverRunnable); 1746 mSidePageHoverIndex = -1; 1747 } 1748 } else { 1749 determineScrollingStart(ev); 1750 } 1751 break; 1752 1753 case MotionEvent.ACTION_UP: 1754 if (mTouchState == TOUCH_STATE_SCROLLING) { 1755 final int activePointerId = mActivePointerId; 1756 final int pointerIndex = ev.findPointerIndex(activePointerId); 1757 final float x = ev.getX(pointerIndex); 1758 final VelocityTracker velocityTracker = mVelocityTracker; 1759 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1760 int velocityX = (int) velocityTracker.getXVelocity(activePointerId); 1761 final int deltaX = (int) (x - mDownMotionX); 1762 final int pageWidth = getPageAt(mCurrentPage).getMeasuredWidth(); 1763 boolean isSignificantMove = Math.abs(deltaX) > pageWidth * 1764 SIGNIFICANT_MOVE_THRESHOLD; 1765 1766 mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x); 1767 1768 boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING && 1769 Math.abs(velocityX) > mFlingThresholdVelocity; 1770 1771 if (!mFreeScroll) { 1772 // In the case that the page is moved far to one direction and then is flung 1773 // in the opposite direction, we use a threshold to determine whether we should 1774 // just return to the starting page, or if we should skip one further. 1775 boolean returnToOriginalPage = false; 1776 if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD && 1777 Math.signum(velocityX) != Math.signum(deltaX) && isFling) { 1778 returnToOriginalPage = true; 1779 } 1780 1781 int finalPage; 1782 // We give flings precedence over large moves, which is why we short-circuit our 1783 // test for a large move if a fling has been registered. That is, a large 1784 // move to the left and fling to the right will register as a fling to the right. 1785 boolean isDeltaXLeft = mIsRtl ? deltaX > 0 : deltaX < 0; 1786 boolean isVelocityXLeft = mIsRtl ? velocityX > 0 : velocityX < 0; 1787 if (((isSignificantMove && !isDeltaXLeft && !isFling) || 1788 (isFling && !isVelocityXLeft)) && mCurrentPage > 0) { 1789 finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1; 1790 snapToPageWithVelocity(finalPage, velocityX); 1791 } else if (((isSignificantMove && isDeltaXLeft && !isFling) || 1792 (isFling && isVelocityXLeft)) && 1793 mCurrentPage < getChildCount() - 1) { 1794 finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1; 1795 snapToPageWithVelocity(finalPage, velocityX); 1796 } else { 1797 snapToDestination(); 1798 } 1799 } else { 1800 if (!mScroller.isFinished()) { 1801 abortScrollerAnimation(true); 1802 } 1803 1804 float scaleX = getScaleX(); 1805 int vX = (int) (-velocityX * scaleX); 1806 int initialScrollX = (int) (getScrollX() * scaleX); 1807 1808 mScroller.setInterpolator(mDefaultInterpolator); 1809 mScroller.fling(initialScrollX, 1810 getScrollY(), vX, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); 1811 invalidate(); 1812 } 1813 onScrollInteractionEnd(); 1814 } else if (mTouchState == TOUCH_STATE_PREV_PAGE) { 1815 // at this point we have not moved beyond the touch slop 1816 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so 1817 // we can just page 1818 int nextPage = Math.max(0, mCurrentPage - 1); 1819 if (nextPage != mCurrentPage) { 1820 snapToPage(nextPage); 1821 } else { 1822 snapToDestination(); 1823 } 1824 } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) { 1825 // at this point we have not moved beyond the touch slop 1826 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so 1827 // we can just page 1828 int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1); 1829 if (nextPage != mCurrentPage) { 1830 snapToPage(nextPage); 1831 } else { 1832 snapToDestination(); 1833 } 1834 } else if (mTouchState == TOUCH_STATE_REORDERING) { 1835 // Update the last motion position 1836 mLastMotionX = ev.getX(); 1837 mLastMotionY = ev.getY(); 1838 1839 // Update the parent down so that our zoom animations take this new movement into 1840 // account 1841 float[] pt = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY); 1842 mParentDownMotionX = pt[0]; 1843 mParentDownMotionY = pt[1]; 1844 updateDragViewTranslationDuringDrag(); 1845 } else { 1846 if (!mCancelTap) { 1847 onUnhandledTap(ev); 1848 } 1849 } 1850 1851 // Remove the callback to wait for the side page hover timeout 1852 removeCallbacks(mSidePageHoverRunnable); 1853 // End any intermediate reordering states 1854 resetTouchState(); 1855 break; 1856 1857 case MotionEvent.ACTION_CANCEL: 1858 if (mTouchState == TOUCH_STATE_SCROLLING) { 1859 snapToDestination(); 1860 } 1861 resetTouchState(); 1862 break; 1863 1864 case MotionEvent.ACTION_POINTER_UP: 1865 onSecondaryPointerUp(ev); 1866 releaseVelocityTracker(); 1867 break; 1868 } 1869 1870 return true; 1871 } 1872 1873 private void resetTouchState() { 1874 releaseVelocityTracker(); 1875 endReordering(); 1876 mCancelTap = false; 1877 mTouchState = TOUCH_STATE_REST; 1878 mActivePointerId = INVALID_POINTER; 1879 mEdgeGlowLeft.onRelease(); 1880 mEdgeGlowRight.onRelease(); 1881 } 1882 1883 /** 1884 * Triggered by scrolling via touch 1885 */ 1886 protected void onScrollInteractionBegin() { 1887 } 1888 1889 protected void onScrollInteractionEnd() { 1890 } 1891 1892 protected void onUnhandledTap(MotionEvent ev) { 1893 ((Launcher) getContext()).onClick(this); 1894 } 1895 1896 @Override 1897 public boolean onGenericMotionEvent(MotionEvent event) { 1898 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { 1899 switch (event.getAction()) { 1900 case MotionEvent.ACTION_SCROLL: { 1901 // Handle mouse (or ext. device) by shifting the page depending on the scroll 1902 final float vscroll; 1903 final float hscroll; 1904 if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) { 1905 vscroll = 0; 1906 hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); 1907 } else { 1908 vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); 1909 hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); 1910 } 1911 if (hscroll != 0 || vscroll != 0) { 1912 boolean isForwardScroll = mIsRtl ? (hscroll < 0 || vscroll < 0) 1913 : (hscroll > 0 || vscroll > 0); 1914 if (isForwardScroll) { 1915 scrollRight(); 1916 } else { 1917 scrollLeft(); 1918 } 1919 return true; 1920 } 1921 } 1922 } 1923 } 1924 return super.onGenericMotionEvent(event); 1925 } 1926 1927 private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { 1928 if (mVelocityTracker == null) { 1929 mVelocityTracker = VelocityTracker.obtain(); 1930 } 1931 mVelocityTracker.addMovement(ev); 1932 } 1933 1934 private void releaseVelocityTracker() { 1935 if (mVelocityTracker != null) { 1936 mVelocityTracker.clear(); 1937 mVelocityTracker.recycle(); 1938 mVelocityTracker = null; 1939 } 1940 } 1941 1942 private void onSecondaryPointerUp(MotionEvent ev) { 1943 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 1944 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 1945 final int pointerId = ev.getPointerId(pointerIndex); 1946 if (pointerId == mActivePointerId) { 1947 // This was our active pointer going up. Choose a new 1948 // active pointer and adjust accordingly. 1949 // TODO: Make this decision more intelligent. 1950 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 1951 mLastMotionX = mDownMotionX = ev.getX(newPointerIndex); 1952 mLastMotionY = ev.getY(newPointerIndex); 1953 mLastMotionXRemainder = 0; 1954 mActivePointerId = ev.getPointerId(newPointerIndex); 1955 if (mVelocityTracker != null) { 1956 mVelocityTracker.clear(); 1957 } 1958 } 1959 } 1960 1961 @Override 1962 public void requestChildFocus(View child, View focused) { 1963 super.requestChildFocus(child, focused); 1964 int page = indexToPage(indexOfChild(child)); 1965 if (page >= 0 && page != getCurrentPage() && !isInTouchMode()) { 1966 snapToPage(page); 1967 } 1968 } 1969 1970 int getPageNearestToCenterOfScreen() { 1971 int minDistanceFromScreenCenter = Integer.MAX_VALUE; 1972 int minDistanceFromScreenCenterIndex = -1; 1973 int screenCenter = getViewportOffsetX() + getScrollX() + (getViewportWidth() / 2); 1974 final int childCount = getChildCount(); 1975 for (int i = 0; i < childCount; ++i) { 1976 View layout = (View) getPageAt(i); 1977 int childWidth = layout.getMeasuredWidth(); 1978 int halfChildWidth = (childWidth / 2); 1979 int childCenter = getViewportOffsetX() + getChildOffset(i) + halfChildWidth; 1980 int distanceFromScreenCenter = Math.abs(childCenter - screenCenter); 1981 if (distanceFromScreenCenter < minDistanceFromScreenCenter) { 1982 minDistanceFromScreenCenter = distanceFromScreenCenter; 1983 minDistanceFromScreenCenterIndex = i; 1984 } 1985 } 1986 return minDistanceFromScreenCenterIndex; 1987 } 1988 1989 protected void snapToDestination() { 1990 snapToPage(getPageNearestToCenterOfScreen(), PAGE_SNAP_ANIMATION_DURATION); 1991 } 1992 1993 private static class ScrollInterpolator implements Interpolator { 1994 public ScrollInterpolator() { 1995 } 1996 1997 public float getInterpolation(float t) { 1998 t -= 1.0f; 1999 return t*t*t*t*t + 1; 2000 } 2001 } 2002 2003 // We want the duration of the page snap animation to be influenced by the distance that 2004 // the screen has to travel, however, we don't want this duration to be effected in a 2005 // purely linear fashion. Instead, we use this method to moderate the effect that the distance 2006 // of travel has on the overall snap duration. 2007 private float distanceInfluenceForSnapDuration(float f) { 2008 f -= 0.5f; // center the values about 0. 2009 f *= 0.3f * Math.PI / 2.0f; 2010 return (float) Math.sin(f); 2011 } 2012 2013 protected void snapToPageWithVelocity(int whichPage, int velocity) { 2014 whichPage = validateNewPage(whichPage); 2015 int halfScreenSize = getViewportWidth() / 2; 2016 2017 final int newX = getScrollForPage(whichPage); 2018 int delta = newX - getScrollX(); 2019 int duration = 0; 2020 2021 if (Math.abs(velocity) < mMinFlingVelocity) { 2022 // If the velocity is low enough, then treat this more as an automatic page advance 2023 // as opposed to an apparent physical response to flinging 2024 snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); 2025 return; 2026 } 2027 2028 // Here we compute a "distance" that will be used in the computation of the overall 2029 // snap duration. This is a function of the actual distance that needs to be traveled; 2030 // we keep this value close to half screen size in order to reduce the variance in snap 2031 // duration as a function of the distance the page needs to travel. 2032 float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize)); 2033 float distance = halfScreenSize + halfScreenSize * 2034 distanceInfluenceForSnapDuration(distanceRatio); 2035 2036 velocity = Math.abs(velocity); 2037 velocity = Math.max(mMinSnapVelocity, velocity); 2038 2039 // we want the page's snap velocity to approximately match the velocity at which the 2040 // user flings, so we scale the duration by a value near to the derivative of the scroll 2041 // interpolator at zero, ie. 5. We use 4 to make it a little slower. 2042 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 2043 2044 snapToPage(whichPage, delta, duration); 2045 } 2046 2047 public void snapToPage(int whichPage) { 2048 snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); 2049 } 2050 2051 protected void snapToPageImmediately(int whichPage) { 2052 snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION, true, null); 2053 } 2054 2055 protected void snapToPage(int whichPage, int duration) { 2056 snapToPage(whichPage, duration, false, null); 2057 } 2058 2059 protected void snapToPage(int whichPage, int duration, TimeInterpolator interpolator) { 2060 snapToPage(whichPage, duration, false, interpolator); 2061 } 2062 2063 protected void snapToPage(int whichPage, int duration, boolean immediate, 2064 TimeInterpolator interpolator) { 2065 whichPage = validateNewPage(whichPage); 2066 2067 int newX = getScrollForPage(whichPage); 2068 final int delta = newX - getScrollX(); 2069 snapToPage(whichPage, delta, duration, immediate, interpolator); 2070 } 2071 2072 protected void snapToPage(int whichPage, int delta, int duration) { 2073 snapToPage(whichPage, delta, duration, false, null); 2074 } 2075 2076 protected void snapToPage(int whichPage, int delta, int duration, boolean immediate, 2077 TimeInterpolator interpolator) { 2078 whichPage = validateNewPage(whichPage); 2079 2080 mNextPage = whichPage; 2081 View focusedChild = getFocusedChild(); 2082 if (focusedChild != null && whichPage != mCurrentPage && 2083 focusedChild == getPageAt(mCurrentPage)) { 2084 focusedChild.clearFocus(); 2085 } 2086 2087 pageBeginMoving(); 2088 awakenScrollBars(duration); 2089 if (immediate) { 2090 duration = 0; 2091 } else if (duration == 0) { 2092 duration = Math.abs(delta); 2093 } 2094 2095 if (!mScroller.isFinished()) { 2096 abortScrollerAnimation(false); 2097 } 2098 2099 if (interpolator != null) { 2100 mScroller.setInterpolator(interpolator); 2101 } else { 2102 mScroller.setInterpolator(mDefaultInterpolator); 2103 } 2104 2105 mScroller.startScroll(getScrollX(), 0, delta, 0, duration); 2106 2107 updatePageIndicator(); 2108 2109 // Trigger a compute() to finish switching pages if necessary 2110 if (immediate) { 2111 computeScroll(); 2112 } 2113 2114 mForceScreenScrolled = true; 2115 invalidate(); 2116 } 2117 2118 public void scrollLeft() { 2119 if (getNextPage() > 0) snapToPage(getNextPage() - 1); 2120 } 2121 2122 public void scrollRight() { 2123 if (getNextPage() < getChildCount() -1) snapToPage(getNextPage() + 1); 2124 } 2125 2126 public int getPageForView(View v) { 2127 int result = -1; 2128 if (v != null) { 2129 ViewParent vp = v.getParent(); 2130 int count = getChildCount(); 2131 for (int i = 0; i < count; i++) { 2132 if (vp == getPageAt(i)) { 2133 return i; 2134 } 2135 } 2136 } 2137 return result; 2138 } 2139 2140 @Override 2141 public boolean performLongClick() { 2142 mCancelTap = true; 2143 return super.performLongClick(); 2144 } 2145 2146 public static class SavedState extends BaseSavedState { 2147 int currentPage = -1; 2148 2149 SavedState(Parcelable superState) { 2150 super(superState); 2151 } 2152 2153 @Thunk SavedState(Parcel in) { 2154 super(in); 2155 currentPage = in.readInt(); 2156 } 2157 2158 @Override 2159 public void writeToParcel(Parcel out, int flags) { 2160 super.writeToParcel(out, flags); 2161 out.writeInt(currentPage); 2162 } 2163 2164 public static final Parcelable.Creator<SavedState> CREATOR = 2165 new Parcelable.Creator<SavedState>() { 2166 public SavedState createFromParcel(Parcel in) { 2167 return new SavedState(in); 2168 } 2169 2170 public SavedState[] newArray(int size) { 2171 return new SavedState[size]; 2172 } 2173 }; 2174 } 2175 2176 // Animate the drag view back to the original position 2177 private void animateDragViewToOriginalPosition() { 2178 if (mDragView != null) { 2179 AnimatorSet anim = new AnimatorSet(); 2180 anim.setDuration(REORDERING_DROP_REPOSITION_DURATION); 2181 anim.playTogether( 2182 ObjectAnimator.ofFloat(mDragView, "translationX", 0f), 2183 ObjectAnimator.ofFloat(mDragView, "translationY", 0f), 2184 ObjectAnimator.ofFloat(mDragView, "scaleX", 1f), 2185 ObjectAnimator.ofFloat(mDragView, "scaleY", 1f)); 2186 anim.addListener(new AnimatorListenerAdapter() { 2187 @Override 2188 public void onAnimationEnd(Animator animation) { 2189 onPostReorderingAnimationCompleted(); 2190 } 2191 }); 2192 anim.start(); 2193 } 2194 } 2195 2196 public void onStartReordering() { 2197 // Set the touch state to reordering (allows snapping to pages, dragging a child, etc.) 2198 mTouchState = TOUCH_STATE_REORDERING; 2199 mIsReordering = true; 2200 2201 // We must invalidate to trigger a redraw to update the layers such that the drag view 2202 // is always drawn on top 2203 invalidate(); 2204 } 2205 2206 @Thunk void onPostReorderingAnimationCompleted() { 2207 // Trigger the callback when reordering has settled 2208 --mPostReorderingPreZoomInRemainingAnimationCount; 2209 if (mPostReorderingPreZoomInRunnable != null && 2210 mPostReorderingPreZoomInRemainingAnimationCount == 0) { 2211 mPostReorderingPreZoomInRunnable.run(); 2212 mPostReorderingPreZoomInRunnable = null; 2213 } 2214 } 2215 2216 public void onEndReordering() { 2217 mIsReordering = false; 2218 } 2219 2220 public boolean startReordering(View v) { 2221 int dragViewIndex = indexOfChild(v); 2222 2223 if (mTouchState != TOUCH_STATE_REST || dragViewIndex == -1) return false; 2224 2225 mTempVisiblePagesRange[0] = 0; 2226 mTempVisiblePagesRange[1] = getPageCount() - 1; 2227 getFreeScrollPageRange(mTempVisiblePagesRange); 2228 mReorderingStarted = true; 2229 2230 // Check if we are within the reordering range 2231 if (mTempVisiblePagesRange[0] <= dragViewIndex && 2232 dragViewIndex <= mTempVisiblePagesRange[1]) { 2233 // Find the drag view under the pointer 2234 mDragView = getChildAt(dragViewIndex); 2235 mDragView.animate().scaleX(1.15f).scaleY(1.15f).setDuration(100).start(); 2236 mDragViewBaselineLeft = mDragView.getLeft(); 2237 snapToPage(getPageNearestToCenterOfScreen()); 2238 disableFreeScroll(); 2239 onStartReordering(); 2240 return true; 2241 } 2242 return false; 2243 } 2244 2245 boolean isReordering(boolean testTouchState) { 2246 boolean state = mIsReordering; 2247 if (testTouchState) { 2248 state &= (mTouchState == TOUCH_STATE_REORDERING); 2249 } 2250 return state; 2251 } 2252 void endReordering() { 2253 // For simplicity, we call endReordering sometimes even if reordering was never started. 2254 // In that case, we don't want to do anything. 2255 if (!mReorderingStarted) return; 2256 mReorderingStarted = false; 2257 2258 // If we haven't flung-to-delete the current child, then we just animate the drag view 2259 // back into position 2260 final Runnable onCompleteRunnable = new Runnable() { 2261 @Override 2262 public void run() { 2263 onEndReordering(); 2264 } 2265 }; 2266 2267 mPostReorderingPreZoomInRunnable = new Runnable() { 2268 public void run() { 2269 onCompleteRunnable.run(); 2270 enableFreeScroll(); 2271 }; 2272 }; 2273 2274 mPostReorderingPreZoomInRemainingAnimationCount = 2275 NUM_ANIMATIONS_RUNNING_BEFORE_ZOOM_OUT; 2276 // Snap to the current page 2277 snapToPage(indexOfChild(mDragView), 0); 2278 // Animate the drag view back to the front position 2279 animateDragViewToOriginalPosition(); 2280 } 2281 2282 private static final int ANIM_TAG_KEY = 100; 2283 2284 /* Accessibility */ 2285 @TargetApi(Build.VERSION_CODES.LOLLIPOP) 2286 @Override 2287 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 2288 super.onInitializeAccessibilityNodeInfo(info); 2289 info.setScrollable(getPageCount() > 1); 2290 if (getCurrentPage() < getPageCount() - 1) { 2291 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); 2292 } 2293 if (getCurrentPage() > 0) { 2294 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 2295 } 2296 info.setClassName(getClass().getName()); 2297 2298 // Accessibility-wise, PagedView doesn't support long click, so disabling it. 2299 // Besides disabling the accessibility long-click, this also prevents this view from getting 2300 // accessibility focus. 2301 info.setLongClickable(false); 2302 if (Utilities.ATLEAST_LOLLIPOP) { 2303 info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); 2304 } 2305 } 2306 2307 @Override 2308 public void sendAccessibilityEvent(int eventType) { 2309 // Don't let the view send real scroll events. 2310 if (eventType != AccessibilityEvent.TYPE_VIEW_SCROLLED) { 2311 super.sendAccessibilityEvent(eventType); 2312 } 2313 } 2314 2315 @Override 2316 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 2317 super.onInitializeAccessibilityEvent(event); 2318 event.setScrollable(getPageCount() > 1); 2319 } 2320 2321 @Override 2322 public boolean performAccessibilityAction(int action, Bundle arguments) { 2323 if (super.performAccessibilityAction(action, arguments)) { 2324 return true; 2325 } 2326 switch (action) { 2327 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 2328 if (getCurrentPage() < getPageCount() - 1) { 2329 scrollRight(); 2330 return true; 2331 } 2332 } break; 2333 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 2334 if (getCurrentPage() > 0) { 2335 scrollLeft(); 2336 return true; 2337 } 2338 } break; 2339 } 2340 return false; 2341 } 2342 2343 protected String getCurrentPageDescription() { 2344 return String.format(getContext().getString(R.string.default_scroll_format), 2345 getNextPage() + 1, getChildCount()); 2346 } 2347 2348 @Override 2349 public boolean onHoverEvent(android.view.MotionEvent event) { 2350 return true; 2351 } 2352} 2353