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