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