1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.widget; 18 19 import android.annotation.IntDef; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.res.TypedArray; 23 import android.graphics.Rect; 24 import android.os.Trace; 25 import android.util.AttributeSet; 26 import android.util.MathUtils; 27 import android.view.Gravity; 28 import android.view.KeyEvent; 29 import android.view.SoundEffectConstants; 30 import android.view.View; 31 import android.view.ViewDebug; 32 import android.view.ViewGroup; 33 import android.view.ViewRootImpl; 34 import android.view.accessibility.AccessibilityEvent; 35 import android.view.accessibility.AccessibilityNodeInfo; 36 import android.view.accessibility.AccessibilityNodeProvider; 37 import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; 38 import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo; 39 import android.view.animation.GridLayoutAnimationController; 40 import android.widget.RemoteViews.RemoteView; 41 42 import java.lang.annotation.Retention; 43 import java.lang.annotation.RetentionPolicy; 44 45 46 /** 47 * A view that shows items in two-dimensional scrolling grid. The items in the 48 * grid come from the {@link ListAdapter} associated with this view. 49 * 50 * <p>See the <a href="{@docRoot}guide/topics/ui/layout/gridview.html">Grid 51 * View</a> guide.</p> 52 * 53 * @attr ref android.R.styleable#GridView_horizontalSpacing 54 * @attr ref android.R.styleable#GridView_verticalSpacing 55 * @attr ref android.R.styleable#GridView_stretchMode 56 * @attr ref android.R.styleable#GridView_columnWidth 57 * @attr ref android.R.styleable#GridView_numColumns 58 * @attr ref android.R.styleable#GridView_gravity 59 */ 60 @RemoteView 61 public class GridView extends AbsListView { 62 /** @hide */ 63 @IntDef({NO_STRETCH, STRETCH_SPACING, STRETCH_COLUMN_WIDTH, STRETCH_SPACING_UNIFORM}) 64 @Retention(RetentionPolicy.SOURCE) 65 public @interface StretchMode {} 66 67 /** 68 * Disables stretching. 69 * 70 * @see #setStretchMode(int) 71 */ 72 public static final int NO_STRETCH = 0; 73 /** 74 * Stretches the spacing between columns. 75 * 76 * @see #setStretchMode(int) 77 */ 78 public static final int STRETCH_SPACING = 1; 79 /** 80 * Stretches columns. 81 * 82 * @see #setStretchMode(int) 83 */ 84 public static final int STRETCH_COLUMN_WIDTH = 2; 85 /** 86 * Stretches the spacing between columns. The spacing is uniform. 87 * 88 * @see #setStretchMode(int) 89 */ 90 public static final int STRETCH_SPACING_UNIFORM = 3; 91 92 /** 93 * Creates as many columns as can fit on screen. 94 * 95 * @see #setNumColumns(int) 96 */ 97 public static final int AUTO_FIT = -1; 98 99 private int mNumColumns = AUTO_FIT; 100 101 private int mHorizontalSpacing = 0; 102 private int mRequestedHorizontalSpacing; 103 private int mVerticalSpacing = 0; 104 private int mStretchMode = STRETCH_COLUMN_WIDTH; 105 private int mColumnWidth; 106 private int mRequestedColumnWidth; 107 private int mRequestedNumColumns; 108 109 private View mReferenceView = null; 110 private View mReferenceViewInSelectedRow = null; 111 112 private int mGravity = Gravity.START; 113 114 private final Rect mTempRect = new Rect(); 115 116 public GridView(Context context) { 117 this(context, null); 118 } 119 120 public GridView(Context context, AttributeSet attrs) { 121 this(context, attrs, com.android.internal.R.attr.gridViewStyle); 122 } 123 124 public GridView(Context context, AttributeSet attrs, int defStyleAttr) { 125 this(context, attrs, defStyleAttr, 0); 126 } 127 128 public GridView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 129 super(context, attrs, defStyleAttr, defStyleRes); 130 131 final TypedArray a = context.obtainStyledAttributes( 132 attrs, com.android.internal.R.styleable.GridView, defStyleAttr, defStyleRes); 133 134 int hSpacing = a.getDimensionPixelOffset( 135 com.android.internal.R.styleable.GridView_horizontalSpacing, 0); 136 setHorizontalSpacing(hSpacing); 137 138 int vSpacing = a.getDimensionPixelOffset( 139 com.android.internal.R.styleable.GridView_verticalSpacing, 0); 140 setVerticalSpacing(vSpacing); 141 142 int index = a.getInt(com.android.internal.R.styleable.GridView_stretchMode, STRETCH_COLUMN_WIDTH); 143 if (index >= 0) { 144 setStretchMode(index); 145 } 146 147 int columnWidth = a.getDimensionPixelOffset(com.android.internal.R.styleable.GridView_columnWidth, -1); 148 if (columnWidth > 0) { 149 setColumnWidth(columnWidth); 150 } 151 152 int numColumns = a.getInt(com.android.internal.R.styleable.GridView_numColumns, 1); 153 setNumColumns(numColumns); 154 155 index = a.getInt(com.android.internal.R.styleable.GridView_gravity, -1); 156 if (index >= 0) { 157 setGravity(index); 158 } 159 160 a.recycle(); 161 } 162 163 @Override 164 public ListAdapter getAdapter() { 165 return mAdapter; 166 } 167 168 /** 169 * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService 170 * through the specified intent. 171 * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to. 172 */ 173 @android.view.RemotableViewMethod 174 public void setRemoteViewsAdapter(Intent intent) { 175 super.setRemoteViewsAdapter(intent); 176 } 177 178 /** 179 * Sets the data behind this GridView. 180 * 181 * @param adapter the adapter providing the grid's data 182 */ 183 @Override 184 public void setAdapter(ListAdapter adapter) { 185 if (mAdapter != null && mDataSetObserver != null) { 186 mAdapter.unregisterDataSetObserver(mDataSetObserver); 187 } 188 189 resetList(); 190 mRecycler.clear(); 191 mAdapter = adapter; 192 193 mOldSelectedPosition = INVALID_POSITION; 194 mOldSelectedRowId = INVALID_ROW_ID; 195 196 // AbsListView#setAdapter will update choice mode states. 197 super.setAdapter(adapter); 198 199 if (mAdapter != null) { 200 mOldItemCount = mItemCount; 201 mItemCount = mAdapter.getCount(); 202 mDataChanged = true; 203 checkFocus(); 204 205 mDataSetObserver = new AdapterDataSetObserver(); 206 mAdapter.registerDataSetObserver(mDataSetObserver); 207 208 mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); 209 210 int position; 211 if (mStackFromBottom) { 212 position = lookForSelectablePosition(mItemCount - 1, false); 213 } else { 214 position = lookForSelectablePosition(0, true); 215 } 216 setSelectedPositionInt(position); 217 setNextSelectedPositionInt(position); 218 checkSelectionChanged(); 219 } else { 220 checkFocus(); 221 // Nothing selected 222 checkSelectionChanged(); 223 } 224 225 requestLayout(); 226 } 227 228 @Override 229 int lookForSelectablePosition(int position, boolean lookDown) { 230 final ListAdapter adapter = mAdapter; 231 if (adapter == null || isInTouchMode()) { 232 return INVALID_POSITION; 233 } 234 235 if (position < 0 || position >= mItemCount) { 236 return INVALID_POSITION; 237 } 238 return position; 239 } 240 241 /** 242 * {@inheritDoc} 243 */ 244 @Override 245 void fillGap(boolean down) { 246 final int numColumns = mNumColumns; 247 final int verticalSpacing = mVerticalSpacing; 248 249 final int count = getChildCount(); 250 251 if (down) { 252 int paddingTop = 0; 253 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 254 paddingTop = getListPaddingTop(); 255 } 256 final int startOffset = count > 0 ? 257 getChildAt(count - 1).getBottom() + verticalSpacing : paddingTop; 258 int position = mFirstPosition + count; 259 if (mStackFromBottom) { 260 position += numColumns - 1; 261 } 262 fillDown(position, startOffset); 263 correctTooHigh(numColumns, verticalSpacing, getChildCount()); 264 } else { 265 int paddingBottom = 0; 266 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 267 paddingBottom = getListPaddingBottom(); 268 } 269 final int startOffset = count > 0 ? 270 getChildAt(0).getTop() - verticalSpacing : getHeight() - paddingBottom; 271 int position = mFirstPosition; 272 if (!mStackFromBottom) { 273 position -= numColumns; 274 } else { 275 position--; 276 } 277 fillUp(position, startOffset); 278 correctTooLow(numColumns, verticalSpacing, getChildCount()); 279 } 280 } 281 282 /** 283 * Fills the list from pos down to the end of the list view. 284 * 285 * @param pos The first position to put in the list 286 * 287 * @param nextTop The location where the top of the item associated with pos 288 * should be drawn 289 * 290 * @return The view that is currently selected, if it happens to be in the 291 * range that we draw. 292 */ 293 private View fillDown(int pos, int nextTop) { 294 View selectedView = null; 295 296 int end = (mBottom - mTop); 297 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 298 end -= mListPadding.bottom; 299 } 300 301 while (nextTop < end && pos < mItemCount) { 302 View temp = makeRow(pos, nextTop, true); 303 if (temp != null) { 304 selectedView = temp; 305 } 306 307 // mReferenceView will change with each call to makeRow() 308 // do not cache in a local variable outside of this loop 309 nextTop = mReferenceView.getBottom() + mVerticalSpacing; 310 311 pos += mNumColumns; 312 } 313 314 setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); 315 return selectedView; 316 } 317 318 private View makeRow(int startPos, int y, boolean flow) { 319 final int columnWidth = mColumnWidth; 320 final int horizontalSpacing = mHorizontalSpacing; 321 322 final boolean isLayoutRtl = isLayoutRtl(); 323 324 int last; 325 int nextLeft; 326 327 if (isLayoutRtl) { 328 nextLeft = getWidth() - mListPadding.right - columnWidth - 329 ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0); 330 } else { 331 nextLeft = mListPadding.left + 332 ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0); 333 } 334 335 if (!mStackFromBottom) { 336 last = Math.min(startPos + mNumColumns, mItemCount); 337 } else { 338 last = startPos + 1; 339 startPos = Math.max(0, startPos - mNumColumns + 1); 340 341 if (last - startPos < mNumColumns) { 342 final int deltaLeft = (mNumColumns - (last - startPos)) * (columnWidth + horizontalSpacing); 343 nextLeft += (isLayoutRtl ? -1 : +1) * deltaLeft; 344 } 345 } 346 347 View selectedView = null; 348 349 final boolean hasFocus = shouldShowSelector(); 350 final boolean inClick = touchModeDrawsInPressedState(); 351 final int selectedPosition = mSelectedPosition; 352 353 View child = null; 354 final int nextChildDir = isLayoutRtl ? -1 : +1; 355 for (int pos = startPos; pos < last; pos++) { 356 // is this the selected item? 357 boolean selected = pos == selectedPosition; 358 // does the list view have focus or contain focus 359 360 final int where = flow ? -1 : pos - startPos; 361 child = makeAndAddView(pos, y, flow, nextLeft, selected, where); 362 363 nextLeft += nextChildDir * columnWidth; 364 if (pos < last - 1) { 365 nextLeft += nextChildDir * horizontalSpacing; 366 } 367 368 if (selected && (hasFocus || inClick)) { 369 selectedView = child; 370 } 371 } 372 373 mReferenceView = child; 374 375 if (selectedView != null) { 376 mReferenceViewInSelectedRow = mReferenceView; 377 } 378 379 return selectedView; 380 } 381 382 /** 383 * Fills the list from pos up to the top of the list view. 384 * 385 * @param pos The first position to put in the list 386 * 387 * @param nextBottom The location where the bottom of the item associated 388 * with pos should be drawn 389 * 390 * @return The view that is currently selected 391 */ 392 private View fillUp(int pos, int nextBottom) { 393 View selectedView = null; 394 395 int end = 0; 396 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 397 end = mListPadding.top; 398 } 399 400 while (nextBottom > end && pos >= 0) { 401 402 View temp = makeRow(pos, nextBottom, false); 403 if (temp != null) { 404 selectedView = temp; 405 } 406 407 nextBottom = mReferenceView.getTop() - mVerticalSpacing; 408 409 mFirstPosition = pos; 410 411 pos -= mNumColumns; 412 } 413 414 if (mStackFromBottom) { 415 mFirstPosition = Math.max(0, pos + 1); 416 } 417 418 setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); 419 return selectedView; 420 } 421 422 /** 423 * Fills the list from top to bottom, starting with mFirstPosition 424 * 425 * @param nextTop The location where the top of the first item should be 426 * drawn 427 * 428 * @return The view that is currently selected 429 */ 430 private View fillFromTop(int nextTop) { 431 mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); 432 mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); 433 if (mFirstPosition < 0) { 434 mFirstPosition = 0; 435 } 436 mFirstPosition -= mFirstPosition % mNumColumns; 437 return fillDown(mFirstPosition, nextTop); 438 } 439 440 private View fillFromBottom(int lastPosition, int nextBottom) { 441 lastPosition = Math.max(lastPosition, mSelectedPosition); 442 lastPosition = Math.min(lastPosition, mItemCount - 1); 443 444 final int invertedPosition = mItemCount - 1 - lastPosition; 445 lastPosition = mItemCount - 1 - (invertedPosition - (invertedPosition % mNumColumns)); 446 447 return fillUp(lastPosition, nextBottom); 448 } 449 450 private View fillSelection(int childrenTop, int childrenBottom) { 451 final int selectedPosition = reconcileSelectedPosition(); 452 final int numColumns = mNumColumns; 453 final int verticalSpacing = mVerticalSpacing; 454 455 int rowStart; 456 int rowEnd = -1; 457 458 if (!mStackFromBottom) { 459 rowStart = selectedPosition - (selectedPosition % numColumns); 460 } else { 461 final int invertedSelection = mItemCount - 1 - selectedPosition; 462 463 rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 464 rowStart = Math.max(0, rowEnd - numColumns + 1); 465 } 466 467 final int fadingEdgeLength = getVerticalFadingEdgeLength(); 468 final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); 469 470 final View sel = makeRow(mStackFromBottom ? rowEnd : rowStart, topSelectionPixel, true); 471 mFirstPosition = rowStart; 472 473 final View referenceView = mReferenceView; 474 475 if (!mStackFromBottom) { 476 fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); 477 pinToBottom(childrenBottom); 478 fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); 479 adjustViewsUpOrDown(); 480 } else { 481 final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, 482 fadingEdgeLength, numColumns, rowStart); 483 final int offset = bottomSelectionPixel - referenceView.getBottom(); 484 offsetChildrenTopAndBottom(offset); 485 fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); 486 pinToTop(childrenTop); 487 fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); 488 adjustViewsUpOrDown(); 489 } 490 491 return sel; 492 } 493 494 private void pinToTop(int childrenTop) { 495 if (mFirstPosition == 0) { 496 final int top = getChildAt(0).getTop(); 497 final int offset = childrenTop - top; 498 if (offset < 0) { 499 offsetChildrenTopAndBottom(offset); 500 } 501 } 502 } 503 504 private void pinToBottom(int childrenBottom) { 505 final int count = getChildCount(); 506 if (mFirstPosition + count == mItemCount) { 507 final int bottom = getChildAt(count - 1).getBottom(); 508 final int offset = childrenBottom - bottom; 509 if (offset > 0) { 510 offsetChildrenTopAndBottom(offset); 511 } 512 } 513 } 514 515 @Override 516 int findMotionRow(int y) { 517 final int childCount = getChildCount(); 518 if (childCount > 0) { 519 520 final int numColumns = mNumColumns; 521 if (!mStackFromBottom) { 522 for (int i = 0; i < childCount; i += numColumns) { 523 if (y <= getChildAt(i).getBottom()) { 524 return mFirstPosition + i; 525 } 526 } 527 } else { 528 for (int i = childCount - 1; i >= 0; i -= numColumns) { 529 if (y >= getChildAt(i).getTop()) { 530 return mFirstPosition + i; 531 } 532 } 533 } 534 } 535 return INVALID_POSITION; 536 } 537 538 /** 539 * Layout during a scroll that results from tracking motion events. Places 540 * the mMotionPosition view at the offset specified by mMotionViewTop, and 541 * then build surrounding views from there. 542 * 543 * @param position the position at which to start filling 544 * @param top the top of the view at that position 545 * @return The selected view, or null if the selected view is outside the 546 * visible area. 547 */ 548 private View fillSpecific(int position, int top) { 549 final int numColumns = mNumColumns; 550 551 int motionRowStart; 552 int motionRowEnd = -1; 553 554 if (!mStackFromBottom) { 555 motionRowStart = position - (position % numColumns); 556 } else { 557 final int invertedSelection = mItemCount - 1 - position; 558 559 motionRowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 560 motionRowStart = Math.max(0, motionRowEnd - numColumns + 1); 561 } 562 563 final View temp = makeRow(mStackFromBottom ? motionRowEnd : motionRowStart, top, true); 564 565 // Possibly changed again in fillUp if we add rows above this one. 566 mFirstPosition = motionRowStart; 567 568 final View referenceView = mReferenceView; 569 // We didn't have anything to layout, bail out 570 if (referenceView == null) { 571 return null; 572 } 573 574 final int verticalSpacing = mVerticalSpacing; 575 576 View above; 577 View below; 578 579 if (!mStackFromBottom) { 580 above = fillUp(motionRowStart - numColumns, referenceView.getTop() - verticalSpacing); 581 adjustViewsUpOrDown(); 582 below = fillDown(motionRowStart + numColumns, referenceView.getBottom() + verticalSpacing); 583 // Check if we have dragged the bottom of the grid too high 584 final int childCount = getChildCount(); 585 if (childCount > 0) { 586 correctTooHigh(numColumns, verticalSpacing, childCount); 587 } 588 } else { 589 below = fillDown(motionRowEnd + numColumns, referenceView.getBottom() + verticalSpacing); 590 adjustViewsUpOrDown(); 591 above = fillUp(motionRowStart - 1, referenceView.getTop() - verticalSpacing); 592 // Check if we have dragged the bottom of the grid too high 593 final int childCount = getChildCount(); 594 if (childCount > 0) { 595 correctTooLow(numColumns, verticalSpacing, childCount); 596 } 597 } 598 599 if (temp != null) { 600 return temp; 601 } else if (above != null) { 602 return above; 603 } else { 604 return below; 605 } 606 } 607 608 private void correctTooHigh(int numColumns, int verticalSpacing, int childCount) { 609 // First see if the last item is visible 610 final int lastPosition = mFirstPosition + childCount - 1; 611 if (lastPosition == mItemCount - 1 && childCount > 0) { 612 // Get the last child ... 613 final View lastChild = getChildAt(childCount - 1); 614 615 // ... and its bottom edge 616 final int lastBottom = lastChild.getBottom(); 617 // This is bottom of our drawable area 618 final int end = (mBottom - mTop) - mListPadding.bottom; 619 620 // This is how far the bottom edge of the last view is from the bottom of the 621 // drawable area 622 int bottomOffset = end - lastBottom; 623 624 final View firstChild = getChildAt(0); 625 final int firstTop = firstChild.getTop(); 626 627 // Make sure we are 1) Too high, and 2) Either there are more rows above the 628 // first row or the first row is scrolled off the top of the drawable area 629 if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top)) { 630 if (mFirstPosition == 0) { 631 // Don't pull the top too far down 632 bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop); 633 } 634 635 // Move everything down 636 offsetChildrenTopAndBottom(bottomOffset); 637 if (mFirstPosition > 0) { 638 // Fill the gap that was opened above mFirstPosition with more rows, if 639 // possible 640 fillUp(mFirstPosition - (mStackFromBottom ? 1 : numColumns), 641 firstChild.getTop() - verticalSpacing); 642 // Close up the remaining gap 643 adjustViewsUpOrDown(); 644 } 645 } 646 } 647 } 648 649 private void correctTooLow(int numColumns, int verticalSpacing, int childCount) { 650 if (mFirstPosition == 0 && childCount > 0) { 651 // Get the first child ... 652 final View firstChild = getChildAt(0); 653 654 // ... and its top edge 655 final int firstTop = firstChild.getTop(); 656 657 // This is top of our drawable area 658 final int start = mListPadding.top; 659 660 // This is bottom of our drawable area 661 final int end = (mBottom - mTop) - mListPadding.bottom; 662 663 // This is how far the top edge of the first view is from the top of the 664 // drawable area 665 int topOffset = firstTop - start; 666 final View lastChild = getChildAt(childCount - 1); 667 final int lastBottom = lastChild.getBottom(); 668 final int lastPosition = mFirstPosition + childCount - 1; 669 670 // Make sure we are 1) Too low, and 2) Either there are more rows below the 671 // last row or the last row is scrolled off the bottom of the drawable area 672 if (topOffset > 0 && (lastPosition < mItemCount - 1 || lastBottom > end)) { 673 if (lastPosition == mItemCount - 1 ) { 674 // Don't pull the bottom too far up 675 topOffset = Math.min(topOffset, lastBottom - end); 676 } 677 678 // Move everything up 679 offsetChildrenTopAndBottom(-topOffset); 680 if (lastPosition < mItemCount - 1) { 681 // Fill the gap that was opened below the last position with more rows, if 682 // possible 683 fillDown(lastPosition + (!mStackFromBottom ? 1 : numColumns), 684 lastChild.getBottom() + verticalSpacing); 685 // Close up the remaining gap 686 adjustViewsUpOrDown(); 687 } 688 } 689 } 690 } 691 692 /** 693 * Fills the grid based on positioning the new selection at a specific 694 * location. The selection may be moved so that it does not intersect the 695 * faded edges. The grid is then filled upwards and downwards from there. 696 * 697 * @param selectedTop Where the selected item should be 698 * @param childrenTop Where to start drawing children 699 * @param childrenBottom Last pixel where children can be drawn 700 * @return The view that currently has selection 701 */ 702 private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) { 703 final int fadingEdgeLength = getVerticalFadingEdgeLength(); 704 final int selectedPosition = mSelectedPosition; 705 final int numColumns = mNumColumns; 706 final int verticalSpacing = mVerticalSpacing; 707 708 int rowStart; 709 int rowEnd = -1; 710 711 if (!mStackFromBottom) { 712 rowStart = selectedPosition - (selectedPosition % numColumns); 713 } else { 714 int invertedSelection = mItemCount - 1 - selectedPosition; 715 716 rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 717 rowStart = Math.max(0, rowEnd - numColumns + 1); 718 } 719 720 View sel; 721 View referenceView; 722 723 int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); 724 int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, 725 numColumns, rowStart); 726 727 sel = makeRow(mStackFromBottom ? rowEnd : rowStart, selectedTop, true); 728 // Possibly changed again in fillUp if we add rows above this one. 729 mFirstPosition = rowStart; 730 731 referenceView = mReferenceView; 732 adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); 733 adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); 734 735 if (!mStackFromBottom) { 736 fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); 737 adjustViewsUpOrDown(); 738 fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); 739 } else { 740 fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); 741 adjustViewsUpOrDown(); 742 fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); 743 } 744 745 746 return sel; 747 } 748 749 /** 750 * Calculate the bottom-most pixel we can draw the selection into 751 * 752 * @param childrenBottom Bottom pixel were children can be drawn 753 * @param fadingEdgeLength Length of the fading edge in pixels, if present 754 * @param numColumns Number of columns in the grid 755 * @param rowStart The start of the row that will contain the selection 756 * @return The bottom-most pixel we can draw the selection into 757 */ 758 private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength, 759 int numColumns, int rowStart) { 760 // Last pixel we can draw the selection into 761 int bottomSelectionPixel = childrenBottom; 762 if (rowStart + numColumns - 1 < mItemCount - 1) { 763 bottomSelectionPixel -= fadingEdgeLength; 764 } 765 return bottomSelectionPixel; 766 } 767 768 /** 769 * Calculate the top-most pixel we can draw the selection into 770 * 771 * @param childrenTop Top pixel were children can be drawn 772 * @param fadingEdgeLength Length of the fading edge in pixels, if present 773 * @param rowStart The start of the row that will contain the selection 774 * @return The top-most pixel we can draw the selection into 775 */ 776 private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int rowStart) { 777 // first pixel we can draw the selection into 778 int topSelectionPixel = childrenTop; 779 if (rowStart > 0) { 780 topSelectionPixel += fadingEdgeLength; 781 } 782 return topSelectionPixel; 783 } 784 785 /** 786 * Move all views upwards so the selected row does not interesect the bottom 787 * fading edge (if necessary). 788 * 789 * @param childInSelectedRow A child in the row that contains the selection 790 * @param topSelectionPixel The topmost pixel we can draw the selection into 791 * @param bottomSelectionPixel The bottommost pixel we can draw the 792 * selection into 793 */ 794 private void adjustForBottomFadingEdge(View childInSelectedRow, 795 int topSelectionPixel, int bottomSelectionPixel) { 796 // Some of the newly selected item extends below the bottom of the 797 // list 798 if (childInSelectedRow.getBottom() > bottomSelectionPixel) { 799 800 // Find space available above the selection into which we can 801 // scroll upwards 802 int spaceAbove = childInSelectedRow.getTop() - topSelectionPixel; 803 804 // Find space required to bring the bottom of the selected item 805 // fully into view 806 int spaceBelow = childInSelectedRow.getBottom() - bottomSelectionPixel; 807 int offset = Math.min(spaceAbove, spaceBelow); 808 809 // Now offset the selected item to get it into view 810 offsetChildrenTopAndBottom(-offset); 811 } 812 } 813 814 /** 815 * Move all views upwards so the selected row does not interesect the top 816 * fading edge (if necessary). 817 * 818 * @param childInSelectedRow A child in the row that contains the selection 819 * @param topSelectionPixel The topmost pixel we can draw the selection into 820 * @param bottomSelectionPixel The bottommost pixel we can draw the 821 * selection into 822 */ 823 private void adjustForTopFadingEdge(View childInSelectedRow, 824 int topSelectionPixel, int bottomSelectionPixel) { 825 // Some of the newly selected item extends above the top of the list 826 if (childInSelectedRow.getTop() < topSelectionPixel) { 827 // Find space required to bring the top of the selected item 828 // fully into view 829 int spaceAbove = topSelectionPixel - childInSelectedRow.getTop(); 830 831 // Find space available below the selection into which we can 832 // scroll downwards 833 int spaceBelow = bottomSelectionPixel - childInSelectedRow.getBottom(); 834 int offset = Math.min(spaceAbove, spaceBelow); 835 836 // Now offset the selected item to get it into view 837 offsetChildrenTopAndBottom(offset); 838 } 839 } 840 841 /** 842 * Smoothly scroll to the specified adapter position. The view will 843 * scroll such that the indicated position is displayed. 844 * @param position Scroll to this adapter position. 845 */ 846 @android.view.RemotableViewMethod 847 public void smoothScrollToPosition(int position) { 848 super.smoothScrollToPosition(position); 849 } 850 851 /** 852 * Smoothly scroll to the specified adapter position offset. The view will 853 * scroll such that the indicated position is displayed. 854 * @param offset The amount to offset from the adapter position to scroll to. 855 */ 856 @android.view.RemotableViewMethod 857 public void smoothScrollByOffset(int offset) { 858 super.smoothScrollByOffset(offset); 859 } 860 861 /** 862 * Fills the grid based on positioning the new selection relative to the old 863 * selection. The new selection will be placed at, above, or below the 864 * location of the new selection depending on how the selection is moving. 865 * The selection will then be pinned to the visible part of the screen, 866 * excluding the edges that are faded. The grid is then filled upwards and 867 * downwards from there. 868 * 869 * @param delta Which way we are moving 870 * @param childrenTop Where to start drawing children 871 * @param childrenBottom Last pixel where children can be drawn 872 * @return The view that currently has selection 873 */ 874 private View moveSelection(int delta, int childrenTop, int childrenBottom) { 875 final int fadingEdgeLength = getVerticalFadingEdgeLength(); 876 final int selectedPosition = mSelectedPosition; 877 final int numColumns = mNumColumns; 878 final int verticalSpacing = mVerticalSpacing; 879 880 int oldRowStart; 881 int rowStart; 882 int rowEnd = -1; 883 884 if (!mStackFromBottom) { 885 oldRowStart = (selectedPosition - delta) - ((selectedPosition - delta) % numColumns); 886 887 rowStart = selectedPosition - (selectedPosition % numColumns); 888 } else { 889 int invertedSelection = mItemCount - 1 - selectedPosition; 890 891 rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 892 rowStart = Math.max(0, rowEnd - numColumns + 1); 893 894 invertedSelection = mItemCount - 1 - (selectedPosition - delta); 895 oldRowStart = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 896 oldRowStart = Math.max(0, oldRowStart - numColumns + 1); 897 } 898 899 final int rowDelta = rowStart - oldRowStart; 900 901 final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); 902 final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, 903 numColumns, rowStart); 904 905 // Possibly changed again in fillUp if we add rows above this one. 906 mFirstPosition = rowStart; 907 908 View sel; 909 View referenceView; 910 911 if (rowDelta > 0) { 912 /* 913 * Case 1: Scrolling down. 914 */ 915 916 final int oldBottom = mReferenceViewInSelectedRow == null ? 0 : 917 mReferenceViewInSelectedRow.getBottom(); 918 919 sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldBottom + verticalSpacing, true); 920 referenceView = mReferenceView; 921 922 adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); 923 } else if (rowDelta < 0) { 924 /* 925 * Case 2: Scrolling up. 926 */ 927 final int oldTop = mReferenceViewInSelectedRow == null ? 928 0 : mReferenceViewInSelectedRow .getTop(); 929 930 sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop - verticalSpacing, false); 931 referenceView = mReferenceView; 932 933 adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); 934 } else { 935 /* 936 * Keep selection where it was 937 */ 938 final int oldTop = mReferenceViewInSelectedRow == null ? 939 0 : mReferenceViewInSelectedRow .getTop(); 940 941 sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop, true); 942 referenceView = mReferenceView; 943 } 944 945 if (!mStackFromBottom) { 946 fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); 947 adjustViewsUpOrDown(); 948 fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); 949 } else { 950 fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); 951 adjustViewsUpOrDown(); 952 fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); 953 } 954 955 return sel; 956 } 957 958 private boolean determineColumns(int availableSpace) { 959 final int requestedHorizontalSpacing = mRequestedHorizontalSpacing; 960 final int stretchMode = mStretchMode; 961 final int requestedColumnWidth = mRequestedColumnWidth; 962 boolean didNotInitiallyFit = false; 963 964 if (mRequestedNumColumns == AUTO_FIT) { 965 if (requestedColumnWidth > 0) { 966 // Client told us to pick the number of columns 967 mNumColumns = (availableSpace + requestedHorizontalSpacing) / 968 (requestedColumnWidth + requestedHorizontalSpacing); 969 } else { 970 // Just make up a number if we don't have enough info 971 mNumColumns = 2; 972 } 973 } else { 974 // We picked the columns 975 mNumColumns = mRequestedNumColumns; 976 } 977 978 if (mNumColumns <= 0) { 979 mNumColumns = 1; 980 } 981 982 switch (stretchMode) { 983 case NO_STRETCH: 984 // Nobody stretches 985 mColumnWidth = requestedColumnWidth; 986 mHorizontalSpacing = requestedHorizontalSpacing; 987 break; 988 989 default: 990 int spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth) - 991 ((mNumColumns - 1) * requestedHorizontalSpacing); 992 993 if (spaceLeftOver < 0) { 994 didNotInitiallyFit = true; 995 } 996 997 switch (stretchMode) { 998 case STRETCH_COLUMN_WIDTH: 999 // Stretch the columns 1000 mColumnWidth = requestedColumnWidth + spaceLeftOver / mNumColumns; 1001 mHorizontalSpacing = requestedHorizontalSpacing; 1002 break; 1003 1004 case STRETCH_SPACING: 1005 // Stretch the spacing between columns 1006 mColumnWidth = requestedColumnWidth; 1007 if (mNumColumns > 1) { 1008 mHorizontalSpacing = requestedHorizontalSpacing + 1009 spaceLeftOver / (mNumColumns - 1); 1010 } else { 1011 mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver; 1012 } 1013 break; 1014 1015 case STRETCH_SPACING_UNIFORM: 1016 // Stretch the spacing between columns 1017 mColumnWidth = requestedColumnWidth; 1018 if (mNumColumns > 1) { 1019 mHorizontalSpacing = requestedHorizontalSpacing + 1020 spaceLeftOver / (mNumColumns + 1); 1021 } else { 1022 mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver; 1023 } 1024 break; 1025 } 1026 1027 break; 1028 } 1029 return didNotInitiallyFit; 1030 } 1031 1032 @Override 1033 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1034 // Sets up mListPadding 1035 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1036 1037 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 1038 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 1039 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 1040 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 1041 1042 if (widthMode == MeasureSpec.UNSPECIFIED) { 1043 if (mColumnWidth > 0) { 1044 widthSize = mColumnWidth + mListPadding.left + mListPadding.right; 1045 } else { 1046 widthSize = mListPadding.left + mListPadding.right; 1047 } 1048 widthSize += getVerticalScrollbarWidth(); 1049 } 1050 1051 int childWidth = widthSize - mListPadding.left - mListPadding.right; 1052 boolean didNotInitiallyFit = determineColumns(childWidth); 1053 1054 int childHeight = 0; 1055 int childState = 0; 1056 1057 mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); 1058 final int count = mItemCount; 1059 if (count > 0) { 1060 final View child = obtainView(0, mIsScrap); 1061 1062 AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); 1063 if (p == null) { 1064 p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); 1065 child.setLayoutParams(p); 1066 } 1067 p.viewType = mAdapter.getItemViewType(0); 1068 p.forceAdd = true; 1069 1070 int childHeightSpec = getChildMeasureSpec( 1071 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); 1072 int childWidthSpec = getChildMeasureSpec( 1073 MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); 1074 child.measure(childWidthSpec, childHeightSpec); 1075 1076 childHeight = child.getMeasuredHeight(); 1077 childState = combineMeasuredStates(childState, child.getMeasuredState()); 1078 1079 if (mRecycler.shouldRecycleViewType(p.viewType)) { 1080 mRecycler.addScrapView(child, -1); 1081 } 1082 } 1083 1084 if (heightMode == MeasureSpec.UNSPECIFIED) { 1085 heightSize = mListPadding.top + mListPadding.bottom + childHeight + 1086 getVerticalFadingEdgeLength() * 2; 1087 } 1088 1089 if (heightMode == MeasureSpec.AT_MOST) { 1090 int ourSize = mListPadding.top + mListPadding.bottom; 1091 1092 final int numColumns = mNumColumns; 1093 for (int i = 0; i < count; i += numColumns) { 1094 ourSize += childHeight; 1095 if (i + numColumns < count) { 1096 ourSize += mVerticalSpacing; 1097 } 1098 if (ourSize >= heightSize) { 1099 ourSize = heightSize; 1100 break; 1101 } 1102 } 1103 heightSize = ourSize; 1104 } 1105 1106 if (widthMode == MeasureSpec.AT_MOST && mRequestedNumColumns != AUTO_FIT) { 1107 int ourSize = (mRequestedNumColumns*mColumnWidth) 1108 + ((mRequestedNumColumns-1)*mHorizontalSpacing) 1109 + mListPadding.left + mListPadding.right; 1110 if (ourSize > widthSize || didNotInitiallyFit) { 1111 widthSize |= MEASURED_STATE_TOO_SMALL; 1112 } 1113 } 1114 1115 setMeasuredDimension(widthSize, heightSize); 1116 mWidthMeasureSpec = widthMeasureSpec; 1117 } 1118 1119 @Override 1120 protected void attachLayoutAnimationParameters(View child, 1121 ViewGroup.LayoutParams params, int index, int count) { 1122 1123 GridLayoutAnimationController.AnimationParameters animationParams = 1124 (GridLayoutAnimationController.AnimationParameters) params.layoutAnimationParameters; 1125 1126 if (animationParams == null) { 1127 animationParams = new GridLayoutAnimationController.AnimationParameters(); 1128 params.layoutAnimationParameters = animationParams; 1129 } 1130 1131 animationParams.count = count; 1132 animationParams.index = index; 1133 animationParams.columnsCount = mNumColumns; 1134 animationParams.rowsCount = count / mNumColumns; 1135 1136 if (!mStackFromBottom) { 1137 animationParams.column = index % mNumColumns; 1138 animationParams.row = index / mNumColumns; 1139 } else { 1140 final int invertedIndex = count - 1 - index; 1141 1142 animationParams.column = mNumColumns - 1 - (invertedIndex % mNumColumns); 1143 animationParams.row = animationParams.rowsCount - 1 - invertedIndex / mNumColumns; 1144 } 1145 } 1146 1147 @Override 1148 protected void layoutChildren() { 1149 final boolean blockLayoutRequests = mBlockLayoutRequests; 1150 if (!blockLayoutRequests) { 1151 mBlockLayoutRequests = true; 1152 } 1153 1154 try { 1155 super.layoutChildren(); 1156 1157 invalidate(); 1158 1159 if (mAdapter == null) { 1160 resetList(); 1161 invokeOnItemScrollListener(); 1162 return; 1163 } 1164 1165 final int childrenTop = mListPadding.top; 1166 final int childrenBottom = mBottom - mTop - mListPadding.bottom; 1167 1168 int childCount = getChildCount(); 1169 int index; 1170 int delta = 0; 1171 1172 View sel; 1173 View oldSel = null; 1174 View oldFirst = null; 1175 View newSel = null; 1176 1177 // Remember stuff we will need down below 1178 switch (mLayoutMode) { 1179 case LAYOUT_SET_SELECTION: 1180 index = mNextSelectedPosition - mFirstPosition; 1181 if (index >= 0 && index < childCount) { 1182 newSel = getChildAt(index); 1183 } 1184 break; 1185 case LAYOUT_FORCE_TOP: 1186 case LAYOUT_FORCE_BOTTOM: 1187 case LAYOUT_SPECIFIC: 1188 case LAYOUT_SYNC: 1189 break; 1190 case LAYOUT_MOVE_SELECTION: 1191 if (mNextSelectedPosition >= 0) { 1192 delta = mNextSelectedPosition - mSelectedPosition; 1193 } 1194 break; 1195 default: 1196 // Remember the previously selected view 1197 index = mSelectedPosition - mFirstPosition; 1198 if (index >= 0 && index < childCount) { 1199 oldSel = getChildAt(index); 1200 } 1201 1202 // Remember the previous first child 1203 oldFirst = getChildAt(0); 1204 } 1205 1206 boolean dataChanged = mDataChanged; 1207 if (dataChanged) { 1208 handleDataChanged(); 1209 } 1210 1211 // Handle the empty set by removing all views that are visible 1212 // and calling it a day 1213 if (mItemCount == 0) { 1214 resetList(); 1215 invokeOnItemScrollListener(); 1216 return; 1217 } 1218 1219 setSelectedPositionInt(mNextSelectedPosition); 1220 1221 AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null; 1222 View accessibilityFocusLayoutRestoreView = null; 1223 int accessibilityFocusPosition = INVALID_POSITION; 1224 1225 // Remember which child, if any, had accessibility focus. This must 1226 // occur before recycling any views, since that will clear 1227 // accessibility focus. 1228 final ViewRootImpl viewRootImpl = getViewRootImpl(); 1229 if (viewRootImpl != null) { 1230 final View focusHost = viewRootImpl.getAccessibilityFocusedHost(); 1231 if (focusHost != null) { 1232 final View focusChild = getAccessibilityFocusedChild(focusHost); 1233 if (focusChild != null) { 1234 if (!dataChanged || focusChild.hasTransientState() 1235 || mAdapterHasStableIds) { 1236 // The views won't be changing, so try to maintain 1237 // focus on the current host and virtual view. 1238 accessibilityFocusLayoutRestoreView = focusHost; 1239 accessibilityFocusLayoutRestoreNode = viewRootImpl 1240 .getAccessibilityFocusedVirtualView(); 1241 } 1242 1243 // Try to maintain focus at the same position. 1244 accessibilityFocusPosition = getPositionForView(focusChild); 1245 } 1246 } 1247 } 1248 1249 // Pull all children into the RecycleBin. 1250 // These views will be reused if possible 1251 final int firstPosition = mFirstPosition; 1252 final RecycleBin recycleBin = mRecycler; 1253 1254 if (dataChanged) { 1255 for (int i = 0; i < childCount; i++) { 1256 recycleBin.addScrapView(getChildAt(i), firstPosition+i); 1257 } 1258 } else { 1259 recycleBin.fillActiveViews(childCount, firstPosition); 1260 } 1261 1262 // Clear out old views 1263 detachAllViewsFromParent(); 1264 recycleBin.removeSkippedScrap(); 1265 1266 switch (mLayoutMode) { 1267 case LAYOUT_SET_SELECTION: 1268 if (newSel != null) { 1269 sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); 1270 } else { 1271 sel = fillSelection(childrenTop, childrenBottom); 1272 } 1273 break; 1274 case LAYOUT_FORCE_TOP: 1275 mFirstPosition = 0; 1276 sel = fillFromTop(childrenTop); 1277 adjustViewsUpOrDown(); 1278 break; 1279 case LAYOUT_FORCE_BOTTOM: 1280 sel = fillUp(mItemCount - 1, childrenBottom); 1281 adjustViewsUpOrDown(); 1282 break; 1283 case LAYOUT_SPECIFIC: 1284 sel = fillSpecific(mSelectedPosition, mSpecificTop); 1285 break; 1286 case LAYOUT_SYNC: 1287 sel = fillSpecific(mSyncPosition, mSpecificTop); 1288 break; 1289 case LAYOUT_MOVE_SELECTION: 1290 // Move the selection relative to its old position 1291 sel = moveSelection(delta, childrenTop, childrenBottom); 1292 break; 1293 default: 1294 if (childCount == 0) { 1295 if (!mStackFromBottom) { 1296 setSelectedPositionInt(mAdapter == null || isInTouchMode() ? 1297 INVALID_POSITION : 0); 1298 sel = fillFromTop(childrenTop); 1299 } else { 1300 final int last = mItemCount - 1; 1301 setSelectedPositionInt(mAdapter == null || isInTouchMode() ? 1302 INVALID_POSITION : last); 1303 sel = fillFromBottom(last, childrenBottom); 1304 } 1305 } else { 1306 if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { 1307 sel = fillSpecific(mSelectedPosition, oldSel == null ? 1308 childrenTop : oldSel.getTop()); 1309 } else if (mFirstPosition < mItemCount) { 1310 sel = fillSpecific(mFirstPosition, oldFirst == null ? 1311 childrenTop : oldFirst.getTop()); 1312 } else { 1313 sel = fillSpecific(0, childrenTop); 1314 } 1315 } 1316 break; 1317 } 1318 1319 // Flush any cached views that did not get reused above 1320 recycleBin.scrapActiveViews(); 1321 1322 if (sel != null) { 1323 positionSelector(INVALID_POSITION, sel); 1324 mSelectedTop = sel.getTop(); 1325 } else { 1326 final boolean inTouchMode = mTouchMode > TOUCH_MODE_DOWN 1327 && mTouchMode < TOUCH_MODE_SCROLL; 1328 if (inTouchMode) { 1329 // If the user's finger is down, select the motion position. 1330 final View child = getChildAt(mMotionPosition - mFirstPosition); 1331 if (child != null) { 1332 positionSelector(mMotionPosition, child); 1333 } 1334 } else if (mSelectedPosition != INVALID_POSITION) { 1335 // If we had previously positioned the selector somewhere, 1336 // put it back there. It might not match up with the data, 1337 // but it's transitioning out so it's not a big deal. 1338 final View child = getChildAt(mSelectorPosition - mFirstPosition); 1339 if (child != null) { 1340 positionSelector(mSelectorPosition, child); 1341 } 1342 } else { 1343 // Otherwise, clear selection. 1344 mSelectedTop = 0; 1345 mSelectorRect.setEmpty(); 1346 } 1347 } 1348 1349 // Attempt to restore accessibility focus, if necessary. 1350 if (viewRootImpl != null) { 1351 final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost(); 1352 if (newAccessibilityFocusedView == null) { 1353 if (accessibilityFocusLayoutRestoreView != null 1354 && accessibilityFocusLayoutRestoreView.isAttachedToWindow()) { 1355 final AccessibilityNodeProvider provider = 1356 accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider(); 1357 if (accessibilityFocusLayoutRestoreNode != null && provider != null) { 1358 final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId( 1359 accessibilityFocusLayoutRestoreNode.getSourceNodeId()); 1360 provider.performAction(virtualViewId, 1361 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 1362 } else { 1363 accessibilityFocusLayoutRestoreView.requestAccessibilityFocus(); 1364 } 1365 } else if (accessibilityFocusPosition != INVALID_POSITION) { 1366 // Bound the position within the visible children. 1367 final int position = MathUtils.constrain( 1368 accessibilityFocusPosition - mFirstPosition, 0, 1369 getChildCount() - 1); 1370 final View restoreView = getChildAt(position); 1371 if (restoreView != null) { 1372 restoreView.requestAccessibilityFocus(); 1373 } 1374 } 1375 } 1376 } 1377 1378 mLayoutMode = LAYOUT_NORMAL; 1379 mDataChanged = false; 1380 if (mPositionScrollAfterLayout != null) { 1381 post(mPositionScrollAfterLayout); 1382 mPositionScrollAfterLayout = null; 1383 } 1384 mNeedSync = false; 1385 setNextSelectedPositionInt(mSelectedPosition); 1386 1387 updateScrollIndicators(); 1388 1389 if (mItemCount > 0) { 1390 checkSelectionChanged(); 1391 } 1392 1393 invokeOnItemScrollListener(); 1394 } finally { 1395 if (!blockLayoutRequests) { 1396 mBlockLayoutRequests = false; 1397 } 1398 } 1399 } 1400 1401 1402 /** 1403 * Obtain the view and add it to our list of children. The view can be made 1404 * fresh, converted from an unused view, or used as is if it was in the 1405 * recycle bin. 1406 * 1407 * @param position Logical position in the list 1408 * @param y Top or bottom edge of the view to add 1409 * @param flow if true, align top edge to y. If false, align bottom edge to 1410 * y. 1411 * @param childrenLeft Left edge where children should be positioned 1412 * @param selected Is this position selected? 1413 * @param where to add new item in the list 1414 * @return View that was added 1415 */ 1416 private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, 1417 boolean selected, int where) { 1418 View child; 1419 1420 if (!mDataChanged) { 1421 // Try to use an existing view for this position 1422 child = mRecycler.getActiveView(position); 1423 if (child != null) { 1424 // Found it -- we're using an existing child 1425 // This just needs to be positioned 1426 setupChild(child, position, y, flow, childrenLeft, selected, true, where); 1427 return child; 1428 } 1429 } 1430 1431 // Make a new view for this position, or convert an unused view if 1432 // possible 1433 child = obtainView(position, mIsScrap); 1434 1435 // This needs to be positioned and measured 1436 setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0], where); 1437 1438 return child; 1439 } 1440 1441 /** 1442 * Add a view as a child and make sure it is measured (if necessary) and 1443 * positioned properly. 1444 * 1445 * @param child The view to add 1446 * @param position The position of the view 1447 * @param y The y position relative to which this view will be positioned 1448 * @param flow if true, align top edge to y. If false, align bottom edge 1449 * to y. 1450 * @param childrenLeft Left edge where children should be positioned 1451 * @param selected Is this position selected? 1452 * @param recycled Has this view been pulled from the recycle bin? If so it 1453 * does not need to be remeasured. 1454 * @param where Where to add the item in the list 1455 * 1456 */ 1457 private void setupChild(View child, int position, int y, boolean flow, int childrenLeft, 1458 boolean selected, boolean recycled, int where) { 1459 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupGridItem"); 1460 1461 boolean isSelected = selected && shouldShowSelector(); 1462 final boolean updateChildSelected = isSelected != child.isSelected(); 1463 final int mode = mTouchMode; 1464 final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && 1465 mMotionPosition == position; 1466 final boolean updateChildPressed = isPressed != child.isPressed(); 1467 1468 boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); 1469 1470 // Respect layout params that are already in the view. Otherwise make 1471 // some up... 1472 AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); 1473 if (p == null) { 1474 p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); 1475 } 1476 p.viewType = mAdapter.getItemViewType(position); 1477 1478 if (recycled && !p.forceAdd) { 1479 attachViewToParent(child, where, p); 1480 } else { 1481 p.forceAdd = false; 1482 addViewInLayout(child, where, p, true); 1483 } 1484 1485 if (updateChildSelected) { 1486 child.setSelected(isSelected); 1487 if (isSelected) { 1488 requestFocus(); 1489 } 1490 } 1491 1492 if (updateChildPressed) { 1493 child.setPressed(isPressed); 1494 } 1495 1496 if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { 1497 if (child instanceof Checkable) { 1498 ((Checkable) child).setChecked(mCheckStates.get(position)); 1499 } else if (getContext().getApplicationInfo().targetSdkVersion 1500 >= android.os.Build.VERSION_CODES.HONEYCOMB) { 1501 child.setActivated(mCheckStates.get(position)); 1502 } 1503 } 1504 1505 if (needToMeasure) { 1506 int childHeightSpec = ViewGroup.getChildMeasureSpec( 1507 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); 1508 1509 int childWidthSpec = ViewGroup.getChildMeasureSpec( 1510 MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); 1511 child.measure(childWidthSpec, childHeightSpec); 1512 } else { 1513 cleanupLayoutState(child); 1514 } 1515 1516 final int w = child.getMeasuredWidth(); 1517 final int h = child.getMeasuredHeight(); 1518 1519 int childLeft; 1520 final int childTop = flow ? y : y - h; 1521 1522 final int layoutDirection = getLayoutDirection(); 1523 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); 1524 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 1525 case Gravity.LEFT: 1526 childLeft = childrenLeft; 1527 break; 1528 case Gravity.CENTER_HORIZONTAL: 1529 childLeft = childrenLeft + ((mColumnWidth - w) / 2); 1530 break; 1531 case Gravity.RIGHT: 1532 childLeft = childrenLeft + mColumnWidth - w; 1533 break; 1534 default: 1535 childLeft = childrenLeft; 1536 break; 1537 } 1538 1539 if (needToMeasure) { 1540 final int childRight = childLeft + w; 1541 final int childBottom = childTop + h; 1542 child.layout(childLeft, childTop, childRight, childBottom); 1543 } else { 1544 child.offsetLeftAndRight(childLeft - child.getLeft()); 1545 child.offsetTopAndBottom(childTop - child.getTop()); 1546 } 1547 1548 if (mCachingStarted) { 1549 child.setDrawingCacheEnabled(true); 1550 } 1551 1552 if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition) 1553 != position) { 1554 child.jumpDrawablesToCurrentState(); 1555 } 1556 1557 Trace.traceEnd(Trace.TRACE_TAG_VIEW); 1558 } 1559 1560 /** 1561 * Sets the currently selected item 1562 * 1563 * @param position Index (starting at 0) of the data item to be selected. 1564 * 1565 * If in touch mode, the item will not be selected but it will still be positioned 1566 * appropriately. 1567 */ 1568 @Override 1569 public void setSelection(int position) { 1570 if (!isInTouchMode()) { 1571 setNextSelectedPositionInt(position); 1572 } else { 1573 mResurrectToPosition = position; 1574 } 1575 mLayoutMode = LAYOUT_SET_SELECTION; 1576 if (mPositionScroller != null) { 1577 mPositionScroller.stop(); 1578 } 1579 requestLayout(); 1580 } 1581 1582 /** 1583 * Makes the item at the supplied position selected. 1584 * 1585 * @param position the position of the new selection 1586 */ 1587 @Override 1588 void setSelectionInt(int position) { 1589 int previousSelectedPosition = mNextSelectedPosition; 1590 1591 if (mPositionScroller != null) { 1592 mPositionScroller.stop(); 1593 } 1594 1595 setNextSelectedPositionInt(position); 1596 layoutChildren(); 1597 1598 final int next = mStackFromBottom ? mItemCount - 1 - mNextSelectedPosition : 1599 mNextSelectedPosition; 1600 final int previous = mStackFromBottom ? mItemCount - 1 1601 - previousSelectedPosition : previousSelectedPosition; 1602 1603 final int nextRow = next / mNumColumns; 1604 final int previousRow = previous / mNumColumns; 1605 1606 if (nextRow != previousRow) { 1607 awakenScrollBars(); 1608 } 1609 1610 } 1611 1612 @Override 1613 public boolean onKeyDown(int keyCode, KeyEvent event) { 1614 return commonKey(keyCode, 1, event); 1615 } 1616 1617 @Override 1618 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { 1619 return commonKey(keyCode, repeatCount, event); 1620 } 1621 1622 @Override 1623 public boolean onKeyUp(int keyCode, KeyEvent event) { 1624 return commonKey(keyCode, 1, event); 1625 } 1626 1627 private boolean commonKey(int keyCode, int count, KeyEvent event) { 1628 if (mAdapter == null) { 1629 return false; 1630 } 1631 1632 if (mDataChanged) { 1633 layoutChildren(); 1634 } 1635 1636 boolean handled = false; 1637 int action = event.getAction(); 1638 1639 if (action != KeyEvent.ACTION_UP) { 1640 switch (keyCode) { 1641 case KeyEvent.KEYCODE_DPAD_LEFT: 1642 if (event.hasNoModifiers()) { 1643 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_LEFT); 1644 } 1645 break; 1646 1647 case KeyEvent.KEYCODE_DPAD_RIGHT: 1648 if (event.hasNoModifiers()) { 1649 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_RIGHT); 1650 } 1651 break; 1652 1653 case KeyEvent.KEYCODE_DPAD_UP: 1654 if (event.hasNoModifiers()) { 1655 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_UP); 1656 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1657 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1658 } 1659 break; 1660 1661 case KeyEvent.KEYCODE_DPAD_DOWN: 1662 if (event.hasNoModifiers()) { 1663 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_DOWN); 1664 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1665 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1666 } 1667 break; 1668 1669 case KeyEvent.KEYCODE_DPAD_CENTER: 1670 case KeyEvent.KEYCODE_ENTER: 1671 if (event.hasNoModifiers()) { 1672 handled = resurrectSelectionIfNeeded(); 1673 if (!handled 1674 && event.getRepeatCount() == 0 && getChildCount() > 0) { 1675 keyPressed(); 1676 handled = true; 1677 } 1678 } 1679 break; 1680 1681 case KeyEvent.KEYCODE_SPACE: 1682 if (mPopup == null || !mPopup.isShowing()) { 1683 if (event.hasNoModifiers()) { 1684 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN); 1685 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { 1686 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP); 1687 } 1688 } 1689 break; 1690 1691 case KeyEvent.KEYCODE_PAGE_UP: 1692 if (event.hasNoModifiers()) { 1693 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP); 1694 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1695 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1696 } 1697 break; 1698 1699 case KeyEvent.KEYCODE_PAGE_DOWN: 1700 if (event.hasNoModifiers()) { 1701 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN); 1702 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1703 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1704 } 1705 break; 1706 1707 case KeyEvent.KEYCODE_MOVE_HOME: 1708 if (event.hasNoModifiers()) { 1709 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1710 } 1711 break; 1712 1713 case KeyEvent.KEYCODE_MOVE_END: 1714 if (event.hasNoModifiers()) { 1715 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1716 } 1717 break; 1718 1719 case KeyEvent.KEYCODE_TAB: 1720 // XXX Sometimes it is useful to be able to TAB through the items in 1721 // a GridView sequentially. Unfortunately this can create an 1722 // asymmetry in TAB navigation order unless the list selection 1723 // always reverts to the top or bottom when receiving TAB focus from 1724 // another widget. Leaving this behavior disabled for now but 1725 // perhaps it should be configurable (and more comprehensive). 1726 if (false) { 1727 if (event.hasNoModifiers()) { 1728 handled = resurrectSelectionIfNeeded() 1729 || sequenceScroll(FOCUS_FORWARD); 1730 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { 1731 handled = resurrectSelectionIfNeeded() 1732 || sequenceScroll(FOCUS_BACKWARD); 1733 } 1734 } 1735 break; 1736 } 1737 } 1738 1739 if (handled) { 1740 return true; 1741 } 1742 1743 if (sendToTextFilter(keyCode, count, event)) { 1744 return true; 1745 } 1746 1747 switch (action) { 1748 case KeyEvent.ACTION_DOWN: 1749 return super.onKeyDown(keyCode, event); 1750 case KeyEvent.ACTION_UP: 1751 return super.onKeyUp(keyCode, event); 1752 case KeyEvent.ACTION_MULTIPLE: 1753 return super.onKeyMultiple(keyCode, count, event); 1754 default: 1755 return false; 1756 } 1757 } 1758 1759 /** 1760 * Scrolls up or down by the number of items currently present on screen. 1761 * 1762 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 1763 * @return whether selection was moved 1764 */ 1765 boolean pageScroll(int direction) { 1766 int nextPage = -1; 1767 1768 if (direction == FOCUS_UP) { 1769 nextPage = Math.max(0, mSelectedPosition - getChildCount()); 1770 } else if (direction == FOCUS_DOWN) { 1771 nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount()); 1772 } 1773 1774 if (nextPage >= 0) { 1775 setSelectionInt(nextPage); 1776 invokeOnItemScrollListener(); 1777 awakenScrollBars(); 1778 return true; 1779 } 1780 1781 return false; 1782 } 1783 1784 /** 1785 * Go to the last or first item if possible. 1786 * 1787 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}. 1788 * 1789 * @return Whether selection was moved. 1790 */ 1791 boolean fullScroll(int direction) { 1792 boolean moved = false; 1793 if (direction == FOCUS_UP) { 1794 mLayoutMode = LAYOUT_SET_SELECTION; 1795 setSelectionInt(0); 1796 invokeOnItemScrollListener(); 1797 moved = true; 1798 } else if (direction == FOCUS_DOWN) { 1799 mLayoutMode = LAYOUT_SET_SELECTION; 1800 setSelectionInt(mItemCount - 1); 1801 invokeOnItemScrollListener(); 1802 moved = true; 1803 } 1804 1805 if (moved) { 1806 awakenScrollBars(); 1807 } 1808 1809 return moved; 1810 } 1811 1812 /** 1813 * Scrolls to the next or previous item, horizontally or vertically. 1814 * 1815 * @param direction either {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, 1816 * {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 1817 * 1818 * @return whether selection was moved 1819 */ 1820 boolean arrowScroll(int direction) { 1821 final int selectedPosition = mSelectedPosition; 1822 final int numColumns = mNumColumns; 1823 1824 int startOfRowPos; 1825 int endOfRowPos; 1826 1827 boolean moved = false; 1828 1829 if (!mStackFromBottom) { 1830 startOfRowPos = (selectedPosition / numColumns) * numColumns; 1831 endOfRowPos = Math.min(startOfRowPos + numColumns - 1, mItemCount - 1); 1832 } else { 1833 final int invertedSelection = mItemCount - 1 - selectedPosition; 1834 endOfRowPos = mItemCount - 1 - (invertedSelection / numColumns) * numColumns; 1835 startOfRowPos = Math.max(0, endOfRowPos - numColumns + 1); 1836 } 1837 1838 switch (direction) { 1839 case FOCUS_UP: 1840 if (startOfRowPos > 0) { 1841 mLayoutMode = LAYOUT_MOVE_SELECTION; 1842 setSelectionInt(Math.max(0, selectedPosition - numColumns)); 1843 moved = true; 1844 } 1845 break; 1846 case FOCUS_DOWN: 1847 if (endOfRowPos < mItemCount - 1) { 1848 mLayoutMode = LAYOUT_MOVE_SELECTION; 1849 setSelectionInt(Math.min(selectedPosition + numColumns, mItemCount - 1)); 1850 moved = true; 1851 } 1852 break; 1853 case FOCUS_LEFT: 1854 if (selectedPosition > startOfRowPos) { 1855 mLayoutMode = LAYOUT_MOVE_SELECTION; 1856 setSelectionInt(Math.max(0, selectedPosition - 1)); 1857 moved = true; 1858 } 1859 break; 1860 case FOCUS_RIGHT: 1861 if (selectedPosition < endOfRowPos) { 1862 mLayoutMode = LAYOUT_MOVE_SELECTION; 1863 setSelectionInt(Math.min(selectedPosition + 1, mItemCount - 1)); 1864 moved = true; 1865 } 1866 break; 1867 } 1868 1869 if (moved) { 1870 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 1871 invokeOnItemScrollListener(); 1872 } 1873 1874 if (moved) { 1875 awakenScrollBars(); 1876 } 1877 1878 return moved; 1879 } 1880 1881 /** 1882 * Goes to the next or previous item according to the order set by the 1883 * adapter. 1884 */ 1885 boolean sequenceScroll(int direction) { 1886 int selectedPosition = mSelectedPosition; 1887 int numColumns = mNumColumns; 1888 int count = mItemCount; 1889 1890 int startOfRow; 1891 int endOfRow; 1892 if (!mStackFromBottom) { 1893 startOfRow = (selectedPosition / numColumns) * numColumns; 1894 endOfRow = Math.min(startOfRow + numColumns - 1, count - 1); 1895 } else { 1896 int invertedSelection = count - 1 - selectedPosition; 1897 endOfRow = count - 1 - (invertedSelection / numColumns) * numColumns; 1898 startOfRow = Math.max(0, endOfRow - numColumns + 1); 1899 } 1900 1901 boolean moved = false; 1902 boolean showScroll = false; 1903 switch (direction) { 1904 case FOCUS_FORWARD: 1905 if (selectedPosition < count - 1) { 1906 // Move to the next item. 1907 mLayoutMode = LAYOUT_MOVE_SELECTION; 1908 setSelectionInt(selectedPosition + 1); 1909 moved = true; 1910 // Show the scrollbar only if changing rows. 1911 showScroll = selectedPosition == endOfRow; 1912 } 1913 break; 1914 1915 case FOCUS_BACKWARD: 1916 if (selectedPosition > 0) { 1917 // Move to the previous item. 1918 mLayoutMode = LAYOUT_MOVE_SELECTION; 1919 setSelectionInt(selectedPosition - 1); 1920 moved = true; 1921 // Show the scrollbar only if changing rows. 1922 showScroll = selectedPosition == startOfRow; 1923 } 1924 break; 1925 } 1926 1927 if (moved) { 1928 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 1929 invokeOnItemScrollListener(); 1930 } 1931 1932 if (showScroll) { 1933 awakenScrollBars(); 1934 } 1935 1936 return moved; 1937 } 1938 1939 @Override 1940 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 1941 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 1942 1943 int closestChildIndex = -1; 1944 if (gainFocus && previouslyFocusedRect != null) { 1945 previouslyFocusedRect.offset(mScrollX, mScrollY); 1946 1947 // figure out which item should be selected based on previously 1948 // focused rect 1949 Rect otherRect = mTempRect; 1950 int minDistance = Integer.MAX_VALUE; 1951 final int childCount = getChildCount(); 1952 for (int i = 0; i < childCount; i++) { 1953 // only consider view's on appropriate edge of grid 1954 if (!isCandidateSelection(i, direction)) { 1955 continue; 1956 } 1957 1958 final View other = getChildAt(i); 1959 other.getDrawingRect(otherRect); 1960 offsetDescendantRectToMyCoords(other, otherRect); 1961 int distance = getDistance(previouslyFocusedRect, otherRect, direction); 1962 1963 if (distance < minDistance) { 1964 minDistance = distance; 1965 closestChildIndex = i; 1966 } 1967 } 1968 } 1969 1970 if (closestChildIndex >= 0) { 1971 setSelection(closestChildIndex + mFirstPosition); 1972 } else { 1973 requestLayout(); 1974 } 1975 } 1976 1977 /** 1978 * Is childIndex a candidate for next focus given the direction the focus 1979 * change is coming from? 1980 * @param childIndex The index to check. 1981 * @param direction The direction, one of 1982 * {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD, FOCUS_BACKWARD} 1983 * @return Whether childIndex is a candidate. 1984 */ 1985 private boolean isCandidateSelection(int childIndex, int direction) { 1986 final int count = getChildCount(); 1987 final int invertedIndex = count - 1 - childIndex; 1988 1989 int rowStart; 1990 int rowEnd; 1991 1992 if (!mStackFromBottom) { 1993 rowStart = childIndex - (childIndex % mNumColumns); 1994 rowEnd = Math.max(rowStart + mNumColumns - 1, count); 1995 } else { 1996 rowEnd = count - 1 - (invertedIndex - (invertedIndex % mNumColumns)); 1997 rowStart = Math.max(0, rowEnd - mNumColumns + 1); 1998 } 1999 2000 switch (direction) { 2001 case View.FOCUS_RIGHT: 2002 // coming from left, selection is only valid if it is on left 2003 // edge 2004 return childIndex == rowStart; 2005 case View.FOCUS_DOWN: 2006 // coming from top; only valid if in top row 2007 return rowStart == 0; 2008 case View.FOCUS_LEFT: 2009 // coming from right, must be on right edge 2010 return childIndex == rowEnd; 2011 case View.FOCUS_UP: 2012 // coming from bottom, need to be in last row 2013 return rowEnd == count - 1; 2014 case View.FOCUS_FORWARD: 2015 // coming from top-left, need to be first in top row 2016 return childIndex == rowStart && rowStart == 0; 2017 case View.FOCUS_BACKWARD: 2018 // coming from bottom-right, need to be last in bottom row 2019 return childIndex == rowEnd && rowEnd == count - 1; 2020 default: 2021 throw new IllegalArgumentException("direction must be one of " 2022 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, " 2023 + "FOCUS_FORWARD, FOCUS_BACKWARD}."); 2024 } 2025 } 2026 2027 /** 2028 * Set the gravity for this grid. Gravity describes how the child views 2029 * are horizontally aligned. Defaults to Gravity.LEFT 2030 * 2031 * @param gravity the gravity to apply to this grid's children 2032 * 2033 * @attr ref android.R.styleable#GridView_gravity 2034 */ 2035 public void setGravity(int gravity) { 2036 if (mGravity != gravity) { 2037 mGravity = gravity; 2038 requestLayoutIfNecessary(); 2039 } 2040 } 2041 2042 /** 2043 * Describes how the child views are horizontally aligned. Defaults to Gravity.LEFT 2044 * 2045 * @return the gravity that will be applied to this grid's children 2046 * 2047 * @attr ref android.R.styleable#GridView_gravity 2048 */ 2049 public int getGravity() { 2050 return mGravity; 2051 } 2052 2053 /** 2054 * Set the amount of horizontal (x) spacing to place between each item 2055 * in the grid. 2056 * 2057 * @param horizontalSpacing The amount of horizontal space between items, 2058 * in pixels. 2059 * 2060 * @attr ref android.R.styleable#GridView_horizontalSpacing 2061 */ 2062 public void setHorizontalSpacing(int horizontalSpacing) { 2063 if (horizontalSpacing != mRequestedHorizontalSpacing) { 2064 mRequestedHorizontalSpacing = horizontalSpacing; 2065 requestLayoutIfNecessary(); 2066 } 2067 } 2068 2069 /** 2070 * Returns the amount of horizontal spacing currently used between each item in the grid. 2071 * 2072 * <p>This is only accurate for the current layout. If {@link #setHorizontalSpacing(int)} 2073 * has been called but layout is not yet complete, this method may return a stale value. 2074 * To get the horizontal spacing that was explicitly requested use 2075 * {@link #getRequestedHorizontalSpacing()}.</p> 2076 * 2077 * @return Current horizontal spacing between each item in pixels 2078 * 2079 * @see #setHorizontalSpacing(int) 2080 * @see #getRequestedHorizontalSpacing() 2081 * 2082 * @attr ref android.R.styleable#GridView_horizontalSpacing 2083 */ 2084 public int getHorizontalSpacing() { 2085 return mHorizontalSpacing; 2086 } 2087 2088 /** 2089 * Returns the requested amount of horizontal spacing between each item in the grid. 2090 * 2091 * <p>The value returned may have been supplied during inflation as part of a style, 2092 * the default GridView style, or by a call to {@link #setHorizontalSpacing(int)}. 2093 * If layout is not yet complete or if GridView calculated a different horizontal spacing 2094 * from what was requested, this may return a different value from 2095 * {@link #getHorizontalSpacing()}.</p> 2096 * 2097 * @return The currently requested horizontal spacing between items, in pixels 2098 * 2099 * @see #setHorizontalSpacing(int) 2100 * @see #getHorizontalSpacing() 2101 * 2102 * @attr ref android.R.styleable#GridView_horizontalSpacing 2103 */ 2104 public int getRequestedHorizontalSpacing() { 2105 return mRequestedHorizontalSpacing; 2106 } 2107 2108 /** 2109 * Set the amount of vertical (y) spacing to place between each item 2110 * in the grid. 2111 * 2112 * @param verticalSpacing The amount of vertical space between items, 2113 * in pixels. 2114 * 2115 * @see #getVerticalSpacing() 2116 * 2117 * @attr ref android.R.styleable#GridView_verticalSpacing 2118 */ 2119 public void setVerticalSpacing(int verticalSpacing) { 2120 if (verticalSpacing != mVerticalSpacing) { 2121 mVerticalSpacing = verticalSpacing; 2122 requestLayoutIfNecessary(); 2123 } 2124 } 2125 2126 /** 2127 * Returns the amount of vertical spacing between each item in the grid. 2128 * 2129 * @return The vertical spacing between items in pixels 2130 * 2131 * @see #setVerticalSpacing(int) 2132 * 2133 * @attr ref android.R.styleable#GridView_verticalSpacing 2134 */ 2135 public int getVerticalSpacing() { 2136 return mVerticalSpacing; 2137 } 2138 2139 /** 2140 * Control how items are stretched to fill their space. 2141 * 2142 * @param stretchMode Either {@link #NO_STRETCH}, 2143 * {@link #STRETCH_SPACING}, {@link #STRETCH_SPACING_UNIFORM}, or {@link #STRETCH_COLUMN_WIDTH}. 2144 * 2145 * @attr ref android.R.styleable#GridView_stretchMode 2146 */ 2147 public void setStretchMode(@StretchMode int stretchMode) { 2148 if (stretchMode != mStretchMode) { 2149 mStretchMode = stretchMode; 2150 requestLayoutIfNecessary(); 2151 } 2152 } 2153 2154 @StretchMode 2155 public int getStretchMode() { 2156 return mStretchMode; 2157 } 2158 2159 /** 2160 * Set the width of columns in the grid. 2161 * 2162 * @param columnWidth The column width, in pixels. 2163 * 2164 * @attr ref android.R.styleable#GridView_columnWidth 2165 */ 2166 public void setColumnWidth(int columnWidth) { 2167 if (columnWidth != mRequestedColumnWidth) { 2168 mRequestedColumnWidth = columnWidth; 2169 requestLayoutIfNecessary(); 2170 } 2171 } 2172 2173 /** 2174 * Return the width of a column in the grid. 2175 * 2176 * <p>This may not be valid yet if a layout is pending.</p> 2177 * 2178 * @return The column width in pixels 2179 * 2180 * @see #setColumnWidth(int) 2181 * @see #getRequestedColumnWidth() 2182 * 2183 * @attr ref android.R.styleable#GridView_columnWidth 2184 */ 2185 public int getColumnWidth() { 2186 return mColumnWidth; 2187 } 2188 2189 /** 2190 * Return the requested width of a column in the grid. 2191 * 2192 * <p>This may not be the actual column width used. Use {@link #getColumnWidth()} 2193 * to retrieve the current real width of a column.</p> 2194 * 2195 * @return The requested column width in pixels 2196 * 2197 * @see #setColumnWidth(int) 2198 * @see #getColumnWidth() 2199 * 2200 * @attr ref android.R.styleable#GridView_columnWidth 2201 */ 2202 public int getRequestedColumnWidth() { 2203 return mRequestedColumnWidth; 2204 } 2205 2206 /** 2207 * Set the number of columns in the grid 2208 * 2209 * @param numColumns The desired number of columns. 2210 * 2211 * @attr ref android.R.styleable#GridView_numColumns 2212 */ 2213 public void setNumColumns(int numColumns) { 2214 if (numColumns != mRequestedNumColumns) { 2215 mRequestedNumColumns = numColumns; 2216 requestLayoutIfNecessary(); 2217 } 2218 } 2219 2220 /** 2221 * Get the number of columns in the grid. 2222 * Returns {@link #AUTO_FIT} if the Grid has never been laid out. 2223 * 2224 * @attr ref android.R.styleable#GridView_numColumns 2225 * 2226 * @see #setNumColumns(int) 2227 */ 2228 @ViewDebug.ExportedProperty 2229 public int getNumColumns() { 2230 return mNumColumns; 2231 } 2232 2233 /** 2234 * Make sure views are touching the top or bottom edge, as appropriate for 2235 * our gravity 2236 */ 2237 private void adjustViewsUpOrDown() { 2238 final int childCount = getChildCount(); 2239 2240 if (childCount > 0) { 2241 int delta; 2242 View child; 2243 2244 if (!mStackFromBottom) { 2245 // Uh-oh -- we came up short. Slide all views up to make them 2246 // align with the top 2247 child = getChildAt(0); 2248 delta = child.getTop() - mListPadding.top; 2249 if (mFirstPosition != 0) { 2250 // It's OK to have some space above the first item if it is 2251 // part of the vertical spacing 2252 delta -= mVerticalSpacing; 2253 } 2254 if (delta < 0) { 2255 // We only are looking to see if we are too low, not too high 2256 delta = 0; 2257 } 2258 } else { 2259 // we are too high, slide all views down to align with bottom 2260 child = getChildAt(childCount - 1); 2261 delta = child.getBottom() - (getHeight() - mListPadding.bottom); 2262 2263 if (mFirstPosition + childCount < mItemCount) { 2264 // It's OK to have some space below the last item if it is 2265 // part of the vertical spacing 2266 delta += mVerticalSpacing; 2267 } 2268 2269 if (delta > 0) { 2270 // We only are looking to see if we are too high, not too low 2271 delta = 0; 2272 } 2273 } 2274 2275 if (delta != 0) { 2276 offsetChildrenTopAndBottom(-delta); 2277 } 2278 } 2279 } 2280 2281 @Override 2282 protected int computeVerticalScrollExtent() { 2283 final int count = getChildCount(); 2284 if (count > 0) { 2285 final int numColumns = mNumColumns; 2286 final int rowCount = (count + numColumns - 1) / numColumns; 2287 2288 int extent = rowCount * 100; 2289 2290 View view = getChildAt(0); 2291 final int top = view.getTop(); 2292 int height = view.getHeight(); 2293 if (height > 0) { 2294 extent += (top * 100) / height; 2295 } 2296 2297 view = getChildAt(count - 1); 2298 final int bottom = view.getBottom(); 2299 height = view.getHeight(); 2300 if (height > 0) { 2301 extent -= ((bottom - getHeight()) * 100) / height; 2302 } 2303 2304 return extent; 2305 } 2306 return 0; 2307 } 2308 2309 @Override 2310 protected int computeVerticalScrollOffset() { 2311 if (mFirstPosition >= 0 && getChildCount() > 0) { 2312 final View view = getChildAt(0); 2313 final int top = view.getTop(); 2314 int height = view.getHeight(); 2315 if (height > 0) { 2316 final int numColumns = mNumColumns; 2317 final int rowCount = (mItemCount + numColumns - 1) / numColumns; 2318 // In case of stackFromBottom the calculation of whichRow needs 2319 // to take into account that counting from the top the first row 2320 // might not be entirely filled. 2321 final int oddItemsOnFirstRow = isStackFromBottom() ? ((rowCount * numColumns) - 2322 mItemCount) : 0; 2323 final int whichRow = (mFirstPosition + oddItemsOnFirstRow) / numColumns; 2324 return Math.max(whichRow * 100 - (top * 100) / height + 2325 (int) ((float) mScrollY / getHeight() * rowCount * 100), 0); 2326 } 2327 } 2328 return 0; 2329 } 2330 2331 @Override 2332 protected int computeVerticalScrollRange() { 2333 // TODO: Account for vertical spacing too 2334 final int numColumns = mNumColumns; 2335 final int rowCount = (mItemCount + numColumns - 1) / numColumns; 2336 int result = Math.max(rowCount * 100, 0); 2337 if (mScrollY != 0) { 2338 // Compensate for overscroll 2339 result += Math.abs((int) ((float) mScrollY / getHeight() * rowCount * 100)); 2340 } 2341 return result; 2342 } 2343 2344 @Override 2345 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 2346 super.onInitializeAccessibilityEvent(event); 2347 event.setClassName(GridView.class.getName()); 2348 } 2349 2350 @Override 2351 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 2352 super.onInitializeAccessibilityNodeInfo(info); 2353 info.setClassName(GridView.class.getName()); 2354 2355 final int columnsCount = getNumColumns(); 2356 final int rowsCount = getCount() / columnsCount; 2357 final int selectionMode = getSelectionModeForAccessibility(); 2358 final CollectionInfo collectionInfo = CollectionInfo.obtain( 2359 rowsCount, columnsCount, false, selectionMode); 2360 info.setCollectionInfo(collectionInfo); 2361 } 2362 2363 @Override 2364 public void onInitializeAccessibilityNodeInfoForItem( 2365 View view, int position, AccessibilityNodeInfo info) { 2366 super.onInitializeAccessibilityNodeInfoForItem(view, position, info); 2367 2368 final int count = getCount(); 2369 final int columnsCount = getNumColumns(); 2370 final int rowsCount = count / columnsCount; 2371 2372 final int row; 2373 final int column; 2374 if (!mStackFromBottom) { 2375 column = position % columnsCount; 2376 row = position / columnsCount; 2377 } else { 2378 final int invertedIndex = count - 1 - position; 2379 2380 column = columnsCount - 1 - (invertedIndex % columnsCount); 2381 row = rowsCount - 1 - invertedIndex / columnsCount; 2382 } 2383 2384 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2385 final boolean isHeading = lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER; 2386 final boolean isSelected = isItemChecked(position); 2387 final CollectionItemInfo itemInfo = CollectionItemInfo.obtain( 2388 row, 1, column, 1, isHeading, isSelected); 2389 info.setCollectionItemInfo(itemInfo); 2390 } 2391 } 2392