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