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