package com.oushangfeng.pinnedsectionitemdecoration; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; import android.support.v4.content.ContextCompat; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; import android.view.View; import android.view.ViewGroup; import com.oushangfeng.pinnedsectionitemdecoration.callback.OnHeaderClickListener; import com.oushangfeng.pinnedsectionitemdecoration.callback.OnItemTouchListener; import com.oushangfeng.pinnedsectionitemdecoration.utils.DividerHelper; import java.lang.reflect.Field; import java.util.ArrayList; /** * Created by Oubowu on 2016/7/21 15:38. * <p>这个是附在数据布局的小标签,只支持LinearLayoutManager或者GridLayoutManager且一行只有一列的情况,这个比较符合使用场景</p> * <p>注意:标签不能设置marginTop,因为往上滚动遮不住真正的标签</p> */ public class SmallPinnedHeaderItemDecoration extends RecyclerView.ItemDecoration { // 取出Adapter private RecyclerView.Adapter mAdapter = null; // 标签的id值 private int mPinnedHeaderId; private OnHeaderClickListener mHeaderClickListener; private int[] mClickIds; private int mDividerId; private boolean mEnableDivider; private boolean mDisableHeaderClick; private Drawable mDrawable; // 标签父布局的左间距 private int mParentPaddingLeft; // RecyclerView的左间距 private int mRecyclerViewPaddingLeft; // 标签父布局的顶间距 private int mParentPaddingTop; // RecyclerView的顶间距 private int mRecyclerViewPaddingTop; private int mHeaderLeftMargin; private int mHeaderRightMargin; private int mHeaderTopMargin; private int mHeaderBottomMargin; private OnItemTouchListener mItemTouchListener; private int mLeft; private int mTop; private int mRight; private int mBottom; private View mPinnedHeaderParentView; // 缓存某个标签 private View mPinnedHeaderView = null; // 缓存某个标签的位置 private int mHeaderPosition = -1; // 顶部标签的Y轴偏移值 private int mPinnedHeaderOffset; // 用于锁定画布绘制范围 private Rect mClipBounds; private int mFirstVisiblePosition; private int mDataPositionOffset; private int mPinnedHeaderType; private boolean mDisableDrawHeader; private RecyclerView mParent; private SmallPinnedHeaderItemDecoration(Builder builder) { mEnableDivider = builder.enableDivider; mHeaderClickListener = builder.headerClickListener; mDividerId = builder.dividerId; mPinnedHeaderId = builder.pinnedHeaderId; mClickIds = builder.clickIds; mDisableHeaderClick = builder.disableHeaderClick; mPinnedHeaderType = builder.pinnedHeaderType; } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { checkCache(parent); if (!mEnableDivider) { return; } if (mDrawable == null) { mDrawable = ContextCompat.getDrawable(parent.getContext(), mDividerId != 0 ? mDividerId : R.drawable.divider); } outRect.set(0, 0, 0, mDrawable.getIntrinsicHeight()); } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { if (mEnableDivider) { drawDivider(c, parent); } // 只支持LinearLayoutManager或者GridLayoutManager且一行只有一列的情况,这个比较符合使用场景 if (parent.getLayoutManager() instanceof GridLayoutManager && ((GridLayoutManager) parent.getLayoutManager()).getSpanCount() > 1) { return; } else if (parent.getLayoutManager() instanceof StaggeredGridLayoutManager) { return; } // 检测到标签存在的时候,将标签强制固定在RecyclerView顶部 createPinnedHeader(parent); if (!mDisableDrawHeader && mPinnedHeaderView != null && mFirstVisiblePosition >= mHeaderPosition) { // 标签相对parent高度加上自身的高度 final int headerEndAt = mPinnedHeaderParentView.getTop() + mPinnedHeaderParentView.getMeasuredHeight() + mRecyclerViewPaddingTop; // 根据xy坐标查找view View v = parent.findChildViewUnder(c.getWidth() / 2, headerEndAt + 1); if (isPinnedHeader(parent, v) && v.getTop() <= mPinnedHeaderView.getHeight() + mRecyclerViewPaddingTop + mParentPaddingTop) { // 如果view是标签的话,那么缓存的标签就要跟随这个真正的标签标签移动了,效果类似于下面的标签把它顶上去一样 // 得到mPinnedHeaderView为标签跟随移动的位移 mPinnedHeaderOffset = v.getTop() - (mRecyclerViewPaddingTop + mParentPaddingTop + mPinnedHeaderView.getHeight()); } else { mPinnedHeaderOffset = 0; } // Log.e("TAG", "SmallPinnedHeaderItemDecoration-152行-onDraw(): " + v.getTop() + ";" + mPinnedHeaderOffset); // 拿到锁定的矩形 mClipBounds = c.getClipBounds(); mClipBounds.left = 0; mClipBounds.right = parent.getWidth(); mClipBounds.top = mRecyclerViewPaddingTop + mParentPaddingTop; mClipBounds.bottom = parent.getHeight(); // 重新锁定 c.clipRect(mClipBounds); } } private void drawDivider(Canvas c, RecyclerView parent) { if (mAdapter == null) { // checkCache的话RecyclerView未设置之前mAdapter为空 return; } // 不让分割线画出界限 c.clipRect(parent.getPaddingLeft(), parent.getPaddingTop(), parent.getWidth() - parent.getPaddingRight(), parent.getHeight() - parent.getPaddingBottom()); int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); DividerHelper.drawBottomAlignItem(c, mDrawable, child, params); } } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { if (!mDisableDrawHeader && mPinnedHeaderView != null && mFirstVisiblePosition >= mHeaderPosition) { c.save(); mClipBounds.left = mRecyclerViewPaddingLeft + mParentPaddingLeft + mHeaderLeftMargin; mClipBounds.right = mClipBounds.left + mPinnedHeaderView.getWidth(); mClipBounds.top = mRecyclerViewPaddingTop + mParentPaddingTop + mHeaderTopMargin; mClipBounds.bottom = mPinnedHeaderOffset + mPinnedHeaderView.getHeight() + mClipBounds.top; if (mItemTouchListener != null) { mItemTouchListener.invalidTopAndBottom(mPinnedHeaderOffset); } // 取AB交集这个就是标签绘制的范围了 c.clipRect(mClipBounds, Region.Op.INTERSECT); c.translate(mRecyclerViewPaddingLeft + mParentPaddingLeft + mHeaderLeftMargin, mPinnedHeaderOffset + mRecyclerViewPaddingTop + mParentPaddingTop + mHeaderTopMargin); mPinnedHeaderView.draw(c); c.restore(); } else if (mItemTouchListener != null) { // 不绘制的时候,把头部的偏移值偏移用户点击不到的程度 mItemTouchListener.invalidTopAndBottom(-1000); } } // 创建标签 @SuppressWarnings("unchecked") private void createPinnedHeader(RecyclerView parent) { if (mAdapter == null) { // checkCache的话RecyclerView未设置之前mAdapter为空 return; } final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); // 获取第一个可见的item位置 mFirstVisiblePosition = 0; int headerPosition; if (layoutManager instanceof GridLayoutManager) { mFirstVisiblePosition = ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition(); } else if (layoutManager instanceof LinearLayoutManager) { mFirstVisiblePosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); } // 通过第一个部分可见的item位置获取标签的位置 headerPosition = findPinnedHeaderPosition(mFirstVisiblePosition); if (headerPosition >= 0 && mHeaderPosition != headerPosition) { // Log.e("TAG", "创建标签"); // 缓存位置 mHeaderPosition = headerPosition; // 获取此位置的type final int viewType = mAdapter.getItemViewType(headerPosition); // 手动调用创建出标签 final RecyclerView.ViewHolder pinnedViewHolder = mAdapter.createViewHolder(parent, viewType); mAdapter.bindViewHolder(pinnedViewHolder, headerPosition); mPinnedHeaderParentView = pinnedViewHolder.itemView; measurePinedHeaderParent(parent); measurePinnedHeader(); mLeft = mRecyclerViewPaddingLeft + mParentPaddingLeft + mHeaderLeftMargin; mRight = mPinnedHeaderView.getMeasuredWidth() + mLeft; mTop = mRecyclerViewPaddingTop + mParentPaddingTop + mHeaderTopMargin; mBottom = mPinnedHeaderView.getMeasuredHeight() + mTop; // 位置强制布局在顶部 mPinnedHeaderView.layout(mLeft, mTop, mRight, mBottom); if (mItemTouchListener == null && mHeaderClickListener != null) { mItemTouchListener = new OnItemTouchListener(parent.getContext()); try { final Field field = parent.getClass().getDeclaredField("mOnItemTouchListeners"); field.setAccessible(true); final ArrayList<RecyclerView.OnItemTouchListener> touchListeners = (ArrayList<RecyclerView.OnItemTouchListener>) field.get(parent); touchListeners.add(0, mItemTouchListener); } catch (NoSuchFieldException e) { e.printStackTrace(); parent.addOnItemTouchListener(mItemTouchListener); } catch (IllegalAccessException e) { e.printStackTrace(); parent.addOnItemTouchListener(mItemTouchListener); } mItemTouchListener.setHeaderClickListener(mHeaderClickListener); mItemTouchListener.disableHeaderClick(mDisableHeaderClick); mItemTouchListener.setClickBounds(OnItemTouchListener.HEADER_ID, mPinnedHeaderView); } if (mHeaderClickListener != null) { // -1代表是标签的Id mItemTouchListener.setClickBounds(OnItemTouchListener.HEADER_ID, mPinnedHeaderView); if (mHeaderClickListener != null && mClickIds != null && mClickIds.length > 0) { for (int mClickId : mClickIds) { final View view = mPinnedHeaderView.findViewById(mClickId); if (view != null && view.getVisibility() == View.VISIBLE) { mItemTouchListener.setClickBounds(mClickId, view); } } } mItemTouchListener.setClickHeaderInfo(mHeaderPosition - mDataPositionOffset); } } } public int getDataPositionOffset() { return mDataPositionOffset; } public void setDataPositionOffset(int offset) { mDataPositionOffset = offset; } // 测量标签父布局的宽高 private void measurePinedHeaderParent(RecyclerView parent) { // 1.测量标签的parent ViewGroup.LayoutParams parentLp = mPinnedHeaderParentView.getLayoutParams(); if (parentLp == null) { parentLp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); mPinnedHeaderParentView.setLayoutParams(parentLp); } int heightMode = View.MeasureSpec.getMode(ViewGroup.LayoutParams.WRAP_CONTENT); int heightSize = View.MeasureSpec.getSize(parentLp.height); switch (heightMode) { case View.MeasureSpec.UNSPECIFIED: heightMode = View.MeasureSpec.EXACTLY; break; case View.MeasureSpec.EXACTLY: heightMode = View.MeasureSpec.EXACTLY; break; case View.MeasureSpec.AT_MOST: default: heightMode = View.MeasureSpec.AT_MOST; break; } int maxHeight = parent.getHeight() - parent.getPaddingTop() - parent.getPaddingBottom(); heightSize = Math.min(heightSize, maxHeight); int ws = View.MeasureSpec.makeMeasureSpec(parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight(), View.MeasureSpec.EXACTLY); int hs = View.MeasureSpec.makeMeasureSpec(heightSize, heightMode); // 强制测量 mPinnedHeaderParentView.measure(ws, hs); mRecyclerViewPaddingLeft = parent.getPaddingLeft(); mParentPaddingLeft = mPinnedHeaderParentView.getPaddingLeft(); mRecyclerViewPaddingTop = parent.getPaddingTop(); mParentPaddingTop = mPinnedHeaderParentView.getPaddingTop(); if (parentLp instanceof RecyclerView.LayoutParams) { mRecyclerViewPaddingLeft += ((RecyclerView.LayoutParams) parentLp).leftMargin; mRecyclerViewPaddingTop += ((RecyclerView.LayoutParams) parentLp).topMargin; } } // 测量标签高度 private void measurePinnedHeader() { // 2.测量标签 mPinnedHeaderView = mPinnedHeaderParentView.findViewById(mPinnedHeaderId); // 获取标签的布局属性 ViewGroup.LayoutParams lp = mPinnedHeaderView.getLayoutParams(); if (lp == null) { lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); mPinnedHeaderView.setLayoutParams(lp); } if (lp instanceof ViewGroup.MarginLayoutParams) { final ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp; mHeaderLeftMargin = mlp.leftMargin; mHeaderRightMargin = mlp.rightMargin; mHeaderTopMargin = mlp.topMargin; mHeaderBottomMargin = mlp.bottomMargin; } // 设置高度 int heightMode = View.MeasureSpec.getMode(lp.height); int heightSize = View.MeasureSpec.getSize(lp.height); switch (heightMode) { case View.MeasureSpec.UNSPECIFIED: heightMode = View.MeasureSpec.EXACTLY; break; case View.MeasureSpec.EXACTLY: heightMode = View.MeasureSpec.EXACTLY; break; case View.MeasureSpec.AT_MOST: heightMode = View.MeasureSpec.AT_MOST; break; default: heightMode = View.MeasureSpec.AT_MOST; break; } // 最大高度为mPinnedHeaderParentView的高度减去padding int maxHeight = mPinnedHeaderParentView.getMeasuredHeight() - mPinnedHeaderParentView.getPaddingTop() - mPinnedHeaderParentView.getPaddingBottom(); heightSize = Math.min(heightSize, maxHeight); int hs = View.MeasureSpec.makeMeasureSpec(heightSize, heightMode); // 设置宽度 int widthMode = View.MeasureSpec.getMode(lp.width); int widthSize = View.MeasureSpec.getSize(lp.width); switch (widthMode) { case View.MeasureSpec.UNSPECIFIED: widthMode = View.MeasureSpec.EXACTLY; break; case View.MeasureSpec.EXACTLY: widthMode = View.MeasureSpec.EXACTLY; break; case View.MeasureSpec.AT_MOST: widthMode = View.MeasureSpec.AT_MOST; break; default: widthMode = View.MeasureSpec.AT_MOST; break; } int maxWidth = mPinnedHeaderParentView.getMeasuredWidth() - mPinnedHeaderParentView.getPaddingLeft() - mPinnedHeaderParentView.getPaddingRight(); widthSize = Math.min(widthSize, maxWidth); int ws = View.MeasureSpec.makeMeasureSpec(widthSize, widthMode); // 强制测量 mPinnedHeaderView.measure(ws, hs); } // 查找标签的位置 private int findPinnedHeaderPosition(int fromPosition) { for (int position = fromPosition; position >= 0; position--) { // 从这个位置开始递减,只要一查到有位置type为标签,立即返回此标签位置 final int viewType = mAdapter.getItemViewType(position); // 检查是否是标签类型 if (isPinnedViewType(viewType)) { // 是标签类型,返回位置 return position; } } return -1; } // 检查传入View是否是标签 private boolean isPinnedHeader(RecyclerView parent, View v) { // 获取View在parent中的位置 final int position = parent.getChildAdapterPosition(v); if (position == RecyclerView.NO_POSITION) { return false; } // 获取View的type final int viewType = mAdapter.getItemViewType(position); // 检查是否是标签类型 return isPinnedViewType(viewType); } // 检查是否是标签类型 private boolean isPinnedViewType(int viewType) { return viewType == mPinnedHeaderType; } // 检查缓存 private void checkCache(RecyclerView parent) { // 取出RecyclerView的适配器 if (mParent != parent) { mParent = parent; } RecyclerView.Adapter adapter = parent.getAdapter(); if (mAdapter != adapter) { // 适配器有差异,清空缓存 mPinnedHeaderView = null; mHeaderPosition = -1; mAdapter = adapter; mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { super.onChanged(); reset(); } @Override public void onItemRangeChanged(int positionStart, int itemCount) { super.onItemRangeChanged(positionStart, itemCount); reset(); } @Override public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { super.onItemRangeChanged(positionStart, itemCount, payload); reset(); } @Override public void onItemRangeInserted(int positionStart, int itemCount) { super.onItemRangeInserted(positionStart, itemCount); reset(); } @Override public void onItemRangeRemoved(int positionStart, int itemCount) { super.onItemRangeRemoved(positionStart, itemCount); reset(); } @Override public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { super.onItemRangeMoved(fromPosition, toPosition, itemCount); reset(); } }); } } private void reset() { mHeaderPosition = -1; mPinnedHeaderView = null; } public View getPinnedHeaderView() { return mPinnedHeaderView; } public int getPinnedHeaderPosition() { return mHeaderPosition; } /** * 是否禁止绘制粘性头部 * * @return true的话不绘制头部 */ public boolean isDisableDrawHeader() { return mDisableDrawHeader; } /** * 禁止绘制粘性头部 * * @param disableDrawHeader true的话不绘制头部,默认false绘制头部 */ public void disableDrawHeader(boolean disableDrawHeader) { mDisableDrawHeader = disableDrawHeader; if (mParent != null) { mParent.invalidateItemDecorations(); } } public static class Builder { public boolean disableHeaderClick; private OnHeaderClickListener headerClickListener; private int dividerId; private int pinnedHeaderId; private boolean enableDivider; private int[] clickIds; private int pinnedHeaderType; /** * 构造方法 * * @param pinnedHeaderId 小标签对应的ID * @param pinnedHeaderType 粘性标签的类型 */ public Builder(int pinnedHeaderId, int pinnedHeaderType) { this.pinnedHeaderId = pinnedHeaderId; this.pinnedHeaderType = pinnedHeaderType; } /** * 设置标签和其内部的子控件的监听,若设置点击监听不为null,但是disableHeaderClick(true)的话,还是不会响应点击事件 * * @param headerClickListener 监听,若不设置这个setClickIds无效 * @return 构建者 */ public Builder setHeaderClickListener(OnHeaderClickListener headerClickListener) { this.headerClickListener = headerClickListener; return this; } /** * 设置分隔线资源ID * * @param dividerId 资源ID,若不设置这个并且enableDivider=true时,使用默认的分隔线 * @return 构建者 */ public Builder setDividerId(int dividerId) { this.dividerId = dividerId; return this; } /** * 是否开启绘制分隔线,默认关闭 * * @param enableDivider true为绘制,false不绘制,false时setDividerId无效 * @return 构建者 */ public Builder enableDivider(boolean enableDivider) { this.enableDivider = enableDivider; return this; } /** * 通过传入包括标签和其内部的子控件的ID设置其对应的点击事件 * * @param clickIds 标签或其内部的子控件的ID * @return 构建者 */ public Builder setClickIds(int... clickIds) { this.clickIds = clickIds; return this; } /** * 是否关闭标签点击事件,默认开启 * * @param disableHeaderClick true为关闭标签点击事件,false为开启标签点击事件 * @return 构建者 */ public Builder disableHeaderClick(boolean disableHeaderClick) { this.disableHeaderClick = disableHeaderClick; return this; } public SmallPinnedHeaderItemDecoration create() { return new SmallPinnedHeaderItemDecoration(this); } } }