1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.widget; 18 19 import android.annotation.IdRes; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.res.TypedArray; 25 import android.graphics.Canvas; 26 import android.graphics.Paint; 27 import android.graphics.PixelFormat; 28 import android.graphics.Rect; 29 import android.graphics.drawable.Drawable; 30 import android.os.Bundle; 31 import android.os.Trace; 32 import android.util.AttributeSet; 33 import android.util.Log; 34 import android.util.MathUtils; 35 import android.util.SparseBooleanArray; 36 import android.view.FocusFinder; 37 import android.view.KeyEvent; 38 import android.view.SoundEffectConstants; 39 import android.view.View; 40 import android.view.ViewDebug; 41 import android.view.ViewGroup; 42 import android.view.ViewHierarchyEncoder; 43 import android.view.ViewParent; 44 import android.view.ViewRootImpl; 45 import android.view.accessibility.AccessibilityNodeInfo; 46 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 47 import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; 48 import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo; 49 import android.view.accessibility.AccessibilityNodeProvider; 50 import android.widget.RemoteViews.RemoteView; 51 52 import com.android.internal.R; 53 54 import com.google.android.collect.Lists; 55 56 import java.util.ArrayList; 57 import java.util.List; 58 import 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 150 public 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 ListView(Context context)214 public ListView(Context context) { 215 this(context, null); 216 } 217 ListView(Context context, AttributeSet attrs)218 public ListView(Context context, AttributeSet attrs) { 219 this(context, attrs, R.attr.listViewStyle); 220 } 221 ListView(Context context, AttributeSet attrs, int defStyleAttr)222 public ListView(Context context, AttributeSet attrs, int defStyleAttr) { 223 this(context, attrs, defStyleAttr, 0); 224 } 225 ListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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 */ getMaxScrollAmount()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 */ adjustViewsUpOrDown()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 */ addHeaderView(View v, Object data, boolean isSelectable)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 */ addHeaderView(View v)382 public void addHeaderView(View v) { 383 addHeaderView(v, null, true); 384 } 385 386 @Override getHeaderViewsCount()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 */ removeHeaderView(View v)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 removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where)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 */ addFooterView(View v, Object data, boolean isSelectable)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 */ addFooterView(View v)483 public void addFooterView(View v) { 484 addFooterView(v, null, true); 485 } 486 487 @Override getFooterViewsCount()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 */ removeFooterView(View v)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 getAdapter()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") setRemoteViewsAdapter(Intent intent)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 setAdapter(ListAdapter adapter)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 resetList()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 clearRecycledState(ArrayList<FixedViewInfo> infos)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 LayoutParams p = (LayoutParams) child.getLayoutParams(); 628 if (p != null) { 629 p.recycledHeaderFooter = false; 630 } 631 } 632 } 633 } 634 635 /** 636 * @return Whether the list needs to show the top fading edge 637 */ showingTopFadingEdge()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 */ showingBottomFadingEdge()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 requestChildRectangleOnScreen(View child, Rect rect, boolean immediate)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 fillGap(boolean down)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 */ fillDown(int pos, int nextTop)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 */ fillUp(int pos, int nextBottom)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 */ fillFromTop(int nextTop)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 */ fillFromMiddle(int childrenTop, int childrenBottom)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 */ fillAboveAndBelow(View sel, int position)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 */ fillFromSelection(int selectedTop, int childrenTop, int childrenBottom)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 */ getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength, int selectedPosition)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 */ getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int selectedPosition)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 smoothScrollToPosition(int position)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 smoothScrollByOffset(int offset)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 */ moveSelection(View oldSel, View newSel, int delta, int childrenTop, int childrenBottom)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 setupForSetSelection(int position, int top)1205 FocusSelector setupForSetSelection(int position, int top) { 1206 mPosition = position; 1207 mPositionTop = top; 1208 mAction = STATE_SET_SELECTION; 1209 return this; 1210 } 1211 run()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 setupFocusIfValid(int position)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 onLayoutComplete()1234 void onLayoutComplete() { 1235 if (mAction == STATE_WAIT_FOR_LAYOUT) { 1236 mAction = -1; 1237 } 1238 } 1239 } 1240 1241 @Override onDetachedFromWindow()1242 protected void onDetachedFromWindow() { 1243 if (mFocusSelector != null) { 1244 removeCallbacks(mFocusSelector); 1245 mFocusSelector = null; 1246 } 1247 super.onDetachedFromWindow(); 1248 } 1249 1250 @Override onSizeChanged(int w, int h, int oldw, int oldh)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 onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 measureScrapChild(View child, int position, int widthMeasureSpec, int heightHint)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") recycleOnMeasure()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 */ measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, int maxHeight, int disallowPartialChildPosition)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 findMotionRow(int y)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 */ fillSpecific(int position, int top)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 */ correctTooHigh(int childCount)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 */ correctTooLow(int childCount)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 layoutChildren()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 trackMotionScroll(int deltaY, int incrementalDeltaY)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 */ removeUnusedFixedViews(@ullable List<FixedViewInfo> infoList)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 */ isDirectChildHeaderOrFooter(View child)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 */ makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected)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 */ setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean isAttachedToWindow)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 canAnimate()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 setSelection(int position)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 setSelectionInt(int position)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 lookForSelectablePosition(int position, boolean lookDown)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 */ lookForSelectablePositionAfter(int current, int position, boolean lookDown)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 */ setSelectionAfterHeaderView()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 dispatchKeyEvent(KeyEvent event)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 onKeyDown(int keyCode, KeyEvent event)2345 public boolean onKeyDown(int keyCode, KeyEvent event) { 2346 return commonKey(keyCode, 1, event); 2347 } 2348 2349 @Override onKeyMultiple(int keyCode, int repeatCount, KeyEvent event)2350 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { 2351 return commonKey(keyCode, repeatCount, event); 2352 } 2353 2354 @Override onKeyUp(int keyCode, KeyEvent event)2355 public boolean onKeyUp(int keyCode, KeyEvent event) { 2356 return commonKey(keyCode, 1, event); 2357 } 2358 commonKey(int keyCode, int count, KeyEvent event)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 */ pageScroll(int direction)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 */ fullScroll(int direction)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 */ handleHorizontalFocusWithinListItem(int direction)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 */ arrowScroll(int direction)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 */ nextSelectedPositionForDirection( View selectedView, int selectedPos, int direction)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 */ arrowScrollImpl(int direction)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 */ handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition, boolean newFocusAssigned)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 */ measureAndAdjustDown(View child, int childIndex, int numChildren)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 */ measureItem(View child)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 */ relayoutMeasuredItem(View child)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 */ getArrowScrollPreviewLength()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 */ amountToScroll(int direction, int nextSelectedPosition)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 */ populate(int selectedPosition, int amountToScroll)3011 void populate(int selectedPosition, int amountToScroll) { 3012 mSelectedPosition = selectedPosition; 3013 mAmountToScroll = amountToScroll; 3014 } 3015 getSelectedPosition()3016 public int getSelectedPosition() { 3017 return mSelectedPosition; 3018 } 3019 getAmountToScroll()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 */ lookForSelectablePositionOnScreen(int direction)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 */ arrowScrollFocused(final int direction)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