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