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