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