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