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 1647 if (action != KeyEvent.ACTION_UP) { 1648 switch (keyCode) { 1649 case KeyEvent.KEYCODE_DPAD_LEFT: 1650 if (event.hasNoModifiers()) { 1651 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_LEFT); 1652 } 1653 break; 1654 1655 case KeyEvent.KEYCODE_DPAD_RIGHT: 1656 if (event.hasNoModifiers()) { 1657 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_RIGHT); 1658 } 1659 break; 1660 1661 case KeyEvent.KEYCODE_DPAD_UP: 1662 if (event.hasNoModifiers()) { 1663 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_UP); 1664 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1665 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1666 } 1667 break; 1668 1669 case KeyEvent.KEYCODE_DPAD_DOWN: 1670 if (event.hasNoModifiers()) { 1671 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_DOWN); 1672 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1673 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1674 } 1675 break; 1676 1677 case KeyEvent.KEYCODE_DPAD_CENTER: 1678 case KeyEvent.KEYCODE_ENTER: 1679 if (event.hasNoModifiers()) { 1680 handled = resurrectSelectionIfNeeded(); 1681 if (!handled 1682 && event.getRepeatCount() == 0 && getChildCount() > 0) { 1683 keyPressed(); 1684 handled = true; 1685 } 1686 } 1687 break; 1688 1689 case KeyEvent.KEYCODE_SPACE: 1690 if (mPopup == null || !mPopup.isShowing()) { 1691 if (event.hasNoModifiers()) { 1692 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN); 1693 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { 1694 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP); 1695 } 1696 } 1697 break; 1698 1699 case KeyEvent.KEYCODE_PAGE_UP: 1700 if (event.hasNoModifiers()) { 1701 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP); 1702 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1703 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1704 } 1705 break; 1706 1707 case KeyEvent.KEYCODE_PAGE_DOWN: 1708 if (event.hasNoModifiers()) { 1709 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN); 1710 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1711 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1712 } 1713 break; 1714 1715 case KeyEvent.KEYCODE_MOVE_HOME: 1716 if (event.hasNoModifiers()) { 1717 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1718 } 1719 break; 1720 1721 case KeyEvent.KEYCODE_MOVE_END: 1722 if (event.hasNoModifiers()) { 1723 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1724 } 1725 break; 1726 1727 case KeyEvent.KEYCODE_TAB: 1728 // XXX Sometimes it is useful to be able to TAB through the items in 1729 // a GridView sequentially. Unfortunately this can create an 1730 // asymmetry in TAB navigation order unless the list selection 1731 // always reverts to the top or bottom when receiving TAB focus from 1732 // another widget. Leaving this behavior disabled for now but 1733 // perhaps it should be configurable (and more comprehensive). 1734 if (false) { 1735 if (event.hasNoModifiers()) { 1736 handled = resurrectSelectionIfNeeded() 1737 || sequenceScroll(FOCUS_FORWARD); 1738 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { 1739 handled = resurrectSelectionIfNeeded() 1740 || sequenceScroll(FOCUS_BACKWARD); 1741 } 1742 } 1743 break; 1744 } 1745 } 1746 1747 if (handled) { 1748 return true; 1749 } 1750 1751 if (sendToTextFilter(keyCode, count, event)) { 1752 return true; 1753 } 1754 1755 switch (action) { 1756 case KeyEvent.ACTION_DOWN: 1757 return super.onKeyDown(keyCode, event); 1758 case KeyEvent.ACTION_UP: 1759 return super.onKeyUp(keyCode, event); 1760 case KeyEvent.ACTION_MULTIPLE: 1761 return super.onKeyMultiple(keyCode, count, event); 1762 default: 1763 return false; 1764 } 1765 } 1766 1767 /** 1768 * Scrolls up or down by the number of items currently present on screen. 1769 * 1770 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 1771 * @return whether selection was moved 1772 */ pageScroll(int direction)1773 boolean pageScroll(int direction) { 1774 int nextPage = -1; 1775 1776 if (direction == FOCUS_UP) { 1777 nextPage = Math.max(0, mSelectedPosition - getChildCount()); 1778 } else if (direction == FOCUS_DOWN) { 1779 nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount()); 1780 } 1781 1782 if (nextPage >= 0) { 1783 setSelectionInt(nextPage); 1784 invokeOnItemScrollListener(); 1785 awakenScrollBars(); 1786 return true; 1787 } 1788 1789 return false; 1790 } 1791 1792 /** 1793 * Go to the last or first item if possible. 1794 * 1795 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}. 1796 * 1797 * @return Whether selection was moved. 1798 */ fullScroll(int direction)1799 boolean fullScroll(int direction) { 1800 boolean moved = false; 1801 if (direction == FOCUS_UP) { 1802 mLayoutMode = LAYOUT_SET_SELECTION; 1803 setSelectionInt(0); 1804 invokeOnItemScrollListener(); 1805 moved = true; 1806 } else if (direction == FOCUS_DOWN) { 1807 mLayoutMode = LAYOUT_SET_SELECTION; 1808 setSelectionInt(mItemCount - 1); 1809 invokeOnItemScrollListener(); 1810 moved = true; 1811 } 1812 1813 if (moved) { 1814 awakenScrollBars(); 1815 } 1816 1817 return moved; 1818 } 1819 1820 /** 1821 * Scrolls to the next or previous item, horizontally or vertically. 1822 * 1823 * @param direction either {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, 1824 * {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 1825 * 1826 * @return whether selection was moved 1827 */ arrowScroll(int direction)1828 boolean arrowScroll(int direction) { 1829 final int selectedPosition = mSelectedPosition; 1830 final int numColumns = mNumColumns; 1831 1832 int startOfRowPos; 1833 int endOfRowPos; 1834 1835 boolean moved = false; 1836 1837 if (!mStackFromBottom) { 1838 startOfRowPos = (selectedPosition / numColumns) * numColumns; 1839 endOfRowPos = Math.min(startOfRowPos + numColumns - 1, mItemCount - 1); 1840 } else { 1841 final int invertedSelection = mItemCount - 1 - selectedPosition; 1842 endOfRowPos = mItemCount - 1 - (invertedSelection / numColumns) * numColumns; 1843 startOfRowPos = Math.max(0, endOfRowPos - numColumns + 1); 1844 } 1845 1846 switch (direction) { 1847 case FOCUS_UP: 1848 if (startOfRowPos > 0) { 1849 mLayoutMode = LAYOUT_MOVE_SELECTION; 1850 setSelectionInt(Math.max(0, selectedPosition - numColumns)); 1851 moved = true; 1852 } 1853 break; 1854 case FOCUS_DOWN: 1855 if (endOfRowPos < mItemCount - 1) { 1856 mLayoutMode = LAYOUT_MOVE_SELECTION; 1857 setSelectionInt(Math.min(selectedPosition + numColumns, mItemCount - 1)); 1858 moved = true; 1859 } 1860 break; 1861 } 1862 1863 final boolean isLayoutRtl = isLayoutRtl(); 1864 if (selectedPosition > startOfRowPos && ((direction == FOCUS_LEFT && !isLayoutRtl) || 1865 (direction == FOCUS_RIGHT && isLayoutRtl))) { 1866 mLayoutMode = LAYOUT_MOVE_SELECTION; 1867 setSelectionInt(Math.max(0, selectedPosition - 1)); 1868 moved = true; 1869 } else if (selectedPosition < endOfRowPos && ((direction == FOCUS_LEFT && isLayoutRtl) || 1870 (direction == FOCUS_RIGHT && !isLayoutRtl))) { 1871 mLayoutMode = LAYOUT_MOVE_SELECTION; 1872 setSelectionInt(Math.min(selectedPosition + 1, mItemCount - 1)); 1873 moved = true; 1874 } 1875 1876 if (moved) { 1877 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 1878 invokeOnItemScrollListener(); 1879 } 1880 1881 if (moved) { 1882 awakenScrollBars(); 1883 } 1884 1885 return moved; 1886 } 1887 1888 /** 1889 * Goes to the next or previous item according to the order set by the 1890 * adapter. 1891 */ sequenceScroll(int direction)1892 boolean sequenceScroll(int direction) { 1893 int selectedPosition = mSelectedPosition; 1894 int numColumns = mNumColumns; 1895 int count = mItemCount; 1896 1897 int startOfRow; 1898 int endOfRow; 1899 if (!mStackFromBottom) { 1900 startOfRow = (selectedPosition / numColumns) * numColumns; 1901 endOfRow = Math.min(startOfRow + numColumns - 1, count - 1); 1902 } else { 1903 int invertedSelection = count - 1 - selectedPosition; 1904 endOfRow = count - 1 - (invertedSelection / numColumns) * numColumns; 1905 startOfRow = Math.max(0, endOfRow - numColumns + 1); 1906 } 1907 1908 boolean moved = false; 1909 boolean showScroll = false; 1910 switch (direction) { 1911 case FOCUS_FORWARD: 1912 if (selectedPosition < count - 1) { 1913 // Move to the next item. 1914 mLayoutMode = LAYOUT_MOVE_SELECTION; 1915 setSelectionInt(selectedPosition + 1); 1916 moved = true; 1917 // Show the scrollbar only if changing rows. 1918 showScroll = selectedPosition == endOfRow; 1919 } 1920 break; 1921 1922 case FOCUS_BACKWARD: 1923 if (selectedPosition > 0) { 1924 // Move to the previous item. 1925 mLayoutMode = LAYOUT_MOVE_SELECTION; 1926 setSelectionInt(selectedPosition - 1); 1927 moved = true; 1928 // Show the scrollbar only if changing rows. 1929 showScroll = selectedPosition == startOfRow; 1930 } 1931 break; 1932 } 1933 1934 if (moved) { 1935 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 1936 invokeOnItemScrollListener(); 1937 } 1938 1939 if (showScroll) { 1940 awakenScrollBars(); 1941 } 1942 1943 return moved; 1944 } 1945 1946 @Override onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)1947 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 1948 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 1949 1950 int closestChildIndex = -1; 1951 if (gainFocus && previouslyFocusedRect != null) { 1952 previouslyFocusedRect.offset(mScrollX, mScrollY); 1953 1954 // figure out which item should be selected based on previously 1955 // focused rect 1956 Rect otherRect = mTempRect; 1957 int minDistance = Integer.MAX_VALUE; 1958 final int childCount = getChildCount(); 1959 for (int i = 0; i < childCount; i++) { 1960 // only consider view's on appropriate edge of grid 1961 if (!isCandidateSelection(i, direction)) { 1962 continue; 1963 } 1964 1965 final View other = getChildAt(i); 1966 other.getDrawingRect(otherRect); 1967 offsetDescendantRectToMyCoords(other, otherRect); 1968 int distance = getDistance(previouslyFocusedRect, otherRect, direction); 1969 1970 if (distance < minDistance) { 1971 minDistance = distance; 1972 closestChildIndex = i; 1973 } 1974 } 1975 } 1976 1977 if (closestChildIndex >= 0) { 1978 setSelection(closestChildIndex + mFirstPosition); 1979 } else { 1980 requestLayout(); 1981 } 1982 } 1983 1984 /** 1985 * Is childIndex a candidate for next focus given the direction the focus 1986 * change is coming from? 1987 * @param childIndex The index to check. 1988 * @param direction The direction, one of 1989 * {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD, FOCUS_BACKWARD} 1990 * @return Whether childIndex is a candidate. 1991 */ isCandidateSelection(int childIndex, int direction)1992 private boolean isCandidateSelection(int childIndex, int direction) { 1993 final int count = getChildCount(); 1994 final int invertedIndex = count - 1 - childIndex; 1995 1996 int rowStart; 1997 int rowEnd; 1998 1999 if (!mStackFromBottom) { 2000 rowStart = childIndex - (childIndex % mNumColumns); 2001 rowEnd = Math.max(rowStart + mNumColumns - 1, count); 2002 } else { 2003 rowEnd = count - 1 - (invertedIndex - (invertedIndex % mNumColumns)); 2004 rowStart = Math.max(0, rowEnd - mNumColumns + 1); 2005 } 2006 2007 switch (direction) { 2008 case View.FOCUS_RIGHT: 2009 // coming from left, selection is only valid if it is on left 2010 // edge 2011 return childIndex == rowStart; 2012 case View.FOCUS_DOWN: 2013 // coming from top; only valid if in top row 2014 return rowStart == 0; 2015 case View.FOCUS_LEFT: 2016 // coming from right, must be on right edge 2017 return childIndex == rowEnd; 2018 case View.FOCUS_UP: 2019 // coming from bottom, need to be in last row 2020 return rowEnd == count - 1; 2021 case View.FOCUS_FORWARD: 2022 // coming from top-left, need to be first in top row 2023 return childIndex == rowStart && rowStart == 0; 2024 case View.FOCUS_BACKWARD: 2025 // coming from bottom-right, need to be last in bottom row 2026 return childIndex == rowEnd && rowEnd == count - 1; 2027 default: 2028 throw new IllegalArgumentException("direction must be one of " 2029 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, " 2030 + "FOCUS_FORWARD, FOCUS_BACKWARD}."); 2031 } 2032 } 2033 2034 /** 2035 * Set the gravity for this grid. Gravity describes how the child views 2036 * are horizontally aligned. Defaults to Gravity.LEFT 2037 * 2038 * @param gravity the gravity to apply to this grid's children 2039 * 2040 * @attr ref android.R.styleable#GridView_gravity 2041 */ setGravity(int gravity)2042 public void setGravity(int gravity) { 2043 if (mGravity != gravity) { 2044 mGravity = gravity; 2045 requestLayoutIfNecessary(); 2046 } 2047 } 2048 2049 /** 2050 * Describes how the child views are horizontally aligned. Defaults to Gravity.LEFT 2051 * 2052 * @return the gravity that will be applied to this grid's children 2053 * 2054 * @attr ref android.R.styleable#GridView_gravity 2055 */ getGravity()2056 public int getGravity() { 2057 return mGravity; 2058 } 2059 2060 /** 2061 * Set the amount of horizontal (x) spacing to place between each item 2062 * in the grid. 2063 * 2064 * @param horizontalSpacing The amount of horizontal space between items, 2065 * in pixels. 2066 * 2067 * @attr ref android.R.styleable#GridView_horizontalSpacing 2068 */ setHorizontalSpacing(int horizontalSpacing)2069 public void setHorizontalSpacing(int horizontalSpacing) { 2070 if (horizontalSpacing != mRequestedHorizontalSpacing) { 2071 mRequestedHorizontalSpacing = horizontalSpacing; 2072 requestLayoutIfNecessary(); 2073 } 2074 } 2075 2076 /** 2077 * Returns the amount of horizontal spacing currently used between each item in the grid. 2078 * 2079 * <p>This is only accurate for the current layout. If {@link #setHorizontalSpacing(int)} 2080 * has been called but layout is not yet complete, this method may return a stale value. 2081 * To get the horizontal spacing that was explicitly requested use 2082 * {@link #getRequestedHorizontalSpacing()}.</p> 2083 * 2084 * @return Current horizontal spacing between each item in pixels 2085 * 2086 * @see #setHorizontalSpacing(int) 2087 * @see #getRequestedHorizontalSpacing() 2088 * 2089 * @attr ref android.R.styleable#GridView_horizontalSpacing 2090 */ getHorizontalSpacing()2091 public int getHorizontalSpacing() { 2092 return mHorizontalSpacing; 2093 } 2094 2095 /** 2096 * Returns the requested amount of horizontal spacing between each item in the grid. 2097 * 2098 * <p>The value returned may have been supplied during inflation as part of a style, 2099 * the default GridView style, or by a call to {@link #setHorizontalSpacing(int)}. 2100 * If layout is not yet complete or if GridView calculated a different horizontal spacing 2101 * from what was requested, this may return a different value from 2102 * {@link #getHorizontalSpacing()}.</p> 2103 * 2104 * @return The currently requested horizontal spacing between items, in pixels 2105 * 2106 * @see #setHorizontalSpacing(int) 2107 * @see #getHorizontalSpacing() 2108 * 2109 * @attr ref android.R.styleable#GridView_horizontalSpacing 2110 */ getRequestedHorizontalSpacing()2111 public int getRequestedHorizontalSpacing() { 2112 return mRequestedHorizontalSpacing; 2113 } 2114 2115 /** 2116 * Set the amount of vertical (y) spacing to place between each item 2117 * in the grid. 2118 * 2119 * @param verticalSpacing The amount of vertical space between items, 2120 * in pixels. 2121 * 2122 * @see #getVerticalSpacing() 2123 * 2124 * @attr ref android.R.styleable#GridView_verticalSpacing 2125 */ setVerticalSpacing(int verticalSpacing)2126 public void setVerticalSpacing(int verticalSpacing) { 2127 if (verticalSpacing != mVerticalSpacing) { 2128 mVerticalSpacing = verticalSpacing; 2129 requestLayoutIfNecessary(); 2130 } 2131 } 2132 2133 /** 2134 * Returns the amount of vertical spacing between each item in the grid. 2135 * 2136 * @return The vertical spacing between items in pixels 2137 * 2138 * @see #setVerticalSpacing(int) 2139 * 2140 * @attr ref android.R.styleable#GridView_verticalSpacing 2141 */ getVerticalSpacing()2142 public int getVerticalSpacing() { 2143 return mVerticalSpacing; 2144 } 2145 2146 /** 2147 * Control how items are stretched to fill their space. 2148 * 2149 * @param stretchMode Either {@link #NO_STRETCH}, 2150 * {@link #STRETCH_SPACING}, {@link #STRETCH_SPACING_UNIFORM}, or {@link #STRETCH_COLUMN_WIDTH}. 2151 * 2152 * @attr ref android.R.styleable#GridView_stretchMode 2153 */ setStretchMode(@tretchMode int stretchMode)2154 public void setStretchMode(@StretchMode int stretchMode) { 2155 if (stretchMode != mStretchMode) { 2156 mStretchMode = stretchMode; 2157 requestLayoutIfNecessary(); 2158 } 2159 } 2160 2161 @StretchMode getStretchMode()2162 public int getStretchMode() { 2163 return mStretchMode; 2164 } 2165 2166 /** 2167 * Set the width of columns in the grid. 2168 * 2169 * @param columnWidth The column width, in pixels. 2170 * 2171 * @attr ref android.R.styleable#GridView_columnWidth 2172 */ setColumnWidth(int columnWidth)2173 public void setColumnWidth(int columnWidth) { 2174 if (columnWidth != mRequestedColumnWidth) { 2175 mRequestedColumnWidth = columnWidth; 2176 requestLayoutIfNecessary(); 2177 } 2178 } 2179 2180 /** 2181 * Return the width of a column in the grid. 2182 * 2183 * <p>This may not be valid yet if a layout is pending.</p> 2184 * 2185 * @return The column width in pixels 2186 * 2187 * @see #setColumnWidth(int) 2188 * @see #getRequestedColumnWidth() 2189 * 2190 * @attr ref android.R.styleable#GridView_columnWidth 2191 */ getColumnWidth()2192 public int getColumnWidth() { 2193 return mColumnWidth; 2194 } 2195 2196 /** 2197 * Return the requested width of a column in the grid. 2198 * 2199 * <p>This may not be the actual column width used. Use {@link #getColumnWidth()} 2200 * to retrieve the current real width of a column.</p> 2201 * 2202 * @return The requested column width in pixels 2203 * 2204 * @see #setColumnWidth(int) 2205 * @see #getColumnWidth() 2206 * 2207 * @attr ref android.R.styleable#GridView_columnWidth 2208 */ getRequestedColumnWidth()2209 public int getRequestedColumnWidth() { 2210 return mRequestedColumnWidth; 2211 } 2212 2213 /** 2214 * Set the number of columns in the grid 2215 * 2216 * @param numColumns The desired number of columns. 2217 * 2218 * @attr ref android.R.styleable#GridView_numColumns 2219 */ setNumColumns(int numColumns)2220 public void setNumColumns(int numColumns) { 2221 if (numColumns != mRequestedNumColumns) { 2222 mRequestedNumColumns = numColumns; 2223 requestLayoutIfNecessary(); 2224 } 2225 } 2226 2227 /** 2228 * Get the number of columns in the grid. 2229 * Returns {@link #AUTO_FIT} if the Grid has never been laid out. 2230 * 2231 * @attr ref android.R.styleable#GridView_numColumns 2232 * 2233 * @see #setNumColumns(int) 2234 */ 2235 @ViewDebug.ExportedProperty getNumColumns()2236 public int getNumColumns() { 2237 return mNumColumns; 2238 } 2239 2240 /** 2241 * Make sure views are touching the top or bottom edge, as appropriate for 2242 * our gravity 2243 */ adjustViewsUpOrDown()2244 private void adjustViewsUpOrDown() { 2245 final int childCount = getChildCount(); 2246 2247 if (childCount > 0) { 2248 int delta; 2249 View child; 2250 2251 if (!mStackFromBottom) { 2252 // Uh-oh -- we came up short. Slide all views up to make them 2253 // align with the top 2254 child = getChildAt(0); 2255 delta = child.getTop() - mListPadding.top; 2256 if (mFirstPosition != 0) { 2257 // It's OK to have some space above the first item if it is 2258 // part of the vertical spacing 2259 delta -= mVerticalSpacing; 2260 } 2261 if (delta < 0) { 2262 // We only are looking to see if we are too low, not too high 2263 delta = 0; 2264 } 2265 } else { 2266 // we are too high, slide all views down to align with bottom 2267 child = getChildAt(childCount - 1); 2268 delta = child.getBottom() - (getHeight() - mListPadding.bottom); 2269 2270 if (mFirstPosition + childCount < mItemCount) { 2271 // It's OK to have some space below the last item if it is 2272 // part of the vertical spacing 2273 delta += mVerticalSpacing; 2274 } 2275 2276 if (delta > 0) { 2277 // We only are looking to see if we are too high, not too low 2278 delta = 0; 2279 } 2280 } 2281 2282 if (delta != 0) { 2283 offsetChildrenTopAndBottom(-delta); 2284 } 2285 } 2286 } 2287 2288 @Override computeVerticalScrollExtent()2289 protected int computeVerticalScrollExtent() { 2290 final int count = getChildCount(); 2291 if (count > 0) { 2292 final int numColumns = mNumColumns; 2293 final int rowCount = (count + numColumns - 1) / numColumns; 2294 2295 int extent = rowCount * 100; 2296 2297 View view = getChildAt(0); 2298 final int top = view.getTop(); 2299 int height = view.getHeight(); 2300 if (height > 0) { 2301 extent += (top * 100) / height; 2302 } 2303 2304 view = getChildAt(count - 1); 2305 final int bottom = view.getBottom(); 2306 height = view.getHeight(); 2307 if (height > 0) { 2308 extent -= ((bottom - getHeight()) * 100) / height; 2309 } 2310 2311 return extent; 2312 } 2313 return 0; 2314 } 2315 2316 @Override computeVerticalScrollOffset()2317 protected int computeVerticalScrollOffset() { 2318 if (mFirstPosition >= 0 && getChildCount() > 0) { 2319 final View view = getChildAt(0); 2320 final int top = view.getTop(); 2321 int height = view.getHeight(); 2322 if (height > 0) { 2323 final int numColumns = mNumColumns; 2324 final int rowCount = (mItemCount + numColumns - 1) / numColumns; 2325 // In case of stackFromBottom the calculation of whichRow needs 2326 // to take into account that counting from the top the first row 2327 // might not be entirely filled. 2328 final int oddItemsOnFirstRow = isStackFromBottom() ? ((rowCount * numColumns) - 2329 mItemCount) : 0; 2330 final int whichRow = (mFirstPosition + oddItemsOnFirstRow) / numColumns; 2331 return Math.max(whichRow * 100 - (top * 100) / height + 2332 (int) ((float) mScrollY / getHeight() * rowCount * 100), 0); 2333 } 2334 } 2335 return 0; 2336 } 2337 2338 @Override computeVerticalScrollRange()2339 protected int computeVerticalScrollRange() { 2340 // TODO: Account for vertical spacing too 2341 final int numColumns = mNumColumns; 2342 final int rowCount = (mItemCount + numColumns - 1) / numColumns; 2343 int result = Math.max(rowCount * 100, 0); 2344 if (mScrollY != 0) { 2345 // Compensate for overscroll 2346 result += Math.abs((int) ((float) mScrollY / getHeight() * rowCount * 100)); 2347 } 2348 return result; 2349 } 2350 2351 @Override getAccessibilityClassName()2352 public CharSequence getAccessibilityClassName() { 2353 return GridView.class.getName(); 2354 } 2355 2356 /** @hide */ 2357 @Override onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)2358 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 2359 super.onInitializeAccessibilityNodeInfoInternal(info); 2360 2361 final int columnsCount = getNumColumns(); 2362 final int rowsCount = getCount() / columnsCount; 2363 final int selectionMode = getSelectionModeForAccessibility(); 2364 final CollectionInfo collectionInfo = CollectionInfo.obtain( 2365 rowsCount, columnsCount, false, selectionMode); 2366 info.setCollectionInfo(collectionInfo); 2367 2368 if (columnsCount > 0 || rowsCount > 0) { 2369 info.addAction(AccessibilityAction.ACTION_SCROLL_TO_POSITION); 2370 } 2371 } 2372 2373 /** @hide */ 2374 @Override performAccessibilityActionInternal(int action, Bundle arguments)2375 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 2376 if (super.performAccessibilityActionInternal(action, arguments)) { 2377 return true; 2378 } 2379 2380 switch (action) { 2381 case R.id.accessibilityActionScrollToPosition: { 2382 // GridView only supports scrolling in one direction, so we can 2383 // ignore the column argument. 2384 final int numColumns = getNumColumns(); 2385 final int row = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, -1); 2386 final int position = Math.min(row * numColumns, getCount() - 1); 2387 if (row >= 0) { 2388 // The accessibility service gets data asynchronously, so 2389 // we'll be a little lenient by clamping the last position. 2390 smoothScrollToPosition(position); 2391 return true; 2392 } 2393 } break; 2394 } 2395 2396 return false; 2397 } 2398 2399 @Override onInitializeAccessibilityNodeInfoForItem( View view, int position, AccessibilityNodeInfo info)2400 public void onInitializeAccessibilityNodeInfoForItem( 2401 View view, int position, AccessibilityNodeInfo info) { 2402 super.onInitializeAccessibilityNodeInfoForItem(view, position, info); 2403 2404 final int count = getCount(); 2405 final int columnsCount = getNumColumns(); 2406 final int rowsCount = count / columnsCount; 2407 2408 final int row; 2409 final int column; 2410 if (!mStackFromBottom) { 2411 column = position % columnsCount; 2412 row = position / columnsCount; 2413 } else { 2414 final int invertedIndex = count - 1 - position; 2415 2416 column = columnsCount - 1 - (invertedIndex % columnsCount); 2417 row = rowsCount - 1 - invertedIndex / columnsCount; 2418 } 2419 2420 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2421 final boolean isHeading = lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER; 2422 final boolean isSelected = isItemChecked(position); 2423 final CollectionItemInfo itemInfo = CollectionItemInfo.obtain( 2424 row, 1, column, 1, isHeading, isSelected); 2425 info.setCollectionItemInfo(itemInfo); 2426 } 2427 2428 /** @hide */ 2429 @Override encodeProperties(@onNull ViewHierarchyEncoder encoder)2430 protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) { 2431 super.encodeProperties(encoder); 2432 encoder.addProperty("numColumns", getNumColumns()); 2433 } 2434 } 2435