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.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 136 public GridView(Context context) { 137 this(context, null); 138 } 139 140 public GridView(Context context, AttributeSet attrs) { 141 this(context, attrs, R.attr.gridViewStyle); 142 } 143 144 public GridView(Context context, AttributeSet attrs, int defStyleAttr) { 145 this(context, attrs, defStyleAttr, 0); 146 } 147 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 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") 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 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 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 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) 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 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) 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 */ 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 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 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 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 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 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 */ 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 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 */ 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 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 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 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 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 */ 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 */ 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 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 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 1648 public boolean onKeyDown(int keyCode, KeyEvent event) { 1649 return commonKey(keyCode, 1, event); 1650 } 1651 1652 @Override 1653 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { 1654 return commonKey(keyCode, repeatCount, event); 1655 } 1656 1657 @Override 1658 public boolean onKeyUp(int keyCode, KeyEvent event) { 1659 return commonKey(keyCode, 1, event); 1660 } 1661 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 */ 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 */ 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 */ 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 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 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 */ 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 */ 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) 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 */ 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 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 */ 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 */ 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 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 */ 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 }) 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 */ 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 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 */ 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 */ 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 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 */ 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 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 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 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 2375 public CharSequence getAccessibilityClassName() { 2376 return GridView.class.getName(); 2377 } 2378 2379 /** @hide */ 2380 @Override 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 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 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 2453 protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) { 2454 super.encodeProperties(encoder); 2455 encoder.addProperty("numColumns", getNumColumns()); 2456 } 2457 } 2458