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