package zrc.widget; import java.util.ArrayList; import android.annotation.TargetApi; import android.content.Context; 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.os.Build; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.widget.ListAdapter; import zrc.util.APIUtil; /** * @author Zaric */ public class ZrcListView extends ZrcAbsListView { static final int NO_POSITION = -1; private static final float MAX_SCROLL_FACTOR = 0.33f; private final Rect mTempRect = new Rect(); Drawable mDivider; int mDividerHeight; private ArrayList<FixedViewInfo> mHeaderViewInfos = new ArrayList<FixedViewInfo>(); private ArrayList<FixedViewInfo> mFooterViewInfos = new ArrayList<FixedViewInfo>(); private boolean mIsCacheColorOpaque; private boolean mDividerIsOpaque; private boolean mHeaderDividersEnabled; private boolean mFooterDividersEnabled; private boolean mAreAllItemsSelectable = true; private boolean mItemsCanFocus = false; private Paint mDividerPaint; private int mItemAnimForTopIn; private int mItemAnimForBottomIn; public ZrcListView(Context context) { this(context, null); } public ZrcListView(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.listViewStyle); } public ZrcListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListView, defStyle, 0); final Drawable d = a.getDrawable(R.styleable.ListView_android_divider); if (d != null) { setDivider(d); } final int dividerHeight = a.getDimensionPixelSize(R.styleable.ListView_android_dividerHeight, 0); if (dividerHeight != 0) { setDividerHeight(dividerHeight); } mHeaderDividersEnabled = a.getBoolean(R.styleable.ListView_android_headerDividersEnabled, true); mFooterDividersEnabled = a.getBoolean(R.styleable.ListView_android_footerDividersEnabled, true); mItemAnimForTopIn = 0; mItemAnimForBottomIn = 0; a.recycle(); } public int getMaxScrollAmount() { return (int) (MAX_SCROLL_FACTOR * (getBottom() - getTop())); } private void adjustViewsUp() { final int childCount = getChildCount(); int delta; if (childCount > 0) { View child; child = getChildAt(0); delta = child.getTop() - mListPadding.top - mFirstTopOffset; if (mFirstPosition != 0) { delta -= mDividerHeight; } if (delta < 0) { delta = 0; } if (delta != 0) { offsetChildrenTopAndBottom(-delta); } } } 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); if (mAdapter != null) { if (!(mAdapter instanceof HeaderViewListAdapter)) { mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter); } if (mDataSetObserver != null) { mDataSetObserver.onChanged(); } } } public void addHeaderView(View v) { addHeaderView(v, null, true); } @Override public int getHeaderViewsCount() { return mHeaderViewInfos.size(); } 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<FixedViewInfo> where) { int len = where.size(); for (int i = 0; i < len; ++i) { FixedViewInfo info = where.get(i); if (info.view == v) { where.remove(i); break; } } } 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); } if (mDataSetObserver != null) { mDataSetObserver.onChanged(); } } } public void addFooterView(View v) { addFooterView(v, null, true); } @Override public int getFooterViewsCount() { return mFooterViewInfos.size(); } 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; } @Override public ListAdapter getAdapter() { return mAdapter; } @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; } // 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()); } else { mAreAllItemsSelectable = true; checkFocus(); } requestLayout(); } @Override void resetList() { clearRecycledState(mHeaderViewInfos); clearRecycledState(mFooterViewInfos); super.resetList(); mLayoutMode = LAYOUT_NORMAL; } private void clearRecycledState(ArrayList<FixedViewInfo> infos) { if (infos != null) { final int count = infos.size(); for (int i = 0; i < count; i++) { final View child = infos.get(i).view; final LayoutParams p = (LayoutParams) child.getLayoutParams(); if (p != null) { p.recycledHeaderFooter = false; } } } } @Override public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { rect.offset(child.getLeft(), child.getTop()); rect.offset(-child.getScrollX(), -child.getScrollY()); final int height = getHeight(); int listUnfadedTop = getScrollY(); int listUnfadedBottom = listUnfadedTop + height; int childCount = getChildCount(); int bottomOfBottomChild = getChildAt(childCount - 1).getBottom(); int scrollYDelta = 0; if (rect.bottom > listUnfadedBottom && rect.top > listUnfadedTop) { // need to MOVE DOWN to get it in view: move down just enough so // that the entire rectangle is in view (or at least the first // screen size chunk). if (rect.height() > height) { // just enough to get screen size chunk on scrollYDelta += (rect.top - listUnfadedTop); } else { // get entire rect at bottom of screen scrollYDelta += (rect.bottom - listUnfadedBottom); } // make sure we aren't scrolling beyond the end of our children int distanceToBottom = bottomOfBottomChild - listUnfadedBottom; scrollYDelta = Math.min(scrollYDelta, distanceToBottom); } else if (rect.top < listUnfadedTop && rect.bottom < listUnfadedBottom) { // need to MOVE UP to get it in view: move up just enough so that // entire rectangle is in view (or at least the first screen // size chunk of it). if (rect.height() > height) { // screen size chunk scrollYDelta -= (listUnfadedBottom - rect.bottom); } else { // entire rect at top scrollYDelta -= (listUnfadedTop - rect.top); } // make sure we aren't scrolling any further than the top our // children int top = getChildAt(0).getTop(); int deltaToTop = top - listUnfadedTop; scrollYDelta = Math.max(scrollYDelta, deltaToTop); } final boolean scroll = scrollYDelta != 0; if (scroll) { scrollListItemsBy(-scrollYDelta); positionSelector(INVALID_POSITION, child); invalidate(); } return scroll; } @Override void fillGap(boolean down) { final int count = getChildCount(); if (down) { final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight : mFirstTop + mListPadding.top + mFirstTopOffset; fillDown(mFirstPosition + count, startOffset, true); // correctTooHigh(getChildCount()); } else { int paddingBottom = 0; final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight : getHeight() - paddingBottom - mLastBottomOffset; fillUp(mFirstPosition - 1, startOffset, true); // correctTooLow(getChildCount()); } } void correctTooHighOrTooLow() { final int count = getCount(); if (count == 0) { return; } if (mFirstPosition == 0) { int dx = mFirstTopOffset - mFirstTop; if (dx == 0) { return; } else if (dx < 0) { trackMotionScroll(dx, dx); return; } } if (mFirstPosition + count == mItemCount) { int lastBottom = getChildAt(count - 1).getBottom(); int dx = getHeight() - mLastBottomOffset - lastBottom; if (dx > 0) { trackMotionScroll(dx, dx); correctTooHighOrTooLow(); return; } } } private void fillDown(int pos, int nextTop, boolean isAnim) { int end = (getBottom() - getTop()); while (nextTop < end && pos < mItemCount) { View child = makeAndAddView(pos, nextTop, true, mListPadding.left, false); nextTop = child.getBottom() + mDividerHeight; if (isAnim && mItemAnimForBottomIn != 0 && child.getVisibility() == View.VISIBLE) { child.startAnimation( AnimationUtils.loadAnimation(getContext(), mItemAnimForBottomIn)); } pos++; } } private void fillUp(int pos, int nextBottom, boolean isAnim) { int end = 0; while (nextBottom > end && pos >= 0) { View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, false); nextBottom = child.getTop() - mDividerHeight; if (isAnim && mItemAnimForTopIn != 0 && child.getVisibility() == View.VISIBLE) { child.startAnimation(AnimationUtils.loadAnimation(getContext(), mItemAnimForTopIn)); } pos--; } mFirstPosition = pos + 1; } private void fillFromTop(int nextTop) { mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); if (mFirstPosition < 0) { mFirstPosition = 0; } fillDown(mFirstPosition, nextTop, false); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Sets up mListPadding super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int childWidth = 0; int childHeight = 0; int childState = 0; mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED)) { final View child = obtainView(0, mIsScrap); measureScrapChild(child, 0, widthMeasureSpec); childWidth = child.getMeasuredWidth(); childHeight = child.getMeasuredHeight(); childState = combineMeasuredStates(childState, child.getMeasuredState()); if (recycleOnMeasure() && mRecycler .shouldRecycleViewType(((LayoutParams) child.getLayoutParams()).viewType)) { mRecycler.addScrapView(child, -1); } } if (widthMode == MeasureSpec.UNSPECIFIED) { widthSize = mListPadding.left + mListPadding.right + childWidth + getVerticalScrollbarWidth(); } else { widthSize |= (childState & MEASURED_STATE_MASK); } if (heightMode == MeasureSpec.UNSPECIFIED) { heightSize = mListPadding.top + mListPadding.bottom + childHeight + mFirstTopOffset + mLastBottomOffset; } if (heightMode == MeasureSpec.AT_MOST) { heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); } setMeasuredDimension(widthSize, heightSize); mWidthMeasureSpec = widthMeasureSpec; } private void measureScrapChild(View child, int position, int widthMeasureSpec) { LayoutParams p = (LayoutParams) child.getLayoutParams(); if (p == null) { p = (ZrcAbsListView.LayoutParams) generateDefaultLayoutParams(); child.setLayoutParams(p); } p.viewType = mAdapter.getItemViewType(position); p.forceAdd = true; int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, 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); } protected boolean recycleOnMeasure() { return true; } final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, final int maxHeight, int disallowPartialChildPosition) { final ListAdapter adapter = mAdapter; if (adapter == null) { return mListPadding.top + mListPadding.bottom + mFirstTopOffset + mLastBottomOffset; } // Include the padding of the list int returnedHeight = mListPadding.top + mListPadding.bottom + mFirstTopOffset + mLastBottomOffset; final int dividerHeight = ((mDividerHeight > 0) && mDivider != null) ? mDividerHeight : 0; // The previous height value that was less than maxHeight and contained // no partial children int prevHeightWithoutPartialChild = 0; int i; View child; // mItemCount - 1 since endPosition parameter is inclusive endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; final ZrcAbsListView.RecycleBin recycleBin = mRecycler; final boolean recyle = recycleOnMeasure(); final boolean[] isScrap = mIsScrap; for (i = startPosition; i <= endPosition; ++i) { child = obtainView(i, isScrap); measureScrapChild(child, i, widthMeasureSpec); if (i > 0) { // Count the divider for all but one child returnedHeight += dividerHeight; } // Recycle the view before we possibly return from the method if (recyle && recycleBin .shouldRecycleViewType(((LayoutParams) child.getLayoutParams()).viewType)) { recycleBin.addScrapView(child, -1); } returnedHeight += child.getMeasuredHeight(); if (returnedHeight >= maxHeight) { // We went over, figure out which height to return. If // returnedHeight > maxHeight, // then the i'th position did not fit completely. return (disallowPartialChildPosition >= 0) // Disallowing is // enabled (> -1) && (i > disallowPartialChildPosition) // We've past the // min pos && (prevHeightWithoutPartialChild > 0) // We have a prev // height && (returnedHeight != maxHeight) // i'th child did not // fit completely ? prevHeightWithoutPartialChild : maxHeight; } if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { prevHeightWithoutPartialChild = returnedHeight; } } // At this point, we went through the range of children, and they each // completely fit, so return the returnedHeight return returnedHeight; } @Override int findMotionRow(int y) { int childCount = getChildCount(); if (childCount > 0) { for (int i = 0; i < childCount; i++) { View v = getChildAt(i); if (y <= v.getBottom()) { return mFirstPosition + i; } } } return INVALID_POSITION; } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override protected void layoutChildren() { final boolean blockLayoutRequests = mBlockLayoutRequests; if (blockLayoutRequests) { return; } mBlockLayoutRequests = true; try { super.layoutChildren(); invalidate(); final int childrenTop = mListPadding.top + mFirstTopOffset; final int childrenBottom = getBottom() - getTop() - mListPadding.bottom - mLastBottomOffset; final int childCount = getChildCount(); boolean dataChanged = mDataChanged; if (dataChanged) { handleDataChanged(); } // Handle the empty set by removing all views that are visible // and calling it a day if (mAdapter != null && mItemCount != mAdapter.getCount()) { throw new IllegalStateException("The content of the adapter has changed but " + "ListView did not receive a notification. Make sure the content of " + "your adapter is not modified from a background thread, but only from " + "the UI thread. Make sure your adapter calls notifyDataSetChanged() " + "when its content changes. [in ListView(" + getId() + ", " + getClass() + ") with Adapter(" + mAdapter.getClass() + ")]"); } // Ensure the child containing focus, if any, has transient state. // If the list data hasn't changed, or if the adapter has stable // IDs, this will maintain focus. final View focusedChild = getFocusedChild(); if (focusedChild != null) { focusedChild.setHasTransientState(true); } // Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; final int firstTop = mFirstTop; final RecycleBin recycleBin = mRecycler; if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition + i); } } else { recycleBin.fillActiveViews(childCount, firstPosition); } // Clear out old views detachAllViewsFromParent(); recycleBin.removeSkippedScrap(); switch (mLayoutMode) { case LAYOUT_FORCE_BOTTOM: fillUp(mItemCount - 1, childrenBottom, false); adjustViewsUp(); break; case LAYOUT_FORCE_TOP: mFirstPosition = 0; fillFromTop(childrenTop); break; default: if (mItemCount == 0) { if (mTouchMode != TOUCH_MODE_SCROLL) { scrollToAdjustViewsUpOrDown(); } } else if (firstPosition >= mItemCount) { mFirstPosition = mItemCount - 1; View child = makeAndAddView(mFirstPosition, 1, false, mListPadding.left, false); if (mItemAnimForTopIn != 0 && child.getVisibility() == View.VISIBLE) { child.startAnimation( AnimationUtils.loadAnimation(getContext(), mItemAnimForTopIn)); } scrollToAdjustViewsUpOrDown(); } else { fillDown(firstPosition, firstTop, false); if (mTouchMode != TOUCH_MODE_SCROLL) { scrollToAdjustViewsUpOrDown(); } } break; } // Flush any cached views that did not get reused above recycleBin.scrapActiveViews(); // If the user's finger is down, select the motion position. // Otherwise, clear selection. if (mTouchMode == TOUCH_MODE_TAP || mTouchMode == TOUCH_MODE_DONE_WAITING) { final View child = getChildAt(mMotionPosition - mFirstPosition); if (child != null) { positionSelector(mMotionPosition, child); } } else { mSelectorRect.setEmpty(); } mLayoutMode = LAYOUT_NORMAL; mDataChanged = false; if (mPositionScrollAfterLayout != null) { post(mPositionScrollAfterLayout); mPositionScrollAfterLayout = null; } updateScrollIndicators(); invokeOnItemScrollListener(); } finally { if (!blockLayoutRequests) { mBlockLayoutRequests = false; } } } /** * Obtain the view and add it to our list of children. The view can be made * fresh, converted from an unused view, or used as is if it was in the * recycle bin. * * @param position Logical position in the list * @param y Top or bottom edge of the view to add * @param flow If flow is true, align top edge to y. If false, align bottom * edge to y. * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @return View that was added */ private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { View child; if (!mDataChanged) { // Try to use an existing view for this position child = mRecycler.getActiveView(position); if (child != null) { // Found it -- we're using an existing child // This just needs to be positioned setupChild(child, position, y, flow, childrenLeft, selected, true); return child; } } // Make a new view for this position, or convert an unused view if // possible child = obtainView(position, mIsScrap); // This needs to be positioned and measured setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; } /** * Add a view as a child and make sure it is measured (if necessary) and * positioned properly. * * @param child The view to add * @param position The position of this child * @param y The y position relative to which this view will be positioned * @param flowDown If true, align top edge to y. If false, align bottom edge * to y. * @param childrenLeft Left edge where children should be positioned * @param selected Is this position selected? * @param recycled Has this view been pulled from the recycle bin? If so it * does not need to be remeasured. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean recycled) { final boolean isSelected = selected && shouldShowSelector(); final boolean updateChildSelected = isSelected != child.isSelected(); final int mode = mTouchMode; final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && mMotionPosition == position; final boolean updateChildPressed = isPressed != child.isPressed(); final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); // Respect layout params that are already in the view. Otherwise make // some up... // noinspection unchecked ZrcAbsListView.LayoutParams p = (ZrcAbsListView.LayoutParams) child.getLayoutParams(); if (p == null) { p = (ZrcAbsListView.LayoutParams) generateDefaultLayoutParams(); } p.viewType = mAdapter.getItemViewType(position); if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter && p.viewType == ZrcAdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { attachViewToParent(child, flowDown ? -1 : 0, p); } else { p.forceAdd = false; if (p.viewType == ZrcAdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { p.recycledHeaderFooter = true; } addViewInLayout(child, flowDown ? -1 : 0, p, true); } if (updateChildSelected) { child.setSelected(isSelected); } if (updateChildPressed) { child.setPressed(isPressed); } if (needToMeasure) { 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); } else { cleanupLayoutState(child); } final int w = child.getMeasuredWidth(); final int h = child.getMeasuredHeight(); final int childTop = flowDown ? y : y - h; if (needToMeasure) { final int childRight = childrenLeft + w; final int childBottom = childTop + h; child.layout(childrenLeft, childTop, childRight, childBottom); } else { child.offsetLeftAndRight(childrenLeft - child.getLeft()); child.offsetTopAndBottom(childTop - child.getTop()); } if (mCachingStarted && !child.isDrawingCacheEnabled()) { child.setDrawingCacheEnabled(true); } if (recycled && (((ZrcAbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition) != position) { if (APIUtil.isSupport(11)) { child.jumpDrawablesToCurrentState(); } } } @Override protected boolean canAnimate() { return super.canAnimate() && mItemCount > 0; } /** * 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 - mLastBottomOffset; final int listTop = mListPadding.top + mFirstTopOffset; final ZrcAbsListView.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) { ZrcAbsListView.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) { ZrcAbsListView.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; } /** * @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); } } @Override 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 listTop = mListPadding != null ? mListPadding.top + mFirstTopOffset : getPaddingTop(); View first = getChildAt(0); if (first == null || first.getTop() > listTop) { return false; } final int listBottom = getHeight() - (mListPadding != null ? mListPadding.bottom + mLastBottomOffset : getPaddingBottom()); 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); } @Override protected void dispatchDraw(Canvas canvas) { if (mCachingStarted) { mCachingActive = true; } // Draw the dividers final int dividerHeight = mDividerHeight; final boolean drawDividers = dividerHeight > 0 && mDivider != null; if (drawDividers) { // Only modify the top and bottom in the loop, we set the left and // right here final Rect bounds = mTempRect; bounds.left = getPaddingLeft(); bounds.right = getRight() - getLeft() - getPaddingRight(); final int mBottom = getBottom(); final int mTop = getTop(); final int mScrollY = getScrollY(); 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 effectivePaddingBottom = 0; final int listBottom = mBottom - mTop - effectivePaddingBottom + mScrollY; int bottom = 0; if (getChildCount() > 0) { final int firstTop = getChildAt(0).getTop(); if (firstTop > 0) { bounds.top = firstTop - dividerHeight; bounds.bottom = firstTop; drawDivider(canvas, bounds, 0); } } 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)) { 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); } } } } } super.dispatchDraw(canvas); } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { boolean more = super.drawChild(canvas, child, drawingTime); if (mCachingActive) { mCachingActive = false; } return more; } void drawDivider(Canvas canvas, Rect bounds, int childIndex) { final Drawable divider = mDivider; divider.setBounds(bounds); divider.draw(canvas); } public Drawable getDivider() { return mDivider; } 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(); } public int getDividerHeight() { return mDividerHeight; } public void setDividerHeight(int height) { mDividerHeight = height; requestLayout(); invalidate(); } public void setHeaderDividersEnabled(boolean headerDividersEnabled) { mHeaderDividersEnabled = headerDividersEnabled; invalidate(); } public boolean areHeaderDividersEnabled() { return mHeaderDividersEnabled; } public void setFooterDividersEnabled(boolean footerDividersEnabled) { mFooterDividersEnabled = footerDividersEnabled; invalidate(); } public boolean areFooterDividersEnabled() { return mFooterDividersEnabled; } @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); final ListAdapter adapter = mAdapter; 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; } } } requestLayout(); } /** * convert all views to header view */ @Override protected void onFinishInflate() { super.onFinishInflate(); int count = getChildCount(); if (count > 0) { for (int i = 0; i < count; ++i) { addHeaderView(getChildAt(i)); } removeAllViews(); } } /** * 设置由顶部进入视图的列表项动画,当animId = 0时,为取消动画。 * * @param animId */ public void setItemAnimForTopIn(int animId) { mItemAnimForTopIn = animId; } /** * 设置由底部进入视图的列表项动画,当animId = 0时,为取消动画。 * * @param animId */ public void setItemAnimForBottomIn(int animId) { mItemAnimForBottomIn = animId; } public void setSelection(int i) { setSelectionFromTop(i, 0); } public void setSelectionFromTop(int i, int offset){ mFirstPosition = i; mFirstTop = offset; mDataChanged = true; requestLayout(); } public static interface OnStartListener { void onStart(); } public static interface OnScrollStateListener { public static final int EDGE = 0; public static final int DOWN = 1; public static final int UP = 2; void onChange(int state); } public static interface OnScrollListener { public static int SCROLL_STATE_IDLE = 0; public static int SCROLL_STATE_TOUCH_SCROLL = 1; public static int SCROLL_STATE_FLING = 2; public void onScrollStateChanged(ZrcAbsListView view, int scrollState); public void onScroll(ZrcAbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount); } public interface OnItemClickListener { void onItemClick(ZrcListView parent, View view, int position, long id); } public interface OnItemLongClickListener { boolean onItemLongClick(ZrcListView parent, View view, int position, long id); } public class FixedViewInfo { public View view; public Object data; public boolean isSelectable; } }