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