ListView.java revision d296fafab597a6597d9eb276abe6f6eb049e9f12
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 Paint mDividerPaint; 138 139 // the single allocated result per list view; kinda cheesey but avoids 140 // allocating these thingies too often. 141 private ArrowScrollFocusResult mArrowScrollFocusResult = new ArrowScrollFocusResult(); 142 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 the item count is less than 15 then subtract disabled items from the count and 1856 // position. Otherwise ignore disabled items. 1857 if (!populated) { 1858 int itemCount = 0; 1859 int currentItemIndex = getSelectedItemPosition(); 1860 1861 ListAdapter adapter = getAdapter(); 1862 if (adapter != null) { 1863 final int count = adapter.getCount(); 1864 if (count < 15) { 1865 for (int i = 0; i < count; i++) { 1866 if (adapter.isEnabled(i)) { 1867 itemCount++; 1868 } else if (i <= currentItemIndex) { 1869 currentItemIndex--; 1870 } 1871 } 1872 } else { 1873 itemCount = count; 1874 } 1875 } 1876 1877 event.setItemCount(itemCount); 1878 event.setCurrentItemIndex(currentItemIndex); 1879 } 1880 1881 return populated; 1882 } 1883 1884 /** 1885 * setSelectionAfterHeaderView set the selection to be the first list item 1886 * after the header views. 1887 */ 1888 public void setSelectionAfterHeaderView() { 1889 final int count = mHeaderViewInfos.size(); 1890 if (count > 0) { 1891 mNextSelectedPosition = 0; 1892 return; 1893 } 1894 1895 if (mAdapter != null) { 1896 setSelection(count); 1897 } else { 1898 mNextSelectedPosition = count; 1899 mLayoutMode = LAYOUT_SET_SELECTION; 1900 } 1901 1902 } 1903 1904 @Override 1905 public boolean dispatchKeyEvent(KeyEvent event) { 1906 // Dispatch in the normal way 1907 boolean handled = super.dispatchKeyEvent(event); 1908 if (!handled) { 1909 // If we didn't handle it... 1910 View focused = getFocusedChild(); 1911 if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) { 1912 // ... and our focused child didn't handle it 1913 // ... give it to ourselves so we can scroll if necessary 1914 handled = onKeyDown(event.getKeyCode(), event); 1915 } 1916 } 1917 return handled; 1918 } 1919 1920 @Override 1921 public boolean onKeyDown(int keyCode, KeyEvent event) { 1922 return commonKey(keyCode, 1, event); 1923 } 1924 1925 @Override 1926 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { 1927 return commonKey(keyCode, repeatCount, event); 1928 } 1929 1930 @Override 1931 public boolean onKeyUp(int keyCode, KeyEvent event) { 1932 return commonKey(keyCode, 1, event); 1933 } 1934 1935 private boolean commonKey(int keyCode, int count, KeyEvent event) { 1936 if (mAdapter == null) { 1937 return false; 1938 } 1939 1940 if (mDataChanged) { 1941 layoutChildren(); 1942 } 1943 1944 boolean handled = false; 1945 int action = event.getAction(); 1946 1947 if (action != KeyEvent.ACTION_UP) { 1948 if (mSelectedPosition < 0) { 1949 switch (keyCode) { 1950 case KeyEvent.KEYCODE_DPAD_UP: 1951 case KeyEvent.KEYCODE_DPAD_DOWN: 1952 case KeyEvent.KEYCODE_DPAD_CENTER: 1953 case KeyEvent.KEYCODE_ENTER: 1954 case KeyEvent.KEYCODE_SPACE: 1955 if (resurrectSelection()) { 1956 return true; 1957 } 1958 } 1959 } 1960 switch (keyCode) { 1961 case KeyEvent.KEYCODE_DPAD_UP: 1962 if (!event.isAltPressed()) { 1963 while (count > 0) { 1964 handled = arrowScroll(FOCUS_UP); 1965 count--; 1966 } 1967 } else { 1968 handled = fullScroll(FOCUS_UP); 1969 } 1970 break; 1971 1972 case KeyEvent.KEYCODE_DPAD_DOWN: 1973 if (!event.isAltPressed()) { 1974 while (count > 0) { 1975 handled = arrowScroll(FOCUS_DOWN); 1976 count--; 1977 } 1978 } else { 1979 handled = fullScroll(FOCUS_DOWN); 1980 } 1981 break; 1982 1983 case KeyEvent.KEYCODE_DPAD_LEFT: 1984 handled = handleHorizontalFocusWithinListItem(View.FOCUS_LEFT); 1985 break; 1986 case KeyEvent.KEYCODE_DPAD_RIGHT: 1987 handled = handleHorizontalFocusWithinListItem(View.FOCUS_RIGHT); 1988 break; 1989 1990 case KeyEvent.KEYCODE_DPAD_CENTER: 1991 case KeyEvent.KEYCODE_ENTER: 1992 if (mItemCount > 0 && event.getRepeatCount() == 0) { 1993 keyPressed(); 1994 } 1995 handled = true; 1996 break; 1997 1998 case KeyEvent.KEYCODE_SPACE: 1999 if (mPopup == null || !mPopup.isShowing()) { 2000 if (!event.isShiftPressed()) { 2001 pageScroll(FOCUS_DOWN); 2002 } else { 2003 pageScroll(FOCUS_UP); 2004 } 2005 handled = true; 2006 } 2007 break; 2008 } 2009 } 2010 2011 if (!handled) { 2012 handled = sendToTextFilter(keyCode, count, event); 2013 } 2014 2015 if (handled) { 2016 return true; 2017 } else { 2018 switch (action) { 2019 case KeyEvent.ACTION_DOWN: 2020 return super.onKeyDown(keyCode, event); 2021 2022 case KeyEvent.ACTION_UP: 2023 return super.onKeyUp(keyCode, event); 2024 2025 case KeyEvent.ACTION_MULTIPLE: 2026 return super.onKeyMultiple(keyCode, count, event); 2027 2028 default: // shouldn't happen 2029 return false; 2030 } 2031 } 2032 } 2033 2034 /** 2035 * Scrolls up or down by the number of items currently present on screen. 2036 * 2037 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 2038 * @return whether selection was moved 2039 */ 2040 boolean pageScroll(int direction) { 2041 int nextPage = -1; 2042 boolean down = false; 2043 2044 if (direction == FOCUS_UP) { 2045 nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1); 2046 } else if (direction == FOCUS_DOWN) { 2047 nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1); 2048 down = true; 2049 } 2050 2051 if (nextPage >= 0) { 2052 int position = lookForSelectablePosition(nextPage, down); 2053 if (position >= 0) { 2054 mLayoutMode = LAYOUT_SPECIFIC; 2055 mSpecificTop = mPaddingTop + getVerticalFadingEdgeLength(); 2056 2057 if (down && position > mItemCount - getChildCount()) { 2058 mLayoutMode = LAYOUT_FORCE_BOTTOM; 2059 } 2060 2061 if (!down && position < getChildCount()) { 2062 mLayoutMode = LAYOUT_FORCE_TOP; 2063 } 2064 2065 setSelectionInt(position); 2066 invokeOnItemScrollListener(); 2067 invalidate(); 2068 2069 return true; 2070 } 2071 } 2072 2073 return false; 2074 } 2075 2076 /** 2077 * Go to the last or first item if possible (not worrying about panning across or navigating 2078 * within the internal focus of the currently selected item.) 2079 * 2080 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 2081 * 2082 * @return whether selection was moved 2083 */ 2084 boolean fullScroll(int direction) { 2085 boolean moved = false; 2086 if (direction == FOCUS_UP) { 2087 if (mSelectedPosition != 0) { 2088 int position = lookForSelectablePosition(0, true); 2089 if (position >= 0) { 2090 mLayoutMode = LAYOUT_FORCE_TOP; 2091 setSelectionInt(position); 2092 invokeOnItemScrollListener(); 2093 } 2094 moved = true; 2095 } 2096 } else if (direction == FOCUS_DOWN) { 2097 if (mSelectedPosition < mItemCount - 1) { 2098 int position = lookForSelectablePosition(mItemCount - 1, true); 2099 if (position >= 0) { 2100 mLayoutMode = LAYOUT_FORCE_BOTTOM; 2101 setSelectionInt(position); 2102 invokeOnItemScrollListener(); 2103 } 2104 moved = true; 2105 } 2106 } 2107 2108 if (moved) { 2109 invalidate(); 2110 } 2111 2112 return moved; 2113 } 2114 2115 /** 2116 * To avoid horizontal focus searches changing the selected item, we 2117 * manually focus search within the selected item (as applicable), and 2118 * prevent focus from jumping to something within another item. 2119 * @param direction one of {View.FOCUS_LEFT, View.FOCUS_RIGHT} 2120 * @return Whether this consumes the key event. 2121 */ 2122 private boolean handleHorizontalFocusWithinListItem(int direction) { 2123 if (direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) { 2124 throw new IllegalArgumentException("direction must be one of" 2125 + " {View.FOCUS_LEFT, View.FOCUS_RIGHT}"); 2126 } 2127 2128 final int numChildren = getChildCount(); 2129 if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) { 2130 final View selectedView = getSelectedView(); 2131 if (selectedView != null && selectedView.hasFocus() && 2132 selectedView instanceof ViewGroup) { 2133 2134 final View currentFocus = selectedView.findFocus(); 2135 final View nextFocus = FocusFinder.getInstance().findNextFocus( 2136 (ViewGroup) selectedView, currentFocus, direction); 2137 if (nextFocus != null) { 2138 // do the math to get interesting rect in next focus' coordinates 2139 currentFocus.getFocusedRect(mTempRect); 2140 offsetDescendantRectToMyCoords(currentFocus, mTempRect); 2141 offsetRectIntoDescendantCoords(nextFocus, mTempRect); 2142 if (nextFocus.requestFocus(direction, mTempRect)) { 2143 return true; 2144 } 2145 } 2146 // we are blocking the key from being handled (by returning true) 2147 // if the global result is going to be some other view within this 2148 // list. this is to acheive the overall goal of having 2149 // horizontal d-pad navigation remain in the current item. 2150 final View globalNextFocus = FocusFinder.getInstance().findNextFocus( 2151 (ViewGroup) getRootView(), currentFocus, direction); 2152 if (globalNextFocus != null) { 2153 return isViewAncestorOf(globalNextFocus, this); 2154 } 2155 } 2156 } 2157 return false; 2158 } 2159 2160 /** 2161 * Scrolls to the next or previous item if possible. 2162 * 2163 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} 2164 * 2165 * @return whether selection was moved 2166 */ 2167 boolean arrowScroll(int direction) { 2168 try { 2169 mInLayout = true; 2170 final boolean handled = arrowScrollImpl(direction); 2171 if (handled) { 2172 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); 2173 } 2174 return handled; 2175 } finally { 2176 mInLayout = false; 2177 } 2178 } 2179 2180 /** 2181 * Handle an arrow scroll going up or down. Take into account whether items are selectable, 2182 * whether there are focusable items etc. 2183 * 2184 * @param direction Either {@link android.view.View#FOCUS_UP} or {@link android.view.View#FOCUS_DOWN}. 2185 * @return Whether any scrolling, selection or focus change occured. 2186 */ 2187 private boolean arrowScrollImpl(int direction) { 2188 if (getChildCount() <= 0) { 2189 return false; 2190 } 2191 2192 View selectedView = getSelectedView(); 2193 2194 int nextSelectedPosition = lookForSelectablePositionOnScreen(direction); 2195 int amountToScroll = amountToScroll(direction, nextSelectedPosition); 2196 2197 // if we are moving focus, we may OVERRIDE the default behavior 2198 final ArrowScrollFocusResult focusResult = mItemsCanFocus ? arrowScrollFocused(direction) : null; 2199 if (focusResult != null) { 2200 nextSelectedPosition = focusResult.getSelectedPosition(); 2201 amountToScroll = focusResult.getAmountToScroll(); 2202 } 2203 2204 boolean needToRedraw = focusResult != null; 2205 if (nextSelectedPosition != INVALID_POSITION) { 2206 handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null); 2207 setSelectedPositionInt(nextSelectedPosition); 2208 setNextSelectedPositionInt(nextSelectedPosition); 2209 selectedView = getSelectedView(); 2210 if (mItemsCanFocus && focusResult == null) { 2211 // there was no new view found to take focus, make sure we 2212 // don't leave focus with the old selection 2213 final View focused = getFocusedChild(); 2214 if (focused != null) { 2215 focused.clearFocus(); 2216 } 2217 } 2218 needToRedraw = true; 2219 checkSelectionChanged(); 2220 } 2221 2222 if (amountToScroll > 0) { 2223 scrollListItemsBy((direction == View.FOCUS_UP) ? amountToScroll : -amountToScroll); 2224 needToRedraw = true; 2225 } 2226 2227 // if we didn't find a new focusable, make sure any existing focused 2228 // item that was panned off screen gives up focus. 2229 if (mItemsCanFocus && (focusResult == null) 2230 && selectedView != null && selectedView.hasFocus()) { 2231 final View focused = selectedView.findFocus(); 2232 if (distanceToView(focused) > 0) { 2233 focused.clearFocus(); 2234 } 2235 } 2236 2237 // if the current selection is panned off, we need to remove the selection 2238 if (nextSelectedPosition == INVALID_POSITION && selectedView != null 2239 && !isViewAncestorOf(selectedView, this)) { 2240 selectedView = null; 2241 hideSelector(); 2242 2243 // but we don't want to set the ressurect position (that would make subsequent 2244 // unhandled key events bring back the item we just scrolled off!) 2245 mResurrectToPosition = INVALID_POSITION; 2246 } 2247 2248 if (needToRedraw) { 2249 if (selectedView != null) { 2250 positionSelector(selectedView); 2251 mSelectedTop = selectedView.getTop(); 2252 } 2253 invalidate(); 2254 invokeOnItemScrollListener(); 2255 return true; 2256 } 2257 2258 return false; 2259 } 2260 2261 /** 2262 * When selection changes, it is possible that the previously selected or the 2263 * next selected item will change its size. If so, we need to offset some folks, 2264 * and re-layout the items as appropriate. 2265 * 2266 * @param selectedView The currently selected view (before changing selection). 2267 * should be <code>null</code> if there was no previous selection. 2268 * @param direction Either {@link android.view.View#FOCUS_UP} or 2269 * {@link android.view.View#FOCUS_DOWN}. 2270 * @param newSelectedPosition The position of the next selection. 2271 * @param newFocusAssigned whether new focus was assigned. This matters because 2272 * when something has focus, we don't want to show selection (ugh). 2273 */ 2274 private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition, 2275 boolean newFocusAssigned) { 2276 if (newSelectedPosition == INVALID_POSITION) { 2277 throw new IllegalArgumentException("newSelectedPosition needs to be valid"); 2278 } 2279 2280 // whether or not we are moving down or up, we want to preserve the 2281 // top of whatever view is on top: 2282 // - moving down: the view that had selection 2283 // - moving up: the view that is getting selection 2284 View topView; 2285 View bottomView; 2286 int topViewIndex, bottomViewIndex; 2287 boolean topSelected = false; 2288 final int selectedIndex = mSelectedPosition - mFirstPosition; 2289 final int nextSelectedIndex = newSelectedPosition - mFirstPosition; 2290 if (direction == View.FOCUS_UP) { 2291 topViewIndex = nextSelectedIndex; 2292 bottomViewIndex = selectedIndex; 2293 topView = getChildAt(topViewIndex); 2294 bottomView = selectedView; 2295 topSelected = true; 2296 } else { 2297 topViewIndex = selectedIndex; 2298 bottomViewIndex = nextSelectedIndex; 2299 topView = selectedView; 2300 bottomView = getChildAt(bottomViewIndex); 2301 } 2302 2303 final int numChildren = getChildCount(); 2304 2305 // start with top view: is it changing size? 2306 if (topView != null) { 2307 topView.setSelected(!newFocusAssigned && topSelected); 2308 measureAndAdjustDown(topView, topViewIndex, numChildren); 2309 } 2310 2311 // is the bottom view changing size? 2312 if (bottomView != null) { 2313 bottomView.setSelected(!newFocusAssigned && !topSelected); 2314 measureAndAdjustDown(bottomView, bottomViewIndex, numChildren); 2315 } 2316 } 2317 2318 /** 2319 * Re-measure a child, and if its height changes, lay it out preserving its 2320 * top, and adjust the children below it appropriately. 2321 * @param child The child 2322 * @param childIndex The view group index of the child. 2323 * @param numChildren The number of children in the view group. 2324 */ 2325 private void measureAndAdjustDown(View child, int childIndex, int numChildren) { 2326 int oldHeight = child.getHeight(); 2327 measureItem(child); 2328 if (child.getMeasuredHeight() != oldHeight) { 2329 // lay out the view, preserving its top 2330 relayoutMeasuredItem(child); 2331 2332 // adjust views below appropriately 2333 final int heightDelta = child.getMeasuredHeight() - oldHeight; 2334 for (int i = childIndex + 1; i < numChildren; i++) { 2335 getChildAt(i).offsetTopAndBottom(heightDelta); 2336 } 2337 } 2338 } 2339 2340 /** 2341 * Measure a particular list child. 2342 * TODO: unify with setUpChild. 2343 * @param child The child. 2344 */ 2345 private void measureItem(View child) { 2346 ViewGroup.LayoutParams p = child.getLayoutParams(); 2347 if (p == null) { 2348 p = new ViewGroup.LayoutParams( 2349 ViewGroup.LayoutParams.FILL_PARENT, 2350 ViewGroup.LayoutParams.WRAP_CONTENT); 2351 } 2352 2353 int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, 2354 mListPadding.left + mListPadding.right, p.width); 2355 int lpHeight = p.height; 2356 int childHeightSpec; 2357 if (lpHeight > 0) { 2358 childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY); 2359 } else { 2360 childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 2361 } 2362 child.measure(childWidthSpec, childHeightSpec); 2363 } 2364 2365 /** 2366 * Layout a child that has been measured, preserving its top position. 2367 * TODO: unify with setUpChild. 2368 * @param child The child. 2369 */ 2370 private void relayoutMeasuredItem(View child) { 2371 final int w = child.getMeasuredWidth(); 2372 final int h = child.getMeasuredHeight(); 2373 final int childLeft = mListPadding.left; 2374 final int childRight = childLeft + w; 2375 final int childTop = child.getTop(); 2376 final int childBottom = childTop + h; 2377 child.layout(childLeft, childTop, childRight, childBottom); 2378 } 2379 2380 /** 2381 * @return The amount to preview next items when arrow srolling. 2382 */ 2383 private int getArrowScrollPreviewLength() { 2384 return Math.max(MIN_SCROLL_PREVIEW_PIXELS, getVerticalFadingEdgeLength()); 2385 } 2386 2387 /** 2388 * Determine how much we need to scroll in order to get the next selected view 2389 * visible, with a fading edge showing below as applicable. The amount is 2390 * capped at {@link #getMaxScrollAmount()} . 2391 * 2392 * @param direction either {@link android.view.View#FOCUS_UP} or 2393 * {@link android.view.View#FOCUS_DOWN}. 2394 * @param nextSelectedPosition The position of the next selection, or 2395 * {@link #INVALID_POSITION} if there is no next selectable position 2396 * @return The amount to scroll. Note: this is always positive! Direction 2397 * needs to be taken into account when actually scrolling. 2398 */ 2399 private int amountToScroll(int direction, int nextSelectedPosition) { 2400 final int listBottom = getHeight() - mListPadding.bottom; 2401 final int listTop = mListPadding.top; 2402 2403 final int numChildren = getChildCount(); 2404 2405 if (direction == View.FOCUS_DOWN) { 2406 int indexToMakeVisible = numChildren - 1; 2407 if (nextSelectedPosition != INVALID_POSITION) { 2408 indexToMakeVisible = nextSelectedPosition - mFirstPosition; 2409 } 2410 2411 final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; 2412 final View viewToMakeVisible = getChildAt(indexToMakeVisible); 2413 2414 int goalBottom = listBottom; 2415 if (positionToMakeVisible < mItemCount - 1) { 2416 goalBottom -= getArrowScrollPreviewLength(); 2417 } 2418 2419 if (viewToMakeVisible.getBottom() <= goalBottom) { 2420 // item is fully visible. 2421 return 0; 2422 } 2423 2424 if (nextSelectedPosition != INVALID_POSITION 2425 && (goalBottom - viewToMakeVisible.getTop()) >= getMaxScrollAmount()) { 2426 // item already has enough of it visible, changing selection is good enough 2427 return 0; 2428 } 2429 2430 int amountToScroll = (viewToMakeVisible.getBottom() - goalBottom); 2431 2432 if ((mFirstPosition + numChildren) == mItemCount) { 2433 // last is last in list -> make sure we don't scroll past it 2434 final int max = getChildAt(numChildren - 1).getBottom() - listBottom; 2435 amountToScroll = Math.min(amountToScroll, max); 2436 } 2437 2438 return Math.min(amountToScroll, getMaxScrollAmount()); 2439 } else { 2440 int indexToMakeVisible = 0; 2441 if (nextSelectedPosition != INVALID_POSITION) { 2442 indexToMakeVisible = nextSelectedPosition - mFirstPosition; 2443 } 2444 final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; 2445 final View viewToMakeVisible = getChildAt(indexToMakeVisible); 2446 int goalTop = listTop; 2447 if (positionToMakeVisible > 0) { 2448 goalTop += getArrowScrollPreviewLength(); 2449 } 2450 if (viewToMakeVisible.getTop() >= goalTop) { 2451 // item is fully visible. 2452 return 0; 2453 } 2454 2455 if (nextSelectedPosition != INVALID_POSITION && 2456 (viewToMakeVisible.getBottom() - goalTop) >= getMaxScrollAmount()) { 2457 // item already has enough of it visible, changing selection is good enough 2458 return 0; 2459 } 2460 2461 int amountToScroll = (goalTop - viewToMakeVisible.getTop()); 2462 if (mFirstPosition == 0) { 2463 // first is first in list -> make sure we don't scroll past it 2464 final int max = listTop - getChildAt(0).getTop(); 2465 amountToScroll = Math.min(amountToScroll, max); 2466 } 2467 return Math.min(amountToScroll, getMaxScrollAmount()); 2468 } 2469 } 2470 2471 /** 2472 * Holds results of focus aware arrow scrolling. 2473 */ 2474 static private class ArrowScrollFocusResult { 2475 private int mSelectedPosition; 2476 private int mAmountToScroll; 2477 2478 /** 2479 * How {@link android.widget.ListView#arrowScrollFocused} returns its values. 2480 */ 2481 void populate(int selectedPosition, int amountToScroll) { 2482 mSelectedPosition = selectedPosition; 2483 mAmountToScroll = amountToScroll; 2484 } 2485 2486 public int getSelectedPosition() { 2487 return mSelectedPosition; 2488 } 2489 2490 public int getAmountToScroll() { 2491 return mAmountToScroll; 2492 } 2493 } 2494 2495 /** 2496 * @param direction either {@link android.view.View#FOCUS_UP} or 2497 * {@link android.view.View#FOCUS_DOWN}. 2498 * @return The position of the next selectable position of the views that 2499 * are currently visible, taking into account the fact that there might 2500 * be no selection. Returns {@link #INVALID_POSITION} if there is no 2501 * selectable view on screen in the given direction. 2502 */ 2503 private int lookForSelectablePositionOnScreen(int direction) { 2504 final int firstPosition = mFirstPosition; 2505 if (direction == View.FOCUS_DOWN) { 2506 int startPos = (mSelectedPosition != INVALID_POSITION) ? 2507 mSelectedPosition + 1 : 2508 firstPosition; 2509 if (startPos >= mAdapter.getCount()) { 2510 return INVALID_POSITION; 2511 } 2512 if (startPos < firstPosition) { 2513 startPos = firstPosition; 2514 } 2515 2516 final int lastVisiblePos = getLastVisiblePosition(); 2517 final ListAdapter adapter = getAdapter(); 2518 for (int pos = startPos; pos <= lastVisiblePos; pos++) { 2519 if (adapter.isEnabled(pos) 2520 && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) { 2521 return pos; 2522 } 2523 } 2524 } else { 2525 int last = firstPosition + getChildCount() - 1; 2526 int startPos = (mSelectedPosition != INVALID_POSITION) ? 2527 mSelectedPosition - 1 : 2528 firstPosition + getChildCount() - 1; 2529 if (startPos < 0) { 2530 return INVALID_POSITION; 2531 } 2532 if (startPos > last) { 2533 startPos = last; 2534 } 2535 2536 final ListAdapter adapter = getAdapter(); 2537 for (int pos = startPos; pos >= firstPosition; pos--) { 2538 if (adapter.isEnabled(pos) 2539 && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) { 2540 return pos; 2541 } 2542 } 2543 } 2544 return INVALID_POSITION; 2545 } 2546 2547 /** 2548 * Do an arrow scroll based on focus searching. If a new view is 2549 * given focus, return the selection delta and amount to scroll via 2550 * an {@link ArrowScrollFocusResult}, otherwise, return null. 2551 * 2552 * @param direction either {@link android.view.View#FOCUS_UP} or 2553 * {@link android.view.View#FOCUS_DOWN}. 2554 * @return The result if focus has changed, or <code>null</code>. 2555 */ 2556 private ArrowScrollFocusResult arrowScrollFocused(final int direction) { 2557 final View selectedView = getSelectedView(); 2558 View newFocus; 2559 if (selectedView != null && selectedView.hasFocus()) { 2560 View oldFocus = selectedView.findFocus(); 2561 newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction); 2562 } else { 2563 if (direction == View.FOCUS_DOWN) { 2564 final boolean topFadingEdgeShowing = (mFirstPosition > 0); 2565 final int listTop = mListPadding.top + 2566 (topFadingEdgeShowing ? getArrowScrollPreviewLength() : 0); 2567 final int ySearchPoint = 2568 (selectedView != null && selectedView.getTop() > listTop) ? 2569 selectedView.getTop() : 2570 listTop; 2571 mTempRect.set(0, ySearchPoint, 0, ySearchPoint); 2572 } else { 2573 final boolean bottomFadingEdgeShowing = 2574 (mFirstPosition + getChildCount() - 1) < mItemCount; 2575 final int listBottom = getHeight() - mListPadding.bottom - 2576 (bottomFadingEdgeShowing ? getArrowScrollPreviewLength() : 0); 2577 final int ySearchPoint = 2578 (selectedView != null && selectedView.getBottom() < listBottom) ? 2579 selectedView.getBottom() : 2580 listBottom; 2581 mTempRect.set(0, ySearchPoint, 0, ySearchPoint); 2582 } 2583 newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction); 2584 } 2585 2586 if (newFocus != null) { 2587 final int positionOfNewFocus = positionOfNewFocus(newFocus); 2588 2589 // if the focus change is in a different new position, make sure 2590 // we aren't jumping over another selectable position 2591 if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) { 2592 final int selectablePosition = lookForSelectablePositionOnScreen(direction); 2593 if (selectablePosition != INVALID_POSITION && 2594 ((direction == View.FOCUS_DOWN && selectablePosition < positionOfNewFocus) || 2595 (direction == View.FOCUS_UP && selectablePosition > positionOfNewFocus))) { 2596 return null; 2597 } 2598 } 2599 2600 int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus); 2601 2602 final int maxScrollAmount = getMaxScrollAmount(); 2603 if (focusScroll < maxScrollAmount) { 2604 // not moving too far, safe to give next view focus 2605 newFocus.requestFocus(direction); 2606 mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll); 2607 return mArrowScrollFocusResult; 2608 } else if (distanceToView(newFocus) < maxScrollAmount){ 2609 // Case to consider: 2610 // too far to get entire next focusable on screen, but by going 2611 // max scroll amount, we are getting it at least partially in view, 2612 // so give it focus and scroll the max ammount. 2613 newFocus.requestFocus(direction); 2614 mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount); 2615 return mArrowScrollFocusResult; 2616 } 2617 } 2618 return null; 2619 } 2620 2621 /** 2622 * @param newFocus The view that would have focus. 2623 * @return the position that contains newFocus 2624 */ 2625 private int positionOfNewFocus(View newFocus) { 2626 final int numChildren = getChildCount(); 2627 for (int i = 0; i < numChildren; i++) { 2628 final View child = getChildAt(i); 2629 if (isViewAncestorOf(newFocus, child)) { 2630 return mFirstPosition + i; 2631 } 2632 } 2633 throw new IllegalArgumentException("newFocus is not a child of any of the" 2634 + " children of the list!"); 2635 } 2636 2637 /** 2638 * Return true if child is an ancestor of parent, (or equal to the parent). 2639 */ 2640 private boolean isViewAncestorOf(View child, View parent) { 2641 if (child == parent) { 2642 return true; 2643 } 2644 2645 final ViewParent theParent = child.getParent(); 2646 return (theParent instanceof ViewGroup) && isViewAncestorOf((View) theParent, parent); 2647 } 2648 2649 /** 2650 * Determine how much we need to scroll in order to get newFocus in view. 2651 * @param direction either {@link android.view.View#FOCUS_UP} or 2652 * {@link android.view.View#FOCUS_DOWN}. 2653 * @param newFocus The view that would take focus. 2654 * @param positionOfNewFocus The position of the list item containing newFocus 2655 * @return The amount to scroll. Note: this is always positive! Direction 2656 * needs to be taken into account when actually scrolling. 2657 */ 2658 private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) { 2659 int amountToScroll = 0; 2660 newFocus.getDrawingRect(mTempRect); 2661 offsetDescendantRectToMyCoords(newFocus, mTempRect); 2662 if (direction == View.FOCUS_UP) { 2663 if (mTempRect.top < mListPadding.top) { 2664 amountToScroll = mListPadding.top - mTempRect.top; 2665 if (positionOfNewFocus > 0) { 2666 amountToScroll += getArrowScrollPreviewLength(); 2667 } 2668 } 2669 } else { 2670 final int listBottom = getHeight() - mListPadding.bottom; 2671 if (mTempRect.bottom > listBottom) { 2672 amountToScroll = mTempRect.bottom - listBottom; 2673 if (positionOfNewFocus < mItemCount - 1) { 2674 amountToScroll += getArrowScrollPreviewLength(); 2675 } 2676 } 2677 } 2678 return amountToScroll; 2679 } 2680 2681 /** 2682 * Determine the distance to the nearest edge of a view in a particular 2683 * direciton. 2684 * @param descendant A descendant of this list. 2685 * @return The distance, or 0 if the nearest edge is already on screen. 2686 */ 2687 private int distanceToView(View descendant) { 2688 int distance = 0; 2689 descendant.getDrawingRect(mTempRect); 2690 offsetDescendantRectToMyCoords(descendant, mTempRect); 2691 final int listBottom = mBottom - mTop - mListPadding.bottom; 2692 if (mTempRect.bottom < mListPadding.top) { 2693 distance = mListPadding.top - mTempRect.bottom; 2694 } else if (mTempRect.top > listBottom) { 2695 distance = mTempRect.top - listBottom; 2696 } 2697 return distance; 2698 } 2699 2700 2701 /** 2702 * Scroll the children by amount, adding a view at the end and removing 2703 * views that fall off as necessary. 2704 * 2705 * @param amount The amount (positive or negative) to scroll. 2706 */ 2707 private void scrollListItemsBy(int amount) { 2708 offsetChildrenTopAndBottom(amount); 2709 2710 final int listBottom = getHeight() - mListPadding.bottom; 2711 final int listTop = mListPadding.top; 2712 final AbsListView.RecycleBin recycleBin = mRecycler; 2713 2714 if (amount < 0) { 2715 // shifted items up 2716 2717 // may need to pan views into the bottom space 2718 int numChildren = getChildCount(); 2719 View last = getChildAt(numChildren - 1); 2720 while (last.getBottom() < listBottom) { 2721 final int lastVisiblePosition = mFirstPosition + numChildren - 1; 2722 if (lastVisiblePosition < mItemCount - 1) { 2723 last = addViewBelow(last, lastVisiblePosition); 2724 numChildren++; 2725 } else { 2726 break; 2727 } 2728 } 2729 2730 // may have brought in the last child of the list that is skinnier 2731 // than the fading edge, thereby leaving space at the end. need 2732 // to shift back 2733 if (last.getBottom() < listBottom) { 2734 offsetChildrenTopAndBottom(listBottom - last.getBottom()); 2735 } 2736 2737 // top views may be panned off screen 2738 View first = getChildAt(0); 2739 while (first.getBottom() < listTop) { 2740 AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams(); 2741 if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { 2742 removeViewInLayout(first); 2743 recycleBin.addScrapView(first); 2744 } else { 2745 detachViewFromParent(first); 2746 } 2747 first = getChildAt(0); 2748 mFirstPosition++; 2749 } 2750 } else { 2751 // shifted items down 2752 View first = getChildAt(0); 2753 2754 // may need to pan views into top 2755 while ((first.getTop() > listTop) && (mFirstPosition > 0)) { 2756 first = addViewAbove(first, mFirstPosition); 2757 mFirstPosition--; 2758 } 2759 2760 // may have brought the very first child of the list in too far and 2761 // need to shift it back 2762 if (first.getTop() > listTop) { 2763 offsetChildrenTopAndBottom(listTop - first.getTop()); 2764 } 2765 2766 int lastIndex = getChildCount() - 1; 2767 View last = getChildAt(lastIndex); 2768 2769 // bottom view may be panned off screen 2770 while (last.getTop() > listBottom) { 2771 AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams(); 2772 if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) { 2773 removeViewInLayout(last); 2774 recycleBin.addScrapView(last); 2775 } else { 2776 detachViewFromParent(last); 2777 } 2778 last = getChildAt(--lastIndex); 2779 } 2780 } 2781 } 2782 2783 private View addViewAbove(View theView, int position) { 2784 int abovePosition = position - 1; 2785 View view = obtainView(abovePosition); 2786 int edgeOfNewChild = theView.getTop() - mDividerHeight; 2787 setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left, false, false); 2788 return view; 2789 } 2790 2791 private View addViewBelow(View theView, int position) { 2792 int belowPosition = position + 1; 2793 View view = obtainView(belowPosition); 2794 int edgeOfNewChild = theView.getBottom() + mDividerHeight; 2795 setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left, false, false); 2796 return view; 2797 } 2798 2799 /** 2800 * Indicates that the views created by the ListAdapter can contain focusable 2801 * items. 2802 * 2803 * @param itemsCanFocus true if items can get focus, false otherwise 2804 */ 2805 public void setItemsCanFocus(boolean itemsCanFocus) { 2806 mItemsCanFocus = itemsCanFocus; 2807 if (!itemsCanFocus) { 2808 setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); 2809 } 2810 } 2811 2812 /** 2813 * @return Whether the views created by the ListAdapter can contain focusable 2814 * items. 2815 */ 2816 public boolean getItemsCanFocus() { 2817 return mItemsCanFocus; 2818 } 2819 2820 /** 2821 * @hide Pending API council approval. 2822 */ 2823 @Override 2824 public boolean isOpaque() { 2825 return (mCachingStarted && mIsCacheColorOpaque && mDividerIsOpaque && 2826 hasOpaqueScrollbars()) || super.isOpaque(); 2827 } 2828 2829 @Override 2830 public void setCacheColorHint(int color) { 2831 final boolean opaque = (color >>> 24) == 0xFF; 2832 mIsCacheColorOpaque = opaque; 2833 if (opaque) { 2834 if (mDividerPaint == null) { 2835 mDividerPaint = new Paint(); 2836 } 2837 mDividerPaint.setColor(color); 2838 } 2839 super.setCacheColorHint(color); 2840 } 2841 2842 @Override 2843 protected void dispatchDraw(Canvas canvas) { 2844 // Draw the dividers 2845 final int dividerHeight = mDividerHeight; 2846 2847 if (dividerHeight > 0 && mDivider != null) { 2848 // Only modify the top and bottom in the loop, we set the left and right here 2849 final Rect bounds = mTempRect; 2850 bounds.left = mPaddingLeft; 2851 bounds.right = mRight - mLeft - mPaddingRight; 2852 2853 final int count = getChildCount(); 2854 final int headerCount = mHeaderViewInfos.size(); 2855 final int footerLimit = mItemCount - mFooterViewInfos.size() - 1; 2856 final boolean headerDividers = mHeaderDividersEnabled; 2857 final boolean footerDividers = mFooterDividersEnabled; 2858 final int first = mFirstPosition; 2859 final boolean areAllItemsSelectable = mAreAllItemsSelectable; 2860 final ListAdapter adapter = mAdapter; 2861 // If the list is opaque *and* the background is not, we want to 2862 // fill a rect where the dividers would be for non-selectable items 2863 // If the list is opaque and the background is also opaque, we don't 2864 // need to draw anything since the background will do it for us 2865 final boolean fillForMissingDividers = isOpaque() && !super.isOpaque(); 2866 2867 if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) { 2868 mDividerPaint = new Paint(); 2869 mDividerPaint.setColor(getCacheColorHint()); 2870 } 2871 final Paint paint = mDividerPaint; 2872 2873 if (!mStackFromBottom) { 2874 int bottom; 2875 int listBottom = mBottom - mTop - mListPadding.bottom; 2876 2877 for (int i = 0; i < count; i++) { 2878 if ((headerDividers || first + i >= headerCount) && 2879 (footerDividers || first + i < footerLimit)) { 2880 View child = getChildAt(i); 2881 bottom = child.getBottom(); 2882 // Don't draw dividers next to items that are not enabled 2883 if (bottom < listBottom) { 2884 if ((areAllItemsSelectable || 2885 (adapter.isEnabled(first + i) && (i == count - 1 || 2886 adapter.isEnabled(first + i + 1))))) { 2887 bounds.top = bottom; 2888 bounds.bottom = bottom + dividerHeight; 2889 drawDivider(canvas, bounds, i); 2890 } else if (fillForMissingDividers) { 2891 bounds.top = bottom; 2892 bounds.bottom = bottom + dividerHeight; 2893 canvas.drawRect(bounds, paint); 2894 } 2895 } 2896 } 2897 } 2898 } else { 2899 int top; 2900 int listTop = mListPadding.top; 2901 2902 for (int i = 0; i < count; i++) { 2903 if ((headerDividers || first + i >= headerCount) && 2904 (footerDividers || first + i < footerLimit)) { 2905 View child = getChildAt(i); 2906 top = child.getTop(); 2907 // Don't draw dividers next to items that are not enabled 2908 if (top > listTop) { 2909 if ((areAllItemsSelectable || 2910 (adapter.isEnabled(first + i) && (i == count - 1 || 2911 adapter.isEnabled(first + i + 1))))) { 2912 bounds.top = top - dividerHeight; 2913 bounds.bottom = top; 2914 // Give the method the child ABOVE the divider, so we 2915 // subtract one from our child 2916 // position. Give -1 when there is no child above the 2917 // divider. 2918 drawDivider(canvas, bounds, i - 1); 2919 } else if (fillForMissingDividers) { 2920 bounds.top = top - dividerHeight; 2921 bounds.bottom = top; 2922 canvas.drawRect(bounds, paint); 2923 } 2924 } 2925 } 2926 } 2927 } 2928 } 2929 2930 // Draw the indicators (these should be drawn above the dividers) and children 2931 super.dispatchDraw(canvas); 2932 } 2933 2934 /** 2935 * Draws a divider for the given child in the given bounds. 2936 * 2937 * @param canvas The canvas to draw to. 2938 * @param bounds The bounds of the divider. 2939 * @param childIndex The index of child (of the View) above the divider. 2940 * This will be -1 if there is no child above the divider to be 2941 * drawn. 2942 */ 2943 void drawDivider(Canvas canvas, Rect bounds, int childIndex) { 2944 // This widget draws the same divider for all children 2945 final Drawable divider = mDivider; 2946 final boolean clipDivider = mClipDivider; 2947 2948 if (!clipDivider) { 2949 divider.setBounds(bounds); 2950 } else { 2951 canvas.save(); 2952 canvas.clipRect(bounds); 2953 } 2954 2955 divider.draw(canvas); 2956 2957 if (clipDivider) { 2958 canvas.restore(); 2959 } 2960 } 2961 2962 /** 2963 * Returns the drawable that will be drawn between each item in the list. 2964 * 2965 * @return the current drawable drawn between list elements 2966 */ 2967 public Drawable getDivider() { 2968 return mDivider; 2969 } 2970 2971 /** 2972 * Sets the drawable that will be drawn between each item in the list. If the drawable does 2973 * not have an intrinsic height, you should also call {@link #setDividerHeight(int)} 2974 * 2975 * @param divider The drawable to use. 2976 */ 2977 public void setDivider(Drawable divider) { 2978 if (divider != null) { 2979 mDividerHeight = divider.getIntrinsicHeight(); 2980 mClipDivider = divider instanceof ColorDrawable; 2981 } else { 2982 mDividerHeight = 0; 2983 mClipDivider = false; 2984 } 2985 mDivider = divider; 2986 mDividerIsOpaque = divider == null || divider.getOpacity() == PixelFormat.OPAQUE; 2987 requestLayoutIfNecessary(); 2988 } 2989 2990 /** 2991 * @return Returns the height of the divider that will be drawn between each item in the list. 2992 */ 2993 public int getDividerHeight() { 2994 return mDividerHeight; 2995 } 2996 2997 /** 2998 * Sets the height of the divider that will be drawn between each item in the list. Calling 2999 * this will override the intrinsic height as set by {@link #setDivider(Drawable)} 3000 * 3001 * @param height The new height of the divider in pixels. 3002 */ 3003 public void setDividerHeight(int height) { 3004 mDividerHeight = height; 3005 requestLayoutIfNecessary(); 3006 } 3007 3008 /** 3009 * Enables or disables the drawing of the divider for header views. 3010 * 3011 * @param headerDividersEnabled True to draw the headers, false otherwise. 3012 * 3013 * @see #setFooterDividersEnabled(boolean) 3014 * @see #addHeaderView(android.view.View) 3015 */ 3016 public void setHeaderDividersEnabled(boolean headerDividersEnabled) { 3017 mHeaderDividersEnabled = headerDividersEnabled; 3018 invalidate(); 3019 } 3020 3021 /** 3022 * Enables or disables the drawing of the divider for footer views. 3023 * 3024 * @param footerDividersEnabled True to draw the footers, false otherwise. 3025 * 3026 * @see #setHeaderDividersEnabled(boolean) 3027 * @see #addFooterView(android.view.View) 3028 */ 3029 public void setFooterDividersEnabled(boolean footerDividersEnabled) { 3030 mFooterDividersEnabled = footerDividersEnabled; 3031 invalidate(); 3032 } 3033 3034 @Override 3035 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 3036 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 3037 3038 int closetChildIndex = -1; 3039 if (gainFocus && previouslyFocusedRect != null) { 3040 previouslyFocusedRect.offset(mScrollX, mScrollY); 3041 3042 // figure out which item should be selected based on previously 3043 // focused rect 3044 Rect otherRect = mTempRect; 3045 int minDistance = Integer.MAX_VALUE; 3046 final int childCount = getChildCount(); 3047 final int firstPosition = mFirstPosition; 3048 final ListAdapter adapter = mAdapter; 3049 3050 for (int i = 0; i < childCount; i++) { 3051 // only consider selectable views 3052 if (!adapter.isEnabled(firstPosition + i)) { 3053 continue; 3054 } 3055 3056 View other = getChildAt(i); 3057 other.getDrawingRect(otherRect); 3058 offsetDescendantRectToMyCoords(other, otherRect); 3059 int distance = getDistance(previouslyFocusedRect, otherRect, direction); 3060 3061 if (distance < minDistance) { 3062 minDistance = distance; 3063 closetChildIndex = i; 3064 } 3065 } 3066 } 3067 3068 if (closetChildIndex >= 0) { 3069 setSelection(closetChildIndex + mFirstPosition); 3070 } else { 3071 requestLayout(); 3072 } 3073 } 3074 3075 3076 /* 3077 * (non-Javadoc) 3078 * 3079 * Children specified in XML are assumed to be header views. After we have 3080 * parsed them move them out of the children list and into mHeaderViews. 3081 */ 3082 @Override 3083 protected void onFinishInflate() { 3084 super.onFinishInflate(); 3085 3086 int count = getChildCount(); 3087 if (count > 0) { 3088 for (int i = 0; i < count; ++i) { 3089 addHeaderView(getChildAt(i)); 3090 } 3091 removeAllViews(); 3092 } 3093 } 3094 3095 /* (non-Javadoc) 3096 * @see android.view.View#findViewById(int) 3097 * First look in our children, then in any header and footer views that may be scrolled off. 3098 */ 3099 @Override 3100 protected View findViewTraversal(int id) { 3101 View v; 3102 v = super.findViewTraversal(id); 3103 if (v == null) { 3104 v = findViewInHeadersOrFooters(mHeaderViewInfos, id); 3105 if (v != null) { 3106 return v; 3107 } 3108 v = findViewInHeadersOrFooters(mFooterViewInfos, id); 3109 if (v != null) { 3110 return v; 3111 } 3112 } 3113 return v; 3114 } 3115 3116 /* (non-Javadoc) 3117 * 3118 * Look in the passed in list of headers or footers for the view. 3119 */ 3120 View findViewInHeadersOrFooters(ArrayList<FixedViewInfo> where, int id) { 3121 if (where != null) { 3122 int len = where.size(); 3123 View v; 3124 3125 for (int i = 0; i < len; i++) { 3126 v = where.get(i).view; 3127 3128 if (!v.isRootNamespace()) { 3129 v = v.findViewById(id); 3130 3131 if (v != null) { 3132 return v; 3133 } 3134 } 3135 } 3136 } 3137 return null; 3138 } 3139 3140 /* (non-Javadoc) 3141 * @see android.view.View#findViewWithTag(String) 3142 * First look in our children, then in any header and footer views that may be scrolled off. 3143 */ 3144 @Override 3145 protected View findViewWithTagTraversal(Object tag) { 3146 View v; 3147 v = super.findViewWithTagTraversal(tag); 3148 if (v == null) { 3149 v = findViewTagInHeadersOrFooters(mHeaderViewInfos, tag); 3150 if (v != null) { 3151 return v; 3152 } 3153 3154 v = findViewTagInHeadersOrFooters(mFooterViewInfos, tag); 3155 if (v != null) { 3156 return v; 3157 } 3158 } 3159 return v; 3160 } 3161 3162 /* (non-Javadoc) 3163 * 3164 * Look in the passed in list of headers or footers for the view with the tag. 3165 */ 3166 View findViewTagInHeadersOrFooters(ArrayList<FixedViewInfo> where, Object tag) { 3167 if (where != null) { 3168 int len = where.size(); 3169 View v; 3170 3171 for (int i = 0; i < len; i++) { 3172 v = where.get(i).view; 3173 3174 if (!v.isRootNamespace()) { 3175 v = v.findViewWithTag(tag); 3176 3177 if (v != null) { 3178 return v; 3179 } 3180 } 3181 } 3182 } 3183 return null; 3184 } 3185 3186 @Override 3187 public boolean onTouchEvent(MotionEvent ev) { 3188 if (mItemsCanFocus && ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { 3189 // Don't handle edge touches immediately -- they may actually belong to one of our 3190 // descendants. 3191 return false; 3192 } 3193 return super.onTouchEvent(ev); 3194 } 3195 3196 /** 3197 * @see #setChoiceMode(int) 3198 * 3199 * @return The current choice mode 3200 */ 3201 public int getChoiceMode() { 3202 return mChoiceMode; 3203 } 3204 3205 /** 3206 * Defines the choice behavior for the List. By default, Lists do not have any choice behavior 3207 * ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the 3208 * List allows up to one item to be in a chosen state. By setting the choiceMode to 3209 * {@link #CHOICE_MODE_MULTIPLE}, the list allows any number of items to be chosen. 3210 * 3211 * @param choiceMode One of {@link #CHOICE_MODE_NONE}, {@link #CHOICE_MODE_SINGLE}, or 3212 * {@link #CHOICE_MODE_MULTIPLE} 3213 */ 3214 public void setChoiceMode(int choiceMode) { 3215 mChoiceMode = choiceMode; 3216 if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates == null) { 3217 mCheckStates = new SparseBooleanArray(); 3218 } 3219 } 3220 3221 @Override 3222 public boolean performItemClick(View view, int position, long id) { 3223 boolean handled = false; 3224 3225 if (mChoiceMode != CHOICE_MODE_NONE) { 3226 handled = true; 3227 3228 if (mChoiceMode == CHOICE_MODE_MULTIPLE) { 3229 boolean oldValue = mCheckStates.get(position, false); 3230 mCheckStates.put(position, !oldValue); 3231 } else { 3232 boolean oldValue = mCheckStates.get(position, false); 3233 if (!oldValue) { 3234 mCheckStates.clear(); 3235 mCheckStates.put(position, true); 3236 } 3237 } 3238 3239 mDataChanged = true; 3240 rememberSyncState(); 3241 requestLayout(); 3242 } 3243 3244 handled |= super.performItemClick(view, position, id); 3245 3246 return handled; 3247 } 3248 3249 /** 3250 * Sets the checked state of the specified position. The is only valid if 3251 * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or 3252 * {@link #CHOICE_MODE_MULTIPLE}. 3253 * 3254 * @param position The item whose checked state is to be checked 3255 * @param value The new checked sate for the item 3256 */ 3257 public void setItemChecked(int position, boolean value) { 3258 if (mChoiceMode == CHOICE_MODE_NONE) { 3259 return; 3260 } 3261 3262 if (mChoiceMode == CHOICE_MODE_MULTIPLE) { 3263 mCheckStates.put(position, value); 3264 } else { 3265 boolean oldValue = mCheckStates.get(position, false); 3266 mCheckStates.clear(); 3267 if (!oldValue) { 3268 mCheckStates.put(position, true); 3269 } 3270 } 3271 3272 // Do not generate a data change while we are in the layout phase 3273 if (!mInLayout && !mBlockLayoutRequests) { 3274 mDataChanged = true; 3275 rememberSyncState(); 3276 requestLayout(); 3277 } 3278 } 3279 3280 /** 3281 * Returns the checked state of the specified position. The result is only 3282 * valid if the choice mode has not been set to {@link #CHOICE_MODE_SINGLE} 3283 * or {@link #CHOICE_MODE_MULTIPLE}. 3284 * 3285 * @param position The item whose checked state to return 3286 * @return The item's checked state 3287 * 3288 * @see #setChoiceMode(int) 3289 */ 3290 public boolean isItemChecked(int position) { 3291 if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { 3292 return mCheckStates.get(position); 3293 } 3294 3295 return false; 3296 } 3297 3298 /** 3299 * Returns the currently checked item. The result is only valid if the choice 3300 * mode has not been set to {@link #CHOICE_MODE_SINGLE}. 3301 * 3302 * @return The position of the currently checked item or 3303 * {@link #INVALID_POSITION} if nothing is selected 3304 * 3305 * @see #setChoiceMode(int) 3306 */ 3307 public int getCheckedItemPosition() { 3308 if (mChoiceMode == CHOICE_MODE_SINGLE && mCheckStates != null && mCheckStates.size() == 1) { 3309 return mCheckStates.keyAt(0); 3310 } 3311 3312 return INVALID_POSITION; 3313 } 3314 3315 /** 3316 * Returns the set of checked items in the list. The result is only valid if 3317 * the choice mode has not been set to {@link #CHOICE_MODE_SINGLE}. 3318 * 3319 * @return A SparseBooleanArray which will return true for each call to 3320 * get(int position) where position is a position in the list. 3321 */ 3322 public SparseBooleanArray getCheckedItemPositions() { 3323 if (mChoiceMode != CHOICE_MODE_NONE) { 3324 return mCheckStates; 3325 } 3326 return null; 3327 } 3328 3329 /** 3330 * Returns the set of checked items ids. The result is only valid if 3331 * the choice mode has not been set to {@link #CHOICE_MODE_SINGLE}. 3332 * 3333 * @return A new array which contains the id of each checked item in the list. 3334 */ 3335 public long[] getCheckItemIds() { 3336 if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null && mAdapter != null) { 3337 final SparseBooleanArray states = mCheckStates; 3338 final int count = states.size(); 3339 final long[] ids = new long[count]; 3340 final ListAdapter adapter = mAdapter; 3341 3342 for (int i = 0; i < count; i++) { 3343 ids[i]= adapter.getItemId(states.keyAt(i)); 3344 } 3345 3346 return ids; 3347 } 3348 3349 return new long[0]; 3350 } 3351 3352 /** 3353 * Clear any choices previously set 3354 */ 3355 public void clearChoices() { 3356 if (mCheckStates != null) { 3357 mCheckStates.clear(); 3358 } 3359 } 3360 3361 static class SavedState extends BaseSavedState { 3362 SparseBooleanArray checkState; 3363 3364 /** 3365 * Constructor called from {@link ListView#onSaveInstanceState()} 3366 */ 3367 SavedState(Parcelable superState, SparseBooleanArray checkState) { 3368 super(superState); 3369 this.checkState = checkState; 3370 } 3371 3372 /** 3373 * Constructor called from {@link #CREATOR} 3374 */ 3375 private SavedState(Parcel in) { 3376 super(in); 3377 checkState = in.readSparseBooleanArray(); 3378 } 3379 3380 @Override 3381 public void writeToParcel(Parcel out, int flags) { 3382 super.writeToParcel(out, flags); 3383 out.writeSparseBooleanArray(checkState); 3384 } 3385 3386 @Override 3387 public String toString() { 3388 return "ListView.SavedState{" 3389 + Integer.toHexString(System.identityHashCode(this)) 3390 + " checkState=" + checkState + "}"; 3391 } 3392 3393 public static final Parcelable.Creator<SavedState> CREATOR 3394 = new Parcelable.Creator<SavedState>() { 3395 public SavedState createFromParcel(Parcel in) { 3396 return new SavedState(in); 3397 } 3398 3399 public SavedState[] newArray(int size) { 3400 return new SavedState[size]; 3401 } 3402 }; 3403 } 3404 3405 @Override 3406 public Parcelable onSaveInstanceState() { 3407 Parcelable superState = super.onSaveInstanceState(); 3408 return new SavedState(superState, mCheckStates); 3409 } 3410 3411 @Override 3412 public void onRestoreInstanceState(Parcelable state) { 3413 SavedState ss = (SavedState) state; 3414 3415 super.onRestoreInstanceState(ss.getSuperState()); 3416 3417 if (ss.checkState != null) { 3418 mCheckStates = ss.checkState; 3419 } 3420 3421 } 3422} 3423