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