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