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