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