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