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