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