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