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