package com.roboo.like.google.views;
import java.lang.reflect.Field;
import java.util.ArrayList;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.SectionIndexer;
import com.roboo.like.google.adapters.StickyHeadersAdapter;
import com.roboo.like.google.stickylistheaders.AdapterWrapper;
import com.roboo.like.google.stickylistheaders.SectionIndexerAdapterWrapper;
import com.roboo.like.google.stickylistheaders.WrapperView;
public class StickyListHeadersListView extends ListView
{
public interface OnHeaderClickListener
{
public void onHeaderClick(StickyListHeadersListView l, View header, int itemPosition, long headerId, boolean currentlySticky);
}
private OnScrollListener mOnScrollListenerDelegate;
private boolean mAreHeadersSticky = true;
private int mHeaderBottomPosition;
private View mHeader;
private int mDividerHeight;
private Drawable mDivider;
private Boolean mClippingToPadding;
private final Rect mClippingRect = new Rect();
private Long mCurrentHeaderId = null;
private AdapterWrapper mAdapter;
private float mHeaderDownY = -1;
private boolean mHeaderBeingPressed = false;
private OnHeaderClickListener mOnHeaderClickListener;
private Integer mHeaderPosition;
private ViewConfiguration mViewConfig;
private ArrayList<View> mFooterViews;
private boolean mDrawingListUnderStickyHeader = false;
private Rect mSelectorRect = new Rect();// for if reflection fails
private Field mSelectorPositionField;
private AdapterWrapper.OnHeaderClickListener mAdapterHeaderClickListener = new AdapterWrapper.OnHeaderClickListener()
{
@Override
public void onHeaderClick(View header, int itemPosition, long headerId)
{
if (mOnHeaderClickListener != null)
{
mOnHeaderClickListener.onHeaderClick(StickyListHeadersListView.this, header, itemPosition, headerId, false);
}
}
};
private DataSetObserver mDataSetChangedObserver = new DataSetObserver()
{
@Override
public void onChanged()
{
reset();
}
@Override
public void onInvalidated()
{
reset();
}
};
private OnScrollListener mOnScrollListener = new OnScrollListener()
{
@Override
public void onScrollStateChanged(AbsListView view, int scrollState)
{
if (mOnScrollListenerDelegate != null)
{
mOnScrollListenerDelegate.onScrollStateChanged(view, scrollState);
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
{
if (mOnScrollListenerDelegate != null)
{
mOnScrollListenerDelegate.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO)
{
scrollChanged(firstVisibleItem);
}
}
};
public StickyListHeadersListView(Context context)
{
this(context, null);
}
public StickyListHeadersListView(Context context, AttributeSet attrs)
{
this(context, attrs, 0);
// ======================================================================
// ======================================================================
// 源代码默认的
// ======================================================================
// ======================================================================
// this(context, attrs, android.R.attr.listViewStyle);
}
public StickyListHeadersListView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
super.setOnScrollListener(mOnScrollListener);
// null out divider, dividers are handled by adapter so they look good
// with headers
super.setDivider(null);
super.setDividerHeight(0);
mViewConfig = ViewConfiguration.get(context);
if (mClippingToPadding == null)
{
mClippingToPadding = true;
}
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
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
super.onLayout(changed, l, t, r, b);
if (changed)
{
reset();
scrollChanged(getFirstVisiblePosition());
}
}
private void reset()
{
mHeader = null;
mCurrentHeaderId = null;
mHeaderPosition = null;
mHeaderBottomPosition = -1;
}
@Override
public boolean performItemClick(View view, int position, long id)
{
if (view instanceof WrapperView)
{
view = ((WrapperView) view).mItem;
}
return super.performItemClick(view, position, id);
}
@Override
public void setSelectionFromTop(int position, int y)
{
if (hasStickyHeaderAtPosition(position))
{
y += getHeaderHeight();
}
super.setSelectionFromTop(position, y);
}
@SuppressLint("NewApi")
@Override
public void smoothScrollToPositionFromTop(int position, int offset)
{
if (hasStickyHeaderAtPosition(position))
{
offset += getHeaderHeight();
}
super.smoothScrollToPositionFromTop(position, offset);
}
@SuppressLint("NewApi")
@Override
public void smoothScrollToPositionFromTop(int position, int offset, int duration)
{
if (hasStickyHeaderAtPosition(position))
{
offset += getHeaderHeight();
}
super.smoothScrollToPositionFromTop(position, offset, duration);
}
private boolean hasStickyHeaderAtPosition(int position)
{
position -= getHeaderViewsCount();
return mAreHeadersSticky && position > 0 && position < mAdapter.getCount() && mAdapter.getHeaderId(position) == mAdapter.getHeaderId(position - 1);
}
@Override
public void setDivider(Drawable divider)
{
this.mDivider = divider;
if (divider != null)
{
int dividerDrawableHeight = divider.getIntrinsicHeight();
if (dividerDrawableHeight >= 0)
{
setDividerHeight(dividerDrawableHeight);
}
}
if (mAdapter != null)
{
mAdapter.setDivider(divider);
requestLayout();
invalidate();
}
}
@Override
public void setDividerHeight(int height)
{
mDividerHeight = height;
if (mAdapter != null)
{
mAdapter.setDividerHeight(height);
requestLayout();
invalidate();
}
}
@Override
public void setOnScrollListener(OnScrollListener l)
{
mOnScrollListenerDelegate = l;
}
public void setAreHeadersSticky(boolean areHeadersSticky)
{
if (this.mAreHeadersSticky != areHeadersSticky)
{
this.mAreHeadersSticky = areHeadersSticky;
requestLayout();
}
}
public boolean getAreHeadersSticky()
{
return mAreHeadersSticky;
}
@Override
public void setAdapter(ListAdapter adapter)
{
if (this.isInEditMode())
{
super.setAdapter(adapter);
return;
}
if (adapter == null)
{
mAdapter = null;
reset();
super.setAdapter(null);
return;
}
if (!(adapter instanceof StickyHeadersAdapter))
{
throw new IllegalArgumentException("Adapter must implement StickyHeadersAdapter");
}
mAdapter = wrapAdapter(adapter);
reset();
super.setAdapter(this.mAdapter);
}
private AdapterWrapper wrapAdapter(ListAdapter adapter)
{
AdapterWrapper wrapper;
if (adapter instanceof SectionIndexer)
{
wrapper = new SectionIndexerAdapterWrapper(getContext(), (StickyHeadersAdapter) adapter);
}
else
{
wrapper = new AdapterWrapper(getContext(), (StickyHeadersAdapter) adapter);
}
wrapper.setDivider(mDivider);
wrapper.setDividerHeight(mDividerHeight);
wrapper.registerDataSetObserver(mDataSetChangedObserver);
wrapper.setOnHeaderClickListener(mAdapterHeaderClickListener);
return wrapper;
}
public StickyHeadersAdapter getWrappedAdapter()
{
return mAdapter == null ? null : mAdapter.mDelegate;
}
public View getWrappedView(int position)
{
View view = getChildAt(position);
if ((view instanceof WrapperView))
return ((WrapperView) view).mItem;
return view;
}
@Override
protected void dispatchDraw(Canvas canvas)
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO)
{
scrollChanged(getFirstVisiblePosition());
}
positionSelectorRect();
if (!mAreHeadersSticky || mHeader == null)
{
super.dispatchDraw(canvas);
return;
}
if (!mDrawingListUnderStickyHeader)
{
mClippingRect.set(0, mHeaderBottomPosition, getWidth(), getHeight());
canvas.save();
canvas.clipRect(mClippingRect);
}
super.dispatchDraw(canvas);
if (!mDrawingListUnderStickyHeader)
{
canvas.restore();
}
drawStickyHeader(canvas);
}
private void positionSelectorRect()
{
if (!mSelectorRect.isEmpty())
{
int selectorPosition = getSelectorPosition();
if (selectorPosition >= 0)
{
int firstVisibleItem = fixedFirstVisibleItem(getFirstVisiblePosition());
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 + fixedFirstVisibleItem(getFirstVisiblePosition());
}
}
}
else
{
try
{
return mSelectorPositionField.getInt(this);
}
catch (IllegalArgumentException e)
{
e.printStackTrace();
}
catch (IllegalAccessException e)
{
e.printStackTrace();
}
}
return -1;
}
private void drawStickyHeader(Canvas canvas)
{
int headerHeight = getHeaderHeight();
int top = mHeaderBottomPosition - headerHeight;
// clip the headers drawing region
mClippingRect.left = getPaddingLeft();
mClippingRect.right = getWidth() - getPaddingRight();
mClippingRect.bottom = top + headerHeight;
mClippingRect.top = mClippingToPadding ? getPaddingTop() : 0;
canvas.save();
canvas.clipRect(mClippingRect);
canvas.translate(getPaddingLeft(), top);
mHeader.draw(canvas);
canvas.restore();
}
@SuppressLint("NewApi")
private void measureHeader()
{
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(getWidth() - getPaddingLeft() - getPaddingRight() - (isScrollBarOverlay() ? 0 : getVerticalScrollbarWidth()), MeasureSpec.EXACTLY);
int heightMeasureSpec = 0;
ViewGroup.LayoutParams params = mHeader.getLayoutParams();
if (params == null)
{
mHeader.setLayoutParams(new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
if (params != null && params.height > 0)
{
heightMeasureSpec = MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY);
}
else
{
heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
mHeader.measure(widthMeasureSpec, heightMeasureSpec);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
{
mHeader.setLayoutDirection(this.getLayoutDirection());
}
mHeader.layout(getPaddingLeft(), 0, getWidth() - getPaddingRight(), mHeader.getMeasuredHeight());
}
private boolean isScrollBarOverlay()
{
int scrollBarStyle = getScrollBarStyle();
return scrollBarStyle == SCROLLBARS_INSIDE_OVERLAY || scrollBarStyle == SCROLLBARS_OUTSIDE_OVERLAY;
}
private int getHeaderHeight()
{
return mHeader == null ? 0 : mHeader.getMeasuredHeight();
}
@Override
public void setClipToPadding(boolean clipToPadding)
{
super.setClipToPadding(clipToPadding);
mClippingToPadding = clipToPadding;
}
private void scrollChanged(int reportedFirstVisibleItem)
{
int adapterCount = mAdapter == null ? 0 : mAdapter.getCount();
if (adapterCount == 0 || !mAreHeadersSticky)
{
return;
}
final int listViewHeaderCount = getHeaderViewsCount();
final int firstVisibleItem = fixedFirstVisibleItem(reportedFirstVisibleItem) - listViewHeaderCount;
if (firstVisibleItem < 0 || firstVisibleItem > adapterCount - 1)
{
reset();
updateHeaderVisibilities();
invalidate();
return;
}
if (mHeaderPosition == null || mHeaderPosition != firstVisibleItem)
{
mHeaderPosition = firstVisibleItem;
mCurrentHeaderId = mAdapter.getHeaderId(firstVisibleItem);
mHeader = mAdapter.getHeaderView(mHeaderPosition, mHeader, this);
measureHeader();
}
int childCount = getChildCount();
if (childCount != 0)
{
View viewToWatch = null;
int watchingChildDistance = Integer.MAX_VALUE;
boolean viewToWatchIsFooter = false;
for (int i = 0; i < childCount; i++)
{
final View child = super.getChildAt(i);
final boolean childIsFooter = mFooterViews != null && mFooterViews.contains(child);
final int childDistance = child.getTop() - (mClippingToPadding ? getPaddingTop() : 0);
if (childDistance < 0)
{
continue;
}
if (viewToWatch == null || (!viewToWatchIsFooter && !((WrapperView) viewToWatch).hasHeader()) || ((childIsFooter || ((WrapperView) child).hasHeader()) && childDistance < watchingChildDistance))
{
viewToWatch = child;
viewToWatchIsFooter = childIsFooter;
watchingChildDistance = childDistance;
}
}
final int headerHeight = getHeaderHeight();
if (viewToWatch != null && (viewToWatchIsFooter || ((WrapperView) viewToWatch).hasHeader()))
{
if (firstVisibleItem == listViewHeaderCount && super.getChildAt(0).getTop() > 0 && !mClippingToPadding)
{
mHeaderBottomPosition = 0;
}
else
{
final int paddingTop = mClippingToPadding ? getPaddingTop() : 0;
mHeaderBottomPosition = Math.min(viewToWatch.getTop(), headerHeight + paddingTop);
mHeaderBottomPosition = mHeaderBottomPosition < paddingTop ? headerHeight + paddingTop : mHeaderBottomPosition;
}
}
else
{
mHeaderBottomPosition = headerHeight + (mClippingToPadding ? getPaddingTop() : 0);
}
}
updateHeaderVisibilities();
invalidate();
}
@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;
}
private void updateHeaderVisibilities()
{
int top = mClippingToPadding ? getPaddingTop() : 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++)
{
View child = super.getChildAt(i);
if (child instanceof WrapperView)
{
WrapperView wrapperViewChild = (WrapperView) child;
if (wrapperViewChild.hasHeader())
{
View childHeader = wrapperViewChild.mHeader;
if (wrapperViewChild.getTop() < top)
{
childHeader.setVisibility(View.INVISIBLE);
}
else
{
childHeader.setVisibility(View.VISIBLE);
}
}
}
}
}
private int fixedFirstVisibleItem(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 (!mClippingToPadding && getPaddingTop() > 0)
{
if (super.getChildAt(0).getTop() > 0)
{
if (firstVisibleItem > 0)
{
firstVisibleItem -= 1;
}
}
}
return firstVisibleItem;
}
public void setOnHeaderClickListener(OnHeaderClickListener onHeaderClickListener)
{
this.mOnHeaderClickListener = onHeaderClickListener;
}
public void setDrawingListUnderStickyHeader(boolean drawingListUnderStickyHeader)
{
mDrawingListUnderStickyHeader = drawingListUnderStickyHeader;
}
public boolean isDrawingListUnderStickyHeader()
{
return mDrawingListUnderStickyHeader;
}
// TODO handle touches better, multitouch etc.
@Override
public boolean onTouchEvent(MotionEvent ev)
{
int action = ev.getAction();
if (action == MotionEvent.ACTION_DOWN && ev.getY() <= mHeaderBottomPosition)
{
mHeaderDownY = ev.getY();
mHeaderBeingPressed = true;
mHeader.setPressed(true);
mHeader.invalidate();
invalidate(0, 0, getWidth(), mHeaderBottomPosition);
return true;
}
if (mHeaderBeingPressed)
{
if (Math.abs(ev.getY() - mHeaderDownY) < mViewConfig.getScaledTouchSlop())
{
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)
{
mHeaderDownY = -1;
mHeaderBeingPressed = false;
mHeader.setPressed(false);
mHeader.invalidate();
invalidate(0, 0, getWidth(), mHeaderBottomPosition);
if (mOnHeaderClickListener != null)
{
mOnHeaderClickListener.onHeaderClick(this, mHeader, mHeaderPosition, mCurrentHeaderId, true);
}
}
return true;
}
else
{
mHeaderDownY = -1;
mHeaderBeingPressed = false;
mHeader.setPressed(false);
mHeader.invalidate();
invalidate(0, 0, getWidth(), mHeaderBottomPosition);
}
}
return super.onTouchEvent(ev);
}
}