Android 使用RecycleView实现吸附小标题的Demo(附源码)

先上,效果图

《Android 使用RecycleView实现吸附小标题的Demo(附源码)》

源码地址

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); //恢复指定层的图层
    }
}

源码地址

GitHub: https://github.com/aiyangtianci/StickyDecoration

码云  :  https://gitee.com/AiYangDian/StickyDecoration

    原文作者:艾阳丶
    原文地址: https://blog.csdn.net/csdn_aiyang/article/details/80096806
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞