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