1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.keyguard; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.TimeInterpolator; 24 import android.animation.ValueAnimator; 25 import android.animation.ValueAnimator.AnimatorUpdateListener; 26 import android.content.Context; 27 import android.content.res.Resources; 28 import android.content.res.TypedArray; 29 import android.graphics.Canvas; 30 import android.graphics.Matrix; 31 import android.graphics.PointF; 32 import android.graphics.Rect; 33 import android.os.Bundle; 34 import android.os.Parcel; 35 import android.os.Parcelable; 36 import android.util.AttributeSet; 37 import android.util.DisplayMetrics; 38 import android.util.Log; 39 import android.view.InputDevice; 40 import android.view.KeyEvent; 41 import android.view.MotionEvent; 42 import android.view.VelocityTracker; 43 import android.view.View; 44 import android.view.ViewConfiguration; 45 import android.view.ViewGroup; 46 import android.view.ViewParent; 47 import android.view.ViewPropertyAnimator; 48 import android.view.accessibility.AccessibilityEvent; 49 import android.view.accessibility.AccessibilityManager; 50 import android.view.accessibility.AccessibilityNodeInfo; 51 import android.view.animation.AccelerateInterpolator; 52 import android.view.animation.AnimationUtils; 53 import android.view.animation.DecelerateInterpolator; 54 import android.view.animation.Interpolator; 55 import android.view.animation.LinearInterpolator; 56 import android.widget.Scroller; 57 58 import java.util.ArrayList; 59 60 /** 61 * An abstraction of the original Workspace which supports browsing through a 62 * sequential list of "pages" 63 */ 64 public 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 (isWarping()) { 1431 KeyguardWidgetFrame v = (KeyguardWidgetFrame) getPageAt(mPageWarpIndex); 1432 v.setTranslationX(v.getTranslationX() - deltaX); 1433 } else if (!mDeferScrollUpdate) { 1434 scrollBy((int) deltaX, 0); 1435 if (DEBUG) Log.d(TAG, "onTouchEvent().Scrolling: " + deltaX); 1436 } else { 1437 invalidate(); 1438 } 1439 mLastMotionX = x; 1440 mLastMotionXRemainder = deltaX - (int) deltaX; 1441 } else { 1442 awakenScrollBars(); 1443 } 1444 } else if (mTouchState == TOUCH_STATE_REORDERING) { 1445 // Update the last motion position 1446 mLastMotionX = ev.getX(); 1447 mLastMotionY = ev.getY(); 1448 1449 // Update the parent down so that our zoom animations take this new movement into 1450 // account 1451 float[] pt = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY); 1452 mParentDownMotionX = pt[0]; 1453 mParentDownMotionY = pt[1]; 1454 updateDragViewTranslationDuringDrag(); 1455 1456 // Find the closest page to the touch point 1457 final int dragViewIndex = indexOfChild(mDragView); 1458 int bufferSize = (int) (REORDERING_SIDE_PAGE_BUFFER_PERCENTAGE * 1459 getViewportWidth()); 1460 int leftBufferEdge = (int) (mapPointFromViewToParent(this, mViewport.left, 0)[0] 1461 + bufferSize); 1462 int rightBufferEdge = (int) (mapPointFromViewToParent(this, mViewport.right, 0)[0] 1463 - bufferSize); 1464 1465 // Change the drag view if we are hovering over the drop target 1466 boolean isHoveringOverDelete = isHoveringOverDeleteDropTarget( 1467 (int) mParentDownMotionX, (int) mParentDownMotionY); 1468 setPageHoveringOverDeleteDropTarget(dragViewIndex, isHoveringOverDelete); 1469 1470 if (DEBUG) Log.d(TAG, "leftBufferEdge: " + leftBufferEdge); 1471 if (DEBUG) Log.d(TAG, "rightBufferEdge: " + rightBufferEdge); 1472 if (DEBUG) Log.d(TAG, "mLastMotionX: " + mLastMotionX); 1473 if (DEBUG) Log.d(TAG, "mLastMotionY: " + mLastMotionY); 1474 if (DEBUG) Log.d(TAG, "mParentDownMotionX: " + mParentDownMotionX); 1475 if (DEBUG) Log.d(TAG, "mParentDownMotionY: " + mParentDownMotionY); 1476 1477 float parentX = mParentDownMotionX; 1478 int pageIndexToSnapTo = -1; 1479 if (parentX < leftBufferEdge && dragViewIndex > 0) { 1480 pageIndexToSnapTo = dragViewIndex - 1; 1481 } else if (parentX > rightBufferEdge && dragViewIndex < getChildCount() - 1) { 1482 pageIndexToSnapTo = dragViewIndex + 1; 1483 } 1484 1485 final int pageUnderPointIndex = pageIndexToSnapTo; 1486 if (pageUnderPointIndex > -1 && !isHoveringOverDelete) { 1487 mTempVisiblePagesRange[0] = 0; 1488 mTempVisiblePagesRange[1] = getPageCount() - 1; 1489 boundByReorderablePages(true, mTempVisiblePagesRange); 1490 if (mTempVisiblePagesRange[0] <= pageUnderPointIndex && 1491 pageUnderPointIndex <= mTempVisiblePagesRange[1] && 1492 pageUnderPointIndex != mSidePageHoverIndex && mScroller.isFinished()) { 1493 mSidePageHoverIndex = pageUnderPointIndex; 1494 mSidePageHoverRunnable = new Runnable() { 1495 @Override 1496 public void run() { 1497 // Update the down scroll position to account for the fact that the 1498 // current page is moved 1499 mDownScrollX = getChildOffset(pageUnderPointIndex) 1500 - getRelativeChildOffset(pageUnderPointIndex); 1501 1502 // Setup the scroll to the correct page before we swap the views 1503 snapToPage(pageUnderPointIndex); 1504 1505 // For each of the pages between the paged view and the drag view, 1506 // animate them from the previous position to the new position in 1507 // the layout (as a result of the drag view moving in the layout) 1508 int shiftDelta = (dragViewIndex < pageUnderPointIndex) ? -1 : 1; 1509 int lowerIndex = (dragViewIndex < pageUnderPointIndex) ? 1510 dragViewIndex + 1 : pageUnderPointIndex; 1511 int upperIndex = (dragViewIndex > pageUnderPointIndex) ? 1512 dragViewIndex - 1 : pageUnderPointIndex; 1513 for (int i = lowerIndex; i <= upperIndex; ++i) { 1514 View v = getChildAt(i); 1515 // dragViewIndex < pageUnderPointIndex, so after we remove the 1516 // drag view all subsequent views to pageUnderPointIndex will 1517 // shift down. 1518 int oldX = getViewportOffsetX() + getChildOffset(i); 1519 int newX = getViewportOffsetX() + getChildOffset(i + shiftDelta); 1520 1521 // Animate the view translation from its old position to its new 1522 // position 1523 AnimatorSet anim = (AnimatorSet) v.getTag(); 1524 if (anim != null) { 1525 anim.cancel(); 1526 } 1527 1528 v.setTranslationX(oldX - newX); 1529 anim = new AnimatorSet(); 1530 anim.setDuration(REORDERING_REORDER_REPOSITION_DURATION); 1531 anim.playTogether( 1532 ObjectAnimator.ofFloat(v, "translationX", 0f)); 1533 anim.start(); 1534 v.setTag(anim); 1535 } 1536 1537 removeView(mDragView); 1538 onRemoveView(mDragView, false); 1539 addView(mDragView, pageUnderPointIndex); 1540 onAddView(mDragView, pageUnderPointIndex); 1541 mSidePageHoverIndex = -1; 1542 } 1543 }; 1544 postDelayed(mSidePageHoverRunnable, REORDERING_SIDE_PAGE_HOVER_TIMEOUT); 1545 } 1546 } else { 1547 removeCallbacks(mSidePageHoverRunnable); 1548 mSidePageHoverIndex = -1; 1549 } 1550 } else if (mIsCameraEvent || determineScrollingStart(ev)) { 1551 startScrolling(ev); 1552 } 1553 break; 1554 1555 case MotionEvent.ACTION_UP: 1556 if (mTouchState == TOUCH_STATE_SCROLLING) { 1557 final int activePointerId = mActivePointerId; 1558 final int pointerIndex = ev.findPointerIndex(activePointerId); 1559 1560 if (pointerIndex == -1) return true; 1561 1562 final float x = ev.getX(pointerIndex); 1563 final VelocityTracker velocityTracker = mVelocityTracker; 1564 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1565 int velocityX = (int) velocityTracker.getXVelocity(activePointerId); 1566 final int deltaX = (int) (x - mDownMotionX); 1567 final int pageWidth = getScaledMeasuredWidth(getPageAt(mCurrentPage)); 1568 boolean isSignificantMove = Math.abs(deltaX) > pageWidth * 1569 SIGNIFICANT_MOVE_THRESHOLD; 1570 1571 mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x); 1572 1573 boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING && 1574 Math.abs(velocityX) > mFlingThresholdVelocity; 1575 1576 // In the case that the page is moved far to one direction and then is flung 1577 // in the opposite direction, we use a threshold to determine whether we should 1578 // just return to the starting page, or if we should skip one further. 1579 boolean returnToOriginalPage = false; 1580 if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD && 1581 Math.signum(velocityX) != Math.signum(deltaX) && isFling) { 1582 returnToOriginalPage = true; 1583 } 1584 1585 int finalPage; 1586 // We give flings precedence over large moves, which is why we short-circuit our 1587 // test for a large move if a fling has been registered. That is, a large 1588 // move to the left and fling to the right will register as a fling to the right. 1589 if (((isSignificantMove && deltaX > 0 && !isFling) || 1590 (isFling && velocityX > 0)) && mCurrentPage > 0) { 1591 finalPage = returnToOriginalPage || isWarping() 1592 ? mCurrentPage : mCurrentPage - 1; 1593 snapToPageWithVelocity(finalPage, velocityX); 1594 } else if (((isSignificantMove && deltaX < 0 && !isFling) || 1595 (isFling && velocityX < 0)) && 1596 mCurrentPage < getChildCount() - 1) { 1597 finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1; 1598 snapToPageWithVelocity(finalPage, velocityX); 1599 } else { 1600 snapToDestination(); 1601 } 1602 } else if (mTouchState == TOUCH_STATE_PREV_PAGE) { 1603 // at this point we have not moved beyond the touch slop 1604 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so 1605 // we can just page 1606 int nextPage = Math.max(0, mCurrentPage - 1); 1607 if (nextPage != mCurrentPage) { 1608 snapToPage(nextPage); 1609 } else { 1610 snapToDestination(); 1611 } 1612 } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) { 1613 // at this point we have not moved beyond the touch slop 1614 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so 1615 // we can just page 1616 int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1); 1617 if (nextPage != mCurrentPage) { 1618 snapToPage(nextPage); 1619 } else { 1620 snapToDestination(); 1621 } 1622 } else if (mTouchState == TOUCH_STATE_REORDERING) { 1623 // Update the last motion position 1624 mLastMotionX = ev.getX(); 1625 mLastMotionY = ev.getY(); 1626 1627 // Update the parent down so that our zoom animations take this new movement into 1628 // account 1629 float[] pt = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY); 1630 mParentDownMotionX = pt[0]; 1631 mParentDownMotionY = pt[1]; 1632 updateDragViewTranslationDuringDrag(); 1633 boolean handledFling = false; 1634 if (!DISABLE_FLING_TO_DELETE) { 1635 // Check the velocity and see if we are flinging-to-delete 1636 PointF flingToDeleteVector = isFlingingToDelete(); 1637 if (flingToDeleteVector != null) { 1638 onFlingToDelete(flingToDeleteVector); 1639 handledFling = true; 1640 } 1641 } 1642 if (!handledFling && isHoveringOverDeleteDropTarget((int) mParentDownMotionX, 1643 (int) mParentDownMotionY)) { 1644 onDropToDelete(); 1645 } 1646 } else { 1647 if (DEBUG_WARP) Log.v(TAG, "calling onUnhandledTap()"); 1648 if (mWarpPageExposed && !isAnimatingWarpPage()) { 1649 animateWarpPageOffScreen("unhandled tap", true); 1650 } 1651 onUnhandledTap(ev); 1652 } 1653 1654 // Remove the callback to wait for the side page hover timeout 1655 removeCallbacks(mSidePageHoverRunnable); 1656 // End any intermediate reordering states 1657 resetTouchState(); 1658 break; 1659 1660 case MotionEvent.ACTION_CANCEL: 1661 if (mTouchState == TOUCH_STATE_SCROLLING) { 1662 snapToDestination(); 1663 } 1664 resetTouchState(); 1665 break; 1666 1667 case MotionEvent.ACTION_POINTER_UP: 1668 onSecondaryPointerUp(ev); 1669 break; 1670 } 1671 1672 return true; 1673 } 1674 1675 //public abstract void onFlingToDelete(View v); 1676 public abstract void onRemoveView(View v, boolean deletePermanently); 1677 public abstract void onRemoveViewAnimationCompleted(); 1678 public abstract void onAddView(View v, int index); 1679 1680 private void resetTouchState() { 1681 releaseVelocityTracker(); 1682 endReordering(); 1683 setTouchState(TOUCH_STATE_REST); 1684 mActivePointerId = INVALID_POINTER; 1685 mDownEventOnEdge = false; 1686 } 1687 1688 protected void onUnhandledTap(MotionEvent ev) {} 1689 1690 @Override 1691 public boolean onGenericMotionEvent(MotionEvent event) { 1692 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { 1693 switch (event.getAction()) { 1694 case MotionEvent.ACTION_SCROLL: { 1695 // Handle mouse (or ext. device) by shifting the page depending on the scroll 1696 final float vscroll; 1697 final float hscroll; 1698 if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) { 1699 vscroll = 0; 1700 hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); 1701 } else { 1702 vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); 1703 hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); 1704 } 1705 if (hscroll != 0 || vscroll != 0) { 1706 if (hscroll > 0 || vscroll > 0) { 1707 scrollRight(); 1708 } else { 1709 scrollLeft(); 1710 } 1711 return true; 1712 } 1713 } 1714 } 1715 } 1716 return super.onGenericMotionEvent(event); 1717 } 1718 1719 private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { 1720 if (mVelocityTracker == null) { 1721 mVelocityTracker = VelocityTracker.obtain(); 1722 } 1723 mVelocityTracker.addMovement(ev); 1724 } 1725 1726 private void releaseVelocityTracker() { 1727 if (mVelocityTracker != null) { 1728 mVelocityTracker.recycle(); 1729 mVelocityTracker = null; 1730 } 1731 } 1732 1733 private void onSecondaryPointerUp(MotionEvent ev) { 1734 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 1735 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 1736 final int pointerId = ev.getPointerId(pointerIndex); 1737 if (pointerId == mActivePointerId) { 1738 // This was our active pointer going up. Choose a new 1739 // active pointer and adjust accordingly. 1740 // TODO: Make this decision more intelligent. 1741 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 1742 mLastMotionX = mDownMotionX = ev.getX(newPointerIndex); 1743 mLastMotionY = ev.getY(newPointerIndex); 1744 mLastMotionXRemainder = 0; 1745 mActivePointerId = ev.getPointerId(newPointerIndex); 1746 if (mVelocityTracker != null) { 1747 mVelocityTracker.clear(); 1748 } 1749 } 1750 } 1751 1752 @Override 1753 public void requestChildFocus(View child, View focused) { 1754 super.requestChildFocus(child, focused); 1755 int page = indexToPage(indexOfChild(child)); 1756 if (page >= 0 && page != getCurrentPage() && !isInTouchMode()) { 1757 snapToPage(page); 1758 } 1759 } 1760 1761 protected int getChildIndexForRelativeOffset(int relativeOffset) { 1762 final int childCount = getChildCount(); 1763 int left; 1764 int right; 1765 for (int i = 0; i < childCount; ++i) { 1766 left = getRelativeChildOffset(i); 1767 right = (left + getScaledMeasuredWidth(getPageAt(i))); 1768 if (left <= relativeOffset && relativeOffset <= right) { 1769 return i; 1770 } 1771 } 1772 return -1; 1773 } 1774 1775 protected int getChildWidth(int index) { 1776 // This functions are called enough times that it actually makes a difference in the 1777 // profiler -- so just inline the max() here 1778 final int measuredWidth = getPageAt(index).getMeasuredWidth(); 1779 final int minWidth = mMinimumWidth; 1780 return (minWidth > measuredWidth) ? minWidth : measuredWidth; 1781 } 1782 1783 int getPageNearestToPoint(float x) { 1784 int index = 0; 1785 for (int i = 0; i < getChildCount(); ++i) { 1786 if (x < getChildAt(i).getRight() - getScrollX()) { 1787 return index; 1788 } else { 1789 index++; 1790 } 1791 } 1792 return Math.min(index, getChildCount() - 1); 1793 } 1794 1795 int getPageNearestToCenterOfScreen() { 1796 int minDistanceFromScreenCenter = Integer.MAX_VALUE; 1797 int minDistanceFromScreenCenterIndex = -1; 1798 int screenCenter = getViewportOffsetX() + getScrollX() + (getViewportWidth() / 2); 1799 final int childCount = getChildCount(); 1800 for (int i = 0; i < childCount; ++i) { 1801 View layout = (View) getPageAt(i); 1802 int childWidth = getScaledMeasuredWidth(layout); 1803 int halfChildWidth = (childWidth / 2); 1804 int childCenter = getViewportOffsetX() + getChildOffset(i) + halfChildWidth; 1805 int distanceFromScreenCenter = Math.abs(childCenter - screenCenter); 1806 if (distanceFromScreenCenter < minDistanceFromScreenCenter) { 1807 minDistanceFromScreenCenter = distanceFromScreenCenter; 1808 minDistanceFromScreenCenterIndex = i; 1809 } 1810 } 1811 return minDistanceFromScreenCenterIndex; 1812 } 1813 1814 protected void snapToDestination() { 1815 final int newPage = getPageNearestToCenterOfScreen(); 1816 if (isWarping()) { 1817 cancelWarpAnimation("snapToDestination", mCurrentPage != newPage); 1818 } 1819 snapToPage(newPage, getPageSnapDuration()); 1820 } 1821 1822 private int getPageSnapDuration() { 1823 return isWarping() ? WARP_SNAP_DURATION : PAGE_SNAP_ANIMATION_DURATION; 1824 } 1825 1826 private static class ScrollInterpolator implements Interpolator { 1827 public ScrollInterpolator() { 1828 } 1829 1830 public float getInterpolation(float t) { 1831 t -= 1.0f; 1832 return t*t*t*t*t + 1; 1833 } 1834 } 1835 1836 // We want the duration of the page snap animation to be influenced by the distance that 1837 // the screen has to travel, however, we don't want this duration to be effected in a 1838 // purely linear fashion. Instead, we use this method to moderate the effect that the distance 1839 // of travel has on the overall snap duration. 1840 float distanceInfluenceForSnapDuration(float f) { 1841 f -= 0.5f; // center the values about 0. 1842 f *= 0.3f * Math.PI / 2.0f; 1843 return (float) Math.sin(f); 1844 } 1845 1846 protected void snapToPageWithVelocity(int whichPage, int velocity) { 1847 whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1)); 1848 int halfScreenSize = getViewportWidth() / 2; 1849 1850 if (isWarping()) { 1851 cancelWarpAnimation("snapToPageWithVelocity", mCurrentPage != whichPage); 1852 } 1853 1854 if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage)); 1855 if (DEBUG) Log.d(TAG, "snapToPageWithVelocity.getRelativeChildOffset(): " 1856 + getViewportWidth() + ", " + getChildWidth(whichPage)); 1857 final int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); 1858 int delta = newX - mUnboundedScrollX; 1859 int duration = 0; 1860 1861 if (Math.abs(velocity) < mMinFlingVelocity) { 1862 // If the velocity is low enough, then treat this more as an automatic page advance 1863 // as opposed to an apparent physical response to flinging 1864 snapToPage(whichPage, getPageSnapDuration()); 1865 return; 1866 } 1867 1868 // Here we compute a "distance" that will be used in the computation of the overall 1869 // snap duration. This is a function of the actual distance that needs to be traveled; 1870 // we keep this value close to half screen size in order to reduce the variance in snap 1871 // duration as a function of the distance the page needs to travel. 1872 float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize)); 1873 float distance = halfScreenSize + halfScreenSize * 1874 distanceInfluenceForSnapDuration(distanceRatio); 1875 1876 velocity = Math.abs(velocity); 1877 velocity = Math.max(mMinSnapVelocity, velocity); 1878 1879 // we want the page's snap velocity to approximately match the velocity at which the 1880 // user flings, so we scale the duration by a value near to the derivative of the scroll 1881 // interpolator at zero, ie. 5. We use 4 to make it a little slower. 1882 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 1883 1884 snapToPage(whichPage, delta, duration); 1885 } 1886 1887 protected void snapToPage(int whichPage) { 1888 snapToPage(whichPage, getPageSnapDuration()); 1889 } 1890 protected void snapToPageImmediately(int whichPage) { 1891 snapToPage(whichPage, getPageSnapDuration(), true); 1892 } 1893 1894 protected void snapToPage(int whichPage, int duration) { 1895 snapToPage(whichPage, duration, false); 1896 } 1897 protected void snapToPage(int whichPage, int duration, boolean immediate) { 1898 whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1)); 1899 1900 if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage)); 1901 if (DEBUG) Log.d(TAG, "snapToPage.getRelativeChildOffset(): " + getViewportWidth() + ", " 1902 + getChildWidth(whichPage)); 1903 int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); 1904 final int sX = mUnboundedScrollX; 1905 final int delta = newX - sX; 1906 snapToPage(whichPage, delta, duration, immediate); 1907 } 1908 1909 protected void snapToPage(int whichPage, int delta, int duration) { 1910 snapToPage(whichPage, delta, duration, false); 1911 } 1912 1913 protected void snapToPage(int whichPage, int delta, int duration, boolean immediate) { 1914 if (mPageSwapIndex != -1 && whichPage == mPageSwapIndex) { 1915 mNextPage = mPageWarpIndex; // jump to the warp page 1916 if (DEBUG_WARP) Log.v(TAG, "snapToPage(" + whichPage + ") : reset mPageSwapIndex"); 1917 } else { 1918 mNextPage = whichPage; 1919 } 1920 1921 if (isWarping()) { 1922 onPageEndWarp(); 1923 resetPageWarp(); 1924 } 1925 1926 notifyPageSwitching(whichPage); 1927 View focusedChild = getFocusedChild(); 1928 if (focusedChild != null && whichPage != mCurrentPage && 1929 focusedChild == getPageAt(mCurrentPage)) { 1930 focusedChild.clearFocus(); 1931 } 1932 1933 pageBeginMoving(); 1934 awakenScrollBars(duration); 1935 if (immediate) { 1936 duration = 0; 1937 } else if (duration == 0) { 1938 duration = Math.abs(delta); 1939 } 1940 1941 if (!mScroller.isFinished()) mScroller.abortAnimation(); 1942 mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration); 1943 1944 notifyPageSwitched(); 1945 1946 // Trigger a compute() to finish switching pages if necessary 1947 if (immediate) { 1948 computeScroll(); 1949 } 1950 1951 mForceScreenScrolled = true; 1952 invalidate(); 1953 } 1954 1955 protected boolean isWarping() { 1956 return mPageWarpIndex != -1; 1957 } 1958 1959 public void scrollLeft() { 1960 if (mScroller.isFinished()) { 1961 if (mCurrentPage > 0) snapToPage(mCurrentPage - 1); 1962 } else { 1963 if (mNextPage > 0) snapToPage(mNextPage - 1); 1964 } 1965 } 1966 1967 public void scrollRight() { 1968 if (mScroller.isFinished()) { 1969 if (mCurrentPage < getChildCount() -1) snapToPage(mCurrentPage + 1); 1970 } else { 1971 if (mNextPage < getChildCount() -1) snapToPage(mNextPage + 1); 1972 } 1973 } 1974 1975 public int getPageForView(View v) { 1976 int result = -1; 1977 if (v != null) { 1978 ViewParent vp = v.getParent(); 1979 int count = getChildCount(); 1980 for (int i = 0; i < count; i++) { 1981 if (vp == getPageAt(i)) { 1982 return i; 1983 } 1984 } 1985 } 1986 return result; 1987 } 1988 1989 public static class SavedState extends BaseSavedState { 1990 int currentPage = -1; 1991 1992 SavedState(Parcelable superState) { 1993 super(superState); 1994 } 1995 1996 private SavedState(Parcel in) { 1997 super(in); 1998 currentPage = in.readInt(); 1999 } 2000 2001 @Override 2002 public void writeToParcel(Parcel out, int flags) { 2003 super.writeToParcel(out, flags); 2004 out.writeInt(currentPage); 2005 } 2006 2007 public static final Parcelable.Creator<SavedState> CREATOR = 2008 new Parcelable.Creator<SavedState>() { 2009 public SavedState createFromParcel(Parcel in) { 2010 return new SavedState(in); 2011 } 2012 2013 public SavedState[] newArray(int size) { 2014 return new SavedState[size]; 2015 } 2016 }; 2017 } 2018 2019 protected View getScrollingIndicator() { 2020 return null; 2021 } 2022 2023 protected boolean isScrollingIndicatorEnabled() { 2024 return false; 2025 } 2026 2027 Runnable hideScrollingIndicatorRunnable = new Runnable() { 2028 @Override 2029 public void run() { 2030 hideScrollingIndicator(false); 2031 } 2032 }; 2033 2034 protected void flashScrollingIndicator(boolean animated) { 2035 removeCallbacks(hideScrollingIndicatorRunnable); 2036 showScrollingIndicator(!animated); 2037 postDelayed(hideScrollingIndicatorRunnable, sScrollIndicatorFlashDuration); 2038 } 2039 2040 protected void showScrollingIndicator(boolean immediately) { 2041 mShouldShowScrollIndicator = true; 2042 mShouldShowScrollIndicatorImmediately = true; 2043 if (getChildCount() <= 1) return; 2044 if (!isScrollingIndicatorEnabled()) return; 2045 2046 mShouldShowScrollIndicator = false; 2047 getScrollingIndicator(); 2048 if (mScrollIndicator != null) { 2049 // Fade the indicator in 2050 updateScrollingIndicatorPosition(); 2051 mScrollIndicator.setVisibility(View.VISIBLE); 2052 cancelScrollingIndicatorAnimations(); 2053 if (immediately) { 2054 mScrollIndicator.setAlpha(1f); 2055 } else { 2056 mScrollIndicatorAnimator = ObjectAnimator.ofFloat(mScrollIndicator, "alpha", 1f); 2057 mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeInDuration); 2058 mScrollIndicatorAnimator.start(); 2059 } 2060 } 2061 } 2062 2063 protected void cancelScrollingIndicatorAnimations() { 2064 if (mScrollIndicatorAnimator != null) { 2065 mScrollIndicatorAnimator.cancel(); 2066 } 2067 } 2068 2069 protected void hideScrollingIndicator(boolean immediately) { 2070 if (getChildCount() <= 1) return; 2071 if (!isScrollingIndicatorEnabled()) return; 2072 2073 getScrollingIndicator(); 2074 if (mScrollIndicator != null) { 2075 // Fade the indicator out 2076 updateScrollingIndicatorPosition(); 2077 cancelScrollingIndicatorAnimations(); 2078 if (immediately) { 2079 mScrollIndicator.setVisibility(View.INVISIBLE); 2080 mScrollIndicator.setAlpha(0f); 2081 } else { 2082 mScrollIndicatorAnimator = ObjectAnimator.ofFloat(mScrollIndicator, "alpha", 0f); 2083 mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeOutDuration); 2084 mScrollIndicatorAnimator.addListener(new AnimatorListenerAdapter() { 2085 private boolean cancelled = false; 2086 @Override 2087 public void onAnimationCancel(android.animation.Animator animation) { 2088 cancelled = true; 2089 } 2090 @Override 2091 public void onAnimationEnd(Animator animation) { 2092 if (!cancelled) { 2093 mScrollIndicator.setVisibility(View.INVISIBLE); 2094 } 2095 } 2096 }); 2097 mScrollIndicatorAnimator.start(); 2098 } 2099 } 2100 } 2101 2102 /** 2103 * To be overridden by subclasses to determine whether the scroll indicator should stretch to 2104 * fill its space on the track or not. 2105 */ 2106 protected boolean hasElasticScrollIndicator() { 2107 return true; 2108 } 2109 2110 private void updateScrollingIndicator() { 2111 if (getChildCount() <= 1) return; 2112 if (!isScrollingIndicatorEnabled()) return; 2113 2114 getScrollingIndicator(); 2115 if (mScrollIndicator != null) { 2116 updateScrollingIndicatorPosition(); 2117 } 2118 if (mShouldShowScrollIndicator) { 2119 showScrollingIndicator(mShouldShowScrollIndicatorImmediately); 2120 } 2121 } 2122 2123 private void updateScrollingIndicatorPosition() { 2124 if (!isScrollingIndicatorEnabled()) return; 2125 if (mScrollIndicator == null) return; 2126 int numPages = getChildCount(); 2127 int pageWidth = getViewportWidth(); 2128 int lastChildIndex = Math.max(0, getChildCount() - 1); 2129 int maxScrollX = getChildOffset(lastChildIndex) - getRelativeChildOffset(lastChildIndex); 2130 int trackWidth = pageWidth - mScrollIndicatorPaddingLeft - mScrollIndicatorPaddingRight; 2131 int indicatorWidth = mScrollIndicator.getMeasuredWidth() - 2132 mScrollIndicator.getPaddingLeft() - mScrollIndicator.getPaddingRight(); 2133 2134 float offset = Math.max(0f, Math.min(1f, (float) getScrollX() / maxScrollX)); 2135 int indicatorSpace = trackWidth / numPages; 2136 int indicatorPos = (int) (offset * (trackWidth - indicatorSpace)) + mScrollIndicatorPaddingLeft; 2137 if (hasElasticScrollIndicator()) { 2138 if (mScrollIndicator.getMeasuredWidth() != indicatorSpace) { 2139 mScrollIndicator.getLayoutParams().width = indicatorSpace; 2140 mScrollIndicator.requestLayout(); 2141 } 2142 } else { 2143 int indicatorCenterOffset = indicatorSpace / 2 - indicatorWidth / 2; 2144 indicatorPos += indicatorCenterOffset; 2145 } 2146 mScrollIndicator.setTranslationX(indicatorPos); 2147 } 2148 2149 // Animate the drag view back to the original position 2150 void animateDragViewToOriginalPosition() { 2151 if (mDragView != null) { 2152 AnimatorSet anim = new AnimatorSet(); 2153 anim.setDuration(REORDERING_DROP_REPOSITION_DURATION); 2154 anim.playTogether( 2155 ObjectAnimator.ofFloat(mDragView, "translationX", 0f), 2156 ObjectAnimator.ofFloat(mDragView, "translationY", 0f)); 2157 anim.addListener(new AnimatorListenerAdapter() { 2158 @Override 2159 public void onAnimationEnd(Animator animation) { 2160 onPostReorderingAnimationCompleted(); 2161 } 2162 }); 2163 anim.start(); 2164 } 2165 } 2166 2167 // "Zooms out" the PagedView to reveal more side pages 2168 protected boolean zoomOut() { 2169 if (mZoomInOutAnim != null && mZoomInOutAnim.isRunning()) { 2170 mZoomInOutAnim.cancel(); 2171 } 2172 2173 if (!(getScaleX() < 1f || getScaleY() < 1f)) { 2174 mZoomInOutAnim = new AnimatorSet(); 2175 mZoomInOutAnim.setDuration(REORDERING_ZOOM_IN_OUT_DURATION); 2176 mZoomInOutAnim.playTogether( 2177 ObjectAnimator.ofFloat(this, "scaleX", mMinScale), 2178 ObjectAnimator.ofFloat(this, "scaleY", mMinScale)); 2179 mZoomInOutAnim.addListener(new AnimatorListenerAdapter() { 2180 @Override 2181 public void onAnimationStart(Animator animation) { 2182 // Show the delete drop target 2183 if (mDeleteDropTarget != null) { 2184 mDeleteDropTarget.setVisibility(View.VISIBLE); 2185 mDeleteDropTarget.animate().alpha(1f) 2186 .setDuration(REORDERING_DELETE_DROP_TARGET_FADE_DURATION) 2187 .setListener(new AnimatorListenerAdapter() { 2188 @Override 2189 public void onAnimationStart(Animator animation) { 2190 mDeleteDropTarget.setAlpha(0f); 2191 } 2192 }); 2193 } 2194 } 2195 }); 2196 mZoomInOutAnim.start(); 2197 return true; 2198 } 2199 return false; 2200 } 2201 2202 protected void onStartReordering() { 2203 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 2204 announceForAccessibility(mContext.getString( 2205 R.string.keyguard_accessibility_widget_reorder_start)); 2206 } 2207 2208 // Set the touch state to reordering (allows snapping to pages, dragging a child, etc.) 2209 setTouchState(TOUCH_STATE_REORDERING); 2210 mIsReordering = true; 2211 2212 // Mark all the non-widget pages as invisible 2213 getVisiblePages(mTempVisiblePagesRange); 2214 boundByReorderablePages(true, mTempVisiblePagesRange); 2215 for (int i = 0; i < getPageCount(); ++i) { 2216 if (i < mTempVisiblePagesRange[0] || i > mTempVisiblePagesRange[1]) { 2217 getPageAt(i).setAlpha(0f); 2218 } 2219 } 2220 2221 // We must invalidate to trigger a redraw to update the layers such that the drag view 2222 // is always drawn on top 2223 invalidate(); 2224 } 2225 2226 private void onPostReorderingAnimationCompleted() { 2227 // Trigger the callback when reordering has settled 2228 --mPostReorderingPreZoomInRemainingAnimationCount; 2229 if (mPostReorderingPreZoomInRunnable != null && 2230 mPostReorderingPreZoomInRemainingAnimationCount == 0) { 2231 mPostReorderingPreZoomInRunnable.run(); 2232 mPostReorderingPreZoomInRunnable = null; 2233 } 2234 } 2235 2236 protected void onEndReordering() { 2237 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 2238 if (mDeleteString != null) { 2239 announceForAccessibility(mDeleteString); 2240 mDeleteString = null; 2241 } else { 2242 announceForAccessibility(mContext.getString( 2243 R.string.keyguard_accessibility_widget_reorder_end)); 2244 } 2245 } 2246 mIsReordering = false; 2247 2248 // Mark all the non-widget pages as visible again 2249 getVisiblePages(mTempVisiblePagesRange); 2250 boundByReorderablePages(true, mTempVisiblePagesRange); 2251 for (int i = 0; i < getPageCount(); ++i) { 2252 if (i < mTempVisiblePagesRange[0] || i > mTempVisiblePagesRange[1]) { 2253 getPageAt(i).setAlpha(1f); 2254 } 2255 } 2256 } 2257 2258 public boolean startReordering() { 2259 int dragViewIndex = getPageNearestToCenterOfScreen(); 2260 mTempVisiblePagesRange[0] = 0; 2261 mTempVisiblePagesRange[1] = getPageCount() - 1; 2262 boundByReorderablePages(true, mTempVisiblePagesRange); 2263 mReorderingStarted = true; 2264 2265 // Check if we are within the reordering range 2266 if (mTempVisiblePagesRange[0] <= dragViewIndex && 2267 dragViewIndex <= mTempVisiblePagesRange[1]) { 2268 if (zoomOut()) { 2269 // Find the drag view under the pointer 2270 mDragView = getChildAt(dragViewIndex); 2271 2272 onStartReordering(); 2273 } 2274 return true; 2275 } 2276 return false; 2277 } 2278 2279 boolean isReordering(boolean testTouchState) { 2280 boolean state = mIsReordering; 2281 if (testTouchState) { 2282 state &= (mTouchState == TOUCH_STATE_REORDERING); 2283 } 2284 return state; 2285 } 2286 void endReordering() { 2287 // For simplicity, we call endReordering sometimes even if reordering was never started. 2288 // In that case, we don't want to do anything. 2289 if (!mReorderingStarted) return; 2290 mReorderingStarted = false; 2291 2292 // If we haven't flung-to-delete the current child, then we just animate the drag view 2293 // back into position 2294 final Runnable onCompleteRunnable = new Runnable() { 2295 @Override 2296 public void run() { 2297 onEndReordering(); 2298 } 2299 }; 2300 if (!mDeferringForDelete) { 2301 mPostReorderingPreZoomInRunnable = new Runnable() { 2302 public void run() { 2303 zoomIn(onCompleteRunnable); 2304 }; 2305 }; 2306 2307 mPostReorderingPreZoomInRemainingAnimationCount = 2308 NUM_ANIMATIONS_RUNNING_BEFORE_ZOOM_OUT; 2309 // Snap to the current page 2310 snapToPage(indexOfChild(mDragView), 0); 2311 // Animate the drag view back to the front position 2312 animateDragViewToOriginalPosition(); 2313 } else { 2314 // Handled in post-delete-animation-callbacks 2315 } 2316 } 2317 2318 // "Zooms in" the PagedView to highlight the current page 2319 protected boolean zoomIn(final Runnable onCompleteRunnable) { 2320 if (mZoomInOutAnim != null && mZoomInOutAnim.isRunning()) { 2321 mZoomInOutAnim.cancel(); 2322 } 2323 if (getScaleX() < 1f || getScaleY() < 1f) { 2324 mZoomInOutAnim = new AnimatorSet(); 2325 mZoomInOutAnim.setDuration(REORDERING_ZOOM_IN_OUT_DURATION); 2326 mZoomInOutAnim.playTogether( 2327 ObjectAnimator.ofFloat(this, "scaleX", 1f), 2328 ObjectAnimator.ofFloat(this, "scaleY", 1f)); 2329 mZoomInOutAnim.addListener(new AnimatorListenerAdapter() { 2330 @Override 2331 public void onAnimationStart(Animator animation) { 2332 // Hide the delete drop target 2333 if (mDeleteDropTarget != null) { 2334 mDeleteDropTarget.animate().alpha(0f) 2335 .setDuration(REORDERING_DELETE_DROP_TARGET_FADE_DURATION) 2336 .setListener(new AnimatorListenerAdapter() { 2337 @Override 2338 public void onAnimationEnd(Animator animation) { 2339 mDeleteDropTarget.setVisibility(View.GONE); 2340 } 2341 }); 2342 } 2343 } 2344 @Override 2345 public void onAnimationCancel(Animator animation) { 2346 mDragView = null; 2347 } 2348 @Override 2349 public void onAnimationEnd(Animator animation) { 2350 mDragView = null; 2351 if (onCompleteRunnable != null) { 2352 onCompleteRunnable.run(); 2353 } 2354 } 2355 }); 2356 mZoomInOutAnim.start(); 2357 return true; 2358 } else { 2359 if (onCompleteRunnable != null) { 2360 onCompleteRunnable.run(); 2361 } 2362 } 2363 return false; 2364 } 2365 2366 /* 2367 * Flinging to delete - IN PROGRESS 2368 */ 2369 private PointF isFlingingToDelete() { 2370 ViewConfiguration config = ViewConfiguration.get(getContext()); 2371 mVelocityTracker.computeCurrentVelocity(1000, config.getScaledMaximumFlingVelocity()); 2372 2373 if (mVelocityTracker.getYVelocity() < mFlingToDeleteThresholdVelocity) { 2374 // Do a quick dot product test to ensure that we are flinging upwards 2375 PointF vel = new PointF(mVelocityTracker.getXVelocity(), 2376 mVelocityTracker.getYVelocity()); 2377 PointF upVec = new PointF(0f, -1f); 2378 float theta = (float) Math.acos(((vel.x * upVec.x) + (vel.y * upVec.y)) / 2379 (vel.length() * upVec.length())); 2380 if (theta <= Math.toRadians(FLING_TO_DELETE_MAX_FLING_DEGREES)) { 2381 return vel; 2382 } 2383 } 2384 return null; 2385 } 2386 2387 /** 2388 * Creates an animation from the current drag view along its current velocity vector. 2389 * For this animation, the alpha runs for a fixed duration and we update the position 2390 * progressively. 2391 */ 2392 private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener { 2393 private View mDragView; 2394 private PointF mVelocity; 2395 private Rect mFrom; 2396 private long mPrevTime; 2397 private float mFriction; 2398 2399 private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f); 2400 2401 public FlingAlongVectorAnimatorUpdateListener(View dragView, PointF vel, Rect from, 2402 long startTime, float friction) { 2403 mDragView = dragView; 2404 mVelocity = vel; 2405 mFrom = from; 2406 mPrevTime = startTime; 2407 mFriction = 1f - (mDragView.getResources().getDisplayMetrics().density * friction); 2408 } 2409 2410 @Override 2411 public void onAnimationUpdate(ValueAnimator animation) { 2412 float t = ((Float) animation.getAnimatedValue()).floatValue(); 2413 long curTime = AnimationUtils.currentAnimationTimeMillis(); 2414 2415 mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f); 2416 mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f); 2417 2418 mDragView.setTranslationX(mFrom.left); 2419 mDragView.setTranslationY(mFrom.top); 2420 mDragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t)); 2421 2422 mVelocity.x *= mFriction; 2423 mVelocity.y *= mFriction; 2424 mPrevTime = curTime; 2425 } 2426 }; 2427 2428 private Runnable createPostDeleteAnimationRunnable(final View dragView) { 2429 return new Runnable() { 2430 @Override 2431 public void run() { 2432 int dragViewIndex = indexOfChild(dragView); 2433 2434 // For each of the pages around the drag view, animate them from the previous 2435 // position to the new position in the layout (as a result of the drag view moving 2436 // in the layout) 2437 // NOTE: We can make an assumption here because we have side-bound pages that we 2438 // will always have pages to animate in from the left 2439 getVisiblePages(mTempVisiblePagesRange); 2440 boundByReorderablePages(true, mTempVisiblePagesRange); 2441 boolean isLastWidgetPage = (mTempVisiblePagesRange[0] == mTempVisiblePagesRange[1]); 2442 boolean slideFromLeft = (isLastWidgetPage || 2443 dragViewIndex > mTempVisiblePagesRange[0]); 2444 2445 // Setup the scroll to the correct page before we swap the views 2446 if (slideFromLeft) { 2447 snapToPageImmediately(dragViewIndex - 1); 2448 } 2449 2450 int firstIndex = (isLastWidgetPage ? 0 : mTempVisiblePagesRange[0]); 2451 int lastIndex = Math.min(mTempVisiblePagesRange[1], getPageCount() - 1); 2452 int lowerIndex = (slideFromLeft ? firstIndex : dragViewIndex + 1 ); 2453 int upperIndex = (slideFromLeft ? dragViewIndex - 1 : lastIndex); 2454 ArrayList<Animator> animations = new ArrayList<Animator>(); 2455 for (int i = lowerIndex; i <= upperIndex; ++i) { 2456 View v = getChildAt(i); 2457 // dragViewIndex < pageUnderPointIndex, so after we remove the 2458 // drag view all subsequent views to pageUnderPointIndex will 2459 // shift down. 2460 int oldX = 0; 2461 int newX = 0; 2462 if (slideFromLeft) { 2463 if (i == 0) { 2464 // Simulate the page being offscreen with the page spacing 2465 oldX = getViewportOffsetX() + getChildOffset(i) - getChildWidth(i) 2466 - mPageSpacing; 2467 } else { 2468 oldX = getViewportOffsetX() + getChildOffset(i - 1); 2469 } 2470 newX = getViewportOffsetX() + getChildOffset(i); 2471 } else { 2472 oldX = getChildOffset(i) - getChildOffset(i - 1); 2473 newX = 0; 2474 } 2475 2476 // Animate the view translation from its old position to its new 2477 // position 2478 AnimatorSet anim = (AnimatorSet) v.getTag(); 2479 if (anim != null) { 2480 anim.cancel(); 2481 } 2482 2483 // Note: Hacky, but we want to skip any optimizations to not draw completely 2484 // hidden views 2485 v.setAlpha(Math.max(v.getAlpha(), 0.01f)); 2486 v.setTranslationX(oldX - newX); 2487 anim = new AnimatorSet(); 2488 anim.playTogether( 2489 ObjectAnimator.ofFloat(v, "translationX", 0f), 2490 ObjectAnimator.ofFloat(v, "alpha", 1f)); 2491 animations.add(anim); 2492 v.setTag(anim); 2493 } 2494 2495 AnimatorSet slideAnimations = new AnimatorSet(); 2496 slideAnimations.playTogether(animations); 2497 slideAnimations.setDuration(DELETE_SLIDE_IN_SIDE_PAGE_DURATION); 2498 slideAnimations.addListener(new AnimatorListenerAdapter() { 2499 @Override 2500 public void onAnimationEnd(Animator animation) { 2501 final Runnable onCompleteRunnable = new Runnable() { 2502 @Override 2503 public void run() { 2504 mDeferringForDelete = false; 2505 onEndReordering(); 2506 onRemoveViewAnimationCompleted(); 2507 } 2508 }; 2509 zoomIn(onCompleteRunnable); 2510 } 2511 }); 2512 slideAnimations.start(); 2513 2514 removeView(dragView); 2515 onRemoveView(dragView, true); 2516 } 2517 }; 2518 } 2519 2520 public void onFlingToDelete(PointF vel) { 2521 final long startTime = AnimationUtils.currentAnimationTimeMillis(); 2522 2523 // NOTE: Because it takes time for the first frame of animation to actually be 2524 // called and we expect the animation to be a continuation of the fling, we have 2525 // to account for the time that has elapsed since the fling finished. And since 2526 // we don't have a startDelay, we will always get call to update when we call 2527 // start() (which we want to ignore). 2528 final TimeInterpolator tInterpolator = new TimeInterpolator() { 2529 private int mCount = -1; 2530 private long mStartTime; 2531 private float mOffset; 2532 /* Anonymous inner class ctor */ { 2533 mStartTime = startTime; 2534 } 2535 2536 @Override 2537 public float getInterpolation(float t) { 2538 if (mCount < 0) { 2539 mCount++; 2540 } else if (mCount == 0) { 2541 mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() - 2542 mStartTime) / FLING_TO_DELETE_FADE_OUT_DURATION); 2543 mCount++; 2544 } 2545 return Math.min(1f, mOffset + t); 2546 } 2547 }; 2548 2549 final Rect from = new Rect(); 2550 final View dragView = mDragView; 2551 from.left = (int) dragView.getTranslationX(); 2552 from.top = (int) dragView.getTranslationY(); 2553 AnimatorUpdateListener updateCb = new FlingAlongVectorAnimatorUpdateListener(dragView, vel, 2554 from, startTime, FLING_TO_DELETE_FRICTION); 2555 2556 mDeleteString = getContext().getResources() 2557 .getString(R.string.keyguard_accessibility_widget_deleted, 2558 mDragView.getContentDescription()); 2559 final Runnable onAnimationEndRunnable = createPostDeleteAnimationRunnable(dragView); 2560 2561 // Create and start the animation 2562 ValueAnimator mDropAnim = new ValueAnimator(); 2563 mDropAnim.setInterpolator(tInterpolator); 2564 mDropAnim.setDuration(FLING_TO_DELETE_FADE_OUT_DURATION); 2565 mDropAnim.setFloatValues(0f, 1f); 2566 mDropAnim.addUpdateListener(updateCb); 2567 mDropAnim.addListener(new AnimatorListenerAdapter() { 2568 public void onAnimationEnd(Animator animation) { 2569 onAnimationEndRunnable.run(); 2570 } 2571 }); 2572 mDropAnim.start(); 2573 mDeferringForDelete = true; 2574 } 2575 2576 /* Drag to delete */ 2577 private boolean isHoveringOverDeleteDropTarget(int x, int y) { 2578 if (mDeleteDropTarget != null) { 2579 mAltTmpRect.set(0, 0, 0, 0); 2580 View parent = (View) mDeleteDropTarget.getParent(); 2581 if (parent != null) { 2582 parent.getGlobalVisibleRect(mAltTmpRect); 2583 } 2584 mDeleteDropTarget.getGlobalVisibleRect(mTmpRect); 2585 mTmpRect.offset(-mAltTmpRect.left, -mAltTmpRect.top); 2586 return mTmpRect.contains(x, y); 2587 } 2588 return false; 2589 } 2590 2591 protected void setPageHoveringOverDeleteDropTarget(int viewIndex, boolean isHovering) {} 2592 2593 private void onDropToDelete() { 2594 final View dragView = mDragView; 2595 2596 final float toScale = 0f; 2597 final float toAlpha = 0f; 2598 2599 // Create and start the complex animation 2600 ArrayList<Animator> animations = new ArrayList<Animator>(); 2601 AnimatorSet motionAnim = new AnimatorSet(); 2602 motionAnim.setInterpolator(new DecelerateInterpolator(2)); 2603 motionAnim.playTogether( 2604 ObjectAnimator.ofFloat(dragView, "scaleX", toScale), 2605 ObjectAnimator.ofFloat(dragView, "scaleY", toScale)); 2606 animations.add(motionAnim); 2607 2608 AnimatorSet alphaAnim = new AnimatorSet(); 2609 alphaAnim.setInterpolator(new LinearInterpolator()); 2610 alphaAnim.playTogether( 2611 ObjectAnimator.ofFloat(dragView, "alpha", toAlpha)); 2612 animations.add(alphaAnim); 2613 2614 mDeleteString = getContext().getResources() 2615 .getString(R.string.keyguard_accessibility_widget_deleted, 2616 mDragView.getContentDescription()); 2617 final Runnable onAnimationEndRunnable = createPostDeleteAnimationRunnable(dragView); 2618 2619 AnimatorSet anim = new AnimatorSet(); 2620 anim.playTogether(animations); 2621 anim.setDuration(DRAG_TO_DELETE_FADE_OUT_DURATION); 2622 anim.addListener(new AnimatorListenerAdapter() { 2623 public void onAnimationEnd(Animator animation) { 2624 onAnimationEndRunnable.run(); 2625 } 2626 }); 2627 anim.start(); 2628 2629 mDeferringForDelete = true; 2630 } 2631 2632 /* Accessibility */ 2633 @Override 2634 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 2635 super.onInitializeAccessibilityNodeInfo(info); 2636 info.setScrollable(getPageCount() > 1); 2637 if (getCurrentPage() < getPageCount() - 1) { 2638 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); 2639 } 2640 if (getCurrentPage() > 0) { 2641 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 2642 } 2643 } 2644 2645 @Override 2646 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 2647 super.onInitializeAccessibilityEvent(event); 2648 event.setScrollable(true); 2649 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { 2650 event.setFromIndex(mCurrentPage); 2651 event.setToIndex(mCurrentPage); 2652 event.setItemCount(getChildCount()); 2653 } 2654 } 2655 2656 @Override 2657 public boolean performAccessibilityAction(int action, Bundle arguments) { 2658 if (super.performAccessibilityAction(action, arguments)) { 2659 return true; 2660 } 2661 switch (action) { 2662 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 2663 if (getCurrentPage() < getPageCount() - 1) { 2664 scrollRight(); 2665 return true; 2666 } 2667 } break; 2668 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 2669 if (getCurrentPage() > 0) { 2670 scrollLeft(); 2671 return true; 2672 } 2673 } break; 2674 } 2675 return false; 2676 } 2677 2678 @Override 2679 public boolean onHoverEvent(android.view.MotionEvent event) { 2680 return true; 2681 } 2682 2683 void beginCameraEvent() { 2684 mIsCameraEvent = true; 2685 } 2686 2687 void endCameraEvent() { 2688 mIsCameraEvent = false; 2689 } 2690 2691 AnimatorListenerAdapter mOnScreenAnimationListener = new AnimatorListenerAdapter() { 2692 @Override 2693 public void onAnimationEnd(Animator animation) { 2694 mWarpAnimation = null; 2695 if (mTouchState != TOUCH_STATE_SCROLLING && mTouchState != TOUCH_STATE_READY) { 2696 animateWarpPageOffScreen("onScreen end", true); 2697 } 2698 } 2699 }; 2700 2701 AnimatorListenerAdapter mOffScreenAnimationListener = new AnimatorListenerAdapter() { 2702 @Override 2703 public void onAnimationEnd(Animator animation) { 2704 mWarpAnimation = null; 2705 mWarpPageExposed = true; 2706 } 2707 }; 2708 2709 private void cancelWarpAnimation(String msg, boolean abortAnimation) { 2710 if (DEBUG_WARP) Log.v(TAG, "cancelWarpAnimation(" + msg + ")"); 2711 if (abortAnimation) { 2712 // We're done with the animation and moving to a new page. Let the scroller 2713 // take over the animation. 2714 KeyguardWidgetFrame v = (KeyguardWidgetFrame) getPageAt(mPageWarpIndex); 2715 v.animate().cancel(); 2716 // Make the scroll amount match the current warp position. 2717 scrollBy(Math.round(-v.getTranslationX()), 0); 2718 v.setTranslationX(0); 2719 } else { 2720 animateWarpPageOffScreen("canceled", true); 2721 } 2722 } 2723 2724 private boolean isAnimatingWarpPage() { 2725 return mWarpAnimation != null; 2726 } 2727 2728 private void animateWarpPageOnScreen(String reason) { 2729 if (DEBUG_WARP) Log.v(TAG, "animateWarpPageOnScreen(" + reason + ")"); 2730 if (isWarping()) { 2731 mWarpPageExposed = true; 2732 onPageBeginWarp(); 2733 KeyguardWidgetFrame v = (KeyguardWidgetFrame) getPageAt(mPageWarpIndex); 2734 if (DEBUG_WARP) Log.v(TAG, "moving page on screen: Tx=" + v.getTranslationX()); 2735 DecelerateInterpolator interp = new DecelerateInterpolator(1.5f); 2736 mWarpAnimation = v.animate(); 2737 mWarpAnimation.translationX(mWarpPeekAmount) 2738 .setInterpolator(interp) 2739 .setDuration(WARP_PEEK_ANIMATION_DURATION) 2740 .setListener(mOnScreenAnimationListener); 2741 } 2742 } 2743 2744 private void animateWarpPageOffScreen(String reason, boolean animate) { 2745 if (DEBUG_WARP) Log.v(TAG, "animateWarpPageOffScreen(" + reason + " anim:" + animate + ")"); 2746 if (isWarping()) { 2747 onPageEndWarp(); 2748 KeyguardWidgetFrame v = (KeyguardWidgetFrame) getPageAt(mPageWarpIndex); 2749 if (DEBUG_WARP) Log.v(TAG, "moving page off screen: Tx=" + v.getTranslationX()); 2750 AccelerateInterpolator interp = new AccelerateInterpolator(1.5f); 2751 v.animate().translationX(0.0f) 2752 .setInterpolator(interp) 2753 .setDuration(animate ? WARP_PEEK_ANIMATION_DURATION : 0) 2754 .setListener(mOffScreenAnimationListener); 2755 } else { 2756 if (DEBUG_WARP) Log.e(TAG, "animateWarpPageOffScreen(): not warping", new Exception()); 2757 } 2758 } 2759 2760 /** 2761 * Swaps the position of the views by setting the left and right edges appropriately. 2762 */ 2763 void swapPages(int indexA, int indexB) { 2764 View viewA = getPageAt(indexA); 2765 View viewB = getPageAt(indexB); 2766 if (viewA != viewB && viewA != null && viewB != null) { 2767 int deltaX = viewA.getLeft() - viewB.getLeft(); 2768 viewA.offsetLeftAndRight(-deltaX); 2769 viewB.offsetLeftAndRight(deltaX); 2770 } 2771 } 2772 2773 public void startPageWarp(int pageIndex) { 2774 if (DEBUG_WARP) Log.v(TAG, "START WARP"); 2775 if (pageIndex != mCurrentPage + 1) { 2776 mPageSwapIndex = mCurrentPage + 1; 2777 } 2778 mPageWarpIndex = pageIndex; 2779 } 2780 2781 protected int getPageWarpIndex() { 2782 return mPageWarpIndex; 2783 } 2784 2785 public void stopPageWarp() { 2786 if (DEBUG_WARP) Log.v(TAG, "END WARP"); 2787 // mPageSwapIndex is reset in snapToPage() after the scroll animation completes 2788 } 2789 2790 public void onPageBeginWarp() { 2791 2792 } 2793 2794 public void onPageEndWarp() { 2795 2796 } 2797 2798 } 2799