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