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