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