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