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