// 其他的文章:https://blog.csdn.net/ldstartnow/article/details/52454223 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.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Parcelable; import android.util.AttributeSet; import android.util.SparseBooleanArray; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.Checkable; import android.widget.FrameLayout; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.SectionIndexer; import java.lang.reflect.Field; import java.util.LinkedList; import java.util.List; /** * Even though this is a FrameLayout subclass we still consider it a ListView. * This is because of 2 reasons: * 1. It acts like as ListView. * 2. It used to be a ListView subclass and refactoring the name would cause compatibility errors. * * @author Emil Sjölander */ class StickyListHeadersListView extends FrameLayout { public interface OnHeaderClickListener { 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. */ void onStickyHeaderOffsetChanged(StickyListHeadersListView l, View header, int offset); } /** * Notifies the listener when the sticky header has been updated */ public interface OnStickyHeaderChangedListener { /** * @param l The view parent * @param header The new sticky header view. * @param itemPosition The position of the item within the adapter's data set of * the item whose header is now sticky. * @param headerId The id of the new sticky header. */ void onStickyHeaderChanged(StickyListHeadersListView l, View header, int itemPosition, long headerId); } /* --- 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 AbsListView.OnScrollListener mOnScrollListenerDelegate; private AdapterWrapper mAdapter; /* --- Settings --- */ private boolean mAreHeadersSticky = true; private boolean mClippingToPadding = true; private boolean mIsDrawingListUnderStickyHeader = true; private int mStickyHeaderTopOffset = 0; private int mPaddingLeft = 0; private int mPaddingTop = 0; private int mPaddingRight = 0; private int mPaddingBottom = 0; /* --- Touch handling --- */ private float mDownY; private boolean mHeaderOwnsTouch; private float mTouchSlop; /* --- Other --- */ private OnHeaderClickListener mOnHeaderClickListener; private OnStickyHeaderOffsetChangedListener mOnStickyHeaderOffsetChangedListener; private OnStickyHeaderChangedListener mOnStickyHeaderChangedListener; 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, R.attr.stickyListHeadersListViewStyle); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public StickyListHeadersListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); // 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); if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.StickyListHeadersListView, defStyle, 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); // scrollbars final int scrollBars = a.getInt(R.styleable.StickyListHeadersListView_android_scrollbars, 0x00000200); mList.setVerticalScrollBarEnabled((scrollBars & 0x00000200) != 0); mList.setHorizontalScrollBarEnabled((scrollBars & 0x00000100) != 0); // overscroll if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { mList.setOverScrollMode(a.getInt(R.styleable.StickyListHeadersListView_android_overScrollMode, 0)); } // -- 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); } mList.setStackFromBottom(a.getBoolean(R.styleable.StickyListHeadersListView_android_stackFromBottom, false)); mDividerHeight = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_dividerHeight, mDividerHeight); mList.setTranscriptMode(a.getInt(R.styleable.StickyListHeadersListView_android_transcriptMode, ListView.TRANSCRIPT_MODE_DISABLED)); // -- 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); header.setLayoutParams(lp); } else if (lp.height == LayoutParams.MATCH_PARENT || lp.width == LayoutParams.WRAP_CONTENT) { lp.height = LayoutParams.WRAP_CONTENT; lp.width = LayoutParams.MATCH_PARENT; 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) { // mHeader覆盖在List上面 MarginLayoutParams lp = (MarginLayoutParams) mHeader.getLayoutParams(); int headerTop = lp.topMargin; 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). // header画在中间:从下往上依次是mList item, header, decorators // onDispatchDrawOccurred 触发绘制Sticky Header // // Step 1, draw the background, if needed: drawBackground // Step 2, If necessary, save the canvas' layers to prepare for fading // Step 3, draw the content: onDraw // Step 4, draw the children: dispatchDraw(canvas); // Step 5, If necessary, draw the fading edges and restore layers // Step 6, draw decorations (foreground, scrollbars): onDrawForeground(canvas); if (mList.getVisibility() == VISIBLE || mList.getAnimation() != null) { 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(); // 非sticky,返回 if (adapterCount == 0 || !mAreHeadersSticky) { return; } final int headerViewCount = mList.getHeaderViewsCount(); int headerPosition = firstVisiblePosition - headerViewCount; if (mList.getChildCount() > 0) { View firstItem = mList.getChildAt(0); if (firstItem.getBottom() < stickyHeaderTop()) { // 第一个子view已经滑到stickyHeader上面去了,使用第二个子view对应的item headerPosition++; } } // 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() >= stickyHeaderTop(); final boolean isHeaderPositionOutsideAdapterRange = headerPosition > adapterCount - 1 || headerPosition < 0; if (!doesListHaveChildren || isHeaderPositionOutsideAdapterRange || isFirstViewBelowTop) { clearHeader(); return; } updateHeader(headerPosition); } private void updateHeader(int headerPosition) { // check if there is a new header should be sticky if (mHeaderPosition == null || mHeaderPosition != headerPosition) { mHeaderPosition = headerPosition; final long headerId = mAdapter.getHeaderId(headerPosition); 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"); } // 设置header swapHeader(header); } ensureHeaderHasCorrectLayoutParams(mHeader); measureHeader(mHeader); if (mOnStickyHeaderChangedListener != null) { mOnStickyHeaderChangedListener.onStickyHeaderChanged(this, mHeader, headerPosition, mHeaderId); } // Reset mHeaderOffset to null ensuring // that it will be set on the header and // not skipped for performance reasons. mHeaderOffset = null; } } int headerOffset = stickyHeaderTop(); // Calculate new header offset // Skip looking at the first view. it never matters because it always // results in a headerOffset = 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() >= stickyHeaderTop() && (doesChildHaveHeader || isChildFooter)) { // 确定Header的offset headerOffset = Math.min(child.getTop() - mHeader.getMeasuredHeight(), headerOffset); break; } } // 设置Header的offset setHeaderOffet(headerOffset); if (!mIsDrawingListUnderStickyHeader) { mList.setTopClippingLength(mHeader.getMeasuredHeight() + mHeaderOffset); } updateHeaderVisibilities(); } private void swapHeader(View newHeader) { if (mHeader != null) { removeView(mHeader); } mHeader = newHeader; addView(mHeader); if (mOnHeaderClickListener != null) { mHeader.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mOnHeaderClickListener.onHeaderClick( StickyListHeadersListView.this, mHeader, mHeaderPosition, mHeaderId, true); } }); } mHeader.setClickable(true); } // hides the headers in the list under the sticky header. // Makes sure the other ones are showing private void updateHeaderVisibilities() { int top = stickyHeaderTop(); 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); } } } @Override public boolean dispatchTouchEvent(MotionEvent ev) { int action = ev.getAction() & MotionEvent.ACTION_MASK; if (action == MotionEvent.ACTION_DOWN) { mDownY = ev.getY(); mHeaderOwnsTouch = mHeader != null && mDownY <= mHeader.getHeight() + mHeaderOffset; } boolean handled; if (mHeaderOwnsTouch) { if (mHeader != null && Math.abs(mDownY - ev.getY()) <= mTouchSlop) { handled = mHeader.dispatchTouchEvent(ev); } else { if (mHeader != null) { MotionEvent cancelEvent = MotionEvent.obtain(ev); cancelEvent.setAction(MotionEvent.ACTION_CANCEL); mHeader.dispatchTouchEvent(cancelEvent); cancelEvent.recycle(); } MotionEvent downEvent = MotionEvent.obtain(ev.getDownTime(), ev.getEventTime(), ev.getAction(), ev.getX(), mDownY, ev.getMetaState()); downEvent.setAction(MotionEvent.ACTION_DOWN); handled = mList.dispatchTouchEvent(downEvent); downEvent.recycle(); mHeaderOwnsTouch = false; } } else { handled = mList.dispatchTouchEvent(ev); } return handled; } private class AdapterWrapperDataSetObserver extends DataSetObserver { @Override public void onChanged() { clearHeader(); } @Override public void onInvalidated() { clearHeader(); } } private class WrapperListScrollListener implements AbsListView.OnScrollListener { @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (mOnScrollListenerDelegate != null) { mOnScrollListenerDelegate.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); } // 滑动的时候动态更改header updateOrClearHeader(mList.getFixedFirstVisibleItem()); } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (mOnScrollListenerDelegate != null) { mOnScrollListenerDelegate.onScrollStateChanged(view, scrollState); } } } private class WrapperViewListLifeCycleListener implements WrapperViewList.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); } public int getHeaderOverlap(int position) { boolean isStartOfSection = isStartOfSection(Math.max(0, position - getHeaderViewsCount())); 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; } private int stickyHeaderTop() { return mStickyHeaderTopOffset + (mClippingToPadding ? mPaddingTop : 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(); } /** * @param stickyHeaderTopOffset The offset of the sticky header fom the top of the view */ public void setStickyHeaderTopOffset(int stickyHeaderTopOffset) { mStickyHeaderTopOffset = stickyHeaderTopOffset; updateOrClearHeader(mList.getFixedFirstVisibleItem()); } public int getStickyHeaderTopOffset() { return mStickyHeaderTopOffset; } 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()); if (mHeader != null) { mHeader.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mOnHeaderClickListener.onHeaderClick( StickyListHeadersListView.this, mHeader, mHeaderPosition, mHeaderId, true); } }); } } else { mAdapter.setOnHeaderClickListener(null); } } } public void setOnStickyHeaderOffsetChangedListener(OnStickyHeaderOffsetChangedListener listener) { mOnStickyHeaderOffsetChangedListener = listener; } public void setOnStickyHeaderChangedListener(OnStickyHeaderChangedListener listener) { mOnStickyHeaderChangedListener = 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; } private boolean requireSdkVersion(int versionCode) { if (Build.VERSION.SDK_INT < versionCode) { Log.e("StickyListHeaders", "Api lvl must be at least " + versionCode + " to call this method"); return false; } return true; } /* ---------- ListView delegate methods ---------- */ public void setAdapter(StickyListHeadersAdapter adapter) { if (adapter == null) { if (mAdapter instanceof SectionIndexerAdapterWrapper) { ((SectionIndexerAdapterWrapper) mAdapter).mSectionIndexerDelegate = null; } if (mAdapter != null) { mAdapter.mDelegate = 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(AbsListView.OnScrollListener onScrollListener) { mOnScrollListenerDelegate = onScrollListener; } @Override public void setOnTouchListener(final OnTouchListener l) { if (l != null) { mList.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return l.onTouch(StickyListHeadersListView.this, event); } }); } else { mList.setOnTouchListener(null); } } public void setOnItemClickListener(AdapterView.OnItemClickListener listener) { mList.setOnItemClickListener(listener); } public void setOnItemLongClickListener(AdapterView.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, Object data, boolean isSelectable) { mList.addFooterView(v, data, isSelectable); } 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 boolean isVerticalScrollBarEnabled() { return mList.isVerticalScrollBarEnabled(); } @Override public boolean isHorizontalScrollBarEnabled() { return mList.isHorizontalScrollBarEnabled(); } @Override public void setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled) { mList.setVerticalScrollBarEnabled(verticalScrollBarEnabled); } @Override public void setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled) { mList.setHorizontalScrollBarEnabled(horizontalScrollBarEnabled); } @Override @TargetApi(Build.VERSION_CODES.GINGERBREAD) public int getOverScrollMode() { if (requireSdkVersion(Build.VERSION_CODES.GINGERBREAD)) { return mList.getOverScrollMode(); } return 0; } @Override @TargetApi(Build.VERSION_CODES.GINGERBREAD) public void setOverScrollMode(int mode) { if (requireSdkVersion(Build.VERSION_CODES.GINGERBREAD)) { if (mList != null) { mList.setOverScrollMode(mode); } } } @TargetApi(Build.VERSION_CODES.FROYO) public void smoothScrollBy(int distance, int duration) { if (requireSdkVersion(Build.VERSION_CODES.FROYO)) { mList.smoothScrollBy(distance, duration); } } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void smoothScrollByOffset(int offset) { if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) { mList.smoothScrollByOffset(offset); } } @SuppressLint("NewApi") @TargetApi(Build.VERSION_CODES.FROYO) public void smoothScrollToPosition(int position) { if (requireSdkVersion(Build.VERSION_CODES.FROYO)) { 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) { if (requireSdkVersion(Build.VERSION_CODES.FROYO)) { mList.smoothScrollToPosition(position, boundPosition); } } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void smoothScrollToPositionFromTop(int position, int offset) { if (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) { if (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(); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void setChoiceMode(int choiceMode) { mList.setChoiceMode(choiceMode); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void setItemChecked(int position, boolean value) { mList.setItemChecked(position, value); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public int getCheckedItemCount() { if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) { return mList.getCheckedItemCount(); } return 0; } @TargetApi(Build.VERSION_CODES.FROYO) public long[] getCheckedItemIds() { if (requireSdkVersion(Build.VERSION_CODES.FROYO)) { return mList.getCheckedItemIds(); } return null; } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public int getCheckedItemPosition() { return mList.getCheckedItemPosition(); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) 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); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void setFastScrollAlwaysVisible(boolean alwaysVisible) { if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) { mList.setFastScrollAlwaysVisible(alwaysVisible); } } /** * @return true if the fast scroller will always show. False on pre-Honeycomb devices. * @see 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(); } public int getPositionForView(View view) { return mList.getPositionForView(view); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void setMultiChoiceModeListener(AbsListView.MultiChoiceModeListener listener) { if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) { mList.setMultiChoiceModeListener(listener); } } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); if (superState != BaseSavedState.EMPTY_STATE) { throw new IllegalStateException("Handling non empty state of parent class is not implemented"); } return mList.onSaveInstanceState(); } @Override public void onRestoreInstanceState(Parcelable state) { super.onRestoreInstanceState(BaseSavedState.EMPTY_STATE); mList.onRestoreInstanceState(state); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override public boolean canScrollVertically(int direction) { return mList.canScrollVertically(direction); } public void setTranscriptMode(int mode) { mList.setTranscriptMode(mode); } public void setBlockLayoutChildren(boolean blockLayoutChildren) { mList.setBlockLayoutChildren(blockLayoutChildren); } public void setStackFromBottom(boolean stackFromBottom) { mList.setStackFromBottom(stackFromBottom); } public boolean isStackFromBottom() { return mList.isStackFromBottom(); } } 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; private boolean mBlockLayoutChildren = false; 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 // 应该是不让ListSelector绘制到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下移,mSelector不绘制到Header位置 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); addInternalFooterView(v); } @Override public void addFooterView(View v, Object data, boolean isSelectable) { super.addFooterView(v, data, isSelectable); addInternalFooterView(v); } private void addInternalFooterView(View 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); } public void setBlockLayoutChildren(boolean block) { mBlockLayoutChildren = block; } @Override protected void layoutChildren() { if (!mBlockLayoutChildren) { super.layoutChildren(); } } } 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; } /** * @param item * @param header * @param divider * @param dividerHeight */ 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) { 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 && mItem.getVisibility() != View.GONE) { measuredHeight += mDividerHeight; } //measure item LayoutParams params = mItem.getLayoutParams(); //enable hiding listview item,ex. toggle off items in group if (mItem.getVisibility() == View.GONE) { mItem.measure(childWidthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY)); } else if (params != null && params.height >= 0) { mItem.measure(childWidthMeasureSpec, MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY)); measuredHeight += mItem.getMeasuredHeight(); } 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(); // Header在前,item在后 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 && mItem.getVisibility() != View.GONE) { // 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); } } } /** * A {@link ListAdapter} which wraps a {@link StickyListHeadersAdapter} and * automatically handles wrapping the result of * {@link StickyListHeadersAdapter#getView(int, View, ViewGroup)} * and * {@link StickyListHeadersAdapter#getHeaderView(int, View, ViewGroup)} * appropriately. * * @author Jake Wharton (jakewharton@gmail.com) */ class AdapterWrapper extends BaseAdapter implements StickyListHeadersAdapter { interface OnHeaderClickListener { void onHeaderClick(View header, int itemPosition, long headerId); } 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) { // 从缓存中拿取ConvertView View header = wv.mHeader == null ? popHeader() : wv.mHeader; // 创建Header 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; // 创建Item view View item = mDelegate.getView(position, wv.mItem, parent); View header = null; if (previousPositionHasSameHeader(position)) { // 如果和前一个item属于同一个section(相同的header),此时将header回收 // 注意此时header是null,WrapperView的header可能不为null recycleHeaderIfExists(wv); } else { // 创建一个Header header = configureHeader(wv, position); } if ((item instanceof Checkable) && !(wv instanceof CheckableWrapperVsiew)) { // 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); } // 更新Item和header、divider 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); } } 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); }
源码注释:StickyListHeadersListView
最新推荐文章于 2022-07-31 13:24:18 发布