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