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