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 private ArrayList<FixedViewInfo> mHeaderViewInfos = Lists.newArrayList(); 116 private 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 mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter); 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 mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter); 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 = new HeaderViewListAdapter(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 * Obtain the view and add it to our list of children. The view can be made 1943 * fresh, converted from an unused view, or used as is if it was in the 1944 * 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 If flow is true, align top edge to y. If false, align bottom 1949 * edge to y. 1950 * @param childrenLeft Left edge where children should be positioned 1951 * @param selected Is this position selected? 1952 * @return View that was added 1953 */ makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected)1954 private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, 1955 boolean selected) { 1956 View child; 1957 1958 1959 if (!mDataChanged) { 1960 // Try to use an existing view for this position 1961 child = mRecycler.getActiveView(position); 1962 if (child != null) { 1963 // Found it -- we're using an existing child 1964 // This just needs to be positioned 1965 setupChild(child, position, y, flow, childrenLeft, selected, true); 1966 1967 return child; 1968 } 1969 } 1970 1971 // Make a new view for this position, or convert an unused view if possible 1972 child = obtainView(position, mIsScrap); 1973 1974 // This needs to be positioned and measured 1975 setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); 1976 1977 return child; 1978 } 1979 1980 /** 1981 * Add a view as a child and make sure it is measured (if necessary) and 1982 * positioned properly. 1983 * 1984 * @param child The view to add 1985 * @param position The position of this child 1986 * @param y The y position relative to which this view will be positioned 1987 * @param flowDown If true, align top edge to y. If false, align bottom 1988 * edge to y. 1989 * @param childrenLeft Left edge where children should be positioned 1990 * @param selected Is this position selected? 1991 * @param recycled Has this view been pulled from the recycle bin? If so it 1992 * does not need to be remeasured. 1993 */ setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean recycled)1994 private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, 1995 boolean selected, boolean recycled) { 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 = !recycled || updateChildSelected || child.isLayoutRequested(); 2005 2006 // Respect layout params that are already in the view. Otherwise make some up... 2007 // noinspection unchecked 2008 AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); 2009 if (p == null) { 2010 p = (AbsListView.LayoutParams) generateDefaultLayoutParams(); 2011 } 2012 p.viewType = mAdapter.getItemViewType(position); 2013 p.isEnabled = mAdapter.isEnabled(position); 2014 2015 if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter 2016 && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { 2017 attachViewToParent(child, flowDown ? -1 : 0, p); 2018 } else { 2019 p.forceAdd = false; 2020 if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { 2021 p.recycledHeaderFooter = true; 2022 } 2023 addViewInLayout(child, flowDown ? -1 : 0, p, true); 2024 } 2025 2026 if (updateChildSelected) { 2027 child.setSelected(isSelected); 2028 } 2029 2030 if (updateChildPressed) { 2031 child.setPressed(isPressed); 2032 } 2033 2034 if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { 2035 if (child instanceof Checkable) { 2036 ((Checkable) child).setChecked(mCheckStates.get(position)); 2037 } else if (getContext().getApplicationInfo().targetSdkVersion 2038 >= android.os.Build.VERSION_CODES.HONEYCOMB) { 2039 child.setActivated(mCheckStates.get(position)); 2040 } 2041 } 2042 2043 if (needToMeasure) { 2044 final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, 2045 mListPadding.left + mListPadding.right, p.width); 2046 final int lpHeight = p.height; 2047 final int childHeightSpec; 2048 if (lpHeight > 0) { 2049 childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); 2050 } else { 2051 childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(), 2052 MeasureSpec.UNSPECIFIED); 2053 } 2054 child.measure(childWidthSpec, childHeightSpec); 2055 } else { 2056 cleanupLayoutState(child); 2057 } 2058 2059 final int w = child.getMeasuredWidth(); 2060 final int h = child.getMeasuredHeight(); 2061 final int childTop = flowDown ? y : y - h; 2062 2063 if (needToMeasure) { 2064 final int childRight = childrenLeft + w; 2065 final int childBottom = childTop + h; 2066 child.layout(childrenLeft, childTop, childRight, childBottom); 2067 } else { 2068 child.offsetLeftAndRight(childrenLeft - child.getLeft()); 2069 child.offsetTopAndBottom(childTop - child.getTop()); 2070 } 2071 2072 if (mCachingStarted && !child.isDrawingCacheEnabled()) { 2073 child.setDrawingCacheEnabled(true); 2074 } 2075 2076 if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition) 2077 != position) { 2078 child.jumpDrawablesToCurrentState(); 2079 } 2080 2081 Trace.traceEnd(Trace.TRACE_TAG_VIEW); 2082 } 2083 2084 @Override canAnimate()2085 protected boolean canAnimate() { 2086 return super.canAnimate() && mItemCount > 0; 2087 } 2088 2089 /** 2090 * Sets the currently selected item. If in touch mode, the item will not be selected 2091 * but it will still be positioned appropriately. If the specified selection position 2092 * is less than 0, then the item at position 0 will be selected. 2093 * 2094 * @param position Index (starting at 0) of the data item to be selected. 2095 */ 2096 @Override setSelection(int position)2097 public void setSelection(int position) { 2098 setSelectionFromTop(position, 0); 2099 } 2100 2101 /** 2102 * Makes the item at the supplied position selected. 2103 * 2104 * @param position the position of the item to select 2105 */ 2106 @Override setSelectionInt(int position)2107 void setSelectionInt(int position) { 2108 setNextSelectedPositionInt(position); 2109 boolean awakeScrollbars = false; 2110 2111 final int selectedPosition = mSelectedPosition; 2112 2113 if (selectedPosition >= 0) { 2114 if (position == selectedPosition - 1) { 2115 awakeScrollbars = true; 2116 } else if (position == selectedPosition + 1) { 2117 awakeScrollbars = true; 2118 } 2119 } 2120 2121 if (mPositionScroller != null) { 2122 mPositionScroller.stop(); 2123 } 2124 2125 layoutChildren(); 2126 2127 if (awakeScrollbars) { 2128 awakenScrollBars(); 2129 } 2130 } 2131 2132 /** 2133 * Find a position that can be selected (i.e., is not a separator). 2134 * 2135 * @param position The starting position to look at. 2136 * @param lookDown Whether to look down for other positions. 2137 * @return The next selectable position starting at position and then searching either up or 2138 * down. Returns {@link #INVALID_POSITION} if nothing can be found. 2139 */ 2140 @Override lookForSelectablePosition(int position, boolean lookDown)2141 int lookForSelectablePosition(int position, boolean lookDown) { 2142 final ListAdapter adapter = mAdapter; 2143 if (adapter == null || isInTouchMode()) { 2144 return INVALID_POSITION; 2145 } 2146 2147 final int count = adapter.getCount(); 2148 if (!mAreAllItemsSelectable) { 2149 if (lookDown) { 2150 position = Math.max(0, position); 2151 while (position < count && !adapter.isEnabled(position)) { 2152 position++; 2153 } 2154 } else { 2155 position = Math.min(position, count - 1); 2156 while (position >= 0 && !adapter.isEnabled(position)) { 2157 position--; 2158 } 2159 } 2160 } 2161 2162 if (position < 0 || position >= count) { 2163 return INVALID_POSITION; 2164 } 2165 2166 return position; 2167 } 2168 2169 /** 2170 * Find a position that can be selected (i.e., is not a separator). If there 2171 * are no selectable positions in the specified direction from the starting 2172 * position, searches in the opposite direction from the starting position 2173 * to the current position. 2174 * 2175 * @param current the current position 2176 * @param position the starting position 2177 * @param lookDown whether to look down for other positions 2178 * @return the next selectable position, or {@link #INVALID_POSITION} if 2179 * nothing can be found 2180 */ lookForSelectablePositionAfter(int current, int position, boolean lookDown)2181 int lookForSelectablePositionAfter(int current, int position, boolean lookDown) { 2182 final ListAdapter adapter = mAdapter; 2183 if (adapter == null || isInTouchMode()) { 2184 return INVALID_POSITION; 2185 } 2186 2187 // First check after the starting position in the specified direction. 2188 final int after = lookForSelectablePosition(position, lookDown); 2189 if (after != INVALID_POSITION) { 2190 return after; 2191 } 2192 2193 // Then check between the starting position and the current position. 2194 final int count = adapter.getCount(); 2195 current = MathUtils.constrain(current, -1, count - 1); 2196 if (lookDown) { 2197 position = Math.min(position - 1, count - 1); 2198 while ((position > current) && !adapter.isEnabled(position)) { 2199 position--; 2200 } 2201 if (position <= current) { 2202 return INVALID_POSITION; 2203 } 2204 } else { 2205 position = Math.max(0, position + 1); 2206 while ((position < current) && !adapter.isEnabled(position)) { 2207 position++; 2208 } 2209 if (position >= current) { 2210 return INVALID_POSITION; 2211 } 2212 } 2213 2214 return position; 2215 } 2216 2217 /** 2218 * setSelectionAfterHeaderView set the selection to be the first list item 2219 * after the header views. 2220 */ setSelectionAfterHeaderView()2221 public void setSelectionAfterHeaderView() { 2222 final int count = mHeaderViewInfos.size(); 2223 if (count > 0) { 2224 mNextSelectedPosition = 0; 2225 return; 2226 } 2227 2228 if (mAdapter != null) { 2229 setSelection(count); 2230 } else { 2231 mNextSelectedPosition = count; 2232 mLayoutMode = LAYOUT_SET_SELECTION; 2233 } 2234 2235 } 2236 2237 @Override dispatchKeyEvent(KeyEvent event)2238 public boolean dispatchKeyEvent(KeyEvent event) { 2239 // Dispatch in the normal way 2240 boolean handled = super.dispatchKeyEvent(event); 2241 if (!handled) { 2242 // If we didn't handle it... 2243 View focused = getFocusedChild(); 2244 if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) { 2245 // ... and our focused child didn't handle it 2246 // ... give it to ourselves so we can scroll if necessary 2247 handled = onKeyDown(event.getKeyCode(), event); 2248 } 2249 } 2250 return handled; 2251 } 2252 2253 @Override onKeyDown(int keyCode, KeyEvent event)2254 public boolean onKeyDown(int keyCode, KeyEvent event) { 2255 return commonKey(keyCode, 1, event); 2256 } 2257 2258 @Override onKeyMultiple(int keyCode, int repeatCount, KeyEvent event)2259 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { 2260 return commonKey(keyCode, repeatCount, event); 2261 } 2262 2263 @Override onKeyUp(int keyCode, KeyEvent event)2264 public boolean onKeyUp(int keyCode, KeyEvent event) { 2265 return commonKey(keyCode, 1, event); 2266 } 2267 commonKey(int keyCode, int count, KeyEvent event)2268 private boolean commonKey(int keyCode, int count, KeyEvent event) { 2269 if (mAdapter == null || !isAttachedToWindow()) { 2270 return false; 2271 } 2272 2273 if (mDataChanged) { 2274 layoutChildren(); 2275 } 2276 2277 boolean handled = false; 2278 int action = event.getAction(); 2279 if (KeyEvent.isConfirmKey(keyCode) 2280 && event.hasNoModifiers() && action != KeyEvent.ACTION_UP) { 2281 handled = resurrectSelectionIfNeeded(); 2282 if (!handled && event.getRepeatCount() == 0 && getChildCount() > 0) { 2283 keyPressed(); 2284 handled = true; 2285 } 2286 } 2287 2288 2289 if (!handled && action != KeyEvent.ACTION_UP) { 2290 switch (keyCode) { 2291 case KeyEvent.KEYCODE_DPAD_UP: 2292 if (event.hasNoModifiers()) { 2293 handled = resurrectSelectionIfNeeded(); 2294 if (!handled) { 2295 while (count-- > 0) { 2296 if (arrowScroll(FOCUS_UP)) { 2297 handled = true; 2298 } else { 2299 break; 2300 } 2301 } 2302 } 2303 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 2304 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 2305 } 2306 break; 2307 2308 case KeyEvent.KEYCODE_DPAD_DOWN: 2309 if (event.hasNoModifiers()) { 2310 handled = resurrectSelectionIfNeeded(); 2311 if (!handled) { 2312 while (count-- > 0) { 2313 if (arrowScroll(FOCUS_DOWN)) { 2314 handled = true; 2315 } else { 2316 break; 2317 } 2318 } 2319 } 2320 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 2321 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 2322 } 2323 break; 2324 2325 case KeyEvent.KEYCODE_DPAD_LEFT: 2326 if (event.hasNoModifiers()) { 2327 handled = handleHorizontalFocusWithinListItem(View.FOCUS_LEFT); 2328 } 2329 break; 2330 2331 case KeyEvent.KEYCODE_DPAD_RIGHT: 2332 if (event.hasNoModifiers()) { 2333 handled = handleHorizontalFocusWithinListItem(View.FOCUS_RIGHT); 2334 } 2335 break; 2336 2337 case KeyEvent.KEYCODE_PAGE_UP: 2338 if (event.hasNoModifiers()) { 2339 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP); 2340 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 2341 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 2342 } 2343 break; 2344 2345 case KeyEvent.KEYCODE_PAGE_DOWN: 2346 if (event.hasNoModifiers()) { 2347 handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN); 2348 } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) { 2349 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 2350 } 2351 break; 2352 2353 case KeyEvent.KEYCODE_MOVE_HOME: 2354 if (event.hasNoModifiers()) { 2355 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP); 2356 } 2357 break; 2358 2359 case KeyEvent.KEYCODE_MOVE_END: 2360 if (event.hasNoModifiers()) { 2361 handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN); 2362 } 2363 break; 2364 2365 case KeyEvent.KEYCODE_TAB: 2366 // This creates an asymmetry in TAB navigation order. At some 2367 // point in the future we may decide that it's preferable to 2368 // force the list selection to the top or bottom when receiving 2369 // TAB focus from another widget, but for now this is adequate. 2370 if (event.hasNoModifiers()) { 2371 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_DOWN); 2372 } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { 2373 handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_UP); 2374 } 2375 break; 2376 } 2377 } 2378 2379 if (handled) { 2380 return true; 2381 } 2382 2383 if (sendToTextFilter(keyCode, count, event)) { 2384 return true; 2385 } 2386 2387 switch (action) { 2388 case KeyEvent.ACTION_DOWN: 2389 return super.onKeyDown(keyCode, event); 2390 2391 case KeyEvent.ACTION_UP: 2392 return super.onKeyUp(keyCode, event); 2393 2394 case KeyEvent.ACTION_MULTIPLE: 2395 return super.onKeyMultiple(keyCode, count, event); 2396 2397 default: // shouldn't happen 2398 return false; 2399 } 2400 } 2401 2402 /** 2403 * Scrolls up or down by the number of items currently present on screen. 2404 * 2405 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 2406 * @return whether selection was moved 2407 */ pageScroll(int direction)2408 boolean pageScroll(int direction) { 2409 final int nextPage; 2410 final boolean down; 2411 2412 if (direction == FOCUS_UP) { 2413 nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1); 2414 down = false; 2415 } else if (direction == FOCUS_DOWN) { 2416 nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1); 2417 down = true; 2418 } else { 2419 return false; 2420 } 2421 2422 if (nextPage >= 0) { 2423 final int position = lookForSelectablePositionAfter(mSelectedPosition, nextPage, down); 2424 if (position >= 0) { 2425 mLayoutMode = LAYOUT_SPECIFIC; 2426 mSpecificTop = mPaddingTop + getVerticalFadingEdgeLength(); 2427 2428 if (down && (position > (mItemCount - getChildCount()))) { 2429 mLayoutMode = LAYOUT_FORCE_BOTTOM; 2430 } 2431 2432 if (!down && (position < getChildCount())) { 2433 mLayoutMode = LAYOUT_FORCE_TOP; 2434 } 2435 2436 setSelectionInt(position); 2437 invokeOnItemScrollListener(); 2438 if (!awakenScrollBars()) { 2439 invalidate(); 2440 } 2441 2442 return true; 2443 } 2444 } 2445 2446 return false; 2447 } 2448 2449 /** 2450 * Go to the last or first item if possible (not worrying about panning 2451 * across or navigating within the internal focus of the currently selected 2452 * item.) 2453 * 2454 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 2455 * @return whether selection was moved 2456 */ fullScroll(int direction)2457 boolean fullScroll(int direction) { 2458 boolean moved = false; 2459 if (direction == FOCUS_UP) { 2460 if (mSelectedPosition != 0) { 2461 final int position = lookForSelectablePositionAfter(mSelectedPosition, 0, true); 2462 if (position >= 0) { 2463 mLayoutMode = LAYOUT_FORCE_TOP; 2464 setSelectionInt(position); 2465 invokeOnItemScrollListener(); 2466 } 2467 moved = true; 2468 } 2469 } else if (direction == FOCUS_DOWN) { 2470 final int lastItem = (mItemCount - 1); 2471 if (mSelectedPosition < lastItem) { 2472 final int position = lookForSelectablePositionAfter( 2473 mSelectedPosition, lastItem, false); 2474 if (position >= 0) { 2475 mLayoutMode = LAYOUT_FORCE_BOTTOM; 2476 setSelectionInt(position); 2477 invokeOnItemScrollListener(); 2478 } 2479 moved = true; 2480 } 2481 } 2482 2483 if (moved && !awakenScrollBars()) { 2484 awakenScrollBars(); 2485 invalidate(); 2486 } 2487 2488 return moved; 2489 } 2490 2491 /** 2492 * To avoid horizontal focus searches changing the selected item, we 2493 * manually focus search within the selected item (as applicable), and 2494 * prevent focus from jumping to something within another item. 2495 * @param direction one of {View.FOCUS_LEFT, View.FOCUS_RIGHT} 2496 * @return Whether this consumes the key event. 2497 */ handleHorizontalFocusWithinListItem(int direction)2498 private boolean handleHorizontalFocusWithinListItem(int direction) { 2499 if (direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) { 2500 throw new IllegalArgumentException("direction must be one of" 2501 + " {View.FOCUS_LEFT, View.FOCUS_RIGHT}"); 2502 } 2503 2504 final int numChildren = getChildCount(); 2505 if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) { 2506 final View selectedView = getSelectedView(); 2507 if (selectedView != null && selectedView.hasFocus() && 2508 selectedView instanceof ViewGroup) { 2509 2510 final View currentFocus = selectedView.findFocus(); 2511 final View nextFocus = FocusFinder.getInstance().findNextFocus( 2512 (ViewGroup) selectedView, currentFocus, direction); 2513 if (nextFocus != null) { 2514 // do the math to get interesting rect in next focus' coordinates 2515 Rect focusedRect = mTempRect; 2516 if (currentFocus != null) { 2517 currentFocus.getFocusedRect(focusedRect); 2518 offsetDescendantRectToMyCoords(currentFocus, focusedRect); 2519 offsetRectIntoDescendantCoords(nextFocus, focusedRect); 2520 } else { 2521 focusedRect = null; 2522 } 2523 if (nextFocus.requestFocus(direction, focusedRect)) { 2524 return true; 2525 } 2526 } 2527 // we are blocking the key from being handled (by returning true) 2528 // if the global result is going to be some other view within this 2529 // list. this is to acheive the overall goal of having 2530 // horizontal d-pad navigation remain in the current item. 2531 final View globalNextFocus = FocusFinder.getInstance().findNextFocus( 2532 (ViewGroup) getRootView(), currentFocus, direction); 2533 if (globalNextFocus != null) { 2534 return isViewAncestorOf(globalNextFocus, this); 2535 } 2536 } 2537 } 2538 return false; 2539 } 2540 2541 /** 2542 * Scrolls to the next or previous item if possible. 2543 * 2544 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 2545 * 2546 * @return whether selection was moved 2547 */ arrowScroll(int direction)2548 boolean arrowScroll(int direction) { 2549 try { 2550 mInLayout = true; 2551 final boolean handled = arrowScrollImpl(direction); 2552 if (handled) { 2553 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 2554 } 2555 return handled; 2556 } finally { 2557 mInLayout = false; 2558 } 2559 } 2560 2561 /** 2562 * Used by {@link #arrowScrollImpl(int)} to help determine the next selected position 2563 * to move to. This return a position in the direction given if the selected item 2564 * is fully visible. 2565 * 2566 * @param selectedView Current selected view to move from 2567 * @param selectedPos Current selected position to move from 2568 * @param direction Direction to move in 2569 * @return Desired selected position after moving in the given direction 2570 */ nextSelectedPositionForDirection( View selectedView, int selectedPos, int direction)2571 private final int nextSelectedPositionForDirection( 2572 View selectedView, int selectedPos, int direction) { 2573 int nextSelected; 2574 2575 if (direction == View.FOCUS_DOWN) { 2576 final int listBottom = getHeight() - mListPadding.bottom; 2577 if (selectedView != null && selectedView.getBottom() <= listBottom) { 2578 nextSelected = selectedPos != INVALID_POSITION && selectedPos >= mFirstPosition ? 2579 selectedPos + 1 : 2580 mFirstPosition; 2581 } else { 2582 return INVALID_POSITION; 2583 } 2584 } else { 2585 final int listTop = mListPadding.top; 2586 if (selectedView != null && selectedView.getTop() >= listTop) { 2587 final int lastPos = mFirstPosition + getChildCount() - 1; 2588 nextSelected = selectedPos != INVALID_POSITION && selectedPos <= lastPos ? 2589 selectedPos - 1 : 2590 lastPos; 2591 } else { 2592 return INVALID_POSITION; 2593 } 2594 } 2595 2596 if (nextSelected < 0 || nextSelected >= mAdapter.getCount()) { 2597 return INVALID_POSITION; 2598 } 2599 return lookForSelectablePosition(nextSelected, direction == View.FOCUS_DOWN); 2600 } 2601 2602 /** 2603 * Handle an arrow scroll going up or down. Take into account whether items are selectable, 2604 * whether there are focusable items etc. 2605 * 2606 * @param direction Either {@link android.view.View#FOCUS_UP} or {@link android.view.View#FOCUS_DOWN}. 2607 * @return Whether any scrolling, selection or focus change occured. 2608 */ arrowScrollImpl(int direction)2609 private boolean arrowScrollImpl(int direction) { 2610 if (getChildCount() <= 0) { 2611 return false; 2612 } 2613 2614 View selectedView = getSelectedView(); 2615 int selectedPos = mSelectedPosition; 2616 2617 int nextSelectedPosition = nextSelectedPositionForDirection(selectedView, selectedPos, direction); 2618 int amountToScroll = amountToScroll(direction, nextSelectedPosition); 2619 2620 // if we are moving focus, we may OVERRIDE the default behavior 2621 final ArrowScrollFocusResult focusResult = mItemsCanFocus ? arrowScrollFocused(direction) : null; 2622 if (focusResult != null) { 2623 nextSelectedPosition = focusResult.getSelectedPosition(); 2624 amountToScroll = focusResult.getAmountToScroll(); 2625 } 2626 2627 boolean needToRedraw = focusResult != null; 2628 if (nextSelectedPosition != INVALID_POSITION) { 2629 handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null); 2630 setSelectedPositionInt(nextSelectedPosition); 2631 setNextSelectedPositionInt(nextSelectedPosition); 2632 selectedView = getSelectedView(); 2633 selectedPos = nextSelectedPosition; 2634 if (mItemsCanFocus && focusResult == null) { 2635 // there was no new view found to take focus, make sure we 2636 // don't leave focus with the old selection 2637 final View focused = getFocusedChild(); 2638 if (focused != null) { 2639 focused.clearFocus(); 2640 } 2641 } 2642 needToRedraw = true; 2643 checkSelectionChanged(); 2644 } 2645 2646 if (amountToScroll > 0) { 2647 scrollListItemsBy((direction == View.FOCUS_UP) ? amountToScroll : -amountToScroll); 2648 needToRedraw = true; 2649 } 2650 2651 // if we didn't find a new focusable, make sure any existing focused 2652 // item that was panned off screen gives up focus. 2653 if (mItemsCanFocus && (focusResult == null) 2654 && selectedView != null && selectedView.hasFocus()) { 2655 final View focused = selectedView.findFocus(); 2656 if (focused != null) { 2657 if (!isViewAncestorOf(focused, this) || distanceToView(focused) > 0) { 2658 focused.clearFocus(); 2659 } 2660 } 2661 } 2662 2663 // if the current selection is panned off, we need to remove the selection 2664 if (nextSelectedPosition == INVALID_POSITION && selectedView != null 2665 && !isViewAncestorOf(selectedView, this)) { 2666 selectedView = null; 2667 hideSelector(); 2668 2669 // but we don't want to set the ressurect position (that would make subsequent 2670 // unhandled key events bring back the item we just scrolled off!) 2671 mResurrectToPosition = INVALID_POSITION; 2672 } 2673 2674 if (needToRedraw) { 2675 if (selectedView != null) { 2676 positionSelectorLikeFocus(selectedPos, selectedView); 2677 mSelectedTop = selectedView.getTop(); 2678 } 2679 if (!awakenScrollBars()) { 2680 invalidate(); 2681 } 2682 invokeOnItemScrollListener(); 2683 return true; 2684 } 2685 2686 return false; 2687 } 2688 2689 /** 2690 * When selection changes, it is possible that the previously selected or the 2691 * next selected item will change its size. If so, we need to offset some folks, 2692 * and re-layout the items as appropriate. 2693 * 2694 * @param selectedView The currently selected view (before changing selection). 2695 * should be <code>null</code> if there was no previous selection. 2696 * @param direction Either {@link android.view.View#FOCUS_UP} or 2697 * {@link android.view.View#FOCUS_DOWN}. 2698 * @param newSelectedPosition The position of the next selection. 2699 * @param newFocusAssigned whether new focus was assigned. This matters because 2700 * when something has focus, we don't want to show selection (ugh). 2701 */ handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition, boolean newFocusAssigned)2702 private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition, 2703 boolean newFocusAssigned) { 2704 if (newSelectedPosition == INVALID_POSITION) { 2705 throw new IllegalArgumentException("newSelectedPosition needs to be valid"); 2706 } 2707 2708 // whether or not we are moving down or up, we want to preserve the 2709 // top of whatever view is on top: 2710 // - moving down: the view that had selection 2711 // - moving up: the view that is getting selection 2712 View topView; 2713 View bottomView; 2714 int topViewIndex, bottomViewIndex; 2715 boolean topSelected = false; 2716 final int selectedIndex = mSelectedPosition - mFirstPosition; 2717 final int nextSelectedIndex = newSelectedPosition - mFirstPosition; 2718 if (direction == View.FOCUS_UP) { 2719 topViewIndex = nextSelectedIndex; 2720 bottomViewIndex = selectedIndex; 2721 topView = getChildAt(topViewIndex); 2722 bottomView = selectedView; 2723 topSelected = true; 2724 } else { 2725 topViewIndex = selectedIndex; 2726 bottomViewIndex = nextSelectedIndex; 2727 topView = selectedView; 2728 bottomView = getChildAt(bottomViewIndex); 2729 } 2730 2731 final int numChildren = getChildCount(); 2732 2733 // start with top view: is it changing size? 2734 if (topView != null) { 2735 topView.setSelected(!newFocusAssigned && topSelected); 2736 measureAndAdjustDown(topView, topViewIndex, numChildren); 2737 } 2738 2739 // is the bottom view changing size? 2740 if (bottomView != null) { 2741 bottomView.setSelected(!newFocusAssigned && !topSelected); 2742 measureAndAdjustDown(bottomView, bottomViewIndex, numChildren); 2743 } 2744 } 2745 2746 /** 2747 * Re-measure a child, and if its height changes, lay it out preserving its 2748 * top, and adjust the children below it appropriately. 2749 * @param child The child 2750 * @param childIndex The view group index of the child. 2751 * @param numChildren The number of children in the view group. 2752 */ measureAndAdjustDown(View child, int childIndex, int numChildren)2753 private void measureAndAdjustDown(View child, int childIndex, int numChildren) { 2754 int oldHeight = child.getHeight(); 2755 measureItem(child); 2756 if (child.getMeasuredHeight() != oldHeight) { 2757 // lay out the view, preserving its top 2758 relayoutMeasuredItem(child); 2759 2760 // adjust views below appropriately 2761 final int heightDelta = child.getMeasuredHeight() - oldHeight; 2762 for (int i = childIndex + 1; i < numChildren; i++) { 2763 getChildAt(i).offsetTopAndBottom(heightDelta); 2764 } 2765 } 2766 } 2767 2768 /** 2769 * Measure a particular list child. 2770 * TODO: unify with setUpChild. 2771 * @param child The child. 2772 */ measureItem(View child)2773 private void measureItem(View child) { 2774 ViewGroup.LayoutParams p = child.getLayoutParams(); 2775 if (p == null) { 2776 p = new ViewGroup.LayoutParams( 2777 ViewGroup.LayoutParams.MATCH_PARENT, 2778 ViewGroup.LayoutParams.WRAP_CONTENT); 2779 } 2780 2781 int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, 2782 mListPadding.left + mListPadding.right, p.width); 2783 int lpHeight = p.height; 2784 int childHeightSpec; 2785 if (lpHeight > 0) { 2786 childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); 2787 } else { 2788 childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(), 2789 MeasureSpec.UNSPECIFIED); 2790 } 2791 child.measure(childWidthSpec, childHeightSpec); 2792 } 2793 2794 /** 2795 * Layout a child that has been measured, preserving its top position. 2796 * TODO: unify with setUpChild. 2797 * @param child The child. 2798 */ relayoutMeasuredItem(View child)2799 private void relayoutMeasuredItem(View child) { 2800 final int w = child.getMeasuredWidth(); 2801 final int h = child.getMeasuredHeight(); 2802 final int childLeft = mListPadding.left; 2803 final int childRight = childLeft + w; 2804 final int childTop = child.getTop(); 2805 final int childBottom = childTop + h; 2806 child.layout(childLeft, childTop, childRight, childBottom); 2807 } 2808 2809 /** 2810 * @return The amount to preview next items when arrow srolling. 2811 */ getArrowScrollPreviewLength()2812 private int getArrowScrollPreviewLength() { 2813 return Math.max(MIN_SCROLL_PREVIEW_PIXELS, getVerticalFadingEdgeLength()); 2814 } 2815 2816 /** 2817 * Determine how much we need to scroll in order to get the next selected view 2818 * visible, with a fading edge showing below as applicable. The amount is 2819 * capped at {@link #getMaxScrollAmount()} . 2820 * 2821 * @param direction either {@link android.view.View#FOCUS_UP} or 2822 * {@link android.view.View#FOCUS_DOWN}. 2823 * @param nextSelectedPosition The position of the next selection, or 2824 * {@link #INVALID_POSITION} if there is no next selectable position 2825 * @return The amount to scroll. Note: this is always positive! Direction 2826 * needs to be taken into account when actually scrolling. 2827 */ amountToScroll(int direction, int nextSelectedPosition)2828 private int amountToScroll(int direction, int nextSelectedPosition) { 2829 final int listBottom = getHeight() - mListPadding.bottom; 2830 final int listTop = mListPadding.top; 2831 2832 int numChildren = getChildCount(); 2833 2834 if (direction == View.FOCUS_DOWN) { 2835 int indexToMakeVisible = numChildren - 1; 2836 if (nextSelectedPosition != INVALID_POSITION) { 2837 indexToMakeVisible = nextSelectedPosition - mFirstPosition; 2838 } 2839 while (numChildren <= indexToMakeVisible) { 2840 // Child to view is not attached yet. 2841 addViewBelow(getChildAt(numChildren - 1), mFirstPosition + numChildren - 1); 2842 numChildren++; 2843 } 2844 final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; 2845 final View viewToMakeVisible = getChildAt(indexToMakeVisible); 2846 2847 int goalBottom = listBottom; 2848 if (positionToMakeVisible < mItemCount - 1) { 2849 goalBottom -= getArrowScrollPreviewLength(); 2850 } 2851 2852 if (viewToMakeVisible.getBottom() <= goalBottom) { 2853 // item is fully visible. 2854 return 0; 2855 } 2856 2857 if (nextSelectedPosition != INVALID_POSITION 2858 && (goalBottom - viewToMakeVisible.getTop()) >= getMaxScrollAmount()) { 2859 // item already has enough of it visible, changing selection is good enough 2860 return 0; 2861 } 2862 2863 int amountToScroll = (viewToMakeVisible.getBottom() - goalBottom); 2864 2865 if ((mFirstPosition + numChildren) == mItemCount) { 2866 // last is last in list -> make sure we don't scroll past it 2867 final int max = getChildAt(numChildren - 1).getBottom() - listBottom; 2868 amountToScroll = Math.min(amountToScroll, max); 2869 } 2870 2871 return Math.min(amountToScroll, getMaxScrollAmount()); 2872 } else { 2873 int indexToMakeVisible = 0; 2874 if (nextSelectedPosition != INVALID_POSITION) { 2875 indexToMakeVisible = nextSelectedPosition - mFirstPosition; 2876 } 2877 while (indexToMakeVisible < 0) { 2878 // Child to view is not attached yet. 2879 addViewAbove(getChildAt(0), mFirstPosition); 2880 mFirstPosition--; 2881 indexToMakeVisible = nextSelectedPosition - mFirstPosition; 2882 } 2883 final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; 2884 final View viewToMakeVisible = getChildAt(indexToMakeVisible); 2885 int goalTop = listTop; 2886 if (positionToMakeVisible > 0) { 2887 goalTop += getArrowScrollPreviewLength(); 2888 } 2889 if (viewToMakeVisible.getTop() >= goalTop) { 2890 // item is fully visible. 2891 return 0; 2892 } 2893 2894 if (nextSelectedPosition != INVALID_POSITION && 2895 (viewToMakeVisible.getBottom() - goalTop) >= getMaxScrollAmount()) { 2896 // item already has enough of it visible, changing selection is good enough 2897 return 0; 2898 } 2899 2900 int amountToScroll = (goalTop - viewToMakeVisible.getTop()); 2901 if (mFirstPosition == 0) { 2902 // first is first in list -> make sure we don't scroll past it 2903 final int max = listTop - getChildAt(0).getTop(); 2904 amountToScroll = Math.min(amountToScroll, max); 2905 } 2906 return Math.min(amountToScroll, getMaxScrollAmount()); 2907 } 2908 } 2909 2910 /** 2911 * Holds results of focus aware arrow scrolling. 2912 */ 2913 static private class ArrowScrollFocusResult { 2914 private int mSelectedPosition; 2915 private int mAmountToScroll; 2916 2917 /** 2918 * How {@link android.widget.ListView#arrowScrollFocused} returns its values. 2919 */ populate(int selectedPosition, int amountToScroll)2920 void populate(int selectedPosition, int amountToScroll) { 2921 mSelectedPosition = selectedPosition; 2922 mAmountToScroll = amountToScroll; 2923 } 2924 getSelectedPosition()2925 public int getSelectedPosition() { 2926 return mSelectedPosition; 2927 } 2928 getAmountToScroll()2929 public int getAmountToScroll() { 2930 return mAmountToScroll; 2931 } 2932 } 2933 2934 /** 2935 * @param direction either {@link android.view.View#FOCUS_UP} or 2936 * {@link android.view.View#FOCUS_DOWN}. 2937 * @return The position of the next selectable position of the views that 2938 * are currently visible, taking into account the fact that there might 2939 * be no selection. Returns {@link #INVALID_POSITION} if there is no 2940 * selectable view on screen in the given direction. 2941 */ lookForSelectablePositionOnScreen(int direction)2942 private int lookForSelectablePositionOnScreen(int direction) { 2943 final int firstPosition = mFirstPosition; 2944 if (direction == View.FOCUS_DOWN) { 2945 int startPos = (mSelectedPosition != INVALID_POSITION) ? 2946 mSelectedPosition + 1 : 2947 firstPosition; 2948 if (startPos >= mAdapter.getCount()) { 2949 return INVALID_POSITION; 2950 } 2951 if (startPos < firstPosition) { 2952 startPos = firstPosition; 2953 } 2954 2955 final int lastVisiblePos = getLastVisiblePosition(); 2956 final ListAdapter adapter = getAdapter(); 2957 for (int pos = startPos; pos <= lastVisiblePos; pos++) { 2958 if (adapter.isEnabled(pos) 2959 && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) { 2960 return pos; 2961 } 2962 } 2963 } else { 2964 int last = firstPosition + getChildCount() - 1; 2965 int startPos = (mSelectedPosition != INVALID_POSITION) ? 2966 mSelectedPosition - 1 : 2967 firstPosition + getChildCount() - 1; 2968 if (startPos < 0 || startPos >= mAdapter.getCount()) { 2969 return INVALID_POSITION; 2970 } 2971 if (startPos > last) { 2972 startPos = last; 2973 } 2974 2975 final ListAdapter adapter = getAdapter(); 2976 for (int pos = startPos; pos >= firstPosition; pos--) { 2977 if (adapter.isEnabled(pos) 2978 && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) { 2979 return pos; 2980 } 2981 } 2982 } 2983 return INVALID_POSITION; 2984 } 2985 2986 /** 2987 * Do an arrow scroll based on focus searching. If a new view is 2988 * given focus, return the selection delta and amount to scroll via 2989 * an {@link ArrowScrollFocusResult}, otherwise, return null. 2990 * 2991 * @param direction either {@link android.view.View#FOCUS_UP} or 2992 * {@link android.view.View#FOCUS_DOWN}. 2993 * @return The result if focus has changed, or <code>null</code>. 2994 */ arrowScrollFocused(final int direction)2995 private ArrowScrollFocusResult arrowScrollFocused(final int direction) { 2996 final View selectedView = getSelectedView(); 2997 View newFocus; 2998 if (selectedView != null && selectedView.hasFocus()) { 2999 View oldFocus = selectedView.findFocus(); 3000 newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction); 3001 } else { 3002 if (direction == View.FOCUS_DOWN) { 3003 final boolean topFadingEdgeShowing = (mFirstPosition > 0); 3004 final int listTop = mListPadding.top + 3005 (topFadingEdgeShowing ? getArrowScrollPreviewLength() : 0); 3006 final int ySearchPoint = 3007 (selectedView != null && selectedView.getTop() > listTop) ? 3008 selectedView.getTop() : 3009 listTop; 3010 mTempRect.set(0, ySearchPoint, 0, ySearchPoint); 3011 } else { 3012 final boolean bottomFadingEdgeShowing = 3013 (mFirstPosition + getChildCount() - 1) < mItemCount; 3014 final int listBottom = getHeight() - mListPadding.bottom - 3015 (bottomFadingEdgeShowing ? getArrowScrollPreviewLength() : 0); 3016 final int ySearchPoint = 3017 (selectedView != null && selectedView.getBottom() < listBottom) ? 3018 selectedView.getBottom() : 3019 listBottom; 3020 mTempRect.set(0, ySearchPoint, 0, ySearchPoint); 3021 } 3022 newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction); 3023 } 3024 3025 if (newFocus != null) { 3026 final int positionOfNewFocus = positionOfNewFocus(newFocus); 3027 3028 // if the focus change is in a different new position, make sure 3029 // we aren't jumping over another selectable position 3030 if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) { 3031 final int selectablePosition = lookForSelectablePositionOnScreen(direction); 3032 if (selectablePosition != INVALID_POSITION && 3033 ((direction == View.FOCUS_DOWN && selectablePosition < positionOfNewFocus) || 3034 (direction == View.FOCUS_UP && selectablePosition > positionOfNewFocus))) { 3035 return null; 3036 } 3037 } 3038 3039 int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus); 3040 3041 final int maxScrollAmount = getMaxScrollAmount(); 3042 if (focusScroll < maxScrollAmount) { 3043 // not moving too far, safe to give next view focus 3044 newFocus.requestFocus(direction); 3045 mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll); 3046 return mArrowScrollFocusResult; 3047 } else if (distanceToView(newFocus) < maxScrollAmount){ 3048 // Case to consider: 3049 // too far to get entire next focusable on screen, but by going 3050 // max scroll amount, we are getting it at least partially in view, 3051 // so give it focus and scroll the max ammount. 3052 newFocus.requestFocus(direction); 3053 mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount); 3054 return mArrowScrollFocusResult; 3055 } 3056 } 3057 return null; 3058 } 3059 3060 /** 3061 * @param newFocus The view that would have focus. 3062 * @return the position that contains newFocus 3063 */ 3064 private int positionOfNewFocus(View newFocus) { 3065 final int numChildren = getChildCount(); 3066 for (int i = 0; i < numChildren; i++) { 3067 final View child = getChildAt(i); 3068 if (isViewAncestorOf(newFocus, child)) { 3069 return mFirstPosition + i; 3070 } 3071 } 3072 throw new IllegalArgumentException("newFocus is not a child of any of the" 3073 + " children of the list!"); 3074 } 3075 3076 /** 3077 * Return true if child is an ancestor of parent, (or equal to the parent). 3078 */ 3079 private boolean isViewAncestorOf(View child, View parent) { 3080 if (child == parent) { 3081 return true; 3082 } 3083 3084 final ViewParent theParent = child.getParent(); 3085 return (theParent instanceof ViewGroup) && isViewAncestorOf((View) theParent, parent); 3086 } 3087 3088 /** 3089 * Determine how much we need to scroll in order to get newFocus in view. 3090 * @param direction either {@link android.view.View#FOCUS_UP} or 3091 * {@link android.view.View#FOCUS_DOWN}. 3092 * @param newFocus The view that would take focus. 3093 * @param positionOfNewFocus The position of the list item containing newFocus 3094 * @return The amount to scroll. Note: this is always positive! Direction 3095 * needs to be taken into account when actually scrolling. 3096 */ 3097 private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) { 3098 int amountToScroll = 0; 3099 newFocus.getDrawingRect(mTempRect); 3100 offsetDescendantRectToMyCoords(newFocus, mTempRect); 3101 if (direction == View.FOCUS_UP) { 3102 if (mTempRect.top < mListPadding.top) { 3103 amountToScroll = mListPadding.top - mTempRect.top; 3104 if (positionOfNewFocus > 0) { 3105 amountToScroll += getArrowScrollPreviewLength(); 3106 } 3107 } 3108 } else { 3109 final int listBottom = getHeight() - mListPadding.bottom; 3110 if (mTempRect.bottom > listBottom) { 3111 amountToScroll = mTempRect.bottom - listBottom; 3112 if (positionOfNewFocus < mItemCount - 1) { 3113 amountToScroll += getArrowScrollPreviewLength(); 3114 } 3115 } 3116 } 3117 return amountToScroll; 3118 } 3119 3120 /** 3121 * Determine the distance to the nearest edge of a view in a particular 3122 * direction. 3123 * 3124 * @param descendant A descendant of this list. 3125 * @return The distance, or 0 if the nearest edge is already on screen. 3126 */ 3127 private int distanceToView(View descendant) { 3128 int distance = 0; 3129 descendant.getDrawingRect(mTempRect); 3130 offsetDescendantRectToMyCoords(descendant, mTempRect); 3131 final int listBottom = mBottom - mTop - mListPadding.bottom; 3132 if (mTempRect.bottom < mListPadding.top) { 3133 distance = mListPadding.top - mTempRect.bottom; 3134 } else if (mTempRect.top > listBottom) { 3135 distance = mTempRect.top - listBottom; 3136 } 3137 return distance; 3138 } 3139 3140 3141 /** 3142 * Scroll the children by amount, adding a view at the end and removing 3143 * views that fall off as necessary. 3144 * 3145 * @param amount The amount (positive or negative) to scroll. 3146 */ 3147 private void scrollListItemsBy(int amount) { 3148 offsetChildrenTopAndBottom(amount); 3149 3150 final int listBottom = getHeight() - mListPadding.bottom; 3151 final int listTop = mListPadding.top; 3152 final AbsListView.RecycleBin recycleBin = mRecycler; 3153 3154 if (amount < 0) { 3155 // shifted items up 3156 3157 // may need to pan views into the bottom space 3158 int numChildren = getChildCount(); 3159 View last = getChildAt(numChildren - 1); 3160 while (last.getBottom() < listBottom) { 3161 final int lastVisiblePosition = mFirstPosition + numChildren - 1; 3162 if (lastVisiblePosition < mItemCount - 1) { 3163 last = addViewBelow(last, lastVisiblePosition); 3164 numChildren++; 3165 } else { 3166 break; 3167 } 3168 } 3169 3170 // may have brought in the last child of the list that is skinnier 3171 // than the fading edge, thereby leaving space at the end. need 3172 // to shift back 3173 if (last.getBottom() < listBottom) { 3174 offsetChildrenTopAndBottom(listBottom - last.getBottom()); 3175 } 3176 3177 // top views may be panned off screen 3178 View first = getChildAt(0); 3179 while (first.getBottom() < listTop) { 3180 AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams(); 3181 if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { 3182 recycleBin.addScrapView(first, mFirstPosition); 3183 } 3184 detachViewFromParent(first); 3185 first = getChildAt(0); 3186 mFirstPosition++; 3187 } 3188 } else { 3189 // shifted items down 3190 View first = getChildAt(0); 3191 3192 // may need to pan views into top 3193 while ((first.getTop() > listTop) && (mFirstPosition > 0)) { 3194 first = addViewAbove(first, mFirstPosition); 3195 mFirstPosition--; 3196 } 3197 3198 // may have brought the very first child of the list in too far and 3199 // need to shift it back 3200 if (first.getTop() > listTop) { 3201 offsetChildrenTopAndBottom(listTop - first.getTop()); 3202 } 3203 3204 int lastIndex = getChildCount() - 1; 3205 View last = getChildAt(lastIndex); 3206 3207 // bottom view may be panned off screen 3208 while (last.getTop() > listBottom) { 3209 AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams(); 3210 if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { 3211 recycleBin.addScrapView(last, mFirstPosition+lastIndex); 3212 } 3213 detachViewFromParent(last); 3214 last = getChildAt(--lastIndex); 3215 } 3216 } 3217 recycleBin.fullyDetachScrapViews(); 3218 removeUnusedFixedViews(mHeaderViewInfos); 3219 removeUnusedFixedViews(mFooterViewInfos); 3220 } 3221 3222 private View addViewAbove(View theView, int position) { 3223 int abovePosition = position - 1; 3224 View view = obtainView(abovePosition, mIsScrap); 3225 int edgeOfNewChild = theView.getTop() - mDividerHeight; 3226 setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left, 3227 false, mIsScrap[0]); 3228 return view; 3229 } 3230 3231 private View addViewBelow(View theView, int position) { 3232 int belowPosition = position + 1; 3233 View view = obtainView(belowPosition, mIsScrap); 3234 int edgeOfNewChild = theView.getBottom() + mDividerHeight; 3235 setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left, 3236 false, mIsScrap[0]); 3237 return view; 3238 } 3239 3240 /** 3241 * Indicates that the views created by the ListAdapter can contain focusable 3242 * items. 3243 * 3244 * @param itemsCanFocus true if items can get focus, false otherwise 3245 */ 3246 public void setItemsCanFocus(boolean itemsCanFocus) { 3247 mItemsCanFocus = itemsCanFocus; 3248 if (!itemsCanFocus) { 3249 setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); 3250 } 3251 } 3252 3253 /** 3254 * @return Whether the views created by the ListAdapter can contain focusable 3255 * items. 3256 */ 3257 public boolean getItemsCanFocus() { 3258 return mItemsCanFocus; 3259 } 3260 3261 @Override 3262 public boolean isOpaque() { 3263 boolean retValue = (mCachingActive && mIsCacheColorOpaque && mDividerIsOpaque && 3264 hasOpaqueScrollbars()) || super.isOpaque(); 3265 if (retValue) { 3266 // only return true if the list items cover the entire area of the view 3267 final int listTop = mListPadding != null ? mListPadding.top : mPaddingTop; 3268 View first = getChildAt(0); 3269 if (first == null || first.getTop() > listTop) { 3270 return false; 3271 } 3272 final int listBottom = getHeight() - 3273 (mListPadding != null ? mListPadding.bottom : mPaddingBottom); 3274 View last = getChildAt(getChildCount() - 1); 3275 if (last == null || last.getBottom() < listBottom) { 3276 return false; 3277 } 3278 } 3279 return retValue; 3280 } 3281 3282 @Override 3283 public void setCacheColorHint(int color) { 3284 final boolean opaque = (color >>> 24) == 0xFF; 3285 mIsCacheColorOpaque = opaque; 3286 if (opaque) { 3287 if (mDividerPaint == null) { 3288 mDividerPaint = new Paint(); 3289 } 3290 mDividerPaint.setColor(color); 3291 } 3292 super.setCacheColorHint(color); 3293 } 3294 3295 void drawOverscrollHeader(Canvas canvas, Drawable drawable, Rect bounds) { 3296 final int height = drawable.getMinimumHeight(); 3297 3298 canvas.save(); 3299 canvas.clipRect(bounds); 3300 3301 final int span = bounds.bottom - bounds.top; 3302 if (span < height) { 3303 bounds.top = bounds.bottom - height; 3304 } 3305 3306 drawable.setBounds(bounds); 3307 drawable.draw(canvas); 3308 3309 canvas.restore(); 3310 } 3311 3312 void drawOverscrollFooter(Canvas canvas, Drawable drawable, Rect bounds) { 3313 final int height = drawable.getMinimumHeight(); 3314 3315 canvas.save(); 3316 canvas.clipRect(bounds); 3317 3318 final int span = bounds.bottom - bounds.top; 3319 if (span < height) { 3320 bounds.bottom = bounds.top + height; 3321 } 3322 3323 drawable.setBounds(bounds); 3324 drawable.draw(canvas); 3325 3326 canvas.restore(); 3327 } 3328 3329 @Override 3330 protected void dispatchDraw(Canvas canvas) { 3331 if (mCachingStarted) { 3332 mCachingActive = true; 3333 } 3334 3335 // Draw the dividers 3336 final int dividerHeight = mDividerHeight; 3337 final Drawable overscrollHeader = mOverScrollHeader; 3338 final Drawable overscrollFooter = mOverScrollFooter; 3339 final boolean drawOverscrollHeader = overscrollHeader != null; 3340 final boolean drawOverscrollFooter = overscrollFooter != null; 3341 final boolean drawDividers = dividerHeight > 0 && mDivider != null; 3342 3343 if (drawDividers || drawOverscrollHeader || drawOverscrollFooter) { 3344 // Only modify the top and bottom in the loop, we set the left and right here 3345 final Rect bounds = mTempRect; 3346 bounds.left = mPaddingLeft; 3347 bounds.right = mRight - mLeft - mPaddingRight; 3348 3349 final int count = getChildCount(); 3350 final int headerCount = mHeaderViewInfos.size(); 3351 final int itemCount = mItemCount; 3352 final int footerLimit = (itemCount - mFooterViewInfos.size()); 3353 final boolean headerDividers = mHeaderDividersEnabled; 3354 final boolean footerDividers = mFooterDividersEnabled; 3355 final int first = mFirstPosition; 3356 final boolean areAllItemsSelectable = mAreAllItemsSelectable; 3357 final ListAdapter adapter = mAdapter; 3358 // If the list is opaque *and* the background is not, we want to 3359 // fill a rect where the dividers would be for non-selectable items 3360 // If the list is opaque and the background is also opaque, we don't 3361 // need to draw anything since the background will do it for us 3362 final boolean fillForMissingDividers = isOpaque() && !super.isOpaque(); 3363 3364 if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) { 3365 mDividerPaint = new Paint(); 3366 mDividerPaint.setColor(getCacheColorHint()); 3367 } 3368 final Paint paint = mDividerPaint; 3369 3370 int effectivePaddingTop = 0; 3371 int effectivePaddingBottom = 0; 3372 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 3373 effectivePaddingTop = mListPadding.top; 3374 effectivePaddingBottom = mListPadding.bottom; 3375 } 3376 3377 final int listBottom = mBottom - mTop - effectivePaddingBottom + mScrollY; 3378 if (!mStackFromBottom) { 3379 int bottom = 0; 3380 3381 // Draw top divider or header for overscroll 3382 final int scrollY = mScrollY; 3383 if (count > 0 && scrollY < 0) { 3384 if (drawOverscrollHeader) { 3385 bounds.bottom = 0; 3386 bounds.top = scrollY; 3387 drawOverscrollHeader(canvas, overscrollHeader, bounds); 3388 } else if (drawDividers) { 3389 bounds.bottom = 0; 3390 bounds.top = -dividerHeight; 3391 drawDivider(canvas, bounds, -1); 3392 } 3393 } 3394 3395 for (int i = 0; i < count; i++) { 3396 final int itemIndex = (first + i); 3397 final boolean isHeader = (itemIndex < headerCount); 3398 final boolean isFooter = (itemIndex >= footerLimit); 3399 if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) { 3400 final View child = getChildAt(i); 3401 bottom = child.getBottom(); 3402 final boolean isLastItem = (i == (count - 1)); 3403 3404 if (drawDividers && (bottom < listBottom) 3405 && !(drawOverscrollFooter && isLastItem)) { 3406 final int nextIndex = (itemIndex + 1); 3407 // Draw dividers between enabled items, headers 3408 // and/or footers when enabled and requested, and 3409 // after the last enabled item. 3410 if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader 3411 && (nextIndex >= headerCount)) && (isLastItem 3412 || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter 3413 && (nextIndex < footerLimit)))) { 3414 bounds.top = bottom; 3415 bounds.bottom = bottom + dividerHeight; 3416 drawDivider(canvas, bounds, i); 3417 } else if (fillForMissingDividers) { 3418 bounds.top = bottom; 3419 bounds.bottom = bottom + dividerHeight; 3420 canvas.drawRect(bounds, paint); 3421 } 3422 } 3423 } 3424 } 3425 3426 final int overFooterBottom = mBottom + mScrollY; 3427 if (drawOverscrollFooter && first + count == itemCount && 3428 overFooterBottom > bottom) { 3429 bounds.top = bottom; 3430 bounds.bottom = overFooterBottom; 3431 drawOverscrollFooter(canvas, overscrollFooter, bounds); 3432 } 3433 } else { 3434 int top; 3435 3436 final int scrollY = mScrollY; 3437 3438 if (count > 0 && drawOverscrollHeader) { 3439 bounds.top = scrollY; 3440 bounds.bottom = getChildAt(0).getTop(); 3441 drawOverscrollHeader(canvas, overscrollHeader, bounds); 3442 } 3443 3444 final int start = drawOverscrollHeader ? 1 : 0; 3445 for (int i = start; i < count; i++) { 3446 final int itemIndex = (first + i); 3447 final boolean isHeader = (itemIndex < headerCount); 3448 final boolean isFooter = (itemIndex >= footerLimit); 3449 if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) { 3450 final View child = getChildAt(i); 3451 top = child.getTop(); 3452 if (drawDividers && (top > effectivePaddingTop)) { 3453 final boolean isFirstItem = (i == start); 3454 final int previousIndex = (itemIndex - 1); 3455 // Draw dividers between enabled items, headers 3456 // and/or footers when enabled and requested, and 3457 // before the first enabled item. 3458 if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader 3459 && (previousIndex >= headerCount)) && (isFirstItem || 3460 adapter.isEnabled(previousIndex) && (footerDividers || !isFooter 3461 && (previousIndex < footerLimit)))) { 3462 bounds.top = top - dividerHeight; 3463 bounds.bottom = top; 3464 // Give the method the child ABOVE the divider, 3465 // so we subtract one from our child position. 3466 // Give -1 when there is no child above the 3467 // divider. 3468 drawDivider(canvas, bounds, i - 1); 3469 } else if (fillForMissingDividers) { 3470 bounds.top = top - dividerHeight; 3471 bounds.bottom = top; 3472 canvas.drawRect(bounds, paint); 3473 } 3474 } 3475 } 3476 } 3477 3478 if (count > 0 && scrollY > 0) { 3479 if (drawOverscrollFooter) { 3480 final int absListBottom = mBottom; 3481 bounds.top = absListBottom; 3482 bounds.bottom = absListBottom + scrollY; 3483 drawOverscrollFooter(canvas, overscrollFooter, bounds); 3484 } else if (drawDividers) { 3485 bounds.top = listBottom; 3486 bounds.bottom = listBottom + dividerHeight; 3487 drawDivider(canvas, bounds, -1); 3488 } 3489 } 3490 } 3491 } 3492 3493 // Draw the indicators (these should be drawn above the dividers) and children 3494 super.dispatchDraw(canvas); 3495 } 3496 3497 @Override 3498 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 3499 boolean more = super.drawChild(canvas, child, drawingTime); 3500 if (mCachingActive && child.mCachingFailed) { 3501 mCachingActive = false; 3502 } 3503 return more; 3504 } 3505 3506 /** 3507 * Draws a divider for the given child in the given bounds. 3508 * 3509 * @param canvas The canvas to draw to. 3510 * @param bounds The bounds of the divider. 3511 * @param childIndex The index of child (of the View) above the divider. 3512 * This will be -1 if there is no child above the divider to be 3513 * drawn. 3514 */ 3515 void drawDivider(Canvas canvas, Rect bounds, int childIndex) { 3516 // This widget draws the same divider for all children 3517 final Drawable divider = mDivider; 3518 3519 divider.setBounds(bounds); 3520 divider.draw(canvas); 3521 } 3522 3523 /** 3524 * Returns the drawable that will be drawn between each item in the list. 3525 * 3526 * @return the current drawable drawn between list elements 3527 * @attr ref R.styleable#ListView_divider 3528 */ 3529 @Nullable 3530 public Drawable getDivider() { 3531 return mDivider; 3532 } 3533 3534 /** 3535 * Sets the drawable that will be drawn between each item in the list. 3536 * <p> 3537 * <strong>Note:</strong> If the drawable does not have an intrinsic 3538 * height, you should also call {@link #setDividerHeight(int)}. 3539 * 3540 * @param divider the drawable to use 3541 * @attr ref R.styleable#ListView_divider 3542 */ 3543 public void setDivider(@Nullable Drawable divider) { 3544 if (divider != null) { 3545 mDividerHeight = divider.getIntrinsicHeight(); 3546 } else { 3547 mDividerHeight = 0; 3548 } 3549 mDivider = divider; 3550 mDividerIsOpaque = divider == null || divider.getOpacity() == PixelFormat.OPAQUE; 3551 requestLayout(); 3552 invalidate(); 3553 } 3554 3555 /** 3556 * @return Returns the height of the divider that will be drawn between each item in the list. 3557 */ 3558 public int getDividerHeight() { 3559 return mDividerHeight; 3560 } 3561 3562 /** 3563 * Sets the height of the divider that will be drawn between each item in the list. Calling 3564 * this will override the intrinsic height as set by {@link #setDivider(Drawable)} 3565 * 3566 * @param height The new height of the divider in pixels. 3567 */ 3568 public void setDividerHeight(int height) { 3569 mDividerHeight = height; 3570 requestLayout(); 3571 invalidate(); 3572 } 3573 3574 /** 3575 * Enables or disables the drawing of the divider for header views. 3576 * 3577 * @param headerDividersEnabled True to draw the headers, false otherwise. 3578 * 3579 * @see #setFooterDividersEnabled(boolean) 3580 * @see #areHeaderDividersEnabled() 3581 * @see #addHeaderView(android.view.View) 3582 */ 3583 public void setHeaderDividersEnabled(boolean headerDividersEnabled) { 3584 mHeaderDividersEnabled = headerDividersEnabled; 3585 invalidate(); 3586 } 3587 3588 /** 3589 * @return Whether the drawing of the divider for header views is enabled 3590 * 3591 * @see #setHeaderDividersEnabled(boolean) 3592 */ 3593 public boolean areHeaderDividersEnabled() { 3594 return mHeaderDividersEnabled; 3595 } 3596 3597 /** 3598 * Enables or disables the drawing of the divider for footer views. 3599 * 3600 * @param footerDividersEnabled True to draw the footers, false otherwise. 3601 * 3602 * @see #setHeaderDividersEnabled(boolean) 3603 * @see #areFooterDividersEnabled() 3604 * @see #addFooterView(android.view.View) 3605 */ 3606 public void setFooterDividersEnabled(boolean footerDividersEnabled) { 3607 mFooterDividersEnabled = footerDividersEnabled; 3608 invalidate(); 3609 } 3610 3611 /** 3612 * @return Whether the drawing of the divider for footer views is enabled 3613 * 3614 * @see #setFooterDividersEnabled(boolean) 3615 */ 3616 public boolean areFooterDividersEnabled() { 3617 return mFooterDividersEnabled; 3618 } 3619 3620 /** 3621 * Sets the drawable that will be drawn above all other list content. 3622 * This area can become visible when the user overscrolls the list. 3623 * 3624 * @param header The drawable to use 3625 */ 3626 public void setOverscrollHeader(Drawable header) { 3627 mOverScrollHeader = header; 3628 if (mScrollY < 0) { 3629 invalidate(); 3630 } 3631 } 3632 3633 /** 3634 * @return The drawable that will be drawn above all other list content 3635 */ 3636 public Drawable getOverscrollHeader() { 3637 return mOverScrollHeader; 3638 } 3639 3640 /** 3641 * Sets the drawable that will be drawn below all other list content. 3642 * This area can become visible when the user overscrolls the list, 3643 * or when the list's content does not fully fill the container area. 3644 * 3645 * @param footer The drawable to use 3646 */ 3647 public void setOverscrollFooter(Drawable footer) { 3648 mOverScrollFooter = footer; 3649 invalidate(); 3650 } 3651 3652 /** 3653 * @return The drawable that will be drawn below all other list content 3654 */ 3655 public Drawable getOverscrollFooter() { 3656 return mOverScrollFooter; 3657 } 3658 3659 @Override 3660 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 3661 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 3662 3663 final ListAdapter adapter = mAdapter; 3664 int closetChildIndex = -1; 3665 int closestChildTop = 0; 3666 if (adapter != null && gainFocus && previouslyFocusedRect != null) { 3667 previouslyFocusedRect.offset(mScrollX, mScrollY); 3668 3669 // Don't cache the result of getChildCount or mFirstPosition here, 3670 // it could change in layoutChildren. 3671 if (adapter.getCount() < getChildCount() + mFirstPosition) { 3672 mLayoutMode = LAYOUT_NORMAL; 3673 layoutChildren(); 3674 } 3675 3676 // figure out which item should be selected based on previously 3677 // focused rect 3678 Rect otherRect = mTempRect; 3679 int minDistance = Integer.MAX_VALUE; 3680 final int childCount = getChildCount(); 3681 final int firstPosition = mFirstPosition; 3682 3683 for (int i = 0; i < childCount; i++) { 3684 // only consider selectable views 3685 if (!adapter.isEnabled(firstPosition + i)) { 3686 continue; 3687 } 3688 3689 View other = getChildAt(i); 3690 other.getDrawingRect(otherRect); 3691 offsetDescendantRectToMyCoords(other, otherRect); 3692 int distance = getDistance(previouslyFocusedRect, otherRect, direction); 3693 3694 if (distance < minDistance) { 3695 minDistance = distance; 3696 closetChildIndex = i; 3697 closestChildTop = other.getTop(); 3698 } 3699 } 3700 } 3701 3702 if (closetChildIndex >= 0) { 3703 setSelectionFromTop(closetChildIndex + mFirstPosition, closestChildTop); 3704 } else { 3705 requestLayout(); 3706 } 3707 } 3708 3709 3710 /* 3711 * (non-Javadoc) 3712 * 3713 * Children specified in XML are assumed to be header views. After we have 3714 * parsed them move them out of the children list and into mHeaderViews. 3715 */ 3716 @Override 3717 protected void onFinishInflate() { 3718 super.onFinishInflate(); 3719 3720 int count = getChildCount(); 3721 if (count > 0) { 3722 for (int i = 0; i < count; ++i) { 3723 addHeaderView(getChildAt(i)); 3724 } 3725 removeAllViews(); 3726 } 3727 } 3728 3729 /* (non-Javadoc) 3730 * @see android.view.View#findViewById(int) 3731 * First look in our children, then in any header and footer views that may be scrolled off. 3732 */ 3733 @Override 3734 protected View findViewTraversal(@IdRes int id) { 3735 View v; 3736 v = super.findViewTraversal(id); 3737 if (v == null) { 3738 v = findViewInHeadersOrFooters(mHeaderViewInfos, id); 3739 if (v != null) { 3740 return v; 3741 } 3742 v = findViewInHeadersOrFooters(mFooterViewInfos, id); 3743 if (v != null) { 3744 return v; 3745 } 3746 } 3747 return v; 3748 } 3749 3750 /* (non-Javadoc) 3751 * 3752 * Look in the passed in list of headers or footers for the view. 3753 */ 3754 View findViewInHeadersOrFooters(ArrayList<FixedViewInfo> where, int id) { 3755 if (where != null) { 3756 int len = where.size(); 3757 View v; 3758 3759 for (int i = 0; i < len; i++) { 3760 v = where.get(i).view; 3761 3762 if (!v.isRootNamespace()) { 3763 v = v.findViewById(id); 3764 3765 if (v != null) { 3766 return v; 3767 } 3768 } 3769 } 3770 } 3771 return null; 3772 } 3773 3774 /* (non-Javadoc) 3775 * @see android.view.View#findViewWithTag(Object) 3776 * First look in our children, then in any header and footer views that may be scrolled off. 3777 */ 3778 @Override 3779 protected View findViewWithTagTraversal(Object tag) { 3780 View v; 3781 v = super.findViewWithTagTraversal(tag); 3782 if (v == null) { 3783 v = findViewWithTagInHeadersOrFooters(mHeaderViewInfos, tag); 3784 if (v != null) { 3785 return v; 3786 } 3787 3788 v = findViewWithTagInHeadersOrFooters(mFooterViewInfos, tag); 3789 if (v != null) { 3790 return v; 3791 } 3792 } 3793 return v; 3794 } 3795 3796 /* (non-Javadoc) 3797 * 3798 * Look in the passed in list of headers or footers for the view with the tag. 3799 */ 3800 View findViewWithTagInHeadersOrFooters(ArrayList<FixedViewInfo> where, Object tag) { 3801 if (where != null) { 3802 int len = where.size(); 3803 View v; 3804 3805 for (int i = 0; i < len; i++) { 3806 v = where.get(i).view; 3807 3808 if (!v.isRootNamespace()) { 3809 v = v.findViewWithTag(tag); 3810 3811 if (v != null) { 3812 return v; 3813 } 3814 } 3815 } 3816 } 3817 return null; 3818 } 3819 3820 /** 3821 * @hide 3822 * @see android.view.View#findViewByPredicate(Predicate) 3823 * First look in our children, then in any header and footer views that may be scrolled off. 3824 */ 3825 @Override 3826 protected View findViewByPredicateTraversal(Predicate<View> predicate, View childToSkip) { 3827 View v; 3828 v = super.findViewByPredicateTraversal(predicate, childToSkip); 3829 if (v == null) { 3830 v = findViewByPredicateInHeadersOrFooters(mHeaderViewInfos, predicate, childToSkip); 3831 if (v != null) { 3832 return v; 3833 } 3834 3835 v = findViewByPredicateInHeadersOrFooters(mFooterViewInfos, predicate, childToSkip); 3836 if (v != null) { 3837 return v; 3838 } 3839 } 3840 return v; 3841 } 3842 3843 /* (non-Javadoc) 3844 * 3845 * Look in the passed in list of headers or footers for the first view that matches 3846 * the predicate. 3847 */ 3848 View findViewByPredicateInHeadersOrFooters(ArrayList<FixedViewInfo> where, 3849 Predicate<View> predicate, View childToSkip) { 3850 if (where != null) { 3851 int len = where.size(); 3852 View v; 3853 3854 for (int i = 0; i < len; i++) { 3855 v = where.get(i).view; 3856 3857 if (v != childToSkip && !v.isRootNamespace()) { 3858 v = v.findViewByPredicate(predicate); 3859 3860 if (v != null) { 3861 return v; 3862 } 3863 } 3864 } 3865 } 3866 return null; 3867 } 3868 3869 /** 3870 * Returns the set of checked items ids. The result is only valid if the 3871 * choice mode has not been set to {@link #CHOICE_MODE_NONE}. 3872 * 3873 * @return A new array which contains the id of each checked item in the 3874 * list. 3875 * 3876 * @deprecated Use {@link #getCheckedItemIds()} instead. 3877 */ 3878 @Deprecated 3879 public long[] getCheckItemIds() { 3880 // Use new behavior that correctly handles stable ID mapping. 3881 if (mAdapter != null && mAdapter.hasStableIds()) { 3882 return getCheckedItemIds(); 3883 } 3884 3885 // Old behavior was buggy, but would sort of work for adapters without stable IDs. 3886 // Fall back to it to support legacy apps. 3887 if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null && mAdapter != null) { 3888 final SparseBooleanArray states = mCheckStates; 3889 final int count = states.size(); 3890 final long[] ids = new long[count]; 3891 final ListAdapter adapter = mAdapter; 3892 3893 int checkedCount = 0; 3894 for (int i = 0; i < count; i++) { 3895 if (states.valueAt(i)) { 3896 ids[checkedCount++] = adapter.getItemId(states.keyAt(i)); 3897 } 3898 } 3899 3900 // Trim array if needed. mCheckStates may contain false values 3901 // resulting in checkedCount being smaller than count. 3902 if (checkedCount == count) { 3903 return ids; 3904 } else { 3905 final long[] result = new long[checkedCount]; 3906 System.arraycopy(ids, 0, result, 0, checkedCount); 3907 3908 return result; 3909 } 3910 } 3911 return new long[0]; 3912 } 3913 3914 @Override 3915 int getHeightForPosition(int position) { 3916 final int height = super.getHeightForPosition(position); 3917 if (shouldAdjustHeightForDivider(position)) { 3918 return height + mDividerHeight; 3919 } 3920 return height; 3921 } 3922 3923 private boolean shouldAdjustHeightForDivider(int itemIndex) { 3924 final int dividerHeight = mDividerHeight; 3925 final Drawable overscrollHeader = mOverScrollHeader; 3926 final Drawable overscrollFooter = mOverScrollFooter; 3927 final boolean drawOverscrollHeader = overscrollHeader != null; 3928 final boolean drawOverscrollFooter = overscrollFooter != null; 3929 final boolean drawDividers = dividerHeight > 0 && mDivider != null; 3930 3931 if (drawDividers) { 3932 final boolean fillForMissingDividers = isOpaque() && !super.isOpaque(); 3933 final int itemCount = mItemCount; 3934 final int headerCount = mHeaderViewInfos.size(); 3935 final int footerLimit = (itemCount - mFooterViewInfos.size()); 3936 final boolean isHeader = (itemIndex < headerCount); 3937 final boolean isFooter = (itemIndex >= footerLimit); 3938 final boolean headerDividers = mHeaderDividersEnabled; 3939 final boolean footerDividers = mFooterDividersEnabled; 3940 if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) { 3941 final ListAdapter adapter = mAdapter; 3942 if (!mStackFromBottom) { 3943 final boolean isLastItem = (itemIndex == (itemCount - 1)); 3944 if (!drawOverscrollFooter || !isLastItem) { 3945 final int nextIndex = itemIndex + 1; 3946 // Draw dividers between enabled items, headers 3947 // and/or footers when enabled and requested, and 3948 // after the last enabled item. 3949 if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader 3950 && (nextIndex >= headerCount)) && (isLastItem 3951 || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter 3952 && (nextIndex < footerLimit)))) { 3953 return true; 3954 } else if (fillForMissingDividers) { 3955 return true; 3956 } 3957 } 3958 } else { 3959 final int start = drawOverscrollHeader ? 1 : 0; 3960 final boolean isFirstItem = (itemIndex == start); 3961 if (!isFirstItem) { 3962 final int previousIndex = (itemIndex - 1); 3963 // Draw dividers between enabled items, headers 3964 // and/or footers when enabled and requested, and 3965 // before the first enabled item. 3966 if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader 3967 && (previousIndex >= headerCount)) && (isFirstItem || 3968 adapter.isEnabled(previousIndex) && (footerDividers || !isFooter 3969 && (previousIndex < footerLimit)))) { 3970 return true; 3971 } else if (fillForMissingDividers) { 3972 return true; 3973 } 3974 } 3975 } 3976 } 3977 } 3978 3979 return false; 3980 } 3981 3982 @Override 3983 public CharSequence getAccessibilityClassName() { 3984 return ListView.class.getName(); 3985 } 3986 3987 /** @hide */ 3988 @Override 3989 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 3990 super.onInitializeAccessibilityNodeInfoInternal(info); 3991 3992 final int rowsCount = getCount(); 3993 final int selectionMode = getSelectionModeForAccessibility(); 3994 final CollectionInfo collectionInfo = CollectionInfo.obtain( 3995 rowsCount, 1, false, selectionMode); 3996 info.setCollectionInfo(collectionInfo); 3997 3998 if (rowsCount > 0) { 3999 info.addAction(AccessibilityAction.ACTION_SCROLL_TO_POSITION); 4000 } 4001 } 4002 4003 /** @hide */ 4004 @Override 4005 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 4006 if (super.performAccessibilityActionInternal(action, arguments)) { 4007 return true; 4008 } 4009 4010 switch (action) { 4011 case R.id.accessibilityActionScrollToPosition: { 4012 final int row = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, -1); 4013 final int position = Math.min(row, getCount() - 1); 4014 if (row >= 0) { 4015 // The accessibility service gets data asynchronously, so 4016 // we'll be a little lenient by clamping the last position. 4017 smoothScrollToPosition(position); 4018 return true; 4019 } 4020 } break; 4021 } 4022 4023 return false; 4024 } 4025 4026 @Override 4027 public void onInitializeAccessibilityNodeInfoForItem( 4028 View view, int position, AccessibilityNodeInfo info) { 4029 super.onInitializeAccessibilityNodeInfoForItem(view, position, info); 4030 4031 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 4032 final boolean isHeading = lp != null && lp.viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER; 4033 final boolean isSelected = isItemChecked(position); 4034 final CollectionItemInfo itemInfo = CollectionItemInfo.obtain( 4035 position, 1, 0, 1, isHeading, isSelected); 4036 info.setCollectionItemInfo(itemInfo); 4037 } 4038 4039 /** @hide */ 4040 @Override 4041 protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) { 4042 super.encodeProperties(encoder); 4043 4044 encoder.addProperty("recycleOnMeasure", recycleOnMeasure()); 4045 } 4046 } 4047