/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.widget; import android.os.Trace; import com.android.internal.R; import com.android.internal.util.Predicate; import com.google.android.collect.Lists; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.MathUtils; import android.util.SparseBooleanArray; import android.view.FocusFinder; import android.view.KeyEvent; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewParent; import android.view.ViewRootImpl; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeProvider; import android.widget.RemoteViews.RemoteView; import java.util.ArrayList; /* * Implementation Notes: * * Some terminology: * * index - index of the items that are currently visible * position - index of the items in the cursor */ /** * A view that shows items in a vertically scrolling list. The items * come from the {@link ListAdapter} associated with this view. * *
See the List View * guide.
* * @attr ref android.R.styleable#ListView_entries * @attr ref android.R.styleable#ListView_divider * @attr ref android.R.styleable#ListView_dividerHeight * @attr ref android.R.styleable#ListView_headerDividersEnabled * @attr ref android.R.styleable#ListView_footerDividersEnabled */ @RemoteView public class ListView extends AbsListView { /** * Used to indicate a no preference for a position type. */ static final int NO_POSITION = -1; /** * When arrow scrolling, ListView will never scroll more than this factor * times the height of the list. */ private static final float MAX_SCROLL_FACTOR = 0.33f; /** * When arrow scrolling, need a certain amount of pixels to preview next * items. This is usually the fading edge, but if that is small enough, * we want to make sure we preview at least this many pixels. */ private static final int MIN_SCROLL_PREVIEW_PIXELS = 2; /** * A class that represents a fixed view in a list, for example a header at the top * or a footer at the bottom. */ public class FixedViewInfo { /** The view to add to the list */ public View view; /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */ public Object data; /**true
if the fixed view should be selectable in the list */
public boolean isSelectable;
}
private ArrayList* Note: When first introduced, this method could only be called before * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with * {@link android.os.Build.VERSION_CODES#KEY_LIME_PIE}, this method may be * called at any time. If the ListView's adapter does not extend * {@link HeaderViewListAdapter}, it will be wrapped with a supporting * instance of {@link WrapperListAdapter}. * * @param v The view to add. * @param data Data to associate with this view * @param isSelectable whether the item is selectable */ public void addHeaderView(View v, Object data, boolean isSelectable) { final FixedViewInfo info = new FixedViewInfo(); info.view = v; info.data = data; info.isSelectable = isSelectable; mHeaderViewInfos.add(info); // Wrap the adapter if it wasn't already wrapped. if (mAdapter != null) { if (!(mAdapter instanceof HeaderViewListAdapter)) { mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter); } // In the case of re-adding a header view, or adding one later on, // we need to notify the observer. if (mDataSetObserver != null) { mDataSetObserver.onChanged(); } } } /** * Add a fixed view to appear at the top of the list. If addHeaderView is * called more than once, the views will appear in the order they were * added. Views added using this call can take focus if they want. *
* Note: When first introduced, this method could only be called before
* setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with
* {@link android.os.Build.VERSION_CODES#KEY_LIME_PIE}, this method may be
* called at any time. If the ListView's adapter does not extend
* {@link HeaderViewListAdapter}, it will be wrapped with a supporting
* instance of {@link WrapperListAdapter}.
*
* @param v The view to add.
*/
public void addHeaderView(View v) {
addHeaderView(v, null, true);
}
@Override
public int getHeaderViewsCount() {
return mHeaderViewInfos.size();
}
/**
* Removes a previously-added header view.
*
* @param v The view to remove
* @return true if the view was removed, false if the view was not a header
* view
*/
public boolean removeHeaderView(View v) {
if (mHeaderViewInfos.size() > 0) {
boolean result = false;
if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeHeader(v)) {
if (mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
result = true;
}
removeFixedViewInfo(v, mHeaderViewInfos);
return result;
}
return false;
}
private void removeFixedViewInfo(View v, ArrayList
* Note: When first introduced, this method could only be called before
* setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with
* {@link android.os.Build.VERSION_CODES#KEY_LIME_PIE}, this method may be
* called at any time. If the ListView's adapter does not extend
* {@link HeaderViewListAdapter}, it will be wrapped with a supporting
* instance of {@link WrapperListAdapter}.
*
* @param v The view to add.
* @param data Data to associate with this view
* @param isSelectable true if the footer view can be selected
*/
public void addFooterView(View v, Object data, boolean isSelectable) {
final FixedViewInfo info = new FixedViewInfo();
info.view = v;
info.data = data;
info.isSelectable = isSelectable;
mFooterViewInfos.add(info);
// Wrap the adapter if it wasn't already wrapped.
if (mAdapter != null) {
if (!(mAdapter instanceof HeaderViewListAdapter)) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
}
// In the case of re-adding a footer view, or adding one later on,
// we need to notify the observer.
if (mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
}
/**
* Add a fixed view to appear at the bottom of the list. If addFooterView is
* called more than once, the views will appear in the order they were
* added. Views added using this call can take focus if they want.
*
* Note: When first introduced, this method could only be called before
* setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with
* {@link android.os.Build.VERSION_CODES#KEY_LIME_PIE}, this method may be
* called at any time. If the ListView's adapter does not extend
* {@link HeaderViewListAdapter}, it will be wrapped with a supporting
* instance of {@link WrapperListAdapter}.
*
* @param v The view to add.
*/
public void addFooterView(View v) {
addFooterView(v, null, true);
}
@Override
public int getFooterViewsCount() {
return mFooterViewInfos.size();
}
/**
* Removes a previously-added footer view.
*
* @param v The view to remove
* @return
* true if the view was removed, false if the view was not a footer view
*/
public boolean removeFooterView(View v) {
if (mFooterViewInfos.size() > 0) {
boolean result = false;
if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeFooter(v)) {
if (mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
result = true;
}
removeFixedViewInfo(v, mFooterViewInfos);
return result;
}
return false;
}
/**
* Returns the adapter currently in use in this ListView. The returned adapter
* might not be the same adapter passed to {@link #setAdapter(ListAdapter)} but
* might be a {@link WrapperListAdapter}.
*
* @return The adapter currently used to display data in this ListView.
*
* @see #setAdapter(ListAdapter)
*/
@Override
public ListAdapter getAdapter() {
return mAdapter;
}
/**
* Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService
* through the specified intent.
* @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
*/
@android.view.RemotableViewMethod
public void setRemoteViewsAdapter(Intent intent) {
super.setRemoteViewsAdapter(intent);
}
/**
* Sets the data behind this ListView.
*
* The adapter passed to this method may be wrapped by a {@link WrapperListAdapter},
* depending on the ListView features currently in use. For instance, adding
* headers and/or footers will cause the adapter to be wrapped.
*
* @param adapter The ListAdapter which is responsible for maintaining the
* data backing this list and for producing a view to represent an
* item in that data set.
*
* @see #getAdapter()
*/
@Override
public void setAdapter(ListAdapter adapter) {
if (mAdapter != null && mDataSetObserver != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
resetList();
mRecycler.clear();
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}
mOldSelectedPosition = INVALID_POSITION;
mOldSelectedRowId = INVALID_ROW_ID;
// AbsListView#setAdapter will update choice mode states.
super.setAdapter(adapter);
if (mAdapter != null) {
mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
mOldItemCount = mItemCount;
mItemCount = mAdapter.getCount();
checkFocus();
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
int position;
if (mStackFromBottom) {
position = lookForSelectablePosition(mItemCount - 1, false);
} else {
position = lookForSelectablePosition(0, true);
}
setSelectedPositionInt(position);
setNextSelectedPositionInt(position);
if (mItemCount == 0) {
// Nothing selected
checkSelectionChanged();
}
} else {
mAreAllItemsSelectable = true;
checkFocus();
// Nothing selected
checkSelectionChanged();
}
requestLayout();
}
/**
* The list is empty. Clear everything out.
*/
@Override
void resetList() {
// The parent's resetList() will remove all views from the layout so we need to
// cleanup the state of our footers and headers
clearRecycledState(mHeaderViewInfos);
clearRecycledState(mFooterViewInfos);
super.resetList();
mLayoutMode = LAYOUT_NORMAL;
}
private void clearRecycledState(ArrayListnull
if there was no previous selection.
* @param direction Either {@link android.view.View#FOCUS_UP} or
* {@link android.view.View#FOCUS_DOWN}.
* @param newSelectedPosition The position of the next selection.
* @param newFocusAssigned whether new focus was assigned. This matters because
* when something has focus, we don't want to show selection (ugh).
*/
private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition,
boolean newFocusAssigned) {
if (newSelectedPosition == INVALID_POSITION) {
throw new IllegalArgumentException("newSelectedPosition needs to be valid");
}
// whether or not we are moving down or up, we want to preserve the
// top of whatever view is on top:
// - moving down: the view that had selection
// - moving up: the view that is getting selection
View topView;
View bottomView;
int topViewIndex, bottomViewIndex;
boolean topSelected = false;
final int selectedIndex = mSelectedPosition - mFirstPosition;
final int nextSelectedIndex = newSelectedPosition - mFirstPosition;
if (direction == View.FOCUS_UP) {
topViewIndex = nextSelectedIndex;
bottomViewIndex = selectedIndex;
topView = getChildAt(topViewIndex);
bottomView = selectedView;
topSelected = true;
} else {
topViewIndex = selectedIndex;
bottomViewIndex = nextSelectedIndex;
topView = selectedView;
bottomView = getChildAt(bottomViewIndex);
}
final int numChildren = getChildCount();
// start with top view: is it changing size?
if (topView != null) {
topView.setSelected(!newFocusAssigned && topSelected);
measureAndAdjustDown(topView, topViewIndex, numChildren);
}
// is the bottom view changing size?
if (bottomView != null) {
bottomView.setSelected(!newFocusAssigned && !topSelected);
measureAndAdjustDown(bottomView, bottomViewIndex, numChildren);
}
}
/**
* Re-measure a child, and if its height changes, lay it out preserving its
* top, and adjust the children below it appropriately.
* @param child The child
* @param childIndex The view group index of the child.
* @param numChildren The number of children in the view group.
*/
private void measureAndAdjustDown(View child, int childIndex, int numChildren) {
int oldHeight = child.getHeight();
measureItem(child);
if (child.getMeasuredHeight() != oldHeight) {
// lay out the view, preserving its top
relayoutMeasuredItem(child);
// adjust views below appropriately
final int heightDelta = child.getMeasuredHeight() - oldHeight;
for (int i = childIndex + 1; i < numChildren; i++) {
getChildAt(i).offsetTopAndBottom(heightDelta);
}
}
}
/**
* Measure a particular list child.
* TODO: unify with setUpChild.
* @param child The child.
*/
private void measureItem(View child) {
ViewGroup.LayoutParams p = child.getLayoutParams();
if (p == null) {
p = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
}
/**
* Layout a child that has been measured, preserving its top position.
* TODO: unify with setUpChild.
* @param child The child.
*/
private void relayoutMeasuredItem(View child) {
final int w = child.getMeasuredWidth();
final int h = child.getMeasuredHeight();
final int childLeft = mListPadding.left;
final int childRight = childLeft + w;
final int childTop = child.getTop();
final int childBottom = childTop + h;
child.layout(childLeft, childTop, childRight, childBottom);
}
/**
* @return The amount to preview next items when arrow srolling.
*/
private int getArrowScrollPreviewLength() {
return Math.max(MIN_SCROLL_PREVIEW_PIXELS, getVerticalFadingEdgeLength());
}
/**
* Determine how much we need to scroll in order to get the next selected view
* visible, with a fading edge showing below as applicable. The amount is
* capped at {@link #getMaxScrollAmount()} .
*
* @param direction either {@link android.view.View#FOCUS_UP} or
* {@link android.view.View#FOCUS_DOWN}.
* @param nextSelectedPosition The position of the next selection, or
* {@link #INVALID_POSITION} if there is no next selectable position
* @return The amount to scroll. Note: this is always positive! Direction
* needs to be taken into account when actually scrolling.
*/
private int amountToScroll(int direction, int nextSelectedPosition) {
final int listBottom = getHeight() - mListPadding.bottom;
final int listTop = mListPadding.top;
int numChildren = getChildCount();
if (direction == View.FOCUS_DOWN) {
int indexToMakeVisible = numChildren - 1;
if (nextSelectedPosition != INVALID_POSITION) {
indexToMakeVisible = nextSelectedPosition - mFirstPosition;
}
while (numChildren <= indexToMakeVisible) {
// Child to view is not attached yet.
addViewBelow(getChildAt(numChildren - 1), mFirstPosition + numChildren - 1);
numChildren++;
}
final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
final View viewToMakeVisible = getChildAt(indexToMakeVisible);
int goalBottom = listBottom;
if (positionToMakeVisible < mItemCount - 1) {
goalBottom -= getArrowScrollPreviewLength();
}
if (viewToMakeVisible.getBottom() <= goalBottom) {
// item is fully visible.
return 0;
}
if (nextSelectedPosition != INVALID_POSITION
&& (goalBottom - viewToMakeVisible.getTop()) >= getMaxScrollAmount()) {
// item already has enough of it visible, changing selection is good enough
return 0;
}
int amountToScroll = (viewToMakeVisible.getBottom() - goalBottom);
if ((mFirstPosition + numChildren) == mItemCount) {
// last is last in list -> make sure we don't scroll past it
final int max = getChildAt(numChildren - 1).getBottom() - listBottom;
amountToScroll = Math.min(amountToScroll, max);
}
return Math.min(amountToScroll, getMaxScrollAmount());
} else {
int indexToMakeVisible = 0;
if (nextSelectedPosition != INVALID_POSITION) {
indexToMakeVisible = nextSelectedPosition - mFirstPosition;
}
while (indexToMakeVisible < 0) {
// Child to view is not attached yet.
addViewAbove(getChildAt(0), mFirstPosition);
mFirstPosition--;
indexToMakeVisible = nextSelectedPosition - mFirstPosition;
}
final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
final View viewToMakeVisible = getChildAt(indexToMakeVisible);
int goalTop = listTop;
if (positionToMakeVisible > 0) {
goalTop += getArrowScrollPreviewLength();
}
if (viewToMakeVisible.getTop() >= goalTop) {
// item is fully visible.
return 0;
}
if (nextSelectedPosition != INVALID_POSITION &&
(viewToMakeVisible.getBottom() - goalTop) >= getMaxScrollAmount()) {
// item already has enough of it visible, changing selection is good enough
return 0;
}
int amountToScroll = (goalTop - viewToMakeVisible.getTop());
if (mFirstPosition == 0) {
// first is first in list -> make sure we don't scroll past it
final int max = listTop - getChildAt(0).getTop();
amountToScroll = Math.min(amountToScroll, max);
}
return Math.min(amountToScroll, getMaxScrollAmount());
}
}
/**
* Holds results of focus aware arrow scrolling.
*/
static private class ArrowScrollFocusResult {
private int mSelectedPosition;
private int mAmountToScroll;
/**
* How {@link android.widget.ListView#arrowScrollFocused} returns its values.
*/
void populate(int selectedPosition, int amountToScroll) {
mSelectedPosition = selectedPosition;
mAmountToScroll = amountToScroll;
}
public int getSelectedPosition() {
return mSelectedPosition;
}
public int getAmountToScroll() {
return mAmountToScroll;
}
}
/**
* @param direction either {@link android.view.View#FOCUS_UP} or
* {@link android.view.View#FOCUS_DOWN}.
* @return The position of the next selectable position of the views that
* are currently visible, taking into account the fact that there might
* be no selection. Returns {@link #INVALID_POSITION} if there is no
* selectable view on screen in the given direction.
*/
private int lookForSelectablePositionOnScreen(int direction) {
final int firstPosition = mFirstPosition;
if (direction == View.FOCUS_DOWN) {
int startPos = (mSelectedPosition != INVALID_POSITION) ?
mSelectedPosition + 1 :
firstPosition;
if (startPos >= mAdapter.getCount()) {
return INVALID_POSITION;
}
if (startPos < firstPosition) {
startPos = firstPosition;
}
final int lastVisiblePos = getLastVisiblePosition();
final ListAdapter adapter = getAdapter();
for (int pos = startPos; pos <= lastVisiblePos; pos++) {
if (adapter.isEnabled(pos)
&& getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
return pos;
}
}
} else {
int last = firstPosition + getChildCount() - 1;
int startPos = (mSelectedPosition != INVALID_POSITION) ?
mSelectedPosition - 1 :
firstPosition + getChildCount() - 1;
if (startPos < 0 || startPos >= mAdapter.getCount()) {
return INVALID_POSITION;
}
if (startPos > last) {
startPos = last;
}
final ListAdapter adapter = getAdapter();
for (int pos = startPos; pos >= firstPosition; pos--) {
if (adapter.isEnabled(pos)
&& getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
return pos;
}
}
}
return INVALID_POSITION;
}
/**
* Do an arrow scroll based on focus searching. If a new view is
* given focus, return the selection delta and amount to scroll via
* an {@link ArrowScrollFocusResult}, otherwise, return null.
*
* @param direction either {@link android.view.View#FOCUS_UP} or
* {@link android.view.View#FOCUS_DOWN}.
* @return The result if focus has changed, or null
.
*/
private ArrowScrollFocusResult arrowScrollFocused(final int direction) {
final View selectedView = getSelectedView();
View newFocus;
if (selectedView != null && selectedView.hasFocus()) {
View oldFocus = selectedView.findFocus();
newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction);
} else {
if (direction == View.FOCUS_DOWN) {
final boolean topFadingEdgeShowing = (mFirstPosition > 0);
final int listTop = mListPadding.top +
(topFadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
final int ySearchPoint =
(selectedView != null && selectedView.getTop() > listTop) ?
selectedView.getTop() :
listTop;
mTempRect.set(0, ySearchPoint, 0, ySearchPoint);
} else {
final boolean bottomFadingEdgeShowing =
(mFirstPosition + getChildCount() - 1) < mItemCount;
final int listBottom = getHeight() - mListPadding.bottom -
(bottomFadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
final int ySearchPoint =
(selectedView != null && selectedView.getBottom() < listBottom) ?
selectedView.getBottom() :
listBottom;
mTempRect.set(0, ySearchPoint, 0, ySearchPoint);
}
newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction);
}
if (newFocus != null) {
final int positionOfNewFocus = positionOfNewFocus(newFocus);
// if the focus change is in a different new position, make sure
// we aren't jumping over another selectable position
if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) {
final int selectablePosition = lookForSelectablePositionOnScreen(direction);
if (selectablePosition != INVALID_POSITION &&
((direction == View.FOCUS_DOWN && selectablePosition < positionOfNewFocus) ||
(direction == View.FOCUS_UP && selectablePosition > positionOfNewFocus))) {
return null;
}
}
int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus);
final int maxScrollAmount = getMaxScrollAmount();
if (focusScroll < maxScrollAmount) {
// not moving too far, safe to give next view focus
newFocus.requestFocus(direction);
mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll);
return mArrowScrollFocusResult;
} else if (distanceToView(newFocus) < maxScrollAmount){
// Case to consider:
// too far to get entire next focusable on screen, but by going
// max scroll amount, we are getting it at least partially in view,
// so give it focus and scroll the max ammount.
newFocus.requestFocus(direction);
mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount);
return mArrowScrollFocusResult;
}
}
return null;
}
/**
* @param newFocus The view that would have focus.
* @return the position that contains newFocus
*/
private int positionOfNewFocus(View newFocus) {
final int numChildren = getChildCount();
for (int i = 0; i < numChildren; i++) {
final View child = getChildAt(i);
if (isViewAncestorOf(newFocus, child)) {
return mFirstPosition + i;
}
}
throw new IllegalArgumentException("newFocus is not a child of any of the"
+ " children of the list!");
}
/**
* Return true if child is an ancestor of parent, (or equal to the parent).
*/
private boolean isViewAncestorOf(View child, View parent) {
if (child == parent) {
return true;
}
final ViewParent theParent = child.getParent();
return (theParent instanceof ViewGroup) && isViewAncestorOf((View) theParent, parent);
}
/**
* Determine how much we need to scroll in order to get newFocus in view.
* @param direction either {@link android.view.View#FOCUS_UP} or
* {@link android.view.View#FOCUS_DOWN}.
* @param newFocus The view that would take focus.
* @param positionOfNewFocus The position of the list item containing newFocus
* @return The amount to scroll. Note: this is always positive! Direction
* needs to be taken into account when actually scrolling.
*/
private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) {
int amountToScroll = 0;
newFocus.getDrawingRect(mTempRect);
offsetDescendantRectToMyCoords(newFocus, mTempRect);
if (direction == View.FOCUS_UP) {
if (mTempRect.top < mListPadding.top) {
amountToScroll = mListPadding.top - mTempRect.top;
if (positionOfNewFocus > 0) {
amountToScroll += getArrowScrollPreviewLength();
}
}
} else {
final int listBottom = getHeight() - mListPadding.bottom;
if (mTempRect.bottom > listBottom) {
amountToScroll = mTempRect.bottom - listBottom;
if (positionOfNewFocus < mItemCount - 1) {
amountToScroll += getArrowScrollPreviewLength();
}
}
}
return amountToScroll;
}
/**
* Determine the distance to the nearest edge of a view in a particular
* direction.
*
* @param descendant A descendant of this list.
* @return The distance, or 0 if the nearest edge is already on screen.
*/
private int distanceToView(View descendant) {
int distance = 0;
descendant.getDrawingRect(mTempRect);
offsetDescendantRectToMyCoords(descendant, mTempRect);
final int listBottom = mBottom - mTop - mListPadding.bottom;
if (mTempRect.bottom < mListPadding.top) {
distance = mListPadding.top - mTempRect.bottom;
} else if (mTempRect.top > listBottom) {
distance = mTempRect.top - listBottom;
}
return distance;
}
/**
* Scroll the children by amount, adding a view at the end and removing
* views that fall off as necessary.
*
* @param amount The amount (positive or negative) to scroll.
*/
private void scrollListItemsBy(int amount) {
offsetChildrenTopAndBottom(amount);
final int listBottom = getHeight() - mListPadding.bottom;
final int listTop = mListPadding.top;
final AbsListView.RecycleBin recycleBin = mRecycler;
if (amount < 0) {
// shifted items up
// may need to pan views into the bottom space
int numChildren = getChildCount();
View last = getChildAt(numChildren - 1);
while (last.getBottom() < listBottom) {
final int lastVisiblePosition = mFirstPosition + numChildren - 1;
if (lastVisiblePosition < mItemCount - 1) {
last = addViewBelow(last, lastVisiblePosition);
numChildren++;
} else {
break;
}
}
// may have brought in the last child of the list that is skinnier
// than the fading edge, thereby leaving space at the end. need
// to shift back
if (last.getBottom() < listBottom) {
offsetChildrenTopAndBottom(listBottom - last.getBottom());
}
// top views may be panned off screen
View first = getChildAt(0);
while (first.getBottom() < listTop) {
AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams();
if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
recycleBin.addScrapView(first, mFirstPosition);
}
detachViewFromParent(first);
first = getChildAt(0);
mFirstPosition++;
}
} else {
// shifted items down
View first = getChildAt(0);
// may need to pan views into top
while ((first.getTop() > listTop) && (mFirstPosition > 0)) {
first = addViewAbove(first, mFirstPosition);
mFirstPosition--;
}
// may have brought the very first child of the list in too far and
// need to shift it back
if (first.getTop() > listTop) {
offsetChildrenTopAndBottom(listTop - first.getTop());
}
int lastIndex = getChildCount() - 1;
View last = getChildAt(lastIndex);
// bottom view may be panned off screen
while (last.getTop() > listBottom) {
AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams();
if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
recycleBin.addScrapView(last, mFirstPosition+lastIndex);
}
detachViewFromParent(last);
last = getChildAt(--lastIndex);
}
}
}
private View addViewAbove(View theView, int position) {
int abovePosition = position - 1;
View view = obtainView(abovePosition, mIsScrap);
int edgeOfNewChild = theView.getTop() - mDividerHeight;
setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left,
false, mIsScrap[0]);
return view;
}
private View addViewBelow(View theView, int position) {
int belowPosition = position + 1;
View view = obtainView(belowPosition, mIsScrap);
int edgeOfNewChild = theView.getBottom() + mDividerHeight;
setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left,
false, mIsScrap[0]);
return view;
}
/**
* Indicates that the views created by the ListAdapter can contain focusable
* items.
*
* @param itemsCanFocus true if items can get focus, false otherwise
*/
public void setItemsCanFocus(boolean itemsCanFocus) {
mItemsCanFocus = itemsCanFocus;
if (!itemsCanFocus) {
setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
}
}
/**
* @return Whether the views created by the ListAdapter can contain focusable
* items.
*/
public boolean getItemsCanFocus() {
return mItemsCanFocus;
}
@Override
public boolean isOpaque() {
boolean retValue = (mCachingActive && mIsCacheColorOpaque && mDividerIsOpaque &&
hasOpaqueScrollbars()) || super.isOpaque();
if (retValue) {
// only return true if the list items cover the entire area of the view
final int listTop = mListPadding != null ? mListPadding.top : mPaddingTop;
View first = getChildAt(0);
if (first == null || first.getTop() > listTop) {
return false;
}
final int listBottom = getHeight() -
(mListPadding != null ? mListPadding.bottom : mPaddingBottom);
View last = getChildAt(getChildCount() - 1);
if (last == null || last.getBottom() < listBottom) {
return false;
}
}
return retValue;
}
@Override
public void setCacheColorHint(int color) {
final boolean opaque = (color >>> 24) == 0xFF;
mIsCacheColorOpaque = opaque;
if (opaque) {
if (mDividerPaint == null) {
mDividerPaint = new Paint();
}
mDividerPaint.setColor(color);
}
super.setCacheColorHint(color);
}
void drawOverscrollHeader(Canvas canvas, Drawable drawable, Rect bounds) {
final int height = drawable.getMinimumHeight();
canvas.save();
canvas.clipRect(bounds);
final int span = bounds.bottom - bounds.top;
if (span < height) {
bounds.top = bounds.bottom - height;
}
drawable.setBounds(bounds);
drawable.draw(canvas);
canvas.restore();
}
void drawOverscrollFooter(Canvas canvas, Drawable drawable, Rect bounds) {
final int height = drawable.getMinimumHeight();
canvas.save();
canvas.clipRect(bounds);
final int span = bounds.bottom - bounds.top;
if (span < height) {
bounds.bottom = bounds.top + height;
}
drawable.setBounds(bounds);
drawable.draw(canvas);
canvas.restore();
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (mCachingStarted) {
mCachingActive = true;
}
// Draw the dividers
final int dividerHeight = mDividerHeight;
final Drawable overscrollHeader = mOverScrollHeader;
final Drawable overscrollFooter = mOverScrollFooter;
final boolean drawOverscrollHeader = overscrollHeader != null;
final boolean drawOverscrollFooter = overscrollFooter != null;
final boolean drawDividers = dividerHeight > 0 && mDivider != null;
if (drawDividers || drawOverscrollHeader || drawOverscrollFooter) {
// Only modify the top and bottom in the loop, we set the left and right here
final Rect bounds = mTempRect;
bounds.left = mPaddingLeft;
bounds.right = mRight - mLeft - mPaddingRight;
final int count = getChildCount();
final int headerCount = mHeaderViewInfos.size();
final int itemCount = mItemCount;
final int footerLimit = (itemCount - mFooterViewInfos.size());
final boolean headerDividers = mHeaderDividersEnabled;
final boolean footerDividers = mFooterDividersEnabled;
final int first = mFirstPosition;
final boolean areAllItemsSelectable = mAreAllItemsSelectable;
final ListAdapter adapter = mAdapter;
// If the list is opaque *and* the background is not, we want to
// fill a rect where the dividers would be for non-selectable items
// If the list is opaque and the background is also opaque, we don't
// need to draw anything since the background will do it for us
final boolean fillForMissingDividers = isOpaque() && !super.isOpaque();
if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) {
mDividerPaint = new Paint();
mDividerPaint.setColor(getCacheColorHint());
}
final Paint paint = mDividerPaint;
int effectivePaddingTop = 0;
int effectivePaddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
effectivePaddingTop = mListPadding.top;
effectivePaddingBottom = mListPadding.bottom;
}
final int listBottom = mBottom - mTop - effectivePaddingBottom + mScrollY;
if (!mStackFromBottom) {
int bottom = 0;
// Draw top divider or header for overscroll
final int scrollY = mScrollY;
if (count > 0 && scrollY < 0) {
if (drawOverscrollHeader) {
bounds.bottom = 0;
bounds.top = scrollY;
drawOverscrollHeader(canvas, overscrollHeader, bounds);
} else if (drawDividers) {
bounds.bottom = 0;
bounds.top = -dividerHeight;
drawDivider(canvas, bounds, -1);
}
}
for (int i = 0; i < count; i++) {
final int itemIndex = (first + i);
final boolean isHeader = (itemIndex < headerCount);
final boolean isFooter = (itemIndex >= footerLimit);
if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) {
final View child = getChildAt(i);
bottom = child.getBottom();
final boolean isLastItem = (i == (count - 1));
if (drawDividers && (bottom < listBottom)
&& !(drawOverscrollFooter && isLastItem)) {
final int nextIndex = (itemIndex + 1);
// Draw dividers between enabled items, headers and/or
// footers when enabled, and the end of the list.
if (areAllItemsSelectable || ((adapter.isEnabled(itemIndex)
|| (headerDividers && isHeader)
|| (footerDividers && isFooter)) && (isLastItem
|| adapter.isEnabled(nextIndex)
|| (headerDividers && (nextIndex < headerCount))
|| (footerDividers && (nextIndex >= footerLimit))))) {
bounds.top = bottom;
bounds.bottom = bottom + dividerHeight;
drawDivider(canvas, bounds, i);
} else if (fillForMissingDividers) {
bounds.top = bottom;
bounds.bottom = bottom + dividerHeight;
canvas.drawRect(bounds, paint);
}
}
}
}
final int overFooterBottom = mBottom + mScrollY;
if (drawOverscrollFooter && first + count == itemCount &&
overFooterBottom > bottom) {
bounds.top = bottom;
bounds.bottom = overFooterBottom;
drawOverscrollFooter(canvas, overscrollFooter, bounds);
}
} else {
int top;
final int scrollY = mScrollY;
if (count > 0 && drawOverscrollHeader) {
bounds.top = scrollY;
bounds.bottom = getChildAt(0).getTop();
drawOverscrollHeader(canvas, overscrollHeader, bounds);
}
final int start = drawOverscrollHeader ? 1 : 0;
for (int i = start; i < count; i++) {
final int itemIndex = (first + i);
final boolean isHeader = (itemIndex < headerCount);
final boolean isFooter = (itemIndex >= footerLimit);
if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) {
final View child = getChildAt(i);
top = child.getTop();
if (drawDividers && (top > effectivePaddingTop)) {
final boolean isFirstItem = (i == start);
final int previousIndex = (itemIndex - 1);
// Draw dividers between enabled items, headers and/or
// footers when enabled, and the end of the list.
if (areAllItemsSelectable || ((adapter.isEnabled(itemIndex)
|| (headerDividers && isHeader)
|| (footerDividers && isFooter)) && (isFirstItem
|| adapter.isEnabled(previousIndex)
|| (headerDividers && (previousIndex < headerCount))
|| (footerDividers && (previousIndex >= footerLimit))))) {
bounds.top = top - dividerHeight;
bounds.bottom = top;
// Give the method the child ABOVE the divider,
// so we subtract one from our child position.
// Give -1 when there is no child above the
// divider.
drawDivider(canvas, bounds, i - 1);
} else if (fillForMissingDividers) {
bounds.top = top - dividerHeight;
bounds.bottom = top;
canvas.drawRect(bounds, paint);
}
}
}
}
if (count > 0 && scrollY > 0) {
if (drawOverscrollFooter) {
final int absListBottom = mBottom;
bounds.top = absListBottom;
bounds.bottom = absListBottom + scrollY;
drawOverscrollFooter(canvas, overscrollFooter, bounds);
} else if (drawDividers) {
bounds.top = listBottom;
bounds.bottom = listBottom + dividerHeight;
drawDivider(canvas, bounds, -1);
}
}
}
}
// Draw the indicators (these should be drawn above the dividers) and children
super.dispatchDraw(canvas);
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
boolean more = super.drawChild(canvas, child, drawingTime);
if (mCachingActive && child.mCachingFailed) {
mCachingActive = false;
}
return more;
}
/**
* Draws a divider for the given child in the given bounds.
*
* @param canvas The canvas to draw to.
* @param bounds The bounds of the divider.
* @param childIndex The index of child (of the View) above the divider.
* This will be -1 if there is no child above the divider to be
* drawn.
*/
void drawDivider(Canvas canvas, Rect bounds, int childIndex) {
// This widget draws the same divider for all children
final Drawable divider = mDivider;
divider.setBounds(bounds);
divider.draw(canvas);
}
/**
* Returns the drawable that will be drawn between each item in the list.
*
* @return the current drawable drawn between list elements
*/
public Drawable getDivider() {
return mDivider;
}
/**
* Sets the drawable that will be drawn between each item in the list. If the drawable does
* not have an intrinsic height, you should also call {@link #setDividerHeight(int)}
*
* @param divider The drawable to use.
*/
public void setDivider(Drawable divider) {
if (divider != null) {
mDividerHeight = divider.getIntrinsicHeight();
} else {
mDividerHeight = 0;
}
mDivider = divider;
mDividerIsOpaque = divider == null || divider.getOpacity() == PixelFormat.OPAQUE;
requestLayout();
invalidate();
}
/**
* @return Returns the height of the divider that will be drawn between each item in the list.
*/
public int getDividerHeight() {
return mDividerHeight;
}
/**
* Sets the height of the divider that will be drawn between each item in the list. Calling
* this will override the intrinsic height as set by {@link #setDivider(Drawable)}
*
* @param height The new height of the divider in pixels.
*/
public void setDividerHeight(int height) {
mDividerHeight = height;
requestLayout();
invalidate();
}
/**
* Enables or disables the drawing of the divider for header views.
*
* @param headerDividersEnabled True to draw the headers, false otherwise.
*
* @see #setFooterDividersEnabled(boolean)
* @see #areHeaderDividersEnabled()
* @see #addHeaderView(android.view.View)
*/
public void setHeaderDividersEnabled(boolean headerDividersEnabled) {
mHeaderDividersEnabled = headerDividersEnabled;
invalidate();
}
/**
* @return Whether the drawing of the divider for header views is enabled
*
* @see #setHeaderDividersEnabled(boolean)
*/
public boolean areHeaderDividersEnabled() {
return mHeaderDividersEnabled;
}
/**
* Enables or disables the drawing of the divider for footer views.
*
* @param footerDividersEnabled True to draw the footers, false otherwise.
*
* @see #setHeaderDividersEnabled(boolean)
* @see #areFooterDividersEnabled()
* @see #addFooterView(android.view.View)
*/
public void setFooterDividersEnabled(boolean footerDividersEnabled) {
mFooterDividersEnabled = footerDividersEnabled;
invalidate();
}
/**
* @return Whether the drawing of the divider for footer views is enabled
*
* @see #setFooterDividersEnabled(boolean)
*/
public boolean areFooterDividersEnabled() {
return mFooterDividersEnabled;
}
/**
* Sets the drawable that will be drawn above all other list content.
* This area can become visible when the user overscrolls the list.
*
* @param header The drawable to use
*/
public void setOverscrollHeader(Drawable header) {
mOverScrollHeader = header;
if (mScrollY < 0) {
invalidate();
}
}
/**
* @return The drawable that will be drawn above all other list content
*/
public Drawable getOverscrollHeader() {
return mOverScrollHeader;
}
/**
* Sets the drawable that will be drawn below all other list content.
* This area can become visible when the user overscrolls the list,
* or when the list's content does not fully fill the container area.
*
* @param footer The drawable to use
*/
public void setOverscrollFooter(Drawable footer) {
mOverScrollFooter = footer;
invalidate();
}
/**
* @return The drawable that will be drawn below all other list content
*/
public Drawable getOverscrollFooter() {
return mOverScrollFooter;
}
@Override
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
final ListAdapter adapter = mAdapter;
int closetChildIndex = -1;
int closestChildTop = 0;
if (adapter != null && gainFocus && previouslyFocusedRect != null) {
previouslyFocusedRect.offset(mScrollX, mScrollY);
// Don't cache the result of getChildCount or mFirstPosition here,
// it could change in layoutChildren.
if (adapter.getCount() < getChildCount() + mFirstPosition) {
mLayoutMode = LAYOUT_NORMAL;
layoutChildren();
}
// figure out which item should be selected based on previously
// focused rect
Rect otherRect = mTempRect;
int minDistance = Integer.MAX_VALUE;
final int childCount = getChildCount();
final int firstPosition = mFirstPosition;
for (int i = 0; i < childCount; i++) {
// only consider selectable views
if (!adapter.isEnabled(firstPosition + i)) {
continue;
}
View other = getChildAt(i);
other.getDrawingRect(otherRect);
offsetDescendantRectToMyCoords(other, otherRect);
int distance = getDistance(previouslyFocusedRect, otherRect, direction);
if (distance < minDistance) {
minDistance = distance;
closetChildIndex = i;
closestChildTop = other.getTop();
}
}
}
if (closetChildIndex >= 0) {
setSelectionFromTop(closetChildIndex + mFirstPosition, closestChildTop);
} else {
requestLayout();
}
}
/*
* (non-Javadoc)
*
* Children specified in XML are assumed to be header views. After we have
* parsed them move them out of the children list and into mHeaderViews.
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
int count = getChildCount();
if (count > 0) {
for (int i = 0; i < count; ++i) {
addHeaderView(getChildAt(i));
}
removeAllViews();
}
}
/* (non-Javadoc)
* @see android.view.View#findViewById(int)
* First look in our children, then in any header and footer views that may be scrolled off.
*/
@Override
protected View findViewTraversal(int id) {
View v;
v = super.findViewTraversal(id);
if (v == null) {
v = findViewInHeadersOrFooters(mHeaderViewInfos, id);
if (v != null) {
return v;
}
v = findViewInHeadersOrFooters(mFooterViewInfos, id);
if (v != null) {
return v;
}
}
return v;
}
/* (non-Javadoc)
*
* Look in the passed in list of headers or footers for the view.
*/
View findViewInHeadersOrFooters(ArrayList