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