先上,效果图



源码地址

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 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 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


更多相关文章

  1. android 绘制图片的一部分
  2. Android(安卓)欢迎全屏图片详解及实例代码
  3. Android(安卓)自定义标题栏
  4. android六大布局和UI组件
  5. Android(安卓)标题栏和状态栏随ScrollView滑动颜色改变轻松实现
  6. Android(安卓)SurfaceView使用详解(很好的实战例子)
  7. Android的图形与图像处理之一 使用简单图片&绘图
  8. android TextInputLayout 更换系统自带眼睛图标
  9. LayoutInflater的使用

随机推荐

  1. 使Android支持Lambda表达式
  2. Android(安卓)之 ProgressBar用法介绍
  3. android jni
  4. Android(安卓)OEM 厂商提供的 USB驱动
  5. Android(安卓)仿苹果底部弹出Dialog
  6. Android(安卓)文件下载与解压缩
  7. android分享应用工具类
  8. java android 获取手机操作系统相关信息
  9. android 跳转并传递参数
  10. android通知栏响应事件