先上,效果图
源码地址
GitHub: https://github.com/aiyangtianci/StickyDecoration
码云 : https://gitee.com/AiYangDian/StickyDecoration
因为实现列表展示的数据和基础实现在上一章讲解了,请看上一篇:
Android 探究onCreateViewHolder和onBindViewHolder两者关系和调用次数
当然,那篇文章还算优美,如果同学只想知道如何实现吸附的小标题,也可以忽略上一篇。请继续往下看。
介绍:RecyclerView.ItemDecoration
ItemDecoration是RecyclerView的一个
抽象内部类,这里的效果需要实现它。它有三个重写的方法,如下:
//可以实现类似padding的效果,实现列表列表间隔
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
//可以实现类似绘制背景的效果,内容在上面
public void onDraw(Canvas c, RecyclerView parent, State state)
//可以绘制在内容的上面,覆盖内容
public void onDrawOver(Canvas c, RecyclerView parent, State state)
其中,我们本篇需要的一个非常关键的方法 onDrawOver。
它会先在初始化中执行一次,后面会随着列表滑动重复的调用,非常适合我们实现本篇效果。
onDrawOver与onDraw的调用顺序?
在官方的开发文档中有指出,onDraw是在itemview绘制之前,onDrawOver是在itemview绘制之后。
为什么说onDrawOver是在itemview绘制之后调用呢?
相信稍微了解过Android中View的绘制流程的都知道,View先会调用draw方法,在draw方法中调用onDraw方法。 在RecyclerView的draw方法中会先通过super.draw() 调用父类的draw方法,再调用OnDraw方法。ItemDecoration的onDraw方法也就在此时会被调用。RecyclerView执行完super.draw()之后,ItemDecoration的onDrawOver方法才被调用。(可忽略)
上代码嘞!
记得上一篇说Adapter的onCreateViewHolder()方法参数 int viewType,并且在方法中判断设置item.setTag(boolean);
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View item = LayoutInflater.from(mContext).inflate(R.layout.item_layout , parent ,false);
if (viewType == 1){//标题
item.setTag(true);
}else{
item.setTag(false);
}
return new ViewHolde(item);
}
其实,这个就是为了设置子项是否是吸附的标题。
StickyItemDecoration.java 自定义类
创建 StickyItemDecoration.java类 , 继承RecyclerView.ItemDecoration 。
使用的时候必须在setAdapter前,通过recyclerView.addItemDecoration()方法设置。
案例调用,如下:
public class MainActivity extends AppCompatActivity {
RecyclerView mRecyclerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView =findViewById(R.id.recyclelist);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.addItemDecoration(new StickyItemDecoration()); // 在setAdapter之前。
mRecyclerView.setAdapter(new RecyclerAdapter(this, data.getDataList()));
}
}
索性,我们直接看这个自定义类的全部代码。
呃呃。。。。其实很简单,只是代码有点繁琐,但是每行关键代码我都有注释。在阅读代码之前,我先捋一下思路会阅读更轻松。
首先,前面介绍 onDrawOver 说 “它会先在初始化中执行一次,后面会随着列表滑动重复的调用”。所以我们在阅读代码时,先忽略关于判断标题在滑动时候的绘制位置相关代码。建议debug调试跟着初始化走一遍。
其次,要理解最外层for循环,遍历的是获取屏幕可见子项Item位置,然后判断可见的第几个子元素是标题。
最后,就细细跟着我蹩脚的注释去理解绘制计算布局相关的代码了。 咳咳,我来个祝愿吧~~~ – -。
▇▇▇▇ . ▇▇▇▇ . ▇▇▇▇ ▇▇▇▇
◢▇▇▇▇◣ ◢▇▇▇▇◣ ◢▇▇▇▇◣ ◢▇▇▇▇◣
▇春节快乐▇. ▇生活愉快▇ ▇吉祥如意▇ ▇合家欢乐▇
◥▇▇▇▇◤ .◥▇▇▇▇◤ ◥▇▇▇▇◤ ◥▇▇▇▇◤
▇▇▇▇ . ▇▇▇▇ . ▇▇▇▇ ▇▇▇▇
| | | | | | | | | | | |
/**
* Created by aiyang on 2018/4/25.
*/
public class StickyItemDecoration extends RecyclerView.ItemDecoration{
/**
* Adapter :托管数据集合,为每个子项创建视图
*/
private RecyclerView.Adapter<RecyclerView.ViewHolder> mAdapter;
/**
* 标记:UI滚动过程中是否找到标题
*/
private boolean mCurrentUIFindStickView;
/**
* 标题距离顶部距离
*/
private int mStickyItemViewMarginTop;
/**
* 标题布局高度
*/
private int mItemViewHeight;
/**
* 标题的视图View
*/
private View mStickyItemView;
/**
* 承载子项视图的holder
*/
private RecyclerView.ViewHolder mViewHolder;
/**
* 子项布局管理
*/
private LinearLayoutManager mLayoutManager;
/**
* 绑定数据的position
*/
private int mBindDataPosition = -1;
/**
* 所有标题的position list
*/
private List<Integer> mStickyPositionList = new ArrayList<>();
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
if (parent.getAdapter().getItemCount() <= 0) return; //非空判断
mCurrentUIFindStickView = false;//标记默认不存在小标题
mLayoutManager = (LinearLayoutManager) parent.getLayoutManager();//获取布局管理方式
for (int i =0 ,size = parent.getChildCount() ; i < size ; i++){//viewgroup.getChildCount():获取所有可见子元素个数。
View item = parent.getChildAt(i); //循环得到每一个子项
if ((boolean)item.getTag() == true){//判断第几个子项是标题(值在Adapter中设置)
mCurrentUIFindStickView =true;//标记为true
getStickyViewHolder(parent);//得到标题的 viewHolder
cacheStickyViewPosition(i); //收集标题的 position
if (item.getTop() <= 0) {//标题和父布局的距离。(一般初始化时候先进入)
bindDataForStickyView(mLayoutManager.findFirstVisibleItemPosition(), parent.getMeasuredWidth());//将第一个可见子项位置 和 父布局宽 传入
} else {
if (mStickyPositionList.size() > 0) {
if (mStickyPositionList.size() == 1) {//若只缓存一个标题
bindDataForStickyView(mStickyPositionList.get(0), parent.getMeasuredWidth());
} else {
int currentPosition = mLayoutManager.findFirstVisibleItemPosition() + i;//得到标题在RecyclerView中的position
int indexOfCurrentPosition = mStickyPositionList.lastIndexOf(currentPosition);//根据标题的position获得所在缓存列表中的索引
bindDataForStickyView(mStickyPositionList.get(indexOfCurrentPosition - 1), parent.getMeasuredWidth());
}
}
}
if (item.getTop() > 0 && item.getTop() <= mItemViewHeight) {//处理两个标题叠在一起的绘制效果
mStickyItemViewMarginTop = mItemViewHeight - item.getTop();
} else {
mStickyItemViewMarginTop = 0;
View nextStickyView = getNextStickyView(parent);//得到下一个标题view
if (nextStickyView != null && nextStickyView.getTop() <= mItemViewHeight) {//若两标题叠在一起了
mStickyItemViewMarginTop = mItemViewHeight - nextStickyView.getTop();//第二个标题盖住第一个标题多少了
}
}
drawStickyItemView(c);// 准备工作已就绪,开始画出吸附的标题
break; //结束循环
}
}
if (!mCurrentUIFindStickView) {//取反判断(因为它默认值是false)表示:若存在小标题则进入
mStickyItemViewMarginTop = 0;
//判断子元素等于item总数并且缓存数大于0
if (mLayoutManager.findFirstVisibleItemPosition() + parent.getChildCount() == parent.getAdapter().getItemCount() && mStickyPositionList.size() > 0) {
bindDataForStickyView(mStickyPositionList.get(mStickyPositionList.size() - 1), parent.getMeasuredWidth());
}
drawStickyItemView(c);//绘制图层
}
}
/**
* 得到下一个标题
* @param parent
* @return
*/
private View getNextStickyView(RecyclerView parent) {
int num = 0;
View nextStickyView = null;
for (int m = 0, size = parent.getChildCount(); m < size; m++) {
View view = parent.getChildAt(m);//循环获取每个子项
if ((boolean)view.getTag() == true) {//拿到标题
nextStickyView = view;
num++;
}
if (num == 2) break;//拿到第二个标题 ,就结束循环。
}
return nextStickyView;
}
/**
* 得到标题的 viewHolder
* @param recyclerView
*/
private void getStickyViewHolder(RecyclerView recyclerView) {
if (mAdapter != null) return; //判断是否已创建
mAdapter = recyclerView.getAdapter();
mViewHolder = mAdapter.onCreateViewHolder(recyclerView, 1); //该方法属于Adapter中的重写Override
mStickyItemView = mViewHolder.itemView;//得到布局
}
/**
* 收集标题的 position
* @param i
*/
private void cacheStickyViewPosition(int i) {
int position = mLayoutManager.findFirstVisibleItemPosition() + i;//得到标题在RecyclerView中的position
if (!mStickyPositionList.contains(position)) {//防止重复
mStickyPositionList.add(position);
}
}
/**
* 给StickyView绑定数据
* @param position
*/
private void bindDataForStickyView(int position, int width) {
if (mBindDataPosition == position || mViewHolder == null) return;//已经是吸附位置了 或 视图不存在
mBindDataPosition = position;
mAdapter.onBindViewHolder(mViewHolder, mBindDataPosition);//改变标题的展示效果,该方法在Adapter中
measureLayoutStickyItemView(width);//设置布局位置及大小
mItemViewHeight = mViewHolder.itemView.getBottom() - mViewHolder.itemView.getTop();//计算标题布局高度
}
/**
* 设置布局位置及大小
* @param parentWidth 父布局宽度
*/
private void measureLayoutStickyItemView(int parentWidth) {
if (mStickyItemView == null || !mStickyItemView.isLayoutRequested()) return;
int widthSpec = View.MeasureSpec.makeMeasureSpec(parentWidth, View.MeasureSpec.EXACTLY);
int heightSpec;
ViewGroup.LayoutParams layoutParams = mStickyItemView.getLayoutParams();
if (layoutParams != null && layoutParams.height > 0) {
heightSpec = View.MeasureSpec.makeMeasureSpec(layoutParams.height, View.MeasureSpec.EXACTLY);
} else {
heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
}
mStickyItemView.measure(widthSpec, heightSpec);
/**
* view.layout(l,t,r,b) ; 子布局相对于父布局的绘制的位置及大小。
* l 和 t 是控件左边缘和上边缘相对于父类控件左边缘和上边缘的距离。r 和 b是控件右边缘和下边缘相对于父类控件左边缘和上边缘的距离。
*/
mStickyItemView.layout(0, 0, mStickyItemView.getMeasuredWidth(), mStickyItemView.getMeasuredHeight());
}
/**
* 绘制标题
* @param canvas
*/
private void drawStickyItemView(Canvas canvas) {
if (mStickyItemView == null) return;
int saveCount = canvas.save();//保存当前图层
canvas.translate(0, -mStickyItemViewMarginTop);//图层转换位移
mStickyItemView.draw(canvas);
canvas.restoreToCount(saveCount); //恢复指定层的图层
}
}