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