1 /* 2 * Copyright (C) 2006 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.os.Trace; 20 import com.android.internal.R; 21 import com.android.internal.util.Predicate; 22 import com.google.android.collect.Lists; 23 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.res.TypedArray; 27 import android.graphics.Canvas; 28 import android.graphics.Paint; 29 import android.graphics.PixelFormat; 30 import android.graphics.Rect; 31 import android.graphics.drawable.Drawable; 32 import android.util.AttributeSet; 33 import android.util.MathUtils; 34 import android.util.SparseBooleanArray; 35 import android.view.FocusFinder; 36 import android.view.KeyEvent; 37 import android.view.SoundEffectConstants; 38 import android.view.View; 39 import android.view.ViewDebug; 40 import android.view.ViewGroup; 41 import android.view.ViewParent; 42 import android.view.ViewRootImpl; 43 import android.view.accessibility.AccessibilityEvent; 44 import android.view.accessibility.AccessibilityNodeInfo; 45 import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; 46 import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo; 47 import android.view.accessibility.AccessibilityNodeProvider; 48 import android.widget.RemoteViews.RemoteView; 49 50 import java.util.ArrayList; 51 52 /* 53 * Implementation Notes: 54 * 55 * Some terminology: 56 * 57 * index - index of the items that are currently visible 58 * position - index of the items in the cursor 59 */ 60 61 62 /** 63 * A view that shows items in a vertically scrolling list. The items 64 * come from the {@link ListAdapter} associated with this view. 65 * 66 * <p>See the <a href="{@docRoot}guide/topics/ui/layout/listview.html">List View</a> 67 * guide.</p> 68 * 69 * @attr ref android.R.styleable#ListView_entries 70 * @attr ref android.R.styleable#ListView_divider 71 * @attr ref android.R.styleable#ListView_dividerHeight 72 * @attr ref android.R.styleable#ListView_headerDividersEnabled 73 * @attr ref android.R.styleable#ListView_footerDividersEnabled 74 */ 75 @RemoteView 76 public class ListView extends AbsListView { 77 /** 78 * Used to indicate a no preference for a position type. 79 */ 80 static final int NO_POSITION = -1; 81 82 /** 83 * When arrow scrolling, ListView will never scroll more than this factor 84 * times the height of the list. 85 */ 86 private static final float MAX_SCROLL_FACTOR = 0.33f; 87 88 /** 89 * When arrow scrolling, need a certain amount of pixels to preview next 90 * items. This is usually the fading edge, but if that is small enough, 91 * we want to make sure we preview at least this many pixels. 92 */ 93 private static final int MIN_SCROLL_PREVIEW_PIXELS = 2; 94 95 /** 96 * A class that represents a fixed view in a list, for example a header at the top 97 * or a footer at the bottom. 98 */ 99 public class FixedViewInfo { 100 /** The view to add to the list */ 101 public View view; 102 /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */ 103 public Object data; 104 /** <code>true</code> if the fixed view should be selectable in the list */ 105 public boolean isSelectable; 106 } 107 108 private ArrayList<FixedViewInfo> mHeaderViewInfos = Lists.newArrayList(); 109 private ArrayList<FixedViewInfo> mFooterViewInfos = Lists.newArrayList(); 110 111 Drawable mDivider; 112 int mDividerHeight; 113 114 Drawable mOverScrollHeader; 115 Drawable mOverScrollFooter; 116 117 private boolean mIsCacheColorOpaque; 118 private boolean mDividerIsOpaque; 119 120 private boolean mHeaderDividersEnabled; 121 private boolean mFooterDividersEnabled; 122 123 private boolean mAreAllItemsSelectable = true; 124 125 private boolean mItemsCanFocus = false; 126 127 // used for temporary calculations. 128 private final Rect mTempRect = new Rect(); 129 private Paint mDividerPaint; 130 131 // the single allocated result per list view; kinda cheesey but avoids 132 // allocating these thingies too often. 133 private final ArrowScrollFocusResult mArrowScrollFocusResult = new ArrowScrollFocusResult(); 134 135 // Keeps focused children visible through resizes 136 private FocusSelector mFocusSelector; 137 ListView(Context context)138 public ListView(Context context) { 139 this(context, null); 140 } 141 ListView(Context context, AttributeSet attrs)142 public ListView(Context context, AttributeSet attrs) { 143 this(context, attrs, com.android.internal.R.attr.listViewStyle); 144 } 145 ListView(Context context, AttributeSet attrs, int defStyleAttr)146 public ListView(Context context, AttributeSet attrs, int defStyleAttr) { 147 this(context, attrs, defStyleAttr, 0); 148 } 149 ListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)150 public ListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 151 super(context, attrs, defStyleAttr, defStyleRes); 152 153 final TypedArray a = context.obtainStyledAttributes( 154 attrs, com.android.internal.R.styleable.ListView, defStyleAttr, defStyleRes); 155 156 CharSequence[] entries = a.getTextArray( 157 com.android.internal.R.styleable.ListView_entries); 158 if (entries != null) { 159 setAdapter(new ArrayAdapter<CharSequence>(context, 160 com.android.internal.R.layout.simple_list_item_1, entries)); 161 } 162 163 final Drawable d = a.getDrawable(com.android.internal.R.styleable.ListView_divider); 164 if (d != null) { 165 // If a divider is specified use its intrinsic height for divider height 166 setDivider(d); 167 } 168 169 final Drawable osHeader = a.getDrawable( 170 com.android.internal.R.styleable.ListView_overScrollHeader); 171 if (osHeader != null) { 172 setOverscrollHeader(osHeader); 173 } 174 175 final Drawable osFooter = a.getDrawable( 176 com.android.internal.R.styleable.ListView_overScrollFooter); 177 if (osFooter != null) { 178 setOverscrollFooter(osFooter); 179 } 180 181 // Use the height specified, zero being the default 182 final int dividerHeight = a.getDimensionPixelSize( 183 com.android.internal.R.styleable.ListView_dividerHeight, 0); 184 if (dividerHeight != 0) { 185 setDividerHeight(dividerHeight); 186 } 187 188 mHeaderDividersEnabled = a.getBoolean(R.styleable.ListView_headerDividersEnabled, true); 189 mFooterDividersEnabled = a.getBoolean(R.styleable.ListView_footerDividersEnabled, true); 190 191 a.recycle(); 192 } 193 194 /** 195 * @return The maximum amount a list view will scroll in response to 196 * an arrow event. 197 */ getMaxScrollAmount()198 public int getMaxScrollAmount() { 199 return (int) (MAX_SCROLL_FACTOR * (mBottom - mTop)); 200 } 201 202 /** 203 * Make sure views are touching the top or bottom edge, as appropriate for 204 * our gravity 205 */ adjustViewsUpOrDown()206 private void adjustViewsUpOrDown() { 207 final int childCount = getChildCount(); 208 int delta; 209 210 if (childCount > 0) { 211 View child; 212 213 if (!mStackFromBottom) { 214 // Uh-oh -- we came up short. Slide all views up to make them 215 // align with the top 216 child = getChildAt(0); 217 delta = child.getTop() - mListPadding.top; 218 if (mFirstPosition != 0) { 219 // It's OK to have some space above the first item if it is 220 // part of the vertical spacing 221 delta -= mDividerHeight; 222 } 223 if (delta < 0) { 224 // We only are looking to see if we are too low, not too high 225 delta = 0; 226 } 227 } else { 228 // we are too high, slide all views down to align with bottom 229 child = getChildAt(childCount - 1); 230 delta = child.getBottom() - (getHeight() - mListPadding.bottom); 231 232 if (mFirstPosition + childCount < mItemCount) { 233 // It's OK to have some space below the last item if it is 234 // part of the vertical spacing 235 delta += mDividerHeight; 236 } 237 238 if (delta > 0) { 239 delta = 0; 240 } 241 } 242 243 if (delta != 0) { 244 offsetChildrenTopAndBottom(-delta); 245 } 246 } 247 } 248 249 /** 250 * Add a fixed view to appear at the top of the list. If this method is 251 * called more than once, the views will appear in the order they were 252 * added. Views added using this call can take focus if they want. 253 * <p> 254 * Note: When first introduced, this method could only be called before 255 * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with 256 * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be 257 * called at any time. If the ListView's adapter does not extend 258 * {@link HeaderViewListAdapter}, it will be wrapped with a supporting 259 * instance of {@link WrapperListAdapter}. 260 * 261 * @param v The view to add. 262 * @param data Data to associate with this view 263 * @param isSelectable whether the item is selectable 264 */ addHeaderView(View v, Object data, boolean isSelectable)265 public void addHeaderView(View v, Object data, boolean isSelectable) { 266 final FixedViewInfo info = new FixedViewInfo(); 267 info.view = v; 268 info.data = data; 269 info.isSelectable = isSelectable; 270 mHeaderViewInfos.add(info); 271 mAreAllItemsSelectable &= isSelectable; 272 273 // Wrap the adapter if it wasn't already wrapped. 274 if (mAdapter != null) { 275 if (!(mAdapter instanceof HeaderViewListAdapter)) { 276 mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter); 277 } 278 279 // In the case of re-adding a header view, or adding one later on, 280 // we need to notify the observer. 281 if (mDataSetObserver != null) { 282 mDataSetObserver.onChanged(); 283 } 284 } 285 } 286 287 /** 288 * Add a fixed view to appear at the top of the list. If addHeaderView is 289 * called more than once, the views will appear in the order they were 290 * added. Views added using this call can take focus if they want. 291 * <p> 292 * Note: When first introduced, this method could only be called before 293 * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with 294 * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be 295 * called at any time. If the ListView's adapter does not extend 296 * {@link HeaderViewListAdapter}, it will be wrapped with a supporting 297 * instance of {@link WrapperListAdapter}. 298 * 299 * @param v The view to add. 300 */ addHeaderView(View v)301 public void addHeaderView(View v) { 302 addHeaderView(v, null, true); 303 } 304 305 @Override getHeaderViewsCount()306 public int getHeaderViewsCount() { 307 return mHeaderViewInfos.size(); 308 } 309 310 /** 311 * Removes a previously-added header view. 312 * 313 * @param v The view to remove 314 * @return true if the view was removed, false if the view was not a header 315 * view 316 */ removeHeaderView(View v)317 public boolean removeHeaderView(View v) { 318 if (mHeaderViewInfos.size() > 0) { 319 boolean result = false; 320 if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeHeader(v)) { 321 if (mDataSetObserver != null) { 322 mDataSetObserver.onChanged(); 323 } 324 result = true; 325 } 326 removeFixedViewInfo(v, mHeaderViewInfos); 327 return result; 328 } 329 return false; 330 } 331 removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where)332 private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where) { 333 int len = where.size(); 334 for (int i = 0; i < len; ++i) { 335 FixedViewInfo info = where.get(i); 336 if (info.view == v) { 337 where.remove(i); 338 break; 339 } 340 } 341 } 342 343 /** 344 * Add a fixed view to appear at the bottom of the list. If addFooterView is 345 * called more than once, the views will appear in the order they were 346 * added. Views added using this call can take focus if they want. 347 * <p> 348 * Note: When first introduced, this method could only be called before 349 * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with 350 * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be 351 * called at any time. If the ListView's adapter does not extend 352 * {@link HeaderViewListAdapter}, it will be wrapped with a supporting 353 * instance of {@link WrapperListAdapter}. 354 * 355 * @param v The view to add. 356 * @param data Data to associate with this view 357 * @param isSelectable true if the footer view can be selected 358 */ addFooterView(View v, Object data, boolean isSelectable)359 public void addFooterView(View v, Object data, boolean isSelectable) { 360 final FixedViewInfo info = new FixedViewInfo(); 361 info.view = v; 362 info.data = data; 363 info.isSelectable = isSelectable; 364 mFooterViewInfos.add(info); 365 mAreAllItemsSelectable &= isSelectable; 366 367 // Wrap the adapter if it wasn't already wrapped. 368 if (mAdapter != null) { 369 if (!(mAdapter instanceof HeaderViewListAdapter)) { 370 mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter); 371 } 372 373 // In the case of re-adding a footer view, or adding one later on, 374 // we need to notify the observer. 375 if (mDataSetObserver != null) { 376 mDataSetObserver.onChanged(); 377 } 378 } 379 } 380 381 /** 382 * Add a fixed view to appear at the bottom of the list. If addFooterView is 383 * called more than once, the views will appear in the order they were 384 * added. Views added using this call can take focus if they want. 385 * <p> 386 * Note: When first introduced, this method could only be called before 387 * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with 388 * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be 389 * called at any time. If the ListView's adapter does not extend 390 * {@link HeaderViewListAdapter}, it will be wrapped with a supporting 391 * instance of {@link WrapperListAdapter}. 392 * 393 * @param v The view to add. 394 */ addFooterView(View v)395 public void addFooterView(View v) { 396 addFooterView(v, null, true); 397 } 398 399 @Override getFooterViewsCount()400 public int getFooterViewsCount() { 401 return mFooterViewInfos.size(); 402 } 403 404 /** 405 * Removes a previously-added footer view. 406 * 407 * @param v The view to remove 408 * @return 409 * true if the view was removed, false if the view was not a footer view 410 */ removeFooterView(View v)411 public boolean removeFooterView(View v) { 412 if (mFooterViewInfos.size() > 0) { 413 boolean result = false; 414 if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeFooter(v)) { 415 if (mDataSetObserver != null) { 416 mDataSetObserver.onChanged(); 417 } 418 result = true; 419 } 420 removeFixedViewInfo(v, mFooterViewInfos); 421 return result; 422 } 423 return false; 424 } 425 426 /** 427 * Returns the adapter currently in use in this ListView. The returned adapter 428 * might not be the same adapter passed to {@link #setAdapter(ListAdapter)} but 429 * might be a {@link WrapperListAdapter}. 430 * 431 * @return The adapter currently used to display data in this ListView. 432 * 433 * @see #setAdapter(ListAdapter) 434 */ 435 @Override getAdapter()436 public ListAdapter getAdapter() { 437 return mAdapter; 438 } 439 440 /** 441 * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService 442 * through the specified intent. 443 * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to. 444 */ 445 @android.view.RemotableViewMethod setRemoteViewsAdapter(Intent intent)446 public void setRemoteViewsAdapter(Intent intent) { 447 super.setRemoteViewsAdapter(intent); 448 } 449 450 /** 451 * Sets the data behind this ListView. 452 * 453 * The adapter passed to this method may be wrapped by a {@link WrapperListAdapter}, 454 * depending on the ListView features currently in use. For instance, adding 455 * headers and/or footers will cause the adapter to be wrapped. 456 * 457 * @param adapter The ListAdapter which is responsible for maintaining the 458 * data backing this list and for producing a view to represent an 459 * item in that data set. 460 * 461 * @see #getAdapter() 462 */ 463 @Override setAdapter(ListAdapter adapter)464 public void setAdapter(ListAdapter adapter) { 465 if (mAdapter != null && mDataSetObserver != null) { 466 mAdapter.unregisterDataSetObserver(mDataSetObserver); 467 } 468 469 resetList(); 470 mRecycler.clear(); 471 472 if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) { 473 mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); 474 } else { 475 mAdapter = adapter; 476 } 477 478 mOldSelectedPosition = INVALID_POSITION; 479 mOldSelectedRowId = INVALID_ROW_ID; 480 481 // AbsListView#setAdapter will update choice mode states. 482 super.setAdapter(adapter); 483 484 if (mAdapter != null) { 485 mAreAllItemsSelectable = mAdapter.areAllItemsEnabled(); 486 mOldItemCount = mItemCount; 487 mItemCount = mAdapter.getCount(); 488 checkFocus(); 489 490 mDataSetObserver = new AdapterDataSetObserver(); 491 mAdapter.registerDataSetObserver(mDataSetObserver); 492 493 mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); 494 495 int position; 496 if (mStackFromBottom) { 497 position = lookForSelectablePosition(mItemCount - 1, false); 498 } else { 499 position = lookForSelectablePosition(0, true); 500 } 501 setSelectedPositionInt(position); 502 setNextSelectedPositionInt(position); 503 504 if (mItemCount == 0) { 505 // Nothing selected 506 checkSelectionChanged(); 507 } 508 } else { 509 mAreAllItemsSelectable = true; 510 checkFocus(); 511 // Nothing selected 512 checkSelectionChanged(); 513 } 514 515 requestLayout(); 516 } 517 518 /** 519 * The list is empty. Clear everything out. 520 */ 521 @Override resetList()522 void resetList() { 523 // The parent's resetList() will remove all views from the layout so we need to 524 // cleanup the state of our footers and headers 525 clearRecycledState(mHeaderViewInfos); 526 clearRecycledState(mFooterViewInfos); 527 528 super.resetList(); 529 530 mLayoutMode = LAYOUT_NORMAL; 531 } 532 clearRecycledState(ArrayList<FixedViewInfo> infos)533 private void clearRecycledState(ArrayList<FixedViewInfo> infos) { 534 if (infos != null) { 535 final int count = infos.size(); 536 537 for (int i = 0; i < count; i++) { 538 final View child = infos.get(i).view; 539 final LayoutParams p = (LayoutParams) child.getLayoutParams(); 540 if (p != null) { 541 p.recycledHeaderFooter = false; 542 } 543 } 544 } 545 } 546 547 /** 548 * @return Whether the list needs to show the top fading edge 549 */ showingTopFadingEdge()550 private boolean showingTopFadingEdge() { 551 final int listTop = mScrollY + mListPadding.top; 552 return (mFirstPosition > 0) || (getChildAt(0).getTop() > listTop); 553 } 554 555 /** 556 * @return Whether the list needs to show the bottom fading edge 557 */ showingBottomFadingEdge()558 private boolean showingBottomFadingEdge() { 559 final int childCount = getChildCount(); 560 final int bottomOfBottomChild = getChildAt(childCount - 1).getBottom(); 561 final int lastVisiblePosition = mFirstPosition + childCount - 1; 562 563 final int listBottom = mScrollY + getHeight() - mListPadding.bottom; 564 565 return (lastVisiblePosition < mItemCount - 1) 566 || (bottomOfBottomChild < listBottom); 567 } 568 569 570 @Override requestChildRectangleOnScreen(View child, Rect rect, boolean immediate)571 public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { 572 573 int rectTopWithinChild = rect.top; 574 575 // offset so rect is in coordinates of the this view 576 rect.offset(child.getLeft(), child.getTop()); 577 rect.offset(-child.getScrollX(), -child.getScrollY()); 578 579 final int height = getHeight(); 580 int listUnfadedTop = getScrollY(); 581 int listUnfadedBottom = listUnfadedTop + height; 582 final int fadingEdge = getVerticalFadingEdgeLength(); 583 584 if (showingTopFadingEdge()) { 585 // leave room for top fading edge as long as rect isn't at very top 586 if ((mSelectedPosition > 0) || (rectTopWithinChild > fadingEdge)) { 587 listUnfadedTop += fadingEdge; 588 } 589 } 590 591 int childCount = getChildCount(); 592 int bottomOfBottomChild = getChildAt(childCount - 1).getBottom(); 593 594 if (showingBottomFadingEdge()) { 595 // leave room for bottom fading edge as long as rect isn't at very bottom 596 if ((mSelectedPosition < mItemCount - 1) 597 || (rect.bottom < (bottomOfBottomChild - fadingEdge))) { 598 listUnfadedBottom -= fadingEdge; 599 } 600 } 601 602 int scrollYDelta = 0; 603 604 if (rect.bottom > listUnfadedBottom && rect.top > listUnfadedTop) { 605 // need to MOVE DOWN to get it in view: move down just enough so 606 // that the entire rectangle is in view (or at least the first 607 // screen size chunk). 608 609 if (rect.height() > height) { 610 // just enough to get screen size chunk on 611 scrollYDelta += (rect.top - listUnfadedTop); 612 } else { 613 // get entire rect at bottom of screen 614 scrollYDelta += (rect.bottom - listUnfadedBottom); 615 } 616 617 // make sure we aren't scrolling beyond the end of our children 618 int distanceToBottom = bottomOfBottomChild - listUnfadedBottom; 619 scrollYDelta = Math.min(scrollYDelta, distanceToBottom); 620 } else if (rect.top < listUnfadedTop && rect.bottom < listUnfadedBottom) { 621 // need to MOVE UP to get it in view: move up just enough so that 622 // entire rectangle is in view (or at least the first screen 623 // size chunk of it). 624 625 if (rect.height() > height) { 626 // screen size chunk 627 scrollYDelta -= (listUnfadedBottom - rect.bottom); 628 } else { 629 // entire rect at top 630 scrollYDelta -= (listUnfadedTop - rect.top); 631 } 632 633 // make sure we aren't scrolling any further than the top our children 634 int top = getChildAt(0).getTop(); 635 int deltaToTop = top - listUnfadedTop; 636 scrollYDelta = Math.max(scrollYDelta, deltaToTop); 637 } 638 639 final boolean scroll = scrollYDelta != 0; 640 if (scroll) { 641 scrollListItemsBy(-scrollYDelta); 642 positionSelector(INVALID_POSITION, child); 643 mSelectedTop = child.getTop(); 644 invalidate(); 645 } 646 return scroll; 647 } 648 649 /** 650 * {@inheritDoc} 651 */ 652 @Override fillGap(boolean down)653 void fillGap(boolean down) { 654 final int count = getChildCount(); 655 if (down) { 656 int paddingTop = 0; 657 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 658 paddingTop = getListPaddingTop(); 659 } 660 final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight : 661 paddingTop; 662 fillDown(mFirstPosition + count, startOffset); 663 correctTooHigh(getChildCount()); 664 } else { 665 int paddingBottom = 0; 666 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 667 paddingBottom = getListPaddingBottom(); 668 } 669 final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight : 670 getHeight() - paddingBottom; 671 fillUp(mFirstPosition - 1, startOffset); 672 correctTooLow(getChildCount()); 673 } 674 } 675 676 /** 677 * Fills the list from pos down to the end of the list view. 678 * 679 * @param pos The first position to put in the list 680 * 681 * @param nextTop The location where the top of the item associated with pos 682 * should be drawn 683 * 684 * @return The view that is currently selected, if it happens to be in the 685 * range that we draw. 686 */ fillDown(int pos, int nextTop)687 private View fillDown(int pos, int nextTop) { 688 View selectedView = null; 689 690 int end = (mBottom - mTop); 691 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 692 end -= mListPadding.bottom; 693 } 694 695 while (nextTop < end && pos < mItemCount) { 696 // is this the selected item? 697 boolean selected = pos == mSelectedPosition; 698 View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); 699 700 nextTop = child.getBottom() + mDividerHeight; 701 if (selected) { 702 selectedView = child; 703 } 704 pos++; 705 } 706 707 setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); 708 return selectedView; 709 } 710 711 /** 712 * Fills the list from pos up to the top of the list view. 713 * 714 * @param pos The first position to put in the list 715 * 716 * @param nextBottom The location where the bottom of the item associated 717 * with pos should be drawn 718 * 719 * @return The view that is currently selected 720 */ fillUp(int pos, int nextBottom)721 private View fillUp(int pos, int nextBottom) { 722 View selectedView = null; 723 724 int end = 0; 725 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 726 end = mListPadding.top; 727 } 728 729 while (nextBottom > end && pos >= 0) { 730 // is this the selected item? 731 boolean selected = pos == mSelectedPosition; 732 View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected); 733 nextBottom = child.getTop() - mDividerHeight; 734 if (selected) { 735 selectedView = child; 736 } 737 pos--; 738 } 739 740 mFirstPosition = pos + 1; 741 setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); 742 return selectedView; 743 } 744 745 /** 746 * Fills the list from top to bottom, starting with mFirstPosition 747 * 748 * @param nextTop The location where the top of the first item should be 749 * drawn 750 * 751 * @return The view that is currently selected 752 */ fillFromTop(int nextTop)753 private View fillFromTop(int nextTop) { 754 mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); 755 mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); 756 if (mFirstPosition < 0) { 757 mFirstPosition = 0; 758 } 759 return fillDown(mFirstPosition, nextTop); 760 } 761 762 763 /** 764 * Put mSelectedPosition in the middle of the screen and then build up and 765 * down from there. This method forces mSelectedPosition to the center. 766 * 767 * @param childrenTop Top of the area in which children can be drawn, as 768 * measured in pixels 769 * @param childrenBottom Bottom of the area in which children can be drawn, 770 * as measured in pixels 771 * @return Currently selected view 772 */ fillFromMiddle(int childrenTop, int childrenBottom)773 private View fillFromMiddle(int childrenTop, int childrenBottom) { 774 int height = childrenBottom - childrenTop; 775 776 int position = reconcileSelectedPosition(); 777 778 View sel = makeAndAddView(position, childrenTop, true, 779 mListPadding.left, true); 780 mFirstPosition = position; 781 782 int selHeight = sel.getMeasuredHeight(); 783 if (selHeight <= height) { 784 sel.offsetTopAndBottom((height - selHeight) / 2); 785 } 786 787 fillAboveAndBelow(sel, position); 788 789 if (!mStackFromBottom) { 790 correctTooHigh(getChildCount()); 791 } else { 792 correctTooLow(getChildCount()); 793 } 794 795 return sel; 796 } 797 798 /** 799 * Once the selected view as been placed, fill up the visible area above and 800 * below it. 801 * 802 * @param sel The selected view 803 * @param position The position corresponding to sel 804 */ fillAboveAndBelow(View sel, int position)805 private void fillAboveAndBelow(View sel, int position) { 806 final int dividerHeight = mDividerHeight; 807 if (!mStackFromBottom) { 808 fillUp(position - 1, sel.getTop() - dividerHeight); 809 adjustViewsUpOrDown(); 810 fillDown(position + 1, sel.getBottom() + dividerHeight); 811 } else { 812 fillDown(position + 1, sel.getBottom() + dividerHeight); 813 adjustViewsUpOrDown(); 814 fillUp(position - 1, sel.getTop() - dividerHeight); 815 } 816 } 817 818 819 /** 820 * Fills the grid based on positioning the new selection at a specific 821 * location. The selection may be moved so that it does not intersect the 822 * faded edges. The grid is then filled upwards and downwards from there. 823 * 824 * @param selectedTop Where the selected item should be 825 * @param childrenTop Where to start drawing children 826 * @param childrenBottom Last pixel where children can be drawn 827 * @return The view that currently has selection 828 */ fillFromSelection(int selectedTop, int childrenTop, int childrenBottom)829 private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) { 830 int fadingEdgeLength = getVerticalFadingEdgeLength(); 831 final int selectedPosition = mSelectedPosition; 832 833 View sel; 834 835 final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, 836 selectedPosition); 837 final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength, 838 selectedPosition); 839 840 sel = makeAndAddView(selectedPosition, selectedTop, true, mListPadding.left, true); 841 842 843 // Some of the newly selected item extends below the bottom of the list 844 if (sel.getBottom() > bottomSelectionPixel) { 845 // Find space available above the selection into which we can scroll 846 // upwards 847 final int spaceAbove = sel.getTop() - topSelectionPixel; 848 849 // Find space required to bring the bottom of the selected item 850 // fully into view 851 final int spaceBelow = sel.getBottom() - bottomSelectionPixel; 852 final int offset = Math.min(spaceAbove, spaceBelow); 853 854 // Now offset the selected item to get it into view 855 sel.offsetTopAndBottom(-offset); 856 } else if (sel.getTop() < topSelectionPixel) { 857 // Find space required to bring the top of the selected item fully 858 // into view 859 final int spaceAbove = topSelectionPixel - sel.getTop(); 860 861 // Find space available below the selection into which we can scroll 862 // downwards 863 final int spaceBelow = bottomSelectionPixel - sel.getBottom(); 864 final int offset = Math.min(spaceAbove, spaceBelow); 865 866 // Offset the selected item to get it into view 867 sel.offsetTopAndBottom(offset); 868 } 869 870 // Fill in views above and below 871 fillAboveAndBelow(sel, selectedPosition); 872 873 if (!mStackFromBottom) { 874 correctTooHigh(getChildCount()); 875 } else { 876 correctTooLow(getChildCount()); 877 } 878 879 return sel; 880 } 881 882 /** 883 * Calculate the bottom-most pixel we can draw the selection into 884 * 885 * @param childrenBottom Bottom pixel were children can be drawn 886 * @param fadingEdgeLength Length of the fading edge in pixels, if present 887 * @param selectedPosition The position that will be selected 888 * @return The bottom-most pixel we can draw the selection into 889 */ getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength, int selectedPosition)890 private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength, 891 int selectedPosition) { 892 int bottomSelectionPixel = childrenBottom; 893 if (selectedPosition != mItemCount - 1) { 894 bottomSelectionPixel -= fadingEdgeLength; 895 } 896 return bottomSelectionPixel; 897 } 898 899 /** 900 * Calculate the top-most pixel we can draw the selection into 901 * 902 * @param childrenTop Top pixel were children can be drawn 903 * @param fadingEdgeLength Length of the fading edge in pixels, if present 904 * @param selectedPosition The position that will be selected 905 * @return The top-most pixel we can draw the selection into 906 */ getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int selectedPosition)907 private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int selectedPosition) { 908 // first pixel we can draw the selection into 909 int topSelectionPixel = childrenTop; 910 if (selectedPosition > 0) { 911 topSelectionPixel += fadingEdgeLength; 912 } 913 return topSelectionPixel; 914 } 915 916 /** 917 * Smoothly scroll to the specified adapter position. The view will 918 * scroll such that the indicated position is displayed. 919 * @param position Scroll to this adapter position. 920 */ 921 @android.view.RemotableViewMethod smoothScrollToPosition(int position)922 public void smoothScrollToPosition(int position) { 923 super.smoothScrollToPosition(position); 924 } 925 926 /** 927 * Smoothly scroll to the specified adapter position offset. The view will 928 * scroll such that the indicated position is displayed. 929 * @param offset The amount to offset from the adapter position to scroll to. 930 */ 931 @android.view.RemotableViewMethod smoothScrollByOffset(int offset)932 public void smoothScrollByOffset(int offset) { 933 super.smoothScrollByOffset(offset); 934 } 935 936 /** 937 * Fills the list based on positioning the new selection relative to the old 938 * selection. The new selection will be placed at, above, or below the 939 * location of the new selection depending on how the selection is moving. 940 * The selection will then be pinned to the visible part of the screen, 941 * excluding the edges that are faded. The list is then filled upwards and 942 * downwards from there. 943 * 944 * @param oldSel The old selected view. Useful for trying to put the new 945 * selection in the same place 946 * @param newSel The view that is to become selected. Useful for trying to 947 * put the new selection in the same place 948 * @param delta Which way we are moving 949 * @param childrenTop Where to start drawing children 950 * @param childrenBottom Last pixel where children can be drawn 951 * @return The view that currently has selection 952 */ moveSelection(View oldSel, View newSel, int delta, int childrenTop, int childrenBottom)953 private View moveSelection(View oldSel, View newSel, int delta, int childrenTop, 954 int childrenBottom) { 955 int fadingEdgeLength = getVerticalFadingEdgeLength(); 956 final int selectedPosition = mSelectedPosition; 957 958 View sel; 959 960 final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, 961 selectedPosition); 962 final int bottomSelectionPixel = getBottomSelectionPixel(childrenTop, fadingEdgeLength, 963 selectedPosition); 964 965 if (delta > 0) { 966 /* 967 * Case 1: Scrolling down. 968 */ 969 970 /* 971 * Before After 972 * | | | | 973 * +-------+ +-------+ 974 * | A | | A | 975 * | 1 | => +-------+ 976 * +-------+ | B | 977 * | B | | 2 | 978 * +-------+ +-------+ 979 * | | | | 980 * 981 * Try to keep the top of the previously selected item where it was. 982 * oldSel = A 983 * sel = B 984 */ 985 986 // Put oldSel (A) where it belongs 987 oldSel = makeAndAddView(selectedPosition - 1, oldSel.getTop(), true, 988 mListPadding.left, false); 989 990 final int dividerHeight = mDividerHeight; 991 992 // Now put the new selection (B) below that 993 sel = makeAndAddView(selectedPosition, oldSel.getBottom() + dividerHeight, true, 994 mListPadding.left, true); 995 996 // Some of the newly selected item extends below the bottom of the list 997 if (sel.getBottom() > bottomSelectionPixel) { 998 999 // Find space available above the selection into which we can scroll upwards 1000 int spaceAbove = sel.getTop() - topSelectionPixel; 1001 1002 // Find space required to bring the bottom of the selected item fully into view 1003 int spaceBelow = sel.getBottom() - bottomSelectionPixel; 1004 1005 // Don't scroll more than half the height of the list 1006 int halfVerticalSpace = (childrenBottom - childrenTop) / 2; 1007 int offset = Math.min(spaceAbove, spaceBelow); 1008 offset = Math.min(offset, halfVerticalSpace); 1009 1010 // We placed oldSel, so offset that item 1011 oldSel.offsetTopAndBottom(-offset); 1012 // Now offset the selected item to get it into view 1013 sel.offsetTopAndBottom(-offset); 1014 } 1015 1016 // Fill in views above and below 1017 if (!mStackFromBottom) { 1018 fillUp(mSelectedPosition - 2, sel.getTop() - dividerHeight); 1019 adjustViewsUpOrDown(); 1020 fillDown(mSelectedPosition + 1, sel.getBottom() + dividerHeight); 1021 } else { 1022 fillDown(mSelectedPosition + 1, sel.getBottom() + dividerHeight); 1023 adjustViewsUpOrDown(); 1024 fillUp(mSelectedPosition - 2, sel.getTop() - dividerHeight); 1025 } 1026 } else if (delta < 0) { 1027 /* 1028 * Case 2: Scrolling up. 1029 */ 1030 1031 /* 1032 * Before After 1033 * | | | | 1034 * +-------+ +-------+ 1035 * | A | | A | 1036 * +-------+ => | 1 | 1037 * | B | +-------+ 1038 * | 2 | | B | 1039 * +-------+ +-------+ 1040 * | | | | 1041 * 1042 * Try to keep the top of the item about to become selected where it was. 1043 * newSel = A 1044 * olSel = B 1045 */ 1046 1047 if (newSel != null) { 1048 // Try to position the top of newSel (A) where it was before it was selected 1049 sel = makeAndAddView(selectedPosition, newSel.getTop(), true, mListPadding.left, 1050 true); 1051 } else { 1052 // If (A) was not on screen and so did not have a view, position 1053 // it above the oldSel (B) 1054 sel = makeAndAddView(selectedPosition, oldSel.getTop(), false, mListPadding.left, 1055 true); 1056 } 1057 1058 // Some of the newly selected item extends above the top of the list 1059 if (sel.getTop() < topSelectionPixel) { 1060 // Find space required to bring the top of the selected item fully into view 1061 int spaceAbove = topSelectionPixel - sel.getTop(); 1062 1063 // Find space available below the selection into which we can scroll downwards 1064 int spaceBelow = bottomSelectionPixel - sel.getBottom(); 1065 1066 // Don't scroll more than half the height of the list 1067 int halfVerticalSpace = (childrenBottom - childrenTop) / 2; 1068 int offset = Math.min(spaceAbove, spaceBelow); 1069 offset = Math.min(offset, halfVerticalSpace); 1070 1071 // Offset the selected item to get it into view 1072 sel.offsetTopAndBottom(offset); 1073 } 1074 1075 // Fill in views above and below 1076 fillAboveAndBelow(sel, selectedPosition); 1077 } else { 1078 1079 int oldTop = oldSel.getTop(); 1080 1081 /* 1082 * Case 3: Staying still 1083 */ 1084 sel = makeAndAddView(selectedPosition, oldTop, true, mListPadding.left, true); 1085 1086 // We're staying still... 1087 if (oldTop < childrenTop) { 1088 // ... but the top of the old selection was off screen. 1089 // (This can happen if the data changes size out from under us) 1090 int newBottom = sel.getBottom(); 1091 if (newBottom < childrenTop + 20) { 1092 // Not enough visible -- bring it onscreen 1093 sel.offsetTopAndBottom(childrenTop - sel.getTop()); 1094 } 1095 } 1096 1097 // Fill in views above and below 1098 fillAboveAndBelow(sel, selectedPosition); 1099 } 1100 1101 return sel; 1102 } 1103 1104 private class FocusSelector implements Runnable { 1105 private int mPosition; 1106 private int mPositionTop; 1107 setup(int position, int top)1108 public FocusSelector setup(int position, int top) { 1109 mPosition = position; 1110 mPositionTop = top; 1111 return this; 1112 } 1113 run()1114 public void run() { 1115 setSelectionFromTop(mPosition, mPositionTop); 1116 } 1117 } 1118 1119 @Override onSizeChanged(int w, int h, int oldw, int oldh)1120 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1121 if (getChildCount() > 0) { 1122 View focusedChild = getFocusedChild(); 1123 if (focusedChild != null) { 1124 final int childPosition = mFirstPosition + indexOfChild(focusedChild); 1125 final int childBottom = focusedChild.getBottom(); 1126 final int offset = Math.max(0, childBottom - (h - mPaddingTop)); 1127 final int top = focusedChild.getTop() - offset; 1128 if (mFocusSelector == null) { 1129 mFocusSelector = new FocusSelector(); 1130 } 1131 post(mFocusSelector.setup(childPosition, top)); 1132 } 1133 } 1134 super.onSizeChanged(w, h, oldw, oldh); 1135 } 1136 1137 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)1138 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1139 // Sets up mListPadding 1140 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1141 1142 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 1143 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 1144 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 1145 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 1146 1147 int childWidth = 0; 1148 int childHeight = 0; 1149 int childState = 0; 1150 1151 mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); 1152 if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED || 1153 heightMode == MeasureSpec.UNSPECIFIED)) { 1154 final View child = obtainView(0, mIsScrap); 1155 1156 measureScrapChild(child, 0, widthMeasureSpec); 1157 1158 childWidth = child.getMeasuredWidth(); 1159 childHeight = child.getMeasuredHeight(); 1160 childState = combineMeasuredStates(childState, child.getMeasuredState()); 1161 1162 if (recycleOnMeasure() && mRecycler.shouldRecycleViewType( 1163 ((LayoutParams) child.getLayoutParams()).viewType)) { 1164 mRecycler.addScrapView(child, 0); 1165 } 1166 } 1167 1168 if (widthMode == MeasureSpec.UNSPECIFIED) { 1169 widthSize = mListPadding.left + mListPadding.right + childWidth + 1170 getVerticalScrollbarWidth(); 1171 } else { 1172 widthSize |= (childState&MEASURED_STATE_MASK); 1173 } 1174 1175 if (heightMode == MeasureSpec.UNSPECIFIED) { 1176 heightSize = mListPadding.top + mListPadding.bottom + childHeight + 1177 getVerticalFadingEdgeLength() * 2; 1178 } 1179 1180 if (heightMode == MeasureSpec.AT_MOST) { 1181 // TODO: after first layout we should maybe start at the first visible position, not 0 1182 heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); 1183 } 1184 1185 setMeasuredDimension(widthSize , heightSize); 1186 mWidthMeasureSpec = widthMeasureSpec; 1187 } 1188 measureScrapChild(View child, int position, int widthMeasureSpec)1189 private void measureScrapChild(View child, int position, int widthMeasureSpec) { 1190 LayoutParams p = (LayoutParams) child.getLayoutParams(); 1191 if (p == null) { 1192 p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); 1193 child.setLayoutParams(p); 1194 } 1195 p.viewType = mAdapter.getItemViewType(position); 1196 p.forceAdd = true; 1197 1198 int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, 1199 mListPadding.left + mListPadding.right, p.width); 1200 int lpHeight = p.height; 1201 int childHeightSpec; 1202 if (lpHeight > 0) { 1203 childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); 1204 } else { 1205 childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 1206 } 1207 child.measure(childWidthSpec, childHeightSpec); 1208 } 1209 1210 /** 1211 * @return True to recycle the views used to measure this ListView in 1212 * UNSPECIFIED/AT_MOST modes, false otherwise. 1213 * @hide 1214 */ 1215 @ViewDebug.ExportedProperty(category = "list") recycleOnMeasure()1216 protected boolean recycleOnMeasure() { 1217 return true; 1218 } 1219 1220 /** 1221 * Measures the height of the given range of children (inclusive) and 1222 * returns the height with this ListView's padding and divider heights 1223 * included. If maxHeight is provided, the measuring will stop when the 1224 * current height reaches maxHeight. 1225 * 1226 * @param widthMeasureSpec The width measure spec to be given to a child's 1227 * {@link View#measure(int, int)}. 1228 * @param startPosition The position of the first child to be shown. 1229 * @param endPosition The (inclusive) position of the last child to be 1230 * shown. Specify {@link #NO_POSITION} if the last child should be 1231 * the last available child from the adapter. 1232 * @param maxHeight The maximum height that will be returned (if all the 1233 * children don't fit in this value, this value will be 1234 * returned). 1235 * @param disallowPartialChildPosition In general, whether the returned 1236 * height should only contain entire children. This is more 1237 * powerful--it is the first inclusive position at which partial 1238 * children will not be allowed. Example: it looks nice to have 1239 * at least 3 completely visible children, and in portrait this 1240 * will most likely fit; but in landscape there could be times 1241 * when even 2 children can not be completely shown, so a value 1242 * of 2 (remember, inclusive) would be good (assuming 1243 * startPosition is 0). 1244 * @return The height of this ListView with the given children. 1245 */ measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, final int maxHeight, int disallowPartialChildPosition)1246 final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, 1247 final int maxHeight, int disallowPartialChildPosition) { 1248 1249 final ListAdapter adapter = mAdapter; 1250 if (adapter == null) { 1251 return mListPadding.top + mListPadding.bottom; 1252 } 1253 1254 // Include the padding of the list 1255 int returnedHeight = mListPadding.top + mListPadding.bottom; 1256 final int dividerHeight = ((mDividerHeight > 0) && mDivider != null) ? mDividerHeight : 0; 1257 // The previous height value that was less than maxHeight and contained 1258 // no partial children 1259 int prevHeightWithoutPartialChild = 0; 1260 int i; 1261 View child; 1262 1263 // mItemCount - 1 since endPosition parameter is inclusive 1264 endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; 1265 final AbsListView.RecycleBin recycleBin = mRecycler; 1266 final boolean recyle = recycleOnMeasure(); 1267 final boolean[] isScrap = mIsScrap; 1268 1269 for (i = startPosition; i <= endPosition; ++i) { 1270 child = obtainView(i, isScrap); 1271 1272 measureScrapChild(child, i, widthMeasureSpec); 1273 1274 if (i > 0) { 1275 // Count the divider for all but one child 1276 returnedHeight += dividerHeight; 1277 } 1278 1279 // Recycle the view before we possibly return from the method 1280 if (recyle && recycleBin.shouldRecycleViewType( 1281 ((LayoutParams) child.getLayoutParams()).viewType)) { 1282 recycleBin.addScrapView(child, -1); 1283 } 1284 1285 returnedHeight += child.getMeasuredHeight(); 1286 1287 if (returnedHeight >= maxHeight) { 1288 // We went over, figure out which height to return. If returnedHeight > maxHeight, 1289 // then the i'th position did not fit completely. 1290 return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1) 1291 && (i > disallowPartialChildPosition) // We've past the min pos 1292 && (prevHeightWithoutPartialChild > 0) // We have a prev height 1293 && (returnedHeight != maxHeight) // i'th child did not fit completely 1294 ? prevHeightWithoutPartialChild 1295 : maxHeight; 1296 } 1297 1298 if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { 1299 prevHeightWithoutPartialChild = returnedHeight; 1300 } 1301 } 1302 1303 // At this point, we went through the range of children, and they each 1304 // completely fit, so return the returnedHeight 1305 return returnedHeight; 1306 } 1307 1308 @Override findMotionRow(int y)1309 int findMotionRow(int y) { 1310 int childCount = getChildCount(); 1311 if (childCount > 0) { 1312 if (!mStackFromBottom) { 1313 for (int i = 0; i < childCount; i++) { 1314 View v = getChildAt(i); 1315 if (y <= v.getBottom()) { 1316 return mFirstPosition + i; 1317 } 1318 } 1319 } else { 1320 for (int i = childCount - 1; i >= 0; i--) { 1321 View v = getChildAt(i); 1322 if (y >= v.getTop()) { 1323 return mFirstPosition + i; 1324 } 1325 } 1326 } 1327 } 1328 return INVALID_POSITION; 1329 } 1330 1331 /** 1332 * Put a specific item at a specific location on the screen and then build 1333 * up and down from there. 1334 * 1335 * @param position The reference view to use as the starting point 1336 * @param top Pixel offset from the top of this view to the top of the 1337 * reference view. 1338 * 1339 * @return The selected view, or null if the selected view is outside the 1340 * visible area. 1341 */ fillSpecific(int position, int top)1342 private View fillSpecific(int position, int top) { 1343 boolean tempIsSelected = position == mSelectedPosition; 1344 View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected); 1345 // Possibly changed again in fillUp if we add rows above this one. 1346 mFirstPosition = position; 1347 1348 View above; 1349 View below; 1350 1351 final int dividerHeight = mDividerHeight; 1352 if (!mStackFromBottom) { 1353 above = fillUp(position - 1, temp.getTop() - dividerHeight); 1354 // This will correct for the top of the first view not touching the top of the list 1355 adjustViewsUpOrDown(); 1356 below = fillDown(position + 1, temp.getBottom() + dividerHeight); 1357 int childCount = getChildCount(); 1358 if (childCount > 0) { 1359 correctTooHigh(childCount); 1360 } 1361 } else { 1362 below = fillDown(position + 1, temp.getBottom() + dividerHeight); 1363 // This will correct for the bottom of the last view not touching the bottom of the list 1364 adjustViewsUpOrDown(); 1365 above = fillUp(position - 1, temp.getTop() - dividerHeight); 1366 int childCount = getChildCount(); 1367 if (childCount > 0) { 1368 correctTooLow(childCount); 1369 } 1370 } 1371 1372 if (tempIsSelected) { 1373 return temp; 1374 } else if (above != null) { 1375 return above; 1376 } else { 1377 return below; 1378 } 1379 } 1380 1381 /** 1382 * Check if we have dragged the bottom of the list too high (we have pushed the 1383 * top element off the top of the screen when we did not need to). Correct by sliding 1384 * everything back down. 1385 * 1386 * @param childCount Number of children 1387 */ correctTooHigh(int childCount)1388 private void correctTooHigh(int childCount) { 1389 // First see if the last item is visible. If it is not, it is OK for the 1390 // top of the list to be pushed up. 1391 int lastPosition = mFirstPosition + childCount - 1; 1392 if (lastPosition == mItemCount - 1 && childCount > 0) { 1393 1394 // Get the last child ... 1395 final View lastChild = getChildAt(childCount - 1); 1396 1397 // ... and its bottom edge 1398 final int lastBottom = lastChild.getBottom(); 1399 1400 // This is bottom of our drawable area 1401 final int end = (mBottom - mTop) - mListPadding.bottom; 1402 1403 // This is how far the bottom edge of the last view is from the bottom of the 1404 // drawable area 1405 int bottomOffset = end - lastBottom; 1406 View firstChild = getChildAt(0); 1407 final int firstTop = firstChild.getTop(); 1408 1409 // Make sure we are 1) Too high, and 2) Either there are more rows above the 1410 // first row or the first row is scrolled off the top of the drawable area 1411 if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top)) { 1412 if (mFirstPosition == 0) { 1413 // Don't pull the top too far down 1414 bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop); 1415 } 1416 // Move everything down 1417 offsetChildrenTopAndBottom(bottomOffset); 1418 if (mFirstPosition > 0) { 1419 // Fill the gap that was opened above mFirstPosition with more rows, if 1420 // possible 1421 fillUp(mFirstPosition - 1, firstChild.getTop() - mDividerHeight); 1422 // Close up the remaining gap 1423 adjustViewsUpOrDown(); 1424 } 1425 1426 } 1427 } 1428 } 1429 1430 /** 1431 * Check if we have dragged the bottom of the list too low (we have pushed the 1432 * bottom element off the bottom of the screen when we did not need to). Correct by sliding 1433 * everything back up. 1434 * 1435 * @param childCount Number of children 1436 */ correctTooLow(int childCount)1437 private void correctTooLow(int childCount) { 1438 // First see if the first item is visible. If it is not, it is OK for the 1439 // bottom of the list to be pushed down. 1440 if (mFirstPosition == 0 && childCount > 0) { 1441 1442 // Get the first child ... 1443 final View firstChild = getChildAt(0); 1444 1445 // ... and its top edge 1446 final int firstTop = firstChild.getTop(); 1447 1448 // This is top of our drawable area 1449 final int start = mListPadding.top; 1450 1451 // This is bottom of our drawable area 1452 final int end = (mBottom - mTop) - mListPadding.bottom; 1453 1454 // This is how far the top edge of the first view is from the top of the 1455 // drawable area 1456 int topOffset = firstTop - start; 1457 View lastChild = getChildAt(childCount - 1); 1458 final int lastBottom = lastChild.getBottom(); 1459 int lastPosition = mFirstPosition + childCount - 1; 1460 1461 // Make sure we are 1) Too low, and 2) Either there are more rows below the 1462 // last row or the last row is scrolled off the bottom of the drawable area 1463 if (topOffset > 0) { 1464 if (lastPosition < mItemCount - 1 || lastBottom > end) { 1465 if (lastPosition == mItemCount - 1) { 1466 // Don't pull the bottom too far up 1467 topOffset = Math.min(topOffset, lastBottom - end); 1468 } 1469 // Move everything up 1470 offsetChildrenTopAndBottom(-topOffset); 1471 if (lastPosition < mItemCount - 1) { 1472 // Fill the gap that was opened below the last position with more rows, if 1473 // possible 1474 fillDown(lastPosition + 1, lastChild.getBottom() + mDividerHeight); 1475 // Close up the remaining gap 1476 adjustViewsUpOrDown(); 1477 } 1478 } else if (lastPosition == mItemCount - 1) { 1479 adjustViewsUpOrDown(); 1480 } 1481 } 1482 } 1483 } 1484 1485 @Override layoutChildren()1486 protected void layoutChildren() { 1487 final boolean blockLayoutRequests = mBlockLayoutRequests; 1488 if (blockLayoutRequests) { 1489 return; 1490 } 1491 1492 mBlockLayoutRequests = true; 1493 1494 try { 1495 super.layoutChildren(); 1496 1497 invalidate(); 1498 1499 if (mAdapter == null) { 1500 resetList(); 1501 invokeOnItemScrollListener(); 1502 return; 1503 } 1504 1505 final int childrenTop = mListPadding.top; 1506 final int childrenBottom = mBottom - mTop - mListPadding.bottom; 1507 final int childCount = getChildCount(); 1508 1509 int index = 0; 1510 int delta = 0; 1511 1512 View sel; 1513 View oldSel = null; 1514 View oldFirst = null; 1515 View newSel = null; 1516 1517 // Remember stuff we will need down below 1518 switch (mLayoutMode) { 1519 case LAYOUT_SET_SELECTION: 1520 index = mNextSelectedPosition - mFirstPosition; 1521 if (index >= 0 && index < childCount) { 1522 newSel = getChildAt(index); 1523 } 1524 break; 1525 case LAYOUT_FORCE_TOP: 1526 case LAYOUT_FORCE_BOTTOM: 1527 case LAYOUT_SPECIFIC: 1528 case LAYOUT_SYNC: 1529 break; 1530 case LAYOUT_MOVE_SELECTION: 1531 default: 1532 // Remember the previously selected view 1533 index = mSelectedPosition - mFirstPosition; 1534 if (index >= 0 && index < childCount) { 1535 oldSel = getChildAt(index); 1536 } 1537 1538 // Remember the previous first child 1539 oldFirst = getChildAt(0); 1540 1541 if (mNextSelectedPosition >= 0) { 1542 delta = mNextSelectedPosition - mSelectedPosition; 1543 } 1544 1545 // Caution: newSel might be null 1546 newSel = getChildAt(index + delta); 1547 } 1548 1549 1550 boolean dataChanged = mDataChanged; 1551 if (dataChanged) { 1552 handleDataChanged(); 1553 } 1554 1555 // Handle the empty set by removing all views that are visible 1556 // and calling it a day 1557 if (mItemCount == 0) { 1558 resetList(); 1559 invokeOnItemScrollListener(); 1560 return; 1561 } else if (mItemCount != mAdapter.getCount()) { 1562 throw new IllegalStateException("The content of the adapter has changed but " 1563 + "ListView did not receive a notification. Make sure the content of " 1564 + "your adapter is not modified from a background thread, but only from " 1565 + "the UI thread. Make sure your adapter calls notifyDataSetChanged() " 1566 + "when its content changes. [in ListView(" + getId() + ", " + getClass() 1567 + ") with Adapter(" + mAdapter.getClass() + ")]"); 1568 } 1569 1570 setSelectedPositionInt(mNextSelectedPosition); 1571 1572 AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null; 1573 View accessibilityFocusLayoutRestoreView = null; 1574 int accessibilityFocusPosition = INVALID_POSITION; 1575 1576 // Remember which child, if any, had accessibility focus. This must 1577 // occur before recycling any views, since that will clear 1578 // accessibility focus. 1579 final ViewRootImpl viewRootImpl = getViewRootImpl(); 1580 if (viewRootImpl != null) { 1581 final View focusHost = viewRootImpl.getAccessibilityFocusedHost(); 1582 if (focusHost != null) { 1583 final View focusChild = getAccessibilityFocusedChild(focusHost); 1584 if (focusChild != null) { 1585 if (!dataChanged || isDirectChildHeaderOrFooter(focusChild) 1586 || focusChild.hasTransientState() || mAdapterHasStableIds) { 1587 // The views won't be changing, so try to maintain 1588 // focus on the current host and virtual view. 1589 accessibilityFocusLayoutRestoreView = focusHost; 1590 accessibilityFocusLayoutRestoreNode = viewRootImpl 1591 .getAccessibilityFocusedVirtualView(); 1592 } 1593 1594 // If all else fails, maintain focus at the same 1595 // position. 1596 accessibilityFocusPosition = getPositionForView(focusChild); 1597 } 1598 } 1599 } 1600 1601 View focusLayoutRestoreDirectChild = null; 1602 View focusLayoutRestoreView = null; 1603 1604 // Take focus back to us temporarily to avoid the eventual call to 1605 // clear focus when removing the focused child below from messing 1606 // things up when ViewAncestor assigns focus back to someone else. 1607 final View focusedChild = getFocusedChild(); 1608 if (focusedChild != null) { 1609 // TODO: in some cases focusedChild.getParent() == null 1610 1611 // We can remember the focused view to restore after re-layout 1612 // if the data hasn't changed, or if the focused position is a 1613 // header or footer. 1614 if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) { 1615 focusLayoutRestoreDirectChild = focusedChild; 1616 // Remember the specific view that had focus. 1617 focusLayoutRestoreView = findFocus(); 1618 if (focusLayoutRestoreView != null) { 1619 // Tell it we are going to mess with it. 1620 focusLayoutRestoreView.onStartTemporaryDetach(); 1621 } 1622 } 1623 requestFocus(); 1624 } 1625 1626 // Pull all children into the RecycleBin. 1627 // These views will be reused if possible 1628 final int firstPosition = mFirstPosition; 1629 final RecycleBin recycleBin = mRecycler; 1630 if (dataChanged) { 1631 for (int i = 0; i < childCount; i++) { 1632 recycleBin.addScrapView(getChildAt(i), firstPosition+i); 1633 } 1634 } else { 1635 recycleBin.fillActiveViews(childCount, firstPosition); 1636 } 1637 1638 // Clear out old views 1639 detachAllViewsFromParent(); 1640 recycleBin.removeSkippedScrap(); 1641 1642 switch (mLayoutMode) { 1643 case LAYOUT_SET_SELECTION: 1644 if (newSel != null) { 1645 sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom); 1646 } else { 1647 sel = fillFromMiddle(childrenTop, childrenBottom); 1648 } 1649 break; 1650 case LAYOUT_SYNC: 1651 sel = fillSpecific(mSyncPosition, mSpecificTop); 1652 break; 1653 case LAYOUT_FORCE_BOTTOM: 1654 sel = fillUp(mItemCount - 1, childrenBottom); 1655 adjustViewsUpOrDown(); 1656 break; 1657 case LAYOUT_FORCE_TOP: 1658 mFirstPosition = 0; 1659 sel = fillFromTop(childrenTop); 1660 adjustViewsUpOrDown(); 1661 break; 1662 case LAYOUT_SPECIFIC: 1663 sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop); 1664 break; 1665 case LAYOUT_MOVE_SELECTION: 1666 sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom); 1667 break; 1668 default: 1669 if (childCount == 0) { 1670 if (!mStackFromBottom) { 1671 final int position = lookForSelectablePosition(0, true); 1672 setSelectedPositionInt(position); 1673 sel = fillFromTop(childrenTop); 1674 } else { 1675 final int position = lookForSelectablePosition(mItemCount - 1, false); 1676 setSelectedPositionInt(position); 1677 sel = fillUp(mItemCount - 1, childrenBottom); 1678 } 1679 } else { 1680 if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { 1681 sel = fillSpecific(mSelectedPosition, 1682 oldSel == null ? childrenTop : oldSel.getTop()); 1683 } else if (mFirstPosition < mItemCount) { 1684 sel = fillSpecific(mFirstPosition, 1685 oldFirst == null ? childrenTop : oldFirst.getTop()); 1686 } else { 1687 sel = fillSpecific(0, childrenTop); 1688 } 1689 } 1690 break; 1691 } 1692 1693 // Flush any cached views that did not get reused above 1694 recycleBin.scrapActiveViews(); 1695 1696 if (sel != null) { 1697 // The current selected item should get focus if items are 1698 // focusable. 1699 if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) { 1700 final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild && 1701 focusLayoutRestoreView != null && 1702 focusLayoutRestoreView.requestFocus()) || sel.requestFocus(); 1703 if (!focusWasTaken) { 1704 // Selected item didn't take focus, but we still want to 1705 // make sure something else outside of the selected view 1706 // has focus. 1707 final View focused = getFocusedChild(); 1708 if (focused != null) { 1709 focused.clearFocus(); 1710 } 1711 positionSelector(INVALID_POSITION, sel); 1712 } else { 1713 sel.setSelected(false); 1714 mSelectorRect.setEmpty(); 1715 } 1716 } else { 1717 positionSelector(INVALID_POSITION, sel); 1718 } 1719 mSelectedTop = sel.getTop(); 1720 } else { 1721 final boolean inTouchMode = mTouchMode == TOUCH_MODE_TAP 1722 || mTouchMode == TOUCH_MODE_DONE_WAITING; 1723 if (inTouchMode) { 1724 // If the user's finger is down, select the motion position. 1725 final View child = getChildAt(mMotionPosition - mFirstPosition); 1726 if (child != null) { 1727 positionSelector(mMotionPosition, child); 1728 } 1729 } else if (mSelectorPosition != INVALID_POSITION) { 1730 // If we had previously positioned the selector somewhere, 1731 // put it back there. It might not match up with the data, 1732 // but it's transitioning out so it's not a big deal. 1733 final View child = getChildAt(mSelectorPosition - mFirstPosition); 1734 if (child != null) { 1735 positionSelector(mSelectorPosition, child); 1736 } 1737 } else { 1738 // Otherwise, clear selection. 1739 mSelectedTop = 0; 1740 mSelectorRect.setEmpty(); 1741 } 1742 1743 // Even if there is not selected position, we may need to 1744 // restore focus (i.e. something focusable in touch mode). 1745 if (hasFocus() && focusLayoutRestoreView != null) { 1746 focusLayoutRestoreView.requestFocus(); 1747 } 1748 } 1749 1750 // Attempt to restore accessibility focus, if necessary. 1751 if (viewRootImpl != null) { 1752 final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost(); 1753 if (newAccessibilityFocusedView == null) { 1754 if (accessibilityFocusLayoutRestoreView != null 1755 && accessibilityFocusLayoutRestoreView.isAttachedToWindow()) { 1756 final AccessibilityNodeProvider provider = 1757 accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider(); 1758 if (accessibilityFocusLayoutRestoreNode != null && provider != null) { 1759 final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId( 1760 accessibilityFocusLayoutRestoreNode.getSourceNodeId()); 1761 provider.performAction(virtualViewId, 1762 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); 1763 } else { 1764 accessibilityFocusLayoutRestoreView.requestAccessibilityFocus(); 1765 } 1766 } else if (accessibilityFocusPosition != INVALID_POSITION) { 1767 // Bound the position within the visible children. 1768 final int position = MathUtils.constrain( 1769 accessibilityFocusPosition - mFirstPosition, 0, 1770 getChildCount() - 1); 1771 final View restoreView = getChildAt(position); 1772 if (restoreView != null) { 1773 restoreView.requestAccessibilityFocus(); 1774 } 1775 } 1776 } 1777 } 1778 1779 // Tell focus view we are done mucking with it, if it is still in 1780 // our view hierarchy. 1781 if (focusLayoutRestoreView != null 1782 && focusLayoutRestoreView.getWindowToken() != null) { 1783 focusLayoutRestoreView.onFinishTemporaryDetach(); 1784 } 1785 1786 mLayoutMode = LAYOUT_NORMAL; 1787 mDataChanged = false; 1788 if (mPositionScrollAfterLayout != null) { 1789 post(mPositionScrollAfterLayout); 1790 mPositionScrollAfterLayout = null; 1791 } 1792 mNeedSync = false; 1793 setNextSelectedPositionInt(mSelectedPosition); 1794 1795 updateScrollIndicators(); 1796 1797 if (mItemCount > 0) { 1798 checkSelectionChanged(); 1799 } 1800 1801 invokeOnItemScrollListener(); 1802 } finally { 1803 if (!blockLayoutRequests) { 1804 mBlockLayoutRequests = false; 1805 } 1806 } 1807 } 1808 1809 /** 1810 * @param child a direct child of this list. 1811 * @return Whether child is a header or footer view. 1812 */ isDirectChildHeaderOrFooter(View child)1813 private boolean isDirectChildHeaderOrFooter(View child) { 1814 final ArrayList<FixedViewInfo> headers = mHeaderViewInfos; 1815 final int numHeaders = headers.size(); 1816 for (int i = 0; i < numHeaders; i++) { 1817 if (child == headers.get(i).view) { 1818 return true; 1819 } 1820 } 1821 1822 final ArrayList<FixedViewInfo> footers = mFooterViewInfos; 1823 final int numFooters = footers.size(); 1824 for (int i = 0; i < numFooters; i++) { 1825 if (child == footers.get(i).view) { 1826 return true; 1827 } 1828 } 1829 1830 return false; 1831 } 1832 1833 /** 1834 * Obtain the view and add it to our list of children. The view can be made 1835 * fresh, converted from an unused view, or used as is if it was in the 1836 * recycle bin. 1837 * 1838 * @param position Logical position in the list 1839 * @param y Top or bottom edge of the view to add 1840 * @param flow If flow is true, align top edge to y. If false, align bottom 1841 * edge to y. 1842 * @param childrenLeft Left edge where children should be positioned 1843 * @param selected Is this position selected? 1844 * @return View that was added 1845 */ makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected)1846 private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, 1847 boolean selected) { 1848 View child; 1849 1850 1851 if (!mDataChanged) { 1852 // Try to use an existing view for this position 1853 child = mRecycler.getActiveView(position); 1854 if (child != null) { 1855 // Found it -- we're using an existing child 1856 // This just needs to be positioned 1857 setupChild(child, position, y, flow, childrenLeft, selected, true); 1858 1859 return child; 1860 } 1861 } 1862 1863 // Make a new view for this position, or convert an unused view if possible 1864 child = obtainView(position, mIsScrap); 1865 1866 // This needs to be positioned and measured 1867 setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); 1868 1869 return child; 1870 } 1871 1872 /** 1873 * Add a view as a child and make sure it is measured (if necessary) and 1874 * positioned properly. 1875 * 1876 * @param child The view to add 1877 * @param position The position of this child 1878 * @param y The y position relative to which this view will be positioned 1879 * @param flowDown If true, align top edge to y. If false, align bottom 1880 * edge to y. 1881 * @param childrenLeft Left edge where children should be positioned 1882 * @param selected Is this position selected? 1883 * @param recycled Has this view been pulled from the recycle bin? If so it 1884 * does not need to be remeasured. 1885 */ setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean recycled)1886 private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, 1887 boolean selected, boolean recycled) { 1888 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem"); 1889 1890 final boolean isSelected = selected && shouldShowSelector(); 1891 final boolean updateChildSelected = isSelected != child.isSelected(); 1892 final int mode = mTouchMode; 1893 final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && 1894 mMotionPosition == position; 1895 final boolean updateChildPressed = isPressed != child.isPressed(); 1896 final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); 1897 1898 // Respect layout params that are already in the view. Otherwise make some up... 1899 // noinspection unchecked 1900 AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); 1901 if (p == null) { 1902 p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); 1903 } 1904 p.viewType = mAdapter.getItemViewType(position); 1905 1906 if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter && 1907 p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { 1908 attachViewToParent(child, flowDown ? -1 : 0, p); 1909 } else { 1910 p.forceAdd = false; 1911 if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { 1912 p.recycledHeaderFooter = true; 1913 } 1914 addViewInLayout(child, flowDown ? -1 : 0, p, true); 1915 } 1916 1917 if (updateChildSelected) { 1918 child.setSelected(isSelected); 1919 } 1920 1921 if (updateChildPressed) { 1922 child.setPressed(isPressed); 1923 } 1924 1925 if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { 1926 if (child instanceof Checkable) { 1927 ((Checkable) child).setChecked(mCheckStates.get(position)); 1928 } else if (getContext().getApplicationInfo().targetSdkVersion 1929 >= android.os.Build.VERSION_CODES.HONEYCOMB) { 1930 child.setActivated(mCheckStates.get(position)); 1931 } 1932 } 1933 1934 if (needToMeasure) { 1935 int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, 1936 mListPadding.left + mListPadding.right, p.width); 1937 int lpHeight = p.height; 1938 int childHeightSpec; 1939 if (lpHeight > 0) { 1940 childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); 1941 } else { 1942 childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 1943 } 1944 child.measure(childWidthSpec, childHeightSpec); 1945 } else { 1946 cleanupLayoutState(child); 1947 } 1948 1949 final int w = child.getMeasuredWidth(); 1950 final int h = child.getMeasuredHeight(); 1951 final int childTop = flowDown ? y : y - h; 1952 1953 if (needToMeasure) { 1954 final int childRight = childrenLeft + w; 1955 final int childBottom = childTop + h; 1956 child.layout(childrenLeft, childTop, childRight, childBottom); 1957 } else { 1958 child.offsetLeftAndRight(childrenLeft - child.getLeft()); 1959 child.offsetTopAndBottom(childTop - child.getTop()); 1960 } 1961 1962 if (mCachingStarted && !child.isDrawingCacheEnabled()) { 1963 child.setDrawingCacheEnabled(true); 1964 } 1965 1966 if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition) 1967 != position) { 1968 child.jumpDrawablesToCurrentState(); 1969 } 1970 1971 Trace.traceEnd(Trace.TRACE_TAG_VIEW); 1972 } 1973 1974 @Override canAnimate()1975 protected boolean canAnimate() { 1976 return super.canAnimate() && mItemCount > 0; 1977 } 1978 1979 /** 1980 * Sets the currently selected item. If in touch mode, the item will not be selected 1981 * but it will still be positioned appropriately. If the specified selection position 1982 * is less than 0, then the item at position 0 will be selected. 1983 * 1984 * @param position Index (starting at 0) of the data item to be selected. 1985 */ 1986 @Override setSelection(int position)1987 public void setSelection(int position) { 1988 setSelectionFromTop(position, 0); 1989 } 1990 1991 /** 1992 * Makes the item at the supplied position selected. 1993 * 1994 * @param position the position of the item to select 1995 */ 1996 @Override setSelectionInt(int position)1997 void setSelectionInt(int position) { 1998 setNextSelectedPositionInt(position); 1999 boolean awakeScrollbars = false; 2000 2001 final int selectedPosition = mSelectedPosition; 2002 2003 if (selectedPosition >= 0) { 2004 if (position == selectedPosition - 1) { 2005 awakeScrollbars = true; 2006 } else if (position == selectedPosition + 1) { 2007 awakeScrollbars = true; 2008 } 2009 } 2010 2011 if (mPositionScroller != null) { 2012 mPositionScroller.stop(); 2013 } 2014 2015 layoutChildren(); 2016 2017 if (awakeScrollbars) { 2018 awakenScrollBars(); 2019 } 2020 } 2021 2022 /** 2023 * Find a position that can be selected (i.e., is not a separator). 2024 * 2025 * @param position The starting position to look at. 2026 * @param lookDown Whether to look down for other positions. 2027 * @return The next selectable position starting at position and then searching either up or 2028 * down. Returns {@link #INVALID_POSITION} if nothing can be found. 2029 */ 2030 @Override lookForSelectablePosition(int position, boolean lookDown)2031 int lookForSelectablePosition(int position, boolean lookDown) { 2032 final ListAdapter adapter = mAdapter; 2033 if (adapter == null || isInTouchMode()) { 2034 return INVALID_POSITION; 2035 } 2036 2037 final int count = adapter.getCount(); 2038 if (!mAreAllItemsSelectable) { 2039 if (lookDown) { 2040 position = Math.max(0, position); 2041 while (position < count && !adapter.isEnabled(position)) { 2042 position++; 2043 } 2044 } else { 2045 position = Math.min(position, count - 1); 2046 while (position >= 0 && !adapter.isEnabled(position)) { 2047 position--; 2048 } 2049 } 2050 } 2051 2052 if (position < 0 || position >= count) { 2053 return INVALID_POSITION; 2054 } 2055 2056 return position; 2057 } 2058 2059 /** 2060 * Find a position that can be selected (i.e., is not a separator). If there 2061 * are no selectable positions in the specified direction from the starting 2062 * position, searches in the opposite direction from the starting position 2063 * to the current position. 2064 * 2065 * @param current the current position 2066 * @param position the starting position 2067 * @param lookDown whether to look down for other positions 2068 * @return the next selectable position, or {@link #INVALID_POSITION} if 2069 * nothing can be found 2070 */ lookForSelectablePositionAfter(int current, int position, boolean lookDown)2071 int lookForSelectablePositionAfter(int current, int position, boolean lookDown) { 2072 final ListAdapter adapter = mAdapter; 2073 if (adapter == null || isInTouchMode()) { 2074 return INVALID_POSITION; 2075 } 2076 2077 // First check after the starting position in the specified direction. 2078 final int after = lookForSelectablePosition(position, lookDown); 2079 if (after != INVALID_POSITION) { 2080 return after; 2081 } 2082 2083 // Then check between the starting position and the current position. 2084 final int count = adapter.getCount(); 2085 current = MathUtils.constrain(current, -1, count - 1); 2086 if (lookDown) { 2087 position = Math.min(position - 1, count - 1); 2088 while ((position > current) && !adapter.isEnabled(position)) { 2089 position--; 2090 } 2091 if (position <= current) { 2092 return INVALID_POSITION; 2093 } 2094 } else { 2095 position = Math.max(0, position + 1); 2096 while ((position < current) && !adapter.isEnabled(position)) { 2097 position++; 2098 } 2099 if (position >= current) { 2100 return INVALID_POSITION; 2101 } 2102 } 2103 2104 return position; 2105 } 2106 2107 /** 2108 * setSelectionAfterHeaderView set the selection to be the first list item 2109 * after the header views. 2110 */ setSelectionAfterHeaderView()2111 public void setSelectionAfterHeaderView() { 2112 final int count = mHeaderViewInfos.size(); 2113 if (count > 0) { 2114 mNextSelectedPosition = 0; 2115 return; 2116 } 2117 2118 if (mAdapter != null) { 2119 setSelection(count); 2120 } else { 2121 mNextSelectedPosition = count; 2122 mLayoutMode = LAYOUT_SET_SELECTION; 2123 } 2124 2125 } 2126 2127 @Override dispatchKeyEvent(KeyEvent event)2128 public boolean dispatchKeyEvent(KeyEvent event) { 2129 // Dispatch in the normal way 2130 boolean handled = super.dispatchKeyEvent(event); 2131 if (!handled) { 2132 // If we didn't handle it... 2133 View focused = getFocusedChild(); 2134 if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) { 2135 // ... and our focused child didn't handle it 2136 // ... give it to ourselves so we can scroll if necessary 2137 handled = onKeyDown(event.getKeyCode(), event); 2138 } 2139 } 2140 return handled; 2141 } 2142 2143 @Override onKeyDown(int keyCode, KeyEvent event)2144 public boolean onKeyDown(int keyCode, KeyEvent event) { 2145 return commonKey(keyCode, 1, event); 2146 } 2147 2148 @Override onKeyMultiple(int keyCode, int repeatCount, KeyEvent event)2149 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { 2150 return commonKey(keyCode, repeatCount, event); 2151 } 2152 2153 @Override onKeyUp(int keyCode, KeyEvent event)2154 public boolean onKeyUp(int keyCode, KeyEvent event) { 2155 return commonKey(keyCode, 1, event); 2156 } 2157 commonKey(int keyCode, int count, KeyEvent event)2158 private boolean commonKey(int keyCode, int count, KeyEvent event) { 2159 if (mAdapter == null || !isAttachedToWindow()) { 2160 return false; 2161 } 2162 2163 if (mDataChanged) { 2164 layoutChildren(); 2165 } 2166 2167 boolean handled = false; 2168 int action = event.getAction(); 2169 2170 if (action != KeyEvent.ACTION_UP) { 2171 switch (keyCode) { 2172 case KeyEvent.KEYCODE_DPAD_UP: 2173 if (event.hasNoModifiers()) { 2174 handled = resurrectSelectionIfNeeded(); 2175 if (!handled) { 2176 while (count-- > 0) { 2177 if (arrowScroll(FOCUS_UP)) { 2178 handled = true; 2179 } else { 2180 break; 2181 } 2182 } 2183 } 2184 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 2185 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 2186 } 2187 break; 2188 2189 case KeyEvent.KEYCODE_DPAD_DOWN: 2190 if (event.hasNoModifiers()) { 2191 handled = resurrectSelectionIfNeeded(); 2192 if (!handled) { 2193 while (count-- > 0) { 2194 if (arrowScroll(FOCUS_DOWN)) { 2195 handled = true; 2196 } else { 2197 break; 2198 } 2199 } 2200 } 2201 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 2202 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 2203 } 2204 break; 2205 2206 case KeyEvent.KEYCODE_DPAD_LEFT: 2207 if (event.hasNoModifiers()) { 2208 handled = handleHorizontalFocusWithinListItem(View.FOCUS_LEFT); 2209 } 2210 break; 2211 2212 case KeyEvent.KEYCODE_DPAD_RIGHT: 2213 if (event.hasNoModifiers()) { 2214 handled = handleHorizontalFocusWithinListItem(View.FOCUS_RIGHT); 2215 } 2216 break; 2217 2218 case KeyEvent.KEYCODE_DPAD_CENTER: 2219 case KeyEvent.KEYCODE_ENTER: 2220 if (event.hasNoModifiers()) { 2221 handled = resurrectSelectionIfNeeded(); 2222 if (!handled 2223 && event.getRepeatCount() == 0 && getChildCount() > 0) { 2224 keyPressed(); 2225 handled = true; 2226 } 2227 } 2228 break; 2229 2230 case KeyEvent.KEYCODE_SPACE: 2231 if (mPopup == null || !mPopup.isShowing()) { 2232 if (event.hasNoModifiers()) { 2233 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN); 2234 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { 2235 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP); 2236 } 2237 handled = true; 2238 } 2239 break; 2240 2241 case KeyEvent.KEYCODE_PAGE_UP: 2242 if (event.hasNoModifiers()) { 2243 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP); 2244 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 2245 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 2246 } 2247 break; 2248 2249 case KeyEvent.KEYCODE_PAGE_DOWN: 2250 if (event.hasNoModifiers()) { 2251 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN); 2252 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 2253 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 2254 } 2255 break; 2256 2257 case KeyEvent.KEYCODE_MOVE_HOME: 2258 if (event.hasNoModifiers()) { 2259 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 2260 } 2261 break; 2262 2263 case KeyEvent.KEYCODE_MOVE_END: 2264 if (event.hasNoModifiers()) { 2265 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 2266 } 2267 break; 2268 2269 case KeyEvent.KEYCODE_TAB: 2270 // XXX Sometimes it is useful to be able to TAB through the items in 2271 // a ListView sequentially. Unfortunately this can create an 2272 // asymmetry in TAB navigation order unless the list selection 2273 // always reverts to the top or bottom when receiving TAB focus from 2274 // another widget. Leaving this behavior disabled for now but 2275 // perhaps it should be configurable (and more comprehensive). 2276 if (false) { 2277 if (event.hasNoModifiers()) { 2278 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_DOWN); 2279 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { 2280 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_UP); 2281 } 2282 } 2283 break; 2284 } 2285 } 2286 2287 if (handled) { 2288 return true; 2289 } 2290 2291 if (sendToTextFilter(keyCode, count, event)) { 2292 return true; 2293 } 2294 2295 switch (action) { 2296 case KeyEvent.ACTION_DOWN: 2297 return super.onKeyDown(keyCode, event); 2298 2299 case KeyEvent.ACTION_UP: 2300 return super.onKeyUp(keyCode, event); 2301 2302 case KeyEvent.ACTION_MULTIPLE: 2303 return super.onKeyMultiple(keyCode, count, event); 2304 2305 default: // shouldn't happen 2306 return false; 2307 } 2308 } 2309 2310 /** 2311 * Scrolls up or down by the number of items currently present on screen. 2312 * 2313 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 2314 * @return whether selection was moved 2315 */ pageScroll(int direction)2316 boolean pageScroll(int direction) { 2317 final int nextPage; 2318 final boolean down; 2319 2320 if (direction == FOCUS_UP) { 2321 nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1); 2322 down = false; 2323 } else if (direction == FOCUS_DOWN) { 2324 nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1); 2325 down = true; 2326 } else { 2327 return false; 2328 } 2329 2330 if (nextPage >= 0) { 2331 final int position = lookForSelectablePositionAfter(mSelectedPosition, nextPage, down); 2332 if (position >= 0) { 2333 mLayoutMode = LAYOUT_SPECIFIC; 2334 mSpecificTop = mPaddingTop + getVerticalFadingEdgeLength(); 2335 2336 if (down && (position > (mItemCount - getChildCount()))) { 2337 mLayoutMode = LAYOUT_FORCE_BOTTOM; 2338 } 2339 2340 if (!down && (position < getChildCount())) { 2341 mLayoutMode = LAYOUT_FORCE_TOP; 2342 } 2343 2344 setSelectionInt(position); 2345 invokeOnItemScrollListener(); 2346 if (!awakenScrollBars()) { 2347 invalidate(); 2348 } 2349 2350 return true; 2351 } 2352 } 2353 2354 return false; 2355 } 2356 2357 /** 2358 * Go to the last or first item if possible (not worrying about panning 2359 * across or navigating within the internal focus of the currently selected 2360 * item.) 2361 * 2362 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 2363 * @return whether selection was moved 2364 */ fullScroll(int direction)2365 boolean fullScroll(int direction) { 2366 boolean moved = false; 2367 if (direction == FOCUS_UP) { 2368 if (mSelectedPosition != 0) { 2369 final int position = lookForSelectablePositionAfter(mSelectedPosition, 0, true); 2370 if (position >= 0) { 2371 mLayoutMode = LAYOUT_FORCE_TOP; 2372 setSelectionInt(position); 2373 invokeOnItemScrollListener(); 2374 } 2375 moved = true; 2376 } 2377 } else if (direction == FOCUS_DOWN) { 2378 final int lastItem = (mItemCount - 1); 2379 if (mSelectedPosition < lastItem) { 2380 final int position = lookForSelectablePositionAfter( 2381 mSelectedPosition, lastItem, false); 2382 if (position >= 0) { 2383 mLayoutMode = LAYOUT_FORCE_BOTTOM; 2384 setSelectionInt(position); 2385 invokeOnItemScrollListener(); 2386 } 2387 moved = true; 2388 } 2389 } 2390 2391 if (moved && !awakenScrollBars()) { 2392 awakenScrollBars(); 2393 invalidate(); 2394 } 2395 2396 return moved; 2397 } 2398 2399 /** 2400 * To avoid horizontal focus searches changing the selected item, we 2401 * manually focus search within the selected item (as applicable), and 2402 * prevent focus from jumping to something within another item. 2403 * @param direction one of {View.FOCUS_LEFT, View.FOCUS_RIGHT} 2404 * @return Whether this consumes the key event. 2405 */ handleHorizontalFocusWithinListItem(int direction)2406 private boolean handleHorizontalFocusWithinListItem(int direction) { 2407 if (direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) { 2408 throw new IllegalArgumentException("direction must be one of" 2409 + " {View.FOCUS_LEFT, View.FOCUS_RIGHT}"); 2410 } 2411 2412 final int numChildren = getChildCount(); 2413 if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) { 2414 final View selectedView = getSelectedView(); 2415 if (selectedView != null && selectedView.hasFocus() && 2416 selectedView instanceof ViewGroup) { 2417 2418 final View currentFocus = selectedView.findFocus(); 2419 final View nextFocus = FocusFinder.getInstance().findNextFocus( 2420 (ViewGroup) selectedView, currentFocus, direction); 2421 if (nextFocus != null) { 2422 // do the math to get interesting rect in next focus' coordinates 2423 currentFocus.getFocusedRect(mTempRect); 2424 offsetDescendantRectToMyCoords(currentFocus, mTempRect); 2425 offsetRectIntoDescendantCoords(nextFocus, mTempRect); 2426 if (nextFocus.requestFocus(direction, mTempRect)) { 2427 return true; 2428 } 2429 } 2430 // we are blocking the key from being handled (by returning true) 2431 // if the global result is going to be some other view within this 2432 // list. this is to acheive the overall goal of having 2433 // horizontal d-pad navigation remain in the current item. 2434 final View globalNextFocus = FocusFinder.getInstance().findNextFocus( 2435 (ViewGroup) getRootView(), currentFocus, direction); 2436 if (globalNextFocus != null) { 2437 return isViewAncestorOf(globalNextFocus, this); 2438 } 2439 } 2440 } 2441 return false; 2442 } 2443 2444 /** 2445 * Scrolls to the next or previous item if possible. 2446 * 2447 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 2448 * 2449 * @return whether selection was moved 2450 */ arrowScroll(int direction)2451 boolean arrowScroll(int direction) { 2452 try { 2453 mInLayout = true; 2454 final boolean handled = arrowScrollImpl(direction); 2455 if (handled) { 2456 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 2457 } 2458 return handled; 2459 } finally { 2460 mInLayout = false; 2461 } 2462 } 2463 2464 /** 2465 * Used by {@link #arrowScrollImpl(int)} to help determine the next selected position 2466 * to move to. This return a position in the direction given if the selected item 2467 * is fully visible. 2468 * 2469 * @param selectedView Current selected view to move from 2470 * @param selectedPos Current selected position to move from 2471 * @param direction Direction to move in 2472 * @return Desired selected position after moving in the given direction 2473 */ nextSelectedPositionForDirection( View selectedView, int selectedPos, int direction)2474 private final int nextSelectedPositionForDirection( 2475 View selectedView, int selectedPos, int direction) { 2476 int nextSelected; 2477 2478 if (direction == View.FOCUS_DOWN) { 2479 final int listBottom = getHeight() - mListPadding.bottom; 2480 if (selectedView != null && selectedView.getBottom() <= listBottom) { 2481 nextSelected = selectedPos != INVALID_POSITION && selectedPos >= mFirstPosition ? 2482 selectedPos + 1 : 2483 mFirstPosition; 2484 } else { 2485 return INVALID_POSITION; 2486 } 2487 } else { 2488 final int listTop = mListPadding.top; 2489 if (selectedView != null && selectedView.getTop() >= listTop) { 2490 final int lastPos = mFirstPosition + getChildCount() - 1; 2491 nextSelected = selectedPos != INVALID_POSITION && selectedPos <= lastPos ? 2492 selectedPos - 1 : 2493 lastPos; 2494 } else { 2495 return INVALID_POSITION; 2496 } 2497 } 2498 2499 if (nextSelected < 0 || nextSelected >= mAdapter.getCount()) { 2500 return INVALID_POSITION; 2501 } 2502 return lookForSelectablePosition(nextSelected, direction == View.FOCUS_DOWN); 2503 } 2504 2505 /** 2506 * Handle an arrow scroll going up or down. Take into account whether items are selectable, 2507 * whether there are focusable items etc. 2508 * 2509 * @param direction Either {@link android.view.View#FOCUS_UP} or {@link android.view.View#FOCUS_DOWN}. 2510 * @return Whether any scrolling, selection or focus change occured. 2511 */ arrowScrollImpl(int direction)2512 private boolean arrowScrollImpl(int direction) { 2513 if (getChildCount() <= 0) { 2514 return false; 2515 } 2516 2517 View selectedView = getSelectedView(); 2518 int selectedPos = mSelectedPosition; 2519 2520 int nextSelectedPosition = nextSelectedPositionForDirection(selectedView, selectedPos, direction); 2521 int amountToScroll = amountToScroll(direction, nextSelectedPosition); 2522 2523 // if we are moving focus, we may OVERRIDE the default behavior 2524 final ArrowScrollFocusResult focusResult = mItemsCanFocus ? arrowScrollFocused(direction) : null; 2525 if (focusResult != null) { 2526 nextSelectedPosition = focusResult.getSelectedPosition(); 2527 amountToScroll = focusResult.getAmountToScroll(); 2528 } 2529 2530 boolean needToRedraw = focusResult != null; 2531 if (nextSelectedPosition != INVALID_POSITION) { 2532 handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null); 2533 setSelectedPositionInt(nextSelectedPosition); 2534 setNextSelectedPositionInt(nextSelectedPosition); 2535 selectedView = getSelectedView(); 2536 selectedPos = nextSelectedPosition; 2537 if (mItemsCanFocus && focusResult == null) { 2538 // there was no new view found to take focus, make sure we 2539 // don't leave focus with the old selection 2540 final View focused = getFocusedChild(); 2541 if (focused != null) { 2542 focused.clearFocus(); 2543 } 2544 } 2545 needToRedraw = true; 2546 checkSelectionChanged(); 2547 } 2548 2549 if (amountToScroll > 0) { 2550 scrollListItemsBy((direction == View.FOCUS_UP) ? amountToScroll : -amountToScroll); 2551 needToRedraw = true; 2552 } 2553 2554 // if we didn't find a new focusable, make sure any existing focused 2555 // item that was panned off screen gives up focus. 2556 if (mItemsCanFocus && (focusResult == null) 2557 && selectedView != null && selectedView.hasFocus()) { 2558 final View focused = selectedView.findFocus(); 2559 if (!isViewAncestorOf(focused, this) || distanceToView(focused) > 0) { 2560 focused.clearFocus(); 2561 } 2562 } 2563 2564 // if the current selection is panned off, we need to remove the selection 2565 if (nextSelectedPosition == INVALID_POSITION && selectedView != null 2566 && !isViewAncestorOf(selectedView, this)) { 2567 selectedView = null; 2568 hideSelector(); 2569 2570 // but we don't want to set the ressurect position (that would make subsequent 2571 // unhandled key events bring back the item we just scrolled off!) 2572 mResurrectToPosition = INVALID_POSITION; 2573 } 2574 2575 if (needToRedraw) { 2576 if (selectedView != null) { 2577 positionSelectorLikeFocus(selectedPos, selectedView); 2578 mSelectedTop = selectedView.getTop(); 2579 } 2580 if (!awakenScrollBars()) { 2581 invalidate(); 2582 } 2583 invokeOnItemScrollListener(); 2584 return true; 2585 } 2586 2587 return false; 2588 } 2589 2590 /** 2591 * When selection changes, it is possible that the previously selected or the 2592 * next selected item will change its size. If so, we need to offset some folks, 2593 * and re-layout the items as appropriate. 2594 * 2595 * @param selectedView The currently selected view (before changing selection). 2596 * should be <code>null</code> if there was no previous selection. 2597 * @param direction Either {@link android.view.View#FOCUS_UP} or 2598 * {@link android.view.View#FOCUS_DOWN}. 2599 * @param newSelectedPosition The position of the next selection. 2600 * @param newFocusAssigned whether new focus was assigned. This matters because 2601 * when something has focus, we don't want to show selection (ugh). 2602 */ handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition, boolean newFocusAssigned)2603 private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition, 2604 boolean newFocusAssigned) { 2605 if (newSelectedPosition == INVALID_POSITION) { 2606 throw new IllegalArgumentException("newSelectedPosition needs to be valid"); 2607 } 2608 2609 // whether or not we are moving down or up, we want to preserve the 2610 // top of whatever view is on top: 2611 // - moving down: the view that had selection 2612 // - moving up: the view that is getting selection 2613 View topView; 2614 View bottomView; 2615 int topViewIndex, bottomViewIndex; 2616 boolean topSelected = false; 2617 final int selectedIndex = mSelectedPosition - mFirstPosition; 2618 final int nextSelectedIndex = newSelectedPosition - mFirstPosition; 2619 if (direction == View.FOCUS_UP) { 2620 topViewIndex = nextSelectedIndex; 2621 bottomViewIndex = selectedIndex; 2622 topView = getChildAt(topViewIndex); 2623 bottomView = selectedView; 2624 topSelected = true; 2625 } else { 2626 topViewIndex = selectedIndex; 2627 bottomViewIndex = nextSelectedIndex; 2628 topView = selectedView; 2629 bottomView = getChildAt(bottomViewIndex); 2630 } 2631 2632 final int numChildren = getChildCount(); 2633 2634 // start with top view: is it changing size? 2635 if (topView != null) { 2636 topView.setSelected(!newFocusAssigned && topSelected); 2637 measureAndAdjustDown(topView, topViewIndex, numChildren); 2638 } 2639 2640 // is the bottom view changing size? 2641 if (bottomView != null) { 2642 bottomView.setSelected(!newFocusAssigned && !topSelected); 2643 measureAndAdjustDown(bottomView, bottomViewIndex, numChildren); 2644 } 2645 } 2646 2647 /** 2648 * Re-measure a child, and if its height changes, lay it out preserving its 2649 * top, and adjust the children below it appropriately. 2650 * @param child The child 2651 * @param childIndex The view group index of the child. 2652 * @param numChildren The number of children in the view group. 2653 */ measureAndAdjustDown(View child, int childIndex, int numChildren)2654 private void measureAndAdjustDown(View child, int childIndex, int numChildren) { 2655 int oldHeight = child.getHeight(); 2656 measureItem(child); 2657 if (child.getMeasuredHeight() != oldHeight) { 2658 // lay out the view, preserving its top 2659 relayoutMeasuredItem(child); 2660 2661 // adjust views below appropriately 2662 final int heightDelta = child.getMeasuredHeight() - oldHeight; 2663 for (int i = childIndex + 1; i < numChildren; i++) { 2664 getChildAt(i).offsetTopAndBottom(heightDelta); 2665 } 2666 } 2667 } 2668 2669 /** 2670 * Measure a particular list child. 2671 * TODO: unify with setUpChild. 2672 * @param child The child. 2673 */ measureItem(View child)2674 private void measureItem(View child) { 2675 ViewGroup.LayoutParams p = child.getLayoutParams(); 2676 if (p == null) { 2677 p = new ViewGroup.LayoutParams( 2678 ViewGroup.LayoutParams.MATCH_PARENT, 2679 ViewGroup.LayoutParams.WRAP_CONTENT); 2680 } 2681 2682 int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, 2683 mListPadding.left + mListPadding.right, p.width); 2684 int lpHeight = p.height; 2685 int childHeightSpec; 2686 if (lpHeight > 0) { 2687 childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); 2688 } else { 2689 childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 2690 } 2691 child.measure(childWidthSpec, childHeightSpec); 2692 } 2693 2694 /** 2695 * Layout a child that has been measured, preserving its top position. 2696 * TODO: unify with setUpChild. 2697 * @param child The child. 2698 */ relayoutMeasuredItem(View child)2699 private void relayoutMeasuredItem(View child) { 2700 final int w = child.getMeasuredWidth(); 2701 final int h = child.getMeasuredHeight(); 2702 final int childLeft = mListPadding.left; 2703 final int childRight = childLeft + w; 2704 final int childTop = child.getTop(); 2705 final int childBottom = childTop + h; 2706 child.layout(childLeft, childTop, childRight, childBottom); 2707 } 2708 2709 /** 2710 * @return The amount to preview next items when arrow srolling. 2711 */ getArrowScrollPreviewLength()2712 private int getArrowScrollPreviewLength() { 2713 return Math.max(MIN_SCROLL_PREVIEW_PIXELS, getVerticalFadingEdgeLength()); 2714 } 2715 2716 /** 2717 * Determine how much we need to scroll in order to get the next selected view 2718 * visible, with a fading edge showing below as applicable. The amount is 2719 * capped at {@link #getMaxScrollAmount()} . 2720 * 2721 * @param direction either {@link android.view.View#FOCUS_UP} or 2722 * {@link android.view.View#FOCUS_DOWN}. 2723 * @param nextSelectedPosition The position of the next selection, or 2724 * {@link #INVALID_POSITION} if there is no next selectable position 2725 * @return The amount to scroll. Note: this is always positive! Direction 2726 * needs to be taken into account when actually scrolling. 2727 */ amountToScroll(int direction, int nextSelectedPosition)2728 private int amountToScroll(int direction, int nextSelectedPosition) { 2729 final int listBottom = getHeight() - mListPadding.bottom; 2730 final int listTop = mListPadding.top; 2731 2732 int numChildren = getChildCount(); 2733 2734 if (direction == View.FOCUS_DOWN) { 2735 int indexToMakeVisible = numChildren - 1; 2736 if (nextSelectedPosition != INVALID_POSITION) { 2737 indexToMakeVisible = nextSelectedPosition - mFirstPosition; 2738 } 2739 while (numChildren <= indexToMakeVisible) { 2740 // Child to view is not attached yet. 2741 addViewBelow(getChildAt(numChildren - 1), mFirstPosition + numChildren - 1); 2742 numChildren++; 2743 } 2744 final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; 2745 final View viewToMakeVisible = getChildAt(indexToMakeVisible); 2746 2747 int goalBottom = listBottom; 2748 if (positionToMakeVisible < mItemCount - 1) { 2749 goalBottom -= getArrowScrollPreviewLength(); 2750 } 2751 2752 if (viewToMakeVisible.getBottom() <= goalBottom) { 2753 // item is fully visible. 2754 return 0; 2755 } 2756 2757 if (nextSelectedPosition != INVALID_POSITION 2758 && (goalBottom - viewToMakeVisible.getTop()) >= getMaxScrollAmount()) { 2759 // item already has enough of it visible, changing selection is good enough 2760 return 0; 2761 } 2762 2763 int amountToScroll = (viewToMakeVisible.getBottom() - goalBottom); 2764 2765 if ((mFirstPosition + numChildren) == mItemCount) { 2766 // last is last in list -> make sure we don't scroll past it 2767 final int max = getChildAt(numChildren - 1).getBottom() - listBottom; 2768 amountToScroll = Math.min(amountToScroll, max); 2769 } 2770 2771 return Math.min(amountToScroll, getMaxScrollAmount()); 2772 } else { 2773 int indexToMakeVisible = 0; 2774 if (nextSelectedPosition != INVALID_POSITION) { 2775 indexToMakeVisible = nextSelectedPosition - mFirstPosition; 2776 } 2777 while (indexToMakeVisible < 0) { 2778 // Child to view is not attached yet. 2779 addViewAbove(getChildAt(0), mFirstPosition); 2780 mFirstPosition--; 2781 indexToMakeVisible = nextSelectedPosition - mFirstPosition; 2782 } 2783 final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; 2784 final View viewToMakeVisible = getChildAt(indexToMakeVisible); 2785 int goalTop = listTop; 2786 if (positionToMakeVisible > 0) { 2787 goalTop += getArrowScrollPreviewLength(); 2788 } 2789 if (viewToMakeVisible.getTop() >= goalTop) { 2790 // item is fully visible. 2791 return 0; 2792 } 2793 2794 if (nextSelectedPosition != INVALID_POSITION && 2795 (viewToMakeVisible.getBottom() - goalTop) >= getMaxScrollAmount()) { 2796 // item already has enough of it visible, changing selection is good enough 2797 return 0; 2798 } 2799 2800 int amountToScroll = (goalTop - viewToMakeVisible.getTop()); 2801 if (mFirstPosition == 0) { 2802 // first is first in list -> make sure we don't scroll past it 2803 final int max = listTop - getChildAt(0).getTop(); 2804 amountToScroll = Math.min(amountToScroll, max); 2805 } 2806 return Math.min(amountToScroll, getMaxScrollAmount()); 2807 } 2808 } 2809 2810 /** 2811 * Holds results of focus aware arrow scrolling. 2812 */ 2813 static private class ArrowScrollFocusResult { 2814 private int mSelectedPosition; 2815 private int mAmountToScroll; 2816 2817 /** 2818 * How {@link android.widget.ListView#arrowScrollFocused} returns its values. 2819 */ populate(int selectedPosition, int amountToScroll)2820 void populate(int selectedPosition, int amountToScroll) { 2821 mSelectedPosition = selectedPosition; 2822 mAmountToScroll = amountToScroll; 2823 } 2824 getSelectedPosition()2825 public int getSelectedPosition() { 2826 return mSelectedPosition; 2827 } 2828 getAmountToScroll()2829 public int getAmountToScroll() { 2830 return mAmountToScroll; 2831 } 2832 } 2833 2834 /** 2835 * @param direction either {@link android.view.View#FOCUS_UP} or 2836 * {@link android.view.View#FOCUS_DOWN}. 2837 * @return The position of the next selectable position of the views that 2838 * are currently visible, taking into account the fact that there might 2839 * be no selection. Returns {@link #INVALID_POSITION} if there is no 2840 * selectable view on screen in the given direction. 2841 */ lookForSelectablePositionOnScreen(int direction)2842 private int lookForSelectablePositionOnScreen(int direction) { 2843 final int firstPosition = mFirstPosition; 2844 if (direction == View.FOCUS_DOWN) { 2845 int startPos = (mSelectedPosition != INVALID_POSITION) ? 2846 mSelectedPosition + 1 : 2847 firstPosition; 2848 if (startPos >= mAdapter.getCount()) { 2849 return INVALID_POSITION; 2850 } 2851 if (startPos < firstPosition) { 2852 startPos = firstPosition; 2853 } 2854 2855 final int lastVisiblePos = getLastVisiblePosition(); 2856 final ListAdapter adapter = getAdapter(); 2857 for (int pos = startPos; pos <= lastVisiblePos; pos++) { 2858 if (adapter.isEnabled(pos) 2859 && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) { 2860 return pos; 2861 } 2862 } 2863 } else { 2864 int last = firstPosition + getChildCount() - 1; 2865 int startPos = (mSelectedPosition != INVALID_POSITION) ? 2866 mSelectedPosition - 1 : 2867 firstPosition + getChildCount() - 1; 2868 if (startPos < 0 || startPos >= mAdapter.getCount()) { 2869 return INVALID_POSITION; 2870 } 2871 if (startPos > last) { 2872 startPos = last; 2873 } 2874 2875 final ListAdapter adapter = getAdapter(); 2876 for (int pos = startPos; pos >= firstPosition; pos--) { 2877 if (adapter.isEnabled(pos) 2878 && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) { 2879 return pos; 2880 } 2881 } 2882 } 2883 return INVALID_POSITION; 2884 } 2885 2886 /** 2887 * Do an arrow scroll based on focus searching. If a new view is 2888 * given focus, return the selection delta and amount to scroll via 2889 * an {@link ArrowScrollFocusResult}, otherwise, return null. 2890 * 2891 * @param direction either {@link android.view.View#FOCUS_UP} or 2892 * {@link android.view.View#FOCUS_DOWN}. 2893 * @return The result if focus has changed, or <code>null</code>. 2894 */ arrowScrollFocused(final int direction)2895 private ArrowScrollFocusResult arrowScrollFocused(final int direction) { 2896 final View selectedView = getSelectedView(); 2897 View newFocus; 2898 if (selectedView != null && selectedView.hasFocus()) { 2899 View oldFocus = selectedView.findFocus(); 2900 newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction); 2901 } else { 2902 if (direction == View.FOCUS_DOWN) { 2903 final boolean topFadingEdgeShowing = (mFirstPosition > 0); 2904 final int listTop = mListPadding.top + 2905 (topFadingEdgeShowing ? getArrowScrollPreviewLength() : 0); 2906 final int ySearchPoint = 2907 (selectedView != null && selectedView.getTop() > listTop) ? 2908 selectedView.getTop() : 2909 listTop; 2910 mTempRect.set(0, ySearchPoint, 0, ySearchPoint); 2911 } else { 2912 final boolean bottomFadingEdgeShowing = 2913 (mFirstPosition + getChildCount() - 1) < mItemCount; 2914 final int listBottom = getHeight() - mListPadding.bottom - 2915 (bottomFadingEdgeShowing ? getArrowScrollPreviewLength() : 0); 2916 final int ySearchPoint = 2917 (selectedView != null && selectedView.getBottom() < listBottom) ? 2918 selectedView.getBottom() : 2919 listBottom; 2920 mTempRect.set(0, ySearchPoint, 0, ySearchPoint); 2921 } 2922 newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction); 2923 } 2924 2925 if (newFocus != null) { 2926 final int positionOfNewFocus = positionOfNewFocus(newFocus); 2927 2928 // if the focus change is in a different new position, make sure 2929 // we aren't jumping over another selectable position 2930 if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) { 2931 final int selectablePosition = lookForSelectablePositionOnScreen(direction); 2932 if (selectablePosition != INVALID_POSITION && 2933 ((direction == View.FOCUS_DOWN && selectablePosition < positionOfNewFocus) || 2934 (direction == View.FOCUS_UP && selectablePosition > positionOfNewFocus))) { 2935 return null; 2936 } 2937 } 2938 2939 int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus); 2940 2941 final int maxScrollAmount = getMaxScrollAmount(); 2942 if (focusScroll < maxScrollAmount) { 2943 // not moving too far, safe to give next view focus 2944 newFocus.requestFocus(direction); 2945 mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll); 2946 return mArrowScrollFocusResult; 2947 } else if (distanceToView(newFocus) < maxScrollAmount){ 2948 // Case to consider: 2949 // too far to get entire next focusable on screen, but by going 2950 // max scroll amount, we are getting it at least partially in view, 2951 // so give it focus and scroll the max ammount. 2952 newFocus.requestFocus(direction); 2953 mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount); 2954 return mArrowScrollFocusResult; 2955 } 2956 } 2957 return null; 2958 } 2959 2960 /** 2961 * @param newFocus The view that would have focus. 2962 * @return the position that contains newFocus 2963 */ 2964 private int positionOfNewFocus(View newFocus) { 2965 final int numChildren = getChildCount(); 2966 for (int i = 0; i < numChildren; i++) { 2967 final View child = getChildAt(i); 2968 if (isViewAncestorOf(newFocus, child)) { 2969 return mFirstPosition + i; 2970 } 2971 } 2972 throw new IllegalArgumentException("newFocus is not a child of any of the" 2973 + " children of the list!"); 2974 } 2975 2976 /** 2977 * Return true if child is an ancestor of parent, (or equal to the parent). 2978 */ 2979 private boolean isViewAncestorOf(View child, View parent) { 2980 if (child == parent) { 2981 return true; 2982 } 2983 2984 final ViewParent theParent = child.getParent(); 2985 return (theParent instanceof ViewGroup) && isViewAncestorOf((View) theParent, parent); 2986 } 2987 2988 /** 2989 * Determine how much we need to scroll in order to get newFocus in view. 2990 * @param direction either {@link android.view.View#FOCUS_UP} or 2991 * {@link android.view.View#FOCUS_DOWN}. 2992 * @param newFocus The view that would take focus. 2993 * @param positionOfNewFocus The position of the list item containing newFocus 2994 * @return The amount to scroll. Note: this is always positive! Direction 2995 * needs to be taken into account when actually scrolling. 2996 */ 2997 private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) { 2998 int amountToScroll = 0; 2999 newFocus.getDrawingRect(mTempRect); 3000 offsetDescendantRectToMyCoords(newFocus, mTempRect); 3001 if (direction == View.FOCUS_UP) { 3002 if (mTempRect.top < mListPadding.top) { 3003 amountToScroll = mListPadding.top - mTempRect.top; 3004 if (positionOfNewFocus > 0) { 3005 amountToScroll += getArrowScrollPreviewLength(); 3006 } 3007 } 3008 } else { 3009 final int listBottom = getHeight() - mListPadding.bottom; 3010 if (mTempRect.bottom > listBottom) { 3011 amountToScroll = mTempRect.bottom - listBottom; 3012 if (positionOfNewFocus < mItemCount - 1) { 3013 amountToScroll += getArrowScrollPreviewLength(); 3014 } 3015 } 3016 } 3017 return amountToScroll; 3018 } 3019 3020 /** 3021 * Determine the distance to the nearest edge of a view in a particular 3022 * direction. 3023 * 3024 * @param descendant A descendant of this list. 3025 * @return The distance, or 0 if the nearest edge is already on screen. 3026 */ 3027 private int distanceToView(View descendant) { 3028 int distance = 0; 3029 descendant.getDrawingRect(mTempRect); 3030 offsetDescendantRectToMyCoords(descendant, mTempRect); 3031 final int listBottom = mBottom - mTop - mListPadding.bottom; 3032 if (mTempRect.bottom < mListPadding.top) { 3033 distance = mListPadding.top - mTempRect.bottom; 3034 } else if (mTempRect.top > listBottom) { 3035 distance = mTempRect.top - listBottom; 3036 } 3037 return distance; 3038 } 3039 3040 3041 /** 3042 * Scroll the children by amount, adding a view at the end and removing 3043 * views that fall off as necessary. 3044 * 3045 * @param amount The amount (positive or negative) to scroll. 3046 */ 3047 private void scrollListItemsBy(int amount) { 3048 offsetChildrenTopAndBottom(amount); 3049 3050 final int listBottom = getHeight() - mListPadding.bottom; 3051 final int listTop = mListPadding.top; 3052 final AbsListView.RecycleBin recycleBin = mRecycler; 3053 3054 if (amount < 0) { 3055 // shifted items up 3056 3057 // may need to pan views into the bottom space 3058 int numChildren = getChildCount(); 3059 View last = getChildAt(numChildren - 1); 3060 while (last.getBottom() < listBottom) { 3061 final int lastVisiblePosition = mFirstPosition + numChildren - 1; 3062 if (lastVisiblePosition < mItemCount - 1) { 3063 last = addViewBelow(last, lastVisiblePosition); 3064 numChildren++; 3065 } else { 3066 break; 3067 } 3068 } 3069 3070 // may have brought in the last child of the list that is skinnier 3071 // than the fading edge, thereby leaving space at the end. need 3072 // to shift back 3073 if (last.getBottom() < listBottom) { 3074 offsetChildrenTopAndBottom(listBottom - last.getBottom()); 3075 } 3076 3077 // top views may be panned off screen 3078 View first = getChildAt(0); 3079 while (first.getBottom() < listTop) { 3080 AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams(); 3081 if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { 3082 recycleBin.addScrapView(first, mFirstPosition); 3083 } 3084 detachViewFromParent(first); 3085 first = getChildAt(0); 3086 mFirstPosition++; 3087 } 3088 } else { 3089 // shifted items down 3090 View first = getChildAt(0); 3091 3092 // may need to pan views into top 3093 while ((first.getTop() > listTop) && (mFirstPosition > 0)) { 3094 first = addViewAbove(first, mFirstPosition); 3095 mFirstPosition--; 3096 } 3097 3098 // may have brought the very first child of the list in too far and 3099 // need to shift it back 3100 if (first.getTop() > listTop) { 3101 offsetChildrenTopAndBottom(listTop - first.getTop()); 3102 } 3103 3104 int lastIndex = getChildCount() - 1; 3105 View last = getChildAt(lastIndex); 3106 3107 // bottom view may be panned off screen 3108 while (last.getTop() > listBottom) { 3109 AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams(); 3110 if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { 3111 recycleBin.addScrapView(last, mFirstPosition+lastIndex); 3112 } 3113 detachViewFromParent(last); 3114 last = getChildAt(--lastIndex); 3115 } 3116 } 3117 } 3118 3119 private View addViewAbove(View theView, int position) { 3120 int abovePosition = position - 1; 3121 View view = obtainView(abovePosition, mIsScrap); 3122 int edgeOfNewChild = theView.getTop() - mDividerHeight; 3123 setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left, 3124 false, mIsScrap[0]); 3125 return view; 3126 } 3127 3128 private View addViewBelow(View theView, int position) { 3129 int belowPosition = position + 1; 3130 View view = obtainView(belowPosition, mIsScrap); 3131 int edgeOfNewChild = theView.getBottom() + mDividerHeight; 3132 setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left, 3133 false, mIsScrap[0]); 3134 return view; 3135 } 3136 3137 /** 3138 * Indicates that the views created by the ListAdapter can contain focusable 3139 * items. 3140 * 3141 * @param itemsCanFocus true if items can get focus, false otherwise 3142 */ 3143 public void setItemsCanFocus(boolean itemsCanFocus) { 3144 mItemsCanFocus = itemsCanFocus; 3145 if (!itemsCanFocus) { 3146 setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); 3147 } 3148 } 3149 3150 /** 3151 * @return Whether the views created by the ListAdapter can contain focusable 3152 * items. 3153 */ 3154 public boolean getItemsCanFocus() { 3155 return mItemsCanFocus; 3156 } 3157 3158 @Override 3159 public boolean isOpaque() { 3160 boolean retValue = (mCachingActive && mIsCacheColorOpaque && mDividerIsOpaque && 3161 hasOpaqueScrollbars()) || super.isOpaque(); 3162 if (retValue) { 3163 // only return true if the list items cover the entire area of the view 3164 final int listTop = mListPadding != null ? mListPadding.top : mPaddingTop; 3165 View first = getChildAt(0); 3166 if (first == null || first.getTop() > listTop) { 3167 return false; 3168 } 3169 final int listBottom = getHeight() - 3170 (mListPadding != null ? mListPadding.bottom : mPaddingBottom); 3171 View last = getChildAt(getChildCount() - 1); 3172 if (last == null || last.getBottom() < listBottom) { 3173 return false; 3174 } 3175 } 3176 return retValue; 3177 } 3178 3179 @Override 3180 public void setCacheColorHint(int color) { 3181 final boolean opaque = (color >>> 24) == 0xFF; 3182 mIsCacheColorOpaque = opaque; 3183 if (opaque) { 3184 if (mDividerPaint == null) { 3185 mDividerPaint = new Paint(); 3186 } 3187 mDividerPaint.setColor(color); 3188 } 3189 super.setCacheColorHint(color); 3190 } 3191 3192 void drawOverscrollHeader(Canvas canvas, Drawable drawable, Rect bounds) { 3193 final int height = drawable.getMinimumHeight(); 3194 3195 canvas.save(); 3196 canvas.clipRect(bounds); 3197 3198 final int span = bounds.bottom - bounds.top; 3199 if (span < height) { 3200 bounds.top = bounds.bottom - height; 3201 } 3202 3203 drawable.setBounds(bounds); 3204 drawable.draw(canvas); 3205 3206 canvas.restore(); 3207 } 3208 3209 void drawOverscrollFooter(Canvas canvas, Drawable drawable, Rect bounds) { 3210 final int height = drawable.getMinimumHeight(); 3211 3212 canvas.save(); 3213 canvas.clipRect(bounds); 3214 3215 final int span = bounds.bottom - bounds.top; 3216 if (span < height) { 3217 bounds.bottom = bounds.top + height; 3218 } 3219 3220 drawable.setBounds(bounds); 3221 drawable.draw(canvas); 3222 3223 canvas.restore(); 3224 } 3225 3226 @Override 3227 protected void dispatchDraw(Canvas canvas) { 3228 if (mCachingStarted) { 3229 mCachingActive = true; 3230 } 3231 3232 // Draw the dividers 3233 final int dividerHeight = mDividerHeight; 3234 final Drawable overscrollHeader = mOverScrollHeader; 3235 final Drawable overscrollFooter = mOverScrollFooter; 3236 final boolean drawOverscrollHeader = overscrollHeader != null; 3237 final boolean drawOverscrollFooter = overscrollFooter != null; 3238 final boolean drawDividers = dividerHeight > 0 && mDivider != null; 3239 3240 if (drawDividers || drawOverscrollHeader || drawOverscrollFooter) { 3241 // Only modify the top and bottom in the loop, we set the left and right here 3242 final Rect bounds = mTempRect; 3243 bounds.left = mPaddingLeft; 3244 bounds.right = mRight - mLeft - mPaddingRight; 3245 3246 final int count = getChildCount(); 3247 final int headerCount = mHeaderViewInfos.size(); 3248 final int itemCount = mItemCount; 3249 final int footerLimit = (itemCount - mFooterViewInfos.size()); 3250 final boolean headerDividers = mHeaderDividersEnabled; 3251 final boolean footerDividers = mFooterDividersEnabled; 3252 final int first = mFirstPosition; 3253 final boolean areAllItemsSelectable = mAreAllItemsSelectable; 3254 final ListAdapter adapter = mAdapter; 3255 // If the list is opaque *and* the background is not, we want to 3256 // fill a rect where the dividers would be for non-selectable items 3257 // If the list is opaque and the background is also opaque, we don't 3258 // need to draw anything since the background will do it for us 3259 final boolean fillForMissingDividers = isOpaque() && !super.isOpaque(); 3260 3261 if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) { 3262 mDividerPaint = new Paint(); 3263 mDividerPaint.setColor(getCacheColorHint()); 3264 } 3265 final Paint paint = mDividerPaint; 3266 3267 int effectivePaddingTop = 0; 3268 int effectivePaddingBottom = 0; 3269 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 3270 effectivePaddingTop = mListPadding.top; 3271 effectivePaddingBottom = mListPadding.bottom; 3272 } 3273 3274 final int listBottom = mBottom - mTop - effectivePaddingBottom + mScrollY; 3275 if (!mStackFromBottom) { 3276 int bottom = 0; 3277 3278 // Draw top divider or header for overscroll 3279 final int scrollY = mScrollY; 3280 if (count > 0 && scrollY < 0) { 3281 if (drawOverscrollHeader) { 3282 bounds.bottom = 0; 3283 bounds.top = scrollY; 3284 drawOverscrollHeader(canvas, overscrollHeader, bounds); 3285 } else if (drawDividers) { 3286 bounds.bottom = 0; 3287 bounds.top = -dividerHeight; 3288 drawDivider(canvas, bounds, -1); 3289 } 3290 } 3291 3292 for (int i = 0; i < count; i++) { 3293 final int itemIndex = (first + i); 3294 final boolean isHeader = (itemIndex < headerCount); 3295 final boolean isFooter = (itemIndex >= footerLimit); 3296 if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) { 3297 final View child = getChildAt(i); 3298 bottom = child.getBottom(); 3299 final boolean isLastItem = (i == (count - 1)); 3300 3301 if (drawDividers && (bottom < listBottom) 3302 && !(drawOverscrollFooter && isLastItem)) { 3303 final int nextIndex = (itemIndex + 1); 3304 // Draw dividers between enabled items, headers 3305 // and/or footers when enabled and requested, and 3306 // after the last enabled item. 3307 if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader 3308 && (nextIndex >= headerCount)) && (isLastItem 3309 || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter 3310 && (nextIndex < footerLimit)))) { 3311 bounds.top = bottom; 3312 bounds.bottom = bottom + dividerHeight; 3313 drawDivider(canvas, bounds, i); 3314 } else if (fillForMissingDividers) { 3315 bounds.top = bottom; 3316 bounds.bottom = bottom + dividerHeight; 3317 canvas.drawRect(bounds, paint); 3318 } 3319 } 3320 } 3321 } 3322 3323 final int overFooterBottom = mBottom + mScrollY; 3324 if (drawOverscrollFooter && first + count == itemCount && 3325 overFooterBottom > bottom) { 3326 bounds.top = bottom; 3327 bounds.bottom = overFooterBottom; 3328 drawOverscrollFooter(canvas, overscrollFooter, bounds); 3329 } 3330 } else { 3331 int top; 3332 3333 final int scrollY = mScrollY; 3334 3335 if (count > 0 && drawOverscrollHeader) { 3336 bounds.top = scrollY; 3337 bounds.bottom = getChildAt(0).getTop(); 3338 drawOverscrollHeader(canvas, overscrollHeader, bounds); 3339 } 3340 3341 final int start = drawOverscrollHeader ? 1 : 0; 3342 for (int i = start; i < count; i++) { 3343 final int itemIndex = (first + i); 3344 final boolean isHeader = (itemIndex < headerCount); 3345 final boolean isFooter = (itemIndex >= footerLimit); 3346 if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) { 3347 final View child = getChildAt(i); 3348 top = child.getTop(); 3349 if (drawDividers && (top > effectivePaddingTop)) { 3350 final boolean isFirstItem = (i == start); 3351 final int previousIndex = (itemIndex - 1); 3352 // Draw dividers between enabled items, headers 3353 // and/or footers when enabled and requested, and 3354 // before the first enabled item. 3355 if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader 3356 && (previousIndex >= headerCount)) && (isFirstItem || 3357 adapter.isEnabled(previousIndex) && (footerDividers || !isFooter 3358 && (previousIndex < footerLimit)))) { 3359 bounds.top = top - dividerHeight; 3360 bounds.bottom = top; 3361 // Give the method the child ABOVE the divider, 3362 // so we subtract one from our child position. 3363 // Give -1 when there is no child above the 3364 // divider. 3365 drawDivider(canvas, bounds, i - 1); 3366 } else if (fillForMissingDividers) { 3367 bounds.top = top - dividerHeight; 3368 bounds.bottom = top; 3369 canvas.drawRect(bounds, paint); 3370 } 3371 } 3372 } 3373 } 3374 3375 if (count > 0 && scrollY > 0) { 3376 if (drawOverscrollFooter) { 3377 final int absListBottom = mBottom; 3378 bounds.top = absListBottom; 3379 bounds.bottom = absListBottom + scrollY; 3380 drawOverscrollFooter(canvas, overscrollFooter, bounds); 3381 } else if (drawDividers) { 3382 bounds.top = listBottom; 3383 bounds.bottom = listBottom + dividerHeight; 3384 drawDivider(canvas, bounds, -1); 3385 } 3386 } 3387 } 3388 } 3389 3390 // Draw the indicators (these should be drawn above the dividers) and children 3391 super.dispatchDraw(canvas); 3392 } 3393 3394 @Override 3395 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 3396 boolean more = super.drawChild(canvas, child, drawingTime); 3397 if (mCachingActive && child.mCachingFailed) { 3398 mCachingActive = false; 3399 } 3400 return more; 3401 } 3402 3403 /** 3404 * Draws a divider for the given child in the given bounds. 3405 * 3406 * @param canvas The canvas to draw to. 3407 * @param bounds The bounds of the divider. 3408 * @param childIndex The index of child (of the View) above the divider. 3409 * This will be -1 if there is no child above the divider to be 3410 * drawn. 3411 */ 3412 void drawDivider(Canvas canvas, Rect bounds, int childIndex) { 3413 // This widget draws the same divider for all children 3414 final Drawable divider = mDivider; 3415 3416 divider.setBounds(bounds); 3417 divider.draw(canvas); 3418 } 3419 3420 /** 3421 * Returns the drawable that will be drawn between each item in the list. 3422 * 3423 * @return the current drawable drawn between list elements 3424 */ 3425 public Drawable getDivider() { 3426 return mDivider; 3427 } 3428 3429 /** 3430 * Sets the drawable that will be drawn between each item in the list. If the drawable does 3431 * not have an intrinsic height, you should also call {@link #setDividerHeight(int)} 3432 * 3433 * @param divider The drawable to use. 3434 */ 3435 public void setDivider(Drawable divider) { 3436 if (divider != null) { 3437 mDividerHeight = divider.getIntrinsicHeight(); 3438 } else { 3439 mDividerHeight = 0; 3440 } 3441 mDivider = divider; 3442 mDividerIsOpaque = divider == null || divider.getOpacity() == PixelFormat.OPAQUE; 3443 requestLayout(); 3444 invalidate(); 3445 } 3446 3447 /** 3448 * @return Returns the height of the divider that will be drawn between each item in the list. 3449 */ 3450 public int getDividerHeight() { 3451 return mDividerHeight; 3452 } 3453 3454 /** 3455 * Sets the height of the divider that will be drawn between each item in the list. Calling 3456 * this will override the intrinsic height as set by {@link #setDivider(Drawable)} 3457 * 3458 * @param height The new height of the divider in pixels. 3459 */ 3460 public void setDividerHeight(int height) { 3461 mDividerHeight = height; 3462 requestLayout(); 3463 invalidate(); 3464 } 3465 3466 /** 3467 * Enables or disables the drawing of the divider for header views. 3468 * 3469 * @param headerDividersEnabled True to draw the headers, false otherwise. 3470 * 3471 * @see #setFooterDividersEnabled(boolean) 3472 * @see #areHeaderDividersEnabled() 3473 * @see #addHeaderView(android.view.View) 3474 */ 3475 public void setHeaderDividersEnabled(boolean headerDividersEnabled) { 3476 mHeaderDividersEnabled = headerDividersEnabled; 3477 invalidate(); 3478 } 3479 3480 /** 3481 * @return Whether the drawing of the divider for header views is enabled 3482 * 3483 * @see #setHeaderDividersEnabled(boolean) 3484 */ 3485 public boolean areHeaderDividersEnabled() { 3486 return mHeaderDividersEnabled; 3487 } 3488 3489 /** 3490 * Enables or disables the drawing of the divider for footer views. 3491 * 3492 * @param footerDividersEnabled True to draw the footers, false otherwise. 3493 * 3494 * @see #setHeaderDividersEnabled(boolean) 3495 * @see #areFooterDividersEnabled() 3496 * @see #addFooterView(android.view.View) 3497 */ 3498 public void setFooterDividersEnabled(boolean footerDividersEnabled) { 3499 mFooterDividersEnabled = footerDividersEnabled; 3500 invalidate(); 3501 } 3502 3503 /** 3504 * @return Whether the drawing of the divider for footer views is enabled 3505 * 3506 * @see #setFooterDividersEnabled(boolean) 3507 */ 3508 public boolean areFooterDividersEnabled() { 3509 return mFooterDividersEnabled; 3510 } 3511 3512 /** 3513 * Sets the drawable that will be drawn above all other list content. 3514 * This area can become visible when the user overscrolls the list. 3515 * 3516 * @param header The drawable to use 3517 */ 3518 public void setOverscrollHeader(Drawable header) { 3519 mOverScrollHeader = header; 3520 if (mScrollY < 0) { 3521 invalidate(); 3522 } 3523 } 3524 3525 /** 3526 * @return The drawable that will be drawn above all other list content 3527 */ 3528 public Drawable getOverscrollHeader() { 3529 return mOverScrollHeader; 3530 } 3531 3532 /** 3533 * Sets the drawable that will be drawn below all other list content. 3534 * This area can become visible when the user overscrolls the list, 3535 * or when the list's content does not fully fill the container area. 3536 * 3537 * @param footer The drawable to use 3538 */ 3539 public void setOverscrollFooter(Drawable footer) { 3540 mOverScrollFooter = footer; 3541 invalidate(); 3542 } 3543 3544 /** 3545 * @return The drawable that will be drawn below all other list content 3546 */ 3547 public Drawable getOverscrollFooter() { 3548 return mOverScrollFooter; 3549 } 3550 3551 @Override 3552 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 3553 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 3554 3555 final ListAdapter adapter = mAdapter; 3556 int closetChildIndex = -1; 3557 int closestChildTop = 0; 3558 if (adapter != null && gainFocus && previouslyFocusedRect != null) { 3559 previouslyFocusedRect.offset(mScrollX, mScrollY); 3560 3561 // Don't cache the result of getChildCount or mFirstPosition here, 3562 // it could change in layoutChildren. 3563 if (adapter.getCount() < getChildCount() + mFirstPosition) { 3564 mLayoutMode = LAYOUT_NORMAL; 3565 layoutChildren(); 3566 } 3567 3568 // figure out which item should be selected based on previously 3569 // focused rect 3570 Rect otherRect = mTempRect; 3571 int minDistance = Integer.MAX_VALUE; 3572 final int childCount = getChildCount(); 3573 final int firstPosition = mFirstPosition; 3574 3575 for (int i = 0; i < childCount; i++) { 3576 // only consider selectable views 3577 if (!adapter.isEnabled(firstPosition + i)) { 3578 continue; 3579 } 3580 3581 View other = getChildAt(i); 3582 other.getDrawingRect(otherRect); 3583 offsetDescendantRectToMyCoords(other, otherRect); 3584 int distance = getDistance(previouslyFocusedRect, otherRect, direction); 3585 3586 if (distance < minDistance) { 3587 minDistance = distance; 3588 closetChildIndex = i; 3589 closestChildTop = other.getTop(); 3590 } 3591 } 3592 } 3593 3594 if (closetChildIndex >= 0) { 3595 setSelectionFromTop(closetChildIndex + mFirstPosition, closestChildTop); 3596 } else { 3597 requestLayout(); 3598 } 3599 } 3600 3601 3602 /* 3603 * (non-Javadoc) 3604 * 3605 * Children specified in XML are assumed to be header views. After we have 3606 * parsed them move them out of the children list and into mHeaderViews. 3607 */ 3608 @Override 3609 protected void onFinishInflate() { 3610 super.onFinishInflate(); 3611 3612 int count = getChildCount(); 3613 if (count > 0) { 3614 for (int i = 0; i < count; ++i) { 3615 addHeaderView(getChildAt(i)); 3616 } 3617 removeAllViews(); 3618 } 3619 } 3620 3621 /* (non-Javadoc) 3622 * @see android.view.View#findViewById(int) 3623 * First look in our children, then in any header and footer views that may be scrolled off. 3624 */ 3625 @Override 3626 protected View findViewTraversal(int id) { 3627 View v; 3628 v = super.findViewTraversal(id); 3629 if (v == null) { 3630 v = findViewInHeadersOrFooters(mHeaderViewInfos, id); 3631 if (v != null) { 3632 return v; 3633 } 3634 v = findViewInHeadersOrFooters(mFooterViewInfos, id); 3635 if (v != null) { 3636 return v; 3637 } 3638 } 3639 return v; 3640 } 3641 3642 /* (non-Javadoc) 3643 * 3644 * Look in the passed in list of headers or footers for the view. 3645 */ 3646 View findViewInHeadersOrFooters(ArrayList<FixedViewInfo> where, int id) { 3647 if (where != null) { 3648 int len = where.size(); 3649 View v; 3650 3651 for (int i = 0; i < len; i++) { 3652 v = where.get(i).view; 3653 3654 if (!v.isRootNamespace()) { 3655 v = v.findViewById(id); 3656 3657 if (v != null) { 3658 return v; 3659 } 3660 } 3661 } 3662 } 3663 return null; 3664 } 3665 3666 /* (non-Javadoc) 3667 * @see android.view.View#findViewWithTag(Object) 3668 * First look in our children, then in any header and footer views that may be scrolled off. 3669 */ 3670 @Override 3671 protected View findViewWithTagTraversal(Object tag) { 3672 View v; 3673 v = super.findViewWithTagTraversal(tag); 3674 if (v == null) { 3675 v = findViewWithTagInHeadersOrFooters(mHeaderViewInfos, tag); 3676 if (v != null) { 3677 return v; 3678 } 3679 3680 v = findViewWithTagInHeadersOrFooters(mFooterViewInfos, tag); 3681 if (v != null) { 3682 return v; 3683 } 3684 } 3685 return v; 3686 } 3687 3688 /* (non-Javadoc) 3689 * 3690 * Look in the passed in list of headers or footers for the view with the tag. 3691 */ 3692 View findViewWithTagInHeadersOrFooters(ArrayList<FixedViewInfo> where, Object tag) { 3693 if (where != null) { 3694 int len = where.size(); 3695 View v; 3696 3697 for (int i = 0; i < len; i++) { 3698 v = where.get(i).view; 3699 3700 if (!v.isRootNamespace()) { 3701 v = v.findViewWithTag(tag); 3702 3703 if (v != null) { 3704 return v; 3705 } 3706 } 3707 } 3708 } 3709 return null; 3710 } 3711 3712 /** 3713 * @hide 3714 * @see android.view.View#findViewByPredicate(Predicate) 3715 * First look in our children, then in any header and footer views that may be scrolled off. 3716 */ 3717 @Override 3718 protected View findViewByPredicateTraversal(Predicate<View> predicate, View childToSkip) { 3719 View v; 3720 v = super.findViewByPredicateTraversal(predicate, childToSkip); 3721 if (v == null) { 3722 v = findViewByPredicateInHeadersOrFooters(mHeaderViewInfos, predicate, childToSkip); 3723 if (v != null) { 3724 return v; 3725 } 3726 3727 v = findViewByPredicateInHeadersOrFooters(mFooterViewInfos, predicate, childToSkip); 3728 if (v != null) { 3729 return v; 3730 } 3731 } 3732 return v; 3733 } 3734 3735 /* (non-Javadoc) 3736 * 3737 * Look in the passed in list of headers or footers for the first view that matches 3738 * the predicate. 3739 */ 3740 View findViewByPredicateInHeadersOrFooters(ArrayList<FixedViewInfo> where, 3741 Predicate<View> predicate, View childToSkip) { 3742 if (where != null) { 3743 int len = where.size(); 3744 View v; 3745 3746 for (int i = 0; i < len; i++) { 3747 v = where.get(i).view; 3748 3749 if (v != childToSkip && !v.isRootNamespace()) { 3750 v = v.findViewByPredicate(predicate); 3751 3752 if (v != null) { 3753 return v; 3754 } 3755 } 3756 } 3757 } 3758 return null; 3759 } 3760 3761 /** 3762 * Returns the set of checked items ids. The result is only valid if the 3763 * choice mode has not been set to {@link #CHOICE_MODE_NONE}. 3764 * 3765 * @return A new array which contains the id of each checked item in the 3766 * list. 3767 * 3768 * @deprecated Use {@link #getCheckedItemIds()} instead. 3769 */ 3770 @Deprecated 3771 public long[] getCheckItemIds() { 3772 // Use new behavior that correctly handles stable ID mapping. 3773 if (mAdapter != null && mAdapter.hasStableIds()) { 3774 return getCheckedItemIds(); 3775 } 3776 3777 // Old behavior was buggy, but would sort of work for adapters without stable IDs. 3778 // Fall back to it to support legacy apps. 3779 if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null && mAdapter != null) { 3780 final SparseBooleanArray states = mCheckStates; 3781 final int count = states.size(); 3782 final long[] ids = new long[count]; 3783 final ListAdapter adapter = mAdapter; 3784 3785 int checkedCount = 0; 3786 for (int i = 0; i < count; i++) { 3787 if (states.valueAt(i)) { 3788 ids[checkedCount++] = adapter.getItemId(states.keyAt(i)); 3789 } 3790 } 3791 3792 // Trim array if needed. mCheckStates may contain false values 3793 // resulting in checkedCount being smaller than count. 3794 if (checkedCount == count) { 3795 return ids; 3796 } else { 3797 final long[] result = new long[checkedCount]; 3798 System.arraycopy(ids, 0, result, 0, checkedCount); 3799 3800 return result; 3801 } 3802 } 3803 return new long[0]; 3804 } 3805 3806 @Override 3807 int getHeightForPosition(int position) { 3808 final int height = super.getHeightForPosition(position); 3809 if (shouldAdjustHeightForDivider(position)) { 3810 return height + mDividerHeight; 3811 } 3812 return height; 3813 } 3814 3815 private boolean shouldAdjustHeightForDivider(int itemIndex) { 3816 final int dividerHeight = mDividerHeight; 3817 final Drawable overscrollHeader = mOverScrollHeader; 3818 final Drawable overscrollFooter = mOverScrollFooter; 3819 final boolean drawOverscrollHeader = overscrollHeader != null; 3820 final boolean drawOverscrollFooter = overscrollFooter != null; 3821 final boolean drawDividers = dividerHeight > 0 && mDivider != null; 3822 3823 if (drawDividers) { 3824 final boolean fillForMissingDividers = isOpaque() && !super.isOpaque(); 3825 final int itemCount = mItemCount; 3826 final int headerCount = mHeaderViewInfos.size(); 3827 final int footerLimit = (itemCount - mFooterViewInfos.size()); 3828 final boolean isHeader = (itemIndex < headerCount); 3829 final boolean isFooter = (itemIndex >= footerLimit); 3830 final boolean headerDividers = mHeaderDividersEnabled; 3831 final boolean footerDividers = mFooterDividersEnabled; 3832 if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) { 3833 final ListAdapter adapter = mAdapter; 3834 if (!mStackFromBottom) { 3835 final boolean isLastItem = (itemIndex == (itemCount - 1)); 3836 if (!drawOverscrollFooter || !isLastItem) { 3837 final int nextIndex = itemIndex + 1; 3838 // Draw dividers between enabled items, headers 3839 // and/or footers when enabled and requested, and 3840 // after the last enabled item. 3841 if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader 3842 && (nextIndex >= headerCount)) && (isLastItem 3843 || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter 3844 && (nextIndex < footerLimit)))) { 3845 return true; 3846 } else if (fillForMissingDividers) { 3847 return true; 3848 } 3849 } 3850 } else { 3851 final int start = drawOverscrollHeader ? 1 : 0; 3852 final boolean isFirstItem = (itemIndex == start); 3853 if (!isFirstItem) { 3854 final int previousIndex = (itemIndex - 1); 3855 // Draw dividers between enabled items, headers 3856 // and/or footers when enabled and requested, and 3857 // before the first enabled item. 3858 if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader 3859 && (previousIndex >= headerCount)) && (isFirstItem || 3860 adapter.isEnabled(previousIndex) && (footerDividers || !isFooter 3861 && (previousIndex < footerLimit)))) { 3862 return true; 3863 } else if (fillForMissingDividers) { 3864 return true; 3865 } 3866 } 3867 } 3868 } 3869 } 3870 3871 return false; 3872 } 3873 3874 @Override 3875 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 3876 super.onInitializeAccessibilityEvent(event); 3877 event.setClassName(ListView.class.getName()); 3878 } 3879 3880 @Override 3881 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 3882 super.onInitializeAccessibilityNodeInfo(info); 3883 info.setClassName(ListView.class.getName()); 3884 3885 final int rowsCount = getCount(); 3886 final int selectionMode = getSelectionModeForAccessibility(); 3887 final CollectionInfo collectionInfo = CollectionInfo.obtain( 3888 rowsCount, 1, false, selectionMode); 3889 info.setCollectionInfo(collectionInfo); 3890 } 3891 3892 @Override 3893 public void onInitializeAccessibilityNodeInfoForItem( 3894 View view, int position, AccessibilityNodeInfo info) { 3895 super.onInitializeAccessibilityNodeInfoForItem(view, position, info); 3896 3897 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 3898 final boolean isHeading = lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER; 3899 final boolean isSelected = isItemChecked(position); 3900 final CollectionItemInfo itemInfo = CollectionItemInfo.obtain( 3901 position, 1, 0, 1, isHeading, isSelected); 3902 info.setCollectionItemInfo(itemInfo); 3903 } 3904 } 3905