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