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