New list with sticky list headers library
This commit is contained in:
@@ -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<View> mHeaderCache = new LinkedList<View>();
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<View> 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<View>();
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user