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