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