package com.emilsjolander.components.stickylistheaders;
import java.util.ArrayList;
import android.annotation.SuppressLint;
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.SparseBooleanArray;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.SectionIndexer;
/**
* @author Emil Sjölander
*/
@SuppressLint("NewApi")
public class StickyListHeadersListView extends ListView implements
OnScrollListener, OnClickListener {
public interface OnHeaderClickListener {
public void onHeaderClick(StickyListHeadersListView l, View header,
int itemPosition, long headerId, boolean currentlySticky);
}
private OnScrollListener scrollListener;
private boolean areHeadersSticky = true;
private int dividerHeight;
private Drawable divider;
private boolean clippingToPadding;
private boolean clipToPaddingHasBeenSet;
private Long currentHeaderId = null;
private StickyListHeadersAdapterWrapper adapter;
private OnHeaderClickListener onHeaderClickListener;
private int headerPosition;
private ArrayList<View> footerViews;
private StickyListHeadersListViewWrapper frame;
private boolean drawingListUnderStickyHeader = true;
private boolean dataChanged = false;
private boolean drawSelectorOnTop;
private OnItemLongClickListener onItemLongClickListenerDelegate;
private MultiChoiceModeListener multiChoiceModeListenerDelegate;
private DataSetObserver dataSetChangedObserver = new DataSetObserver() {
@Override
public void onChanged() {
dataChanged = true;
currentHeaderId = null;
}
@Override
public void onInvalidated() {
currentHeaderId = null;
frame.removeHeader();
}
};
private OnItemLongClickListener onItemLongClickListenerWrapper = new OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> l, View v, int position,
long id) {
if (onItemLongClickListenerDelegate != null) {
return onItemLongClickListenerDelegate.onItemLongClick(l, v,
adapter.translateListViewPosition(position), id);
}
return false;
}
};
private MultiChoiceModeListener multiChoiceModeListenerWrapper;
public StickyListHeadersListView(Context context) {
this(context, null);
}
public StickyListHeadersListView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.listViewStyle);
}
public StickyListHeadersListView(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
super.setOnScrollListener(this);
// null out divider, dividers are handled by adapter so they look good
// with headers
super.setDivider(null);
super.setDividerHeight(0);
setVerticalFadingEdgeEnabled(false);
int[] attrsArray = new int[] { android.R.attr.drawSelectorOnTop };
TypedArray a = context.obtainStyledAttributes(attrs, attrsArray,
defStyle, 0);
drawSelectorOnTop = a.getBoolean(0, false);
a.recycle();
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
setMultiChoiceModeListenerWrapper();
}
}
private void setMultiChoiceModeListenerWrapper() {
multiChoiceModeListenerWrapper = new MultiChoiceModeListener() {
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
if (multiChoiceModeListenerDelegate != null) {
return multiChoiceModeListenerDelegate.onPrepareActionMode(
mode, menu);
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
if (multiChoiceModeListenerDelegate != null) {
multiChoiceModeListenerDelegate.onDestroyActionMode(mode);
}
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
if (multiChoiceModeListenerDelegate != null) {
return multiChoiceModeListenerDelegate.onCreateActionMode(mode,
menu);
}
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (multiChoiceModeListenerDelegate != null) {
return multiChoiceModeListenerDelegate.onActionItemClicked(
mode, item);
}
return false;
}
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position,
long id, boolean checked) {
if (multiChoiceModeListenerDelegate != null) {
position = adapter.translateListViewPosition(position);
multiChoiceModeListenerDelegate.onItemCheckedStateChanged(mode,
position, id, checked);
}
}
};
}
@SuppressWarnings("deprecation")
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (frame == null) {
ViewGroup parent = ((ViewGroup) getParent());
int listIndex = parent.indexOfChild(this);
parent.removeView(this);
frame = new StickyListHeadersListViewWrapper(getContext());
frame.setSelector(getSelector());
frame.setDrawSelectorOnTop(drawSelectorOnTop);
ViewGroup.MarginLayoutParams p = (MarginLayoutParams) getLayoutParams();
if (clippingToPadding) {
frame.setPadding(0, getPaddingTop(), 0, getPaddingBottom());
setPadding(getPaddingLeft(), 0, getPaddingRight(), 0);
}
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
setLayoutParams(params);
frame.addView(this);
frame.setBackgroundDrawable(getBackground());
super.setBackgroundDrawable(null);
frame.setLayoutParams(p);
parent.addView(frame, listIndex);
}
}
@Override
@Deprecated
public void setBackgroundDrawable(Drawable background) {
if (frame != null) {
frame.setBackgroundDrawable(background);
} else {
super.setBackgroundDrawable(background);
}
}
@Override
public void setDrawSelectorOnTop(boolean onTop) {
super.setDrawSelectorOnTop(onTop);
drawSelectorOnTop = onTop;
if (frame != null) {
frame.setDrawSelectorOnTop(drawSelectorOnTop);
}
}
@Override
public boolean performItemClick(View view, int position, long id) {
OnItemClickListener listener = getOnItemClickListener();
int headerViewsCount = getHeaderViewsCount();
final int viewType = adapter.getItemViewType(position
- headerViewsCount);
if (viewType == adapter.headerViewType) {
if (onHeaderClickListener != null) {
position = adapter.translateListViewPosition(position
- headerViewsCount);
onHeaderClickListener.onHeaderClick(this, view, position, id,
false);
return true;
}
return false;
} else if (viewType == adapter.dividerViewType) {
return false;
} else {
if (listener != null) {
if (position >= adapter.getCount()) {
position -= adapter.getHeaderCount();
} else if (!(position < headerViewsCount)) {
position = adapter.translateListViewPosition(position
- headerViewsCount)
+ headerViewsCount;
}
listener.onItemClick(this, view, position, id);
return true;
}
return false;
}
}
@Override
public void setOnItemLongClickListener(OnItemLongClickListener listener) {
onItemLongClickListenerDelegate = listener;
if (listener == null) {
super.setOnItemLongClickListener(null);
} else {
super.setOnItemLongClickListener(onItemLongClickListenerWrapper);
}
}
@Override
public Object getItemAtPosition(int position) {
if(isCalledFromSuper()){
return super.getItemAtPosition(position);
}else{
return (adapter == null || position < 0) ? null : adapter.delegate
.getItem(position);
}
}
@Override
public long getItemIdAtPosition(int position) {
if(isCalledFromSuper()){
return super.getItemIdAtPosition(position);
}else{
return (adapter == null || position < 0) ? ListView.INVALID_ROW_ID
: adapter.delegate.getItemId(position);
}
}
private boolean isCalledFromSuper() {
// i feel dirty...
// could not think if better way, need to translate positions when not
// called from super
StackTraceElement callingFrame = Thread.currentThread().getStackTrace()[5];
return callingFrame.getClassName().contains(
"android.widget.AbsListView");
}
@Override
public void setItemChecked(int position, boolean value) {
if (!isCalledFromSuper()) {
position = adapter.translateAdapterPosition(position);
}
// only real items are checkable
int viewtype = adapter.getItemViewType(position);
if (viewtype != adapter.dividerViewType
&& viewtype != adapter.headerViewType) {
super.setItemChecked(position, value);
}
}
@Override
public boolean isItemChecked(int position) {
if (!isCalledFromSuper()) {
position = adapter.translateAdapterPosition(position);
}
return super.isItemChecked(position);
}
@Override
public int getCheckedItemPosition() {
int position = super.getCheckedItemPosition();
if (!isCalledFromSuper() && position != ListView.INVALID_POSITION) {
position = adapter.translateAdapterPosition(position);
}
return position;
}
@Override
public SparseBooleanArray getCheckedItemPositions() {
SparseBooleanArray superCheckeditems = super.getCheckedItemPositions();
if (!isCalledFromSuper() && superCheckeditems != null) {
SparseBooleanArray checkeditems = new SparseBooleanArray(superCheckeditems.size());
for(int i = 0 ; i<superCheckeditems.size() ; i++){
int key = adapter.translateListViewPosition(superCheckeditems.keyAt(i));
boolean value = superCheckeditems.valueAt(i);
checkeditems.put(key, value);
}
return checkeditems;
}
return superCheckeditems;
}
@Override
public void setMultiChoiceModeListener(MultiChoiceModeListener listener) {
multiChoiceModeListenerDelegate = listener;
if (listener == null) {
super.setMultiChoiceModeListener(null);
} else {
super.setMultiChoiceModeListener(multiChoiceModeListenerWrapper);
}
}
/**
* can only be set to false if headers are sticky, not compatible with
* fading edges
*/
@Override
public void setVerticalFadingEdgeEnabled(boolean verticalFadingEdgeEnabled) {
if (areHeadersSticky) {
super.setVerticalFadingEdgeEnabled(false);
} else {
super.setVerticalFadingEdgeEnabled(verticalFadingEdgeEnabled);
}
}
@Override
public void setDivider(Drawable divider) {
this.divider = divider;
if (divider != null) {
int dividerDrawableHeight = divider.getIntrinsicHeight();
if (dividerDrawableHeight >= 0) {
setDividerHeight(dividerDrawableHeight);
}
}
if (adapter != null) {
adapter.setDivider(divider);
requestLayout();
invalidate();
}
}
@Override
public void setDividerHeight(int height) {
dividerHeight = height;
if (adapter != null) {
adapter.setDividerHeight(height);
requestLayout();
invalidate();
}
}
@Override
public void setOnScrollListener(OnScrollListener l) {
scrollListener = l;
}
public void setAreHeadersSticky(boolean areHeadersSticky) {
if (this.areHeadersSticky != areHeadersSticky) {
if (areHeadersSticky) {
super.setVerticalFadingEdgeEnabled(false);
}
requestLayout();
this.areHeadersSticky = areHeadersSticky;
}
}
public boolean getAreHeadersSticky() {
return areHeadersSticky;
}
@Override
public void setAdapter(ListAdapter adapter) {
if (!clipToPaddingHasBeenSet) {
clippingToPadding = true;
}
if (adapter != null && !(adapter instanceof StickyListHeadersAdapter)) {
throw new IllegalArgumentException(
"Adapter must implement StickyListHeadersAdapter");
}
if (this.adapter != null) {
this.adapter.unregisterDataSetObserver(dataSetChangedObserver);
this.adapter = null;
}
if (adapter != null) {
if (adapter instanceof SectionIndexer) {
this.adapter = new StickyListHeadersSectionIndexerAdapterWrapper(
getContext(), (StickyListHeadersAdapter) adapter);
} else {
this.adapter = new StickyListHeadersAdapterWrapper(
getContext(), (StickyListHeadersAdapter) adapter);
}
this.adapter.setDivider(divider);
this.adapter.setDividerHeight(dividerHeight);
this.adapter.registerDataSetObserver(dataSetChangedObserver);
}
currentHeaderId = null;
super.setAdapter(this.adapter);
}
public StickyListHeadersAdapter getWrappedAdapter() {
if (adapter != null) {
return adapter.getDelegate();
}
return null;
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
post(new Runnable() {
@Override
public void run() {
scrollChanged(getFirstVisiblePosition());
}
});
}
if (!drawingListUnderStickyHeader) {
canvas.clipRect(0, Math.max(frame.getHeaderBottomPosition(), 0),
canvas.getWidth(), canvas.getHeight());
}
super.dispatchDraw(canvas);
}
@Override
public void setClipToPadding(boolean clipToPadding) {
super.setClipToPadding(clipToPadding);
clippingToPadding = clipToPadding;
clipToPaddingHasBeenSet = true;
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (scrollListener != null) {
scrollListener.onScroll(view, firstVisibleItem, visibleItemCount,
totalItemCount);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
scrollChanged(firstVisibleItem);
}
}
private void scrollChanged(int firstVisibleItem) {
if (adapter == null) {
return;
}
int adapterCount = adapter.getCount();
if (adapterCount == 0 || !areHeadersSticky) {
return;
}
final int listViewHeaderCount = getHeaderViewsCount();
firstVisibleItem = getFixedFirstVisibleItem(firstVisibleItem)
- listViewHeaderCount;
if (firstVisibleItem < 0 || firstVisibleItem > adapterCount - 1) {
if (currentHeaderId != null || dataChanged) {
currentHeaderId = null;
frame.removeHeader();
updateHeaderVisibilities();
invalidate();
dataChanged = false;
}
return;
}
boolean headerHasChanged = false;
long newHeaderId = adapter.getHeaderId(firstVisibleItem);
if (currentHeaderId == null || currentHeaderId != newHeaderId) {
headerPosition = firstVisibleItem;
View header = adapter.getHeaderView(headerPosition,
frame.removeHeader(), frame);
header.setOnClickListener(this);
frame.setHeader(header);
headerHasChanged = true;
}
currentHeaderId = newHeaderId;
int childCount = getChildCount();
if (childCount > 0) {
View viewToWatch = null;
int watchingChildDistance = Integer.MAX_VALUE;
boolean viewToWatchIsFooter = false;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
boolean childIsFooter = footerViews != null
&& footerViews.contains(child);
int childDistance;
if (clippingToPadding) {
childDistance = child.getTop() - getPaddingTop();
} else {
childDistance = child.getTop();
}
if (childDistance < 0) {
continue;
}
if (viewToWatch == null
|| (!viewToWatchIsFooter && !adapter
.isHeader(viewToWatch))
|| ((childIsFooter || adapter.isHeader(child)) && childDistance < watchingChildDistance)) {
viewToWatch = child;
viewToWatchIsFooter = childIsFooter;
watchingChildDistance = childDistance;
}
}
int headerHeight = frame.getHeaderHeight();
int headerBottomPosition = 0;
if (viewToWatch != null
&& (viewToWatchIsFooter || adapter.isHeader(viewToWatch))) {
if (firstVisibleItem == listViewHeaderCount
&& getChildAt(0).getTop() > 0 && !clippingToPadding) {
headerBottomPosition = 0;
} else {
if (clippingToPadding) {
headerBottomPosition = Math.min(viewToWatch.getTop(),
headerHeight + getPaddingTop());
headerBottomPosition = headerBottomPosition < getPaddingTop() ? headerHeight
+ getPaddingTop()
: headerBottomPosition;
} else {
headerBottomPosition = Math.min(viewToWatch.getTop(),
headerHeight);
headerBottomPosition = headerBottomPosition < 0 ? headerHeight
: headerBottomPosition;
}
}
} else {
headerBottomPosition = headerHeight;
if (clippingToPadding) {
headerBottomPosition += getPaddingTop();
}
}
if (frame.getHeaderBottomPosition() != headerBottomPosition
|| headerHasChanged) {
frame.setHeaderBottomPosition(headerBottomPosition);
}
updateHeaderVisibilities();
}
}
@Override
public void setSelector(Drawable sel) {
super.setSelector(sel);
if (frame != null) {
frame.setSelector(sel);
}
}
@Override
public void addFooterView(View v) {
super.addFooterView(v);
if (footerViews == null) {
footerViews = new ArrayList<View>();
}
footerViews.add(v);
}
@Override
public boolean removeFooterView(View v) {
boolean removed = super.removeFooterView(v);
if (removed) {
footerViews.remove(v);
}
return removed;
}
private void updateHeaderVisibilities() {
int top = clippingToPadding ? getPaddingTop() : 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (adapter.isHeader(child)) {
if (child.getTop() < top) {
if (child.getVisibility() != View.INVISIBLE) {
child.setVisibility(View.INVISIBLE);
}
} else {
if (child.getVisibility() != View.VISIBLE) {
child.setVisibility(View.VISIBLE);
}
}
}
}
}
private int getFixedFirstVisibleItem(int firstVisibleItem) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
return firstVisibleItem;
}
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
// listview does not take clipToPadding=false into account
if (!clippingToPadding && getPaddingTop() > 0) {
if (super.getChildAt(0).getTop() > 0) {
if (firstVisibleItem > 0) {
firstVisibleItem -= 1;
}
}
}
return firstVisibleItem;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollListener != null) {
scrollListener.onScrollStateChanged(view, scrollState);
}
}
@Override
public void setSelectionFromTop(int position, int y) {
if (areHeadersSticky) {
if (frame != null && frame.hasHeader()) {
y += frame.getHeaderHeight();
}
}
super.setSelectionFromTop(position, y);
}
public void setOnHeaderClickListener(
OnHeaderClickListener onHeaderClickListener) {
this.onHeaderClickListener = onHeaderClickListener;
}
@Override
public void onClick(View v) {
if (frame.isHeader(v)) {
if (onHeaderClickListener != null) {
onHeaderClickListener.onHeaderClick(this, v, headerPosition,
currentHeaderId, true);
}
}
}
public boolean isDrawingListUnderStickyHeader() {
return drawingListUnderStickyHeader;
}
public void setDrawingListUnderStickyHeader(
boolean drawingListUnderStickyHeader) {
this.drawingListUnderStickyHeader = drawingListUnderStickyHeader;
}
}