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.content.Context; 21 import android.content.Intent; 22 import android.content.res.TypedArray; 23 import android.graphics.Rect; 24 import android.os.Trace; 25 import android.util.AttributeSet; 26 import android.util.MathUtils; 27 import android.view.Gravity; 28 import android.view.KeyEvent; 29 import android.view.SoundEffectConstants; 30 import android.view.View; 31 import android.view.ViewDebug; 32 import android.view.ViewGroup; 33 import android.view.ViewRootImpl; 34 import android.view.accessibility.AccessibilityEvent; 35 import android.view.accessibility.AccessibilityNodeInfo; 36 import android.view.accessibility.AccessibilityNodeProvider; 37 import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; 38 import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo; 39 import android.view.animation.GridLayoutAnimationController; 40 import android.widget.RemoteViews.RemoteView; 41 42 import java.lang.annotation.Retention; 43 import java.lang.annotation.RetentionPolicy; 44 45 46 /** 47 * A view that shows items in two-dimensional scrolling grid. The items in the 48 * grid come from the {@link ListAdapter} associated with this view. 49 * 50 * <p>See the <a href="{@docRoot}guide/topics/ui/layout/gridview.html">Grid 51 * View</a> guide.</p> 52 * 53 * @attr ref android.R.styleable#GridView_horizontalSpacing 54 * @attr ref android.R.styleable#GridView_verticalSpacing 55 * @attr ref android.R.styleable#GridView_stretchMode 56 * @attr ref android.R.styleable#GridView_columnWidth 57 * @attr ref android.R.styleable#GridView_numColumns 58 * @attr ref android.R.styleable#GridView_gravity 59 */ 60 @RemoteView 61 public class GridView extends AbsListView { 62 /** @hide */ 63 @IntDef({NO_STRETCH, STRETCH_SPACING, STRETCH_COLUMN_WIDTH, STRETCH_SPACING_UNIFORM}) 64 @Retention(RetentionPolicy.SOURCE) 65 public @interface StretchMode {} 66 67 /** 68 * Disables stretching. 69 * 70 * @see #setStretchMode(int) 71 */ 72 public static final int NO_STRETCH = 0; 73 /** 74 * Stretches the spacing between columns. 75 * 76 * @see #setStretchMode(int) 77 */ 78 public static final int STRETCH_SPACING = 1; 79 /** 80 * Stretches columns. 81 * 82 * @see #setStretchMode(int) 83 */ 84 public static final int STRETCH_COLUMN_WIDTH = 2; 85 /** 86 * Stretches the spacing between columns. The spacing is uniform. 87 * 88 * @see #setStretchMode(int) 89 */ 90 public static final int STRETCH_SPACING_UNIFORM = 3; 91 92 /** 93 * Creates as many columns as can fit on screen. 94 * 95 * @see #setNumColumns(int) 96 */ 97 public static final int AUTO_FIT = -1; 98 99 private int mNumColumns = AUTO_FIT; 100 101 private int mHorizontalSpacing = 0; 102 private int mRequestedHorizontalSpacing; 103 private int mVerticalSpacing = 0; 104 private int mStretchMode = STRETCH_COLUMN_WIDTH; 105 private int mColumnWidth; 106 private int mRequestedColumnWidth; 107 private int mRequestedNumColumns; 108 109 private View mReferenceView = null; 110 private View mReferenceViewInSelectedRow = null; 111 112 private int mGravity = Gravity.START; 113 114 private final Rect mTempRect = new Rect(); 115 GridView(Context context)116 public GridView(Context context) { 117 this(context, null); 118 } 119 GridView(Context context, AttributeSet attrs)120 public GridView(Context context, AttributeSet attrs) { 121 this(context, attrs, com.android.internal.R.attr.gridViewStyle); 122 } 123 GridView(Context context, AttributeSet attrs, int defStyleAttr)124 public GridView(Context context, AttributeSet attrs, int defStyleAttr) { 125 this(context, attrs, defStyleAttr, 0); 126 } 127 GridView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)128 public GridView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 129 super(context, attrs, defStyleAttr, defStyleRes); 130 131 final TypedArray a = context.obtainStyledAttributes( 132 attrs, com.android.internal.R.styleable.GridView, defStyleAttr, defStyleRes); 133 134 int hSpacing = a.getDimensionPixelOffset( 135 com.android.internal.R.styleable.GridView_horizontalSpacing, 0); 136 setHorizontalSpacing(hSpacing); 137 138 int vSpacing = a.getDimensionPixelOffset( 139 com.android.internal.R.styleable.GridView_verticalSpacing, 0); 140 setVerticalSpacing(vSpacing); 141 142 int index = a.getInt(com.android.internal.R.styleable.GridView_stretchMode, STRETCH_COLUMN_WIDTH); 143 if (index >= 0) { 144 setStretchMode(index); 145 } 146 147 int columnWidth = a.getDimensionPixelOffset(com.android.internal.R.styleable.GridView_columnWidth, -1); 148 if (columnWidth > 0) { 149 setColumnWidth(columnWidth); 150 } 151 152 int numColumns = a.getInt(com.android.internal.R.styleable.GridView_numColumns, 1); 153 setNumColumns(numColumns); 154 155 index = a.getInt(com.android.internal.R.styleable.GridView_gravity, -1); 156 if (index >= 0) { 157 setGravity(index); 158 } 159 160 a.recycle(); 161 } 162 163 @Override getAdapter()164 public ListAdapter getAdapter() { 165 return mAdapter; 166 } 167 168 /** 169 * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService 170 * through the specified intent. 171 * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to. 172 */ 173 @android.view.RemotableViewMethod setRemoteViewsAdapter(Intent intent)174 public void setRemoteViewsAdapter(Intent intent) { 175 super.setRemoteViewsAdapter(intent); 176 } 177 178 /** 179 * Sets the data behind this GridView. 180 * 181 * @param adapter the adapter providing the grid's data 182 */ 183 @Override setAdapter(ListAdapter adapter)184 public void setAdapter(ListAdapter adapter) { 185 if (mAdapter != null && mDataSetObserver != null) { 186 mAdapter.unregisterDataSetObserver(mDataSetObserver); 187 } 188 189 resetList(); 190 mRecycler.clear(); 191 mAdapter = adapter; 192 193 mOldSelectedPosition = INVALID_POSITION; 194 mOldSelectedRowId = INVALID_ROW_ID; 195 196 // AbsListView#setAdapter will update choice mode states. 197 super.setAdapter(adapter); 198 199 if (mAdapter != null) { 200 mOldItemCount = mItemCount; 201 mItemCount = mAdapter.getCount(); 202 mDataChanged = true; 203 checkFocus(); 204 205 mDataSetObserver = new AdapterDataSetObserver(); 206 mAdapter.registerDataSetObserver(mDataSetObserver); 207 208 mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); 209 210 int position; 211 if (mStackFromBottom) { 212 position = lookForSelectablePosition(mItemCount - 1, false); 213 } else { 214 position = lookForSelectablePosition(0, true); 215 } 216 setSelectedPositionInt(position); 217 setNextSelectedPositionInt(position); 218 checkSelectionChanged(); 219 } else { 220 checkFocus(); 221 // Nothing selected 222 checkSelectionChanged(); 223 } 224 225 requestLayout(); 226 } 227 228 @Override lookForSelectablePosition(int position, boolean lookDown)229 int lookForSelectablePosition(int position, boolean lookDown) { 230 final ListAdapter adapter = mAdapter; 231 if (adapter == null || isInTouchMode()) { 232 return INVALID_POSITION; 233 } 234 235 if (position < 0 || position >= mItemCount) { 236 return INVALID_POSITION; 237 } 238 return position; 239 } 240 241 /** 242 * {@inheritDoc} 243 */ 244 @Override fillGap(boolean down)245 void fillGap(boolean down) { 246 final int numColumns = mNumColumns; 247 final int verticalSpacing = mVerticalSpacing; 248 249 final int count = getChildCount(); 250 251 if (down) { 252 int paddingTop = 0; 253 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 254 paddingTop = getListPaddingTop(); 255 } 256 final int startOffset = count > 0 ? 257 getChildAt(count - 1).getBottom() + verticalSpacing : paddingTop; 258 int position = mFirstPosition + count; 259 if (mStackFromBottom) { 260 position += numColumns - 1; 261 } 262 fillDown(position, startOffset); 263 correctTooHigh(numColumns, verticalSpacing, getChildCount()); 264 } else { 265 int paddingBottom = 0; 266 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 267 paddingBottom = getListPaddingBottom(); 268 } 269 final int startOffset = count > 0 ? 270 getChildAt(0).getTop() - verticalSpacing : getHeight() - paddingBottom; 271 int position = mFirstPosition; 272 if (!mStackFromBottom) { 273 position -= numColumns; 274 } else { 275 position--; 276 } 277 fillUp(position, startOffset); 278 correctTooLow(numColumns, verticalSpacing, getChildCount()); 279 } 280 } 281 282 /** 283 * Fills the list from pos down to the end of the list view. 284 * 285 * @param pos The first position to put in the list 286 * 287 * @param nextTop The location where the top of the item associated with pos 288 * should be drawn 289 * 290 * @return The view that is currently selected, if it happens to be in the 291 * range that we draw. 292 */ fillDown(int pos, int nextTop)293 private View fillDown(int pos, int nextTop) { 294 View selectedView = null; 295 296 int end = (mBottom - mTop); 297 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 298 end -= mListPadding.bottom; 299 } 300 301 while (nextTop < end && pos < mItemCount) { 302 View temp = makeRow(pos, nextTop, true); 303 if (temp != null) { 304 selectedView = temp; 305 } 306 307 // mReferenceView will change with each call to makeRow() 308 // do not cache in a local variable outside of this loop 309 nextTop = mReferenceView.getBottom() + mVerticalSpacing; 310 311 pos += mNumColumns; 312 } 313 314 setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); 315 return selectedView; 316 } 317 makeRow(int startPos, int y, boolean flow)318 private View makeRow(int startPos, int y, boolean flow) { 319 final int columnWidth = mColumnWidth; 320 final int horizontalSpacing = mHorizontalSpacing; 321 322 final boolean isLayoutRtl = isLayoutRtl(); 323 324 int last; 325 int nextLeft; 326 327 if (isLayoutRtl) { 328 nextLeft = getWidth() - mListPadding.right - columnWidth - 329 ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0); 330 } else { 331 nextLeft = mListPadding.left + 332 ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0); 333 } 334 335 if (!mStackFromBottom) { 336 last = Math.min(startPos + mNumColumns, mItemCount); 337 } else { 338 last = startPos + 1; 339 startPos = Math.max(0, startPos - mNumColumns + 1); 340 341 if (last - startPos < mNumColumns) { 342 final int deltaLeft = (mNumColumns - (last - startPos)) * (columnWidth + horizontalSpacing); 343 nextLeft += (isLayoutRtl ? -1 : +1) * deltaLeft; 344 } 345 } 346 347 View selectedView = null; 348 349 final boolean hasFocus = shouldShowSelector(); 350 final boolean inClick = touchModeDrawsInPressedState(); 351 final int selectedPosition = mSelectedPosition; 352 353 View child = null; 354 final int nextChildDir = isLayoutRtl ? -1 : +1; 355 for (int pos = startPos; pos < last; pos++) { 356 // is this the selected item? 357 boolean selected = pos == selectedPosition; 358 // does the list view have focus or contain focus 359 360 final int where = flow ? -1 : pos - startPos; 361 child = makeAndAddView(pos, y, flow, nextLeft, selected, where); 362 363 nextLeft += nextChildDir * columnWidth; 364 if (pos < last - 1) { 365 nextLeft += nextChildDir * horizontalSpacing; 366 } 367 368 if (selected && (hasFocus || inClick)) { 369 selectedView = child; 370 } 371 } 372 373 mReferenceView = child; 374 375 if (selectedView != null) { 376 mReferenceViewInSelectedRow = mReferenceView; 377 } 378 379 return selectedView; 380 } 381 382 /** 383 * Fills the list from pos up to the top of the list view. 384 * 385 * @param pos The first position to put in the list 386 * 387 * @param nextBottom The location where the bottom of the item associated 388 * with pos should be drawn 389 * 390 * @return The view that is currently selected 391 */ fillUp(int pos, int nextBottom)392 private View fillUp(int pos, int nextBottom) { 393 View selectedView = null; 394 395 int end = 0; 396 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 397 end = mListPadding.top; 398 } 399 400 while (nextBottom > end && pos >= 0) { 401 402 View temp = makeRow(pos, nextBottom, false); 403 if (temp != null) { 404 selectedView = temp; 405 } 406 407 nextBottom = mReferenceView.getTop() - mVerticalSpacing; 408 409 mFirstPosition = pos; 410 411 pos -= mNumColumns; 412 } 413 414 if (mStackFromBottom) { 415 mFirstPosition = Math.max(0, pos + 1); 416 } 417 418 setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); 419 return selectedView; 420 } 421 422 /** 423 * Fills the list from top to bottom, starting with mFirstPosition 424 * 425 * @param nextTop The location where the top of the first item should be 426 * drawn 427 * 428 * @return The view that is currently selected 429 */ fillFromTop(int nextTop)430 private View fillFromTop(int nextTop) { 431 mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); 432 mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); 433 if (mFirstPosition < 0) { 434 mFirstPosition = 0; 435 } 436 mFirstPosition -= mFirstPosition % mNumColumns; 437 return fillDown(mFirstPosition, nextTop); 438 } 439 fillFromBottom(int lastPosition, int nextBottom)440 private View fillFromBottom(int lastPosition, int nextBottom) { 441 lastPosition = Math.max(lastPosition, mSelectedPosition); 442 lastPosition = Math.min(lastPosition, mItemCount - 1); 443 444 final int invertedPosition = mItemCount - 1 - lastPosition; 445 lastPosition = mItemCount - 1 - (invertedPosition - (invertedPosition % mNumColumns)); 446 447 return fillUp(lastPosition, nextBottom); 448 } 449 fillSelection(int childrenTop, int childrenBottom)450 private View fillSelection(int childrenTop, int childrenBottom) { 451 final int selectedPosition = reconcileSelectedPosition(); 452 final int numColumns = mNumColumns; 453 final int verticalSpacing = mVerticalSpacing; 454 455 int rowStart; 456 int rowEnd = -1; 457 458 if (!mStackFromBottom) { 459 rowStart = selectedPosition - (selectedPosition % numColumns); 460 } else { 461 final int invertedSelection = mItemCount - 1 - selectedPosition; 462 463 rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 464 rowStart = Math.max(0, rowEnd - numColumns + 1); 465 } 466 467 final int fadingEdgeLength = getVerticalFadingEdgeLength(); 468 final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); 469 470 final View sel = makeRow(mStackFromBottom ? rowEnd : rowStart, topSelectionPixel, true); 471 mFirstPosition = rowStart; 472 473 final View referenceView = mReferenceView; 474 475 if (!mStackFromBottom) { 476 fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); 477 pinToBottom(childrenBottom); 478 fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); 479 adjustViewsUpOrDown(); 480 } else { 481 final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, 482 fadingEdgeLength, numColumns, rowStart); 483 final int offset = bottomSelectionPixel - referenceView.getBottom(); 484 offsetChildrenTopAndBottom(offset); 485 fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); 486 pinToTop(childrenTop); 487 fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); 488 adjustViewsUpOrDown(); 489 } 490 491 return sel; 492 } 493 pinToTop(int childrenTop)494 private void pinToTop(int childrenTop) { 495 if (mFirstPosition == 0) { 496 final int top = getChildAt(0).getTop(); 497 final int offset = childrenTop - top; 498 if (offset < 0) { 499 offsetChildrenTopAndBottom(offset); 500 } 501 } 502 } 503 pinToBottom(int childrenBottom)504 private void pinToBottom(int childrenBottom) { 505 final int count = getChildCount(); 506 if (mFirstPosition + count == mItemCount) { 507 final int bottom = getChildAt(count - 1).getBottom(); 508 final int offset = childrenBottom - bottom; 509 if (offset > 0) { 510 offsetChildrenTopAndBottom(offset); 511 } 512 } 513 } 514 515 @Override findMotionRow(int y)516 int findMotionRow(int y) { 517 final int childCount = getChildCount(); 518 if (childCount > 0) { 519 520 final int numColumns = mNumColumns; 521 if (!mStackFromBottom) { 522 for (int i = 0; i < childCount; i += numColumns) { 523 if (y <= getChildAt(i).getBottom()) { 524 return mFirstPosition + i; 525 } 526 } 527 } else { 528 for (int i = childCount - 1; i >= 0; i -= numColumns) { 529 if (y >= getChildAt(i).getTop()) { 530 return mFirstPosition + i; 531 } 532 } 533 } 534 } 535 return INVALID_POSITION; 536 } 537 538 /** 539 * Layout during a scroll that results from tracking motion events. Places 540 * the mMotionPosition view at the offset specified by mMotionViewTop, and 541 * then build surrounding views from there. 542 * 543 * @param position the position at which to start filling 544 * @param top the top of the view at that position 545 * @return The selected view, or null if the selected view is outside the 546 * visible area. 547 */ fillSpecific(int position, int top)548 private View fillSpecific(int position, int top) { 549 final int numColumns = mNumColumns; 550 551 int motionRowStart; 552 int motionRowEnd = -1; 553 554 if (!mStackFromBottom) { 555 motionRowStart = position - (position % numColumns); 556 } else { 557 final int invertedSelection = mItemCount - 1 - position; 558 559 motionRowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 560 motionRowStart = Math.max(0, motionRowEnd - numColumns + 1); 561 } 562 563 final View temp = makeRow(mStackFromBottom ? motionRowEnd : motionRowStart, top, true); 564 565 // Possibly changed again in fillUp if we add rows above this one. 566 mFirstPosition = motionRowStart; 567 568 final View referenceView = mReferenceView; 569 // We didn't have anything to layout, bail out 570 if (referenceView == null) { 571 return null; 572 } 573 574 final int verticalSpacing = mVerticalSpacing; 575 576 View above; 577 View below; 578 579 if (!mStackFromBottom) { 580 above = fillUp(motionRowStart - numColumns, referenceView.getTop() - verticalSpacing); 581 adjustViewsUpOrDown(); 582 below = fillDown(motionRowStart + numColumns, referenceView.getBottom() + verticalSpacing); 583 // Check if we have dragged the bottom of the grid too high 584 final int childCount = getChildCount(); 585 if (childCount > 0) { 586 correctTooHigh(numColumns, verticalSpacing, childCount); 587 } 588 } else { 589 below = fillDown(motionRowEnd + numColumns, referenceView.getBottom() + verticalSpacing); 590 adjustViewsUpOrDown(); 591 above = fillUp(motionRowStart - 1, referenceView.getTop() - verticalSpacing); 592 // Check if we have dragged the bottom of the grid too high 593 final int childCount = getChildCount(); 594 if (childCount > 0) { 595 correctTooLow(numColumns, verticalSpacing, childCount); 596 } 597 } 598 599 if (temp != null) { 600 return temp; 601 } else if (above != null) { 602 return above; 603 } else { 604 return below; 605 } 606 } 607 correctTooHigh(int numColumns, int verticalSpacing, int childCount)608 private void correctTooHigh(int numColumns, int verticalSpacing, int childCount) { 609 // First see if the last item is visible 610 final int lastPosition = mFirstPosition + childCount - 1; 611 if (lastPosition == mItemCount - 1 && childCount > 0) { 612 // Get the last child ... 613 final View lastChild = getChildAt(childCount - 1); 614 615 // ... and its bottom edge 616 final int lastBottom = lastChild.getBottom(); 617 // This is bottom of our drawable area 618 final int end = (mBottom - mTop) - mListPadding.bottom; 619 620 // This is how far the bottom edge of the last view is from the bottom of the 621 // drawable area 622 int bottomOffset = end - lastBottom; 623 624 final View firstChild = getChildAt(0); 625 final int firstTop = firstChild.getTop(); 626 627 // Make sure we are 1) Too high, and 2) Either there are more rows above the 628 // first row or the first row is scrolled off the top of the drawable area 629 if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top)) { 630 if (mFirstPosition == 0) { 631 // Don't pull the top too far down 632 bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop); 633 } 634 635 // Move everything down 636 offsetChildrenTopAndBottom(bottomOffset); 637 if (mFirstPosition > 0) { 638 // Fill the gap that was opened above mFirstPosition with more rows, if 639 // possible 640 fillUp(mFirstPosition - (mStackFromBottom ? 1 : numColumns), 641 firstChild.getTop() - verticalSpacing); 642 // Close up the remaining gap 643 adjustViewsUpOrDown(); 644 } 645 } 646 } 647 } 648 correctTooLow(int numColumns, int verticalSpacing, int childCount)649 private void correctTooLow(int numColumns, int verticalSpacing, int childCount) { 650 if (mFirstPosition == 0 && childCount > 0) { 651 // Get the first child ... 652 final View firstChild = getChildAt(0); 653 654 // ... and its top edge 655 final int firstTop = firstChild.getTop(); 656 657 // This is top of our drawable area 658 final int start = mListPadding.top; 659 660 // This is bottom of our drawable area 661 final int end = (mBottom - mTop) - mListPadding.bottom; 662 663 // This is how far the top edge of the first view is from the top of the 664 // drawable area 665 int topOffset = firstTop - start; 666 final View lastChild = getChildAt(childCount - 1); 667 final int lastBottom = lastChild.getBottom(); 668 final int lastPosition = mFirstPosition + childCount - 1; 669 670 // Make sure we are 1) Too low, and 2) Either there are more rows below the 671 // last row or the last row is scrolled off the bottom of the drawable area 672 if (topOffset > 0 && (lastPosition < mItemCount - 1 || lastBottom > end)) { 673 if (lastPosition == mItemCount - 1 ) { 674 // Don't pull the bottom too far up 675 topOffset = Math.min(topOffset, lastBottom - end); 676 } 677 678 // Move everything up 679 offsetChildrenTopAndBottom(-topOffset); 680 if (lastPosition < mItemCount - 1) { 681 // Fill the gap that was opened below the last position with more rows, if 682 // possible 683 fillDown(lastPosition + (!mStackFromBottom ? 1 : numColumns), 684 lastChild.getBottom() + verticalSpacing); 685 // Close up the remaining gap 686 adjustViewsUpOrDown(); 687 } 688 } 689 } 690 } 691 692 /** 693 * Fills the grid based on positioning the new selection at a specific 694 * location. The selection may be moved so that it does not intersect the 695 * faded edges. The grid is then filled upwards and downwards from there. 696 * 697 * @param selectedTop Where the selected item should be 698 * @param childrenTop Where to start drawing children 699 * @param childrenBottom Last pixel where children can be drawn 700 * @return The view that currently has selection 701 */ fillFromSelection(int selectedTop, int childrenTop, int childrenBottom)702 private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) { 703 final int fadingEdgeLength = getVerticalFadingEdgeLength(); 704 final int selectedPosition = mSelectedPosition; 705 final int numColumns = mNumColumns; 706 final int verticalSpacing = mVerticalSpacing; 707 708 int rowStart; 709 int rowEnd = -1; 710 711 if (!mStackFromBottom) { 712 rowStart = selectedPosition - (selectedPosition % numColumns); 713 } else { 714 int invertedSelection = mItemCount - 1 - selectedPosition; 715 716 rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 717 rowStart = Math.max(0, rowEnd - numColumns + 1); 718 } 719 720 View sel; 721 View referenceView; 722 723 int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); 724 int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, 725 numColumns, rowStart); 726 727 sel = makeRow(mStackFromBottom ? rowEnd : rowStart, selectedTop, true); 728 // Possibly changed again in fillUp if we add rows above this one. 729 mFirstPosition = rowStart; 730 731 referenceView = mReferenceView; 732 adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); 733 adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); 734 735 if (!mStackFromBottom) { 736 fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); 737 adjustViewsUpOrDown(); 738 fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); 739 } else { 740 fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); 741 adjustViewsUpOrDown(); 742 fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); 743 } 744 745 746 return sel; 747 } 748 749 /** 750 * Calculate the bottom-most pixel we can draw the selection into 751 * 752 * @param childrenBottom Bottom pixel were children can be drawn 753 * @param fadingEdgeLength Length of the fading edge in pixels, if present 754 * @param numColumns Number of columns in the grid 755 * @param rowStart The start of the row that will contain the selection 756 * @return The bottom-most pixel we can draw the selection into 757 */ getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength, int numColumns, int rowStart)758 private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength, 759 int numColumns, int rowStart) { 760 // Last pixel we can draw the selection into 761 int bottomSelectionPixel = childrenBottom; 762 if (rowStart + numColumns - 1 < mItemCount - 1) { 763 bottomSelectionPixel -= fadingEdgeLength; 764 } 765 return bottomSelectionPixel; 766 } 767 768 /** 769 * Calculate the top-most pixel we can draw the selection into 770 * 771 * @param childrenTop Top pixel were children can be drawn 772 * @param fadingEdgeLength Length of the fading edge in pixels, if present 773 * @param rowStart The start of the row that will contain the selection 774 * @return The top-most pixel we can draw the selection into 775 */ getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int rowStart)776 private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int rowStart) { 777 // first pixel we can draw the selection into 778 int topSelectionPixel = childrenTop; 779 if (rowStart > 0) { 780 topSelectionPixel += fadingEdgeLength; 781 } 782 return topSelectionPixel; 783 } 784 785 /** 786 * Move all views upwards so the selected row does not interesect the bottom 787 * fading edge (if necessary). 788 * 789 * @param childInSelectedRow A child in the row that contains the selection 790 * @param topSelectionPixel The topmost pixel we can draw the selection into 791 * @param bottomSelectionPixel The bottommost pixel we can draw the 792 * selection into 793 */ adjustForBottomFadingEdge(View childInSelectedRow, int topSelectionPixel, int bottomSelectionPixel)794 private void adjustForBottomFadingEdge(View childInSelectedRow, 795 int topSelectionPixel, int bottomSelectionPixel) { 796 // Some of the newly selected item extends below the bottom of the 797 // list 798 if (childInSelectedRow.getBottom() > bottomSelectionPixel) { 799 800 // Find space available above the selection into which we can 801 // scroll upwards 802 int spaceAbove = childInSelectedRow.getTop() - topSelectionPixel; 803 804 // Find space required to bring the bottom of the selected item 805 // fully into view 806 int spaceBelow = childInSelectedRow.getBottom() - bottomSelectionPixel; 807 int offset = Math.min(spaceAbove, spaceBelow); 808 809 // Now offset the selected item to get it into view 810 offsetChildrenTopAndBottom(-offset); 811 } 812 } 813 814 /** 815 * Move all views upwards so the selected row does not interesect the top 816 * fading edge (if necessary). 817 * 818 * @param childInSelectedRow A child in the row that contains the selection 819 * @param topSelectionPixel The topmost pixel we can draw the selection into 820 * @param bottomSelectionPixel The bottommost pixel we can draw the 821 * selection into 822 */ adjustForTopFadingEdge(View childInSelectedRow, int topSelectionPixel, int bottomSelectionPixel)823 private void adjustForTopFadingEdge(View childInSelectedRow, 824 int topSelectionPixel, int bottomSelectionPixel) { 825 // Some of the newly selected item extends above the top of the list 826 if (childInSelectedRow.getTop() < topSelectionPixel) { 827 // Find space required to bring the top of the selected item 828 // fully into view 829 int spaceAbove = topSelectionPixel - childInSelectedRow.getTop(); 830 831 // Find space available below the selection into which we can 832 // scroll downwards 833 int spaceBelow = bottomSelectionPixel - childInSelectedRow.getBottom(); 834 int offset = Math.min(spaceAbove, spaceBelow); 835 836 // Now offset the selected item to get it into view 837 offsetChildrenTopAndBottom(offset); 838 } 839 } 840 841 /** 842 * Smoothly scroll to the specified adapter position. The view will 843 * scroll such that the indicated position is displayed. 844 * @param position Scroll to this adapter position. 845 */ 846 @android.view.RemotableViewMethod smoothScrollToPosition(int position)847 public void smoothScrollToPosition(int position) { 848 super.smoothScrollToPosition(position); 849 } 850 851 /** 852 * Smoothly scroll to the specified adapter position offset. The view will 853 * scroll such that the indicated position is displayed. 854 * @param offset The amount to offset from the adapter position to scroll to. 855 */ 856 @android.view.RemotableViewMethod smoothScrollByOffset(int offset)857 public void smoothScrollByOffset(int offset) { 858 super.smoothScrollByOffset(offset); 859 } 860 861 /** 862 * Fills the grid based on positioning the new selection relative to the old 863 * selection. The new selection will be placed at, above, or below the 864 * location of the new selection depending on how the selection is moving. 865 * The selection will then be pinned to the visible part of the screen, 866 * excluding the edges that are faded. The grid is then filled upwards and 867 * downwards from there. 868 * 869 * @param delta Which way we are moving 870 * @param childrenTop Where to start drawing children 871 * @param childrenBottom Last pixel where children can be drawn 872 * @return The view that currently has selection 873 */ moveSelection(int delta, int childrenTop, int childrenBottom)874 private View moveSelection(int delta, int childrenTop, int childrenBottom) { 875 final int fadingEdgeLength = getVerticalFadingEdgeLength(); 876 final int selectedPosition = mSelectedPosition; 877 final int numColumns = mNumColumns; 878 final int verticalSpacing = mVerticalSpacing; 879 880 int oldRowStart; 881 int rowStart; 882 int rowEnd = -1; 883 884 if (!mStackFromBottom) { 885 oldRowStart = (selectedPosition - delta) - ((selectedPosition - delta) % numColumns); 886 887 rowStart = selectedPosition - (selectedPosition % numColumns); 888 } else { 889 int invertedSelection = mItemCount - 1 - selectedPosition; 890 891 rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 892 rowStart = Math.max(0, rowEnd - numColumns + 1); 893 894 invertedSelection = mItemCount - 1 - (selectedPosition - delta); 895 oldRowStart = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns)); 896 oldRowStart = Math.max(0, oldRowStart - numColumns + 1); 897 } 898 899 final int rowDelta = rowStart - oldRowStart; 900 901 final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart); 902 final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, 903 numColumns, rowStart); 904 905 // Possibly changed again in fillUp if we add rows above this one. 906 mFirstPosition = rowStart; 907 908 View sel; 909 View referenceView; 910 911 if (rowDelta > 0) { 912 /* 913 * Case 1: Scrolling down. 914 */ 915 916 final int oldBottom = mReferenceViewInSelectedRow == null ? 0 : 917 mReferenceViewInSelectedRow.getBottom(); 918 919 sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldBottom + verticalSpacing, true); 920 referenceView = mReferenceView; 921 922 adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); 923 } else if (rowDelta < 0) { 924 /* 925 * Case 2: Scrolling up. 926 */ 927 final int oldTop = mReferenceViewInSelectedRow == null ? 928 0 : mReferenceViewInSelectedRow .getTop(); 929 930 sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop - verticalSpacing, false); 931 referenceView = mReferenceView; 932 933 adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel); 934 } else { 935 /* 936 * Keep selection where it was 937 */ 938 final int oldTop = mReferenceViewInSelectedRow == null ? 939 0 : mReferenceViewInSelectedRow .getTop(); 940 941 sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop, true); 942 referenceView = mReferenceView; 943 } 944 945 if (!mStackFromBottom) { 946 fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing); 947 adjustViewsUpOrDown(); 948 fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing); 949 } else { 950 fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing); 951 adjustViewsUpOrDown(); 952 fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing); 953 } 954 955 return sel; 956 } 957 determineColumns(int availableSpace)958 private boolean determineColumns(int availableSpace) { 959 final int requestedHorizontalSpacing = mRequestedHorizontalSpacing; 960 final int stretchMode = mStretchMode; 961 final int requestedColumnWidth = mRequestedColumnWidth; 962 boolean didNotInitiallyFit = false; 963 964 if (mRequestedNumColumns == AUTO_FIT) { 965 if (requestedColumnWidth > 0) { 966 // Client told us to pick the number of columns 967 mNumColumns = (availableSpace + requestedHorizontalSpacing) / 968 (requestedColumnWidth + requestedHorizontalSpacing); 969 } else { 970 // Just make up a number if we don't have enough info 971 mNumColumns = 2; 972 } 973 } else { 974 // We picked the columns 975 mNumColumns = mRequestedNumColumns; 976 } 977 978 if (mNumColumns <= 0) { 979 mNumColumns = 1; 980 } 981 982 switch (stretchMode) { 983 case NO_STRETCH: 984 // Nobody stretches 985 mColumnWidth = requestedColumnWidth; 986 mHorizontalSpacing = requestedHorizontalSpacing; 987 break; 988 989 default: 990 int spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth) - 991 ((mNumColumns - 1) * requestedHorizontalSpacing); 992 993 if (spaceLeftOver < 0) { 994 didNotInitiallyFit = true; 995 } 996 997 switch (stretchMode) { 998 case STRETCH_COLUMN_WIDTH: 999 // Stretch the columns 1000 mColumnWidth = requestedColumnWidth + spaceLeftOver / mNumColumns; 1001 mHorizontalSpacing = requestedHorizontalSpacing; 1002 break; 1003 1004 case STRETCH_SPACING: 1005 // Stretch the spacing between columns 1006 mColumnWidth = requestedColumnWidth; 1007 if (mNumColumns > 1) { 1008 mHorizontalSpacing = requestedHorizontalSpacing + 1009 spaceLeftOver / (mNumColumns - 1); 1010 } else { 1011 mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver; 1012 } 1013 break; 1014 1015 case STRETCH_SPACING_UNIFORM: 1016 // Stretch the spacing between columns 1017 mColumnWidth = requestedColumnWidth; 1018 if (mNumColumns > 1) { 1019 mHorizontalSpacing = requestedHorizontalSpacing + 1020 spaceLeftOver / (mNumColumns + 1); 1021 } else { 1022 mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver; 1023 } 1024 break; 1025 } 1026 1027 break; 1028 } 1029 return didNotInitiallyFit; 1030 } 1031 1032 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)1033 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1034 // Sets up mListPadding 1035 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1036 1037 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 1038 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 1039 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 1040 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 1041 1042 if (widthMode == MeasureSpec.UNSPECIFIED) { 1043 if (mColumnWidth > 0) { 1044 widthSize = mColumnWidth + mListPadding.left + mListPadding.right; 1045 } else { 1046 widthSize = mListPadding.left + mListPadding.right; 1047 } 1048 widthSize += getVerticalScrollbarWidth(); 1049 } 1050 1051 int childWidth = widthSize - mListPadding.left - mListPadding.right; 1052 boolean didNotInitiallyFit = determineColumns(childWidth); 1053 1054 int childHeight = 0; 1055 int childState = 0; 1056 1057 mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); 1058 final int count = mItemCount; 1059 if (count > 0) { 1060 final View child = obtainView(0, mIsScrap); 1061 1062 AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); 1063 if (p == null) { 1064 p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); 1065 child.setLayoutParams(p); 1066 } 1067 p.viewType = mAdapter.getItemViewType(0); 1068 p.forceAdd = true; 1069 1070 int childHeightSpec = getChildMeasureSpec( 1071 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); 1072 int childWidthSpec = getChildMeasureSpec( 1073 MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); 1074 child.measure(childWidthSpec, childHeightSpec); 1075 1076 childHeight = child.getMeasuredHeight(); 1077 childState = combineMeasuredStates(childState, child.getMeasuredState()); 1078 1079 if (mRecycler.shouldRecycleViewType(p.viewType)) { 1080 mRecycler.addScrapView(child, -1); 1081 } 1082 } 1083 1084 if (heightMode == MeasureSpec.UNSPECIFIED) { 1085 heightSize = mListPadding.top + mListPadding.bottom + childHeight + 1086 getVerticalFadingEdgeLength() * 2; 1087 } 1088 1089 if (heightMode == MeasureSpec.AT_MOST) { 1090 int ourSize = mListPadding.top + mListPadding.bottom; 1091 1092 final int numColumns = mNumColumns; 1093 for (int i = 0; i < count; i += numColumns) { 1094 ourSize += childHeight; 1095 if (i + numColumns < count) { 1096 ourSize += mVerticalSpacing; 1097 } 1098 if (ourSize >= heightSize) { 1099 ourSize = heightSize; 1100 break; 1101 } 1102 } 1103 heightSize = ourSize; 1104 } 1105 1106 if (widthMode == MeasureSpec.AT_MOST && mRequestedNumColumns != AUTO_FIT) { 1107 int ourSize = (mRequestedNumColumns*mColumnWidth) 1108 + ((mRequestedNumColumns-1)*mHorizontalSpacing) 1109 + mListPadding.left + mListPadding.right; 1110 if (ourSize > widthSize || didNotInitiallyFit) { 1111 widthSize |= MEASURED_STATE_TOO_SMALL; 1112 } 1113 } 1114 1115 setMeasuredDimension(widthSize, heightSize); 1116 mWidthMeasureSpec = widthMeasureSpec; 1117 } 1118 1119 @Override attachLayoutAnimationParameters(View child, ViewGroup.LayoutParams params, int index, int count)1120 protected void attachLayoutAnimationParameters(View child, 1121 ViewGroup.LayoutParams params, int index, int count) { 1122 1123 GridLayoutAnimationController.AnimationParameters animationParams = 1124 (GridLayoutAnimationController.AnimationParameters) params.layoutAnimationParameters; 1125 1126 if (animationParams == null) { 1127 animationParams = new GridLayoutAnimationController.AnimationParameters(); 1128 params.layoutAnimationParameters = animationParams; 1129 } 1130 1131 animationParams.count = count; 1132 animationParams.index = index; 1133 animationParams.columnsCount = mNumColumns; 1134 animationParams.rowsCount = count / mNumColumns; 1135 1136 if (!mStackFromBottom) { 1137 animationParams.column = index % mNumColumns; 1138 animationParams.row = index / mNumColumns; 1139 } else { 1140 final int invertedIndex = count - 1 - index; 1141 1142 animationParams.column = mNumColumns - 1 - (invertedIndex % mNumColumns); 1143 animationParams.row = animationParams.rowsCount - 1 - invertedIndex / mNumColumns; 1144 } 1145 } 1146 1147 @Override layoutChildren()1148 protected void layoutChildren() { 1149 final boolean blockLayoutRequests = mBlockLayoutRequests; 1150 if (!blockLayoutRequests) { 1151 mBlockLayoutRequests = true; 1152 } 1153 1154 try { 1155 super.layoutChildren(); 1156 1157 invalidate(); 1158 1159 if (mAdapter == null) { 1160 resetList(); 1161 invokeOnItemScrollListener(); 1162 return; 1163 } 1164 1165 final int childrenTop = mListPadding.top; 1166 final int childrenBottom = mBottom - mTop - mListPadding.bottom; 1167 1168 int childCount = getChildCount(); 1169 int index; 1170 int delta = 0; 1171 1172 View sel; 1173 View oldSel = null; 1174 View oldFirst = null; 1175 View newSel = null; 1176 1177 // Remember stuff we will need down below 1178 switch (mLayoutMode) { 1179 case LAYOUT_SET_SELECTION: 1180 index = mNextSelectedPosition - mFirstPosition; 1181 if (index >= 0 && index < childCount) { 1182 newSel = getChildAt(index); 1183 } 1184 break; 1185 case LAYOUT_FORCE_TOP: 1186 case LAYOUT_FORCE_BOTTOM: 1187 case LAYOUT_SPECIFIC: 1188 case LAYOUT_SYNC: 1189 break; 1190 case LAYOUT_MOVE_SELECTION: 1191 if (mNextSelectedPosition >= 0) { 1192 delta = mNextSelectedPosition - mSelectedPosition; 1193 } 1194 break; 1195 default: 1196 // Remember the previously selected view 1197 index = mSelectedPosition - mFirstPosition; 1198 if (index >= 0 && index < childCount) { 1199 oldSel = getChildAt(index); 1200 } 1201 1202 // Remember the previous first child 1203 oldFirst = getChildAt(0); 1204 } 1205 1206 boolean dataChanged = mDataChanged; 1207 if (dataChanged) { 1208 handleDataChanged(); 1209 } 1210 1211 // Handle the empty set by removing all views that are visible 1212 // and calling it a day 1213 if (mItemCount == 0) { 1214 resetList(); 1215 invokeOnItemScrollListener(); 1216 return; 1217 } 1218 1219 setSelectedPositionInt(mNextSelectedPosition); 1220 1221 AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null; 1222 View accessibilityFocusLayoutRestoreView = null; 1223 int accessibilityFocusPosition = INVALID_POSITION; 1224 1225 // Remember which child, if any, had accessibility focus. This must 1226 // occur before recycling any views, since that will clear 1227 // accessibility focus. 1228 final ViewRootImpl viewRootImpl = getViewRootImpl(); 1229 if (viewRootImpl != null) { 1230 final View focusHost = viewRootImpl.getAccessibilityFocusedHost(); 1231 if (focusHost != null) { 1232 final View focusChild = getAccessibilityFocusedChild(focusHost); 1233 if (focusChild != null) { 1234 if (!dataChanged || focusChild.hasTransientState() 1235 || mAdapterHasStableIds) { 1236 // The views won't be changing, so try to maintain 1237 // focus on the current host and virtual view. 1238 accessibilityFocusLayoutRestoreView = focusHost; 1239 accessibilityFocusLayoutRestoreNode = viewRootImpl 1240 .getAccessibilityFocusedVirtualView(); 1241 } 1242 1243 // Try to maintain focus at the same position. 1244 accessibilityFocusPosition = getPositionForView(focusChild); 1245 } 1246 } 1247 } 1248 1249 // Pull all children into the RecycleBin. 1250 // These views will be reused if possible 1251 final int firstPosition = mFirstPosition; 1252 final RecycleBin recycleBin = mRecycler; 1253 1254 if (dataChanged) { 1255 for (int i = 0; i < childCount; i++) { 1256 recycleBin.addScrapView(getChildAt(i), firstPosition+i); 1257 } 1258 } else { 1259 recycleBin.fillActiveViews(childCount, firstPosition); 1260 } 1261 1262 // Clear out old views 1263 detachAllViewsFromParent(); 1264 recycleBin.removeSkippedScrap(); 1265 1266 switch (mLayoutMode) { 1267 case LAYOUT_SET_SELECTION: 1268 if (newSel != null) { 1269 sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); 1270 } else { 1271 sel = fillSelection(childrenTop, childrenBottom); 1272 } 1273 break; 1274 case LAYOUT_FORCE_TOP: 1275 mFirstPosition = 0; 1276 sel = fillFromTop(childrenTop); 1277 adjustViewsUpOrDown(); 1278 break; 1279 case LAYOUT_FORCE_BOTTOM: 1280 sel = fillUp(mItemCount - 1, childrenBottom); 1281 adjustViewsUpOrDown(); 1282 break; 1283 case LAYOUT_SPECIFIC: 1284 sel = fillSpecific(mSelectedPosition, mSpecificTop); 1285 break; 1286 case LAYOUT_SYNC: 1287 sel = fillSpecific(mSyncPosition, mSpecificTop); 1288 break; 1289 case LAYOUT_MOVE_SELECTION: 1290 // Move the selection relative to its old position 1291 sel = moveSelection(delta, childrenTop, childrenBottom); 1292 break; 1293 default: 1294 if (childCount == 0) { 1295 if (!mStackFromBottom) { 1296 setSelectedPositionInt(mAdapter == null || isInTouchMode() ? 1297 INVALID_POSITION : 0); 1298 sel = fillFromTop(childrenTop); 1299 } else { 1300 final int last = mItemCount - 1; 1301 setSelectedPositionInt(mAdapter == null || isInTouchMode() ? 1302 INVALID_POSITION : last); 1303 sel = fillFromBottom(last, childrenBottom); 1304 } 1305 } else { 1306 if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { 1307 sel = fillSpecific(mSelectedPosition, oldSel == null ? 1308 childrenTop : oldSel.getTop()); 1309 } else if (mFirstPosition < mItemCount) { 1310 sel = fillSpecific(mFirstPosition, oldFirst == null ? 1311 childrenTop : oldFirst.getTop()); 1312 } else { 1313 sel = fillSpecific(0, childrenTop); 1314 } 1315 } 1316 break; 1317 } 1318 1319 // Flush any cached views that did not get reused above 1320 recycleBin.scrapActiveViews(); 1321 1322 if (sel != null) { 1323 positionSelector(INVALID_POSITION, sel); 1324 mSelectedTop = sel.getTop(); 1325 } else { 1326 final boolean inTouchMode = mTouchMode > TOUCH_MODE_DOWN 1327 && mTouchMode < TOUCH_MODE_SCROLL; 1328 if (inTouchMode) { 1329 // If the user's finger is down, select the motion position. 1330 final View child = getChildAt(mMotionPosition - mFirstPosition); 1331 if (child != null) { 1332 positionSelector(mMotionPosition, child); 1333 } 1334 } else if (mSelectedPosition != INVALID_POSITION) { 1335 // If we had previously positioned the selector somewhere, 1336 // put it back there. It might not match up with the data, 1337 // but it's transitioning out so it's not a big deal. 1338 final View child = getChildAt(mSelectorPosition - mFirstPosition); 1339 if (child != null) { 1340 positionSelector(mSelectorPosition, child); 1341 } 1342 } else { 1343 // Otherwise, clear selection. 1344 mSelectedTop = 0; 1345 mSelectorRect.setEmpty(); 1346 } 1347 } 1348 1349 // Attempt to restore accessibility focus, if necessary. 1350 if (viewRootImpl != null) { 1351 final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost(); 1352 if (newAccessibilityFocusedView == null) { 1353 if (accessibilityFocusLayoutRestoreView != null 1354 && accessibilityFocusLayoutRestoreView.isAttachedToWindow()) { 1355 final AccessibilityNodeProvider provider = 1356 accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider(); 1357 if (accessibilityFocusLayoutRestoreNode != null && provider != null) { 1358 final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId( 1359 accessibilityFocusLayoutRestoreNode.getSourceNodeId()); 1360 provider.performAction(virtualViewId, 1361 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 1362 } else { 1363 accessibilityFocusLayoutRestoreView.requestAccessibilityFocus(); 1364 } 1365 } else if (accessibilityFocusPosition != INVALID_POSITION) { 1366 // Bound the position within the visible children. 1367 final int position = MathUtils.constrain( 1368 accessibilityFocusPosition - mFirstPosition, 0, 1369 getChildCount() - 1); 1370 final View restoreView = getChildAt(position); 1371 if (restoreView != null) { 1372 restoreView.requestAccessibilityFocus(); 1373 } 1374 } 1375 } 1376 } 1377 1378 mLayoutMode = LAYOUT_NORMAL; 1379 mDataChanged = false; 1380 if (mPositionScrollAfterLayout != null) { 1381 post(mPositionScrollAfterLayout); 1382 mPositionScrollAfterLayout = null; 1383 } 1384 mNeedSync = false; 1385 setNextSelectedPositionInt(mSelectedPosition); 1386 1387 updateScrollIndicators(); 1388 1389 if (mItemCount > 0) { 1390 checkSelectionChanged(); 1391 } 1392 1393 invokeOnItemScrollListener(); 1394 } finally { 1395 if (!blockLayoutRequests) { 1396 mBlockLayoutRequests = false; 1397 } 1398 } 1399 } 1400 1401 1402 /** 1403 * Obtain the view and add it to our list of children. The view can be made 1404 * fresh, converted from an unused view, or used as is if it was in the 1405 * recycle bin. 1406 * 1407 * @param position Logical position in the list 1408 * @param y Top or bottom edge of the view to add 1409 * @param flow if true, align top edge to y. If false, align bottom edge to 1410 * y. 1411 * @param childrenLeft Left edge where children should be positioned 1412 * @param selected Is this position selected? 1413 * @param where to add new item in the list 1414 * @return View that was added 1415 */ makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected, int where)1416 private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, 1417 boolean selected, int where) { 1418 View child; 1419 1420 if (!mDataChanged) { 1421 // Try to use an existing view for this position 1422 child = mRecycler.getActiveView(position); 1423 if (child != null) { 1424 // Found it -- we're using an existing child 1425 // This just needs to be positioned 1426 setupChild(child, position, y, flow, childrenLeft, selected, true, where); 1427 return child; 1428 } 1429 } 1430 1431 // Make a new view for this position, or convert an unused view if 1432 // possible 1433 child = obtainView(position, mIsScrap); 1434 1435 // This needs to be positioned and measured 1436 setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0], where); 1437 1438 return child; 1439 } 1440 1441 /** 1442 * Add a view as a child and make sure it is measured (if necessary) and 1443 * positioned properly. 1444 * 1445 * @param child The view to add 1446 * @param position The position of the view 1447 * @param y The y position relative to which this view will be positioned 1448 * @param flow if true, align top edge to y. If false, align bottom edge 1449 * to y. 1450 * @param childrenLeft Left edge where children should be positioned 1451 * @param selected Is this position selected? 1452 * @param recycled Has this view been pulled from the recycle bin? If so it 1453 * does not need to be remeasured. 1454 * @param where Where to add the item in the list 1455 * 1456 */ setupChild(View child, int position, int y, boolean flow, int childrenLeft, boolean selected, boolean recycled, int where)1457 private void setupChild(View child, int position, int y, boolean flow, int childrenLeft, 1458 boolean selected, boolean recycled, int where) { 1459 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupGridItem"); 1460 1461 boolean isSelected = selected && shouldShowSelector(); 1462 final boolean updateChildSelected = isSelected != child.isSelected(); 1463 final int mode = mTouchMode; 1464 final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && 1465 mMotionPosition == position; 1466 final boolean updateChildPressed = isPressed != child.isPressed(); 1467 1468 boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); 1469 1470 // Respect layout params that are already in the view. Otherwise make 1471 // some up... 1472 AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); 1473 if (p == null) { 1474 p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); 1475 } 1476 p.viewType = mAdapter.getItemViewType(position); 1477 1478 if (recycled && !p.forceAdd) { 1479 attachViewToParent(child, where, p); 1480 } else { 1481 p.forceAdd = false; 1482 addViewInLayout(child, where, p, true); 1483 } 1484 1485 if (updateChildSelected) { 1486 child.setSelected(isSelected); 1487 if (isSelected) { 1488 requestFocus(); 1489 } 1490 } 1491 1492 if (updateChildPressed) { 1493 child.setPressed(isPressed); 1494 } 1495 1496 if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { 1497 if (child instanceof Checkable) { 1498 ((Checkable) child).setChecked(mCheckStates.get(position)); 1499 } else if (getContext().getApplicationInfo().targetSdkVersion 1500 >= android.os.Build.VERSION_CODES.HONEYCOMB) { 1501 child.setActivated(mCheckStates.get(position)); 1502 } 1503 } 1504 1505 if (needToMeasure) { 1506 int childHeightSpec = ViewGroup.getChildMeasureSpec( 1507 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height); 1508 1509 int childWidthSpec = ViewGroup.getChildMeasureSpec( 1510 MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width); 1511 child.measure(childWidthSpec, childHeightSpec); 1512 } else { 1513 cleanupLayoutState(child); 1514 } 1515 1516 final int w = child.getMeasuredWidth(); 1517 final int h = child.getMeasuredHeight(); 1518 1519 int childLeft; 1520 final int childTop = flow ? y : y - h; 1521 1522 final int layoutDirection = getLayoutDirection(); 1523 final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); 1524 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 1525 case Gravity.LEFT: 1526 childLeft = childrenLeft; 1527 break; 1528 case Gravity.CENTER_HORIZONTAL: 1529 childLeft = childrenLeft + ((mColumnWidth - w) / 2); 1530 break; 1531 case Gravity.RIGHT: 1532 childLeft = childrenLeft + mColumnWidth - w; 1533 break; 1534 default: 1535 childLeft = childrenLeft; 1536 break; 1537 } 1538 1539 if (needToMeasure) { 1540 final int childRight = childLeft + w; 1541 final int childBottom = childTop + h; 1542 child.layout(childLeft, childTop, childRight, childBottom); 1543 } else { 1544 child.offsetLeftAndRight(childLeft - child.getLeft()); 1545 child.offsetTopAndBottom(childTop - child.getTop()); 1546 } 1547 1548 if (mCachingStarted) { 1549 child.setDrawingCacheEnabled(true); 1550 } 1551 1552 if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition) 1553 != position) { 1554 child.jumpDrawablesToCurrentState(); 1555 } 1556 1557 Trace.traceEnd(Trace.TRACE_TAG_VIEW); 1558 } 1559 1560 /** 1561 * Sets the currently selected item 1562 * 1563 * @param position Index (starting at 0) of the data item to be selected. 1564 * 1565 * If in touch mode, the item will not be selected but it will still be positioned 1566 * appropriately. 1567 */ 1568 @Override setSelection(int position)1569 public void setSelection(int position) { 1570 if (!isInTouchMode()) { 1571 setNextSelectedPositionInt(position); 1572 } else { 1573 mResurrectToPosition = position; 1574 } 1575 mLayoutMode = LAYOUT_SET_SELECTION; 1576 if (mPositionScroller != null) { 1577 mPositionScroller.stop(); 1578 } 1579 requestLayout(); 1580 } 1581 1582 /** 1583 * Makes the item at the supplied position selected. 1584 * 1585 * @param position the position of the new selection 1586 */ 1587 @Override setSelectionInt(int position)1588 void setSelectionInt(int position) { 1589 int previousSelectedPosition = mNextSelectedPosition; 1590 1591 if (mPositionScroller != null) { 1592 mPositionScroller.stop(); 1593 } 1594 1595 setNextSelectedPositionInt(position); 1596 layoutChildren(); 1597 1598 final int next = mStackFromBottom ? mItemCount - 1 - mNextSelectedPosition : 1599 mNextSelectedPosition; 1600 final int previous = mStackFromBottom ? mItemCount - 1 1601 - previousSelectedPosition : previousSelectedPosition; 1602 1603 final int nextRow = next / mNumColumns; 1604 final int previousRow = previous / mNumColumns; 1605 1606 if (nextRow != previousRow) { 1607 awakenScrollBars(); 1608 } 1609 1610 } 1611 1612 @Override onKeyDown(int keyCode, KeyEvent event)1613 public boolean onKeyDown(int keyCode, KeyEvent event) { 1614 return commonKey(keyCode, 1, event); 1615 } 1616 1617 @Override onKeyMultiple(int keyCode, int repeatCount, KeyEvent event)1618 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { 1619 return commonKey(keyCode, repeatCount, event); 1620 } 1621 1622 @Override onKeyUp(int keyCode, KeyEvent event)1623 public boolean onKeyUp(int keyCode, KeyEvent event) { 1624 return commonKey(keyCode, 1, event); 1625 } 1626 commonKey(int keyCode, int count, KeyEvent event)1627 private boolean commonKey(int keyCode, int count, KeyEvent event) { 1628 if (mAdapter == null) { 1629 return false; 1630 } 1631 1632 if (mDataChanged) { 1633 layoutChildren(); 1634 } 1635 1636 boolean handled = false; 1637 int action = event.getAction(); 1638 1639 if (action != KeyEvent.ACTION_UP) { 1640 switch (keyCode) { 1641 case KeyEvent.KEYCODE_DPAD_LEFT: 1642 if (event.hasNoModifiers()) { 1643 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_LEFT); 1644 } 1645 break; 1646 1647 case KeyEvent.KEYCODE_DPAD_RIGHT: 1648 if (event.hasNoModifiers()) { 1649 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_RIGHT); 1650 } 1651 break; 1652 1653 case KeyEvent.KEYCODE_DPAD_UP: 1654 if (event.hasNoModifiers()) { 1655 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_UP); 1656 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1657 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1658 } 1659 break; 1660 1661 case KeyEvent.KEYCODE_DPAD_DOWN: 1662 if (event.hasNoModifiers()) { 1663 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_DOWN); 1664 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1665 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1666 } 1667 break; 1668 1669 case KeyEvent.KEYCODE_DPAD_CENTER: 1670 case KeyEvent.KEYCODE_ENTER: 1671 if (event.hasNoModifiers()) { 1672 handled = resurrectSelectionIfNeeded(); 1673 if (!handled 1674 && event.getRepeatCount() == 0 && getChildCount() > 0) { 1675 keyPressed(); 1676 handled = true; 1677 } 1678 } 1679 break; 1680 1681 case KeyEvent.KEYCODE_SPACE: 1682 if (mPopup == null || !mPopup.isShowing()) { 1683 if (event.hasNoModifiers()) { 1684 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN); 1685 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { 1686 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP); 1687 } 1688 } 1689 break; 1690 1691 case KeyEvent.KEYCODE_PAGE_UP: 1692 if (event.hasNoModifiers()) { 1693 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP); 1694 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1695 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1696 } 1697 break; 1698 1699 case KeyEvent.KEYCODE_PAGE_DOWN: 1700 if (event.hasNoModifiers()) { 1701 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN); 1702 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 1703 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1704 } 1705 break; 1706 1707 case KeyEvent.KEYCODE_MOVE_HOME: 1708 if (event.hasNoModifiers()) { 1709 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 1710 } 1711 break; 1712 1713 case KeyEvent.KEYCODE_MOVE_END: 1714 if (event.hasNoModifiers()) { 1715 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 1716 } 1717 break; 1718 1719 case KeyEvent.KEYCODE_TAB: 1720 // XXX Sometimes it is useful to be able to TAB through the items in 1721 // a GridView sequentially. Unfortunately this can create an 1722 // asymmetry in TAB navigation order unless the list selection 1723 // always reverts to the top or bottom when receiving TAB focus from 1724 // another widget. Leaving this behavior disabled for now but 1725 // perhaps it should be configurable (and more comprehensive). 1726 if (false) { 1727 if (event.hasNoModifiers()) { 1728 handled = resurrectSelectionIfNeeded() 1729 || sequenceScroll(FOCUS_FORWARD); 1730 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { 1731 handled = resurrectSelectionIfNeeded() 1732 || sequenceScroll(FOCUS_BACKWARD); 1733 } 1734 } 1735 break; 1736 } 1737 } 1738 1739 if (handled) { 1740 return true; 1741 } 1742 1743 if (sendToTextFilter(keyCode, count, event)) { 1744 return true; 1745 } 1746 1747 switch (action) { 1748 case KeyEvent.ACTION_DOWN: 1749 return super.onKeyDown(keyCode, event); 1750 case KeyEvent.ACTION_UP: 1751 return super.onKeyUp(keyCode, event); 1752 case KeyEvent.ACTION_MULTIPLE: 1753 return super.onKeyMultiple(keyCode, count, event); 1754 default: 1755 return false; 1756 } 1757 } 1758 1759 /** 1760 * Scrolls up or down by the number of items currently present on screen. 1761 * 1762 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 1763 * @return whether selection was moved 1764 */ pageScroll(int direction)1765 boolean pageScroll(int direction) { 1766 int nextPage = -1; 1767 1768 if (direction == FOCUS_UP) { 1769 nextPage = Math.max(0, mSelectedPosition - getChildCount()); 1770 } else if (direction == FOCUS_DOWN) { 1771 nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount()); 1772 } 1773 1774 if (nextPage >= 0) { 1775 setSelectionInt(nextPage); 1776 invokeOnItemScrollListener(); 1777 awakenScrollBars(); 1778 return true; 1779 } 1780 1781 return false; 1782 } 1783 1784 /** 1785 * Go to the last or first item if possible. 1786 * 1787 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}. 1788 * 1789 * @return Whether selection was moved. 1790 */ fullScroll(int direction)1791 boolean fullScroll(int direction) { 1792 boolean moved = false; 1793 if (direction == FOCUS_UP) { 1794 mLayoutMode = LAYOUT_SET_SELECTION; 1795 setSelectionInt(0); 1796 invokeOnItemScrollListener(); 1797 moved = true; 1798 } else if (direction == FOCUS_DOWN) { 1799 mLayoutMode = LAYOUT_SET_SELECTION; 1800 setSelectionInt(mItemCount - 1); 1801 invokeOnItemScrollListener(); 1802 moved = true; 1803 } 1804 1805 if (moved) { 1806 awakenScrollBars(); 1807 } 1808 1809 return moved; 1810 } 1811 1812 /** 1813 * Scrolls to the next or previous item, horizontally or vertically. 1814 * 1815 * @param direction either {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, 1816 * {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 1817 * 1818 * @return whether selection was moved 1819 */ arrowScroll(int direction)1820 boolean arrowScroll(int direction) { 1821 final int selectedPosition = mSelectedPosition; 1822 final int numColumns = mNumColumns; 1823 1824 int startOfRowPos; 1825 int endOfRowPos; 1826 1827 boolean moved = false; 1828 1829 if (!mStackFromBottom) { 1830 startOfRowPos = (selectedPosition / numColumns) * numColumns; 1831 endOfRowPos = Math.min(startOfRowPos + numColumns - 1, mItemCount - 1); 1832 } else { 1833 final int invertedSelection = mItemCount - 1 - selectedPosition; 1834 endOfRowPos = mItemCount - 1 - (invertedSelection / numColumns) * numColumns; 1835 startOfRowPos = Math.max(0, endOfRowPos - numColumns + 1); 1836 } 1837 1838 switch (direction) { 1839 case FOCUS_UP: 1840 if (startOfRowPos > 0) { 1841 mLayoutMode = LAYOUT_MOVE_SELECTION; 1842 setSelectionInt(Math.max(0, selectedPosition - numColumns)); 1843 moved = true; 1844 } 1845 break; 1846 case FOCUS_DOWN: 1847 if (endOfRowPos < mItemCount - 1) { 1848 mLayoutMode = LAYOUT_MOVE_SELECTION; 1849 setSelectionInt(Math.min(selectedPosition + numColumns, mItemCount - 1)); 1850 moved = true; 1851 } 1852 break; 1853 case FOCUS_LEFT: 1854 if (selectedPosition > startOfRowPos) { 1855 mLayoutMode = LAYOUT_MOVE_SELECTION; 1856 setSelectionInt(Math.max(0, selectedPosition - 1)); 1857 moved = true; 1858 } 1859 break; 1860 case FOCUS_RIGHT: 1861 if (selectedPosition < endOfRowPos) { 1862 mLayoutMode = LAYOUT_MOVE_SELECTION; 1863 setSelectionInt(Math.min(selectedPosition + 1, mItemCount - 1)); 1864 moved = true; 1865 } 1866 break; 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 onInitializeAccessibilityEvent(AccessibilityEvent event)2345 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 2346 super.onInitializeAccessibilityEvent(event); 2347 event.setClassName(GridView.class.getName()); 2348 } 2349 2350 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)2351 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 2352 super.onInitializeAccessibilityNodeInfo(info); 2353 info.setClassName(GridView.class.getName()); 2354 2355 final int columnsCount = getNumColumns(); 2356 final int rowsCount = getCount() / columnsCount; 2357 final int selectionMode = getSelectionModeForAccessibility(); 2358 final CollectionInfo collectionInfo = CollectionInfo.obtain( 2359 rowsCount, columnsCount, false, selectionMode); 2360 info.setCollectionInfo(collectionInfo); 2361 } 2362 2363 @Override onInitializeAccessibilityNodeInfoForItem( View view, int position, AccessibilityNodeInfo info)2364 public void onInitializeAccessibilityNodeInfoForItem( 2365 View view, int position, AccessibilityNodeInfo info) { 2366 super.onInitializeAccessibilityNodeInfoForItem(view, position, info); 2367 2368 final int count = getCount(); 2369 final int columnsCount = getNumColumns(); 2370 final int rowsCount = count / columnsCount; 2371 2372 final int row; 2373 final int column; 2374 if (!mStackFromBottom) { 2375 column = position % columnsCount; 2376 row = position / columnsCount; 2377 } else { 2378 final int invertedIndex = count - 1 - position; 2379 2380 column = columnsCount - 1 - (invertedIndex % columnsCount); 2381 row = rowsCount - 1 - invertedIndex / columnsCount; 2382 } 2383 2384 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2385 final boolean isHeading = lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER; 2386 final boolean isSelected = isItemChecked(position); 2387 final CollectionItemInfo itemInfo = CollectionItemInfo.obtain( 2388 row, 1, column, 1, isHeading, isSelected); 2389 info.setCollectionItemInfo(itemInfo); 2390 } 2391 } 2392