1 /* 2 * Copyright (C) 2007 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 android.widget; 18 19 import android.annotation.Widget; 20 import android.content.Context; 21 import android.content.res.TypedArray; 22 import android.graphics.Rect; 23 import android.os.Bundle; 24 import android.util.AttributeSet; 25 import android.util.Log; 26 import android.view.ContextMenu.ContextMenuInfo; 27 import android.view.GestureDetector; 28 import android.view.Gravity; 29 import android.view.HapticFeedbackConstants; 30 import android.view.KeyEvent; 31 import android.view.MotionEvent; 32 import android.view.SoundEffectConstants; 33 import android.view.View; 34 import android.view.ViewConfiguration; 35 import android.view.ViewGroup; 36 import android.view.accessibility.AccessibilityEvent; 37 import android.view.accessibility.AccessibilityNodeInfo; 38 import android.view.animation.Transformation; 39 40 import com.android.internal.R; 41 42 /** 43 * A view that shows items in a center-locked, horizontally scrolling list. 44 * <p> 45 * The default values for the Gallery assume you will be using 46 * {@link android.R.styleable#Theme_galleryItemBackground} as the background for 47 * each View given to the Gallery from the Adapter. If you are not doing this, 48 * you may need to adjust some Gallery properties, such as the spacing. 49 * <p> 50 * Views given to the Gallery should use {@link Gallery.LayoutParams} as their 51 * layout parameters type. 52 * 53 * @attr ref android.R.styleable#Gallery_animationDuration 54 * @attr ref android.R.styleable#Gallery_spacing 55 * @attr ref android.R.styleable#Gallery_gravity 56 * 57 * @deprecated This widget is no longer supported. Other horizontally scrolling 58 * widgets include {@link HorizontalScrollView} and {@link android.support.v4.view.ViewPager} 59 * from the support library. 60 */ 61 @Deprecated 62 @Widget 63 public class Gallery extends AbsSpinner implements GestureDetector.OnGestureListener { 64 65 private static final String TAG = "Gallery"; 66 67 private static final boolean localLOGV = false; 68 69 /** 70 * Duration in milliseconds from the start of a scroll during which we're 71 * unsure whether the user is scrolling or flinging. 72 */ 73 private static final int SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT = 250; 74 75 /** 76 * Horizontal spacing between items. 77 */ 78 private int mSpacing = 0; 79 80 /** 81 * How long the transition animation should run when a child view changes 82 * position, measured in milliseconds. 83 */ 84 private int mAnimationDuration = 400; 85 86 /** 87 * The alpha of items that are not selected. 88 */ 89 private float mUnselectedAlpha; 90 91 /** 92 * Left most edge of a child seen so far during layout. 93 */ 94 private int mLeftMost; 95 96 /** 97 * Right most edge of a child seen so far during layout. 98 */ 99 private int mRightMost; 100 101 private int mGravity; 102 103 /** 104 * Helper for detecting touch gestures. 105 */ 106 private GestureDetector mGestureDetector; 107 108 /** 109 * The position of the item that received the user's down touch. 110 */ 111 private int mDownTouchPosition; 112 113 /** 114 * The view of the item that received the user's down touch. 115 */ 116 private View mDownTouchView; 117 118 /** 119 * Executes the delta scrolls from a fling or scroll movement. 120 */ 121 private FlingRunnable mFlingRunnable = new FlingRunnable(); 122 123 /** 124 * Sets mSuppressSelectionChanged = false. This is used to set it to false 125 * in the future. It will also trigger a selection changed. 126 */ 127 private Runnable mDisableSuppressSelectionChangedRunnable = new Runnable() { 128 @Override 129 public void run() { 130 mSuppressSelectionChanged = false; 131 selectionChanged(); 132 } 133 }; 134 135 /** 136 * When fling runnable runs, it resets this to false. Any method along the 137 * path until the end of its run() can set this to true to abort any 138 * remaining fling. For example, if we've reached either the leftmost or 139 * rightmost item, we will set this to true. 140 */ 141 private boolean mShouldStopFling; 142 143 /** 144 * The currently selected item's child. 145 */ 146 private View mSelectedChild; 147 148 /** 149 * Whether to continuously callback on the item selected listener during a 150 * fling. 151 */ 152 private boolean mShouldCallbackDuringFling = true; 153 154 /** 155 * Whether to callback when an item that is not selected is clicked. 156 */ 157 private boolean mShouldCallbackOnUnselectedItemClick = true; 158 159 /** 160 * If true, do not callback to item selected listener. 161 */ 162 private boolean mSuppressSelectionChanged; 163 164 /** 165 * If true, we have received the "invoke" (center or enter buttons) key 166 * down. This is checked before we action on the "invoke" key up, and is 167 * subsequently cleared. 168 */ 169 private boolean mReceivedInvokeKeyDown; 170 171 private AdapterContextMenuInfo mContextMenuInfo; 172 173 /** 174 * If true, this onScroll is the first for this user's drag (remember, a 175 * drag sends many onScrolls). 176 */ 177 private boolean mIsFirstScroll; 178 179 /** 180 * If true, mFirstPosition is the position of the rightmost child, and 181 * the children are ordered right to left. 182 */ 183 private boolean mIsRtl = true; 184 185 /** 186 * Offset between the center of the selected child view and the center of the Gallery. 187 * Used to reset position correctly during layout. 188 */ 189 private int mSelectedCenterOffset; 190 Gallery(Context context)191 public Gallery(Context context) { 192 this(context, null); 193 } 194 Gallery(Context context, AttributeSet attrs)195 public Gallery(Context context, AttributeSet attrs) { 196 this(context, attrs, R.attr.galleryStyle); 197 } 198 Gallery(Context context, AttributeSet attrs, int defStyle)199 public Gallery(Context context, AttributeSet attrs, int defStyle) { 200 super(context, attrs, defStyle); 201 202 mGestureDetector = new GestureDetector(context, this); 203 mGestureDetector.setIsLongpressEnabled(true); 204 205 TypedArray a = context.obtainStyledAttributes( 206 attrs, com.android.internal.R.styleable.Gallery, defStyle, 0); 207 208 int index = a.getInt(com.android.internal.R.styleable.Gallery_gravity, -1); 209 if (index >= 0) { 210 setGravity(index); 211 } 212 213 int animationDuration = 214 a.getInt(com.android.internal.R.styleable.Gallery_animationDuration, -1); 215 if (animationDuration > 0) { 216 setAnimationDuration(animationDuration); 217 } 218 219 int spacing = 220 a.getDimensionPixelOffset(com.android.internal.R.styleable.Gallery_spacing, 0); 221 setSpacing(spacing); 222 223 float unselectedAlpha = a.getFloat( 224 com.android.internal.R.styleable.Gallery_unselectedAlpha, 0.5f); 225 setUnselectedAlpha(unselectedAlpha); 226 227 a.recycle(); 228 229 // We draw the selected item last (because otherwise the item to the 230 // right overlaps it) 231 mGroupFlags |= FLAG_USE_CHILD_DRAWING_ORDER; 232 233 mGroupFlags |= FLAG_SUPPORT_STATIC_TRANSFORMATIONS; 234 } 235 236 /** 237 * Whether or not to callback on any {@link #getOnItemSelectedListener()} 238 * while the items are being flinged. If false, only the final selected item 239 * will cause the callback. If true, all items between the first and the 240 * final will cause callbacks. 241 * 242 * @param shouldCallback Whether or not to callback on the listener while 243 * the items are being flinged. 244 */ setCallbackDuringFling(boolean shouldCallback)245 public void setCallbackDuringFling(boolean shouldCallback) { 246 mShouldCallbackDuringFling = shouldCallback; 247 } 248 249 /** 250 * Whether or not to callback when an item that is not selected is clicked. 251 * If false, the item will become selected (and re-centered). If true, the 252 * {@link #getOnItemClickListener()} will get the callback. 253 * 254 * @param shouldCallback Whether or not to callback on the listener when a 255 * item that is not selected is clicked. 256 * @hide 257 */ setCallbackOnUnselectedItemClick(boolean shouldCallback)258 public void setCallbackOnUnselectedItemClick(boolean shouldCallback) { 259 mShouldCallbackOnUnselectedItemClick = shouldCallback; 260 } 261 262 /** 263 * Sets how long the transition animation should run when a child view 264 * changes position. Only relevant if animation is turned on. 265 * 266 * @param animationDurationMillis The duration of the transition, in 267 * milliseconds. 268 * 269 * @attr ref android.R.styleable#Gallery_animationDuration 270 */ setAnimationDuration(int animationDurationMillis)271 public void setAnimationDuration(int animationDurationMillis) { 272 mAnimationDuration = animationDurationMillis; 273 } 274 275 /** 276 * Sets the spacing between items in a Gallery 277 * 278 * @param spacing The spacing in pixels between items in the Gallery 279 * 280 * @attr ref android.R.styleable#Gallery_spacing 281 */ setSpacing(int spacing)282 public void setSpacing(int spacing) { 283 mSpacing = spacing; 284 } 285 286 /** 287 * Sets the alpha of items that are not selected in the Gallery. 288 * 289 * @param unselectedAlpha the alpha for the items that are not selected. 290 * 291 * @attr ref android.R.styleable#Gallery_unselectedAlpha 292 */ setUnselectedAlpha(float unselectedAlpha)293 public void setUnselectedAlpha(float unselectedAlpha) { 294 mUnselectedAlpha = unselectedAlpha; 295 } 296 297 @Override getChildStaticTransformation(View child, Transformation t)298 protected boolean getChildStaticTransformation(View child, Transformation t) { 299 300 t.clear(); 301 t.setAlpha(child == mSelectedChild ? 1.0f : mUnselectedAlpha); 302 303 return true; 304 } 305 306 @Override computeHorizontalScrollExtent()307 protected int computeHorizontalScrollExtent() { 308 // Only 1 item is considered to be selected 309 return 1; 310 } 311 312 @Override computeHorizontalScrollOffset()313 protected int computeHorizontalScrollOffset() { 314 // Current scroll position is the same as the selected position 315 return mSelectedPosition; 316 } 317 318 @Override computeHorizontalScrollRange()319 protected int computeHorizontalScrollRange() { 320 // Scroll range is the same as the item count 321 return mItemCount; 322 } 323 324 @Override checkLayoutParams(ViewGroup.LayoutParams p)325 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 326 return p instanceof LayoutParams; 327 } 328 329 @Override generateLayoutParams(ViewGroup.LayoutParams p)330 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 331 return new LayoutParams(p); 332 } 333 334 @Override generateLayoutParams(AttributeSet attrs)335 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 336 return new LayoutParams(getContext(), attrs); 337 } 338 339 @Override generateDefaultLayoutParams()340 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 341 /* 342 * Gallery expects Gallery.LayoutParams. 343 */ 344 return new Gallery.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 345 ViewGroup.LayoutParams.WRAP_CONTENT); 346 } 347 348 @Override onLayout(boolean changed, int l, int t, int r, int b)349 protected void onLayout(boolean changed, int l, int t, int r, int b) { 350 super.onLayout(changed, l, t, r, b); 351 352 /* 353 * Remember that we are in layout to prevent more layout request from 354 * being generated. 355 */ 356 mInLayout = true; 357 layout(0, false); 358 mInLayout = false; 359 } 360 361 @Override getChildHeight(View child)362 int getChildHeight(View child) { 363 return child.getMeasuredHeight(); 364 } 365 366 /** 367 * Tracks a motion scroll. In reality, this is used to do just about any 368 * movement to items (touch scroll, arrow-key scroll, set an item as selected). 369 * 370 * @param deltaX Change in X from the previous event. 371 */ trackMotionScroll(int deltaX)372 void trackMotionScroll(int deltaX) { 373 374 if (getChildCount() == 0) { 375 return; 376 } 377 378 boolean toLeft = deltaX < 0; 379 380 int limitedDeltaX = getLimitedMotionScrollAmount(toLeft, deltaX); 381 if (limitedDeltaX != deltaX) { 382 // The above call returned a limited amount, so stop any scrolls/flings 383 mFlingRunnable.endFling(false); 384 onFinishedMovement(); 385 } 386 387 offsetChildrenLeftAndRight(limitedDeltaX); 388 389 detachOffScreenChildren(toLeft); 390 391 if (toLeft) { 392 // If moved left, there will be empty space on the right 393 fillToGalleryRight(); 394 } else { 395 // Similarly, empty space on the left 396 fillToGalleryLeft(); 397 } 398 399 // Clear unused views 400 mRecycler.clear(); 401 402 setSelectionToCenterChild(); 403 404 final View selChild = mSelectedChild; 405 if (selChild != null) { 406 final int childLeft = selChild.getLeft(); 407 final int childCenter = selChild.getWidth() / 2; 408 final int galleryCenter = getWidth() / 2; 409 mSelectedCenterOffset = childLeft + childCenter - galleryCenter; 410 } 411 412 onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these. 413 414 invalidate(); 415 } 416 417 int getLimitedMotionScrollAmount(boolean motionToLeft, int deltaX) { 418 int extremeItemPosition = motionToLeft != mIsRtl ? mItemCount - 1 : 0; 419 View extremeChild = getChildAt(extremeItemPosition - mFirstPosition); 420 421 if (extremeChild == null) { 422 return deltaX; 423 } 424 425 int extremeChildCenter = getCenterOfView(extremeChild); 426 int galleryCenter = getCenterOfGallery(); 427 428 if (motionToLeft) { 429 if (extremeChildCenter <= galleryCenter) { 430 431 // The extreme child is past his boundary point! 432 return 0; 433 } 434 } else { 435 if (extremeChildCenter >= galleryCenter) { 436 437 // The extreme child is past his boundary point! 438 return 0; 439 } 440 } 441 442 int centerDifference = galleryCenter - extremeChildCenter; 443 444 return motionToLeft 445 ? Math.max(centerDifference, deltaX) 446 : Math.min(centerDifference, deltaX); 447 } 448 449 /** 450 * Offset the horizontal location of all children of this view by the 451 * specified number of pixels. 452 * 453 * @param offset the number of pixels to offset 454 */ 455 private void offsetChildrenLeftAndRight(int offset) { 456 for (int i = getChildCount() - 1; i >= 0; i--) { 457 getChildAt(i).offsetLeftAndRight(offset); 458 } 459 } 460 461 /** 462 * @return The center of this Gallery. 463 */ getCenterOfGallery()464 private int getCenterOfGallery() { 465 return (getWidth() - mPaddingLeft - mPaddingRight) / 2 + mPaddingLeft; 466 } 467 468 /** 469 * @return The center of the given view. 470 */ getCenterOfView(View view)471 private static int getCenterOfView(View view) { 472 return view.getLeft() + view.getWidth() / 2; 473 } 474 475 /** 476 * Detaches children that are off the screen (i.e.: Gallery bounds). 477 * 478 * @param toLeft Whether to detach children to the left of the Gallery, or 479 * to the right. 480 */ detachOffScreenChildren(boolean toLeft)481 private void detachOffScreenChildren(boolean toLeft) { 482 int numChildren = getChildCount(); 483 int firstPosition = mFirstPosition; 484 int start = 0; 485 int count = 0; 486 487 if (toLeft) { 488 final int galleryLeft = mPaddingLeft; 489 for (int i = 0; i < numChildren; i++) { 490 int n = mIsRtl ? (numChildren - 1 - i) : i; 491 final View child = getChildAt(n); 492 if (child.getRight() >= galleryLeft) { 493 break; 494 } else { 495 start = n; 496 count++; 497 mRecycler.put(firstPosition + n, child); 498 } 499 } 500 if (!mIsRtl) { 501 start = 0; 502 } 503 } else { 504 final int galleryRight = getWidth() - mPaddingRight; 505 for (int i = numChildren - 1; i >= 0; i--) { 506 int n = mIsRtl ? numChildren - 1 - i : i; 507 final View child = getChildAt(n); 508 if (child.getLeft() <= galleryRight) { 509 break; 510 } else { 511 start = n; 512 count++; 513 mRecycler.put(firstPosition + n, child); 514 } 515 } 516 if (mIsRtl) { 517 start = 0; 518 } 519 } 520 521 detachViewsFromParent(start, count); 522 523 if (toLeft != mIsRtl) { 524 mFirstPosition += count; 525 } 526 } 527 528 /** 529 * Scrolls the items so that the selected item is in its 'slot' (its center 530 * is the gallery's center). 531 */ scrollIntoSlots()532 private void scrollIntoSlots() { 533 534 if (getChildCount() == 0 || mSelectedChild == null) return; 535 536 int selectedCenter = getCenterOfView(mSelectedChild); 537 int targetCenter = getCenterOfGallery(); 538 539 int scrollAmount = targetCenter - selectedCenter; 540 if (scrollAmount != 0) { 541 mFlingRunnable.startUsingDistance(scrollAmount); 542 } else { 543 onFinishedMovement(); 544 } 545 } 546 onFinishedMovement()547 private void onFinishedMovement() { 548 if (mSuppressSelectionChanged) { 549 mSuppressSelectionChanged = false; 550 551 // We haven't been callbacking during the fling, so do it now 552 super.selectionChanged(); 553 } 554 mSelectedCenterOffset = 0; 555 invalidate(); 556 } 557 558 @Override selectionChanged()559 void selectionChanged() { 560 if (!mSuppressSelectionChanged) { 561 super.selectionChanged(); 562 } 563 } 564 565 /** 566 * Looks for the child that is closest to the center and sets it as the 567 * selected child. 568 */ setSelectionToCenterChild()569 private void setSelectionToCenterChild() { 570 571 View selView = mSelectedChild; 572 if (mSelectedChild == null) return; 573 574 int galleryCenter = getCenterOfGallery(); 575 576 // Common case where the current selected position is correct 577 if (selView.getLeft() <= galleryCenter && selView.getRight() >= galleryCenter) { 578 return; 579 } 580 581 // TODO better search 582 int closestEdgeDistance = Integer.MAX_VALUE; 583 int newSelectedChildIndex = 0; 584 for (int i = getChildCount() - 1; i >= 0; i--) { 585 586 View child = getChildAt(i); 587 588 if (child.getLeft() <= galleryCenter && child.getRight() >= galleryCenter) { 589 // This child is in the center 590 newSelectedChildIndex = i; 591 break; 592 } 593 594 int childClosestEdgeDistance = Math.min(Math.abs(child.getLeft() - galleryCenter), 595 Math.abs(child.getRight() - galleryCenter)); 596 if (childClosestEdgeDistance < closestEdgeDistance) { 597 closestEdgeDistance = childClosestEdgeDistance; 598 newSelectedChildIndex = i; 599 } 600 } 601 602 int newPos = mFirstPosition + newSelectedChildIndex; 603 604 if (newPos != mSelectedPosition) { 605 setSelectedPositionInt(newPos); 606 setNextSelectedPositionInt(newPos); 607 checkSelectionChanged(); 608 } 609 } 610 611 /** 612 * Creates and positions all views for this Gallery. 613 * <p> 614 * We layout rarely, most of the time {@link #trackMotionScroll(int)} takes 615 * care of repositioning, adding, and removing children. 616 * 617 * @param delta Change in the selected position. +1 means the selection is 618 * moving to the right, so views are scrolling to the left. -1 619 * means the selection is moving to the left. 620 */ 621 @Override layout(int delta, boolean animate)622 void layout(int delta, boolean animate) { 623 624 mIsRtl = isLayoutRtl(); 625 626 int childrenLeft = mSpinnerPadding.left; 627 int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right; 628 629 if (mDataChanged) { 630 handleDataChanged(); 631 } 632 633 // Handle an empty gallery by removing all views. 634 if (mItemCount == 0) { 635 resetList(); 636 return; 637 } 638 639 // Update to the new selected position. 640 if (mNextSelectedPosition >= 0) { 641 setSelectedPositionInt(mNextSelectedPosition); 642 } 643 644 // All views go in recycler while we are in layout 645 recycleAllViews(); 646 647 // Clear out old views 648 //removeAllViewsInLayout(); 649 detachAllViewsFromParent(); 650 651 /* 652 * These will be used to give initial positions to views entering the 653 * gallery as we scroll 654 */ 655 mRightMost = 0; 656 mLeftMost = 0; 657 658 // Make selected view and center it 659 660 /* 661 * mFirstPosition will be decreased as we add views to the left later 662 * on. The 0 for x will be offset in a couple lines down. 663 */ 664 mFirstPosition = mSelectedPosition; 665 View sel = makeAndAddView(mSelectedPosition, 0, 0, true); 666 667 // Put the selected child in the center 668 int selectedOffset = childrenLeft + (childrenWidth / 2) - (sel.getWidth() / 2) + 669 mSelectedCenterOffset; 670 sel.offsetLeftAndRight(selectedOffset); 671 672 fillToGalleryRight(); 673 fillToGalleryLeft(); 674 675 // Flush any cached views that did not get reused above 676 mRecycler.clear(); 677 678 invalidate(); 679 checkSelectionChanged(); 680 681 mDataChanged = false; 682 mNeedSync = false; 683 setNextSelectedPositionInt(mSelectedPosition); 684 685 updateSelectedItemMetadata(); 686 } 687 fillToGalleryLeft()688 private void fillToGalleryLeft() { 689 if (mIsRtl) { 690 fillToGalleryLeftRtl(); 691 } else { 692 fillToGalleryLeftLtr(); 693 } 694 } 695 fillToGalleryLeftRtl()696 private void fillToGalleryLeftRtl() { 697 int itemSpacing = mSpacing; 698 int galleryLeft = mPaddingLeft; 699 int numChildren = getChildCount(); 700 int numItems = mItemCount; 701 702 // Set state for initial iteration 703 View prevIterationView = getChildAt(numChildren - 1); 704 int curPosition; 705 int curRightEdge; 706 707 if (prevIterationView != null) { 708 curPosition = mFirstPosition + numChildren; 709 curRightEdge = prevIterationView.getLeft() - itemSpacing; 710 } else { 711 // No children available! 712 mFirstPosition = curPosition = mItemCount - 1; 713 curRightEdge = mRight - mLeft - mPaddingRight; 714 mShouldStopFling = true; 715 } 716 717 while (curRightEdge > galleryLeft && curPosition < mItemCount) { 718 prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition, 719 curRightEdge, false); 720 721 // Set state for next iteration 722 curRightEdge = prevIterationView.getLeft() - itemSpacing; 723 curPosition++; 724 } 725 } 726 fillToGalleryLeftLtr()727 private void fillToGalleryLeftLtr() { 728 int itemSpacing = mSpacing; 729 int galleryLeft = mPaddingLeft; 730 731 // Set state for initial iteration 732 View prevIterationView = getChildAt(0); 733 int curPosition; 734 int curRightEdge; 735 736 if (prevIterationView != null) { 737 curPosition = mFirstPosition - 1; 738 curRightEdge = prevIterationView.getLeft() - itemSpacing; 739 } else { 740 // No children available! 741 curPosition = 0; 742 curRightEdge = mRight - mLeft - mPaddingRight; 743 mShouldStopFling = true; 744 } 745 746 while (curRightEdge > galleryLeft && curPosition >= 0) { 747 prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition, 748 curRightEdge, false); 749 750 // Remember some state 751 mFirstPosition = curPosition; 752 753 // Set state for next iteration 754 curRightEdge = prevIterationView.getLeft() - itemSpacing; 755 curPosition--; 756 } 757 } 758 fillToGalleryRight()759 private void fillToGalleryRight() { 760 if (mIsRtl) { 761 fillToGalleryRightRtl(); 762 } else { 763 fillToGalleryRightLtr(); 764 } 765 } 766 fillToGalleryRightRtl()767 private void fillToGalleryRightRtl() { 768 int itemSpacing = mSpacing; 769 int galleryRight = mRight - mLeft - mPaddingRight; 770 771 // Set state for initial iteration 772 View prevIterationView = getChildAt(0); 773 int curPosition; 774 int curLeftEdge; 775 776 if (prevIterationView != null) { 777 curPosition = mFirstPosition -1; 778 curLeftEdge = prevIterationView.getRight() + itemSpacing; 779 } else { 780 curPosition = 0; 781 curLeftEdge = mPaddingLeft; 782 mShouldStopFling = true; 783 } 784 785 while (curLeftEdge < galleryRight && curPosition >= 0) { 786 prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition, 787 curLeftEdge, true); 788 789 // Remember some state 790 mFirstPosition = curPosition; 791 792 // Set state for next iteration 793 curLeftEdge = prevIterationView.getRight() + itemSpacing; 794 curPosition--; 795 } 796 } 797 fillToGalleryRightLtr()798 private void fillToGalleryRightLtr() { 799 int itemSpacing = mSpacing; 800 int galleryRight = mRight - mLeft - mPaddingRight; 801 int numChildren = getChildCount(); 802 int numItems = mItemCount; 803 804 // Set state for initial iteration 805 View prevIterationView = getChildAt(numChildren - 1); 806 int curPosition; 807 int curLeftEdge; 808 809 if (prevIterationView != null) { 810 curPosition = mFirstPosition + numChildren; 811 curLeftEdge = prevIterationView.getRight() + itemSpacing; 812 } else { 813 mFirstPosition = curPosition = mItemCount - 1; 814 curLeftEdge = mPaddingLeft; 815 mShouldStopFling = true; 816 } 817 818 while (curLeftEdge < galleryRight && curPosition < numItems) { 819 prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition, 820 curLeftEdge, true); 821 822 // Set state for next iteration 823 curLeftEdge = prevIterationView.getRight() + itemSpacing; 824 curPosition++; 825 } 826 } 827 828 /** 829 * Obtain a view, either by pulling an existing view from the recycler or by 830 * getting a new one from the adapter. If we are animating, make sure there 831 * is enough information in the view's layout parameters to animate from the 832 * old to new positions. 833 * 834 * @param position Position in the gallery for the view to obtain 835 * @param offset Offset from the selected position 836 * @param x X-coordinate indicating where this view should be placed. This 837 * will either be the left or right edge of the view, depending on 838 * the fromLeft parameter 839 * @param fromLeft Are we positioning views based on the left edge? (i.e., 840 * building from left to right)? 841 * @return A view that has been added to the gallery 842 */ makeAndAddView(int position, int offset, int x, boolean fromLeft)843 private View makeAndAddView(int position, int offset, int x, boolean fromLeft) { 844 845 View child; 846 if (!mDataChanged) { 847 child = mRecycler.get(position); 848 if (child != null) { 849 // Can reuse an existing view 850 int childLeft = child.getLeft(); 851 852 // Remember left and right edges of where views have been placed 853 mRightMost = Math.max(mRightMost, childLeft 854 + child.getMeasuredWidth()); 855 mLeftMost = Math.min(mLeftMost, childLeft); 856 857 // Position the view 858 setUpChild(child, offset, x, fromLeft); 859 860 return child; 861 } 862 } 863 864 // Nothing found in the recycler -- ask the adapter for a view 865 child = mAdapter.getView(position, null, this); 866 867 // Position the view 868 setUpChild(child, offset, x, fromLeft); 869 870 return child; 871 } 872 873 /** 874 * Helper for makeAndAddView to set the position of a view and fill out its 875 * layout parameters. 876 * 877 * @param child The view to position 878 * @param offset Offset from the selected position 879 * @param x X-coordinate indicating where this view should be placed. This 880 * will either be the left or right edge of the view, depending on 881 * the fromLeft parameter 882 * @param fromLeft Are we positioning views based on the left edge? (i.e., 883 * building from left to right)? 884 */ setUpChild(View child, int offset, int x, boolean fromLeft)885 private void setUpChild(View child, int offset, int x, boolean fromLeft) { 886 887 // Respect layout params that are already in the view. Otherwise 888 // make some up... 889 Gallery.LayoutParams lp = (Gallery.LayoutParams) child.getLayoutParams(); 890 if (lp == null) { 891 lp = (Gallery.LayoutParams) generateDefaultLayoutParams(); 892 } 893 894 addViewInLayout(child, fromLeft != mIsRtl ? -1 : 0, lp, true); 895 896 child.setSelected(offset == 0); 897 898 // Get measure specs 899 int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec, 900 mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height); 901 int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, 902 mSpinnerPadding.left + mSpinnerPadding.right, lp.width); 903 904 // Measure child 905 child.measure(childWidthSpec, childHeightSpec); 906 907 int childLeft; 908 int childRight; 909 910 // Position vertically based on gravity setting 911 int childTop = calculateTop(child, true); 912 int childBottom = childTop + child.getMeasuredHeight(); 913 914 int width = child.getMeasuredWidth(); 915 if (fromLeft) { 916 childLeft = x; 917 childRight = childLeft + width; 918 } else { 919 childLeft = x - width; 920 childRight = x; 921 } 922 923 child.layout(childLeft, childTop, childRight, childBottom); 924 } 925 926 /** 927 * Figure out vertical placement based on mGravity 928 * 929 * @param child Child to place 930 * @return Where the top of the child should be 931 */ calculateTop(View child, boolean duringLayout)932 private int calculateTop(View child, boolean duringLayout) { 933 int myHeight = duringLayout ? getMeasuredHeight() : getHeight(); 934 int childHeight = duringLayout ? child.getMeasuredHeight() : child.getHeight(); 935 936 int childTop = 0; 937 938 switch (mGravity) { 939 case Gravity.TOP: 940 childTop = mSpinnerPadding.top; 941 break; 942 case Gravity.CENTER_VERTICAL: 943 int availableSpace = myHeight - mSpinnerPadding.bottom 944 - mSpinnerPadding.top - childHeight; 945 childTop = mSpinnerPadding.top + (availableSpace / 2); 946 break; 947 case Gravity.BOTTOM: 948 childTop = myHeight - mSpinnerPadding.bottom - childHeight; 949 break; 950 } 951 return childTop; 952 } 953 954 @Override onTouchEvent(MotionEvent event)955 public boolean onTouchEvent(MotionEvent event) { 956 957 // Give everything to the gesture detector 958 boolean retValue = mGestureDetector.onTouchEvent(event); 959 960 int action = event.getAction(); 961 if (action == MotionEvent.ACTION_UP) { 962 // Helper method for lifted finger 963 onUp(); 964 } else if (action == MotionEvent.ACTION_CANCEL) { 965 onCancel(); 966 } 967 968 return retValue; 969 } 970 971 @Override onSingleTapUp(MotionEvent e)972 public boolean onSingleTapUp(MotionEvent e) { 973 974 if (mDownTouchPosition >= 0) { 975 976 // An item tap should make it selected, so scroll to this child. 977 scrollToChild(mDownTouchPosition - mFirstPosition); 978 979 // Also pass the click so the client knows, if it wants to. 980 if (mShouldCallbackOnUnselectedItemClick || mDownTouchPosition == mSelectedPosition) { 981 performItemClick(mDownTouchView, mDownTouchPosition, mAdapter 982 .getItemId(mDownTouchPosition)); 983 } 984 985 return true; 986 } 987 988 return false; 989 } 990 991 @Override onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)992 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 993 994 if (!mShouldCallbackDuringFling) { 995 // We want to suppress selection changes 996 997 // Remove any future code to set mSuppressSelectionChanged = false 998 removeCallbacks(mDisableSuppressSelectionChangedRunnable); 999 1000 // This will get reset once we scroll into slots 1001 if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true; 1002 } 1003 1004 // Fling the gallery! 1005 mFlingRunnable.startUsingVelocity((int) -velocityX); 1006 1007 return true; 1008 } 1009 1010 @Override onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)1011 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 1012 1013 if (localLOGV) Log.v(TAG, String.valueOf(e2.getX() - e1.getX())); 1014 1015 /* 1016 * Now's a good time to tell our parent to stop intercepting our events! 1017 * The user has moved more than the slop amount, since GestureDetector 1018 * ensures this before calling this method. Also, if a parent is more 1019 * interested in this touch's events than we are, it would have 1020 * intercepted them by now (for example, we can assume when a Gallery is 1021 * in the ListView, a vertical scroll would not end up in this method 1022 * since a ListView would have intercepted it by now). 1023 */ 1024 mParent.requestDisallowInterceptTouchEvent(true); 1025 1026 // As the user scrolls, we want to callback selection changes so related- 1027 // info on the screen is up-to-date with the gallery's selection 1028 if (!mShouldCallbackDuringFling) { 1029 if (mIsFirstScroll) { 1030 /* 1031 * We're not notifying the client of selection changes during 1032 * the fling, and this scroll could possibly be a fling. Don't 1033 * do selection changes until we're sure it is not a fling. 1034 */ 1035 if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true; 1036 postDelayed(mDisableSuppressSelectionChangedRunnable, SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT); 1037 } 1038 } else { 1039 if (mSuppressSelectionChanged) mSuppressSelectionChanged = false; 1040 } 1041 1042 // Track the motion 1043 trackMotionScroll(-1 * (int) distanceX); 1044 1045 mIsFirstScroll = false; 1046 return true; 1047 } 1048 1049 @Override onDown(MotionEvent e)1050 public boolean onDown(MotionEvent e) { 1051 1052 // Kill any existing fling/scroll 1053 mFlingRunnable.stop(false); 1054 1055 // Get the item's view that was touched 1056 mDownTouchPosition = pointToPosition((int) e.getX(), (int) e.getY()); 1057 1058 if (mDownTouchPosition >= 0) { 1059 mDownTouchView = getChildAt(mDownTouchPosition - mFirstPosition); 1060 mDownTouchView.setPressed(true); 1061 } 1062 1063 // Reset the multiple-scroll tracking state 1064 mIsFirstScroll = true; 1065 1066 // Must return true to get matching events for this down event. 1067 return true; 1068 } 1069 1070 /** 1071 * Called when a touch event's action is MotionEvent.ACTION_UP. 1072 */ onUp()1073 void onUp() { 1074 1075 if (mFlingRunnable.mScroller.isFinished()) { 1076 scrollIntoSlots(); 1077 } 1078 1079 dispatchUnpress(); 1080 } 1081 1082 /** 1083 * Called when a touch event's action is MotionEvent.ACTION_CANCEL. 1084 */ onCancel()1085 void onCancel() { 1086 onUp(); 1087 } 1088 1089 @Override onLongPress(MotionEvent e)1090 public void onLongPress(MotionEvent e) { 1091 1092 if (mDownTouchPosition < 0) { 1093 return; 1094 } 1095 1096 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 1097 long id = getItemIdAtPosition(mDownTouchPosition); 1098 dispatchLongPress(mDownTouchView, mDownTouchPosition, id); 1099 } 1100 1101 // Unused methods from GestureDetector.OnGestureListener below 1102 1103 @Override onShowPress(MotionEvent e)1104 public void onShowPress(MotionEvent e) { 1105 } 1106 1107 // Unused methods from GestureDetector.OnGestureListener above 1108 dispatchPress(View child)1109 private void dispatchPress(View child) { 1110 1111 if (child != null) { 1112 child.setPressed(true); 1113 } 1114 1115 setPressed(true); 1116 } 1117 dispatchUnpress()1118 private void dispatchUnpress() { 1119 1120 for (int i = getChildCount() - 1; i >= 0; i--) { 1121 getChildAt(i).setPressed(false); 1122 } 1123 1124 setPressed(false); 1125 } 1126 1127 @Override dispatchSetSelected(boolean selected)1128 public void dispatchSetSelected(boolean selected) { 1129 /* 1130 * We don't want to pass the selected state given from its parent to its 1131 * children since this widget itself has a selected state to give to its 1132 * children. 1133 */ 1134 } 1135 1136 @Override dispatchSetPressed(boolean pressed)1137 protected void dispatchSetPressed(boolean pressed) { 1138 1139 // Show the pressed state on the selected child 1140 if (mSelectedChild != null) { 1141 mSelectedChild.setPressed(pressed); 1142 } 1143 } 1144 1145 @Override getContextMenuInfo()1146 protected ContextMenuInfo getContextMenuInfo() { 1147 return mContextMenuInfo; 1148 } 1149 1150 @Override showContextMenuForChild(View originalView)1151 public boolean showContextMenuForChild(View originalView) { 1152 1153 final int longPressPosition = getPositionForView(originalView); 1154 if (longPressPosition < 0) { 1155 return false; 1156 } 1157 1158 final long longPressId = mAdapter.getItemId(longPressPosition); 1159 return dispatchLongPress(originalView, longPressPosition, longPressId); 1160 } 1161 1162 @Override showContextMenu()1163 public boolean showContextMenu() { 1164 1165 if (isPressed() && mSelectedPosition >= 0) { 1166 int index = mSelectedPosition - mFirstPosition; 1167 View v = getChildAt(index); 1168 return dispatchLongPress(v, mSelectedPosition, mSelectedRowId); 1169 } 1170 1171 return false; 1172 } 1173 dispatchLongPress(View view, int position, long id)1174 private boolean dispatchLongPress(View view, int position, long id) { 1175 boolean handled = false; 1176 1177 if (mOnItemLongClickListener != null) { 1178 handled = mOnItemLongClickListener.onItemLongClick(this, mDownTouchView, 1179 mDownTouchPosition, id); 1180 } 1181 1182 if (!handled) { 1183 mContextMenuInfo = new AdapterContextMenuInfo(view, position, id); 1184 handled = super.showContextMenuForChild(this); 1185 } 1186 1187 if (handled) { 1188 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 1189 } 1190 1191 return handled; 1192 } 1193 1194 @Override dispatchKeyEvent(KeyEvent event)1195 public boolean dispatchKeyEvent(KeyEvent event) { 1196 // Gallery steals all key events 1197 return event.dispatch(this, null, null); 1198 } 1199 1200 /** 1201 * Handles left, right, and clicking 1202 * @see android.view.View#onKeyDown 1203 */ 1204 @Override onKeyDown(int keyCode, KeyEvent event)1205 public boolean onKeyDown(int keyCode, KeyEvent event) { 1206 switch (keyCode) { 1207 1208 case KeyEvent.KEYCODE_DPAD_LEFT: 1209 if (movePrevious()) { 1210 playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); 1211 return true; 1212 } 1213 break; 1214 case KeyEvent.KEYCODE_DPAD_RIGHT: 1215 if (moveNext()) { 1216 playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); 1217 return true; 1218 } 1219 break; 1220 case KeyEvent.KEYCODE_DPAD_CENTER: 1221 case KeyEvent.KEYCODE_ENTER: 1222 mReceivedInvokeKeyDown = true; 1223 // fallthrough to default handling 1224 } 1225 1226 return super.onKeyDown(keyCode, event); 1227 } 1228 1229 @Override onKeyUp(int keyCode, KeyEvent event)1230 public boolean onKeyUp(int keyCode, KeyEvent event) { 1231 switch (keyCode) { 1232 case KeyEvent.KEYCODE_DPAD_CENTER: 1233 case KeyEvent.KEYCODE_ENTER: { 1234 1235 if (mReceivedInvokeKeyDown) { 1236 if (mItemCount > 0) { 1237 1238 dispatchPress(mSelectedChild); 1239 postDelayed(new Runnable() { 1240 @Override 1241 public void run() { 1242 dispatchUnpress(); 1243 } 1244 }, ViewConfiguration.getPressedStateDuration()); 1245 1246 int selectedIndex = mSelectedPosition - mFirstPosition; 1247 performItemClick(getChildAt(selectedIndex), mSelectedPosition, mAdapter 1248 .getItemId(mSelectedPosition)); 1249 } 1250 } 1251 1252 // Clear the flag 1253 mReceivedInvokeKeyDown = false; 1254 1255 return true; 1256 } 1257 } 1258 1259 return super.onKeyUp(keyCode, event); 1260 } 1261 movePrevious()1262 boolean movePrevious() { 1263 if (mItemCount > 0 && mSelectedPosition > 0) { 1264 scrollToChild(mSelectedPosition - mFirstPosition - 1); 1265 return true; 1266 } else { 1267 return false; 1268 } 1269 } 1270 moveNext()1271 boolean moveNext() { 1272 if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) { 1273 scrollToChild(mSelectedPosition - mFirstPosition + 1); 1274 return true; 1275 } else { 1276 return false; 1277 } 1278 } 1279 scrollToChild(int childPosition)1280 private boolean scrollToChild(int childPosition) { 1281 View child = getChildAt(childPosition); 1282 1283 if (child != null) { 1284 int distance = getCenterOfGallery() - getCenterOfView(child); 1285 mFlingRunnable.startUsingDistance(distance); 1286 return true; 1287 } 1288 1289 return false; 1290 } 1291 1292 @Override setSelectedPositionInt(int position)1293 void setSelectedPositionInt(int position) { 1294 super.setSelectedPositionInt(position); 1295 1296 // Updates any metadata we keep about the selected item. 1297 updateSelectedItemMetadata(); 1298 } 1299 updateSelectedItemMetadata()1300 private void updateSelectedItemMetadata() { 1301 1302 View oldSelectedChild = mSelectedChild; 1303 1304 View child = mSelectedChild = getChildAt(mSelectedPosition - mFirstPosition); 1305 if (child == null) { 1306 return; 1307 } 1308 1309 child.setSelected(true); 1310 child.setFocusable(true); 1311 1312 if (hasFocus()) { 1313 child.requestFocus(); 1314 } 1315 1316 // We unfocus the old child down here so the above hasFocus check 1317 // returns true 1318 if (oldSelectedChild != null && oldSelectedChild != child) { 1319 1320 // Make sure its drawable state doesn't contain 'selected' 1321 oldSelectedChild.setSelected(false); 1322 1323 // Make sure it is not focusable anymore, since otherwise arrow keys 1324 // can make this one be focused 1325 oldSelectedChild.setFocusable(false); 1326 } 1327 1328 } 1329 1330 /** 1331 * Describes how the child views are aligned. 1332 * @param gravity 1333 * 1334 * @attr ref android.R.styleable#Gallery_gravity 1335 */ setGravity(int gravity)1336 public void setGravity(int gravity) 1337 { 1338 if (mGravity != gravity) { 1339 mGravity = gravity; 1340 requestLayout(); 1341 } 1342 } 1343 1344 @Override getChildDrawingOrder(int childCount, int i)1345 protected int getChildDrawingOrder(int childCount, int i) { 1346 int selectedIndex = mSelectedPosition - mFirstPosition; 1347 1348 // Just to be safe 1349 if (selectedIndex < 0) return i; 1350 1351 if (i == childCount - 1) { 1352 // Draw the selected child last 1353 return selectedIndex; 1354 } else if (i >= selectedIndex) { 1355 // Move the children after the selected child earlier one 1356 return i + 1; 1357 } else { 1358 // Keep the children before the selected child the same 1359 return i; 1360 } 1361 } 1362 1363 @Override onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)1364 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 1365 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 1366 1367 /* 1368 * The gallery shows focus by focusing the selected item. So, give 1369 * focus to our selected item instead. We steal keys from our 1370 * selected item elsewhere. 1371 */ 1372 if (gainFocus && mSelectedChild != null) { 1373 mSelectedChild.requestFocus(direction); 1374 mSelectedChild.setSelected(true); 1375 } 1376 1377 } 1378 1379 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)1380 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1381 super.onInitializeAccessibilityEvent(event); 1382 event.setClassName(Gallery.class.getName()); 1383 } 1384 1385 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)1386 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1387 super.onInitializeAccessibilityNodeInfo(info); 1388 info.setClassName(Gallery.class.getName()); 1389 info.setScrollable(mItemCount > 1); 1390 if (isEnabled()) { 1391 if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) { 1392 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); 1393 } 1394 if (isEnabled() && mItemCount > 0 && mSelectedPosition > 0) { 1395 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 1396 } 1397 } 1398 } 1399 1400 @Override performAccessibilityAction(int action, Bundle arguments)1401 public boolean performAccessibilityAction(int action, Bundle arguments) { 1402 if (super.performAccessibilityAction(action, arguments)) { 1403 return true; 1404 } 1405 switch (action) { 1406 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 1407 if (isEnabled() && mItemCount > 0 && mSelectedPosition < mItemCount - 1) { 1408 final int currentChildIndex = mSelectedPosition - mFirstPosition; 1409 return scrollToChild(currentChildIndex + 1); 1410 } 1411 } return false; 1412 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 1413 if (isEnabled() && mItemCount > 0 && mSelectedPosition > 0) { 1414 final int currentChildIndex = mSelectedPosition - mFirstPosition; 1415 return scrollToChild(currentChildIndex - 1); 1416 } 1417 } return false; 1418 } 1419 return false; 1420 } 1421 1422 /** 1423 * Responsible for fling behavior. Use {@link #startUsingVelocity(int)} to 1424 * initiate a fling. Each frame of the fling is handled in {@link #run()}. 1425 * A FlingRunnable will keep re-posting itself until the fling is done. 1426 */ 1427 private class FlingRunnable implements Runnable { 1428 /** 1429 * Tracks the decay of a fling scroll 1430 */ 1431 private Scroller mScroller; 1432 1433 /** 1434 * X value reported by mScroller on the previous fling 1435 */ 1436 private int mLastFlingX; 1437 FlingRunnable()1438 public FlingRunnable() { 1439 mScroller = new Scroller(getContext()); 1440 } 1441 startCommon()1442 private void startCommon() { 1443 // Remove any pending flings 1444 removeCallbacks(this); 1445 } 1446 startUsingVelocity(int initialVelocity)1447 public void startUsingVelocity(int initialVelocity) { 1448 if (initialVelocity == 0) return; 1449 1450 startCommon(); 1451 1452 int initialX = initialVelocity < 0 ? Integer.MAX_VALUE : 0; 1453 mLastFlingX = initialX; 1454 mScroller.fling(initialX, 0, initialVelocity, 0, 1455 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE); 1456 post(this); 1457 } 1458 1459 public void startUsingDistance(int distance) { 1460 if (distance == 0) return; 1461 1462 startCommon(); 1463 1464 mLastFlingX = 0; 1465 mScroller.startScroll(0, 0, -distance, 0, mAnimationDuration); 1466 post(this); 1467 } 1468 1469 public void stop(boolean scrollIntoSlots) { 1470 removeCallbacks(this); 1471 endFling(scrollIntoSlots); 1472 } 1473 1474 private void endFling(boolean scrollIntoSlots) { 1475 /* 1476 * Force the scroller's status to finished (without setting its 1477 * position to the end) 1478 */ 1479 mScroller.forceFinished(true); 1480 1481 if (scrollIntoSlots) scrollIntoSlots(); 1482 } 1483 1484 @Override 1485 public void run() { 1486 1487 if (mItemCount == 0) { 1488 endFling(true); 1489 return; 1490 } 1491 1492 mShouldStopFling = false; 1493 1494 final Scroller scroller = mScroller; 1495 boolean more = scroller.computeScrollOffset(); 1496 final int x = scroller.getCurrX(); 1497 1498 // Flip sign to convert finger direction to list items direction 1499 // (e.g. finger moving down means list is moving towards the top) 1500 int delta = mLastFlingX - x; 1501 1502 // Pretend that each frame of a fling scroll is a touch scroll 1503 if (delta > 0) { 1504 // Moving towards the left. Use leftmost view as mDownTouchPosition 1505 mDownTouchPosition = mIsRtl ? (mFirstPosition + getChildCount() - 1) : 1506 mFirstPosition; 1507 1508 // Don't fling more than 1 screen 1509 delta = Math.min(getWidth() - mPaddingLeft - mPaddingRight - 1, delta); 1510 } else { 1511 // Moving towards the right. Use rightmost view as mDownTouchPosition 1512 int offsetToLast = getChildCount() - 1; 1513 mDownTouchPosition = mIsRtl ? mFirstPosition : 1514 (mFirstPosition + getChildCount() - 1); 1515 1516 // Don't fling more than 1 screen 1517 delta = Math.max(-(getWidth() - mPaddingRight - mPaddingLeft - 1), delta); 1518 } 1519 1520 trackMotionScroll(delta); 1521 1522 if (more && !mShouldStopFling) { 1523 mLastFlingX = x; 1524 post(this); 1525 } else { 1526 endFling(true); 1527 } 1528 } 1529 1530 } 1531 1532 /** 1533 * Gallery extends LayoutParams to provide a place to hold current 1534 * Transformation information along with previous position/transformation 1535 * info. 1536 */ 1537 public static class LayoutParams extends ViewGroup.LayoutParams { LayoutParams(Context c, AttributeSet attrs)1538 public LayoutParams(Context c, AttributeSet attrs) { 1539 super(c, attrs); 1540 } 1541 LayoutParams(int w, int h)1542 public LayoutParams(int w, int h) { 1543 super(w, h); 1544 } 1545 LayoutParams(ViewGroup.LayoutParams source)1546 public LayoutParams(ViewGroup.LayoutParams source) { 1547 super(source); 1548 } 1549 } 1550 } 1551