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