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 17 package com.android.launcher3; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.TimeInterpolator; 24 import android.animation.ValueAnimator; 25 import android.animation.ValueAnimator.AnimatorUpdateListener; 26 import android.content.Context; 27 import android.content.res.TypedArray; 28 import android.graphics.Canvas; 29 import android.graphics.Matrix; 30 import android.graphics.PointF; 31 import android.graphics.Rect; 32 import android.os.Bundle; 33 import android.os.Parcel; 34 import android.os.Parcelable; 35 import android.util.AttributeSet; 36 import android.util.DisplayMetrics; 37 import android.util.Log; 38 import android.view.InputDevice; 39 import android.view.KeyEvent; 40 import android.view.MotionEvent; 41 import android.view.VelocityTracker; 42 import android.view.View; 43 import android.view.ViewConfiguration; 44 import android.view.ViewGroup; 45 import android.view.ViewParent; 46 import android.view.accessibility.AccessibilityEvent; 47 import android.view.accessibility.AccessibilityManager; 48 import android.view.accessibility.AccessibilityNodeInfo; 49 import android.view.animation.AnimationUtils; 50 import android.view.animation.DecelerateInterpolator; 51 import android.view.animation.Interpolator; 52 import android.view.animation.LinearInterpolator; 53 54 import java.util.ArrayList; 55 56 interface Page { getPageChildCount()57 public int getPageChildCount(); getChildOnPageAt(int i)58 public View getChildOnPageAt(int i); removeAllViewsOnPage()59 public void removeAllViewsOnPage(); removeViewOnPageAt(int i)60 public void removeViewOnPageAt(int i); indexOfChildOnPage(View v)61 public int indexOfChildOnPage(View v); 62 } 63 64 /** 65 * An abstraction of the original Workspace which supports browsing through a 66 * sequential list of "pages" 67 */ 68 public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarchyChangeListener { 69 private static final String TAG = "PagedView"; 70 private static final boolean DEBUG = false; 71 protected static final int INVALID_PAGE = -1; 72 73 // the min drag distance for a fling to register, to prevent random page shifts 74 private static final int MIN_LENGTH_FOR_FLING = 25; 75 76 protected static final int PAGE_SNAP_ANIMATION_DURATION = 750; 77 protected static final int OVER_SCROLL_PAGE_SNAP_ANIMATION_DURATION = 350; 78 protected static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950; 79 protected static final float NANOTIME_DIV = 1000000000.0f; 80 81 private static final float OVERSCROLL_ACCELERATE_FACTOR = 2; 82 private static final float OVERSCROLL_DAMP_FACTOR = 0.07f; 83 84 private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f; 85 // The page is moved more than halfway, automatically move to the next page on touch up. 86 private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f; 87 88 // The following constants need to be scaled based on density. The scaled versions will be 89 // assigned to the corresponding member variables below. 90 private static final int FLING_THRESHOLD_VELOCITY = 500; 91 private static final int MIN_SNAP_VELOCITY = 1500; 92 private static final int MIN_FLING_VELOCITY = 250; 93 94 // We are disabling touch interaction of the widget region for factory ROM. 95 private static final boolean DISABLE_TOUCH_INTERACTION = false; 96 private static final boolean DISABLE_TOUCH_SIDE_PAGES = true; 97 private static final boolean DISABLE_FLING_TO_DELETE = true; 98 99 public static final int INVALID_RESTORE_PAGE = -1001; 100 101 private boolean mFreeScroll = false; 102 private int mFreeScrollMinScrollX = -1; 103 private int mFreeScrollMaxScrollX = -1; 104 105 static final int AUTOMATIC_PAGE_SPACING = -1; 106 107 protected int mFlingThresholdVelocity; 108 protected int mMinFlingVelocity; 109 protected int mMinSnapVelocity; 110 111 protected float mDensity; 112 protected float mSmoothingTime; 113 protected float mTouchX; 114 115 protected boolean mFirstLayout = true; 116 private int mNormalChildHeight; 117 118 protected int mCurrentPage; 119 protected int mRestorePage = INVALID_RESTORE_PAGE; 120 protected int mChildCountOnLastLayout; 121 122 protected int mNextPage = INVALID_PAGE; 123 protected int mMaxScrollX; 124 protected LauncherScroller mScroller; 125 private Interpolator mDefaultInterpolator; 126 private VelocityTracker mVelocityTracker; 127 private int mPageSpacing = 0; 128 129 private float mParentDownMotionX; 130 private float mParentDownMotionY; 131 private float mDownMotionX; 132 private float mDownMotionY; 133 private float mDownScrollX; 134 private float mDragViewBaselineLeft; 135 protected float mLastMotionX; 136 protected float mLastMotionXRemainder; 137 protected float mLastMotionY; 138 protected float mTotalMotionX; 139 private int mLastScreenCenter = -1; 140 141 private boolean mCancelTap; 142 143 private int[] mPageScrolls; 144 145 protected final static int TOUCH_STATE_REST = 0; 146 protected final static int TOUCH_STATE_SCROLLING = 1; 147 protected final static int TOUCH_STATE_PREV_PAGE = 2; 148 protected final static int TOUCH_STATE_NEXT_PAGE = 3; 149 protected final static int TOUCH_STATE_REORDERING = 4; 150 151 protected final static float ALPHA_QUANTIZE_LEVEL = 0.0001f; 152 153 protected int mTouchState = TOUCH_STATE_REST; 154 protected boolean mForceScreenScrolled = false; 155 156 protected OnLongClickListener mLongClickListener; 157 158 protected int mTouchSlop; 159 private int mPagingTouchSlop; 160 private int mMaximumVelocity; 161 protected int mPageLayoutWidthGap; 162 protected int mPageLayoutHeightGap; 163 protected int mCellCountX = 0; 164 protected int mCellCountY = 0; 165 protected boolean mCenterPagesVertically; 166 protected boolean mAllowOverScroll = true; 167 protected int mUnboundedScrollX; 168 protected int[] mTempVisiblePagesRange = new int[2]; 169 protected boolean mForceDrawAllChildrenNextFrame; 170 private boolean mSpacePagesAutomatically = false; 171 172 // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. Otherwise 173 // it is equal to the scaled overscroll position. We use a separate value so as to prevent 174 // the screens from continuing to translate beyond the normal bounds. 175 protected int mOverScrollX; 176 177 protected static final int INVALID_POINTER = -1; 178 179 protected int mActivePointerId = INVALID_POINTER; 180 181 private PageSwitchListener mPageSwitchListener; 182 183 protected ArrayList<Boolean> mDirtyPageContent; 184 185 // If true, syncPages and syncPageItems will be called to refresh pages 186 protected boolean mContentIsRefreshable = true; 187 188 // If true, modify alpha of neighboring pages as user scrolls left/right 189 protected boolean mFadeInAdjacentScreens = false; 190 191 // It true, use a different slop parameter (pagingTouchSlop = 2 * touchSlop) for deciding 192 // to switch to a new page 193 protected boolean mUsePagingTouchSlop = true; 194 195 // If true, the subclass should directly update scrollX itself in its computeScroll method 196 // (SmoothPagedView does this) 197 protected boolean mDeferScrollUpdate = false; 198 protected boolean mDeferLoadAssociatedPagesUntilScrollCompletes = false; 199 200 protected boolean mIsPageMoving = false; 201 202 // All syncs and layout passes are deferred until data is ready. 203 protected boolean mIsDataReady = false; 204 205 protected boolean mAllowLongPress = true; 206 207 private boolean mWasInOverscroll = false; 208 209 // Page Indicator 210 private int mPageIndicatorViewId; 211 private PageIndicator mPageIndicator; 212 private boolean mAllowPagedViewAnimations = true; 213 214 // The viewport whether the pages are to be contained (the actual view may be larger than the 215 // viewport) 216 private Rect mViewport = new Rect(); 217 218 // Reordering 219 // We use the min scale to determine how much to expand the actually PagedView measured 220 // dimensions such that when we are zoomed out, the view is not clipped 221 private int REORDERING_DROP_REPOSITION_DURATION = 200; 222 protected int REORDERING_REORDER_REPOSITION_DURATION = 300; 223 protected int REORDERING_ZOOM_IN_OUT_DURATION = 250; 224 private int REORDERING_SIDE_PAGE_HOVER_TIMEOUT = 80; 225 private float mMinScale = 1f; 226 private boolean mUseMinScale = false; 227 protected View mDragView; 228 protected AnimatorSet mZoomInOutAnim; 229 private Runnable mSidePageHoverRunnable; 230 private int mSidePageHoverIndex = -1; 231 // This variable's scope is only for the duration of startReordering() and endReordering() 232 private boolean mReorderingStarted = false; 233 // This variable's scope is for the duration of startReordering() and after the zoomIn() 234 // animation after endReordering() 235 private boolean mIsReordering; 236 // The runnable that settles the page after snapToPage and animateDragViewToOriginalPosition 237 private int NUM_ANIMATIONS_RUNNING_BEFORE_ZOOM_OUT = 2; 238 private int mPostReorderingPreZoomInRemainingAnimationCount; 239 private Runnable mPostReorderingPreZoomInRunnable; 240 241 // Convenience/caching 242 private Matrix mTmpInvMatrix = new Matrix(); 243 private float[] mTmpPoint = new float[2]; 244 private int[] mTmpIntPoint = new int[2]; 245 private Rect mTmpRect = new Rect(); 246 private Rect mAltTmpRect = new Rect(); 247 248 // Fling to delete 249 private int FLING_TO_DELETE_FADE_OUT_DURATION = 350; 250 private float FLING_TO_DELETE_FRICTION = 0.035f; 251 // The degrees specifies how much deviation from the up vector to still consider a fling "up" 252 private float FLING_TO_DELETE_MAX_FLING_DEGREES = 65f; 253 protected int mFlingToDeleteThresholdVelocity = -1400; 254 // Drag to delete 255 private boolean mDeferringForDelete = false; 256 private int DELETE_SLIDE_IN_SIDE_PAGE_DURATION = 250; 257 private int DRAG_TO_DELETE_FADE_OUT_DURATION = 350; 258 259 // Drop to delete 260 private View mDeleteDropTarget; 261 262 // Bouncer 263 private boolean mTopAlignPageWhenShrinkingForBouncer = false; 264 265 protected final Rect mInsets = new Rect(); 266 267 public interface PageSwitchListener { onPageSwitch(View newPage, int newPageIndex)268 void onPageSwitch(View newPage, int newPageIndex); 269 } 270 PagedView(Context context)271 public PagedView(Context context) { 272 this(context, null); 273 } 274 PagedView(Context context, AttributeSet attrs)275 public PagedView(Context context, AttributeSet attrs) { 276 this(context, attrs, 0); 277 } 278 PagedView(Context context, AttributeSet attrs, int defStyle)279 public PagedView(Context context, AttributeSet attrs, int defStyle) { 280 super(context, attrs, defStyle); 281 282 TypedArray a = context.obtainStyledAttributes(attrs, 283 R.styleable.PagedView, defStyle, 0); 284 285 mPageLayoutWidthGap = a.getDimensionPixelSize( 286 R.styleable.PagedView_pageLayoutWidthGap, 0); 287 mPageLayoutHeightGap = a.getDimensionPixelSize( 288 R.styleable.PagedView_pageLayoutHeightGap, 0); 289 mPageIndicatorViewId = a.getResourceId(R.styleable.PagedView_pageIndicator, -1); 290 a.recycle(); 291 292 setHapticFeedbackEnabled(false); 293 init(); 294 } 295 296 /** 297 * Initializes various states for this workspace. 298 */ init()299 protected void init() { 300 mDirtyPageContent = new ArrayList<Boolean>(); 301 mDirtyPageContent.ensureCapacity(32); 302 mScroller = new LauncherScroller(getContext()); 303 setDefaultInterpolator(new ScrollInterpolator()); 304 mCurrentPage = 0; 305 mCenterPagesVertically = true; 306 307 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 308 mTouchSlop = configuration.getScaledPagingTouchSlop(); 309 mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); 310 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 311 mDensity = getResources().getDisplayMetrics().density; 312 313 // Scale the fling-to-delete threshold by the density 314 mFlingToDeleteThresholdVelocity = 315 (int) (mFlingToDeleteThresholdVelocity * mDensity); 316 317 mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); 318 mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * mDensity); 319 mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * mDensity); 320 setOnHierarchyChangeListener(this); 321 } 322 setDefaultInterpolator(Interpolator interpolator)323 protected void setDefaultInterpolator(Interpolator interpolator) { 324 mDefaultInterpolator = interpolator; 325 mScroller.setInterpolator(mDefaultInterpolator); 326 } 327 onAttachedToWindow()328 protected void onAttachedToWindow() { 329 super.onAttachedToWindow(); 330 331 // Hook up the page indicator 332 ViewGroup parent = (ViewGroup) getParent(); 333 ViewGroup grandParent = (ViewGroup) parent.getParent(); 334 if (mPageIndicator == null && mPageIndicatorViewId > -1) { 335 mPageIndicator = (PageIndicator) grandParent.findViewById(mPageIndicatorViewId); 336 mPageIndicator.removeAllMarkers(mAllowPagedViewAnimations); 337 338 ArrayList<PageIndicator.PageMarkerResources> markers = 339 new ArrayList<PageIndicator.PageMarkerResources>(); 340 for (int i = 0; i < getChildCount(); ++i) { 341 markers.add(getPageIndicatorMarker(i)); 342 } 343 344 mPageIndicator.addMarkers(markers, mAllowPagedViewAnimations); 345 346 OnClickListener listener = getPageIndicatorClickListener(); 347 if (listener != null) { 348 mPageIndicator.setOnClickListener(listener); 349 } 350 mPageIndicator.setContentDescription(getPageIndicatorDescription()); 351 } 352 } 353 getPageIndicatorDescription()354 protected String getPageIndicatorDescription() { 355 return getCurrentPageDescription(); 356 } 357 getPageIndicatorClickListener()358 protected OnClickListener getPageIndicatorClickListener() { 359 return null; 360 } 361 onDetachedFromWindow()362 protected void onDetachedFromWindow() { 363 // Unhook the page indicator 364 mPageIndicator = null; 365 } 366 setDeleteDropTarget(View v)367 void setDeleteDropTarget(View v) { 368 mDeleteDropTarget = v; 369 } 370 371 // Convenience methods to map points from self to parent and vice versa mapPointFromViewToParent(View v, float x, float y)372 float[] mapPointFromViewToParent(View v, float x, float y) { 373 mTmpPoint[0] = x; 374 mTmpPoint[1] = y; 375 v.getMatrix().mapPoints(mTmpPoint); 376 mTmpPoint[0] += v.getLeft(); 377 mTmpPoint[1] += v.getTop(); 378 return mTmpPoint; 379 } mapPointFromParentToView(View v, float x, float y)380 float[] mapPointFromParentToView(View v, float x, float y) { 381 mTmpPoint[0] = x - v.getLeft(); 382 mTmpPoint[1] = y - v.getTop(); 383 v.getMatrix().invert(mTmpInvMatrix); 384 mTmpInvMatrix.mapPoints(mTmpPoint); 385 return mTmpPoint; 386 } 387 updateDragViewTranslationDuringDrag()388 void updateDragViewTranslationDuringDrag() { 389 if (mDragView != null) { 390 float x = (mLastMotionX - mDownMotionX) + (getScrollX() - mDownScrollX) + 391 (mDragViewBaselineLeft - mDragView.getLeft()); 392 float y = mLastMotionY - mDownMotionY; 393 mDragView.setTranslationX(x); 394 mDragView.setTranslationY(y); 395 396 if (DEBUG) Log.d(TAG, "PagedView.updateDragViewTranslationDuringDrag(): " 397 + x + ", " + y); 398 } 399 } 400 setMinScale(float f)401 public void setMinScale(float f) { 402 mMinScale = f; 403 mUseMinScale = true; 404 requestLayout(); 405 } 406 407 @Override setScaleX(float scaleX)408 public void setScaleX(float scaleX) { 409 super.setScaleX(scaleX); 410 if (isReordering(true)) { 411 float[] p = mapPointFromParentToView(this, mParentDownMotionX, mParentDownMotionY); 412 mLastMotionX = p[0]; 413 mLastMotionY = p[1]; 414 updateDragViewTranslationDuringDrag(); 415 } 416 } 417 418 // Convenience methods to get the actual width/height of the PagedView (since it is measured 419 // to be larger to account for the minimum possible scale) getViewportWidth()420 int getViewportWidth() { 421 return mViewport.width(); 422 } getViewportHeight()423 int getViewportHeight() { 424 return mViewport.height(); 425 } 426 427 // Convenience methods to get the offset ASSUMING that we are centering the pages in the 428 // PagedView both horizontally and vertically getViewportOffsetX()429 int getViewportOffsetX() { 430 return (getMeasuredWidth() - getViewportWidth()) / 2; 431 } 432 getViewportOffsetY()433 int getViewportOffsetY() { 434 return (getMeasuredHeight() - getViewportHeight()) / 2; 435 } 436 getPageIndicator()437 PageIndicator getPageIndicator() { 438 return mPageIndicator; 439 } getPageIndicatorMarker(int pageIndex)440 protected PageIndicator.PageMarkerResources getPageIndicatorMarker(int pageIndex) { 441 return new PageIndicator.PageMarkerResources(); 442 } 443 444 /** 445 * Add a page change listener which will be called when a page is _finished_ listening. 446 * 447 */ setPageSwitchListener(PageSwitchListener pageSwitchListener)448 public void setPageSwitchListener(PageSwitchListener pageSwitchListener) { 449 mPageSwitchListener = pageSwitchListener; 450 if (mPageSwitchListener != null) { 451 mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage); 452 } 453 } 454 455 /** 456 * Note: this is a reimplementation of View.isLayoutRtl() since that is currently hidden api. 457 */ isLayoutRtl()458 public boolean isLayoutRtl() { 459 return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); 460 } 461 462 /** 463 * Called by subclasses to mark that data is ready, and that we can begin loading and laying 464 * out pages. 465 */ setDataIsReady()466 protected void setDataIsReady() { 467 mIsDataReady = true; 468 } 469 isDataReady()470 protected boolean isDataReady() { 471 return mIsDataReady; 472 } 473 474 /** 475 * Returns the index of the currently displayed page. 476 * 477 * @return The index of the currently displayed page. 478 */ getCurrentPage()479 int getCurrentPage() { 480 return mCurrentPage; 481 } 482 getNextPage()483 int getNextPage() { 484 return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; 485 } 486 getPageCount()487 int getPageCount() { 488 return getChildCount(); 489 } 490 getPageAt(int index)491 View getPageAt(int index) { 492 return getChildAt(index); 493 } 494 indexToPage(int index)495 protected int indexToPage(int index) { 496 return index; 497 } 498 499 /** 500 * Updates the scroll of the current page immediately to its final scroll position. We use this 501 * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of 502 * the previous tab page. 503 */ updateCurrentPageScroll()504 protected void updateCurrentPageScroll() { 505 // If the current page is invalid, just reset the scroll position to zero 506 int newX = 0; 507 if (0 <= mCurrentPage && mCurrentPage < getPageCount()) { 508 newX = getScrollForPage(mCurrentPage); 509 } 510 scrollTo(newX, 0); 511 mScroller.setFinalX(newX); 512 forceFinishScroller(); 513 } 514 515 /** 516 * Called during AllApps/Home transitions to avoid unnecessary work. When that other animation 517 * {@link #updateCurrentPageScroll()} should be called, to correctly set the final state and 518 * re-enable scrolling. 519 */ stopScrolling()520 void stopScrolling() { 521 mCurrentPage = getNextPage(); 522 notifyPageSwitchListener(); 523 forceFinishScroller(); 524 } 525 abortScrollerAnimation(boolean resetNextPage)526 private void abortScrollerAnimation(boolean resetNextPage) { 527 mScroller.abortAnimation(); 528 // We need to clean up the next page here to avoid computeScrollHelper from 529 // updating current page on the pass. 530 if (resetNextPage) { 531 mNextPage = INVALID_PAGE; 532 } 533 } 534 forceFinishScroller()535 private void forceFinishScroller() { 536 mScroller.forceFinished(true); 537 // We need to clean up the next page here to avoid computeScrollHelper from 538 // updating current page on the pass. 539 mNextPage = INVALID_PAGE; 540 } 541 validateNewPage(int newPage)542 private int validateNewPage(int newPage) { 543 int validatedPage = newPage; 544 // When in free scroll mode, we need to clamp to the free scroll page range. 545 if (mFreeScroll) { 546 getFreeScrollPageRange(mTempVisiblePagesRange); 547 validatedPage = Math.max(mTempVisiblePagesRange[0], 548 Math.min(newPage, mTempVisiblePagesRange[1])); 549 } 550 // Ensure that it is clamped by the actual set of children in all cases 551 validatedPage = Math.max(0, Math.min(validatedPage, getPageCount() - 1)); 552 return validatedPage; 553 } 554 555 /** 556 * Sets the current page. 557 */ setCurrentPage(int currentPage)558 void setCurrentPage(int currentPage) { 559 if (!mScroller.isFinished()) { 560 abortScrollerAnimation(true); 561 } 562 // don't introduce any checks like mCurrentPage == currentPage here-- if we change the 563 // the default 564 if (getChildCount() == 0) { 565 return; 566 } 567 mForceScreenScrolled = true; 568 mCurrentPage = validateNewPage(currentPage); 569 updateCurrentPageScroll(); 570 notifyPageSwitchListener(); 571 invalidate(); 572 } 573 574 /** 575 * The restore page will be set in place of the current page at the next (likely first) 576 * layout. 577 */ setRestorePage(int restorePage)578 void setRestorePage(int restorePage) { 579 mRestorePage = restorePage; 580 } getRestorePage()581 int getRestorePage() { 582 return mRestorePage; 583 } 584 585 /** 586 * Should be called whenever the page changes. In the case of a scroll, we wait until the page 587 * has settled. 588 */ notifyPageSwitchListener()589 protected void notifyPageSwitchListener() { 590 if (mPageSwitchListener != null) { 591 mPageSwitchListener.onPageSwitch(getPageAt(getNextPage()), getNextPage()); 592 } 593 594 updatePageIndicator(); 595 } 596 updatePageIndicator()597 private void updatePageIndicator() { 598 // Update the page indicator (when we aren't reordering) 599 if (mPageIndicator != null) { 600 mPageIndicator.setContentDescription(getPageIndicatorDescription()); 601 if (!isReordering(false)) { 602 mPageIndicator.setActiveMarker(getNextPage()); 603 } 604 } 605 } pageBeginMoving()606 protected void pageBeginMoving() { 607 if (!mIsPageMoving) { 608 mIsPageMoving = true; 609 onPageBeginMoving(); 610 } 611 } 612 pageEndMoving()613 protected void pageEndMoving() { 614 if (mIsPageMoving) { 615 mIsPageMoving = false; 616 onPageEndMoving(); 617 } 618 } 619 isPageMoving()620 protected boolean isPageMoving() { 621 return mIsPageMoving; 622 } 623 624 // a method that subclasses can override to add behavior onPageBeginMoving()625 protected void onPageBeginMoving() { 626 } 627 628 // a method that subclasses can override to add behavior onPageEndMoving()629 protected void onPageEndMoving() { 630 mWasInOverscroll = false; 631 } 632 633 /** 634 * Registers the specified listener on each page contained in this workspace. 635 * 636 * @param l The listener used to respond to long clicks. 637 */ 638 @Override setOnLongClickListener(OnLongClickListener l)639 public void setOnLongClickListener(OnLongClickListener l) { 640 mLongClickListener = l; 641 final int count = getPageCount(); 642 for (int i = 0; i < count; i++) { 643 getPageAt(i).setOnLongClickListener(l); 644 } 645 super.setOnLongClickListener(l); 646 } 647 648 @Override scrollBy(int x, int y)649 public void scrollBy(int x, int y) { 650 scrollTo(mUnboundedScrollX + x, getScrollY() + y); 651 } 652 653 @Override scrollTo(int x, int y)654 public void scrollTo(int x, int y) { 655 // In free scroll mode, we clamp the scrollX 656 if (mFreeScroll) { 657 x = Math.min(x, mFreeScrollMaxScrollX); 658 x = Math.max(x, mFreeScrollMinScrollX); 659 } 660 661 final boolean isRtl = isLayoutRtl(); 662 mUnboundedScrollX = x; 663 664 boolean isXBeforeFirstPage = isRtl ? (x > mMaxScrollX) : (x < 0); 665 boolean isXAfterLastPage = isRtl ? (x < 0) : (x > mMaxScrollX); 666 if (isXBeforeFirstPage) { 667 super.scrollTo(0, y); 668 if (mAllowOverScroll) { 669 mWasInOverscroll = true; 670 if (isRtl) { 671 overScroll(x - mMaxScrollX); 672 } else { 673 overScroll(x); 674 } 675 } 676 } else if (isXAfterLastPage) { 677 super.scrollTo(mMaxScrollX, y); 678 if (mAllowOverScroll) { 679 mWasInOverscroll = true; 680 if (isRtl) { 681 overScroll(x); 682 } else { 683 overScroll(x - mMaxScrollX); 684 } 685 } 686 } else { 687 if (mWasInOverscroll) { 688 overScroll(0); 689 mWasInOverscroll = false; 690 } 691 mOverScrollX = x; 692 super.scrollTo(x, y); 693 } 694 695 mTouchX = x; 696 mSmoothingTime = System.nanoTime() / NANOTIME_DIV; 697 698 // Update the last motion events when scrolling 699 if (isReordering(true)) { 700 float[] p = mapPointFromParentToView(this, mParentDownMotionX, mParentDownMotionY); 701 mLastMotionX = p[0]; 702 mLastMotionY = p[1]; 703 updateDragViewTranslationDuringDrag(); 704 } 705 } 706 sendScrollAccessibilityEvent()707 private void sendScrollAccessibilityEvent() { 708 AccessibilityManager am = 709 (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 710 if (am.isEnabled()) { 711 AccessibilityEvent ev = 712 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED); 713 ev.setItemCount(getChildCount()); 714 ev.setFromIndex(mCurrentPage); 715 ev.setToIndex(getNextPage()); 716 717 final int action; 718 if (getNextPage() >= mCurrentPage) { 719 action = AccessibilityNodeInfo.ACTION_SCROLL_FORWARD; 720 } else { 721 action = AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD; 722 } 723 724 ev.setAction(action); 725 sendAccessibilityEventUnchecked(ev); 726 } 727 } 728 729 // we moved this functionality to a helper function so SmoothPagedView can reuse it computeScrollHelper()730 protected boolean computeScrollHelper() { 731 if (mScroller.computeScrollOffset()) { 732 // Don't bother scrolling if the page does not need to be moved 733 if (getScrollX() != mScroller.getCurrX() 734 || getScrollY() != mScroller.getCurrY() 735 || mOverScrollX != mScroller.getCurrX()) { 736 float scaleX = mFreeScroll ? getScaleX() : 1f; 737 int scrollX = (int) (mScroller.getCurrX() * (1 / scaleX)); 738 scrollTo(scrollX, mScroller.getCurrY()); 739 } 740 invalidate(); 741 return true; 742 } else if (mNextPage != INVALID_PAGE) { 743 sendScrollAccessibilityEvent(); 744 745 mCurrentPage = validateNewPage(mNextPage); 746 mNextPage = INVALID_PAGE; 747 notifyPageSwitchListener(); 748 749 // Load the associated pages if necessary 750 if (mDeferLoadAssociatedPagesUntilScrollCompletes) { 751 loadAssociatedPages(mCurrentPage); 752 mDeferLoadAssociatedPagesUntilScrollCompletes = false; 753 } 754 755 // We don't want to trigger a page end moving unless the page has settled 756 // and the user has stopped scrolling 757 if (mTouchState == TOUCH_STATE_REST) { 758 pageEndMoving(); 759 } 760 761 onPostReorderingAnimationCompleted(); 762 AccessibilityManager am = (AccessibilityManager) 763 getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 764 if (am.isEnabled()) { 765 // Notify the user when the page changes 766 announceForAccessibility(getCurrentPageDescription()); 767 } 768 return true; 769 } 770 return false; 771 } 772 773 @Override computeScroll()774 public void computeScroll() { 775 computeScrollHelper(); 776 } 777 shouldSetTopAlignedPivotForWidget(int childIndex)778 protected boolean shouldSetTopAlignedPivotForWidget(int childIndex) { 779 return mTopAlignPageWhenShrinkingForBouncer; 780 } 781 782 public static class LayoutParams extends ViewGroup.LayoutParams { 783 public boolean isFullScreenPage = false; 784 785 /** 786 * {@inheritDoc} 787 */ LayoutParams(int width, int height)788 public LayoutParams(int width, int height) { 789 super(width, height); 790 } 791 LayoutParams(ViewGroup.LayoutParams source)792 public LayoutParams(ViewGroup.LayoutParams source) { 793 super(source); 794 } 795 } 796 generateDefaultLayoutParams()797 protected LayoutParams generateDefaultLayoutParams() { 798 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 799 } 800 addFullScreenPage(View page)801 public void addFullScreenPage(View page) { 802 LayoutParams lp = generateDefaultLayoutParams(); 803 lp.isFullScreenPage = true; 804 super.addView(page, 0, lp); 805 } 806 getNormalChildHeight()807 public int getNormalChildHeight() { 808 return mNormalChildHeight; 809 } 810 811 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)812 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 813 if (!mIsDataReady || getChildCount() == 0) { 814 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 815 return; 816 } 817 818 // We measure the dimensions of the PagedView to be larger than the pages so that when we 819 // zoom out (and scale down), the view is still contained in the parent 820 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 821 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 822 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 823 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 824 // NOTE: We multiply by 2f to account for the fact that depending on the offset of the 825 // viewport, we can be at most one and a half screens offset once we scale down 826 DisplayMetrics dm = getResources().getDisplayMetrics(); 827 int maxSize = Math.max(dm.widthPixels + mInsets.left + mInsets.right, 828 dm.heightPixels + mInsets.top + mInsets.bottom); 829 830 int parentWidthSize = (int) (2f * maxSize); 831 int parentHeightSize = (int) (2f * maxSize); 832 int scaledWidthSize, scaledHeightSize; 833 if (mUseMinScale) { 834 scaledWidthSize = (int) (parentWidthSize / mMinScale); 835 scaledHeightSize = (int) (parentHeightSize / mMinScale); 836 } else { 837 scaledWidthSize = widthSize; 838 scaledHeightSize = heightSize; 839 } 840 mViewport.set(0, 0, widthSize, heightSize); 841 842 if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) { 843 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 844 return; 845 } 846 847 // Return early if we aren't given a proper dimension 848 if (widthSize <= 0 || heightSize <= 0) { 849 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 850 return; 851 } 852 853 /* Allow the height to be set as WRAP_CONTENT. This allows the particular case 854 * of the All apps view on XLarge displays to not take up more space then it needs. Width 855 * is still not allowed to be set as WRAP_CONTENT since many parts of the code expect 856 * each page to have the same width. 857 */ 858 final int verticalPadding = getPaddingTop() + getPaddingBottom(); 859 final int horizontalPadding = getPaddingLeft() + getPaddingRight(); 860 861 int referenceChildWidth = 0; 862 // The children are given the same width and height as the workspace 863 // unless they were set to WRAP_CONTENT 864 if (DEBUG) Log.d(TAG, "PagedView.onMeasure(): " + widthSize + ", " + heightSize); 865 if (DEBUG) Log.d(TAG, "PagedView.scaledSize: " + scaledWidthSize + ", " + scaledHeightSize); 866 if (DEBUG) Log.d(TAG, "PagedView.parentSize: " + parentWidthSize + ", " + parentHeightSize); 867 if (DEBUG) Log.d(TAG, "PagedView.horizontalPadding: " + horizontalPadding); 868 if (DEBUG) Log.d(TAG, "PagedView.verticalPadding: " + verticalPadding); 869 final int childCount = getChildCount(); 870 for (int i = 0; i < childCount; i++) { 871 // disallowing padding in paged view (just pass 0) 872 final View child = getPageAt(i); 873 if (child.getVisibility() != GONE) { 874 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 875 876 int childWidthMode; 877 int childHeightMode; 878 int childWidth; 879 int childHeight; 880 881 if (!lp.isFullScreenPage) { 882 if (lp.width == LayoutParams.WRAP_CONTENT) { 883 childWidthMode = MeasureSpec.AT_MOST; 884 } else { 885 childWidthMode = MeasureSpec.EXACTLY; 886 } 887 888 if (lp.height == LayoutParams.WRAP_CONTENT) { 889 childHeightMode = MeasureSpec.AT_MOST; 890 } else { 891 childHeightMode = MeasureSpec.EXACTLY; 892 } 893 894 childWidth = getViewportWidth() - horizontalPadding 895 - mInsets.left - mInsets.right; 896 childHeight = getViewportHeight() - verticalPadding 897 - mInsets.top - mInsets.bottom; 898 mNormalChildHeight = childHeight; 899 } else { 900 childWidthMode = MeasureSpec.EXACTLY; 901 childHeightMode = MeasureSpec.EXACTLY; 902 903 childWidth = getViewportWidth() - mInsets.left - mInsets.right; 904 childHeight = getViewportHeight(); 905 } 906 if (referenceChildWidth == 0) { 907 referenceChildWidth = childWidth; 908 } 909 910 final int childWidthMeasureSpec = 911 MeasureSpec.makeMeasureSpec(childWidth, childWidthMode); 912 final int childHeightMeasureSpec = 913 MeasureSpec.makeMeasureSpec(childHeight, childHeightMode); 914 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 915 } 916 } 917 if (mSpacePagesAutomatically) { 918 int spacing = (getViewportWidth() - mInsets.left - mInsets.right 919 - referenceChildWidth) / 2; 920 if (spacing >= 0) { 921 setPageSpacing(spacing); 922 } 923 mSpacePagesAutomatically = false; 924 } 925 setMeasuredDimension(scaledWidthSize, scaledHeightSize); 926 } 927 928 /** 929 * This method should be called once before first layout / measure pass. 930 */ setSinglePageInViewport()931 protected void setSinglePageInViewport() { 932 mSpacePagesAutomatically = true; 933 } 934 935 @Override onLayout(boolean changed, int left, int top, int right, int bottom)936 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 937 if (!mIsDataReady || getChildCount() == 0) { 938 return; 939 } 940 941 if (DEBUG) Log.d(TAG, "PagedView.onLayout()"); 942 final int childCount = getChildCount(); 943 944 int offsetX = getViewportOffsetX(); 945 int offsetY = getViewportOffsetY(); 946 947 // Update the viewport offsets 948 mViewport.offset(offsetX, offsetY); 949 950 final boolean isRtl = isLayoutRtl(); 951 952 final int startIndex = isRtl ? childCount - 1 : 0; 953 final int endIndex = isRtl ? -1 : childCount; 954 final int delta = isRtl ? -1 : 1; 955 956 int verticalPadding = getPaddingTop() + getPaddingBottom(); 957 958 LayoutParams lp = (LayoutParams) getChildAt(startIndex).getLayoutParams(); 959 LayoutParams nextLp; 960 961 int childLeft = offsetX + (lp.isFullScreenPage ? 0 : getPaddingLeft()); 962 if (mPageScrolls == null || getChildCount() != mChildCountOnLastLayout) { 963 mPageScrolls = new int[getChildCount()]; 964 } 965 966 for (int i = startIndex; i != endIndex; i += delta) { 967 final View child = getPageAt(i); 968 if (child.getVisibility() != View.GONE) { 969 lp = (LayoutParams) child.getLayoutParams(); 970 int childTop; 971 if (lp.isFullScreenPage) { 972 childTop = offsetY; 973 } else { 974 childTop = offsetY + getPaddingTop() + mInsets.top; 975 if (mCenterPagesVertically) { 976 childTop += (getViewportHeight() - mInsets.top - mInsets.bottom - verticalPadding - child.getMeasuredHeight()) / 2; 977 } 978 } 979 980 final int childWidth = child.getMeasuredWidth(); 981 final int childHeight = child.getMeasuredHeight(); 982 983 if (DEBUG) Log.d(TAG, "\tlayout-child" + i + ": " + childLeft + ", " + childTop); 984 child.layout(childLeft, childTop, 985 childLeft + child.getMeasuredWidth(), childTop + childHeight); 986 987 int scrollOffsetLeft = lp.isFullScreenPage ? 0 : getPaddingLeft(); 988 mPageScrolls[i] = childLeft - scrollOffsetLeft - offsetX; 989 990 int pageGap = mPageSpacing; 991 int next = i + delta; 992 if (next != endIndex) { 993 nextLp = (LayoutParams) getPageAt(next).getLayoutParams(); 994 } else { 995 nextLp = null; 996 } 997 998 // Prevent full screen pages from showing in the viewport 999 // when they are not the current page. 1000 if (lp.isFullScreenPage) { 1001 pageGap = getPaddingLeft(); 1002 } else if (nextLp != null && nextLp.isFullScreenPage) { 1003 pageGap = getPaddingRight(); 1004 } 1005 1006 childLeft += childWidth + pageGap; 1007 } 1008 } 1009 1010 if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { 1011 updateCurrentPageScroll(); 1012 mFirstLayout = false; 1013 } 1014 1015 if (childCount > 0) { 1016 final int index = isLayoutRtl() ? 0 : childCount - 1; 1017 mMaxScrollX = getScrollForPage(index); 1018 } else { 1019 mMaxScrollX = 0; 1020 } 1021 1022 if (mScroller.isFinished() && mChildCountOnLastLayout != getChildCount() && 1023 !mDeferringForDelete) { 1024 if (mRestorePage != INVALID_RESTORE_PAGE) { 1025 setCurrentPage(mRestorePage); 1026 mRestorePage = INVALID_RESTORE_PAGE; 1027 } else { 1028 setCurrentPage(getNextPage()); 1029 } 1030 } 1031 mChildCountOnLastLayout = getChildCount(); 1032 1033 if (isReordering(true)) { 1034 updateDragViewTranslationDuringDrag(); 1035 } 1036 } 1037 setPageSpacing(int pageSpacing)1038 public void setPageSpacing(int pageSpacing) { 1039 mPageSpacing = pageSpacing; 1040 requestLayout(); 1041 } 1042 screenScrolled(int screenCenter)1043 protected void screenScrolled(int screenCenter) { 1044 boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX; 1045 1046 if (mFadeInAdjacentScreens && !isInOverscroll) { 1047 for (int i = 0; i < getChildCount(); i++) { 1048 View child = getChildAt(i); 1049 if (child != null) { 1050 float scrollProgress = getScrollProgress(screenCenter, child, i); 1051 float alpha = 1 - Math.abs(scrollProgress); 1052 child.setAlpha(alpha); 1053 } 1054 } 1055 invalidate(); 1056 } 1057 } 1058 enablePagedViewAnimations()1059 protected void enablePagedViewAnimations() { 1060 mAllowPagedViewAnimations = true; 1061 1062 } disablePagedViewAnimations()1063 protected void disablePagedViewAnimations() { 1064 mAllowPagedViewAnimations = false; 1065 } 1066 1067 @Override onChildViewAdded(View parent, View child)1068 public void onChildViewAdded(View parent, View child) { 1069 // Update the page indicator, we don't update the page indicator as we 1070 // add/remove pages 1071 if (mPageIndicator != null && !isReordering(false)) { 1072 int pageIndex = indexOfChild(child); 1073 mPageIndicator.addMarker(pageIndex, 1074 getPageIndicatorMarker(pageIndex), 1075 mAllowPagedViewAnimations); 1076 } 1077 1078 // This ensures that when children are added, they get the correct transforms / alphas 1079 // in accordance with any scroll effects. 1080 mForceScreenScrolled = true; 1081 updateFreescrollBounds(); 1082 invalidate(); 1083 } 1084 1085 @Override onChildViewRemoved(View parent, View child)1086 public void onChildViewRemoved(View parent, View child) { 1087 mForceScreenScrolled = true; 1088 updateFreescrollBounds(); 1089 invalidate(); 1090 } 1091 removeMarkerForView(int index)1092 private void removeMarkerForView(int index) { 1093 // Update the page indicator, we don't update the page indicator as we 1094 // add/remove pages 1095 if (mPageIndicator != null && !isReordering(false)) { 1096 mPageIndicator.removeMarker(index, mAllowPagedViewAnimations); 1097 } 1098 } 1099 1100 @Override removeView(View v)1101 public void removeView(View v) { 1102 // XXX: We should find a better way to hook into this before the view 1103 // gets removed form its parent... 1104 removeMarkerForView(indexOfChild(v)); 1105 super.removeView(v); 1106 } 1107 @Override removeViewInLayout(View v)1108 public void removeViewInLayout(View v) { 1109 // XXX: We should find a better way to hook into this before the view 1110 // gets removed form its parent... 1111 removeMarkerForView(indexOfChild(v)); 1112 super.removeViewInLayout(v); 1113 } 1114 @Override removeViewAt(int index)1115 public void removeViewAt(int index) { 1116 // XXX: We should find a better way to hook into this before the view 1117 // gets removed form its parent... 1118 removeViewAt(index); 1119 super.removeViewAt(index); 1120 } 1121 @Override removeAllViewsInLayout()1122 public void removeAllViewsInLayout() { 1123 // Update the page indicator, we don't update the page indicator as we 1124 // add/remove pages 1125 if (mPageIndicator != null) { 1126 mPageIndicator.removeAllMarkers(mAllowPagedViewAnimations); 1127 } 1128 1129 super.removeAllViewsInLayout(); 1130 } 1131 getChildOffset(int index)1132 protected int getChildOffset(int index) { 1133 if (index < 0 || index > getChildCount() - 1) return 0; 1134 1135 int offset = getPageAt(index).getLeft() - getViewportOffsetX(); 1136 1137 return offset; 1138 } 1139 getFreeScrollPageRange(int[] range)1140 protected void getFreeScrollPageRange(int[] range) { 1141 range[0] = 0; 1142 range[1] = Math.max(0, getChildCount() - 1); 1143 } 1144 getVisiblePages(int[] range)1145 protected void getVisiblePages(int[] range) { 1146 final int pageCount = getChildCount(); 1147 mTmpIntPoint[0] = mTmpIntPoint[1] = 0; 1148 1149 range[0] = -1; 1150 range[1] = -1; 1151 1152 if (pageCount > 0) { 1153 int viewportWidth = getViewportWidth(); 1154 int curScreen = 0; 1155 1156 int count = getChildCount(); 1157 for (int i = 0; i < count; i++) { 1158 View currPage = getPageAt(i); 1159 1160 mTmpIntPoint[0] = 0; 1161 Utilities.getDescendantCoordRelativeToParent(currPage, this, mTmpIntPoint, false); 1162 if (mTmpIntPoint[0] > viewportWidth) { 1163 if (range[0] == -1) { 1164 continue; 1165 } else { 1166 break; 1167 } 1168 } 1169 1170 mTmpIntPoint[0] = currPage.getMeasuredWidth(); 1171 Utilities.getDescendantCoordRelativeToParent(currPage, this, mTmpIntPoint, false); 1172 if (mTmpIntPoint[0] < 0) { 1173 if (range[0] == -1) { 1174 continue; 1175 } else { 1176 break; 1177 } 1178 } 1179 curScreen = i; 1180 if (range[0] < 0) { 1181 range[0] = curScreen; 1182 } 1183 } 1184 1185 range[1] = curScreen; 1186 } else { 1187 range[0] = -1; 1188 range[1] = -1; 1189 } 1190 } 1191 shouldDrawChild(View child)1192 protected boolean shouldDrawChild(View child) { 1193 return child.getVisibility() == VISIBLE; 1194 } 1195 1196 @Override dispatchDraw(Canvas canvas)1197 protected void dispatchDraw(Canvas canvas) { 1198 // Find out which screens are visible; as an optimization we only call draw on them 1199 final int pageCount = getChildCount(); 1200 if (pageCount > 0) { 1201 int halfScreenSize = getViewportWidth() / 2; 1202 // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. 1203 // Otherwise it is equal to the scaled overscroll position. 1204 int screenCenter = mOverScrollX + halfScreenSize; 1205 1206 if (screenCenter != mLastScreenCenter || mForceScreenScrolled) { 1207 // set mForceScreenScrolled before calling screenScrolled so that screenScrolled can 1208 // set it for the next frame 1209 mForceScreenScrolled = false; 1210 screenScrolled(screenCenter); 1211 mLastScreenCenter = screenCenter; 1212 } 1213 1214 getVisiblePages(mTempVisiblePagesRange); 1215 final int leftScreen = mTempVisiblePagesRange[0]; 1216 final int rightScreen = mTempVisiblePagesRange[1]; 1217 if (leftScreen != -1 && rightScreen != -1) { 1218 final long drawingTime = getDrawingTime(); 1219 // Clip to the bounds 1220 canvas.save(); 1221 canvas.clipRect(getScrollX(), getScrollY(), getScrollX() + getRight() - getLeft(), 1222 getScrollY() + getBottom() - getTop()); 1223 1224 // Draw all the children, leaving the drag view for last 1225 for (int i = pageCount - 1; i >= 0; i--) { 1226 final View v = getPageAt(i); 1227 if (v == mDragView) continue; 1228 if (mForceDrawAllChildrenNextFrame || 1229 (leftScreen <= i && i <= rightScreen && shouldDrawChild(v))) { 1230 drawChild(canvas, v, drawingTime); 1231 } 1232 } 1233 // Draw the drag view on top (if there is one) 1234 if (mDragView != null) { 1235 drawChild(canvas, mDragView, drawingTime); 1236 } 1237 1238 mForceDrawAllChildrenNextFrame = false; 1239 canvas.restore(); 1240 } 1241 } 1242 } 1243 1244 @Override requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)1245 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { 1246 int page = indexToPage(indexOfChild(child)); 1247 if (page != mCurrentPage || !mScroller.isFinished()) { 1248 snapToPage(page); 1249 return true; 1250 } 1251 return false; 1252 } 1253 1254 @Override onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)1255 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 1256 int focusablePage; 1257 if (mNextPage != INVALID_PAGE) { 1258 focusablePage = mNextPage; 1259 } else { 1260 focusablePage = mCurrentPage; 1261 } 1262 View v = getPageAt(focusablePage); 1263 if (v != null) { 1264 return v.requestFocus(direction, previouslyFocusedRect); 1265 } 1266 return false; 1267 } 1268 1269 @Override dispatchUnhandledMove(View focused, int direction)1270 public boolean dispatchUnhandledMove(View focused, int direction) { 1271 // XXX-RTL: This will be fixed in a future CL 1272 if (direction == View.FOCUS_LEFT) { 1273 if (getCurrentPage() > 0) { 1274 snapToPage(getCurrentPage() - 1); 1275 return true; 1276 } 1277 } else if (direction == View.FOCUS_RIGHT) { 1278 if (getCurrentPage() < getPageCount() - 1) { 1279 snapToPage(getCurrentPage() + 1); 1280 return true; 1281 } 1282 } 1283 return super.dispatchUnhandledMove(focused, direction); 1284 } 1285 1286 @Override addFocusables(ArrayList<View> views, int direction, int focusableMode)1287 public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { 1288 // XXX-RTL: This will be fixed in a future CL 1289 if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) { 1290 getPageAt(mCurrentPage).addFocusables(views, direction, focusableMode); 1291 } 1292 if (direction == View.FOCUS_LEFT) { 1293 if (mCurrentPage > 0) { 1294 getPageAt(mCurrentPage - 1).addFocusables(views, direction, focusableMode); 1295 } 1296 } else if (direction == View.FOCUS_RIGHT){ 1297 if (mCurrentPage < getPageCount() - 1) { 1298 getPageAt(mCurrentPage + 1).addFocusables(views, direction, focusableMode); 1299 } 1300 } 1301 } 1302 1303 /** 1304 * If one of our descendant views decides that it could be focused now, only 1305 * pass that along if it's on the current page. 1306 * 1307 * This happens when live folders requery, and if they're off page, they 1308 * end up calling requestFocus, which pulls it on page. 1309 */ 1310 @Override focusableViewAvailable(View focused)1311 public void focusableViewAvailable(View focused) { 1312 View current = getPageAt(mCurrentPage); 1313 View v = focused; 1314 while (true) { 1315 if (v == current) { 1316 super.focusableViewAvailable(focused); 1317 return; 1318 } 1319 if (v == this) { 1320 return; 1321 } 1322 ViewParent parent = v.getParent(); 1323 if (parent instanceof View) { 1324 v = (View)v.getParent(); 1325 } else { 1326 return; 1327 } 1328 } 1329 } 1330 1331 /** 1332 * {@inheritDoc} 1333 */ 1334 @Override requestDisallowInterceptTouchEvent(boolean disallowIntercept)1335 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 1336 if (disallowIntercept) { 1337 // We need to make sure to cancel our long press if 1338 // a scrollable widget takes over touch events 1339 final View currentPage = getPageAt(mCurrentPage); 1340 currentPage.cancelLongPress(); 1341 } 1342 super.requestDisallowInterceptTouchEvent(disallowIntercept); 1343 } 1344 1345 /** 1346 * Return true if a tap at (x, y) should trigger a flip to the previous page. 1347 */ hitsPreviousPage(float x, float y)1348 protected boolean hitsPreviousPage(float x, float y) { 1349 if (isLayoutRtl()) { 1350 return (x > (getViewportOffsetX() + getViewportWidth() - 1351 getPaddingRight() - mPageSpacing)); 1352 } 1353 return (x < getViewportOffsetX() + getPaddingLeft() + mPageSpacing); 1354 } 1355 1356 /** 1357 * Return true if a tap at (x, y) should trigger a flip to the next page. 1358 */ hitsNextPage(float x, float y)1359 protected boolean hitsNextPage(float x, float y) { 1360 if (isLayoutRtl()) { 1361 return (x < getViewportOffsetX() + getPaddingLeft() + mPageSpacing); 1362 } 1363 return (x > (getViewportOffsetX() + getViewportWidth() - 1364 getPaddingRight() - mPageSpacing)); 1365 } 1366 1367 /** Returns whether x and y originated within the buffered viewport */ isTouchPointInViewportWithBuffer(int x, int y)1368 private boolean isTouchPointInViewportWithBuffer(int x, int y) { 1369 mTmpRect.set(mViewport.left - mViewport.width() / 2, mViewport.top, 1370 mViewport.right + mViewport.width() / 2, mViewport.bottom); 1371 return mTmpRect.contains(x, y); 1372 } 1373 1374 @Override onInterceptTouchEvent(MotionEvent ev)1375 public boolean onInterceptTouchEvent(MotionEvent ev) { 1376 if (DISABLE_TOUCH_INTERACTION) { 1377 return false; 1378 } 1379 1380 /* 1381 * This method JUST determines whether we want to intercept the motion. 1382 * If we return true, onTouchEvent will be called and we do the actual 1383 * scrolling there. 1384 */ 1385 acquireVelocityTrackerAndAddMovement(ev); 1386 1387 // Skip touch handling if there are no pages to swipe 1388 if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev); 1389 1390 /* 1391 * Shortcut the most recurring case: the user is in the dragging 1392 * state and he is moving his finger. We want to intercept this 1393 * motion. 1394 */ 1395 final int action = ev.getAction(); 1396 if ((action == MotionEvent.ACTION_MOVE) && 1397 (mTouchState == TOUCH_STATE_SCROLLING)) { 1398 return true; 1399 } 1400 1401 switch (action & MotionEvent.ACTION_MASK) { 1402 case MotionEvent.ACTION_MOVE: { 1403 /* 1404 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1405 * whether the user has moved far enough from his original down touch. 1406 */ 1407 if (mActivePointerId != INVALID_POINTER) { 1408 determineScrollingStart(ev); 1409 } 1410 // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN 1411 // event. in that case, treat the first occurence of a move event as a ACTION_DOWN 1412 // i.e. fall through to the next case (don't break) 1413 // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events 1414 // while it's small- this was causing a crash before we checked for INVALID_POINTER) 1415 break; 1416 } 1417 1418 case MotionEvent.ACTION_DOWN: { 1419 final float x = ev.getX(); 1420 final float y = ev.getY(); 1421 // Remember location of down touch 1422 mDownMotionX = x; 1423 mDownMotionY = y; 1424 mDownScrollX = getScrollX(); 1425 mLastMotionX = x; 1426 mLastMotionY = y; 1427 float[] p = mapPointFromViewToParent(this, x, y); 1428 mParentDownMotionX = p[0]; 1429 mParentDownMotionY = p[1]; 1430 mLastMotionXRemainder = 0; 1431 mTotalMotionX = 0; 1432 mActivePointerId = ev.getPointerId(0); 1433 1434 /* 1435 * If being flinged and user touches the screen, initiate drag; 1436 * otherwise don't. mScroller.isFinished should be false when 1437 * being flinged. 1438 */ 1439 final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX()); 1440 final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop / 3); 1441 1442 if (finishedScrolling) { 1443 mTouchState = TOUCH_STATE_REST; 1444 if (!mScroller.isFinished() && !mFreeScroll) { 1445 setCurrentPage(getNextPage()); 1446 pageEndMoving(); 1447 } 1448 } else { 1449 if (isTouchPointInViewportWithBuffer((int) mDownMotionX, (int) mDownMotionY)) { 1450 mTouchState = TOUCH_STATE_SCROLLING; 1451 } else { 1452 mTouchState = TOUCH_STATE_REST; 1453 } 1454 } 1455 1456 // check if this can be the beginning of a tap on the side of the pages 1457 // to scroll the current page 1458 if (!DISABLE_TOUCH_SIDE_PAGES) { 1459 if (mTouchState != TOUCH_STATE_PREV_PAGE && mTouchState != TOUCH_STATE_NEXT_PAGE) { 1460 if (getChildCount() > 0) { 1461 if (hitsPreviousPage(x, y)) { 1462 mTouchState = TOUCH_STATE_PREV_PAGE; 1463 } else if (hitsNextPage(x, y)) { 1464 mTouchState = TOUCH_STATE_NEXT_PAGE; 1465 } 1466 } 1467 } 1468 } 1469 break; 1470 } 1471 1472 case MotionEvent.ACTION_UP: 1473 case MotionEvent.ACTION_CANCEL: 1474 resetTouchState(); 1475 break; 1476 1477 case MotionEvent.ACTION_POINTER_UP: 1478 onSecondaryPointerUp(ev); 1479 releaseVelocityTracker(); 1480 break; 1481 } 1482 1483 /* 1484 * The only time we want to intercept motion events is if we are in the 1485 * drag mode. 1486 */ 1487 return mTouchState != TOUCH_STATE_REST; 1488 } 1489 determineScrollingStart(MotionEvent ev)1490 protected void determineScrollingStart(MotionEvent ev) { 1491 determineScrollingStart(ev, 1.0f); 1492 } 1493 1494 /* 1495 * Determines if we should change the touch state to start scrolling after the 1496 * user moves their touch point too far. 1497 */ determineScrollingStart(MotionEvent ev, float touchSlopScale)1498 protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) { 1499 // Disallow scrolling if we don't have a valid pointer index 1500 final int pointerIndex = ev.findPointerIndex(mActivePointerId); 1501 if (pointerIndex == -1) return; 1502 1503 // Disallow scrolling if we started the gesture from outside the viewport 1504 final float x = ev.getX(pointerIndex); 1505 final float y = ev.getY(pointerIndex); 1506 if (!isTouchPointInViewportWithBuffer((int) x, (int) y)) return; 1507 1508 final int xDiff = (int) Math.abs(x - mLastMotionX); 1509 final int yDiff = (int) Math.abs(y - mLastMotionY); 1510 1511 final int touchSlop = Math.round(touchSlopScale * mTouchSlop); 1512 boolean xPaged = xDiff > mPagingTouchSlop; 1513 boolean xMoved = xDiff > touchSlop; 1514 boolean yMoved = yDiff > touchSlop; 1515 1516 if (xMoved || xPaged || yMoved) { 1517 if (mUsePagingTouchSlop ? xPaged : xMoved) { 1518 // Scroll if the user moved far enough along the X axis 1519 mTouchState = TOUCH_STATE_SCROLLING; 1520 mTotalMotionX += Math.abs(mLastMotionX - x); 1521 mLastMotionX = x; 1522 mLastMotionXRemainder = 0; 1523 mTouchX = getViewportOffsetX() + getScrollX(); 1524 mSmoothingTime = System.nanoTime() / NANOTIME_DIV; 1525 onScrollInteractionBegin(); 1526 pageBeginMoving(); 1527 } 1528 } 1529 } 1530 getMaxScrollProgress()1531 protected float getMaxScrollProgress() { 1532 return 1.0f; 1533 } 1534 cancelCurrentPageLongPress()1535 protected void cancelCurrentPageLongPress() { 1536 if (mAllowLongPress) { 1537 //mAllowLongPress = false; 1538 // Try canceling the long press. It could also have been scheduled 1539 // by a distant descendant, so use the mAllowLongPress flag to block 1540 // everything 1541 final View currentPage = getPageAt(mCurrentPage); 1542 if (currentPage != null) { 1543 currentPage.cancelLongPress(); 1544 } 1545 } 1546 } 1547 getBoundedScrollProgress(int screenCenter, View v, int page)1548 protected float getBoundedScrollProgress(int screenCenter, View v, int page) { 1549 final int halfScreenSize = getViewportWidth() / 2; 1550 1551 screenCenter = Math.min(getScrollX() + halfScreenSize, screenCenter); 1552 screenCenter = Math.max(halfScreenSize, screenCenter); 1553 1554 return getScrollProgress(screenCenter, v, page); 1555 } 1556 getScrollProgress(int screenCenter, View v, int page)1557 protected float getScrollProgress(int screenCenter, View v, int page) { 1558 final int halfScreenSize = getViewportWidth() / 2; 1559 1560 int delta = screenCenter - (getScrollForPage(page) + halfScreenSize); 1561 int count = getChildCount(); 1562 1563 final int totalDistance; 1564 1565 int adjacentPage = page + 1; 1566 if ((delta < 0 && !isLayoutRtl()) || (delta > 0 && isLayoutRtl())) { 1567 adjacentPage = page - 1; 1568 } 1569 1570 if (adjacentPage < 0 || adjacentPage > count - 1) { 1571 totalDistance = v.getMeasuredWidth() + mPageSpacing; 1572 } else { 1573 totalDistance = Math.abs(getScrollForPage(adjacentPage) - getScrollForPage(page)); 1574 } 1575 1576 float scrollProgress = delta / (totalDistance * 1.0f); 1577 scrollProgress = Math.min(scrollProgress, getMaxScrollProgress()); 1578 scrollProgress = Math.max(scrollProgress, - getMaxScrollProgress()); 1579 return scrollProgress; 1580 } 1581 getScrollForPage(int index)1582 public int getScrollForPage(int index) { 1583 if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) { 1584 return 0; 1585 } else { 1586 return mPageScrolls[index]; 1587 } 1588 } 1589 1590 // While layout transitions are occurring, a child's position may stray from its baseline 1591 // position. This method returns the magnitude of this stray at any given time. getLayoutTransitionOffsetForPage(int index)1592 public int getLayoutTransitionOffsetForPage(int index) { 1593 if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) { 1594 return 0; 1595 } else { 1596 View child = getChildAt(index); 1597 1598 int scrollOffset = 0; 1599 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1600 if (!lp.isFullScreenPage) { 1601 scrollOffset = isLayoutRtl() ? getPaddingRight() : getPaddingLeft(); 1602 } 1603 1604 int baselineX = mPageScrolls[index] + scrollOffset + getViewportOffsetX(); 1605 return (int) (child.getX() - baselineX); 1606 } 1607 } 1608 1609 // This curve determines how the effect of scrolling over the limits of the page dimishes 1610 // as the user pulls further and further from the bounds overScrollInfluenceCurve(float f)1611 private float overScrollInfluenceCurve(float f) { 1612 f -= 1.0f; 1613 return f * f * f + 1.0f; 1614 } 1615 acceleratedOverFactor(float amount)1616 protected float acceleratedOverFactor(float amount) { 1617 int screenSize = getViewportWidth(); 1618 1619 // We want to reach the max over scroll effect when the user has 1620 // over scrolled half the size of the screen 1621 float f = OVERSCROLL_ACCELERATE_FACTOR * (amount / screenSize); 1622 1623 if (f == 0) return 0; 1624 1625 // Clamp this factor, f, to -1 < f < 1 1626 if (Math.abs(f) >= 1) { 1627 f /= Math.abs(f); 1628 } 1629 return f; 1630 } 1631 dampedOverScroll(float amount)1632 protected void dampedOverScroll(float amount) { 1633 int screenSize = getViewportWidth(); 1634 1635 float f = (amount / screenSize); 1636 1637 if (f == 0) return; 1638 f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); 1639 1640 // Clamp this factor, f, to -1 < f < 1 1641 if (Math.abs(f) >= 1) { 1642 f /= Math.abs(f); 1643 } 1644 1645 int overScrollAmount = (int) Math.round(OVERSCROLL_DAMP_FACTOR * f * screenSize); 1646 if (amount < 0) { 1647 mOverScrollX = overScrollAmount; 1648 super.scrollTo(mOverScrollX, getScrollY()); 1649 } else { 1650 mOverScrollX = mMaxScrollX + overScrollAmount; 1651 super.scrollTo(mOverScrollX, getScrollY()); 1652 } 1653 invalidate(); 1654 } 1655 overScroll(float amount)1656 protected void overScroll(float amount) { 1657 dampedOverScroll(amount); 1658 } 1659 maxOverScroll()1660 protected float maxOverScroll() { 1661 // Using the formula in overScroll, assuming that f = 1.0 (which it should generally not 1662 // exceed). Used to find out how much extra wallpaper we need for the over scroll effect 1663 float f = 1.0f; 1664 f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); 1665 return OVERSCROLL_DAMP_FACTOR * f; 1666 } 1667 enableFreeScroll()1668 protected void enableFreeScroll() { 1669 setEnableFreeScroll(true); 1670 } 1671 disableFreeScroll()1672 protected void disableFreeScroll() { 1673 setEnableFreeScroll(false); 1674 } 1675 updateFreescrollBounds()1676 void updateFreescrollBounds() { 1677 getFreeScrollPageRange(mTempVisiblePagesRange); 1678 if (isLayoutRtl()) { 1679 mFreeScrollMinScrollX = getScrollForPage(mTempVisiblePagesRange[1]); 1680 mFreeScrollMaxScrollX = getScrollForPage(mTempVisiblePagesRange[0]); 1681 } else { 1682 mFreeScrollMinScrollX = getScrollForPage(mTempVisiblePagesRange[0]); 1683 mFreeScrollMaxScrollX = getScrollForPage(mTempVisiblePagesRange[1]); 1684 } 1685 } 1686 setEnableFreeScroll(boolean freeScroll)1687 private void setEnableFreeScroll(boolean freeScroll) { 1688 mFreeScroll = freeScroll; 1689 1690 if (mFreeScroll) { 1691 updateFreescrollBounds(); 1692 getFreeScrollPageRange(mTempVisiblePagesRange); 1693 if (getCurrentPage() < mTempVisiblePagesRange[0]) { 1694 setCurrentPage(mTempVisiblePagesRange[0]); 1695 } else if (getCurrentPage() > mTempVisiblePagesRange[1]) { 1696 setCurrentPage(mTempVisiblePagesRange[1]); 1697 } 1698 } 1699 1700 setEnableOverscroll(!freeScroll); 1701 } 1702 setEnableOverscroll(boolean enable)1703 private void setEnableOverscroll(boolean enable) { 1704 mAllowOverScroll = enable; 1705 } 1706 getNearestHoverOverPageIndex()1707 int getNearestHoverOverPageIndex() { 1708 if (mDragView != null) { 1709 int dragX = (int) (mDragView.getLeft() + (mDragView.getMeasuredWidth() / 2) 1710 + mDragView.getTranslationX()); 1711 getFreeScrollPageRange(mTempVisiblePagesRange); 1712 int minDistance = Integer.MAX_VALUE; 1713 int minIndex = indexOfChild(mDragView); 1714 for (int i = mTempVisiblePagesRange[0]; i <= mTempVisiblePagesRange[1]; i++) { 1715 View page = getPageAt(i); 1716 int pageX = (int) (page.getLeft() + page.getMeasuredWidth() / 2); 1717 int d = Math.abs(dragX - pageX); 1718 if (d < minDistance) { 1719 minIndex = i; 1720 minDistance = d; 1721 } 1722 } 1723 return minIndex; 1724 } 1725 return -1; 1726 } 1727 1728 @Override onTouchEvent(MotionEvent ev)1729 public boolean onTouchEvent(MotionEvent ev) { 1730 if (DISABLE_TOUCH_INTERACTION) { 1731 return false; 1732 } 1733 1734 super.onTouchEvent(ev); 1735 1736 // Skip touch handling if there are no pages to swipe 1737 if (getChildCount() <= 0) return super.onTouchEvent(ev); 1738 1739 acquireVelocityTrackerAndAddMovement(ev); 1740 1741 final int action = ev.getAction(); 1742 1743 switch (action & MotionEvent.ACTION_MASK) { 1744 case MotionEvent.ACTION_DOWN: 1745 /* 1746 * If being flinged and user touches, stop the fling. isFinished 1747 * will be false if being flinged. 1748 */ 1749 if (!mScroller.isFinished()) { 1750 abortScrollerAnimation(false); 1751 } 1752 1753 // Remember where the motion event started 1754 mDownMotionX = mLastMotionX = ev.getX(); 1755 mDownMotionY = mLastMotionY = ev.getY(); 1756 mDownScrollX = getScrollX(); 1757 float[] p = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY); 1758 mParentDownMotionX = p[0]; 1759 mParentDownMotionY = p[1]; 1760 mLastMotionXRemainder = 0; 1761 mTotalMotionX = 0; 1762 mActivePointerId = ev.getPointerId(0); 1763 1764 if (mTouchState == TOUCH_STATE_SCROLLING) { 1765 onScrollInteractionBegin(); 1766 pageBeginMoving(); 1767 } 1768 break; 1769 1770 case MotionEvent.ACTION_MOVE: 1771 if (mTouchState == TOUCH_STATE_SCROLLING) { 1772 // Scroll to follow the motion event 1773 final int pointerIndex = ev.findPointerIndex(mActivePointerId); 1774 1775 if (pointerIndex == -1) return true; 1776 1777 final float x = ev.getX(pointerIndex); 1778 final float deltaX = mLastMotionX + mLastMotionXRemainder - x; 1779 1780 mTotalMotionX += Math.abs(deltaX); 1781 1782 // Only scroll and update mLastMotionX if we have moved some discrete amount. We 1783 // keep the remainder because we are actually testing if we've moved from the last 1784 // scrolled position (which is discrete). 1785 if (Math.abs(deltaX) >= 1.0f) { 1786 mTouchX += deltaX; 1787 mSmoothingTime = System.nanoTime() / NANOTIME_DIV; 1788 if (!mDeferScrollUpdate) { 1789 scrollBy((int) deltaX, 0); 1790 if (DEBUG) Log.d(TAG, "onTouchEvent().Scrolling: " + deltaX); 1791 } else { 1792 invalidate(); 1793 } 1794 mLastMotionX = x; 1795 mLastMotionXRemainder = deltaX - (int) deltaX; 1796 } else { 1797 awakenScrollBars(); 1798 } 1799 } else if (mTouchState == TOUCH_STATE_REORDERING) { 1800 // Update the last motion position 1801 mLastMotionX = ev.getX(); 1802 mLastMotionY = ev.getY(); 1803 1804 // Update the parent down so that our zoom animations take this new movement into 1805 // account 1806 float[] pt = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY); 1807 mParentDownMotionX = pt[0]; 1808 mParentDownMotionY = pt[1]; 1809 updateDragViewTranslationDuringDrag(); 1810 1811 // Find the closest page to the touch point 1812 final int dragViewIndex = indexOfChild(mDragView); 1813 1814 // Change the drag view if we are hovering over the drop target 1815 boolean isHoveringOverDelete = isHoveringOverDeleteDropTarget( 1816 (int) mParentDownMotionX, (int) mParentDownMotionY); 1817 setPageHoveringOverDeleteDropTarget(dragViewIndex, isHoveringOverDelete); 1818 1819 if (DEBUG) Log.d(TAG, "mLastMotionX: " + mLastMotionX); 1820 if (DEBUG) Log.d(TAG, "mLastMotionY: " + mLastMotionY); 1821 if (DEBUG) Log.d(TAG, "mParentDownMotionX: " + mParentDownMotionX); 1822 if (DEBUG) Log.d(TAG, "mParentDownMotionY: " + mParentDownMotionY); 1823 1824 final int pageUnderPointIndex = getNearestHoverOverPageIndex(); 1825 if (pageUnderPointIndex > -1 && pageUnderPointIndex != indexOfChild(mDragView) && 1826 !isHoveringOverDelete) { 1827 mTempVisiblePagesRange[0] = 0; 1828 mTempVisiblePagesRange[1] = getPageCount() - 1; 1829 getFreeScrollPageRange(mTempVisiblePagesRange); 1830 if (mTempVisiblePagesRange[0] <= pageUnderPointIndex && 1831 pageUnderPointIndex <= mTempVisiblePagesRange[1] && 1832 pageUnderPointIndex != mSidePageHoverIndex && mScroller.isFinished()) { 1833 mSidePageHoverIndex = pageUnderPointIndex; 1834 mSidePageHoverRunnable = new Runnable() { 1835 @Override 1836 public void run() { 1837 // Setup the scroll to the correct page before we swap the views 1838 snapToPage(pageUnderPointIndex); 1839 1840 // For each of the pages between the paged view and the drag view, 1841 // animate them from the previous position to the new position in 1842 // the layout (as a result of the drag view moving in the layout) 1843 int shiftDelta = (dragViewIndex < pageUnderPointIndex) ? -1 : 1; 1844 int lowerIndex = (dragViewIndex < pageUnderPointIndex) ? 1845 dragViewIndex + 1 : pageUnderPointIndex; 1846 int upperIndex = (dragViewIndex > pageUnderPointIndex) ? 1847 dragViewIndex - 1 : pageUnderPointIndex; 1848 for (int i = lowerIndex; i <= upperIndex; ++i) { 1849 View v = getChildAt(i); 1850 // dragViewIndex < pageUnderPointIndex, so after we remove the 1851 // drag view all subsequent views to pageUnderPointIndex will 1852 // shift down. 1853 int oldX = getViewportOffsetX() + getChildOffset(i); 1854 int newX = getViewportOffsetX() + getChildOffset(i + shiftDelta); 1855 1856 // Animate the view translation from its old position to its new 1857 // position 1858 AnimatorSet anim = (AnimatorSet) v.getTag(ANIM_TAG_KEY); 1859 if (anim != null) { 1860 anim.cancel(); 1861 } 1862 1863 v.setTranslationX(oldX - newX); 1864 anim = new AnimatorSet(); 1865 anim.setDuration(REORDERING_REORDER_REPOSITION_DURATION); 1866 anim.playTogether( 1867 ObjectAnimator.ofFloat(v, "translationX", 0f)); 1868 anim.start(); 1869 v.setTag(anim); 1870 } 1871 1872 removeView(mDragView); 1873 onRemoveView(mDragView, false); 1874 addView(mDragView, pageUnderPointIndex); 1875 onAddView(mDragView, pageUnderPointIndex); 1876 mSidePageHoverIndex = -1; 1877 if (mPageIndicator != null) { 1878 mPageIndicator.setActiveMarker(getNextPage()); 1879 } 1880 } 1881 }; 1882 postDelayed(mSidePageHoverRunnable, REORDERING_SIDE_PAGE_HOVER_TIMEOUT); 1883 } 1884 } else { 1885 removeCallbacks(mSidePageHoverRunnable); 1886 mSidePageHoverIndex = -1; 1887 } 1888 } else { 1889 determineScrollingStart(ev); 1890 } 1891 break; 1892 1893 case MotionEvent.ACTION_UP: 1894 if (mTouchState == TOUCH_STATE_SCROLLING) { 1895 final int activePointerId = mActivePointerId; 1896 final int pointerIndex = ev.findPointerIndex(activePointerId); 1897 final float x = ev.getX(pointerIndex); 1898 final VelocityTracker velocityTracker = mVelocityTracker; 1899 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1900 int velocityX = (int) velocityTracker.getXVelocity(activePointerId); 1901 final int deltaX = (int) (x - mDownMotionX); 1902 final int pageWidth = getPageAt(mCurrentPage).getMeasuredWidth(); 1903 boolean isSignificantMove = Math.abs(deltaX) > pageWidth * 1904 SIGNIFICANT_MOVE_THRESHOLD; 1905 1906 mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x); 1907 1908 boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING && 1909 Math.abs(velocityX) > mFlingThresholdVelocity; 1910 1911 if (!mFreeScroll) { 1912 // In the case that the page is moved far to one direction and then is flung 1913 // in the opposite direction, we use a threshold to determine whether we should 1914 // just return to the starting page, or if we should skip one further. 1915 boolean returnToOriginalPage = false; 1916 if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD && 1917 Math.signum(velocityX) != Math.signum(deltaX) && isFling) { 1918 returnToOriginalPage = true; 1919 } 1920 1921 int finalPage; 1922 // We give flings precedence over large moves, which is why we short-circuit our 1923 // test for a large move if a fling has been registered. That is, a large 1924 // move to the left and fling to the right will register as a fling to the right. 1925 final boolean isRtl = isLayoutRtl(); 1926 boolean isDeltaXLeft = isRtl ? deltaX > 0 : deltaX < 0; 1927 boolean isVelocityXLeft = isRtl ? velocityX > 0 : velocityX < 0; 1928 if (((isSignificantMove && !isDeltaXLeft && !isFling) || 1929 (isFling && !isVelocityXLeft)) && mCurrentPage > 0) { 1930 finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1; 1931 snapToPageWithVelocity(finalPage, velocityX); 1932 } else if (((isSignificantMove && isDeltaXLeft && !isFling) || 1933 (isFling && isVelocityXLeft)) && 1934 mCurrentPage < getChildCount() - 1) { 1935 finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1; 1936 snapToPageWithVelocity(finalPage, velocityX); 1937 } else { 1938 snapToDestination(); 1939 } 1940 } else { 1941 if (!mScroller.isFinished()) { 1942 abortScrollerAnimation(true); 1943 } 1944 1945 float scaleX = getScaleX(); 1946 int vX = (int) (-velocityX * scaleX); 1947 int initialScrollX = (int) (getScrollX() * scaleX); 1948 1949 mScroller.setInterpolator(mDefaultInterpolator); 1950 mScroller.fling(initialScrollX, 1951 getScrollY(), vX, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); 1952 invalidate(); 1953 } 1954 onScrollInteractionEnd(); 1955 } else if (mTouchState == TOUCH_STATE_PREV_PAGE) { 1956 // at this point we have not moved beyond the touch slop 1957 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so 1958 // we can just page 1959 int nextPage = Math.max(0, mCurrentPage - 1); 1960 if (nextPage != mCurrentPage) { 1961 snapToPage(nextPage); 1962 } else { 1963 snapToDestination(); 1964 } 1965 } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) { 1966 // at this point we have not moved beyond the touch slop 1967 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so 1968 // we can just page 1969 int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1); 1970 if (nextPage != mCurrentPage) { 1971 snapToPage(nextPage); 1972 } else { 1973 snapToDestination(); 1974 } 1975 } else if (mTouchState == TOUCH_STATE_REORDERING) { 1976 // Update the last motion position 1977 mLastMotionX = ev.getX(); 1978 mLastMotionY = ev.getY(); 1979 1980 // Update the parent down so that our zoom animations take this new movement into 1981 // account 1982 float[] pt = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY); 1983 mParentDownMotionX = pt[0]; 1984 mParentDownMotionY = pt[1]; 1985 updateDragViewTranslationDuringDrag(); 1986 boolean handledFling = false; 1987 if (!DISABLE_FLING_TO_DELETE) { 1988 // Check the velocity and see if we are flinging-to-delete 1989 PointF flingToDeleteVector = isFlingingToDelete(); 1990 if (flingToDeleteVector != null) { 1991 onFlingToDelete(flingToDeleteVector); 1992 handledFling = true; 1993 } 1994 } 1995 if (!handledFling && isHoveringOverDeleteDropTarget((int) mParentDownMotionX, 1996 (int) mParentDownMotionY)) { 1997 onDropToDelete(); 1998 } 1999 } else { 2000 if (!mCancelTap) { 2001 onUnhandledTap(ev); 2002 } 2003 } 2004 2005 // Remove the callback to wait for the side page hover timeout 2006 removeCallbacks(mSidePageHoverRunnable); 2007 // End any intermediate reordering states 2008 resetTouchState(); 2009 break; 2010 2011 case MotionEvent.ACTION_CANCEL: 2012 if (mTouchState == TOUCH_STATE_SCROLLING) { 2013 snapToDestination(); 2014 } 2015 resetTouchState(); 2016 break; 2017 2018 case MotionEvent.ACTION_POINTER_UP: 2019 onSecondaryPointerUp(ev); 2020 releaseVelocityTracker(); 2021 break; 2022 } 2023 2024 return true; 2025 } 2026 onFlingToDelete(View v)2027 public void onFlingToDelete(View v) {} onRemoveView(View v, boolean deletePermanently)2028 public void onRemoveView(View v, boolean deletePermanently) {} onRemoveViewAnimationCompleted()2029 public void onRemoveViewAnimationCompleted() {} onAddView(View v, int index)2030 public void onAddView(View v, int index) {} 2031 resetTouchState()2032 private void resetTouchState() { 2033 releaseVelocityTracker(); 2034 endReordering(); 2035 mCancelTap = false; 2036 mTouchState = TOUCH_STATE_REST; 2037 mActivePointerId = INVALID_POINTER; 2038 } 2039 2040 /** 2041 * Triggered by scrolling via touch 2042 */ onScrollInteractionBegin()2043 protected void onScrollInteractionBegin() { 2044 } 2045 onScrollInteractionEnd()2046 protected void onScrollInteractionEnd() { 2047 } 2048 onUnhandledTap(MotionEvent ev)2049 protected void onUnhandledTap(MotionEvent ev) { 2050 ((Launcher) getContext()).onClick(this); 2051 } 2052 2053 @Override onGenericMotionEvent(MotionEvent event)2054 public boolean onGenericMotionEvent(MotionEvent event) { 2055 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { 2056 switch (event.getAction()) { 2057 case MotionEvent.ACTION_SCROLL: { 2058 // Handle mouse (or ext. device) by shifting the page depending on the scroll 2059 final float vscroll; 2060 final float hscroll; 2061 if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) { 2062 vscroll = 0; 2063 hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); 2064 } else { 2065 vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); 2066 hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); 2067 } 2068 if (hscroll != 0 || vscroll != 0) { 2069 boolean isForwardScroll = isLayoutRtl() ? (hscroll < 0 || vscroll < 0) 2070 : (hscroll > 0 || vscroll > 0); 2071 if (isForwardScroll) { 2072 scrollRight(); 2073 } else { 2074 scrollLeft(); 2075 } 2076 return true; 2077 } 2078 } 2079 } 2080 } 2081 return super.onGenericMotionEvent(event); 2082 } 2083 acquireVelocityTrackerAndAddMovement(MotionEvent ev)2084 private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { 2085 if (mVelocityTracker == null) { 2086 mVelocityTracker = VelocityTracker.obtain(); 2087 } 2088 mVelocityTracker.addMovement(ev); 2089 } 2090 releaseVelocityTracker()2091 private void releaseVelocityTracker() { 2092 if (mVelocityTracker != null) { 2093 mVelocityTracker.clear(); 2094 mVelocityTracker.recycle(); 2095 mVelocityTracker = null; 2096 } 2097 } 2098 onSecondaryPointerUp(MotionEvent ev)2099 private void onSecondaryPointerUp(MotionEvent ev) { 2100 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 2101 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 2102 final int pointerId = ev.getPointerId(pointerIndex); 2103 if (pointerId == mActivePointerId) { 2104 // This was our active pointer going up. Choose a new 2105 // active pointer and adjust accordingly. 2106 // TODO: Make this decision more intelligent. 2107 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 2108 mLastMotionX = mDownMotionX = ev.getX(newPointerIndex); 2109 mLastMotionY = ev.getY(newPointerIndex); 2110 mLastMotionXRemainder = 0; 2111 mActivePointerId = ev.getPointerId(newPointerIndex); 2112 if (mVelocityTracker != null) { 2113 mVelocityTracker.clear(); 2114 } 2115 } 2116 } 2117 2118 @Override requestChildFocus(View child, View focused)2119 public void requestChildFocus(View child, View focused) { 2120 super.requestChildFocus(child, focused); 2121 int page = indexToPage(indexOfChild(child)); 2122 if (page >= 0 && page != getCurrentPage() && !isInTouchMode()) { 2123 snapToPage(page); 2124 } 2125 } 2126 getChildWidth(int index)2127 protected int getChildWidth(int index) { 2128 return getPageAt(index).getMeasuredWidth(); 2129 } 2130 getPageNearestToPoint(float x)2131 int getPageNearestToPoint(float x) { 2132 int index = 0; 2133 for (int i = 0; i < getChildCount(); ++i) { 2134 if (x < getChildAt(i).getRight() - getScrollX()) { 2135 return index; 2136 } else { 2137 index++; 2138 } 2139 } 2140 return Math.min(index, getChildCount() - 1); 2141 } 2142 getPageNearestToCenterOfScreen()2143 int getPageNearestToCenterOfScreen() { 2144 int minDistanceFromScreenCenter = Integer.MAX_VALUE; 2145 int minDistanceFromScreenCenterIndex = -1; 2146 int screenCenter = getViewportOffsetX() + getScrollX() + (getViewportWidth() / 2); 2147 final int childCount = getChildCount(); 2148 for (int i = 0; i < childCount; ++i) { 2149 View layout = (View) getPageAt(i); 2150 int childWidth = layout.getMeasuredWidth(); 2151 int halfChildWidth = (childWidth / 2); 2152 int childCenter = getViewportOffsetX() + getChildOffset(i) + halfChildWidth; 2153 int distanceFromScreenCenter = Math.abs(childCenter - screenCenter); 2154 if (distanceFromScreenCenter < minDistanceFromScreenCenter) { 2155 minDistanceFromScreenCenter = distanceFromScreenCenter; 2156 minDistanceFromScreenCenterIndex = i; 2157 } 2158 } 2159 return minDistanceFromScreenCenterIndex; 2160 } 2161 isInOverScroll()2162 protected boolean isInOverScroll() { 2163 return (mOverScrollX > mMaxScrollX || mOverScrollX < 0); 2164 } 2165 getPageSnapDuration()2166 protected int getPageSnapDuration() { 2167 if (isInOverScroll()) { 2168 return OVER_SCROLL_PAGE_SNAP_ANIMATION_DURATION; 2169 } 2170 return PAGE_SNAP_ANIMATION_DURATION; 2171 2172 } 2173 snapToDestination()2174 protected void snapToDestination() { 2175 snapToPage(getPageNearestToCenterOfScreen(), getPageSnapDuration()); 2176 } 2177 2178 private static class ScrollInterpolator implements Interpolator { ScrollInterpolator()2179 public ScrollInterpolator() { 2180 } 2181 getInterpolation(float t)2182 public float getInterpolation(float t) { 2183 t -= 1.0f; 2184 return t*t*t*t*t + 1; 2185 } 2186 } 2187 2188 // We want the duration of the page snap animation to be influenced by the distance that 2189 // the screen has to travel, however, we don't want this duration to be effected in a 2190 // purely linear fashion. Instead, we use this method to moderate the effect that the distance 2191 // of travel has on the overall snap duration. distanceInfluenceForSnapDuration(float f)2192 float distanceInfluenceForSnapDuration(float f) { 2193 f -= 0.5f; // center the values about 0. 2194 f *= 0.3f * Math.PI / 2.0f; 2195 return (float) Math.sin(f); 2196 } 2197 snapToPageWithVelocity(int whichPage, int velocity)2198 protected void snapToPageWithVelocity(int whichPage, int velocity) { 2199 whichPage = validateNewPage(whichPage); 2200 int halfScreenSize = getViewportWidth() / 2; 2201 2202 final int newX = getScrollForPage(whichPage); 2203 int delta = newX - mUnboundedScrollX; 2204 int duration = 0; 2205 2206 if (Math.abs(velocity) < mMinFlingVelocity || isInOverScroll()) { 2207 // If the velocity is low enough, then treat this more as an automatic page advance 2208 // as opposed to an apparent physical response to flinging 2209 snapToPage(whichPage, getPageSnapDuration()); 2210 return; 2211 } 2212 2213 // Here we compute a "distance" that will be used in the computation of the overall 2214 // snap duration. This is a function of the actual distance that needs to be traveled; 2215 // we keep this value close to half screen size in order to reduce the variance in snap 2216 // duration as a function of the distance the page needs to travel. 2217 float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize)); 2218 float distance = halfScreenSize + halfScreenSize * 2219 distanceInfluenceForSnapDuration(distanceRatio); 2220 2221 velocity = Math.abs(velocity); 2222 velocity = Math.max(mMinSnapVelocity, velocity); 2223 2224 // we want the page's snap velocity to approximately match the velocity at which the 2225 // user flings, so we scale the duration by a value near to the derivative of the scroll 2226 // interpolator at zero, ie. 5. We use 4 to make it a little slower. 2227 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 2228 2229 snapToPage(whichPage, delta, duration); 2230 } 2231 snapToPage(int whichPage)2232 protected void snapToPage(int whichPage) { 2233 snapToPage(whichPage, getPageSnapDuration()); 2234 } 2235 snapToPageImmediately(int whichPage)2236 protected void snapToPageImmediately(int whichPage) { 2237 snapToPage(whichPage, getPageSnapDuration(), true, null); 2238 } 2239 snapToPage(int whichPage, int duration)2240 protected void snapToPage(int whichPage, int duration) { 2241 snapToPage(whichPage, duration, false, null); 2242 } 2243 snapToPage(int whichPage, int duration, TimeInterpolator interpolator)2244 protected void snapToPage(int whichPage, int duration, TimeInterpolator interpolator) { 2245 snapToPage(whichPage, duration, false, interpolator); 2246 } 2247 snapToPage(int whichPage, int duration, boolean immediate, TimeInterpolator interpolator)2248 protected void snapToPage(int whichPage, int duration, boolean immediate, 2249 TimeInterpolator interpolator) { 2250 whichPage = validateNewPage(whichPage); 2251 2252 int newX = getScrollForPage(whichPage); 2253 final int sX = mUnboundedScrollX; 2254 final int delta = newX - sX; 2255 snapToPage(whichPage, delta, duration, immediate, interpolator); 2256 } 2257 snapToPage(int whichPage, int delta, int duration)2258 protected void snapToPage(int whichPage, int delta, int duration) { 2259 snapToPage(whichPage, delta, duration, false, null); 2260 } 2261 snapToPage(int whichPage, int delta, int duration, boolean immediate, TimeInterpolator interpolator)2262 protected void snapToPage(int whichPage, int delta, int duration, boolean immediate, 2263 TimeInterpolator interpolator) { 2264 whichPage = validateNewPage(whichPage); 2265 2266 mNextPage = whichPage; 2267 View focusedChild = getFocusedChild(); 2268 if (focusedChild != null && whichPage != mCurrentPage && 2269 focusedChild == getPageAt(mCurrentPage)) { 2270 focusedChild.clearFocus(); 2271 } 2272 2273 sendScrollAccessibilityEvent(); 2274 2275 pageBeginMoving(); 2276 awakenScrollBars(duration); 2277 if (immediate) { 2278 duration = 0; 2279 } else if (duration == 0) { 2280 duration = Math.abs(delta); 2281 } 2282 2283 if (!mScroller.isFinished()) { 2284 abortScrollerAnimation(false); 2285 } 2286 2287 if (interpolator != null) { 2288 mScroller.setInterpolator(interpolator); 2289 } else { 2290 mScroller.setInterpolator(mDefaultInterpolator); 2291 } 2292 2293 mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration); 2294 2295 updatePageIndicator(); 2296 2297 // Trigger a compute() to finish switching pages if necessary 2298 if (immediate) { 2299 computeScroll(); 2300 } 2301 2302 // Defer loading associated pages until the scroll settles 2303 mDeferLoadAssociatedPagesUntilScrollCompletes = true; 2304 2305 mForceScreenScrolled = true; 2306 invalidate(); 2307 } 2308 scrollLeft()2309 public void scrollLeft() { 2310 if (getNextPage() > 0) snapToPage(getNextPage() - 1); 2311 } 2312 scrollRight()2313 public void scrollRight() { 2314 if (getNextPage() < getChildCount() -1) snapToPage(getNextPage() + 1); 2315 } 2316 getPageForView(View v)2317 public int getPageForView(View v) { 2318 int result = -1; 2319 if (v != null) { 2320 ViewParent vp = v.getParent(); 2321 int count = getChildCount(); 2322 for (int i = 0; i < count; i++) { 2323 if (vp == getPageAt(i)) { 2324 return i; 2325 } 2326 } 2327 } 2328 return result; 2329 } 2330 2331 /** 2332 * @return True is long presses are still allowed for the current touch 2333 */ allowLongPress()2334 public boolean allowLongPress() { 2335 return mAllowLongPress; 2336 } 2337 2338 @Override performLongClick()2339 public boolean performLongClick() { 2340 mCancelTap = true; 2341 return super.performLongClick(); 2342 } 2343 2344 /** 2345 * Set true to allow long-press events to be triggered, usually checked by 2346 * {@link Launcher} to accept or block dpad-initiated long-presses. 2347 */ setAllowLongPress(boolean allowLongPress)2348 public void setAllowLongPress(boolean allowLongPress) { 2349 mAllowLongPress = allowLongPress; 2350 } 2351 2352 public static class SavedState extends BaseSavedState { 2353 int currentPage = -1; 2354 SavedState(Parcelable superState)2355 SavedState(Parcelable superState) { 2356 super(superState); 2357 } 2358 SavedState(Parcel in)2359 private SavedState(Parcel in) { 2360 super(in); 2361 currentPage = in.readInt(); 2362 } 2363 2364 @Override writeToParcel(Parcel out, int flags)2365 public void writeToParcel(Parcel out, int flags) { 2366 super.writeToParcel(out, flags); 2367 out.writeInt(currentPage); 2368 } 2369 2370 public static final Parcelable.Creator<SavedState> CREATOR = 2371 new Parcelable.Creator<SavedState>() { 2372 public SavedState createFromParcel(Parcel in) { 2373 return new SavedState(in); 2374 } 2375 2376 public SavedState[] newArray(int size) { 2377 return new SavedState[size]; 2378 } 2379 }; 2380 } 2381 loadAssociatedPages(int page)2382 protected void loadAssociatedPages(int page) { 2383 loadAssociatedPages(page, false); 2384 } loadAssociatedPages(int page, boolean immediateAndOnly)2385 protected void loadAssociatedPages(int page, boolean immediateAndOnly) { 2386 if (mContentIsRefreshable) { 2387 final int count = getChildCount(); 2388 if (page < count) { 2389 int lowerPageBound = getAssociatedLowerPageBound(page); 2390 int upperPageBound = getAssociatedUpperPageBound(page); 2391 if (DEBUG) Log.d(TAG, "loadAssociatedPages: " + lowerPageBound + "/" 2392 + upperPageBound); 2393 // First, clear any pages that should no longer be loaded 2394 for (int i = 0; i < count; ++i) { 2395 Page layout = (Page) getPageAt(i); 2396 if ((i < lowerPageBound) || (i > upperPageBound)) { 2397 if (layout.getPageChildCount() > 0) { 2398 layout.removeAllViewsOnPage(); 2399 } 2400 mDirtyPageContent.set(i, true); 2401 } 2402 } 2403 // Next, load any new pages 2404 for (int i = 0; i < count; ++i) { 2405 if ((i != page) && immediateAndOnly) { 2406 continue; 2407 } 2408 if (lowerPageBound <= i && i <= upperPageBound) { 2409 if (mDirtyPageContent.get(i)) { 2410 syncPageItems(i, (i == page) && immediateAndOnly); 2411 mDirtyPageContent.set(i, false); 2412 } 2413 } 2414 } 2415 } 2416 } 2417 } 2418 getAssociatedLowerPageBound(int page)2419 protected int getAssociatedLowerPageBound(int page) { 2420 return Math.max(0, page - 1); 2421 } getAssociatedUpperPageBound(int page)2422 protected int getAssociatedUpperPageBound(int page) { 2423 final int count = getChildCount(); 2424 return Math.min(page + 1, count - 1); 2425 } 2426 2427 /** 2428 * This method is called ONLY to synchronize the number of pages that the paged view has. 2429 * To actually fill the pages with information, implement syncPageItems() below. It is 2430 * guaranteed that syncPageItems() will be called for a particular page before it is shown, 2431 * and therefore, individual page items do not need to be updated in this method. 2432 */ 2433 public abstract void syncPages(); 2434 2435 /** 2436 * This method is called to synchronize the items that are on a particular page. If views on 2437 * the page can be reused, then they should be updated within this method. 2438 */ 2439 public abstract void syncPageItems(int page, boolean immediate); 2440 invalidatePageData()2441 protected void invalidatePageData() { 2442 invalidatePageData(-1, false); 2443 } invalidatePageData(int currentPage)2444 protected void invalidatePageData(int currentPage) { 2445 invalidatePageData(currentPage, false); 2446 } invalidatePageData(int currentPage, boolean immediateAndOnly)2447 protected void invalidatePageData(int currentPage, boolean immediateAndOnly) { 2448 if (!mIsDataReady) { 2449 return; 2450 } 2451 2452 if (mContentIsRefreshable) { 2453 // Force all scrolling-related behavior to end 2454 forceFinishScroller(); 2455 2456 // Update all the pages 2457 syncPages(); 2458 2459 // We must force a measure after we've loaded the pages to update the content width and 2460 // to determine the full scroll width 2461 measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), 2462 MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY)); 2463 2464 // Set a new page as the current page if necessary 2465 if (currentPage > -1) { 2466 setCurrentPage(Math.min(getPageCount() - 1, currentPage)); 2467 } 2468 2469 // Mark each of the pages as dirty 2470 final int count = getChildCount(); 2471 mDirtyPageContent.clear(); 2472 for (int i = 0; i < count; ++i) { 2473 mDirtyPageContent.add(true); 2474 } 2475 2476 // Load any pages that are necessary for the current window of views 2477 loadAssociatedPages(mCurrentPage, immediateAndOnly); 2478 requestLayout(); 2479 } 2480 if (isPageMoving()) { 2481 // If the page is moving, then snap it to the final position to ensure we don't get 2482 // stuck between pages 2483 snapToDestination(); 2484 } 2485 } 2486 2487 // Animate the drag view back to the original position animateDragViewToOriginalPosition()2488 void animateDragViewToOriginalPosition() { 2489 if (mDragView != null) { 2490 AnimatorSet anim = new AnimatorSet(); 2491 anim.setDuration(REORDERING_DROP_REPOSITION_DURATION); 2492 anim.playTogether( 2493 ObjectAnimator.ofFloat(mDragView, "translationX", 0f), 2494 ObjectAnimator.ofFloat(mDragView, "translationY", 0f), 2495 ObjectAnimator.ofFloat(mDragView, "scaleX", 1f), 2496 ObjectAnimator.ofFloat(mDragView, "scaleY", 1f)); 2497 anim.addListener(new AnimatorListenerAdapter() { 2498 @Override 2499 public void onAnimationEnd(Animator animation) { 2500 onPostReorderingAnimationCompleted(); 2501 } 2502 }); 2503 anim.start(); 2504 } 2505 } 2506 onStartReordering()2507 protected void onStartReordering() { 2508 // Set the touch state to reordering (allows snapping to pages, dragging a child, etc.) 2509 mTouchState = TOUCH_STATE_REORDERING; 2510 mIsReordering = true; 2511 2512 // We must invalidate to trigger a redraw to update the layers such that the drag view 2513 // is always drawn on top 2514 invalidate(); 2515 } 2516 onPostReorderingAnimationCompleted()2517 private void onPostReorderingAnimationCompleted() { 2518 // Trigger the callback when reordering has settled 2519 --mPostReorderingPreZoomInRemainingAnimationCount; 2520 if (mPostReorderingPreZoomInRunnable != null && 2521 mPostReorderingPreZoomInRemainingAnimationCount == 0) { 2522 mPostReorderingPreZoomInRunnable.run(); 2523 mPostReorderingPreZoomInRunnable = null; 2524 } 2525 } 2526 onEndReordering()2527 protected void onEndReordering() { 2528 mIsReordering = false; 2529 } 2530 startReordering(View v)2531 public boolean startReordering(View v) { 2532 int dragViewIndex = indexOfChild(v); 2533 2534 if (mTouchState != TOUCH_STATE_REST || dragViewIndex == -1) return false; 2535 2536 mTempVisiblePagesRange[0] = 0; 2537 mTempVisiblePagesRange[1] = getPageCount() - 1; 2538 getFreeScrollPageRange(mTempVisiblePagesRange); 2539 mReorderingStarted = true; 2540 2541 // Check if we are within the reordering range 2542 if (mTempVisiblePagesRange[0] <= dragViewIndex && 2543 dragViewIndex <= mTempVisiblePagesRange[1]) { 2544 // Find the drag view under the pointer 2545 mDragView = getChildAt(dragViewIndex); 2546 mDragView.animate().scaleX(1.15f).scaleY(1.15f).setDuration(100).start(); 2547 mDragViewBaselineLeft = mDragView.getLeft(); 2548 snapToPage(getPageNearestToCenterOfScreen()); 2549 disableFreeScroll(); 2550 onStartReordering(); 2551 return true; 2552 } 2553 return false; 2554 } 2555 isReordering(boolean testTouchState)2556 boolean isReordering(boolean testTouchState) { 2557 boolean state = mIsReordering; 2558 if (testTouchState) { 2559 state &= (mTouchState == TOUCH_STATE_REORDERING); 2560 } 2561 return state; 2562 } endReordering()2563 void endReordering() { 2564 // For simplicity, we call endReordering sometimes even if reordering was never started. 2565 // In that case, we don't want to do anything. 2566 if (!mReorderingStarted) return; 2567 mReorderingStarted = false; 2568 2569 // If we haven't flung-to-delete the current child, then we just animate the drag view 2570 // back into position 2571 final Runnable onCompleteRunnable = new Runnable() { 2572 @Override 2573 public void run() { 2574 onEndReordering(); 2575 } 2576 }; 2577 if (!mDeferringForDelete) { 2578 mPostReorderingPreZoomInRunnable = new Runnable() { 2579 public void run() { 2580 onCompleteRunnable.run(); 2581 enableFreeScroll(); 2582 }; 2583 }; 2584 2585 mPostReorderingPreZoomInRemainingAnimationCount = 2586 NUM_ANIMATIONS_RUNNING_BEFORE_ZOOM_OUT; 2587 // Snap to the current page 2588 snapToPage(indexOfChild(mDragView), 0); 2589 // Animate the drag view back to the front position 2590 animateDragViewToOriginalPosition(); 2591 } else { 2592 // Handled in post-delete-animation-callbacks 2593 } 2594 } 2595 2596 /* 2597 * Flinging to delete - IN PROGRESS 2598 */ isFlingingToDelete()2599 private PointF isFlingingToDelete() { 2600 ViewConfiguration config = ViewConfiguration.get(getContext()); 2601 mVelocityTracker.computeCurrentVelocity(1000, config.getScaledMaximumFlingVelocity()); 2602 2603 if (mVelocityTracker.getYVelocity() < mFlingToDeleteThresholdVelocity) { 2604 // Do a quick dot product test to ensure that we are flinging upwards 2605 PointF vel = new PointF(mVelocityTracker.getXVelocity(), 2606 mVelocityTracker.getYVelocity()); 2607 PointF upVec = new PointF(0f, -1f); 2608 float theta = (float) Math.acos(((vel.x * upVec.x) + (vel.y * upVec.y)) / 2609 (vel.length() * upVec.length())); 2610 if (theta <= Math.toRadians(FLING_TO_DELETE_MAX_FLING_DEGREES)) { 2611 return vel; 2612 } 2613 } 2614 return null; 2615 } 2616 2617 /** 2618 * Creates an animation from the current drag view along its current velocity vector. 2619 * For this animation, the alpha runs for a fixed duration and we update the position 2620 * progressively. 2621 */ 2622 private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener { 2623 private View mDragView; 2624 private PointF mVelocity; 2625 private Rect mFrom; 2626 private long mPrevTime; 2627 private float mFriction; 2628 2629 private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f); 2630 FlingAlongVectorAnimatorUpdateListener(View dragView, PointF vel, Rect from, long startTime, float friction)2631 public FlingAlongVectorAnimatorUpdateListener(View dragView, PointF vel, Rect from, 2632 long startTime, float friction) { 2633 mDragView = dragView; 2634 mVelocity = vel; 2635 mFrom = from; 2636 mPrevTime = startTime; 2637 mFriction = 1f - (mDragView.getResources().getDisplayMetrics().density * friction); 2638 } 2639 2640 @Override onAnimationUpdate(ValueAnimator animation)2641 public void onAnimationUpdate(ValueAnimator animation) { 2642 float t = ((Float) animation.getAnimatedValue()).floatValue(); 2643 long curTime = AnimationUtils.currentAnimationTimeMillis(); 2644 2645 mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f); 2646 mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f); 2647 2648 mDragView.setTranslationX(mFrom.left); 2649 mDragView.setTranslationY(mFrom.top); 2650 mDragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t)); 2651 2652 mVelocity.x *= mFriction; 2653 mVelocity.y *= mFriction; 2654 mPrevTime = curTime; 2655 } 2656 }; 2657 2658 private static final int ANIM_TAG_KEY = 100; 2659 createPostDeleteAnimationRunnable(final View dragView)2660 private Runnable createPostDeleteAnimationRunnable(final View dragView) { 2661 return new Runnable() { 2662 @Override 2663 public void run() { 2664 int dragViewIndex = indexOfChild(dragView); 2665 2666 // For each of the pages around the drag view, animate them from the previous 2667 // position to the new position in the layout (as a result of the drag view moving 2668 // in the layout) 2669 // NOTE: We can make an assumption here because we have side-bound pages that we 2670 // will always have pages to animate in from the left 2671 getFreeScrollPageRange(mTempVisiblePagesRange); 2672 boolean isLastWidgetPage = (mTempVisiblePagesRange[0] == mTempVisiblePagesRange[1]); 2673 boolean slideFromLeft = (isLastWidgetPage || 2674 dragViewIndex > mTempVisiblePagesRange[0]); 2675 2676 // Setup the scroll to the correct page before we swap the views 2677 if (slideFromLeft) { 2678 snapToPageImmediately(dragViewIndex - 1); 2679 } 2680 2681 int firstIndex = (isLastWidgetPage ? 0 : mTempVisiblePagesRange[0]); 2682 int lastIndex = Math.min(mTempVisiblePagesRange[1], getPageCount() - 1); 2683 int lowerIndex = (slideFromLeft ? firstIndex : dragViewIndex + 1 ); 2684 int upperIndex = (slideFromLeft ? dragViewIndex - 1 : lastIndex); 2685 ArrayList<Animator> animations = new ArrayList<Animator>(); 2686 for (int i = lowerIndex; i <= upperIndex; ++i) { 2687 View v = getChildAt(i); 2688 // dragViewIndex < pageUnderPointIndex, so after we remove the 2689 // drag view all subsequent views to pageUnderPointIndex will 2690 // shift down. 2691 int oldX = 0; 2692 int newX = 0; 2693 if (slideFromLeft) { 2694 if (i == 0) { 2695 // Simulate the page being offscreen with the page spacing 2696 oldX = getViewportOffsetX() + getChildOffset(i) - getChildWidth(i) 2697 - mPageSpacing; 2698 } else { 2699 oldX = getViewportOffsetX() + getChildOffset(i - 1); 2700 } 2701 newX = getViewportOffsetX() + getChildOffset(i); 2702 } else { 2703 oldX = getChildOffset(i) - getChildOffset(i - 1); 2704 newX = 0; 2705 } 2706 2707 // Animate the view translation from its old position to its new 2708 // position 2709 AnimatorSet anim = (AnimatorSet) v.getTag(); 2710 if (anim != null) { 2711 anim.cancel(); 2712 } 2713 2714 // Note: Hacky, but we want to skip any optimizations to not draw completely 2715 // hidden views 2716 v.setAlpha(Math.max(v.getAlpha(), 0.01f)); 2717 v.setTranslationX(oldX - newX); 2718 anim = new AnimatorSet(); 2719 anim.playTogether( 2720 ObjectAnimator.ofFloat(v, "translationX", 0f), 2721 ObjectAnimator.ofFloat(v, "alpha", 1f)); 2722 animations.add(anim); 2723 v.setTag(ANIM_TAG_KEY, anim); 2724 } 2725 2726 AnimatorSet slideAnimations = new AnimatorSet(); 2727 slideAnimations.playTogether(animations); 2728 slideAnimations.setDuration(DELETE_SLIDE_IN_SIDE_PAGE_DURATION); 2729 slideAnimations.addListener(new AnimatorListenerAdapter() { 2730 @Override 2731 public void onAnimationEnd(Animator animation) { 2732 mDeferringForDelete = false; 2733 onEndReordering(); 2734 onRemoveViewAnimationCompleted(); 2735 } 2736 }); 2737 slideAnimations.start(); 2738 2739 removeView(dragView); 2740 onRemoveView(dragView, true); 2741 } 2742 }; 2743 } 2744 onFlingToDelete(PointF vel)2745 public void onFlingToDelete(PointF vel) { 2746 final long startTime = AnimationUtils.currentAnimationTimeMillis(); 2747 2748 // NOTE: Because it takes time for the first frame of animation to actually be 2749 // called and we expect the animation to be a continuation of the fling, we have 2750 // to account for the time that has elapsed since the fling finished. And since 2751 // we don't have a startDelay, we will always get call to update when we call 2752 // start() (which we want to ignore). 2753 final TimeInterpolator tInterpolator = new TimeInterpolator() { 2754 private int mCount = -1; 2755 private long mStartTime; 2756 private float mOffset; 2757 /* Anonymous inner class ctor */ { 2758 mStartTime = startTime; 2759 } 2760 2761 @Override 2762 public float getInterpolation(float t) { 2763 if (mCount < 0) { 2764 mCount++; 2765 } else if (mCount == 0) { 2766 mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() - 2767 mStartTime) / FLING_TO_DELETE_FADE_OUT_DURATION); 2768 mCount++; 2769 } 2770 return Math.min(1f, mOffset + t); 2771 } 2772 }; 2773 2774 final Rect from = new Rect(); 2775 final View dragView = mDragView; 2776 from.left = (int) dragView.getTranslationX(); 2777 from.top = (int) dragView.getTranslationY(); 2778 AnimatorUpdateListener updateCb = new FlingAlongVectorAnimatorUpdateListener(dragView, vel, 2779 from, startTime, FLING_TO_DELETE_FRICTION); 2780 2781 final Runnable onAnimationEndRunnable = createPostDeleteAnimationRunnable(dragView); 2782 2783 // Create and start the animation 2784 ValueAnimator mDropAnim = new ValueAnimator(); 2785 mDropAnim.setInterpolator(tInterpolator); 2786 mDropAnim.setDuration(FLING_TO_DELETE_FADE_OUT_DURATION); 2787 mDropAnim.setFloatValues(0f, 1f); 2788 mDropAnim.addUpdateListener(updateCb); 2789 mDropAnim.addListener(new AnimatorListenerAdapter() { 2790 public void onAnimationEnd(Animator animation) { 2791 onAnimationEndRunnable.run(); 2792 } 2793 }); 2794 mDropAnim.start(); 2795 mDeferringForDelete = true; 2796 } 2797 2798 /* Drag to delete */ isHoveringOverDeleteDropTarget(int x, int y)2799 private boolean isHoveringOverDeleteDropTarget(int x, int y) { 2800 if (mDeleteDropTarget != null) { 2801 mAltTmpRect.set(0, 0, 0, 0); 2802 View parent = (View) mDeleteDropTarget.getParent(); 2803 if (parent != null) { 2804 parent.getGlobalVisibleRect(mAltTmpRect); 2805 } 2806 mDeleteDropTarget.getGlobalVisibleRect(mTmpRect); 2807 mTmpRect.offset(-mAltTmpRect.left, -mAltTmpRect.top); 2808 return mTmpRect.contains(x, y); 2809 } 2810 return false; 2811 } 2812 setPageHoveringOverDeleteDropTarget(int viewIndex, boolean isHovering)2813 protected void setPageHoveringOverDeleteDropTarget(int viewIndex, boolean isHovering) {} 2814 onDropToDelete()2815 private void onDropToDelete() { 2816 final View dragView = mDragView; 2817 2818 final float toScale = 0f; 2819 final float toAlpha = 0f; 2820 2821 // Create and start the complex animation 2822 ArrayList<Animator> animations = new ArrayList<Animator>(); 2823 AnimatorSet motionAnim = new AnimatorSet(); 2824 motionAnim.setInterpolator(new DecelerateInterpolator(2)); 2825 motionAnim.playTogether( 2826 ObjectAnimator.ofFloat(dragView, "scaleX", toScale), 2827 ObjectAnimator.ofFloat(dragView, "scaleY", toScale)); 2828 animations.add(motionAnim); 2829 2830 AnimatorSet alphaAnim = new AnimatorSet(); 2831 alphaAnim.setInterpolator(new LinearInterpolator()); 2832 alphaAnim.playTogether( 2833 ObjectAnimator.ofFloat(dragView, "alpha", toAlpha)); 2834 animations.add(alphaAnim); 2835 2836 final Runnable onAnimationEndRunnable = createPostDeleteAnimationRunnable(dragView); 2837 2838 AnimatorSet anim = new AnimatorSet(); 2839 anim.playTogether(animations); 2840 anim.setDuration(DRAG_TO_DELETE_FADE_OUT_DURATION); 2841 anim.addListener(new AnimatorListenerAdapter() { 2842 public void onAnimationEnd(Animator animation) { 2843 onAnimationEndRunnable.run(); 2844 } 2845 }); 2846 anim.start(); 2847 2848 mDeferringForDelete = true; 2849 } 2850 2851 /* Accessibility */ 2852 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)2853 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 2854 super.onInitializeAccessibilityNodeInfo(info); 2855 info.setScrollable(getPageCount() > 1); 2856 if (getCurrentPage() < getPageCount() - 1) { 2857 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); 2858 } 2859 if (getCurrentPage() > 0) { 2860 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 2861 } 2862 } 2863 2864 @Override sendAccessibilityEvent(int eventType)2865 public void sendAccessibilityEvent(int eventType) { 2866 // Don't let the view send real scroll events. 2867 if (eventType != AccessibilityEvent.TYPE_VIEW_SCROLLED) { 2868 super.sendAccessibilityEvent(eventType); 2869 } 2870 } 2871 2872 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)2873 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 2874 super.onInitializeAccessibilityEvent(event); 2875 event.setScrollable(true); 2876 } 2877 2878 @Override performAccessibilityAction(int action, Bundle arguments)2879 public boolean performAccessibilityAction(int action, Bundle arguments) { 2880 if (super.performAccessibilityAction(action, arguments)) { 2881 return true; 2882 } 2883 switch (action) { 2884 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 2885 if (getCurrentPage() < getPageCount() - 1) { 2886 scrollRight(); 2887 return true; 2888 } 2889 } break; 2890 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 2891 if (getCurrentPage() > 0) { 2892 scrollLeft(); 2893 return true; 2894 } 2895 } break; 2896 } 2897 return false; 2898 } 2899 getCurrentPageDescription()2900 protected String getCurrentPageDescription() { 2901 return String.format(getContext().getString(R.string.default_scroll_format), 2902 getNextPage() + 1, getChildCount()); 2903 } 2904 2905 @Override onHoverEvent(android.view.MotionEvent event)2906 public boolean onHoverEvent(android.view.MotionEvent event) { 2907 return true; 2908 } 2909 } 2910