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