package com.lzx.demo.util;
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.lzx.demo.R;
import com.lzx.demo.bean.ClickBounds;
import com.lzx.demo.interfaces.OnHeaderClickListener;
import com.lzx.demo.interfaces.PinnedHeaderNotifyer;
import com.lzx.demo.listener.OnItemTouchListener;
/**
* Created by Oubowu on 2016/7/21 15:38.
* <p>
* 这个是单独一个布局的标签
* <p>
* porting from https://github.com/takahr/pinned-section-item-decoration
* <p>
* 注意:标签所在最外层布局不能设置marginTop,因为往上滚动遮不住真正的标签;marginBottom还有问题待解决
*/
public class PinnedHeaderItemDecoration<T> extends RecyclerView.ItemDecoration {
private OnHeaderClickListener<T> mHeaderClickListener;
private boolean mEnableDivider;
private boolean mDisableHeaderClick;
private int mDividerId;
private int[] mClickIds;
private Drawable mDrawable;
private RecyclerView.Adapter mAdapter;
// 缓存的标签
private View mPinnedHeaderView;
// 缓存的标签位置
int mPinnedHeaderPosition = -1;
// 顶部标签的Y轴偏移值
private int mPinnedHeaderOffset;
// 用于锁定画布绘制范围
private Rect mClipBounds;
// 父布局的左间距
private int mRecyclerViewPaddingLeft;
// 父布局的顶间距
private int mRecyclerViewPaddingTop;
private int mHeaderLeftMargin;
private int mHeaderTopMargin;
private int mHeaderRightMargin;
private int mHeaderBottomMargin;
// 用于处理头部点击事件屏蔽与响应
private OnItemTouchListener<T> mItemTouchListener;
private int mLeft;
private int mTop;
private int mRight;
private int mBottom;
// 当我们调用mRecyclerView.addItemDecoration()方法添加decoration的时候,RecyclerView在绘制的时候,去会绘制decorator,即调用该类的onDraw和onDrawOver方法,
// 1.onDraw方法先于drawChildren
// 2.onDrawOver在drawChildren之后,一般我们选择复写其中一个即可。
// 3.getItemOffsets 可以通过outRect.set()为每个Item设置一定的偏移量,主要用于绘制Decorator。
private PinnedHeaderItemDecoration(Builder<T> builder) {
mEnableDivider = builder.enableDivider;
mHeaderClickListener = builder.headerClickListener;
mDividerId = builder.dividerId;
mClickIds = builder.clickIds;
mDisableHeaderClick = builder.disableHeaderClick;
}
@Override
public void getItemOffsets(final Rect outRect, final View view, final RecyclerView parent, RecyclerView.State state) {
checkCache(parent);
if (!mEnableDivider) {
return;
}
if (mDrawable == null) {
mDrawable = ContextCompat.getDrawable(parent.getContext(), mDividerId != 0 ? mDividerId : R.drawable.divider);
}
if (parent.getLayoutManager() instanceof GridLayoutManager) {
if (!isPinnedHeader(parent, view)) {
final int spanCount = getSpanCount(parent);
int position = parent.getChildAdapterPosition(view);
if (isFirstColumn(parent, position, spanCount)) {
// 第一列要多画左边
outRect.set(mDrawable.getIntrinsicWidth(), 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
}
} else {
// 标签画底部分隔线
outRect.set(0, 0, 0, mDrawable.getIntrinsicHeight());
}
} else if (parent.getLayoutManager() instanceof LinearLayoutManager) {
outRect.set(0, 0, 0, mDrawable.getIntrinsicHeight());
} else if (parent.getLayoutManager() instanceof StaggeredGridLayoutManager) {
if (isPinnedHeader(parent, view)) {
outRect.set(0, 0, 0, mDrawable.getIntrinsicHeight());
} else {
final StaggeredGridLayoutManager.LayoutParams slp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
// slp.getSpanIndex(): 这个可以拿到它在同一行排序的真实顺序
if (slp.getSpanIndex() == 0) {
outRect.set(mDrawable.getIntrinsicWidth(), 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
}
}
}
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
// 检测到标签存在的时候,将标签强制固定在顶部
createPinnedHeader(parent);
if (mPinnedHeaderView != null) {
mClipBounds = c.getClipBounds();
// getTop拿到的是它的原点(它自身的padding值包含在内)相对parent的顶部距离,加上它的高度后就是它的底部所处的位置
final int headEnd = mPinnedHeaderView.getTop() + mPinnedHeaderView.getHeight();
// 根据坐标查找view,headEnd + 1找到的就是mPinnedHeaderView底部下面的view
final View belowView = parent.findChildViewUnder(c.getWidth() / 2, headEnd + 1);
if (isPinnedHeader(parent, belowView)) {
// 如果是标签的话,缓存的标签就要同步跟此标签移动
// 根据belowView相对顶部距离计算出缓存标签的位移
mPinnedHeaderOffset = belowView.getTop() - (mRecyclerViewPaddingTop + mPinnedHeaderView.getHeight() + mHeaderTopMargin);
// 锁定的矩形顶部为v.getTop(趋势是mPinnedHeaderView.getHeight()->0)
mClipBounds.top = belowView.getTop();
} else {
mPinnedHeaderOffset = 0;
mClipBounds.top = mRecyclerViewPaddingTop + mPinnedHeaderView.getHeight();
}
// 锁定画布绘制范围,记为A
c.clipRect(mClipBounds);
}
if (mEnableDivider) {
drawDivider(c, parent);
}
}
// 画分隔线
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());
if (parent.getLayoutManager() instanceof GridLayoutManager) {
int childCount = parent.getChildCount();
final int spanCount = getSpanCount(parent);
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
// 要考虑View的重用啊
int realPosition = parent.getChildAdapterPosition(child);
if (isPinnedHeaderType(mAdapter.getItemViewType(realPosition))) {
DividerHelper.drawBottomAlignItem(c, mDrawable, child, params);
} else {
if (isFirstColumn(parent, realPosition, spanCount)) {
DividerHelper.drawLeft(c, mDrawable, child, params);
}
DividerHelper.drawBottom(c, mDrawable, child, params);
DividerHelper.drawRight(c, mDrawable, child, params);
}
}
} else if (parent.getLayoutManager() instanceof LinearLayoutManager) {
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);
}
} else if (parent.getLayoutManager() instanceof StaggeredGridLayoutManager) {
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
if (isPinnedHeader(parent, child)) {
DividerHelper.drawBottomAlignItem(c, mDrawable, child, params);
} else {
DividerHelper.drawLeft(c, mDrawable, child, params);
DividerHelper.drawBottom(c, mDrawable, child, params);
DividerHelper.drawRight(c, mDrawable, child, params);
}
}
}
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (mPinnedHeaderView != null) {
c.save();
mItemTouchListener.invalidTopAndBottom(mPinnedHeaderOffset);
mClipBounds.top = mRecyclerViewPaddingTop + mHeaderTopMargin;
// 锁定画布绘制范围,记为B
// REVERSE_DIFFERENCE,实际上就是求得的B和A的差集范围,即B-A,只有在此范围内的绘制内容才会被显示
// 因此,只绘制(0,0,parent.getWidth(),belowView.getTop())这个范围,然后画布移动了mPinnedHeaderTop,所以刚好是绘制顶部标签移动的范围
// 低版本不行,换回Region.Op.UNION并集
c.clipRect(mClipBounds, Region.Op.UNION);
c.translate(mRecyclerViewPaddingLeft + mHeaderLeftMargin, mPinnedHeaderOffset + mRecyclerViewPaddingTop + mHeaderTopMargin);
mPinnedHeaderView.draw(c);
c.restore();
}
}
/**
* 查找到view对应的位置从而判断出是否标签类型
*
* @param parent
* @param view
* @return
*/
private boolean isPinnedHeader(RecyclerView parent, View view) {
final int position = parent.getChildAdapterPosition(view);
if (position == RecyclerView.NO_POSITION) {
return false;
}
final int type = mAdapter.getItemViewType(position);
return isPinnedHeaderType(type);
}
/**
* 创建标签强制固定在顶部
*
* @param parent
*/
@SuppressWarnings("unchecked")
private void createPinnedHeader(RecyclerView parent) {
if (mAdapter == null) {
// checkCache的话RecyclerView未设置之前mAdapter为空
return;
}
final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
// 获取第一个可见的item位置
int firstVisiblePosition = findFirstVisiblePosition(layoutManager);
// 获取标签的位置,
int pinnedHeaderPosition = findPinnedHeaderPosition(firstVisiblePosition);
if (pinnedHeaderPosition >= 0 && mPinnedHeaderPosition != pinnedHeaderPosition) {
// 标签位置有效并且和缓存的位置不同
mPinnedHeaderPosition = pinnedHeaderPosition;
// 获取标签的type
final int type = mAdapter.getItemViewType(mPinnedHeaderPosition);
// 手动调用创建标签
final RecyclerView.ViewHolder holder = mAdapter.createViewHolder(parent, type);
mAdapter.bindViewHolder(holder, mPinnedHeaderPosition);
// 缓存标签
mPinnedHeaderView = holder.itemView;
ViewGroup.LayoutParams lp = mPinnedHeaderView.getLayoutParams();
if (lp == null) {
// 标签默认宽度占满parent
lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mPinnedHeaderView.setLayoutParams(lp);
}
// 对高度进行处理
int heightMode = View.MeasureSpec.getMode(lp.height);
int heightSize = View.MeasureSpec.getSize(lp.height);
if (heightMode == View.MeasureSpec.UNSPECIFIED) {
heightMode = View.MeasureSpec.EXACTLY;
}
mRecyclerViewPaddingLeft = parent.getPaddingLeft();
int recyclerViewPaddingRight = parent.getPaddingRight();
mRecyclerViewPaddingTop = parent.getPaddingTop();
int recyclerViewPaddingBottom = parent.getPaddingBottom();
if (lp instanceof ViewGroup.MarginLayoutParams) {
final ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp;
mHeaderLeftMargin = mlp.leftMargin;
mHeaderTopMargin = mlp.topMargin;
mHeaderRightMargin = mlp.rightMargin;
mHeaderBottomMargin = mlp.bottomMargin;
}
// 最大高度为RecyclerView的高度减去padding
final int maxHeight = parent.getHeight() - mRecyclerViewPaddingTop - recyclerViewPaddingBottom;
// 不能超过maxHeight
heightSize = Math.min(heightSize, maxHeight);
// 因为标签默认宽度占满parent,所以宽度强制为RecyclerView的宽度减去padding
final int widthSpec = View.MeasureSpec
.makeMeasureSpec(parent.getWidth() - mRecyclerViewPaddingLeft - recyclerViewPaddingRight - mHeaderLeftMargin - mHeaderRightMargin,
View.MeasureSpec.EXACTLY);
final int heightSpec = View.MeasureSpec.makeMeasureSpec(heightSize, heightMode);
// 强制测量
mPinnedHeaderView.measure(widthSpec, heightSpec);
mLeft = mRecyclerViewPaddingLeft + mHeaderLeftMargin;
mTop = mRecyclerViewPaddingTop + mHeaderTopMargin;
mRight = mPinnedHeaderView.getMeasuredWidth() + mRecyclerViewPaddingLeft + mHeaderLeftMargin + mHeaderRightMargin;
mBottom = mPinnedHeaderView.getMeasuredHeight() + mRecyclerViewPaddingTop + mHeaderTopMargin + mHeaderBottomMargin;
// 位置强制布局在顶部
mPinnedHeaderView.layout(mLeft, mTop, mRight - mHeaderRightMargin, mBottom - mHeaderBottomMargin);
if (mItemTouchListener == null) {
mItemTouchListener = new OnItemTouchListener<T>(parent.getContext());
parent.addOnItemTouchListener(mItemTouchListener);
if (mHeaderClickListener != null) {
mItemTouchListener.setHeaderClickListener(mHeaderClickListener);
mItemTouchListener.disableHeaderClick(mDisableHeaderClick);
}
// OnItemTouchListener.HEADER_ID代表是标签的Id
mItemTouchListener.setViewAndBounds(OnItemTouchListener.HEADER_ID, new ClickBounds(mLeft, mTop, mRight, mBottom));
if (mHeaderClickListener != null && mClickIds != null && mClickIds.length > 0) {
for (int mClickId : mClickIds) {
final View view = mPinnedHeaderView.findViewById(mClickId);
mItemTouchListener.setViewAndBounds(mClickId,
new ClickBounds(view.getLeft(), view.getTop(), view.getLeft() + view.getMeasuredWidth(), view.getTop() + view.getMeasuredHeight()));
}
}
}
if (mHeaderClickListener != null) {
mItemTouchListener.setClickHeaderInfo(mPinnedHeaderPosition, (T) ((PinnedHeaderNotifyer) mAdapter).getPinnedHeaderInfo(mPinnedHeaderPosition));
}
}
}
/**
* 从传入位置递减找出标签的位置
*
* @param formPosition
* @return
*/
private int findPinnedHeaderPosition(int formPosition) {
for (int position = formPosition; position >= 0; position--) {
// 位置递减,只要查到位置是标签,立即返回此位置
final int type = mAdapter.getItemViewType(position);
if (isPinnedHeaderType(type)) {
return position;
}
}
return 0;
}
/**
* 通过适配器告知类型是否为标签
*
* @param type
* @return
*/
private boolean isPinnedHeaderType(int type) {
return ((PinnedHeaderNotifyer) mAdapter).isPinnedHeaderType(type);
}
/**
* 找出第一个可见的Item的位置
*
* @param layoutManager
* @return
*/
private int findFirstVisiblePosition(RecyclerView.LayoutManager layoutManager) {
int firstVisiblePosition = 0;
if (layoutManager instanceof GridLayoutManager) {
firstVisiblePosition = ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition();
} else if (layoutManager instanceof LinearLayoutManager) {
firstVisiblePosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
int[] into = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];
((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(into);
firstVisiblePosition = Integer.MAX_VALUE;
for (int pos : into) {
firstVisiblePosition = Math.min(pos, firstVisiblePosition);
}
}
return firstVisiblePosition;
}
/**
* 检查缓存
*
* @param parent
*/
private void checkCache(final RecyclerView parent) {
final RecyclerView.Adapter adapter = parent.getAdapter();
if (mAdapter != adapter) {
// 适配器为null或者不同,清空缓存
mPinnedHeaderView = null;
mPinnedHeaderPosition = -1;
// 明确了适配器必须继承PinnedHeaderNotifyer接口,因为没有这个就获取不到哪个位置对应的类型是标签类型
if (adapter instanceof PinnedHeaderNotifyer) {
mAdapter = adapter;
} else {
throw new IllegalStateException("Adapter must implements " + PinnedHeaderNotifyer.class.getSimpleName());
}
}
}
/**
* 适用于网格布局,用于判断是否是第一列
*
* @param parent
* @param pos
* @param spanCount
* @return
*/
private boolean isFirstColumn(RecyclerView parent, int pos, int spanCount) {
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
final int headerPosition = findPinnedHeaderPosition(pos);
if ((pos - (headerPosition + 1)) % spanCount == 0) {
// 找到头部位置减去包括头部位置之前的个数
return true;
}
}
return false;
}
private int getSpanCount(RecyclerView parent) {
// 列数
int spanCount = -1;
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
spanCount = ((StaggeredGridLayoutManager) layoutManager).getSpanCount();
}
return spanCount;
}
public static class Builder<T> {
private OnHeaderClickListener<T> headerClickListener;
private int dividerId;
private boolean enableDivider;
private int[] clickIds;
public boolean disableHeaderClick;
public Builder() {
}
/**
* 设置标签监听,若设置点击监听不为null,并且开启标签的点击监听,那么标签的点击回调返回的id为ItemTouchListener.HEADER_ID
*
* @param headerClickListener 监听,若不设置这个setClickIds无效
* @return 构建者
*/
public Builder<T> setHeaderClickListener(OnHeaderClickListener<T> headerClickListener) {
this.headerClickListener = headerClickListener;
return this;
}
/**
* 设置分隔线资源ID
*
* @param dividerId 资源ID,若不设置这个并且enableDivider=true时,使用默认的分隔线
* @return 构建者
*/
public Builder<T> setDividerId(int dividerId) {
this.dividerId = dividerId;
return this;
}
/**
* 是否开启绘制分隔线,默认关闭
*
* @param enableDivider true为绘制,false不绘制,false时setDividerId无效
* @return 构建者
*/
public Builder<T> enableDivider(boolean enableDivider) {
this.enableDivider = enableDivider;
return this;
}
/**
* 通过传入包括标签和其内部的子控件的ID设置其对应的点击事件
*
* @param clickIds 标签或其内部的子控件的ID
* @return 构建者
*/
public Builder<T> setClickIds(int... clickIds) {
this.clickIds = clickIds;
return this;
}
/**
* 开启或关闭标签点击事件(不包括标签里面的子控件),默认开启,当setHeaderClickListener不为null时有效
*
* @param disableHeaderClick true为关闭标签点击事件,false为开启标签点击事件
* @return 构建者
*/
public Builder<T> disableHeaderClick(boolean disableHeaderClick) {
this.disableHeaderClick = disableHeaderClick;
return this;
}
public PinnedHeaderItemDecoration<T> create() {
return new PinnedHeaderItemDecoration<T>(this);
}
}
}