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