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