/* * Author: alessandro crugnola * alessandro.crugnola@gmail.com * * Fork based on the google opensource framework * * 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 com.basic.security.widget.hlistview; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; 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.support.v4.util.SparseArrayCompat; import android.util.AttributeSet; import android.util.Log; 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.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ArrayAdapter; import android.widget.Checkable; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.RemoteViews.RemoteView; import android.widget.WrapperListAdapter; import com.basic.security.utils.RUtils; 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 HListView extends AbsHListView { /** * 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; private static final String LOG_TAG = "HListView"; // used for temporary calculations. private final Rect mTempRect = new Rect(); // the single allocated result per list view; kinda cheesey but avoids // allocating these thingies too often. private final ArrowScrollFocusResult mArrowScrollFocusResult = new ArrowScrollFocusResult(); Drawable mDivider; int mDividerWidth; int mMeasureWithChild; Drawable mOverScrollHeader; Drawable mOverScrollFooter; private ArrayList* NOTE: Call this before calling setAdapter. This is so ListView can wrap the supplied cursor with one that will also account * for header and footer views. * * @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) { if (mAdapter != null && !(mAdapter instanceof HeaderViewListAdapter)) { throw new IllegalStateException( "Cannot add header view to list -- setAdapter has already been called."); } FixedViewInfo info = new FixedViewInfo(); info.view = v; info.data = data; info.isSelectable = isSelectable; mHeaderViewInfos.add(info); // in the case of re-adding a header view, or adding one later on, // we need to notify the observer if (mAdapter != null && 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: Call this before calling setAdapter. This is so ListView can wrap the supplied cursor with one that will also account
* for header and footer views.
*
* @param v The view to add.
*/
public void addHeaderView(View v) {
addHeaderView(v, null, true);
}
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: Call this before calling setAdapter. This is so ListView can wrap the supplied cursor with one that will also account
* for header and footer views.
*
* @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) {
// NOTE: do not enforce the adapter being null here, since unlike in
// addHeaderView, it was never enforced here, and so existing apps are
// relying on being able to add a footer and then calling setAdapter to
// force creation of the HeaderViewListAdapter wrapper
FixedViewInfo info = new FixedViewInfo();
info.view = v;
info.data = data;
info.isSelectable = isSelectable;
mFooterViewInfos.add(info);
// in the case of re-adding a footer view, or adding one later on,
// we need to notify the observer
if (mAdapter != null && 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: Call this before calling setAdapter. This is so ListView can wrap the supplied cursor with one that will also account
* for header and footer views.
*
* @param v The view to add.
*/
public void addFooterView(View v) {
addFooterView(v, null, true);
}
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)
*/
public ListAdapter getAdapter() {
return mAdapter;
}
/**
* 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()
*/
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;
mOldSelectedColId = INVALID_COL_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 (mStackFromRight) {
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.
*/
protected 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 leftView;
View rightView;
int leftViewIndex, rightViewIndex;
boolean leftSelected = false;
final int selectedIndex = mSelectedPosition - mFirstPosition;
final int nextSelectedIndex = newSelectedPosition - mFirstPosition;
if (direction == View.FOCUS_UP) {
leftViewIndex = nextSelectedIndex;
rightViewIndex = selectedIndex;
leftView = getChildAt(leftViewIndex);
rightView = selectedView;
leftSelected = true;
} else {
leftViewIndex = selectedIndex;
rightViewIndex = nextSelectedIndex;
leftView = selectedView;
rightView = getChildAt(rightViewIndex);
}
final int numChildren = getChildCount();
// start with top view: is it changing size?
if (leftView != null) {
leftView.setSelected(!newFocusAssigned && leftSelected);
measureAndAdjustRight(leftView, leftViewIndex, numChildren);
}
// is the bottom view changing size?
if (rightView != null) {
rightView.setSelected(!newFocusAssigned && !leftSelected);
measureAndAdjustRight(rightView, rightViewIndex, 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 measureAndAdjustRight(View child, int childIndex, int numChildren) {
int oldWidth = child.getWidth();
measureItem(child);
if (child.getMeasuredWidth() != oldWidth) {
// lay out the view, preserving its top
relayoutMeasuredItem(child);
// adjust views below appropriately
final int widthDelta = child.getMeasuredWidth() - oldWidth;
for (int i = childIndex + 1; i < numChildren; i++) {
getChildAt(i).offsetLeftAndRight(widthDelta);
}
}
}
/**
* 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.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT);
}
int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec, mListPadding.top + mListPadding.bottom, p.height);
int lpWidth = p.width;
int childWidthSpec;
if (lpWidth > 0) {
childWidthSpec = MeasureSpec.makeMeasureSpec(lpWidth, MeasureSpec.EXACTLY);
} else {
childWidthSpec = 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 childTop = mListPadding.top;
final int childBottom = childTop + h;
final int childLeft = child.getLeft();
final int childRight = childLeft + w;
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, getHorizontalFadingEdgeLength());
}
/**
* 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 listRight = getWidth() - mListPadding.right;
final int listLeft = mListPadding.left;
final int numChildren = getChildCount();
if (direction == View.FOCUS_DOWN) {
int indexToMakeVisible = numChildren - 1;
if (nextSelectedPosition != INVALID_POSITION) {
indexToMakeVisible = nextSelectedPosition - mFirstPosition;
}
final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
final View viewToMakeVisible = getChildAt(indexToMakeVisible);
int goalRight = listRight;
if (positionToMakeVisible < mItemCount - 1) {
goalRight -= getArrowScrollPreviewLength();
}
if (viewToMakeVisible.getRight() <= goalRight) {
// item is fully visible.
return 0;
}
if (nextSelectedPosition != INVALID_POSITION
&& (goalRight - viewToMakeVisible.getLeft()) >= getMaxScrollAmount()) {
// item already has enough of it visible, changing selection is good enough
return 0;
}
int amountToScroll = (viewToMakeVisible.getRight() - goalRight);
if ((mFirstPosition + numChildren) == mItemCount) {
// last is last in list -> make sure we don't scroll past it
final int max = getChildAt(numChildren - 1).getRight() - listRight;
amountToScroll = Math.min(amountToScroll, max);
}
return Math.min(amountToScroll, getMaxScrollAmount());
} else {
int indexToMakeVisible = 0;
if (nextSelectedPosition != INVALID_POSITION) {
indexToMakeVisible = nextSelectedPosition - mFirstPosition;
}
final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
final View viewToMakeVisible = getChildAt(indexToMakeVisible);
int goalLeft = listLeft;
if (positionToMakeVisible > 0) {
goalLeft += getArrowScrollPreviewLength();
}
if (viewToMakeVisible.getLeft() >= goalLeft) {
// item is fully visible.
return 0;
}
if (nextSelectedPosition != INVALID_POSITION &&
(viewToMakeVisible.getRight() - goalLeft) >= getMaxScrollAmount()) {
// item already has enough of it visible, changing selection is good enough
return 0;
}
int amountToScroll = (goalLeft - viewToMakeVisible.getLeft());
if (mFirstPosition == 0) {
// first is first in list -> make sure we don't scroll past it
final int max = listLeft - getChildAt(0).getLeft();
amountToScroll = Math.min(amountToScroll, max);
}
return Math.min(amountToScroll, getMaxScrollAmount());
}
}
/**
* @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 leftFadingEdgeShowing = (mFirstPosition > 0);
final int listLeft = mListPadding.left +
(leftFadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
final int xSearchPoint =
(selectedView != null && selectedView.getLeft() > listLeft) ?
selectedView.getLeft() :
listLeft;
mTempRect.set(xSearchPoint, 0, xSearchPoint, 0);
} else {
final boolean rightFadingEdgeShowing =
(mFirstPosition + getChildCount() - 1) < mItemCount;
final int listRight = getWidth() - mListPadding.right -
(rightFadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
final int xSearchPoint =
(selectedView != null && selectedView.getRight() < listRight) ?
selectedView.getRight() :
listRight;
mTempRect.set(xSearchPoint, 0, xSearchPoint, 0);
}
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.left < mListPadding.left) {
amountToScroll = mListPadding.left - mTempRect.left;
if (positionOfNewFocus > 0) {
amountToScroll += getArrowScrollPreviewLength();
}
}
} else {
final int listRight = getWidth() - mListPadding.right;
if (mTempRect.bottom > listRight) {
amountToScroll = mTempRect.right - listRight;
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 listRight = getRight() - getLeft() - mListPadding.right;
if (mTempRect.right < mListPadding.left) {
distance = mListPadding.left - mTempRect.right;
} else if (mTempRect.left > listRight) {
distance = mTempRect.left - listRight;
}
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) {
offsetChildrenLeftAndRight(amount);
final int listRight = getWidth() - mListPadding.right;
final int listLeft = mListPadding.left;
final AbsHListView.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.getRight() < listRight) {
final int lastVisiblePosition = mFirstPosition + numChildren - 1;
if (lastVisiblePosition < mItemCount - 1) {
last = addViewAfter(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() < listRight) {
offsetChildrenLeftAndRight(listRight - last.getRight());
}
// top views may be panned off screen
View first = getChildAt(0);
while (first.getRight() < listLeft) {
AbsHListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams();
if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
detachViewFromParent(first);
recycleBin.addScrapView(first, mFirstPosition);
} else {
removeViewInLayout(first);
}
first = getChildAt(0);
mFirstPosition++;
}
} else {
// shifted items down
View first = getChildAt(0);
// may need to pan views into top
while ((first.getLeft() > listLeft) && (mFirstPosition > 0)) {
first = addViewBefore(first, mFirstPosition);
mFirstPosition--;
}
// may have brought the very first child of the list in too far and
// need to shift it back
if (first.getLeft() > listLeft) {
offsetChildrenLeftAndRight(listLeft - first.getLeft());
}
int lastIndex = getChildCount() - 1;
View last = getChildAt(lastIndex);
// bottom view may be panned off screen
while (last.getLeft() > listRight) {
AbsHListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams();
if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
detachViewFromParent(last);
recycleBin.addScrapView(last, mFirstPosition + lastIndex);
} else {
removeViewInLayout(last);
}
last = getChildAt(--lastIndex);
}
}
}
private View addViewBefore(View theView, int position) {
int abovePosition = position - 1;
View view = obtainView(abovePosition, mIsScrap);
int edgeOfNewChild = theView.getLeft() - mDividerWidth;
setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.top, false, mIsScrap[0]);
return view;
}
private View addViewAfter(View theView, int position) {
int belowPosition = position + 1;
View view = obtainView(belowPosition, mIsScrap);
int edgeOfNewChild = theView.getRight() + mDividerWidth;
setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.top, false, mIsScrap[0]);
return view;
}
/**
* @return Whether the views created by the ListAdapter can contain focusable items.
*/
public boolean getItemsCanFocus() {
return mItemsCanFocus;
}
/**
* 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);
}
}
public boolean isOpaque() {
boolean retValue = (mCachingActive && mIsCacheColorOpaque && mDividerIsOpaque) || super.isOpaque();
if (retValue) {
// only return true if the list items cover the entire area of the view
final int listLeft = mListPadding != null ? mListPadding.left : getPaddingLeft();
View first = getChildAt(0);
if (first == null || first.getLeft() > listLeft) {
return false;
}
final int listRight = getWidth() - (mListPadding != null ? mListPadding.right : getPaddingRight());
View last = getChildAt(getChildCount() - 1);
if (last == null || last.getRight() < listRight) {
return false;
}
}
return retValue;
}
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 width = drawable.getMinimumWidth();
canvas.save();
canvas.clipRect(bounds);
final int span = bounds.right - bounds.left;
if (span < width) {
bounds.left = bounds.right - width;
}
drawable.setBounds(bounds);
drawable.draw(canvas);
canvas.restore();
}
void drawOverscrollFooter(Canvas canvas, Drawable drawable, Rect bounds) {
final int width = drawable.getMinimumWidth();
canvas.save();
canvas.clipRect(bounds);
final int span = bounds.right - bounds.left;
if (span < width) {
bounds.right = bounds.left + width;
}
drawable.setBounds(bounds);
drawable.draw(canvas);
canvas.restore();
}
protected void dispatchDraw(Canvas canvas) {
if (mCachingStarted) {
mCachingActive = true;
}
// Draw the dividers
final int dividerWidth = mDividerWidth;
final Drawable overscrollHeader = mOverScrollHeader;
final Drawable overscrollFooter = mOverScrollFooter;
final boolean drawOverscrollHeader = overscrollHeader != null;
final boolean drawOverscrollFooter = overscrollFooter != null;
final boolean drawDividers = dividerWidth > 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.top = getPaddingTop();
bounds.bottom = getBottom() - getTop() - getPaddingBottom();
final int count = getChildCount();
final int headerCount = mHeaderViewInfos.size();
final int itemCount = mItemCount;
final int footerLimit = itemCount - mFooterViewInfos.size() - 1;
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 effectivePaddingLeft = 0;
int effectivePaddingRight = 0;
// if ( ( mGroupFlags & CLIP_TO_PADDING_MASK ) == CLIP_TO_PADDING_MASK ) {
// effectivePaddingTop = mListPadding.top;
// effectivePaddingBottom = mListPadding.bottom;
// }
final int listRight = getRight() - getLeft() - effectivePaddingRight + getScrollX();
if (!mStackFromRight) {
int right = 0;
// Draw top divider or header for overscroll
final int scrollX = getScrollX();
if (count > 0 && scrollX < 0) {
if (drawOverscrollHeader) {
bounds.right = 0;
bounds.left = scrollX;
drawOverscrollHeader(canvas, overscrollHeader, bounds);
} else if (drawDividers) {
bounds.right = 0;
bounds.left = -dividerWidth;
drawDivider(canvas, bounds, -1);
}
}
for (int i = 0; i < count; i++) {
if ((headerDividers || first + i >= headerCount) &&
(footerDividers || first + i < footerLimit)) {
View child = getChildAt(i);
right = child.getRight();
// Don't draw dividers next to items that are not enabled
if (drawDividers &&
(right < listRight && !(drawOverscrollFooter && i == count - 1))) {
if ((areAllItemsSelectable || (adapter.isEnabled(first + i) && (i == count - 1 || adapter
.isEnabled(first + i + 1))))) {
bounds.left = right;
bounds.right = right + dividerWidth;
drawDivider(canvas, bounds, i);
} else if (fillForMissingDividers) {
bounds.left = right;
bounds.right = right + dividerWidth;
canvas.drawRect(bounds, paint);
}
}
}
}
final int overFooterBottom = getRight() + getScrollX();
if (drawOverscrollFooter && first + count == itemCount &&
overFooterBottom > right) {
bounds.left = right;
bounds.right = overFooterBottom;
drawOverscrollFooter(canvas, overscrollFooter, bounds);
}
} else {
int left;
final int scrollX = getScrollX();
if (count > 0 && drawOverscrollHeader) {
bounds.left = scrollX;
bounds.right = getChildAt(0).getLeft();
drawOverscrollHeader(canvas, overscrollHeader, bounds);
}
final int start = drawOverscrollHeader ? 1 : 0;
for (int i = start; i < count; i++) {
if ((headerDividers || first + i >= headerCount) &&
(footerDividers || first + i < footerLimit)) {
View child = getChildAt(i);
left = child.getLeft();
// Don't draw dividers next to items that are not enabled
if (left > effectivePaddingLeft) {
if ((areAllItemsSelectable || (adapter.isEnabled(first + i) && (i == count - 1 || adapter
.isEnabled(first + i + 1))))) {
bounds.left = left - dividerWidth;
bounds.right = left;
// 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.left = left - dividerWidth;
bounds.right = left;
canvas.drawRect(bounds, paint);
}
}
}
}
if (count > 0 && scrollX > 0) {
if (drawOverscrollFooter) {
final int absListRight = getRight();
bounds.left = absListRight;
bounds.right = absListRight + scrollX;
drawOverscrollFooter(canvas, overscrollFooter, bounds);
} else if (drawDividers) {
bounds.left = listRight;
bounds.right = listRight + dividerWidth;
drawDivider(canvas, bounds, -1);
}
}
}
}
// Draw the indicators (these should be drawn above the dividers) and children
super.dispatchDraw(canvas);
}
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 (LOG_ENABLED) {
Log.i(LOG_TAG, "setDivider: " + divider);
}
if (divider != null) {
mDividerWidth = divider.getIntrinsicWidth();
} else {
mDividerWidth = 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 getDividerWidth() {
return mDividerWidth;
}
/**
* 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 setDividerWidth(int width) {
if (LOG_ENABLED) {
Log.i(LOG_TAG, "setDividerWidth: " + width);
}
mDividerWidth = width;
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 #addHeaderView(android.view.View)
*/
public void setHeaderDividersEnabled(boolean headerDividersEnabled) {
mHeaderDividersEnabled = headerDividersEnabled;
invalidate();
}
/**
* Enables or disables the drawing of the divider for footer views.
*
* @param footerDividersEnabled True to draw the footers, false otherwise.
* @see #setHeaderDividersEnabled(boolean)
* @see #addFooterView(android.view.View)
*/
public void setFooterDividersEnabled(boolean footerDividersEnabled) {
mFooterDividersEnabled = footerDividersEnabled;
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 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 (getScrollX() < 0) {
invalidate();
}
}
/**
* @return The drawable that will be drawn below all other list content
*/
public Drawable getOverscrollFooter() {
return mOverScrollFooter;
}
/**
* 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();
}
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
final ListAdapter adapter = mAdapter;
int closetChildIndex = -1;
int closestChildLeft = 0;
if (adapter != null && gainFocus && previouslyFocusedRect != null) {
previouslyFocusedRect.offset(getScrollX(), getScrollY());
// 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;
closestChildLeft = other.getLeft();
}
}
}
if (closetChildIndex >= 0) {
setSelectionFromLeft(closetChildIndex + mFirstPosition, closestChildLeft);
} 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.
*/
protected void onFinishInflate() {
super.onFinishInflate();
int count = getChildCount();
if (count > 0) {
for (int i = 0; i < count; ++i) {
addHeaderView(getChildAt(i));
}
removeAllViews();
}
}
/**
* Returns the set of checked items ids. The result is only valid if the choice mode has not been set to
* {@link #CHOICE_MODE_NONE}.
*
* @return A new array which contains the id of each checked item in the list.
* @deprecated Use {@link #getCheckedItemIds()} instead.
*/
@Deprecated
public long[] getCheckItemIds() {
// Use new behavior that correctly handles stable ID mapping.
if (mAdapter != null && mAdapter.hasStableIds()) {
return getCheckedItemIds();
}
// Old behavior was buggy, but would sort of work for adapters without stable IDs.
// Fall back to it to support legacy apps.
if (mChoiceMode != ListView.CHOICE_MODE_NONE && mCheckStates != null && mAdapter != null) {
final SparseArrayCompattrue
if the fixed view should be selectable in the list
*/
public boolean isSelectable;
}
/**
* Holds results of focus aware arrow scrolling.
*/
static private class ArrowScrollFocusResult {
private int mSelectedPosition;
private int mAmountToScroll;
/**
* How {@link HListView#arrowScrollFocused} returns its values.
*/
void populate(int selectedPosition, int amountToScroll) {
mSelectedPosition = selectedPosition;
mAmountToScroll = amountToScroll;
}
public int getSelectedPosition() {
return mSelectedPosition;
}
public int getAmountToScroll() {
return mAmountToScroll;
}
}
private class FocusSelector implements Runnable {
private int mPosition;
private int mPositionLeft;
public FocusSelector setup(int position, int left) {
mPosition = position;
mPositionLeft = left;
return this;
}
public void run() {
setSelectionFromLeft(mPosition, mPositionLeft);
}
}
}