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