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 GridView(Context context)101 public GridView(Context context) { 102 this(context, null); 103 } 104 GridView(Context context, AttributeSet attrs)105 public GridView(Context context, AttributeSet attrs) { 106 this(context, attrs, com.android.internal.R.attr.gridViewStyle); 107 } 108 GridView(Context context, AttributeSet attrs, int defStyle)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 getAdapter()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 setRemoteViewsAdapter(Intent intent)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 setAdapter(ListAdapter adapter)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 lookForSelectablePosition(int position, boolean lookDown)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 fillGap(boolean down)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 */ fillDown(int pos, int nextTop)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 makeRow(int startPos, int y, boolean flow)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 */ fillUp(int pos, int nextBottom)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 */ fillFromTop(int nextTop)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 fillFromBottom(int lastPosition, int nextBottom)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 fillSelection(int childrenTop, int childrenBottom)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 pinToTop(int childrenTop)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 pinToBottom(int childrenBottom)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 findMotionRow(int y)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 */ fillSpecific(int position, int top)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 correctTooHigh(int numColumns, int verticalSpacing, int childCount)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 correctTooLow(int numColumns, int verticalSpacing, int childCount)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 */ fillFromSelection(int selectedTop, int childrenTop, int childrenBottom)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 */ getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength, int numColumns, int rowStart)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 */ getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int rowStart)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 */ adjustForBottomFadingEdge(View childInSelectedRow, int topSelectionPixel, int bottomSelectionPixel)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 */ adjustForTopFadingEdge(View childInSelectedRow, int topSelectionPixel, int bottomSelectionPixel)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 smoothScrollToPosition(int position)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 smoothScrollByOffset(int offset)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 */ moveSelection(int delta, int childrenTop, int childrenBottom)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 determineColumns(int availableSpace)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 onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 attachLayoutAnimationParameters(View child, ViewGroup.LayoutParams params, int index, int count)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 layoutChildren()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 */ makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected, int where)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 */ setupChild(View child, int position, int y, boolean flow, int childrenLeft, boolean selected, boolean recycled, int where)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 setSelection(int position)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 setSelectionInt(int position)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 onKeyDown(int keyCode, KeyEvent event)1517 public boolean onKeyDown(int keyCode, KeyEvent event) { 1518 return commonKey(keyCode, 1, event); 1519 } 1520 1521 @Override onKeyMultiple(int keyCode, int repeatCount, KeyEvent event)1522 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { 1523 return commonKey(keyCode, repeatCount, event); 1524 } 1525 1526 @Override onKeyUp(int keyCode, KeyEvent event)1527 public boolean onKeyUp(int keyCode, KeyEvent event) { 1528 return commonKey(keyCode, 1, event); 1529 } 1530 commonKey(int keyCode, int count, KeyEvent event)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 */ pageScroll(int direction)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 */ fullScroll(int direction)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 */ arrowScroll(int direction)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 */ sequenceScroll(int direction)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 onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)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 */ isCandidateSelection(int childIndex, int direction)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 */ setGravity(int gravity)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 */ getGravity()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 */ setHorizontalSpacing(int horizontalSpacing)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 */ getHorizontalSpacing()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 */ getRequestedHorizontalSpacing()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 */ setVerticalSpacing(int verticalSpacing)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 */ getVerticalSpacing()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 */ setStretchMode(int stretchMode)2051 public void setStretchMode(int stretchMode) { 2052 if (stretchMode != mStretchMode) { 2053 mStretchMode = stretchMode; 2054 requestLayoutIfNecessary(); 2055 } 2056 } 2057 getStretchMode()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 */ setColumnWidth(int columnWidth)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 */ getColumnWidth()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 */ getRequestedColumnWidth()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 */ setNumColumns(int numColumns)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 getNumColumns()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 */ adjustViewsUpOrDown()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 computeVerticalScrollExtent()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 computeVerticalScrollOffset()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 computeVerticalScrollRange()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 onInitializeAccessibilityEvent(AccessibilityEvent event)2248 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 2249 super.onInitializeAccessibilityEvent(event); 2250 event.setClassName(GridView.class.getName()); 2251 } 2252 2253 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)2254 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 2255 super.onInitializeAccessibilityNodeInfo(info); 2256 info.setClassName(GridView.class.getName()); 2257 } 2258 } 2259