GridView.java revision f2a204e792df5593cfe54efc95d04b7e764795c1
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.res.TypedArray; 21import android.graphics.Rect; 22import android.util.AttributeSet; 23import android.view.Gravity; 24import android.view.KeyEvent; 25import android.view.View; 26import android.view.ViewGroup; 27import android.view.SoundEffectConstants; 28import 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 */ 35public 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 60 public GridView(Context context) { 61 super(context); 62 } 63 64 public GridView(Context context, AttributeSet attrs) { 65 this(context, attrs, com.android.internal.R.attr.gridViewStyle); 66 } 67 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 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 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 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 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 */ 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 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 */ 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 */ 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 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 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 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 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 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 */ 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 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, mIsScrap); 935 936 AbsListView.LayoutParams p = (AbsListView.LayoutParams)child.getLayoutParams(); 937 if (p == null) { 938 p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_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 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 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 */ 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 existing 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, mIsScrap); 1219 1220 // This needs to be positioned and measured 1221 setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0], 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 */ 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.MATCH_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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 1737 public void setStretchMode(int stretchMode) { 1738 if (stretchMode != mStretchMode) { 1739 mStretchMode = stretchMode; 1740 requestLayoutIfNecessary(); 1741 } 1742 } 1743 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 */ 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 */ 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 */ 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 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 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 numColumns = mNumColumns; 1860 final int whichRow = mFirstPosition / numColumns; 1861 final int rowCount = (mItemCount + numColumns - 1) / numColumns; 1862 return Math.max(whichRow * 100 - (top * 100) / height + 1863 (int) ((float) mScrollY / getHeight() * rowCount * 100), 0); 1864 } 1865 } 1866 return 0; 1867 } 1868 1869 @Override 1870 protected int computeVerticalScrollRange() { 1871 // TODO: Account for vertical spacing too 1872 final int numColumns = mNumColumns; 1873 final int rowCount = (mItemCount + numColumns - 1) / numColumns; 1874 int result = Math.max(rowCount * 100, 0); 1875 if (mScrollY != 0) { 1876 // Compensate for overscroll 1877 result += Math.abs((int) ((float) mScrollY / getHeight() * rowCount * 100)); 1878 } 1879 return result; 1880 } 1881} 1882 1883