From f5da63f9882e1807c6bd2adb5205ad7482c45339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Thu, 2 Jan 2014 21:10:08 +0100 Subject: New list with sticky list headers library --- .../stickylistheaders/AdapterWrapper.java | 225 +++++ .../stickylistheaders/ApiLevelTooLowException.java | 11 + .../stickylistheaders/CheckableWrapperView.java | 31 + .../SectionIndexerAdapterWrapper.java | 32 + .../StickyListHeadersAdapter.java | 38 + .../StickyListHeadersListView.java | 925 +++++++++++++++++++++ .../stickylistheaders/WrapperView.java | 150 ++++ .../stickylistheaders/WrapperViewList.java | 175 ++++ 8 files changed, 1587 insertions(+) create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/AdapterWrapper.java create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/ApiLevelTooLowException.java create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/CheckableWrapperView.java create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/SectionIndexerAdapterWrapper.java create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersAdapter.java create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersListView.java create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperView.java create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperViewList.java (limited to 'libraries/StickyListHeaders/library/src') diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/AdapterWrapper.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/AdapterWrapper.java new file mode 100644 index 000000000..e67de0dbf --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/AdapterWrapper.java @@ -0,0 +1,225 @@ +package se.emilsjolander.stickylistheaders; + +import java.util.LinkedList; +import java.util.List; + +import android.content.Context; +import android.database.DataSetObserver; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Checkable; +import android.widget.ListAdapter; + +/** + * A {@link ListAdapter} which wraps a {@link StickyListHeadersAdapter} and + * automatically handles wrapping the result of + * {@link StickyListHeadersAdapter#getView(int, android.view.View, android.view.ViewGroup)} + * and + * {@link StickyListHeadersAdapter#getHeaderView(int, android.view.View, android.view.ViewGroup)} + * appropriately. + * + * @author Jake Wharton (jakewharton@gmail.com) + */ +class AdapterWrapper extends BaseAdapter implements StickyListHeadersAdapter { + + interface OnHeaderClickListener { + public void onHeaderClick(View header, int itemPosition, long headerId); + } + + final StickyListHeadersAdapter mDelegate; + private final List mHeaderCache = new LinkedList(); + private final Context mContext; + private Drawable mDivider; + private int mDividerHeight; + private OnHeaderClickListener mOnHeaderClickListener; + private DataSetObserver mDataSetObserver = new DataSetObserver() { + + @Override + public void onInvalidated() { + mHeaderCache.clear(); + AdapterWrapper.super.notifyDataSetInvalidated(); + } + + @Override + public void onChanged() { + AdapterWrapper.super.notifyDataSetChanged(); + } + }; + + AdapterWrapper(Context context, + StickyListHeadersAdapter delegate) { + this.mContext = context; + this.mDelegate = delegate; + delegate.registerDataSetObserver(mDataSetObserver); + } + + void setDivider(Drawable divider, int dividerHeight) { + this.mDivider = divider; + this.mDividerHeight = dividerHeight; + notifyDataSetChanged(); + } + + @Override + public boolean areAllItemsEnabled() { + return mDelegate.areAllItemsEnabled(); + } + + @Override + public boolean isEnabled(int position) { + return mDelegate.isEnabled(position); + } + + @Override + public int getCount() { + return mDelegate.getCount(); + } + + @Override + public Object getItem(int position) { + return mDelegate.getItem(position); + } + + @Override + public long getItemId(int position) { + return mDelegate.getItemId(position); + } + + @Override + public boolean hasStableIds() { + return mDelegate.hasStableIds(); + } + + @Override + public int getItemViewType(int position) { + return mDelegate.getItemViewType(position); + } + + @Override + public int getViewTypeCount() { + return mDelegate.getViewTypeCount(); + } + + @Override + public boolean isEmpty() { + return mDelegate.isEmpty(); + } + + /** + * Will recycle header from {@link WrapperView} if it exists + */ + private void recycleHeaderIfExists(WrapperView wv) { + View header = wv.mHeader; + if (header != null) { + // reset the headers visibility when adding it to the cache + header.setVisibility(View.VISIBLE); + mHeaderCache.add(header); + } + } + + /** + * Get a header view. This optionally pulls a header from the supplied + * {@link WrapperView} and will also recycle the divider if it exists. + */ + private View configureHeader(WrapperView wv, final int position) { + View header = wv.mHeader == null ? popHeader() : wv.mHeader; + header = mDelegate.getHeaderView(position, header, wv); + if (header == null) { + throw new NullPointerException("Header view must not be null."); + } + //if the header isn't clickable, the listselector will be drawn on top of the header + header.setClickable(true); + header.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if(mOnHeaderClickListener != null){ + long headerId = mDelegate.getHeaderId(position); + mOnHeaderClickListener.onHeaderClick(v, position, headerId); + } + } + }); + return header; + } + + private View popHeader() { + if(mHeaderCache.size() > 0) { + return mHeaderCache.remove(0); + } + return null; + } + + /** Returns {@code true} if the previous position has the same header ID. */ + private boolean previousPositionHasSameHeader(int position) { + return position != 0 + && mDelegate.getHeaderId(position) == mDelegate + .getHeaderId(position - 1); + } + + @Override + public WrapperView getView(int position, View convertView, ViewGroup parent) { + WrapperView wv = (convertView == null) ? new WrapperView(mContext) : (WrapperView) convertView; + View item = mDelegate.getView(position, wv.mItem, parent); + View header = null; + if (previousPositionHasSameHeader(position)) { + recycleHeaderIfExists(wv); + } else { + header = configureHeader(wv, position); + } + if((item instanceof Checkable) && !(wv instanceof CheckableWrapperView)) { + // Need to create Checkable subclass of WrapperView for ListView to work correctly + wv = new CheckableWrapperView(mContext); + } else if(!(item instanceof Checkable) && (wv instanceof CheckableWrapperView)) { + wv = new WrapperView(mContext); + } + wv.update(item, header, mDivider, mDividerHeight); + return wv; + } + + public void setOnHeaderClickListener(OnHeaderClickListener onHeaderClickListener){ + this.mOnHeaderClickListener = onHeaderClickListener; + } + + @Override + public boolean equals(Object o) { + return mDelegate.equals(o); + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return ((BaseAdapter) mDelegate).getDropDownView(position, convertView, parent); + } + + @Override + public int hashCode() { + return mDelegate.hashCode(); + } + + @Override + public void notifyDataSetChanged() { + ((BaseAdapter) mDelegate).notifyDataSetChanged(); + } + + @Override + public void notifyDataSetInvalidated() { + ((BaseAdapter) mDelegate).notifyDataSetInvalidated(); + } + + @Override + public String toString() { + return mDelegate.toString(); + } + + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + return mDelegate.getHeaderView(position, convertView, parent); + } + + @Override + public long getHeaderId(int position) { + return mDelegate.getHeaderId(position); + } + +} diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/ApiLevelTooLowException.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/ApiLevelTooLowException.java new file mode 100644 index 000000000..5b0a83827 --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/ApiLevelTooLowException.java @@ -0,0 +1,11 @@ +package se.emilsjolander.stickylistheaders; + +public class ApiLevelTooLowException extends RuntimeException { + + private static final long serialVersionUID = -5480068364264456757L; + + public ApiLevelTooLowException(int versionCode) { + super("Requires API level " + versionCode); + } + +} diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/CheckableWrapperView.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/CheckableWrapperView.java new file mode 100644 index 000000000..9039c3f5c --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/CheckableWrapperView.java @@ -0,0 +1,31 @@ +package se.emilsjolander.stickylistheaders; + +import android.content.Context; +import android.widget.Checkable; + +/** + * A WrapperView that implements the checkable interface + * + * @author Emil Sjölander + */ +class CheckableWrapperView extends WrapperView implements Checkable { + + public CheckableWrapperView(final Context context) { + super(context); + } + + @Override + public boolean isChecked() { + return ((Checkable) mItem).isChecked(); + } + + @Override + public void setChecked(final boolean checked) { + ((Checkable) mItem).setChecked(checked); + } + + @Override + public void toggle() { + setChecked(!isChecked()); + } +} diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/SectionIndexerAdapterWrapper.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/SectionIndexerAdapterWrapper.java new file mode 100644 index 000000000..ff7e293af --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/SectionIndexerAdapterWrapper.java @@ -0,0 +1,32 @@ +package se.emilsjolander.stickylistheaders; + +import android.content.Context; +import android.widget.SectionIndexer; + +class SectionIndexerAdapterWrapper extends + AdapterWrapper implements SectionIndexer { + + final SectionIndexer mSectionIndexerDelegate; + + SectionIndexerAdapterWrapper(Context context, + StickyListHeadersAdapter delegate) { + super(context, delegate); + mSectionIndexerDelegate = (SectionIndexer) delegate; + } + + @Override + public int getPositionForSection(int section) { + return mSectionIndexerDelegate.getPositionForSection(section); + } + + @Override + public int getSectionForPosition(int position) { + return mSectionIndexerDelegate.getSectionForPosition(position); + } + + @Override + public Object[] getSections() { + return mSectionIndexerDelegate.getSections(); + } + +} diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersAdapter.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersAdapter.java new file mode 100644 index 000000000..8b80b71f1 --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersAdapter.java @@ -0,0 +1,38 @@ +package se.emilsjolander.stickylistheaders; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListAdapter; + +public interface StickyListHeadersAdapter extends ListAdapter { + /** + * Get a View that displays the header data at the specified position in the + * set. You can either create a View manually or inflate it from an XML layout + * file. + * + * @param position + * The position of the item within the adapter's data set of the item whose + * header view we want. + * @param convertView + * The old view to reuse, if possible. Note: You should check that this view is + * non-null and of an appropriate type before using. If it is not possible to + * convert this view to display the correct data, this method can create a new + * view. + * @param parent + * The parent that this view will eventually be attached to. + * @return + * A View corresponding to the data at the specified position. + */ + View getHeaderView(int position, View convertView, ViewGroup parent); + + /** + * Get the header id associated with the specified position in the list. + * + * @param position + * The position of the item within the adapter's data set whose header id we + * want. + * @return + * The id of the header at the specified position. + */ + long getHeaderId(int position); +} diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersListView.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersListView.java new file mode 100644 index 000000000..476f6cfad --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersListView.java @@ -0,0 +1,925 @@ +package se.emilsjolander.stickylistheaders; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.FrameLayout; +import android.widget.ListView; +import android.widget.SectionIndexer; + +import se.emilsjolander.stickylistheaders.WrapperViewList.LifeCycleListener; + +/** + * Even though this is a FrameLayout subclass we it is called a ListView. This + * is because of 2 reasons. 1. It acts like as ListView 2. It used to be a + * ListView subclass and i did not was to change to name causing compatibility + * errors. + * + * @author Emil Sjölander + */ +public class StickyListHeadersListView extends FrameLayout { + + public interface OnHeaderClickListener { + public void onHeaderClick(StickyListHeadersListView l, View header, + int itemPosition, long headerId, boolean currentlySticky); + } + + /** + * Notifies the listener when the sticky headers top offset has changed. + */ + public interface OnStickyHeaderOffsetChangedListener { + /** + * @param l The view parent + * @param header The currently sticky header being offset. + * This header is not guaranteed to have it's measurements set. + * It is however guaranteed that this view has been measured, + * therefor you should user getMeasured* methods instead of + * get* methods for determining the view's size. + * @param offset The amount the sticky header is offset by towards to top of the screen. + */ + public void onStickyHeaderOffsetChanged(StickyListHeadersListView l, View header, int offset); + } + + /* --- Children --- */ + private WrapperViewList mList; + private View mHeader; + + /* --- Header state --- */ + private Long mHeaderId; + // used to not have to call getHeaderId() all the time + private Integer mHeaderPosition; + private Integer mHeaderOffset; + + /* --- Delegates --- */ + private OnScrollListener mOnScrollListenerDelegate; + private AdapterWrapper mAdapter; + + /* --- Settings --- */ + private boolean mAreHeadersSticky = true; + private boolean mClippingToPadding = true; + private boolean mIsDrawingListUnderStickyHeader = true; + private int mPaddingLeft = 0; + private int mPaddingTop = 0; + private int mPaddingRight = 0; + private int mPaddingBottom = 0; + + /* --- Other --- */ + private OnHeaderClickListener mOnHeaderClickListener; + private OnStickyHeaderOffsetChangedListener mOnStickyHeaderOffsetChangedListener; + private AdapterWrapperDataSetObserver mDataSetObserver; + private Drawable mDivider; + private int mDividerHeight; + + public StickyListHeadersListView(Context context) { + this(context, null); + } + + public StickyListHeadersListView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public StickyListHeadersListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + // Initialize the wrapped list + mList = new WrapperViewList(context); + + // null out divider, dividers are handled by adapter so they look good with headers + mDivider = mList.getDivider(); + mDividerHeight = mList.getDividerHeight(); + mList.setDivider(null); + mList.setDividerHeight(0); + + mList.setVerticalScrollBarEnabled(isVerticalScrollBarEnabled()); + mList.setHorizontalScrollBarEnabled(isHorizontalScrollBarEnabled()); + + if (attrs != null) { + TypedArray a = context.getTheme().obtainStyledAttributes(attrs,R.styleable.StickyListHeadersListView, 0, 0); + + try { + // -- View attributes -- + int padding = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_padding, 0); + mPaddingLeft = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingLeft, padding); + mPaddingTop = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingTop, padding); + mPaddingRight = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingRight, padding); + mPaddingBottom = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingBottom, padding); + + setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, + mPaddingBottom); + + // Set clip to padding on the list and reset value to default on + // wrapper + mClippingToPadding = a.getBoolean(R.styleable.StickyListHeadersListView_android_clipToPadding, true); + super.setClipToPadding(true); + mList.setClipToPadding(mClippingToPadding); + + // -- ListView attributes -- + mList.setFadingEdgeLength(a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_fadingEdgeLength, + mList.getVerticalFadingEdgeLength())); + final int fadingEdge = a.getInt(R.styleable.StickyListHeadersListView_android_requiresFadingEdge, 0); + if (fadingEdge == 0x00001000) { + mList.setVerticalFadingEdgeEnabled(false); + mList.setHorizontalFadingEdgeEnabled(true); + } else if (fadingEdge == 0x00002000) { + mList.setVerticalFadingEdgeEnabled(true); + mList.setHorizontalFadingEdgeEnabled(false); + } else { + mList.setVerticalFadingEdgeEnabled(false); + mList.setHorizontalFadingEdgeEnabled(false); + } + mList.setCacheColorHint(a + .getColor(R.styleable.StickyListHeadersListView_android_cacheColorHint, mList.getCacheColorHint())); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mList.setChoiceMode(a.getInt(R.styleable.StickyListHeadersListView_android_choiceMode, + mList.getChoiceMode())); + } + mList.setDrawSelectorOnTop(a.getBoolean(R.styleable.StickyListHeadersListView_android_drawSelectorOnTop, false)); + mList.setFastScrollEnabled(a.getBoolean(R.styleable.StickyListHeadersListView_android_fastScrollEnabled, + mList.isFastScrollEnabled())); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mList.setFastScrollAlwaysVisible(a.getBoolean( + R.styleable.StickyListHeadersListView_android_fastScrollAlwaysVisible, + mList.isFastScrollAlwaysVisible())); + } + + mList.setScrollBarStyle(a.getInt(R.styleable.StickyListHeadersListView_android_scrollbarStyle, 0)); + + if (a.hasValue(R.styleable.StickyListHeadersListView_android_listSelector)) { + mList.setSelector(a.getDrawable(R.styleable.StickyListHeadersListView_android_listSelector)); + } + + mList.setScrollingCacheEnabled(a.getBoolean(R.styleable.StickyListHeadersListView_android_scrollingCache, + mList.isScrollingCacheEnabled())); + + if (a.hasValue(R.styleable.StickyListHeadersListView_android_divider)) { + mDivider = a.getDrawable(R.styleable.StickyListHeadersListView_android_divider); + } + + mDividerHeight = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_dividerHeight, + mDividerHeight); + + // -- StickyListHeaders attributes -- + mAreHeadersSticky = a.getBoolean(R.styleable.StickyListHeadersListView_hasStickyHeaders, true); + mIsDrawingListUnderStickyHeader = a.getBoolean( + R.styleable.StickyListHeadersListView_isDrawingListUnderStickyHeader, + true); + } finally { + a.recycle(); + } + } + + // attach some listeners to the wrapped list + mList.setLifeCycleListener(new WrapperViewListLifeCycleListener()); + mList.setOnScrollListener(new WrapperListScrollListener()); + + addView(mList); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + measureHeader(mHeader); + } + + private void ensureHeaderHasCorrectLayoutParams(View header) { + ViewGroup.LayoutParams lp = header.getLayoutParams(); + if (lp == null) { + lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } else if (lp.height == LayoutParams.MATCH_PARENT) { + lp.height = LayoutParams.WRAP_CONTENT; + } + header.setLayoutParams(lp); + } + + private void measureHeader(View header) { + if (header != null) { + final int width = getMeasuredWidth() - mPaddingLeft - mPaddingRight; + final int parentWidthMeasureSpec = MeasureSpec.makeMeasureSpec( + width, MeasureSpec.EXACTLY); + final int parentHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, + MeasureSpec.UNSPECIFIED); + measureChild(header, parentWidthMeasureSpec, + parentHeightMeasureSpec); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, + int bottom) { + mList.layout(0, 0, mList.getMeasuredWidth(), getHeight()); + if (mHeader != null) { + MarginLayoutParams lp = (MarginLayoutParams) mHeader + .getLayoutParams(); + int headerTop = lp.topMargin + + (mClippingToPadding ? mPaddingTop : 0); + // The left parameter must for some reason be set to 0. + // I think it should be set to mPaddingLeft but apparently not + mHeader.layout(mPaddingLeft, headerTop, mHeader.getMeasuredWidth() + + mPaddingLeft, headerTop + mHeader.getMeasuredHeight()); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + // Only draw the list here. + // The header should be drawn right after the lists children are drawn. + // This is done so that the header is above the list items + // but below the list decorators (scroll bars etc). + drawChild(canvas, mList, 0); + } + + // Reset values tied the header. also remove header form layout + // This is called in response to the data set or the adapter changing + private void clearHeader() { + if (mHeader != null) { + removeView(mHeader); + mHeader = null; + mHeaderId = null; + mHeaderPosition = null; + mHeaderOffset = null; + + // reset the top clipping length + mList.setTopClippingLength(0); + updateHeaderVisibilities(); + } + } + + private void updateOrClearHeader(int firstVisiblePosition) { + final int adapterCount = mAdapter == null ? 0 : mAdapter.getCount(); + if (adapterCount == 0 || !mAreHeadersSticky) { + return; + } + + final int headerViewCount = mList.getHeaderViewsCount(); + final int realFirstVisibleItem = firstVisiblePosition - headerViewCount; + + // It is not a mistake to call getFirstVisiblePosition() here. + // Most of the time getFixedFirstVisibleItem() should be called + // but that does not work great together with getChildAt() + final boolean doesListHaveChildren = mList.getChildCount() != 0; + final boolean isFirstViewBelowTop = doesListHaveChildren && mList + .getFirstVisiblePosition() == 0 + && mList.getChildAt(0).getTop() > (mClippingToPadding ? mPaddingTop : 0); + final boolean isFirstVisibleItemOutsideAdapterRange = realFirstVisibleItem > adapterCount - 1 + || realFirstVisibleItem < 0; + if (!doesListHaveChildren || isFirstVisibleItemOutsideAdapterRange + || isFirstViewBelowTop) { + clearHeader(); + return; + } + + updateHeader(realFirstVisibleItem); + } + + private void updateHeader(int firstVisiblePosition) { + + // check if there is a new header should be sticky + if (mHeaderPosition == null || mHeaderPosition != firstVisiblePosition) { + mHeaderPosition = firstVisiblePosition; + final long headerId = mAdapter.getHeaderId(firstVisiblePosition); + if (mHeaderId == null || mHeaderId != headerId) { + mHeaderId = headerId; + final View header = mAdapter.getHeaderView(mHeaderPosition, + mHeader, this); + if (mHeader != header) { + if (header == null) { + throw new NullPointerException("header may not be null"); + } + swapHeader(header); + } + + ensureHeaderHasCorrectLayoutParams(mHeader); + measureHeader(mHeader); + + // Reset mHeaderOffset to null ensuring + // that it will be set on the header and + // not skipped for performance reasons. + mHeaderOffset = null; + } + } + + int headerOffset = 0; + + // Calculate new header offset + // Skip looking at the first view. it never matters because it always + // results in a headerOffset = 0 + int headerBottom = mHeader.getMeasuredHeight() + + (mClippingToPadding ? mPaddingTop : 0); + for (int i = 0; i < mList.getChildCount(); i++) { + final View child = mList.getChildAt(i); + final boolean doesChildHaveHeader = child instanceof WrapperView + && ((WrapperView) child).hasHeader(); + final boolean isChildFooter = mList.containsFooterView(child); + if (child.getTop() >= (mClippingToPadding ? mPaddingTop : 0) + && (doesChildHaveHeader || isChildFooter)) { + headerOffset = Math.min(child.getTop() - headerBottom, 0); + break; + } + } + + setHeaderOffet(headerOffset); + + if (!mIsDrawingListUnderStickyHeader) { + mList.setTopClippingLength(mHeader.getMeasuredHeight() + + mHeaderOffset); + } + + updateHeaderVisibilities(); + } + + private void swapHeader(View newHeader) { + if (mHeader != null) { + removeView(mHeader); + } + mHeader = newHeader; + addView(mHeader); + mHeader.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (mOnHeaderClickListener != null) { + mOnHeaderClickListener.onHeaderClick( + StickyListHeadersListView.this, mHeader, + mHeaderPosition, mHeaderId, true); + } + } + + }); + } + + // hides the headers in the list under the sticky header. + // Makes sure the other ones are showing + private void updateHeaderVisibilities() { + int top; + if (mHeader != null) { + top = mHeader.getMeasuredHeight() + + (mHeaderOffset != null ? mHeaderOffset : 0); + } else { + top = mClippingToPadding ? mPaddingTop : 0; + } + int childCount = mList.getChildCount(); + for (int i = 0; i < childCount; i++) { + + // ensure child is a wrapper view + View child = mList.getChildAt(i); + if (!(child instanceof WrapperView)) { + continue; + } + + // ensure wrapper view child has a header + WrapperView wrapperViewChild = (WrapperView) child; + if (!wrapperViewChild.hasHeader()) { + continue; + } + + // update header views visibility + View childHeader = wrapperViewChild.mHeader; + if (wrapperViewChild.getTop() < top) { + if (childHeader.getVisibility() != View.INVISIBLE) { + childHeader.setVisibility(View.INVISIBLE); + } + } else { + if (childHeader.getVisibility() != View.VISIBLE) { + childHeader.setVisibility(View.VISIBLE); + } + } + } + } + + // Wrapper around setting the header offset in different ways depending on + // the API version + @SuppressLint("NewApi") + private void setHeaderOffet(int offset) { + if (mHeaderOffset == null || mHeaderOffset != offset) { + mHeaderOffset = offset; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mHeader.setTranslationY(mHeaderOffset); + } else { + MarginLayoutParams params = (MarginLayoutParams) mHeader + .getLayoutParams(); + params.topMargin = mHeaderOffset; + mHeader.setLayoutParams(params); + } + if (mOnStickyHeaderOffsetChangedListener != null) { + mOnStickyHeaderOffsetChangedListener + .onStickyHeaderOffsetChanged(this, mHeader, -mHeaderOffset); + } + } + } + + private class AdapterWrapperDataSetObserver extends DataSetObserver { + + @Override + public void onChanged() { + clearHeader(); + } + + @Override + public void onInvalidated() { + clearHeader(); + } + + } + + private class WrapperListScrollListener implements OnScrollListener { + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + if (mOnScrollListenerDelegate != null) { + mOnScrollListenerDelegate.onScroll(view, firstVisibleItem, + visibleItemCount, totalItemCount); + } + updateOrClearHeader(mList.getFixedFirstVisibleItem()); + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (mOnScrollListenerDelegate != null) { + mOnScrollListenerDelegate.onScrollStateChanged(view, + scrollState); + } + } + + } + + private class WrapperViewListLifeCycleListener implements LifeCycleListener { + + @Override + public void onDispatchDrawOccurred(Canvas canvas) { + // onScroll is not called often at all before froyo + // therefor we need to update the header here as well. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) { + updateOrClearHeader(mList.getFixedFirstVisibleItem()); + } + if (mHeader != null) { + if (mClippingToPadding) { + canvas.save(); + canvas.clipRect(0, mPaddingTop, getRight(), getBottom()); + drawChild(canvas, mHeader, 0); + canvas.restore(); + } else { + drawChild(canvas, mHeader, 0); + } + } + } + + } + + private class AdapterWrapperHeaderClickHandler implements + AdapterWrapper.OnHeaderClickListener { + + @Override + public void onHeaderClick(View header, int itemPosition, long headerId) { + mOnHeaderClickListener.onHeaderClick( + StickyListHeadersListView.this, header, itemPosition, + headerId, false); + } + + } + + private boolean isStartOfSection(int position) { + return position == 0 + || mAdapter.getHeaderId(position) != mAdapter + .getHeaderId(position - 1); + } + + private int getHeaderOverlap(int position) { + boolean isStartOfSection = isStartOfSection(position); + if (!isStartOfSection) { + View header = mAdapter.getHeaderView(position, null, mList); + if (header == null) { + throw new NullPointerException("header may not be null"); + } + ensureHeaderHasCorrectLayoutParams(header); + measureHeader(header); + return header.getMeasuredHeight(); + } + return 0; + } + + /* ---------- StickyListHeaders specific API ---------- */ + + public void setAreHeadersSticky(boolean areHeadersSticky) { + mAreHeadersSticky = areHeadersSticky; + if (!areHeadersSticky) { + clearHeader(); + } else { + updateOrClearHeader(mList.getFixedFirstVisibleItem()); + } + // invalidating the list will trigger dispatchDraw() + mList.invalidate(); + } + + public boolean areHeadersSticky() { + return mAreHeadersSticky; + } + + /** + * Use areHeadersSticky() method instead + */ + @Deprecated + public boolean getAreHeadersSticky() { + return areHeadersSticky(); + } + + public void setDrawingListUnderStickyHeader( + boolean drawingListUnderStickyHeader) { + mIsDrawingListUnderStickyHeader = drawingListUnderStickyHeader; + // reset the top clipping length + mList.setTopClippingLength(0); + } + + public boolean isDrawingListUnderStickyHeader() { + return mIsDrawingListUnderStickyHeader; + } + + public void setOnHeaderClickListener(OnHeaderClickListener listener) { + mOnHeaderClickListener = listener; + if (mAdapter != null) { + if (mOnHeaderClickListener != null) { + mAdapter.setOnHeaderClickListener(new AdapterWrapperHeaderClickHandler()); + } else { + mAdapter.setOnHeaderClickListener(null); + } + } + } + + public void setOnStickyHeaderOffsetChangedListener(OnStickyHeaderOffsetChangedListener listener) { + mOnStickyHeaderOffsetChangedListener = listener; + } + + public View getListChildAt(int index) { + return mList.getChildAt(index); + } + + public int getListChildCount() { + return mList.getChildCount(); + } + + /** + * Use the method with extreme caution!! Changing any values on the + * underlying ListView might break everything. + * + * @return the ListView backing this view. + */ + public ListView getWrappedList() { + return mList; + } + + /* ---------- ListView delegate methods ---------- */ + + public void setAdapter(StickyListHeadersAdapter adapter) { + if (adapter == null) { + mList.setAdapter(null); + clearHeader(); + return; + } + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mDataSetObserver); + } + + if (adapter instanceof SectionIndexer) { + mAdapter = new SectionIndexerAdapterWrapper(getContext(), adapter); + } else { + mAdapter = new AdapterWrapper(getContext(), adapter); + } + mDataSetObserver = new AdapterWrapperDataSetObserver(); + mAdapter.registerDataSetObserver(mDataSetObserver); + + if (mOnHeaderClickListener != null) { + mAdapter.setOnHeaderClickListener(new AdapterWrapperHeaderClickHandler()); + } else { + mAdapter.setOnHeaderClickListener(null); + } + + mAdapter.setDivider(mDivider, mDividerHeight); + + mList.setAdapter(mAdapter); + clearHeader(); + } + + public StickyListHeadersAdapter getAdapter() { + return mAdapter == null ? null : mAdapter.mDelegate; + } + + public void setDivider(Drawable divider) { + mDivider = divider; + if (mAdapter != null) { + mAdapter.setDivider(mDivider, mDividerHeight); + } + } + + public void setDividerHeight(int dividerHeight) { + mDividerHeight = dividerHeight; + if (mAdapter != null) { + mAdapter.setDivider(mDivider, mDividerHeight); + } + } + + public Drawable getDivider() { + return mDivider; + } + + public int getDividerHeight() { + return mDividerHeight; + } + + public void setOnScrollListener(OnScrollListener onScrollListener) { + mOnScrollListenerDelegate = onScrollListener; + } + + public void setOnItemClickListener(OnItemClickListener listener) { + mList.setOnItemClickListener(listener); + } + + public void setOnItemLongClickListener(OnItemLongClickListener listener) { + mList.setOnItemLongClickListener(listener); + } + + public void addHeaderView(View v, Object data, boolean isSelectable) { + mList.addHeaderView(v, data, isSelectable); + } + + public void addHeaderView(View v) { + mList.addHeaderView(v); + } + + public void removeHeaderView(View v) { + mList.removeHeaderView(v); + } + + public int getHeaderViewsCount() { + return mList.getHeaderViewsCount(); + } + + public void addFooterView(View v) { + mList.addFooterView(v); + } + + public void removeFooterView(View v) { + mList.removeFooterView(v); + } + + public int getFooterViewsCount() { + return mList.getFooterViewsCount(); + } + + public void setEmptyView(View v) { + mList.setEmptyView(v); + } + + public View getEmptyView() { + return mList.getEmptyView(); + } + + @Override + public void setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled) { + mList.setVerticalScrollBarEnabled(verticalScrollBarEnabled); + } + + @Override + public void setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled) { + mList.setHorizontalScrollBarEnabled(horizontalScrollBarEnabled); + } + + @TargetApi(Build.VERSION_CODES.FROYO) + public void smoothScrollBy(int distance, int duration) { + requireSdkVersion(Build.VERSION_CODES.FROYO); + mList.smoothScrollBy(distance, duration); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void smoothScrollByOffset(int offset) { + requireSdkVersion(Build.VERSION_CODES.HONEYCOMB); + mList.smoothScrollByOffset(offset); + } + + @SuppressLint("NewApi") + @TargetApi(Build.VERSION_CODES.FROYO) + public void smoothScrollToPosition(int position) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + mList.smoothScrollToPosition(position); + } else { + int offset = mAdapter == null ? 0 : getHeaderOverlap(position); + offset -= mClippingToPadding ? 0 : mPaddingTop; + mList.smoothScrollToPositionFromTop(position, offset); + } + } + + @TargetApi(Build.VERSION_CODES.FROYO) + public void smoothScrollToPosition(int position, int boundPosition) { + requireSdkVersion(Build.VERSION_CODES.FROYO); + mList.smoothScrollToPosition(position, boundPosition); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void smoothScrollToPositionFromTop(int position, int offset) { + requireSdkVersion(Build.VERSION_CODES.HONEYCOMB); + offset += mAdapter == null ? 0 : getHeaderOverlap(position); + offset -= mClippingToPadding ? 0 : mPaddingTop; + mList.smoothScrollToPositionFromTop(position, offset); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void smoothScrollToPositionFromTop(int position, int offset, + int duration) { + requireSdkVersion(Build.VERSION_CODES.HONEYCOMB); + offset += mAdapter == null ? 0 : getHeaderOverlap(position); + offset -= mClippingToPadding ? 0 : mPaddingTop; + mList.smoothScrollToPositionFromTop(position, offset, duration); + } + + public void setSelection(int position) { + setSelectionFromTop(position, 0); + } + + public void setSelectionAfterHeaderView() { + mList.setSelectionAfterHeaderView(); + } + + public void setSelectionFromTop(int position, int y) { + y += mAdapter == null ? 0 : getHeaderOverlap(position); + y -= mClippingToPadding ? 0 : mPaddingTop; + mList.setSelectionFromTop(position, y); + } + + public void setSelector(Drawable sel) { + mList.setSelector(sel); + } + + public void setSelector(int resID) { + mList.setSelector(resID); + } + + public int getFirstVisiblePosition() { + return mList.getFirstVisiblePosition(); + } + + public int getLastVisiblePosition() { + return mList.getLastVisiblePosition(); + } + + public void setChoiceMode(int choiceMode) { + mList.setChoiceMode(choiceMode); + } + + public void setItemChecked(int position, boolean value) { + mList.setItemChecked(position, value); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public int getCheckedItemCount() { + requireSdkVersion(Build.VERSION_CODES.HONEYCOMB); + return mList.getCheckedItemCount(); + } + + @TargetApi(Build.VERSION_CODES.FROYO) + public long[] getCheckedItemIds() { + requireSdkVersion(Build.VERSION_CODES.FROYO); + return mList.getCheckedItemIds(); + } + + public int getCheckedItemPosition() { + return mList.getCheckedItemPosition(); + } + + public SparseBooleanArray getCheckedItemPositions() { + return mList.getCheckedItemPositions(); + } + + public int getCount() { + return mList.getCount(); + } + + public Object getItemAtPosition(int position) { + return mList.getItemAtPosition(position); + } + + public long getItemIdAtPosition(int position) { + return mList.getItemIdAtPosition(position); + } + + @Override + public void setOnCreateContextMenuListener(OnCreateContextMenuListener l) { + mList.setOnCreateContextMenuListener(l); + } + + @Override + public boolean showContextMenu() { + return mList.showContextMenu(); + } + + public void invalidateViews() { + mList.invalidateViews(); + } + + @Override + public void setClipToPadding(boolean clipToPadding) { + if (mList != null) { + mList.setClipToPadding(clipToPadding); + } + mClippingToPadding = clipToPadding; + } + + @Override + public void setPadding(int left, int top, int right, int bottom) { + mPaddingLeft = left; + mPaddingTop = top; + mPaddingRight = right; + mPaddingBottom = bottom; + + if (mList != null) { + mList.setPadding(left, top, right, bottom); + } + super.setPadding(0, 0, 0, 0); + requestLayout(); + } + + /* + * Overrides an @hide method in View + */ + protected void recomputePadding() { + setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom); + } + + @Override + public int getPaddingLeft() { + return mPaddingLeft; + } + + @Override + public int getPaddingTop() { + return mPaddingTop; + } + + @Override + public int getPaddingRight() { + return mPaddingRight; + } + + @Override + public int getPaddingBottom() { + return mPaddingBottom; + } + + public void setFastScrollEnabled(boolean fastScrollEnabled) { + mList.setFastScrollEnabled(fastScrollEnabled); + } + + /** + * @throws ApiLevelTooLowException on pre-Honeycomb device. + * @see android.widget.AbsListView#setFastScrollAlwaysVisible(boolean) + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void setFastScrollAlwaysVisible(boolean alwaysVisible) { + requireSdkVersion(Build.VERSION_CODES.HONEYCOMB); + mList.setFastScrollAlwaysVisible(alwaysVisible); + } + + /** + * @return true if the fast scroller will always show. False on pre-Honeycomb devices. + * @see android.widget.AbsListView#isFastScrollAlwaysVisible() + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public boolean isFastScrollAlwaysVisible() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + return false; + } + return mList.isFastScrollAlwaysVisible(); + } + + public void setScrollBarStyle(int style) { + mList.setScrollBarStyle(style); + } + + public int getScrollBarStyle() { + return mList.getScrollBarStyle(); + } + + private void requireSdkVersion(int versionCode) { + if (Build.VERSION.SDK_INT < versionCode) { + throw new ApiLevelTooLowException(versionCode); + } + } + + public int getPositionForView(View view) { + return mList.getPositionForView(view); + } + +} diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperView.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperView.java new file mode 100644 index 000000000..f51416c1c --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperView.java @@ -0,0 +1,150 @@ +package se.emilsjolander.stickylistheaders; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +/** + * + * the view that wrapps a divider header and a normal list item. The listview sees this as 1 item + * + * @author Emil Sjölander + */ +public class WrapperView extends ViewGroup { + + View mItem; + Drawable mDivider; + int mDividerHeight; + View mHeader; + int mItemTop; + + WrapperView(Context c) { + super(c); + } + + public boolean hasHeader() { + return mHeader != null; + } + + public View getItem() { + return mItem; + } + + public View getHeader() { + return mHeader; + } + + void update(View item, View header, Drawable divider, int dividerHeight) { + + //every wrapperview must have a list item + if (item == null) { + throw new NullPointerException("List view item must not be null."); + } + + //only remove the current item if it is not the same as the new item. this can happen if wrapping a recycled view + if (this.mItem != item) { + removeView(this.mItem); + this.mItem = item; + final ViewParent parent = item.getParent(); + if(parent != null && parent != this) { + if(parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(item); + } + } + addView(item); + } + + //same logik as above but for the header + if (this.mHeader != header) { + if (this.mHeader != null) { + removeView(this.mHeader); + } + this.mHeader = header; + if (header != null) { + addView(header); + } + } + + if (this.mDivider != divider) { + this.mDivider = divider; + this.mDividerHeight = dividerHeight; + invalidate(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); + int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, + MeasureSpec.EXACTLY); + int measuredHeight = 0; + + //measure header or divider. when there is a header visible it acts as the divider + if (mHeader != null) { + ViewGroup.LayoutParams params = mHeader.getLayoutParams(); + if (params != null && params.height > 0) { + mHeader.measure(childWidthMeasureSpec, + MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY)); + } else { + mHeader.measure(childWidthMeasureSpec, + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + } + measuredHeight += mHeader.getMeasuredHeight(); + } else if (mDivider != null) { + measuredHeight += mDividerHeight; + } + + //measure item + ViewGroup.LayoutParams params = mItem.getLayoutParams(); + if (params != null && params.height > 0) { + mItem.measure(childWidthMeasureSpec, + MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY)); + } else { + mItem.measure(childWidthMeasureSpec, + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + } + measuredHeight += mItem.getMeasuredHeight(); + + setMeasuredDimension(measuredWidth, measuredHeight); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + + l = 0; + t = 0; + r = getWidth(); + b = getHeight(); + + if (mHeader != null) { + int headerHeight = mHeader.getMeasuredHeight(); + mHeader.layout(l, t, r, headerHeight); + mItemTop = headerHeight; + mItem.layout(l, headerHeight, r, b); + } else if (mDivider != null) { + mDivider.setBounds(l, t, r, mDividerHeight); + mItemTop = mDividerHeight; + mItem.layout(l, mDividerHeight, r, b); + } else { + mItemTop = t; + mItem.layout(l, t, r, b); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + if (mHeader == null && mDivider != null) { + // Drawable.setBounds() does not seem to work pre-honeycomb. So have + // to do this instead + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + canvas.clipRect(0, 0, getWidth(), mDividerHeight); + } + mDivider.draw(canvas); + } + } +} diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperViewList.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperViewList.java new file mode 100644 index 000000000..3d68e98db --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperViewList.java @@ -0,0 +1,175 @@ +package se.emilsjolander.stickylistheaders; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Build; +import android.view.View; +import android.widget.AbsListView; +import android.widget.ListView; + +class WrapperViewList extends ListView { + + interface LifeCycleListener { + void onDispatchDrawOccurred(Canvas canvas); + } + + private LifeCycleListener mLifeCycleListener; + private List mFooterViews; + private int mTopClippingLength; + private Rect mSelectorRect = new Rect();// for if reflection fails + private Field mSelectorPositionField; + private boolean mClippingToPadding = true; + + public WrapperViewList(Context context) { + super(context); + + // Use reflection to be able to change the size/position of the list + // selector so it does not come under/over the header + try { + Field selectorRectField = AbsListView.class.getDeclaredField("mSelectorRect"); + selectorRectField.setAccessible(true); + mSelectorRect = (Rect) selectorRectField.get(this); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + mSelectorPositionField = AbsListView.class.getDeclaredField("mSelectorPosition"); + mSelectorPositionField.setAccessible(true); + } + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + @Override + public boolean performItemClick(View view, int position, long id) { + if (view instanceof WrapperView) { + view = ((WrapperView) view).mItem; + } + return super.performItemClick(view, position, id); + } + + private void positionSelectorRect() { + if (!mSelectorRect.isEmpty()) { + int selectorPosition = getSelectorPosition(); + if (selectorPosition >= 0) { + int firstVisibleItem = getFixedFirstVisibleItem(); + View v = getChildAt(selectorPosition - firstVisibleItem); + if (v instanceof WrapperView) { + WrapperView wrapper = ((WrapperView) v); + mSelectorRect.top = wrapper.getTop() + wrapper.mItemTop; + } + } + } + } + + private int getSelectorPosition() { + if (mSelectorPositionField == null) { // not all supported andorid + // version have this variable + for (int i = 0; i < getChildCount(); i++) { + if (getChildAt(i).getBottom() == mSelectorRect.bottom) { + return i + getFixedFirstVisibleItem(); + } + } + } else { + try { + return mSelectorPositionField.getInt(this); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + return -1; + } + + @Override + protected void dispatchDraw(Canvas canvas) { + positionSelectorRect(); + if (mTopClippingLength != 0) { + canvas.save(); + Rect clipping = canvas.getClipBounds(); + clipping.top = mTopClippingLength; + canvas.clipRect(clipping); + super.dispatchDraw(canvas); + canvas.restore(); + } else { + super.dispatchDraw(canvas); + } + mLifeCycleListener.onDispatchDrawOccurred(canvas); + } + + void setLifeCycleListener(LifeCycleListener lifeCycleListener) { + mLifeCycleListener = lifeCycleListener; + } + + @Override + public void addFooterView(View v) { + super.addFooterView(v); + if (mFooterViews == null) { + mFooterViews = new ArrayList(); + } + mFooterViews.add(v); + } + + @Override + public boolean removeFooterView(View v) { + if (super.removeFooterView(v)) { + mFooterViews.remove(v); + return true; + } + return false; + } + + boolean containsFooterView(View v) { + if (mFooterViews == null) { + return false; + } + return mFooterViews.contains(v); + } + + void setTopClippingLength(int topClipping) { + mTopClippingLength = topClipping; + } + + int getFixedFirstVisibleItem() { + int firstVisibleItem = getFirstVisiblePosition(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + return firstVisibleItem; + } + + // first getFirstVisiblePosition() reports items + // outside the view sometimes on old versions of android + for (int i = 0; i < getChildCount(); i++) { + if (getChildAt(i).getBottom() >= 0) { + firstVisibleItem += i; + break; + } + } + + // work around to fix bug with firstVisibleItem being to high + // because list view does not take clipToPadding=false into account + // on old versions of android + if (!mClippingToPadding && getPaddingTop() > 0 && firstVisibleItem > 0) { + if (getChildAt(0).getTop() > 0) { + firstVisibleItem -= 1; + } + } + + return firstVisibleItem; + } + + @Override + public void setClipToPadding(boolean clipToPadding) { + mClippingToPadding = clipToPadding; + super.setClipToPadding(clipToPadding); + } + +} -- cgit v1.2.3