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