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