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