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.content.Context; 20 import android.content.Intent; 21 import android.content.res.TypedArray; 22 import android.graphics.Rect; 23 import android.util.AttributeSet; 24 import android.view.Gravity; 25 import android.view.KeyEvent; 26 import android.view.SoundEffectConstants; 27 import android.view.View; 28 import android.view.ViewDebug; 29 import android.view.ViewGroup; 30 import android.view.accessibility.AccessibilityEvent; 31 import android.view.accessibility.AccessibilityNodeInfo; 32 import android.view.animation.GridLayoutAnimationController; 33 import android.widget.RemoteViews.RemoteView; 34 35 36 /** 37 * A view that shows items in two-dimensional scrolling grid. The items in the 38 * grid come from the {@link ListAdapter} associated with this view. 39 * 40 * <p>See the <a href="{@docRoot}guide/topics/ui/layout/gridview.html">Grid 41 * View</a> guide.</p> 42 * 43 * @attr ref android.R.styleable#GridView_horizontalSpacing 44 * @attr ref android.R.styleable#GridView_verticalSpacing 45 * @attr ref android.R.styleable#GridView_stretchMode 46 * @attr ref android.R.styleable#GridView_columnWidth 47 * @attr ref android.R.styleable#GridView_numColumns 48 * @attr ref android.R.styleable#GridView_gravity 49 */ 50 @RemoteView 51 public class GridView extends AbsListView { 52 /** 53 * Disables stretching. 54 * 55 * @see #setStretchMode(int) 56 */ 57 public static final int NO_STRETCH = 0; 58 /** 59 * Stretches the spacing between columns. 60 * 61 * @see #setStretchMode(int) 62 */ 63 public static final int STRETCH_SPACING = 1; 64 /** 65 * Stretches columns. 66 * 67 * @see #setStretchMode(int) 68 */ 69 public static final int STRETCH_COLUMN_WIDTH = 2; 70 /** 71 * Stretches the spacing between columns. The spacing is uniform. 72 * 73 * @see #setStretchMode(int) 74 */ 75 public static final int STRETCH_SPACING_UNIFORM = 3; 76 77 /** 78 * Creates as many columns as can fit on screen. 79 * 80 * @see #setNumColumns(int) 81 */ 82 public static final int AUTO_FIT = -1; 83 84 private int mNumColumns = AUTO_FIT; 85 86 private int mHorizontalSpacing = 0; 87 private int mRequestedHorizontalSpacing; 88 private int mVerticalSpacing = 0; 89 private int mStretchMode = STRETCH_COLUMN_WIDTH; 90 private int mColumnWidth; 91 private int mRequestedColumnWidth; 92 private int mRequestedNumColumns; 93 94 private View mReferenceView = null; 95 private View mReferenceViewInSelectedRow = null; 96 97 private int mGravity = Gravity.START; 98 99 private final Rect mTempRect = new Rect(); 100 101 public GridView(Context context) { 102 this(context, null); 103 } 104 105 public GridView(Context context, AttributeSet attrs) { 106 this(context, attrs, com.android.internal.R.attr.gridViewStyle); 107 } 108 109 public GridView(Context context, AttributeSet attrs, int defStyle) { 110 super(context, attrs, defStyle); 111 112 TypedArray a = context.obtainStyledAttributes(attrs, 113 com.android.internal.R.styleable.GridView, defStyle, 0); 114 115 int hSpacing = a.getDimensionPixelOffset( 116 com.android.internal.R.styleable.GridView_horizontalSpacing, 0); 117 setHorizontalSpacing(hSpacing); 118 119 int vSpacing = a.getDimensionPixelOffset( 120 com.android.internal.R.styleable.GridView_verticalSpacing, 0); 121 setVerticalSpacing(vSpacing); 122 123 int index = a.getInt(com.android.internal.R.styleable.GridView_stretchMode, STRETCH_COLUMN_WIDTH); 124 if (index >= 0) { 125 setStretchMode(index); 126 } 127 128 int columnWidth = a.getDimensionPixelOffset(com.android.internal.R.styleable.GridView_columnWidth, -1); 129 if (columnWidth > 0) { 130 setColumnWidth(columnWidth); 131 } 132 133 int numColumns = a.getInt(com.android.internal.R.styleable.GridView_numColumns, 1); 134 setNumColumns(numColumns); 135 136 index = a.getInt(com.android.internal.R.styleable.GridView_gravity, -1); 137 if (index >= 0) { 138 setGravity(index); 139 } 140 141 a.recycle(); 142 } 143 144 @Override 145 public ListAdapter getAdapter() { 146 return mAdapter; 147 } 148 149 /** 150 * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService 151 * through the specified intent. 152 * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to. 153 */ 154 @android.view.RemotableViewMethod 155 public void setRemoteViewsAdapter(Intent intent) { 156 super.setRemoteViewsAdapter(intent); 157 } 158 159 /** 160 * Sets the data behind this GridView. 161 * 162 * @param adapter the adapter providing the grid's data 163 */ 164 @Override 165 public void setAdapter(ListAdapter adapter) { 166 if (mAdapter != null && mDataSetObserver != null) { 167 mAdapter.unregisterDataSetObserver(mDataSetObserver); 168 } 169 170 resetList(); 171 mRecycler.clear(); 172 mAdapter = adapter; 173 174 mOldSelectedPosition = INVALID_POSITION; 175 mOldSelectedRowId = INVALID_ROW_ID; 176 177 // AbsListView#setAdapter will update choice mode states. 178 super.setAdapter(adapter); 179 180 if (mAdapter != null) { 181 mOldItemCount = mItemCount; 182 mItemCount = mAdapter.getCount(); 183 mDataChanged = true; 184 checkFocus(); 185 186 mDataSetObserver = new AdapterDataSetObserver(); 187 mAdapter.registerDataSetObserver(mDataSetObserver); 188 189 mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); 190 191 int position; 192 if (mStackFromBottom) { 193 position = lookForSelectablePosition(mItemCount - 1, false); 194 } else { 195 position = lookForSelectablePosition(0, true); 196 } 197 setSelectedPositionInt(position); 198 setNextSelectedPositionInt(position); 199 checkSelectionChanged(); 200 } else { 201 checkFocus(); 202 // Nothing selected 203 checkSelectionChanged(); 204 } 205 206 requestLayout(); 207 } 208 209 @Override 210 int lookForSelectablePosition(int position, boolean lookDown) { 211 final ListAdapter adapter = mAdapter; 212 if (adapter == null || isInTouchMode()) { 213 return INVALID_POSITION; 214 } 215 216 if (position < 0 || position >= mItemCount) { 217 return INVALID_POSITION; 218 } 219 return position; 220 } 221 222 /** 223 * {@inheritDoc} 224 */ 225 @Override 226 void fillGap(boolean down) { 227 final int numColumns = mNumColumns; 228 final int verticalSpacing = mVerticalSpacing; 229 230 final int count = getChildCount(); 231 232 if (down) { 233 int paddingTop = 0; 234 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 235 paddingTop = getListPaddingTop(); 236 } 237 final int startOffset = count > 0 ? 238 getChildAt(count - 1).getBottom() + verticalSpacing : paddingTop; 239 int position = mFirstPosition + count; 240 if (mStackFromBottom) { 241 position += numColumns - 1; 242 } 243 fillDown(position, startOffset); 244 correctTooHigh(numColumns, verticalSpacing, getChildCount()); 245 } else { 246 int paddingBottom = 0; 247 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 248 paddingBottom = getListPaddingBottom(); 249 } 250 final int startOffset = count > 0 ? 251 getChildAt(0).getTop() - verticalSpacing : getHeight() - paddingBottom; 252 int position = mFirstPosition; 253 if (!mStackFromBottom) { 254 position -= numColumns; 255 } else { 256 position--; 257 } 258 fillUp(position, startOffset); 259 correctTooLow(numColumns, verticalSpacing, getChildCount()); 260 } 261 } 262 263 /** 264 * Fills the list from pos down to the end of the list view. 265 * 266 * @param pos The first position to put in the list 267 * 268 * @param nextTop The location where the top of the item associated with pos 269 * should be drawn 270 * 271 * @return The view that is currently selected, if it happens to be in the 272 * range that we draw. 273 */ 274 private View fillDown(int pos, int nextTop) { 275 View selectedView = null; 276 277 int end = (mBottom - mTop); 278 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 279 end -= mListPadding.bottom; 280 } 281 282 while (nextTop < end && pos < mItemCount) { 283 View temp = makeRow(pos, nextTop, true); 284 if (temp != null) { 285 selectedView = temp; 286 } 287 288 // mReferenceView will change with each call to makeRow() 289 // do not cache in a local variable outside of this loop 290 nextTop = mReferenceView.getBottom() + mVerticalSpacing; 291 292 pos += mNumColumns; 293 } 294 295 setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); 296 return selectedView; 297 } 298 299 private View makeRow(int startPos, int y, boolean flow) { 300 final int columnWidth = mColumnWidth; 301 final int horizontalSpacing = mHorizontalSpacing; 302 303 final boolean isLayoutRtl = isLayoutRtl(); 304 305 int last; 306 int nextLeft; 307 308 if (isLayoutRtl) { 309 nextLeft = getWidth() - mListPadding.right - columnWidth - 310 ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0); 311 } else { 312 nextLeft = mListPadding.left + 313 ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0); 314 } 315 316 if (!mStackFromBottom) { 317 last = Math.min(startPos + mNumColumns, mItemCount); 318 } else { 319 last = startPos + 1; 320 startPos = Math.max(0, startPos - mNumColumns + 1); 321 322 if (last - startPos < mNumColumns) { 323 final int deltaLeft = (mNumColumns - (last - startPos)) * (columnWidth + horizontalSpacing); 324 nextLeft += (isLayoutRtl ? -1 : +1) * deltaLeft; 325 } 326 } 327 328 View selectedView = null; 329 330 final boolean hasFocus = shouldShowSelector(); 331 final boolean inClick = touchModeDrawsInPressedState(); 332 final int selectedPosition = mSelectedPosition; 333 334 View child = null; 335 for (int pos = startPos; pos < last; pos++) { 336 // is this the selected item? 337 boolean selected = pos == selectedPosition; 338 // does the list view have focus or contain focus 339 340 final int where = flow ? -1 : pos - startPos; 341 child = makeAndAddView(pos, y, flow, nextLeft, selected, where); 342 343 nextLeft += (isLayoutRtl ? -1 : +1) * columnWidth; 344 if (pos < last - 1) { 345 nextLeft += horizontalSpacing; 346 } 347 348 if (selected && (hasFocus || inClick)) { 349 selectedView = child; 350 } 351 } 352 353 mReferenceView = child; 354 355 if (selectedView != null) { 356 mReferenceViewInSelectedRow = mReferenceView; 357 } 358 359 return selectedView; 360 } 361 362 /** 363 * Fills the list from pos up to the top of the list view. 364 * 365 * @param pos The first position to put in the list 366 * 367 * @param nextBottom The location where the bottom of the item associated 368 * with pos should be drawn 369 * 370 * @return The view that is currently selected 371 */ 372 private View fillUp(int pos, int nextBottom) { 373 View selectedView = null; 374 375 int end = 0; 376 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 377 end = mListPadding.top; 378 } 379 380 while (nextBottom > end && pos >= 0) { 381 382 View temp = makeRow(pos, nextBottom, false); 383 if (temp != null) { 384 selectedView = temp; 385 } 386 387 nextBottom = mReferenceView.getTop() - mVerticalSpacing; 388 389 mFirstPosition = pos; 390 391 pos -= mNumColumns; 392 } 393 394 if (mStackFromBottom) { 395 mFirstPosition = Math.max(0, pos + 1); 396 } 397 398 setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); 399 return selectedView; 400 } 401 402 /** 403 * Fills the list from top to bottom, starting with mFirstPosition 404 * 405 * @param nextTop The location where the top of the first item should be 406 * drawn 407 * 408 * @return The view that is currently selected 409 */ 410 private View fillFromTop(int nextTop) { 411 mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); 412 mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); 413 if (mFirstPosition < 0) { 414 mFirstPosition = 0; 415 } 416 mFirstPosition -= mFirstPosition % mNumColumns; 417 return fillDown(mFirstPosition, nextTop); 418 } 419 420 private View fillFromBottom(int lastPosition, int nextBottom) { 421 lastPosition = Math.max(lastPosition, mSelectedPosition); 422 lastPosition = Math.min(lastPosition, mItemCount - 1); 423 424 final int invertedPosition = mItemCount - 1 - lastPosition; 425 lastPosition = mItemCount - 1 - (invertedPosition - (invertedPosition % mNumColumns)); 426 427 return fillUp(lastPosition, nextBottom); 428 } 429 430 private View fillSelection(int childrenTop, int childrenBottom) { 431 final int selectedPosition = reconcileSelectedPosition(); 432 final int numColumns = mNumColumns; 433 final int verticalSpacing = mVerticalSpacing; 434 435 int rowStart; 436 int rowEnd = -1; 437 438 if (!mStackFromBottom) { 439 rowStart = selectedPosition - (selectedPosition % numColumns); 440 } else { 441 final int invertedSelection = mItemCount - 1 - selectedPosition; 442 443 rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 444 rowStart = Math.max(0, rowEnd - numColumns + 1); 445 } 446 447 final int fadingEdgeLength = getVerticalFadingEdgeLength(); 448 final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); 449 450 final View sel = makeRow(mStackFromBottom ? rowEnd : rowStart, topSelectionPixel, true); 451 mFirstPosition = rowStart; 452 453 final View referenceView = mReferenceView; 454 455 if (!mStackFromBottom) { 456 fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); 457 pinToBottom(childrenBottom); 458 fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); 459 adjustViewsUpOrDown(); 460 } else { 461 final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, 462 fadingEdgeLength, numColumns, rowStart); 463 final int offset = bottomSelectionPixel - referenceView.getBottom(); 464 offsetChildrenTopAndBottom(offset); 465 fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); 466 pinToTop(childrenTop); 467 fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); 468 adjustViewsUpOrDown(); 469 } 470 471 return sel; 472 } 473 474 private void pinToTop(int childrenTop) { 475 if (mFirstPosition == 0) { 476 final int top = getChildAt(0).getTop(); 477 final int offset = childrenTop - top; 478 if (offset < 0) { 479 offsetChildrenTopAndBottom(offset); 480 } 481 } 482 } 483 484 private void pinToBottom(int childrenBottom) { 485 final int count = getChildCount(); 486 if (mFirstPosition + count == mItemCount) { 487 final int bottom = getChildAt(count - 1).getBottom(); 488 final int offset = childrenBottom - bottom; 489 if (offset > 0) { 490 offsetChildrenTopAndBottom(offset); 491 } 492 } 493 } 494 495 @Override 496 int findMotionRow(int y) { 497 final int childCount = getChildCount(); 498 if (childCount > 0) { 499 500 final int numColumns = mNumColumns; 501 if (!mStackFromBottom) { 502 for (int i = 0; i < childCount; i += numColumns) { 503 if (y <= getChildAt(i).getBottom()) { 504 return mFirstPosition + i; 505 } 506 } 507 } else { 508 for (int i = childCount - 1; i >= 0; i -= numColumns) { 509 if (y >= getChildAt(i).getTop()) { 510 return mFirstPosition + i; 511 } 512 } 513 } 514 } 515 return INVALID_POSITION; 516 } 517 518 /** 519 * Layout during a scroll that results from tracking motion events. Places 520 * the mMotionPosition view at the offset specified by mMotionViewTop, and 521 * then build surrounding views from there. 522 * 523 * @param position the position at which to start filling 524 * @param top the top of the view at that position 525 * @return The selected view, or null if the selected view is outside the 526 * visible area. 527 */ 528 private View fillSpecific(int position, int top) { 529 final int numColumns = mNumColumns; 530 531 int motionRowStart; 532 int motionRowEnd = -1; 533 534 if (!mStackFromBottom) { 535 motionRowStart = position - (position % numColumns); 536 } else { 537 final int invertedSelection = mItemCount - 1 - position; 538 539 motionRowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 540 motionRowStart = Math.max(0, motionRowEnd - numColumns + 1); 541 } 542 543 final View temp = makeRow(mStackFromBottom ? motionRowEnd : motionRowStart, top, true); 544 545 // Possibly changed again in fillUp if we add rows above this one. 546 mFirstPosition = motionRowStart; 547 548 final View referenceView = mReferenceView; 549 // We didn't have anything to layout, bail out 550 if (referenceView == null) { 551 return null; 552 } 553 554 final int verticalSpacing = mVerticalSpacing; 555 556 View above; 557 View below; 558 559 if (!mStackFromBottom) { 560 above = fillUp(motionRowStart - numColumns, referenceView.getTop() - verticalSpacing); 561 adjustViewsUpOrDown(); 562 below = fillDown(motionRowStart + numColumns, referenceView.getBottom() + verticalSpacing); 563 // Check if we have dragged the bottom of the grid too high 564 final int childCount = getChildCount(); 565 if (childCount > 0) { 566 correctTooHigh(numColumns, verticalSpacing, childCount); 567 } 568 } else { 569 below = fillDown(motionRowEnd + numColumns, referenceView.getBottom() + verticalSpacing); 570 adjustViewsUpOrDown(); 571 above = fillUp(motionRowStart - 1, referenceView.getTop() - verticalSpacing); 572 // Check if we have dragged the bottom of the grid too high 573 final int childCount = getChildCount(); 574 if (childCount > 0) { 575 correctTooLow(numColumns, verticalSpacing, childCount); 576 } 577 } 578 579 if (temp != null) { 580 return temp; 581 } else if (above != null) { 582 return above; 583 } else { 584 return below; 585 } 586 } 587 588 private void correctTooHigh(int numColumns, int verticalSpacing, int childCount) { 589 // First see if the last item is visible 590 final int lastPosition = mFirstPosition + childCount - 1; 591 if (lastPosition == mItemCount - 1 && childCount > 0) { 592 // Get the last child ... 593 final View lastChild = getChildAt(childCount - 1); 594 595 // ... and its bottom edge 596 final int lastBottom = lastChild.getBottom(); 597 // This is bottom of our drawable area 598 final int end = (mBottom - mTop) - mListPadding.bottom; 599 600 // This is how far the bottom edge of the last view is from the bottom of the 601 // drawable area 602 int bottomOffset = end - lastBottom; 603 604 final View firstChild = getChildAt(0); 605 final int firstTop = firstChild.getTop(); 606 607 // Make sure we are 1) Too high, and 2) Either there are more rows above the 608 // first row or the first row is scrolled off the top of the drawable area 609 if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top)) { 610 if (mFirstPosition == 0) { 611 // Don't pull the top too far down 612 bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop); 613 } 614 615 // Move everything down 616 offsetChildrenTopAndBottom(bottomOffset); 617 if (mFirstPosition > 0) { 618 // Fill the gap that was opened above mFirstPosition with more rows, if 619 // possible 620 fillUp(mFirstPosition - (mStackFromBottom ? 1 : numColumns), 621 firstChild.getTop() - verticalSpacing); 622 // Close up the remaining gap 623 adjustViewsUpOrDown(); 624 } 625 } 626 } 627 } 628 629 private void correctTooLow(int numColumns, int verticalSpacing, int childCount) { 630 if (mFirstPosition == 0 && childCount > 0) { 631 // Get the first child ... 632 final View firstChild = getChildAt(0); 633 634 // ... and its top edge 635 final int firstTop = firstChild.getTop(); 636 637 // This is top of our drawable area 638 final int start = mListPadding.top; 639 640 // This is bottom of our drawable area 641 final int end = (mBottom - mTop) - mListPadding.bottom; 642 643 // This is how far the top edge of the first view is from the top of the 644 // drawable area 645 int topOffset = firstTop - start; 646 final View lastChild = getChildAt(childCount - 1); 647 final int lastBottom = lastChild.getBottom(); 648 final int lastPosition = mFirstPosition + childCount - 1; 649 650 // Make sure we are 1) Too low, and 2) Either there are more rows below the 651 // last row or the last row is scrolled off the bottom of the drawable area 652 if (topOffset > 0 && (lastPosition < mItemCount - 1 || lastBottom > end)) { 653 if (lastPosition == mItemCount - 1 ) { 654 // Don't pull the bottom too far up 655 topOffset = Math.min(topOffset, lastBottom - end); 656 } 657 658 // Move everything up 659 offsetChildrenTopAndBottom(-topOffset); 660 if (lastPosition < mItemCount - 1) { 661 // Fill the gap that was opened below the last position with more rows, if 662 // possible 663 fillDown(lastPosition + (!mStackFromBottom ? 1 : numColumns), 664 lastChild.getBottom() + verticalSpacing); 665 // Close up the remaining gap 666 adjustViewsUpOrDown(); 667 } 668 } 669 } 670 } 671 672 /** 673 * Fills the grid based on positioning the new selection at a specific 674 * location. The selection may be moved so that it does not intersect the 675 * faded edges. The grid is then filled upwards and downwards from there. 676 * 677 * @param selectedTop Where the selected item should be 678 * @param childrenTop Where to start drawing children 679 * @param childrenBottom Last pixel where children can be drawn 680 * @return The view that currently has selection 681 */ 682 private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) { 683 final int fadingEdgeLength = getVerticalFadingEdgeLength(); 684 final int selectedPosition = mSelectedPosition; 685 final int numColumns = mNumColumns; 686 final int verticalSpacing = mVerticalSpacing; 687 688 int rowStart; 689 int rowEnd = -1; 690 691 if (!mStackFromBottom) { 692 rowStart = selectedPosition - (selectedPosition % numColumns); 693 } else { 694 int invertedSelection = mItemCount - 1 - selectedPosition; 695 696 rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 697 rowStart = Math.max(0, rowEnd - numColumns + 1); 698 } 699 700 View sel; 701 View referenceView; 702 703 int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); 704 int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, 705 numColumns, rowStart); 706 707 sel = makeRow(mStackFromBottom ? rowEnd : rowStart, selectedTop, true); 708 // Possibly changed again in fillUp if we add rows above this one. 709 mFirstPosition = rowStart; 710 711 referenceView = mReferenceView; 712 adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); 713 adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); 714 715 if (!mStackFromBottom) { 716 fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); 717 adjustViewsUpOrDown(); 718 fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); 719 } else { 720 fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); 721 adjustViewsUpOrDown(); 722 fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); 723 } 724 725 726 return sel; 727 } 728 729 /** 730 * Calculate the bottom-most pixel we can draw the selection into 731 * 732 * @param childrenBottom Bottom pixel were children can be drawn 733 * @param fadingEdgeLength Length of the fading edge in pixels, if present 734 * @param numColumns Number of columns in the grid 735 * @param rowStart The start of the row that will contain the selection 736 * @return The bottom-most pixel we can draw the selection into 737 */ 738 private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength, 739 int numColumns, int rowStart) { 740 // Last pixel we can draw the selection into 741 int bottomSelectionPixel = childrenBottom; 742 if (rowStart + numColumns - 1 < mItemCount - 1) { 743 bottomSelectionPixel -= fadingEdgeLength; 744 } 745 return bottomSelectionPixel; 746 } 747 748 /** 749 * Calculate the top-most pixel we can draw the selection into 750 * 751 * @param childrenTop Top pixel were children can be drawn 752 * @param fadingEdgeLength Length of the fading edge in pixels, if present 753 * @param rowStart The start of the row that will contain the selection 754 * @return The top-most pixel we can draw the selection into 755 */ 756 private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int rowStart) { 757 // first pixel we can draw the selection into 758 int topSelectionPixel = childrenTop; 759 if (rowStart > 0) { 760 topSelectionPixel += fadingEdgeLength; 761 } 762 return topSelectionPixel; 763 } 764 765 /** 766 * Move all views upwards so the selected row does not interesect the bottom 767 * fading edge (if necessary). 768 * 769 * @param childInSelectedRow A child in the row that contains the selection 770 * @param topSelectionPixel The topmost pixel we can draw the selection into 771 * @param bottomSelectionPixel The bottommost pixel we can draw the 772 * selection into 773 */ 774 private void adjustForBottomFadingEdge(View childInSelectedRow, 775 int topSelectionPixel, int bottomSelectionPixel) { 776 // Some of the newly selected item extends below the bottom of the 777 // list 778 if (childInSelectedRow.getBottom() > bottomSelectionPixel) { 779 780 // Find space available above the selection into which we can 781 // scroll upwards 782 int spaceAbove = childInSelectedRow.getTop() - topSelectionPixel; 783 784 // Find space required to bring the bottom of the selected item 785 // fully into view 786 int spaceBelow = childInSelectedRow.getBottom() - bottomSelectionPixel; 787 int offset = Math.min(spaceAbove, spaceBelow); 788 789 // Now offset the selected item to get it into view 790 offsetChildrenTopAndBottom(-offset); 791 } 792 } 793 794 /** 795 * Move all views upwards so the selected row does not interesect the top 796 * fading edge (if necessary). 797 * 798 * @param childInSelectedRow A child in the row that contains the selection 799 * @param topSelectionPixel The topmost pixel we can draw the selection into 800 * @param bottomSelectionPixel The bottommost pixel we can draw the 801 * selection into 802 */ 803 private void adjustForTopFadingEdge(View childInSelectedRow, 804 int topSelectionPixel, int bottomSelectionPixel) { 805 // Some of the newly selected item extends above the top of the list 806 if (childInSelectedRow.getTop() < topSelectionPixel) { 807 // Find space required to bring the top of the selected item 808 // fully into view 809 int spaceAbove = topSelectionPixel - childInSelectedRow.getTop(); 810 811 // Find space available below the selection into which we can 812 // scroll downwards 813 int spaceBelow = bottomSelectionPixel - childInSelectedRow.getBottom(); 814 int offset = Math.min(spaceAbove, spaceBelow); 815 816 // Now offset the selected item to get it into view 817 offsetChildrenTopAndBottom(offset); 818 } 819 } 820 821 /** 822 * Smoothly scroll to the specified adapter position. The view will 823 * scroll such that the indicated position is displayed. 824 * @param position Scroll to this adapter position. 825 */ 826 @android.view.RemotableViewMethod 827 public void smoothScrollToPosition(int position) { 828 super.smoothScrollToPosition(position); 829 } 830 831 /** 832 * Smoothly scroll to the specified adapter position offset. The view will 833 * scroll such that the indicated position is displayed. 834 * @param offset The amount to offset from the adapter position to scroll to. 835 */ 836 @android.view.RemotableViewMethod 837 public void smoothScrollByOffset(int offset) { 838 super.smoothScrollByOffset(offset); 839 } 840 841 /** 842 * Fills the grid based on positioning the new selection relative to the old 843 * selection. The new selection will be placed at, above, or below the 844 * location of the new selection depending on how the selection is moving. 845 * The selection will then be pinned to the visible part of the screen, 846 * excluding the edges that are faded. The grid is then filled upwards and 847 * downwards from there. 848 * 849 * @param delta Which way we are moving 850 * @param childrenTop Where to start drawing children 851 * @param childrenBottom Last pixel where children can be drawn 852 * @return The view that currently has selection 853 */ 854 private View moveSelection(int delta, int childrenTop, int childrenBottom) { 855 final int fadingEdgeLength = getVerticalFadingEdgeLength(); 856 final int selectedPosition = mSelectedPosition; 857 final int numColumns = mNumColumns; 858 final int verticalSpacing = mVerticalSpacing; 859 860 int oldRowStart; 861 int rowStart; 862 int rowEnd = -1; 863 864 if (!mStackFromBottom) { 865 oldRowStart = (selectedPosition - delta) - ((selectedPosition - delta) % numColumns); 866 867 rowStart = selectedPosition - (selectedPosition % numColumns); 868 } else { 869 int invertedSelection = mItemCount - 1 - selectedPosition; 870 871 rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 872 rowStart = Math.max(0, rowEnd - numColumns + 1); 873 874 invertedSelection = mItemCount - 1 - (selectedPosition - delta); 875 oldRowStart = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 876 oldRowStart = Math.max(0, oldRowStart - numColumns + 1); 877 } 878 879 final int rowDelta = rowStart - oldRowStart; 880 881 final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); 882 final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, 883 numColumns, rowStart); 884 885 // Possibly changed again in fillUp if we add rows above this one. 886 mFirstPosition = rowStart; 887 888 View sel; 889 View referenceView; 890 891 if (rowDelta > 0) { 892 /* 893 * Case 1: Scrolling down. 894 */ 895 896 final int oldBottom = mReferenceViewInSelectedRow == null ? 0 : 897 mReferenceViewInSelectedRow.getBottom(); 898 899 sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldBottom + verticalSpacing, true); 900 referenceView = mReferenceView; 901 902 adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); 903 } else if (rowDelta < 0) { 904 /* 905 * Case 2: Scrolling up. 906 */ 907 final int oldTop = mReferenceViewInSelectedRow == null ? 908 0 : mReferenceViewInSelectedRow .getTop(); 909 910 sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop - verticalSpacing, false); 911 referenceView = mReferenceView; 912 913 adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); 914 } else { 915 /* 916 * Keep selection where it was 917 */ 918 final int oldTop = mReferenceViewInSelectedRow == null ? 919 0 : mReferenceViewInSelectedRow .getTop(); 920 921 sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop, true); 922 referenceView = mReferenceView; 923 } 924 925 if (!mStackFromBottom) { 926 fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); 927 adjustViewsUpOrDown(); 928 fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); 929 } else { 930 fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); 931 adjustViewsUpOrDown(); 932 fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); 933 } 934 935 return sel; 936 } 937 938 private boolean determineColumns(int availableSpace) { 939 final int requestedHorizontalSpacing = mRequestedHorizontalSpacing; 940 final int stretchMode = mStretchMode; 941 final int requestedColumnWidth = mRequestedColumnWidth; 942 boolean didNotInitiallyFit = false; 943 944 if (mRequestedNumColumns == AUTO_FIT) { 945 if (requestedColumnWidth > 0) { 946 // Client told us to pick the number of columns 947 mNumColumns = (availableSpace + requestedHorizontalSpacing) / 948 (requestedColumnWidth + requestedHorizontalSpacing); 949 } else { 950 // Just make up a number if we don't have enough info 951 mNumColumns = 2; 952 } 953 } else { 954 // We picked the columns 955 mNumColumns = mRequestedNumColumns; 956 } 957 958 if (mNumColumns <= 0) { 959 mNumColumns = 1; 960 } 961 962 switch (stretchMode) { 963 case NO_STRETCH: 964 // Nobody stretches 965 mColumnWidth = requestedColumnWidth; 966 mHorizontalSpacing = requestedHorizontalSpacing; 967 break; 968 969 default: 970 int spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth) - 971 ((mNumColumns - 1) * requestedHorizontalSpacing); 972 973 if (spaceLeftOver < 0) { 974 didNotInitiallyFit = true; 975 } 976 977 switch (stretchMode) { 978 case STRETCH_COLUMN_WIDTH: 979 // Stretch the columns 980 mColumnWidth = requestedColumnWidth + spaceLeftOver / mNumColumns; 981 mHorizontalSpacing = requestedHorizontalSpacing; 982 break; 983 984 case STRETCH_SPACING: 985 // Stretch the spacing between columns 986 mColumnWidth = requestedColumnWidth; 987 if (mNumColumns > 1) { 988 mHorizontalSpacing = requestedHorizontalSpacing + 989 spaceLeftOver / (mNumColumns - 1); 990 } else { 991 mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver; 992 } 993 break; 994 995 case STRETCH_SPACING_UNIFORM: 996 // Stretch the spacing between columns 997 mColumnWidth = requestedColumnWidth; 998 if (mNumColumns > 1) { 999 mHorizontalSpacing = requestedHorizontalSpacing + 1000 spaceLeftOver / (mNumColumns + 1); 1001 } else { 1002 mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver; 1003 } 1004 break; 1005 } 1006 1007 break; 1008 } 1009 return didNotInitiallyFit; 1010 } 1011 1012 @Override 1013 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1014 // Sets up mListPadding 1015 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1016 1017 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 1018 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 1019 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 1020 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 1021 1022 if (widthMode == MeasureSpec.UNSPECIFIED) { 1023 if (mColumnWidth > 0) { 1024 widthSize = mColumnWidth + mListPadding.left + mListPadding.right; 1025 } else { 1026 widthSize = mListPadding.left + mListPadding.right; 1027 } 1028 widthSize += getVerticalScrollbarWidth(); 1029 } 1030 1031 int childWidth = widthSize - mListPadding.left - mListPadding.right; 1032 boolean didNotInitiallyFit = determineColumns(childWidth); 1033 1034 int childHeight = 0; 1035 int childState = 0; 1036 1037 mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); 1038 final int count = mItemCount; 1039 if (count > 0) { 1040 final View child = obtainView(0, mIsScrap); 1041 1042 AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); 1043 if (p == null) { 1044 p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); 1045 child.setLayoutParams(p); 1046 } 1047 p.viewType = mAdapter.getItemViewType(0); 1048 p.forceAdd = true; 1049 1050 int childHeightSpec = getChildMeasureSpec( 1051 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); 1052 int childWidthSpec = getChildMeasureSpec( 1053 MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); 1054 child.measure(childWidthSpec, childHeightSpec); 1055 1056 childHeight = child.getMeasuredHeight(); 1057 childState = combineMeasuredStates(childState, child.getMeasuredState()); 1058 1059 if (mRecycler.shouldRecycleViewType(p.viewType)) { 1060 mRecycler.addScrapView(child, -1); 1061 } 1062 } 1063 1064 if (heightMode == MeasureSpec.UNSPECIFIED) { 1065 heightSize = mListPadding.top + mListPadding.bottom + childHeight + 1066 getVerticalFadingEdgeLength() * 2; 1067 } 1068 1069 if (heightMode == MeasureSpec.AT_MOST) { 1070 int ourSize = mListPadding.top + mListPadding.bottom; 1071 1072 final int numColumns = mNumColumns; 1073 for (int i = 0; i < count; i += numColumns) { 1074 ourSize += childHeight; 1075 if (i + numColumns < count) { 1076 ourSize += mVerticalSpacing; 1077 } 1078 if (ourSize >= heightSize) { 1079 ourSize = heightSize; 1080 break; 1081 } 1082 } 1083 heightSize = ourSize; 1084 } 1085 1086 if (widthMode == MeasureSpec.AT_MOST && mRequestedNumColumns != AUTO_FIT) { 1087 int ourSize = (mRequestedNumColumns*mColumnWidth) 1088 + ((mRequestedNumColumns-1)*mHorizontalSpacing) 1089 + mListPadding.left + mListPadding.right; 1090 if (ourSize > widthSize || didNotInitiallyFit) { 1091 widthSize |= MEASURED_STATE_TOO_SMALL; 1092 } 1093 } 1094 1095 setMeasuredDimension(widthSize, heightSize); 1096 mWidthMeasureSpec = widthMeasureSpec; 1097 } 1098 1099 @Override 1100 protected void attachLayoutAnimationParameters(View child, 1101 ViewGroup.LayoutParams params, int index, int count) { 1102 1103 GridLayoutAnimationController.AnimationParameters animationParams = 1104 (GridLayoutAnimationController.AnimationParameters) params.layoutAnimationParameters; 1105 1106 if (animationParams == null) { 1107 animationParams = new GridLayoutAnimationController.AnimationParameters(); 1108 params.layoutAnimationParameters = animationParams; 1109 } 1110 1111 animationParams.count = count; 1112 animationParams.index = index; 1113 animationParams.columnsCount = mNumColumns; 1114 animationParams.rowsCount = count / mNumColumns; 1115 1116 if (!mStackFromBottom) { 1117 animationParams.column = index % mNumColumns; 1118 animationParams.row = index / mNumColumns; 1119 } else { 1120 final int invertedIndex = count - 1 - index; 1121 1122 animationParams.column = mNumColumns - 1 - (invertedIndex % mNumColumns); 1123 animationParams.row = animationParams.rowsCount - 1 - invertedIndex / mNumColumns; 1124 } 1125 } 1126 1127 @Override 1128 protected void layoutChildren() { 1129 final boolean blockLayoutRequests = mBlockLayoutRequests; 1130 if (!blockLayoutRequests) { 1131 mBlockLayoutRequests = true; 1132 } 1133 1134 try { 1135 super.layoutChildren(); 1136 1137 invalidate(); 1138 1139 if (mAdapter == null) { 1140 resetList(); 1141 invokeOnItemScrollListener(); 1142 return; 1143 } 1144 1145 final int childrenTop = mListPadding.top; 1146 final int childrenBottom = mBottom - mTop - mListPadding.bottom; 1147 1148 int childCount = getChildCount(); 1149 int index; 1150 int delta = 0; 1151 1152 View sel; 1153 View oldSel = null; 1154 View oldFirst = null; 1155 View newSel = null; 1156 1157 // Remember stuff we will need down below 1158 switch (mLayoutMode) { 1159 case LAYOUT_SET_SELECTION: 1160 index = mNextSelectedPosition - mFirstPosition; 1161 if (index >= 0 && index < childCount) { 1162 newSel = getChildAt(index); 1163 } 1164 break; 1165 case LAYOUT_FORCE_TOP: 1166 case LAYOUT_FORCE_BOTTOM: 1167 case LAYOUT_SPECIFIC: 1168 case LAYOUT_SYNC: 1169 break; 1170 case LAYOUT_MOVE_SELECTION: 1171 if (mNextSelectedPosition >= 0) { 1172 delta = mNextSelectedPosition - mSelectedPosition; 1173 } 1174 break; 1175 default: 1176 // Remember the previously selected view 1177 index = mSelectedPosition - mFirstPosition; 1178 if (index >= 0 && index < childCount) { 1179 oldSel = getChildAt(index); 1180 } 1181 1182 // Remember the previous first child 1183 oldFirst = getChildAt(0); 1184 } 1185 1186 boolean dataChanged = mDataChanged; 1187 if (dataChanged) { 1188 handleDataChanged(); 1189 } 1190 1191 // Handle the empty set by removing all views that are visible 1192 // and calling it a day 1193 if (mItemCount == 0) { 1194 resetList(); 1195 invokeOnItemScrollListener(); 1196 return; 1197 } 1198 1199 setSelectedPositionInt(mNextSelectedPosition); 1200 1201 // Pull all children into the RecycleBin. 1202 // These views will be reused if possible 1203 final int firstPosition = mFirstPosition; 1204 final RecycleBin recycleBin = mRecycler; 1205 1206 if (dataChanged) { 1207 for (int i = 0; i < childCount; i++) { 1208 recycleBin.addScrapView(getChildAt(i), firstPosition+i); 1209 } 1210 } else { 1211 recycleBin.fillActiveViews(childCount, firstPosition); 1212 } 1213 1214 // Clear out old views 1215 //removeAllViewsInLayout(); 1216 detachAllViewsFromParent(); 1217 recycleBin.removeSkippedScrap(); 1218 1219 switch (mLayoutMode) { 1220 case LAYOUT_SET_SELECTION: 1221 if (newSel != null) { 1222 sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); 1223 } else { 1224 sel = fillSelection(childrenTop, childrenBottom); 1225 } 1226 break; 1227 case LAYOUT_FORCE_TOP: 1228 mFirstPosition = 0; 1229 sel = fillFromTop(childrenTop); 1230 adjustViewsUpOrDown(); 1231 break; 1232 case LAYOUT_FORCE_BOTTOM: 1233 sel = fillUp(mItemCount - 1, childrenBottom); 1234 adjustViewsUpOrDown(); 1235 break; 1236 case LAYOUT_SPECIFIC: 1237 sel = fillSpecific(mSelectedPosition, mSpecificTop); 1238 break; 1239 case LAYOUT_SYNC: 1240 sel = fillSpecific(mSyncPosition, mSpecificTop); 1241 break; 1242 case LAYOUT_MOVE_SELECTION: 1243 // Move the selection relative to its old position 1244 sel = moveSelection(delta, childrenTop, childrenBottom); 1245 break; 1246 default: 1247 if (childCount == 0) { 1248 if (!mStackFromBottom) { 1249 setSelectedPositionInt(mAdapter == null || isInTouchMode() ? 1250 INVALID_POSITION : 0); 1251 sel = fillFromTop(childrenTop); 1252 } else { 1253 final int last = mItemCount - 1; 1254 setSelectedPositionInt(mAdapter == null || isInTouchMode() ? 1255 INVALID_POSITION : last); 1256 sel = fillFromBottom(last, childrenBottom); 1257 } 1258 } else { 1259 if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { 1260 sel = fillSpecific(mSelectedPosition, oldSel == null ? 1261 childrenTop : oldSel.getTop()); 1262 } else if (mFirstPosition < mItemCount) { 1263 sel = fillSpecific(mFirstPosition, oldFirst == null ? 1264 childrenTop : oldFirst.getTop()); 1265 } else { 1266 sel = fillSpecific(0, childrenTop); 1267 } 1268 } 1269 break; 1270 } 1271 1272 // Flush any cached views that did not get reused above 1273 recycleBin.scrapActiveViews(); 1274 1275 if (sel != null) { 1276 positionSelector(INVALID_POSITION, sel); 1277 mSelectedTop = sel.getTop(); 1278 } else if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) { 1279 View child = getChildAt(mMotionPosition - mFirstPosition); 1280 if (child != null) positionSelector(mMotionPosition, child); 1281 } else { 1282 mSelectedTop = 0; 1283 mSelectorRect.setEmpty(); 1284 } 1285 1286 mLayoutMode = LAYOUT_NORMAL; 1287 mDataChanged = false; 1288 if (mPositionScrollAfterLayout != null) { 1289 post(mPositionScrollAfterLayout); 1290 mPositionScrollAfterLayout = null; 1291 } 1292 mNeedSync = false; 1293 setNextSelectedPositionInt(mSelectedPosition); 1294 1295 updateScrollIndicators(); 1296 1297 if (mItemCount > 0) { 1298 checkSelectionChanged(); 1299 } 1300 1301 invokeOnItemScrollListener(); 1302 } finally { 1303 if (!blockLayoutRequests) { 1304 mBlockLayoutRequests = false; 1305 } 1306 } 1307 } 1308 1309 1310 /** 1311 * Obtain the view and add it to our list of children. The view can be made 1312 * fresh, converted from an unused view, or used as is if it was in the 1313 * recycle bin. 1314 * 1315 * @param position Logical position in the list 1316 * @param y Top or bottom edge of the view to add 1317 * @param flow if true, align top edge to y. If false, align bottom edge to 1318 * y. 1319 * @param childrenLeft Left edge where children should be positioned 1320 * @param selected Is this position selected? 1321 * @param where to add new item in the list 1322 * @return View that was added 1323 */ 1324 private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, 1325 boolean selected, int where) { 1326 View child; 1327 1328 if (!mDataChanged) { 1329 // Try to use an existing view for this position 1330 child = mRecycler.getActiveView(position); 1331 if (child != null) { 1332 // Found it -- we're using an existing child 1333 // This just needs to be positioned 1334 setupChild(child, position, y, flow, childrenLeft, selected, true, where); 1335 return child; 1336 } 1337 } 1338 1339 // Make a new view for this position, or convert an unused view if 1340 // possible 1341 child = obtainView(position, mIsScrap); 1342 1343 // This needs to be positioned and measured 1344 setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0], where); 1345 1346 return child; 1347 } 1348 1349 /** 1350 * Add a view as a child and make sure it is measured (if necessary) and 1351 * positioned properly. 1352 * 1353 * @param child The view to add 1354 * @param position The position of the view 1355 * @param y The y position relative to which this view will be positioned 1356 * @param flow if true, align top edge to y. If false, align bottom edge 1357 * to y. 1358 * @param childrenLeft Left edge where children should be positioned 1359 * @param selected Is this position selected? 1360 * @param recycled Has this view been pulled from the recycle bin? If so it 1361 * does not need to be remeasured. 1362 * @param where Where to add the item in the list 1363 * 1364 */ 1365 private void setupChild(View child, int position, int y, boolean flow, int childrenLeft, 1366 boolean selected, boolean recycled, int where) { 1367 boolean isSelected = selected && shouldShowSelector(); 1368 final boolean updateChildSelected = isSelected != child.isSelected(); 1369 final int mode = mTouchMode; 1370 final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && 1371 mMotionPosition == position; 1372 final boolean updateChildPressed = isPressed != child.isPressed(); 1373 1374 boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); 1375 1376 // Respect layout params that are already in the view. Otherwise make 1377 // some up... 1378 AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); 1379 if (p == null) { 1380 p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); 1381 } 1382 p.viewType = mAdapter.getItemViewType(position); 1383 1384 if (recycled && !p.forceAdd) { 1385 attachViewToParent(child, where, p); 1386 } else { 1387 p.forceAdd = false; 1388 addViewInLayout(child, where, p, true); 1389 } 1390 1391 if (updateChildSelected) { 1392 child.setSelected(isSelected); 1393 if (isSelected) { 1394 requestFocus(); 1395 } 1396 } 1397 1398 if (updateChildPressed) { 1399 child.setPressed(isPressed); 1400 } 1401 1402 if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { 1403 if (child instanceof Checkable) { 1404 ((Checkable) child).setChecked(mCheckStates.get(position)); 1405 } else if (getContext().getApplicationInfo().targetSdkVersion 1406 >= android.os.Build.VERSION_CODES.HONEYCOMB) { 1407 child.setActivated(mCheckStates.get(position)); 1408 } 1409 } 1410 1411 if (needToMeasure) { 1412 int childHeightSpec = ViewGroup.getChildMeasureSpec( 1413 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); 1414 1415 int childWidthSpec = ViewGroup.getChildMeasureSpec( 1416 MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); 1417 child.measure(childWidthSpec, childHeightSpec); 1418 } else { 1419 cleanupLayoutState(child); 1420 } 1421 1422 final int w = child.getMeasuredWidth(); 1423 final int h = child.getMeasuredHeight(); 1424 1425 int childLeft; 1426 final int childTop = flow ? y : y - h; 1427 1428 final int layoutDirection = getLayoutDirection(); 1429 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); 1430 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 1431 case Gravity.LEFT: 1432 childLeft = childrenLeft; 1433 break; 1434 case Gravity.CENTER_HORIZONTAL: 1435 childLeft = childrenLeft + ((mColumnWidth - w) / 2); 1436 break; 1437 case Gravity.RIGHT: 1438 childLeft = childrenLeft + mColumnWidth - w; 1439 break; 1440 default: 1441 childLeft = childrenLeft; 1442 break; 1443 } 1444 1445 if (needToMeasure) { 1446 final int childRight = childLeft + w; 1447 final int childBottom = childTop + h; 1448 child.layout(childLeft, childTop, childRight, childBottom); 1449 } else { 1450 child.offsetLeftAndRight(childLeft - child.getLeft()); 1451 child.offsetTopAndBottom(childTop - child.getTop()); 1452 } 1453 1454 if (mCachingStarted) { 1455 child.setDrawingCacheEnabled(true); 1456 } 1457 1458 if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition) 1459 != position) { 1460 child.jumpDrawablesToCurrentState(); 1461 } 1462 } 1463 1464 /** 1465 * Sets the currently selected item 1466 * 1467 * @param position Index (starting at 0) of the data item to be selected. 1468 * 1469 * If in touch mode, the item will not be selected but it will still be positioned 1470 * appropriately. 1471 */ 1472 @Override 1473 public void setSelection(int position) { 1474 if (!isInTouchMode()) { 1475 setNextSelectedPositionInt(position); 1476 } else { 1477 mResurrectToPosition = position; 1478 } 1479 mLayoutMode = LAYOUT_SET_SELECTION; 1480 if (mPositionScroller != null) { 1481 mPositionScroller.stop(); 1482 } 1483 requestLayout(); 1484 } 1485 1486 /** 1487 * Makes the item at the supplied position selected. 1488 * 1489 * @param position the position of the new selection 1490 */ 1491 @Override 1492 void setSelectionInt(int position) { 1493 int previousSelectedPosition = mNextSelectedPosition; 1494 1495 if (mPositionScroller != null) { 1496 mPositionScroller.stop(); 1497 } 1498 1499 setNextSelectedPositionInt(position); 1500 layoutChildren(); 1501 1502 final int next = mStackFromBottom ? mItemCount - 1 - mNextSelectedPosition : 1503 mNextSelectedPosition; 1504 final int previous = mStackFromBottom ? mItemCount - 1 1505 - previousSelectedPosition : previousSelectedPosition; 1506 1507 final int nextRow = next / mNumColumns; 1508 final int previousRow = previous / mNumColumns; 1509 1510 if (nextRow != previousRow) { 1511 awakenScrollBars(); 1512 } 1513 1514 } 1515 1516 @Override 1517 public boolean onKeyDown(int keyCode, KeyEvent event) { 1518 return commonKey(keyCode, 1, event); 1519 } 1520 1521 @Override 1522 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { 1523 return commonKey(keyCode, repeatCount, event); 1524 } 1525 1526 @Override 1527 public boolean onKeyUp(int keyCode, KeyEvent event) { 1528 return commonKey(keyCode, 1, event); 1529 } 1530 1531 private boolean commonKey(int keyCode, int count, KeyEvent event) { 1532 if (mAdapter == null) { 1533 return false; 1534 } 1535 1536 if (mDataChanged) { 1537 layoutChildren(); 1538 } 1539 1540 boolean handled = false; 1541 int action = event.getAction(); 1542 1543 if (action != KeyEvent.ACTION_UP) { 1544 switch (keyCode) { 1545 case KeyEvent.KEYCODE_DPAD_LEFT: 1546 if (event.hasNoModifiers()) { 1547 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_LEFT); 1548 } 1549 break; 1550 1551 case KeyEvent.KEYCODE_DPAD_RIGHT: 1552 if (event.hasNoModifiers()) { 1553 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_RIGHT); 1554 } 1555 break; 1556 1557 case KeyEvent.KEYCODE_DPAD_UP: 1558 if (event.hasNoModifiers()) { 1559 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_UP); 1560 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1561 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1562 } 1563 break; 1564 1565 case KeyEvent.KEYCODE_DPAD_DOWN: 1566 if (event.hasNoModifiers()) { 1567 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_DOWN); 1568 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1569 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1570 } 1571 break; 1572 1573 case KeyEvent.KEYCODE_DPAD_CENTER: 1574 case KeyEvent.KEYCODE_ENTER: 1575 if (event.hasNoModifiers()) { 1576 handled = resurrectSelectionIfNeeded(); 1577 if (!handled 1578 && event.getRepeatCount() == 0 && getChildCount() > 0) { 1579 keyPressed(); 1580 handled = true; 1581 } 1582 } 1583 break; 1584 1585 case KeyEvent.KEYCODE_SPACE: 1586 if (mPopup == null || !mPopup.isShowing()) { 1587 if (event.hasNoModifiers()) { 1588 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN); 1589 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { 1590 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP); 1591 } 1592 } 1593 break; 1594 1595 case KeyEvent.KEYCODE_PAGE_UP: 1596 if (event.hasNoModifiers()) { 1597 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP); 1598 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1599 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1600 } 1601 break; 1602 1603 case KeyEvent.KEYCODE_PAGE_DOWN: 1604 if (event.hasNoModifiers()) { 1605 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN); 1606 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1607 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1608 } 1609 break; 1610 1611 case KeyEvent.KEYCODE_MOVE_HOME: 1612 if (event.hasNoModifiers()) { 1613 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1614 } 1615 break; 1616 1617 case KeyEvent.KEYCODE_MOVE_END: 1618 if (event.hasNoModifiers()) { 1619 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1620 } 1621 break; 1622 1623 case KeyEvent.KEYCODE_TAB: 1624 // XXX Sometimes it is useful to be able to TAB through the items in 1625 // a GridView sequentially. Unfortunately this can create an 1626 // asymmetry in TAB navigation order unless the list selection 1627 // always reverts to the top or bottom when receiving TAB focus from 1628 // another widget. Leaving this behavior disabled for now but 1629 // perhaps it should be configurable (and more comprehensive). 1630 if (false) { 1631 if (event.hasNoModifiers()) { 1632 handled = resurrectSelectionIfNeeded() 1633 || sequenceScroll(FOCUS_FORWARD); 1634 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { 1635 handled = resurrectSelectionIfNeeded() 1636 || sequenceScroll(FOCUS_BACKWARD); 1637 } 1638 } 1639 break; 1640 } 1641 } 1642 1643 if (handled) { 1644 return true; 1645 } 1646 1647 if (sendToTextFilter(keyCode, count, event)) { 1648 return true; 1649 } 1650 1651 switch (action) { 1652 case KeyEvent.ACTION_DOWN: 1653 return super.onKeyDown(keyCode, event); 1654 case KeyEvent.ACTION_UP: 1655 return super.onKeyUp(keyCode, event); 1656 case KeyEvent.ACTION_MULTIPLE: 1657 return super.onKeyMultiple(keyCode, count, event); 1658 default: 1659 return false; 1660 } 1661 } 1662 1663 /** 1664 * Scrolls up or down by the number of items currently present on screen. 1665 * 1666 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 1667 * @return whether selection was moved 1668 */ 1669 boolean pageScroll(int direction) { 1670 int nextPage = -1; 1671 1672 if (direction == FOCUS_UP) { 1673 nextPage = Math.max(0, mSelectedPosition - getChildCount()); 1674 } else if (direction == FOCUS_DOWN) { 1675 nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount()); 1676 } 1677 1678 if (nextPage >= 0) { 1679 setSelectionInt(nextPage); 1680 invokeOnItemScrollListener(); 1681 awakenScrollBars(); 1682 return true; 1683 } 1684 1685 return false; 1686 } 1687 1688 /** 1689 * Go to the last or first item if possible. 1690 * 1691 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}. 1692 * 1693 * @return Whether selection was moved. 1694 */ 1695 boolean fullScroll(int direction) { 1696 boolean moved = false; 1697 if (direction == FOCUS_UP) { 1698 mLayoutMode = LAYOUT_SET_SELECTION; 1699 setSelectionInt(0); 1700 invokeOnItemScrollListener(); 1701 moved = true; 1702 } else if (direction == FOCUS_DOWN) { 1703 mLayoutMode = LAYOUT_SET_SELECTION; 1704 setSelectionInt(mItemCount - 1); 1705 invokeOnItemScrollListener(); 1706 moved = true; 1707 } 1708 1709 if (moved) { 1710 awakenScrollBars(); 1711 } 1712 1713 return moved; 1714 } 1715 1716 /** 1717 * Scrolls to the next or previous item, horizontally or vertically. 1718 * 1719 * @param direction either {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, 1720 * {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 1721 * 1722 * @return whether selection was moved 1723 */ 1724 boolean arrowScroll(int direction) { 1725 final int selectedPosition = mSelectedPosition; 1726 final int numColumns = mNumColumns; 1727 1728 int startOfRowPos; 1729 int endOfRowPos; 1730 1731 boolean moved = false; 1732 1733 if (!mStackFromBottom) { 1734 startOfRowPos = (selectedPosition / numColumns) * numColumns; 1735 endOfRowPos = Math.min(startOfRowPos + numColumns - 1, mItemCount - 1); 1736 } else { 1737 final int invertedSelection = mItemCount - 1 - selectedPosition; 1738 endOfRowPos = mItemCount - 1 - (invertedSelection / numColumns) * numColumns; 1739 startOfRowPos = Math.max(0, endOfRowPos - numColumns + 1); 1740 } 1741 1742 switch (direction) { 1743 case FOCUS_UP: 1744 if (startOfRowPos > 0) { 1745 mLayoutMode = LAYOUT_MOVE_SELECTION; 1746 setSelectionInt(Math.max(0, selectedPosition - numColumns)); 1747 moved = true; 1748 } 1749 break; 1750 case FOCUS_DOWN: 1751 if (endOfRowPos < mItemCount - 1) { 1752 mLayoutMode = LAYOUT_MOVE_SELECTION; 1753 setSelectionInt(Math.min(selectedPosition + numColumns, mItemCount - 1)); 1754 moved = true; 1755 } 1756 break; 1757 case FOCUS_LEFT: 1758 if (selectedPosition > startOfRowPos) { 1759 mLayoutMode = LAYOUT_MOVE_SELECTION; 1760 setSelectionInt(Math.max(0, selectedPosition - 1)); 1761 moved = true; 1762 } 1763 break; 1764 case FOCUS_RIGHT: 1765 if (selectedPosition < endOfRowPos) { 1766 mLayoutMode = LAYOUT_MOVE_SELECTION; 1767 setSelectionInt(Math.min(selectedPosition + 1, mItemCount - 1)); 1768 moved = true; 1769 } 1770 break; 1771 } 1772 1773 if (moved) { 1774 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 1775 invokeOnItemScrollListener(); 1776 } 1777 1778 if (moved) { 1779 awakenScrollBars(); 1780 } 1781 1782 return moved; 1783 } 1784 1785 /** 1786 * Goes to the next or previous item according to the order set by the 1787 * adapter. 1788 */ 1789 boolean sequenceScroll(int direction) { 1790 int selectedPosition = mSelectedPosition; 1791 int numColumns = mNumColumns; 1792 int count = mItemCount; 1793 1794 int startOfRow; 1795 int endOfRow; 1796 if (!mStackFromBottom) { 1797 startOfRow = (selectedPosition / numColumns) * numColumns; 1798 endOfRow = Math.min(startOfRow + numColumns - 1, count - 1); 1799 } else { 1800 int invertedSelection = count - 1 - selectedPosition; 1801 endOfRow = count - 1 - (invertedSelection / numColumns) * numColumns; 1802 startOfRow = Math.max(0, endOfRow - numColumns + 1); 1803 } 1804 1805 boolean moved = false; 1806 boolean showScroll = false; 1807 switch (direction) { 1808 case FOCUS_FORWARD: 1809 if (selectedPosition < count - 1) { 1810 // Move to the next item. 1811 mLayoutMode = LAYOUT_MOVE_SELECTION; 1812 setSelectionInt(selectedPosition + 1); 1813 moved = true; 1814 // Show the scrollbar only if changing rows. 1815 showScroll = selectedPosition == endOfRow; 1816 } 1817 break; 1818 1819 case FOCUS_BACKWARD: 1820 if (selectedPosition > 0) { 1821 // Move to the previous item. 1822 mLayoutMode = LAYOUT_MOVE_SELECTION; 1823 setSelectionInt(selectedPosition - 1); 1824 moved = true; 1825 // Show the scrollbar only if changing rows. 1826 showScroll = selectedPosition == startOfRow; 1827 } 1828 break; 1829 } 1830 1831 if (moved) { 1832 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 1833 invokeOnItemScrollListener(); 1834 } 1835 1836 if (showScroll) { 1837 awakenScrollBars(); 1838 } 1839 1840 return moved; 1841 } 1842 1843 @Override 1844 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 1845 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 1846 1847 int closestChildIndex = -1; 1848 if (gainFocus && previouslyFocusedRect != null) { 1849 previouslyFocusedRect.offset(mScrollX, mScrollY); 1850 1851 // figure out which item should be selected based on previously 1852 // focused rect 1853 Rect otherRect = mTempRect; 1854 int minDistance = Integer.MAX_VALUE; 1855 final int childCount = getChildCount(); 1856 for (int i = 0; i < childCount; i++) { 1857 // only consider view's on appropriate edge of grid 1858 if (!isCandidateSelection(i, direction)) { 1859 continue; 1860 } 1861 1862 final View other = getChildAt(i); 1863 other.getDrawingRect(otherRect); 1864 offsetDescendantRectToMyCoords(other, otherRect); 1865 int distance = getDistance(previouslyFocusedRect, otherRect, direction); 1866 1867 if (distance < minDistance) { 1868 minDistance = distance; 1869 closestChildIndex = i; 1870 } 1871 } 1872 } 1873 1874 if (closestChildIndex >= 0) { 1875 setSelection(closestChildIndex + mFirstPosition); 1876 } else { 1877 requestLayout(); 1878 } 1879 } 1880 1881 /** 1882 * Is childIndex a candidate for next focus given the direction the focus 1883 * change is coming from? 1884 * @param childIndex The index to check. 1885 * @param direction The direction, one of 1886 * {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD, FOCUS_BACKWARD} 1887 * @return Whether childIndex is a candidate. 1888 */ 1889 private boolean isCandidateSelection(int childIndex, int direction) { 1890 final int count = getChildCount(); 1891 final int invertedIndex = count - 1 - childIndex; 1892 1893 int rowStart; 1894 int rowEnd; 1895 1896 if (!mStackFromBottom) { 1897 rowStart = childIndex - (childIndex % mNumColumns); 1898 rowEnd = Math.max(rowStart + mNumColumns - 1, count); 1899 } else { 1900 rowEnd = count - 1 - (invertedIndex - (invertedIndex % mNumColumns)); 1901 rowStart = Math.max(0, rowEnd - mNumColumns + 1); 1902 } 1903 1904 switch (direction) { 1905 case View.FOCUS_RIGHT: 1906 // coming from left, selection is only valid if it is on left 1907 // edge 1908 return childIndex == rowStart; 1909 case View.FOCUS_DOWN: 1910 // coming from top; only valid if in top row 1911 return rowStart == 0; 1912 case View.FOCUS_LEFT: 1913 // coming from right, must be on right edge 1914 return childIndex == rowEnd; 1915 case View.FOCUS_UP: 1916 // coming from bottom, need to be in last row 1917 return rowEnd == count - 1; 1918 case View.FOCUS_FORWARD: 1919 // coming from top-left, need to be first in top row 1920 return childIndex == rowStart && rowStart == 0; 1921 case View.FOCUS_BACKWARD: 1922 // coming from bottom-right, need to be last in bottom row 1923 return childIndex == rowEnd && rowEnd == count - 1; 1924 default: 1925 throw new IllegalArgumentException("direction must be one of " 1926 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, " 1927 + "FOCUS_FORWARD, FOCUS_BACKWARD}."); 1928 } 1929 } 1930 1931 /** 1932 * Set the gravity for this grid. Gravity describes how the child views 1933 * are horizontally aligned. Defaults to Gravity.LEFT 1934 * 1935 * @param gravity the gravity to apply to this grid's children 1936 * 1937 * @attr ref android.R.styleable#GridView_gravity 1938 */ 1939 public void setGravity(int gravity) { 1940 if (mGravity != gravity) { 1941 mGravity = gravity; 1942 requestLayoutIfNecessary(); 1943 } 1944 } 1945 1946 /** 1947 * Describes how the child views are horizontally aligned. Defaults to Gravity.LEFT 1948 * 1949 * @return the gravity that will be applied to this grid's children 1950 * 1951 * @attr ref android.R.styleable#GridView_gravity 1952 */ 1953 public int getGravity() { 1954 return mGravity; 1955 } 1956 1957 /** 1958 * Set the amount of horizontal (x) spacing to place between each item 1959 * in the grid. 1960 * 1961 * @param horizontalSpacing The amount of horizontal space between items, 1962 * in pixels. 1963 * 1964 * @attr ref android.R.styleable#GridView_horizontalSpacing 1965 */ 1966 public void setHorizontalSpacing(int horizontalSpacing) { 1967 if (horizontalSpacing != mRequestedHorizontalSpacing) { 1968 mRequestedHorizontalSpacing = horizontalSpacing; 1969 requestLayoutIfNecessary(); 1970 } 1971 } 1972 1973 /** 1974 * Returns the amount of horizontal spacing currently used between each item in the grid. 1975 * 1976 * <p>This is only accurate for the current layout. If {@link #setHorizontalSpacing(int)} 1977 * has been called but layout is not yet complete, this method may return a stale value. 1978 * To get the horizontal spacing that was explicitly requested use 1979 * {@link #getRequestedHorizontalSpacing()}.</p> 1980 * 1981 * @return Current horizontal spacing between each item in pixels 1982 * 1983 * @see #setHorizontalSpacing(int) 1984 * @see #getRequestedHorizontalSpacing() 1985 * 1986 * @attr ref android.R.styleable#GridView_horizontalSpacing 1987 */ 1988 public int getHorizontalSpacing() { 1989 return mHorizontalSpacing; 1990 } 1991 1992 /** 1993 * Returns the requested amount of horizontal spacing between each item in the grid. 1994 * 1995 * <p>The value returned may have been supplied during inflation as part of a style, 1996 * the default GridView style, or by a call to {@link #setHorizontalSpacing(int)}. 1997 * If layout is not yet complete or if GridView calculated a different horizontal spacing 1998 * from what was requested, this may return a different value from 1999 * {@link #getHorizontalSpacing()}.</p> 2000 * 2001 * @return The currently requested horizontal spacing between items, in pixels 2002 * 2003 * @see #setHorizontalSpacing(int) 2004 * @see #getHorizontalSpacing() 2005 * 2006 * @attr ref android.R.styleable#GridView_horizontalSpacing 2007 */ 2008 public int getRequestedHorizontalSpacing() { 2009 return mRequestedHorizontalSpacing; 2010 } 2011 2012 /** 2013 * Set the amount of vertical (y) spacing to place between each item 2014 * in the grid. 2015 * 2016 * @param verticalSpacing The amount of vertical space between items, 2017 * in pixels. 2018 * 2019 * @see #getVerticalSpacing() 2020 * 2021 * @attr ref android.R.styleable#GridView_verticalSpacing 2022 */ 2023 public void setVerticalSpacing(int verticalSpacing) { 2024 if (verticalSpacing != mVerticalSpacing) { 2025 mVerticalSpacing = verticalSpacing; 2026 requestLayoutIfNecessary(); 2027 } 2028 } 2029 2030 /** 2031 * Returns the amount of vertical spacing between each item in the grid. 2032 * 2033 * @return The vertical spacing between items in pixels 2034 * 2035 * @see #setVerticalSpacing(int) 2036 * 2037 * @attr ref android.R.styleable#GridView_verticalSpacing 2038 */ 2039 public int getVerticalSpacing() { 2040 return mVerticalSpacing; 2041 } 2042 2043 /** 2044 * Control how items are stretched to fill their space. 2045 * 2046 * @param stretchMode Either {@link #NO_STRETCH}, 2047 * {@link #STRETCH_SPACING}, {@link #STRETCH_SPACING_UNIFORM}, or {@link #STRETCH_COLUMN_WIDTH}. 2048 * 2049 * @attr ref android.R.styleable#GridView_stretchMode 2050 */ 2051 public void setStretchMode(int stretchMode) { 2052 if (stretchMode != mStretchMode) { 2053 mStretchMode = stretchMode; 2054 requestLayoutIfNecessary(); 2055 } 2056 } 2057 2058 public int getStretchMode() { 2059 return mStretchMode; 2060 } 2061 2062 /** 2063 * Set the width of columns in the grid. 2064 * 2065 * @param columnWidth The column width, in pixels. 2066 * 2067 * @attr ref android.R.styleable#GridView_columnWidth 2068 */ 2069 public void setColumnWidth(int columnWidth) { 2070 if (columnWidth != mRequestedColumnWidth) { 2071 mRequestedColumnWidth = columnWidth; 2072 requestLayoutIfNecessary(); 2073 } 2074 } 2075 2076 /** 2077 * Return the width of a column in the grid. 2078 * 2079 * <p>This may not be valid yet if a layout is pending.</p> 2080 * 2081 * @return The column width in pixels 2082 * 2083 * @see #setColumnWidth(int) 2084 * @see #getRequestedColumnWidth() 2085 * 2086 * @attr ref android.R.styleable#GridView_columnWidth 2087 */ 2088 public int getColumnWidth() { 2089 return mColumnWidth; 2090 } 2091 2092 /** 2093 * Return the requested width of a column in the grid. 2094 * 2095 * <p>This may not be the actual column width used. Use {@link #getColumnWidth()} 2096 * to retrieve the current real width of a column.</p> 2097 * 2098 * @return The requested column width in pixels 2099 * 2100 * @see #setColumnWidth(int) 2101 * @see #getColumnWidth() 2102 * 2103 * @attr ref android.R.styleable#GridView_columnWidth 2104 */ 2105 public int getRequestedColumnWidth() { 2106 return mRequestedColumnWidth; 2107 } 2108 2109 /** 2110 * Set the number of columns in the grid 2111 * 2112 * @param numColumns The desired number of columns. 2113 * 2114 * @attr ref android.R.styleable#GridView_numColumns 2115 */ 2116 public void setNumColumns(int numColumns) { 2117 if (numColumns != mRequestedNumColumns) { 2118 mRequestedNumColumns = numColumns; 2119 requestLayoutIfNecessary(); 2120 } 2121 } 2122 2123 /** 2124 * Get the number of columns in the grid. 2125 * Returns {@link #AUTO_FIT} if the Grid has never been laid out. 2126 * 2127 * @attr ref android.R.styleable#GridView_numColumns 2128 * 2129 * @see #setNumColumns(int) 2130 */ 2131 @ViewDebug.ExportedProperty 2132 public int getNumColumns() { 2133 return mNumColumns; 2134 } 2135 2136 /** 2137 * Make sure views are touching the top or bottom edge, as appropriate for 2138 * our gravity 2139 */ 2140 private void adjustViewsUpOrDown() { 2141 final int childCount = getChildCount(); 2142 2143 if (childCount > 0) { 2144 int delta; 2145 View child; 2146 2147 if (!mStackFromBottom) { 2148 // Uh-oh -- we came up short. Slide all views up to make them 2149 // align with the top 2150 child = getChildAt(0); 2151 delta = child.getTop() - mListPadding.top; 2152 if (mFirstPosition != 0) { 2153 // It's OK to have some space above the first item if it is 2154 // part of the vertical spacing 2155 delta -= mVerticalSpacing; 2156 } 2157 if (delta < 0) { 2158 // We only are looking to see if we are too low, not too high 2159 delta = 0; 2160 } 2161 } else { 2162 // we are too high, slide all views down to align with bottom 2163 child = getChildAt(childCount - 1); 2164 delta = child.getBottom() - (getHeight() - mListPadding.bottom); 2165 2166 if (mFirstPosition + childCount < mItemCount) { 2167 // It's OK to have some space below the last item if it is 2168 // part of the vertical spacing 2169 delta += mVerticalSpacing; 2170 } 2171 2172 if (delta > 0) { 2173 // We only are looking to see if we are too high, not too low 2174 delta = 0; 2175 } 2176 } 2177 2178 if (delta != 0) { 2179 offsetChildrenTopAndBottom(-delta); 2180 } 2181 } 2182 } 2183 2184 @Override 2185 protected int computeVerticalScrollExtent() { 2186 final int count = getChildCount(); 2187 if (count > 0) { 2188 final int numColumns = mNumColumns; 2189 final int rowCount = (count + numColumns - 1) / numColumns; 2190 2191 int extent = rowCount * 100; 2192 2193 View view = getChildAt(0); 2194 final int top = view.getTop(); 2195 int height = view.getHeight(); 2196 if (height > 0) { 2197 extent += (top * 100) / height; 2198 } 2199 2200 view = getChildAt(count - 1); 2201 final int bottom = view.getBottom(); 2202 height = view.getHeight(); 2203 if (height > 0) { 2204 extent -= ((bottom - getHeight()) * 100) / height; 2205 } 2206 2207 return extent; 2208 } 2209 return 0; 2210 } 2211 2212 @Override 2213 protected int computeVerticalScrollOffset() { 2214 if (mFirstPosition >= 0 && getChildCount() > 0) { 2215 final View view = getChildAt(0); 2216 final int top = view.getTop(); 2217 int height = view.getHeight(); 2218 if (height > 0) { 2219 final int numColumns = mNumColumns; 2220 final int rowCount = (mItemCount + numColumns - 1) / numColumns; 2221 // In case of stackFromBottom the calculation of whichRow needs 2222 // to take into account that counting from the top the first row 2223 // might not be entirely filled. 2224 final int oddItemsOnFirstRow = isStackFromBottom() ? ((rowCount * numColumns) - 2225 mItemCount) : 0; 2226 final int whichRow = (mFirstPosition + oddItemsOnFirstRow) / numColumns; 2227 return Math.max(whichRow * 100 - (top * 100) / height + 2228 (int) ((float) mScrollY / getHeight() * rowCount * 100), 0); 2229 } 2230 } 2231 return 0; 2232 } 2233 2234 @Override 2235 protected int computeVerticalScrollRange() { 2236 // TODO: Account for vertical spacing too 2237 final int numColumns = mNumColumns; 2238 final int rowCount = (mItemCount + numColumns - 1) / numColumns; 2239 int result = Math.max(rowCount * 100, 0); 2240 if (mScrollY != 0) { 2241 // Compensate for overscroll 2242 result += Math.abs((int) ((float) mScrollY / getHeight() * rowCount * 100)); 2243 } 2244 return result; 2245 } 2246 2247 @Override 2248 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 2249 super.onInitializeAccessibilityEvent(event); 2250 event.setClassName(GridView.class.getName()); 2251 } 2252 2253 @Override 2254 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 2255 super.onInitializeAccessibilityNodeInfo(info); 2256 info.setClassName(GridView.class.getName()); 2257 } 2258 } 2259