1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.widget; 18 19 import android.annotation.IntDef; 20 import android.annotation.NonNull; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.res.TypedArray; 24 import android.graphics.Rect; 25 import android.os.Bundle; 26 import android.os.Trace; 27 import android.util.AttributeSet; 28 import android.util.MathUtils; 29 import android.view.Gravity; 30 import android.view.KeyEvent; 31 import android.view.SoundEffectConstants; 32 import android.view.View; 33 import android.view.ViewDebug; 34 import android.view.ViewGroup; 35 import android.view.ViewHierarchyEncoder; 36 import android.view.ViewRootImpl; 37 import android.view.accessibility.AccessibilityNodeInfo; 38 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 39 import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; 40 import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo; 41 import android.view.accessibility.AccessibilityNodeProvider; 42 import android.view.animation.GridLayoutAnimationController; 43 import android.widget.RemoteViews.RemoteView; 44 45 import com.android.internal.R; 46 47 import java.lang.annotation.Retention; 48 import java.lang.annotation.RetentionPolicy; 49 50 51 /** 52 * A view that shows items in two-dimensional scrolling grid. The items in the 53 * grid come from the {@link ListAdapter} associated with this view. 54 * 55 * <p>See the <a href="{@docRoot}guide/topics/ui/layout/gridview.html">Grid 56 * View</a> guide.</p> 57 * 58 * @attr ref android.R.styleable#GridView_horizontalSpacing 59 * @attr ref android.R.styleable#GridView_verticalSpacing 60 * @attr ref android.R.styleable#GridView_stretchMode 61 * @attr ref android.R.styleable#GridView_columnWidth 62 * @attr ref android.R.styleable#GridView_numColumns 63 * @attr ref android.R.styleable#GridView_gravity 64 */ 65 @RemoteView 66 public class GridView extends AbsListView { 67 /** @hide */ 68 @IntDef({NO_STRETCH, STRETCH_SPACING, STRETCH_COLUMN_WIDTH, STRETCH_SPACING_UNIFORM}) 69 @Retention(RetentionPolicy.SOURCE) 70 public @interface StretchMode {} 71 72 /** 73 * Disables stretching. 74 * 75 * @see #setStretchMode(int) 76 */ 77 public static final int NO_STRETCH = 0; 78 /** 79 * Stretches the spacing between columns. 80 * 81 * @see #setStretchMode(int) 82 */ 83 public static final int STRETCH_SPACING = 1; 84 /** 85 * Stretches columns. 86 * 87 * @see #setStretchMode(int) 88 */ 89 public static final int STRETCH_COLUMN_WIDTH = 2; 90 /** 91 * Stretches the spacing between columns. The spacing is uniform. 92 * 93 * @see #setStretchMode(int) 94 */ 95 public static final int STRETCH_SPACING_UNIFORM = 3; 96 97 /** 98 * Creates as many columns as can fit on screen. 99 * 100 * @see #setNumColumns(int) 101 */ 102 public static final int AUTO_FIT = -1; 103 104 private int mNumColumns = AUTO_FIT; 105 106 private int mHorizontalSpacing = 0; 107 private int mRequestedHorizontalSpacing; 108 private int mVerticalSpacing = 0; 109 private int mStretchMode = STRETCH_COLUMN_WIDTH; 110 private int mColumnWidth; 111 private int mRequestedColumnWidth; 112 private int mRequestedNumColumns; 113 114 private View mReferenceView = null; 115 private View mReferenceViewInSelectedRow = null; 116 117 private int mGravity = Gravity.START; 118 119 private final Rect mTempRect = new Rect(); 120 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(asyncImpl="setRemoteViewsAdapterAsync") 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 * Obtains the view and adds it to our list of children. The view can be 1411 * made fresh, converted from an unused view, or used as is if it was in 1412 * the recycle bin. 1413 * 1414 * @param position logical position in the list 1415 * @param y top or bottom edge of the view to add 1416 * @param flow {@code true} to align top edge to y, {@code false} to align 1417 * bottom edge to y 1418 * @param childrenLeft left edge where children should be positioned 1419 * @param selected {@code true} if the position is selected, {@code false} 1420 * otherwise 1421 * @param where position at which to add new item in the list 1422 * @return View that was added 1423 */ makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected, int where)1424 private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, 1425 boolean selected, int where) { 1426 if (!mDataChanged) { 1427 // Try to use an existing view for this position 1428 final View activeView = mRecycler.getActiveView(position); 1429 if (activeView != null) { 1430 // Found it -- we're using an existing child 1431 // This just needs to be positioned 1432 setupChild(activeView, position, y, flow, childrenLeft, selected, true, where); 1433 return activeView; 1434 } 1435 } 1436 1437 // Make a new view for this position, or convert an unused view if 1438 // possible. 1439 final View child = obtainView(position, mIsScrap); 1440 1441 // This needs to be positioned and measured. 1442 setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0], where); 1443 1444 return child; 1445 } 1446 1447 /** 1448 * Adds a view as a child and make sure it is measured (if necessary) and 1449 * positioned properly. 1450 * 1451 * @param child the view to add 1452 * @param position the position of this child 1453 * @param y the y position relative to which this view will be positioned 1454 * @param flowDown {@code true} to align top edge to y, {@code false} to 1455 * align bottom edge to y 1456 * @param childrenLeft left edge where children should be positioned 1457 * @param selected {@code true} if the position is selected, {@code false} 1458 * otherwise 1459 * @param isAttachedToWindow {@code true} if the view is already attached 1460 * to the window, e.g. whether it was reused, or 1461 * {@code false} otherwise 1462 * @param where position at which to add new item in the list 1463 * 1464 */ setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean isAttachedToWindow, int where)1465 private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, 1466 boolean selected, boolean isAttachedToWindow, int where) { 1467 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupGridItem"); 1468 1469 boolean isSelected = selected && shouldShowSelector(); 1470 final boolean updateChildSelected = isSelected != child.isSelected(); 1471 final int mode = mTouchMode; 1472 final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL 1473 && mMotionPosition == position; 1474 final boolean updateChildPressed = isPressed != child.isPressed(); 1475 final boolean needToMeasure = !isAttachedToWindow || updateChildSelected 1476 || child.isLayoutRequested(); 1477 1478 // Respect layout params that are already in the view. Otherwise make 1479 // some up... 1480 AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); 1481 if (p == null) { 1482 p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); 1483 } 1484 p.viewType = mAdapter.getItemViewType(position); 1485 p.isEnabled = mAdapter.isEnabled(position); 1486 1487 // Set up view state before attaching the view, since we may need to 1488 // rely on the jumpDrawablesToCurrentState() call that occurs as part 1489 // of view attachment. 1490 if (updateChildSelected) { 1491 child.setSelected(isSelected); 1492 if (isSelected) { 1493 requestFocus(); 1494 } 1495 } 1496 1497 if (updateChildPressed) { 1498 child.setPressed(isPressed); 1499 } 1500 1501 if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { 1502 if (child instanceof Checkable) { 1503 ((Checkable) child).setChecked(mCheckStates.get(position)); 1504 } else if (getContext().getApplicationInfo().targetSdkVersion 1505 >= android.os.Build.VERSION_CODES.HONEYCOMB) { 1506 child.setActivated(mCheckStates.get(position)); 1507 } 1508 } 1509 1510 if (isAttachedToWindow && !p.forceAdd) { 1511 attachViewToParent(child, where, p); 1512 1513 // If the view isn't attached, or if it's attached but for a different 1514 // position, then jump the drawables. 1515 if (!isAttachedToWindow 1516 || (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition) 1517 != position) { 1518 child.jumpDrawablesToCurrentState(); 1519 } 1520 } else { 1521 p.forceAdd = false; 1522 addViewInLayout(child, where, p, true); 1523 } 1524 1525 if (needToMeasure) { 1526 int childHeightSpec = ViewGroup.getChildMeasureSpec( 1527 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); 1528 1529 int childWidthSpec = ViewGroup.getChildMeasureSpec( 1530 MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); 1531 child.measure(childWidthSpec, childHeightSpec); 1532 } else { 1533 cleanupLayoutState(child); 1534 } 1535 1536 final int w = child.getMeasuredWidth(); 1537 final int h = child.getMeasuredHeight(); 1538 1539 int childLeft; 1540 final int childTop = flowDown ? y : y - h; 1541 1542 final int layoutDirection = getLayoutDirection(); 1543 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); 1544 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 1545 case Gravity.LEFT: 1546 childLeft = childrenLeft; 1547 break; 1548 case Gravity.CENTER_HORIZONTAL: 1549 childLeft = childrenLeft + ((mColumnWidth - w) / 2); 1550 break; 1551 case Gravity.RIGHT: 1552 childLeft = childrenLeft + mColumnWidth - w; 1553 break; 1554 default: 1555 childLeft = childrenLeft; 1556 break; 1557 } 1558 1559 if (needToMeasure) { 1560 final int childRight = childLeft + w; 1561 final int childBottom = childTop + h; 1562 child.layout(childLeft, childTop, childRight, childBottom); 1563 } else { 1564 child.offsetLeftAndRight(childLeft - child.getLeft()); 1565 child.offsetTopAndBottom(childTop - child.getTop()); 1566 } 1567 1568 if (mCachingStarted && !child.isDrawingCacheEnabled()) { 1569 child.setDrawingCacheEnabled(true); 1570 } 1571 1572 Trace.traceEnd(Trace.TRACE_TAG_VIEW); 1573 } 1574 1575 /** 1576 * Sets the currently selected item 1577 * 1578 * @param position Index (starting at 0) of the data item to be selected. 1579 * 1580 * If in touch mode, the item will not be selected but it will still be positioned 1581 * appropriately. 1582 */ 1583 @Override setSelection(int position)1584 public void setSelection(int position) { 1585 if (!isInTouchMode()) { 1586 setNextSelectedPositionInt(position); 1587 } else { 1588 mResurrectToPosition = position; 1589 } 1590 mLayoutMode = LAYOUT_SET_SELECTION; 1591 if (mPositionScroller != null) { 1592 mPositionScroller.stop(); 1593 } 1594 requestLayout(); 1595 } 1596 1597 /** 1598 * Makes the item at the supplied position selected. 1599 * 1600 * @param position the position of the new selection 1601 */ 1602 @Override setSelectionInt(int position)1603 void setSelectionInt(int position) { 1604 int previousSelectedPosition = mNextSelectedPosition; 1605 1606 if (mPositionScroller != null) { 1607 mPositionScroller.stop(); 1608 } 1609 1610 setNextSelectedPositionInt(position); 1611 layoutChildren(); 1612 1613 final int next = mStackFromBottom ? mItemCount - 1 - mNextSelectedPosition : 1614 mNextSelectedPosition; 1615 final int previous = mStackFromBottom ? mItemCount - 1 1616 - previousSelectedPosition : previousSelectedPosition; 1617 1618 final int nextRow = next / mNumColumns; 1619 final int previousRow = previous / mNumColumns; 1620 1621 if (nextRow != previousRow) { 1622 awakenScrollBars(); 1623 } 1624 1625 } 1626 1627 @Override onKeyDown(int keyCode, KeyEvent event)1628 public boolean onKeyDown(int keyCode, KeyEvent event) { 1629 return commonKey(keyCode, 1, event); 1630 } 1631 1632 @Override onKeyMultiple(int keyCode, int repeatCount, KeyEvent event)1633 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { 1634 return commonKey(keyCode, repeatCount, event); 1635 } 1636 1637 @Override onKeyUp(int keyCode, KeyEvent event)1638 public boolean onKeyUp(int keyCode, KeyEvent event) { 1639 return commonKey(keyCode, 1, event); 1640 } 1641 commonKey(int keyCode, int count, KeyEvent event)1642 private boolean commonKey(int keyCode, int count, KeyEvent event) { 1643 if (mAdapter == null) { 1644 return false; 1645 } 1646 1647 if (mDataChanged) { 1648 layoutChildren(); 1649 } 1650 1651 boolean handled = false; 1652 int action = event.getAction(); 1653 if (KeyEvent.isConfirmKey(keyCode) 1654 && event.hasNoModifiers() && action != KeyEvent.ACTION_UP) { 1655 handled = resurrectSelectionIfNeeded(); 1656 if (!handled && event.getRepeatCount() == 0 && getChildCount() > 0) { 1657 keyPressed(); 1658 handled = true; 1659 } 1660 } 1661 1662 if (!handled && action != KeyEvent.ACTION_UP) { 1663 switch (keyCode) { 1664 case KeyEvent.KEYCODE_DPAD_LEFT: 1665 if (event.hasNoModifiers()) { 1666 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_LEFT); 1667 } 1668 break; 1669 1670 case KeyEvent.KEYCODE_DPAD_RIGHT: 1671 if (event.hasNoModifiers()) { 1672 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_RIGHT); 1673 } 1674 break; 1675 1676 case KeyEvent.KEYCODE_DPAD_UP: 1677 if (event.hasNoModifiers()) { 1678 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_UP); 1679 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1680 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1681 } 1682 break; 1683 1684 case KeyEvent.KEYCODE_DPAD_DOWN: 1685 if (event.hasNoModifiers()) { 1686 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_DOWN); 1687 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1688 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1689 } 1690 break; 1691 1692 case KeyEvent.KEYCODE_PAGE_UP: 1693 if (event.hasNoModifiers()) { 1694 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP); 1695 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1696 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1697 } 1698 break; 1699 1700 case KeyEvent.KEYCODE_PAGE_DOWN: 1701 if (event.hasNoModifiers()) { 1702 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN); 1703 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1704 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1705 } 1706 break; 1707 1708 case KeyEvent.KEYCODE_MOVE_HOME: 1709 if (event.hasNoModifiers()) { 1710 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1711 } 1712 break; 1713 1714 case KeyEvent.KEYCODE_MOVE_END: 1715 if (event.hasNoModifiers()) { 1716 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1717 } 1718 break; 1719 1720 case KeyEvent.KEYCODE_TAB: 1721 // TODO: Sometimes it is useful to be able to TAB through the items in 1722 // a GridView sequentially. Unfortunately this can create an 1723 // asymmetry in TAB navigation order unless the list selection 1724 // always reverts to the top or bottom when receiving TAB focus from 1725 // another widget. 1726 if (event.hasNoModifiers()) { 1727 handled = resurrectSelectionIfNeeded() 1728 || sequenceScroll(FOCUS_FORWARD); 1729 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { 1730 handled = resurrectSelectionIfNeeded() 1731 || sequenceScroll(FOCUS_BACKWARD); 1732 } 1733 break; 1734 } 1735 } 1736 1737 if (handled) { 1738 return true; 1739 } 1740 1741 if (sendToTextFilter(keyCode, count, event)) { 1742 return true; 1743 } 1744 1745 switch (action) { 1746 case KeyEvent.ACTION_DOWN: 1747 return super.onKeyDown(keyCode, event); 1748 case KeyEvent.ACTION_UP: 1749 return super.onKeyUp(keyCode, event); 1750 case KeyEvent.ACTION_MULTIPLE: 1751 return super.onKeyMultiple(keyCode, count, event); 1752 default: 1753 return false; 1754 } 1755 } 1756 1757 /** 1758 * Scrolls up or down by the number of items currently present on screen. 1759 * 1760 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 1761 * @return whether selection was moved 1762 */ pageScroll(int direction)1763 boolean pageScroll(int direction) { 1764 int nextPage = -1; 1765 1766 if (direction == FOCUS_UP) { 1767 nextPage = Math.max(0, mSelectedPosition - getChildCount()); 1768 } else if (direction == FOCUS_DOWN) { 1769 nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount()); 1770 } 1771 1772 if (nextPage >= 0) { 1773 setSelectionInt(nextPage); 1774 invokeOnItemScrollListener(); 1775 awakenScrollBars(); 1776 return true; 1777 } 1778 1779 return false; 1780 } 1781 1782 /** 1783 * Go to the last or first item if possible. 1784 * 1785 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}. 1786 * 1787 * @return Whether selection was moved. 1788 */ fullScroll(int direction)1789 boolean fullScroll(int direction) { 1790 boolean moved = false; 1791 if (direction == FOCUS_UP) { 1792 mLayoutMode = LAYOUT_SET_SELECTION; 1793 setSelectionInt(0); 1794 invokeOnItemScrollListener(); 1795 moved = true; 1796 } else if (direction == FOCUS_DOWN) { 1797 mLayoutMode = LAYOUT_SET_SELECTION; 1798 setSelectionInt(mItemCount - 1); 1799 invokeOnItemScrollListener(); 1800 moved = true; 1801 } 1802 1803 if (moved) { 1804 awakenScrollBars(); 1805 } 1806 1807 return moved; 1808 } 1809 1810 /** 1811 * Scrolls to the next or previous item, horizontally or vertically. 1812 * 1813 * @param direction either {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, 1814 * {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 1815 * 1816 * @return whether selection was moved 1817 */ arrowScroll(int direction)1818 boolean arrowScroll(int direction) { 1819 final int selectedPosition = mSelectedPosition; 1820 final int numColumns = mNumColumns; 1821 1822 int startOfRowPos; 1823 int endOfRowPos; 1824 1825 boolean moved = false; 1826 1827 if (!mStackFromBottom) { 1828 startOfRowPos = (selectedPosition / numColumns) * numColumns; 1829 endOfRowPos = Math.min(startOfRowPos + numColumns - 1, mItemCount - 1); 1830 } else { 1831 final int invertedSelection = mItemCount - 1 - selectedPosition; 1832 endOfRowPos = mItemCount - 1 - (invertedSelection / numColumns) * numColumns; 1833 startOfRowPos = Math.max(0, endOfRowPos - numColumns + 1); 1834 } 1835 1836 switch (direction) { 1837 case FOCUS_UP: 1838 if (startOfRowPos > 0) { 1839 mLayoutMode = LAYOUT_MOVE_SELECTION; 1840 setSelectionInt(Math.max(0, selectedPosition - numColumns)); 1841 moved = true; 1842 } 1843 break; 1844 case FOCUS_DOWN: 1845 if (endOfRowPos < mItemCount - 1) { 1846 mLayoutMode = LAYOUT_MOVE_SELECTION; 1847 setSelectionInt(Math.min(selectedPosition + numColumns, mItemCount - 1)); 1848 moved = true; 1849 } 1850 break; 1851 } 1852 1853 final boolean isLayoutRtl = isLayoutRtl(); 1854 if (selectedPosition > startOfRowPos && ((direction == FOCUS_LEFT && !isLayoutRtl) || 1855 (direction == FOCUS_RIGHT && isLayoutRtl))) { 1856 mLayoutMode = LAYOUT_MOVE_SELECTION; 1857 setSelectionInt(Math.max(0, selectedPosition - 1)); 1858 moved = true; 1859 } else if (selectedPosition < endOfRowPos && ((direction == FOCUS_LEFT && isLayoutRtl) || 1860 (direction == FOCUS_RIGHT && !isLayoutRtl))) { 1861 mLayoutMode = LAYOUT_MOVE_SELECTION; 1862 setSelectionInt(Math.min(selectedPosition + 1, mItemCount - 1)); 1863 moved = true; 1864 } 1865 1866 if (moved) { 1867 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 1868 invokeOnItemScrollListener(); 1869 } 1870 1871 if (moved) { 1872 awakenScrollBars(); 1873 } 1874 1875 return moved; 1876 } 1877 1878 /** 1879 * Goes to the next or previous item according to the order set by the 1880 * adapter. 1881 */ sequenceScroll(int direction)1882 boolean sequenceScroll(int direction) { 1883 int selectedPosition = mSelectedPosition; 1884 int numColumns = mNumColumns; 1885 int count = mItemCount; 1886 1887 int startOfRow; 1888 int endOfRow; 1889 if (!mStackFromBottom) { 1890 startOfRow = (selectedPosition / numColumns) * numColumns; 1891 endOfRow = Math.min(startOfRow + numColumns - 1, count - 1); 1892 } else { 1893 int invertedSelection = count - 1 - selectedPosition; 1894 endOfRow = count - 1 - (invertedSelection / numColumns) * numColumns; 1895 startOfRow = Math.max(0, endOfRow - numColumns + 1); 1896 } 1897 1898 boolean moved = false; 1899 boolean showScroll = false; 1900 switch (direction) { 1901 case FOCUS_FORWARD: 1902 if (selectedPosition < count - 1) { 1903 // Move to the next item. 1904 mLayoutMode = LAYOUT_MOVE_SELECTION; 1905 setSelectionInt(selectedPosition + 1); 1906 moved = true; 1907 // Show the scrollbar only if changing rows. 1908 showScroll = selectedPosition == endOfRow; 1909 } 1910 break; 1911 1912 case FOCUS_BACKWARD: 1913 if (selectedPosition > 0) { 1914 // Move to the previous item. 1915 mLayoutMode = LAYOUT_MOVE_SELECTION; 1916 setSelectionInt(selectedPosition - 1); 1917 moved = true; 1918 // Show the scrollbar only if changing rows. 1919 showScroll = selectedPosition == startOfRow; 1920 } 1921 break; 1922 } 1923 1924 if (moved) { 1925 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 1926 invokeOnItemScrollListener(); 1927 } 1928 1929 if (showScroll) { 1930 awakenScrollBars(); 1931 } 1932 1933 return moved; 1934 } 1935 1936 @Override onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)1937 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 1938 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 1939 1940 int closestChildIndex = -1; 1941 if (gainFocus && previouslyFocusedRect != null) { 1942 previouslyFocusedRect.offset(mScrollX, mScrollY); 1943 1944 // figure out which item should be selected based on previously 1945 // focused rect 1946 Rect otherRect = mTempRect; 1947 int minDistance = Integer.MAX_VALUE; 1948 final int childCount = getChildCount(); 1949 for (int i = 0; i < childCount; i++) { 1950 // only consider view's on appropriate edge of grid 1951 if (!isCandidateSelection(i, direction)) { 1952 continue; 1953 } 1954 1955 final View other = getChildAt(i); 1956 other.getDrawingRect(otherRect); 1957 offsetDescendantRectToMyCoords(other, otherRect); 1958 int distance = getDistance(previouslyFocusedRect, otherRect, direction); 1959 1960 if (distance < minDistance) { 1961 minDistance = distance; 1962 closestChildIndex = i; 1963 } 1964 } 1965 } 1966 1967 if (closestChildIndex >= 0) { 1968 setSelection(closestChildIndex + mFirstPosition); 1969 } else { 1970 requestLayout(); 1971 } 1972 } 1973 1974 /** 1975 * Is childIndex a candidate for next focus given the direction the focus 1976 * change is coming from? 1977 * @param childIndex The index to check. 1978 * @param direction The direction, one of 1979 * {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD, FOCUS_BACKWARD} 1980 * @return Whether childIndex is a candidate. 1981 */ isCandidateSelection(int childIndex, int direction)1982 private boolean isCandidateSelection(int childIndex, int direction) { 1983 final int count = getChildCount(); 1984 final int invertedIndex = count - 1 - childIndex; 1985 1986 int rowStart; 1987 int rowEnd; 1988 1989 if (!mStackFromBottom) { 1990 rowStart = childIndex - (childIndex % mNumColumns); 1991 rowEnd = Math.min(rowStart + mNumColumns - 1, count); 1992 } else { 1993 rowEnd = count - 1 - (invertedIndex - (invertedIndex % mNumColumns)); 1994 rowStart = Math.max(0, rowEnd - mNumColumns + 1); 1995 } 1996 1997 switch (direction) { 1998 case View.FOCUS_RIGHT: 1999 // coming from left, selection is only valid if it is on left 2000 // edge 2001 return childIndex == rowStart; 2002 case View.FOCUS_DOWN: 2003 // coming from top; only valid if in top row 2004 return rowStart == 0; 2005 case View.FOCUS_LEFT: 2006 // coming from right, must be on right edge 2007 return childIndex == rowEnd; 2008 case View.FOCUS_UP: 2009 // coming from bottom, need to be in last row 2010 return rowEnd == count - 1; 2011 case View.FOCUS_FORWARD: 2012 // coming from top-left, need to be first in top row 2013 return childIndex == rowStart && rowStart == 0; 2014 case View.FOCUS_BACKWARD: 2015 // coming from bottom-right, need to be last in bottom row 2016 return childIndex == rowEnd && rowEnd == count - 1; 2017 default: 2018 throw new IllegalArgumentException("direction must be one of " 2019 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, " 2020 + "FOCUS_FORWARD, FOCUS_BACKWARD}."); 2021 } 2022 } 2023 2024 /** 2025 * Set the gravity for this grid. Gravity describes how the child views 2026 * are horizontally aligned. Defaults to Gravity.LEFT 2027 * 2028 * @param gravity the gravity to apply to this grid's children 2029 * 2030 * @attr ref android.R.styleable#GridView_gravity 2031 */ setGravity(int gravity)2032 public void setGravity(int gravity) { 2033 if (mGravity != gravity) { 2034 mGravity = gravity; 2035 requestLayoutIfNecessary(); 2036 } 2037 } 2038 2039 /** 2040 * Describes how the child views are horizontally aligned. Defaults to Gravity.LEFT 2041 * 2042 * @return the gravity that will be applied to this grid's children 2043 * 2044 * @attr ref android.R.styleable#GridView_gravity 2045 */ getGravity()2046 public int getGravity() { 2047 return mGravity; 2048 } 2049 2050 /** 2051 * Set the amount of horizontal (x) spacing to place between each item 2052 * in the grid. 2053 * 2054 * @param horizontalSpacing The amount of horizontal space between items, 2055 * in pixels. 2056 * 2057 * @attr ref android.R.styleable#GridView_horizontalSpacing 2058 */ setHorizontalSpacing(int horizontalSpacing)2059 public void setHorizontalSpacing(int horizontalSpacing) { 2060 if (horizontalSpacing != mRequestedHorizontalSpacing) { 2061 mRequestedHorizontalSpacing = horizontalSpacing; 2062 requestLayoutIfNecessary(); 2063 } 2064 } 2065 2066 /** 2067 * Returns the amount of horizontal spacing currently used between each item in the grid. 2068 * 2069 * <p>This is only accurate for the current layout. If {@link #setHorizontalSpacing(int)} 2070 * has been called but layout is not yet complete, this method may return a stale value. 2071 * To get the horizontal spacing that was explicitly requested use 2072 * {@link #getRequestedHorizontalSpacing()}.</p> 2073 * 2074 * @return Current horizontal spacing between each item in pixels 2075 * 2076 * @see #setHorizontalSpacing(int) 2077 * @see #getRequestedHorizontalSpacing() 2078 * 2079 * @attr ref android.R.styleable#GridView_horizontalSpacing 2080 */ getHorizontalSpacing()2081 public int getHorizontalSpacing() { 2082 return mHorizontalSpacing; 2083 } 2084 2085 /** 2086 * Returns the requested amount of horizontal spacing between each item in the grid. 2087 * 2088 * <p>The value returned may have been supplied during inflation as part of a style, 2089 * the default GridView style, or by a call to {@link #setHorizontalSpacing(int)}. 2090 * If layout is not yet complete or if GridView calculated a different horizontal spacing 2091 * from what was requested, this may return a different value from 2092 * {@link #getHorizontalSpacing()}.</p> 2093 * 2094 * @return The currently requested horizontal spacing between items, in pixels 2095 * 2096 * @see #setHorizontalSpacing(int) 2097 * @see #getHorizontalSpacing() 2098 * 2099 * @attr ref android.R.styleable#GridView_horizontalSpacing 2100 */ getRequestedHorizontalSpacing()2101 public int getRequestedHorizontalSpacing() { 2102 return mRequestedHorizontalSpacing; 2103 } 2104 2105 /** 2106 * Set the amount of vertical (y) spacing to place between each item 2107 * in the grid. 2108 * 2109 * @param verticalSpacing The amount of vertical space between items, 2110 * in pixels. 2111 * 2112 * @see #getVerticalSpacing() 2113 * 2114 * @attr ref android.R.styleable#GridView_verticalSpacing 2115 */ setVerticalSpacing(int verticalSpacing)2116 public void setVerticalSpacing(int verticalSpacing) { 2117 if (verticalSpacing != mVerticalSpacing) { 2118 mVerticalSpacing = verticalSpacing; 2119 requestLayoutIfNecessary(); 2120 } 2121 } 2122 2123 /** 2124 * Returns the amount of vertical spacing between each item in the grid. 2125 * 2126 * @return The vertical spacing between items in pixels 2127 * 2128 * @see #setVerticalSpacing(int) 2129 * 2130 * @attr ref android.R.styleable#GridView_verticalSpacing 2131 */ getVerticalSpacing()2132 public int getVerticalSpacing() { 2133 return mVerticalSpacing; 2134 } 2135 2136 /** 2137 * Control how items are stretched to fill their space. 2138 * 2139 * @param stretchMode Either {@link #NO_STRETCH}, 2140 * {@link #STRETCH_SPACING}, {@link #STRETCH_SPACING_UNIFORM}, or {@link #STRETCH_COLUMN_WIDTH}. 2141 * 2142 * @attr ref android.R.styleable#GridView_stretchMode 2143 */ setStretchMode(@tretchMode int stretchMode)2144 public void setStretchMode(@StretchMode int stretchMode) { 2145 if (stretchMode != mStretchMode) { 2146 mStretchMode = stretchMode; 2147 requestLayoutIfNecessary(); 2148 } 2149 } 2150 2151 @StretchMode getStretchMode()2152 public int getStretchMode() { 2153 return mStretchMode; 2154 } 2155 2156 /** 2157 * Set the width of columns in the grid. 2158 * 2159 * @param columnWidth The column width, in pixels. 2160 * 2161 * @attr ref android.R.styleable#GridView_columnWidth 2162 */ setColumnWidth(int columnWidth)2163 public void setColumnWidth(int columnWidth) { 2164 if (columnWidth != mRequestedColumnWidth) { 2165 mRequestedColumnWidth = columnWidth; 2166 requestLayoutIfNecessary(); 2167 } 2168 } 2169 2170 /** 2171 * Return the width of a column in the grid. 2172 * 2173 * <p>This may not be valid yet if a layout is pending.</p> 2174 * 2175 * @return The column width in pixels 2176 * 2177 * @see #setColumnWidth(int) 2178 * @see #getRequestedColumnWidth() 2179 * 2180 * @attr ref android.R.styleable#GridView_columnWidth 2181 */ getColumnWidth()2182 public int getColumnWidth() { 2183 return mColumnWidth; 2184 } 2185 2186 /** 2187 * Return the requested width of a column in the grid. 2188 * 2189 * <p>This may not be the actual column width used. Use {@link #getColumnWidth()} 2190 * to retrieve the current real width of a column.</p> 2191 * 2192 * @return The requested column width in pixels 2193 * 2194 * @see #setColumnWidth(int) 2195 * @see #getColumnWidth() 2196 * 2197 * @attr ref android.R.styleable#GridView_columnWidth 2198 */ getRequestedColumnWidth()2199 public int getRequestedColumnWidth() { 2200 return mRequestedColumnWidth; 2201 } 2202 2203 /** 2204 * Set the number of columns in the grid 2205 * 2206 * @param numColumns The desired number of columns. 2207 * 2208 * @attr ref android.R.styleable#GridView_numColumns 2209 */ setNumColumns(int numColumns)2210 public void setNumColumns(int numColumns) { 2211 if (numColumns != mRequestedNumColumns) { 2212 mRequestedNumColumns = numColumns; 2213 requestLayoutIfNecessary(); 2214 } 2215 } 2216 2217 /** 2218 * Get the number of columns in the grid. 2219 * Returns {@link #AUTO_FIT} if the Grid has never been laid out. 2220 * 2221 * @attr ref android.R.styleable#GridView_numColumns 2222 * 2223 * @see #setNumColumns(int) 2224 */ 2225 @ViewDebug.ExportedProperty getNumColumns()2226 public int getNumColumns() { 2227 return mNumColumns; 2228 } 2229 2230 /** 2231 * Make sure views are touching the top or bottom edge, as appropriate for 2232 * our gravity 2233 */ adjustViewsUpOrDown()2234 private void adjustViewsUpOrDown() { 2235 final int childCount = getChildCount(); 2236 2237 if (childCount > 0) { 2238 int delta; 2239 View child; 2240 2241 if (!mStackFromBottom) { 2242 // Uh-oh -- we came up short. Slide all views up to make them 2243 // align with the top 2244 child = getChildAt(0); 2245 delta = child.getTop() - mListPadding.top; 2246 if (mFirstPosition != 0) { 2247 // It's OK to have some space above the first item if it is 2248 // part of the vertical spacing 2249 delta -= mVerticalSpacing; 2250 } 2251 if (delta < 0) { 2252 // We only are looking to see if we are too low, not too high 2253 delta = 0; 2254 } 2255 } else { 2256 // we are too high, slide all views down to align with bottom 2257 child = getChildAt(childCount - 1); 2258 delta = child.getBottom() - (getHeight() - mListPadding.bottom); 2259 2260 if (mFirstPosition + childCount < mItemCount) { 2261 // It's OK to have some space below the last item if it is 2262 // part of the vertical spacing 2263 delta += mVerticalSpacing; 2264 } 2265 2266 if (delta > 0) { 2267 // We only are looking to see if we are too high, not too low 2268 delta = 0; 2269 } 2270 } 2271 2272 if (delta != 0) { 2273 offsetChildrenTopAndBottom(-delta); 2274 } 2275 } 2276 } 2277 2278 @Override computeVerticalScrollExtent()2279 protected int computeVerticalScrollExtent() { 2280 final int count = getChildCount(); 2281 if (count > 0) { 2282 final int numColumns = mNumColumns; 2283 final int rowCount = (count + numColumns - 1) / numColumns; 2284 2285 int extent = rowCount * 100; 2286 2287 View view = getChildAt(0); 2288 final int top = view.getTop(); 2289 int height = view.getHeight(); 2290 if (height > 0) { 2291 extent += (top * 100) / height; 2292 } 2293 2294 view = getChildAt(count - 1); 2295 final int bottom = view.getBottom(); 2296 height = view.getHeight(); 2297 if (height > 0) { 2298 extent -= ((bottom - getHeight()) * 100) / height; 2299 } 2300 2301 return extent; 2302 } 2303 return 0; 2304 } 2305 2306 @Override computeVerticalScrollOffset()2307 protected int computeVerticalScrollOffset() { 2308 if (mFirstPosition >= 0 && getChildCount() > 0) { 2309 final View view = getChildAt(0); 2310 final int top = view.getTop(); 2311 int height = view.getHeight(); 2312 if (height > 0) { 2313 final int numColumns = mNumColumns; 2314 final int rowCount = (mItemCount + numColumns - 1) / numColumns; 2315 // In case of stackFromBottom the calculation of whichRow needs 2316 // to take into account that counting from the top the first row 2317 // might not be entirely filled. 2318 final int oddItemsOnFirstRow = isStackFromBottom() ? ((rowCount * numColumns) - 2319 mItemCount) : 0; 2320 final int whichRow = (mFirstPosition + oddItemsOnFirstRow) / numColumns; 2321 return Math.max(whichRow * 100 - (top * 100) / height + 2322 (int) ((float) mScrollY / getHeight() * rowCount * 100), 0); 2323 } 2324 } 2325 return 0; 2326 } 2327 2328 @Override computeVerticalScrollRange()2329 protected int computeVerticalScrollRange() { 2330 // TODO: Account for vertical spacing too 2331 final int numColumns = mNumColumns; 2332 final int rowCount = (mItemCount + numColumns - 1) / numColumns; 2333 int result = Math.max(rowCount * 100, 0); 2334 if (mScrollY != 0) { 2335 // Compensate for overscroll 2336 result += Math.abs((int) ((float) mScrollY / getHeight() * rowCount * 100)); 2337 } 2338 return result; 2339 } 2340 2341 @Override getAccessibilityClassName()2342 public CharSequence getAccessibilityClassName() { 2343 return GridView.class.getName(); 2344 } 2345 2346 /** @hide */ 2347 @Override onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)2348 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 2349 super.onInitializeAccessibilityNodeInfoInternal(info); 2350 2351 final int columnsCount = getNumColumns(); 2352 final int rowsCount = getCount() / columnsCount; 2353 final int selectionMode = getSelectionModeForAccessibility(); 2354 final CollectionInfo collectionInfo = CollectionInfo.obtain( 2355 rowsCount, columnsCount, false, selectionMode); 2356 info.setCollectionInfo(collectionInfo); 2357 2358 if (columnsCount > 0 || rowsCount > 0) { 2359 info.addAction(AccessibilityAction.ACTION_SCROLL_TO_POSITION); 2360 } 2361 } 2362 2363 /** @hide */ 2364 @Override performAccessibilityActionInternal(int action, Bundle arguments)2365 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 2366 if (super.performAccessibilityActionInternal(action, arguments)) { 2367 return true; 2368 } 2369 2370 switch (action) { 2371 case R.id.accessibilityActionScrollToPosition: { 2372 // GridView only supports scrolling in one direction, so we can 2373 // ignore the column argument. 2374 final int numColumns = getNumColumns(); 2375 final int row = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, -1); 2376 final int position = Math.min(row * numColumns, getCount() - 1); 2377 if (row >= 0) { 2378 // The accessibility service gets data asynchronously, so 2379 // we'll be a little lenient by clamping the last position. 2380 smoothScrollToPosition(position); 2381 return true; 2382 } 2383 } break; 2384 } 2385 2386 return false; 2387 } 2388 2389 @Override onInitializeAccessibilityNodeInfoForItem( View view, int position, AccessibilityNodeInfo info)2390 public void onInitializeAccessibilityNodeInfoForItem( 2391 View view, int position, AccessibilityNodeInfo info) { 2392 super.onInitializeAccessibilityNodeInfoForItem(view, position, info); 2393 2394 final int count = getCount(); 2395 final int columnsCount = getNumColumns(); 2396 final int rowsCount = count / columnsCount; 2397 2398 final int row; 2399 final int column; 2400 if (!mStackFromBottom) { 2401 column = position % columnsCount; 2402 row = position / columnsCount; 2403 } else { 2404 final int invertedIndex = count - 1 - position; 2405 2406 column = columnsCount - 1 - (invertedIndex % columnsCount); 2407 row = rowsCount - 1 - invertedIndex / columnsCount; 2408 } 2409 2410 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2411 final boolean isHeading = lp != null && lp.viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER; 2412 final boolean isSelected = isItemChecked(position); 2413 final CollectionItemInfo itemInfo = CollectionItemInfo.obtain( 2414 row, 1, column, 1, isHeading, isSelected); 2415 info.setCollectionItemInfo(itemInfo); 2416 } 2417 2418 /** @hide */ 2419 @Override encodeProperties(@onNull ViewHierarchyEncoder encoder)2420 protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) { 2421 super.encodeProperties(encoder); 2422 encoder.addProperty("numColumns", getNumColumns()); 2423 } 2424 } 2425