RecyclerView 中 ItemDecoration 使用细节及逻辑分析 (粘性头部)

RecyclerView 中 ItemDecoration 使用细节及逻辑分析 (粘性头部),第1张

概述这里写个简单的例子,给RecyclerView添加一个红色的分割线。上一章中,我们写了这么个简单的例子,现在就简单的分析一下。publicclassColorDividerItemDecorationextendsRecyclerView.ItemDecoration{  finalstaticStringTAG="ColorDividerItem";  private

这里写个简单的例子,给 RecyclerVIEw 添加一个红色的分割线。上一章中,我们写了这么个简单的例子,现在就简单的分析一下。

public class colordivIDerItemdecoration extends RecyclerVIEw.Itemdecoration {

    final static String TAG = "colordivIDerItem";

    private float mdivIDerHeight;

    private Paint mPaint;

    public colordivIDerItemdecoration() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setcolor(color.RED);
    }

    @OverrIDe
    public voID getItemOffsets(Rect outRect, VIEw vIEw, RecyclerVIEw parent, RecyclerVIEw.State state) {
//       第一个ItemVIEw不需要在上面绘制分割线
        if (parent.getChildAdapterposition(vIEw) != 0){
            //这里直接硬编码为1px
            outRect.top = 1;
            mdivIDerHeight = 1;
        }
    }

    @OverrIDe
    public voID onDraw(Canvas c, RecyclerVIEw parent, RecyclerVIEw.State state) {

        int childCount = parent.getChildCount();
        for ( int i = 0; i < childCount; i++ ) {
            VIEw vIEw = parent.getChildAt(i);
            int index = parent.getChildAdapterposition(vIEw);
            //第一个ItemVIEw不需要绘制
            if ( index == 0 ) {
                continue;
            }
            float divIDertop = vIEw.gettop() - mdivIDerHeight;
            float divIDerleft = parent.getpaddingleft();
            float divIDerBottom = vIEw.gettop();
            float divIDerRight = parent.getWIDth() - parent.getpaddingRight();
            c.drawRect(divIDerleft,divIDertop,divIDerRight,divIDerBottom,mPaint);
        }
    }

}

我们使用 linearLayout 布局时,属性设置为垂直,往里面塞了三个vIEw,那么三个vIEw会依次从上到下排布;如果是 FrameLayout 布局,没有设置 gravity 属性,那么三个vIEw会按顺序依次叠加在一起;使用 ListVIEw 时,我们能看到 item 一条接一条,紧密挨在一起;使用 RecyclerVIEw 时,设置垂直属性,效果与 ListVIEw 雷同。刚开始时,这样的效果给我造成了一个错觉,一直觉得 item 在复用,item 之间就应该是紧挨在一起,没有空隙的,后来随着接触的东西多了,见识广了,才知道当初的无知有多么的可笑。VIEwGroup 中的子vIEw的大小与布局,都在父控件的掌控之中,换句话说,父控件决定子控件的位置,所以只要我们喜欢,完全可以让item之间隔个100像素,左边都不与父控件左边对齐。上一章中,我们看到了 RecyclerVIEw 在布局item时,把从 Itemdecoration 中 getItemOffsets() 方法中获取的数据都用上了,这就导致 layout 的时候,如果 getItemOffsets() 中返回有值,那么子vIEw也就是 item 之间就有间隙了。这个时候,item 会绘制出自己的布局,间隙就显示父控件的背景颜色,就像上面 colordivIDerItemdecoration 中,如果我们把 onDraw() 方法中的代码移除,同时在 xml 布局中把 RecyclerVIEw 的背景色设置为红色,此时也能达到 item 之间添加红线的效果。


我们不通过xml这种方法,而是在 Itemdecoration 把它给画出来,并且还能实现更复杂的画面。我们看看 onDraw() 方法中,首先是 int childCount = parent.getChildCount() 获取到 RecyclerVIEw 的子vIEw 的个数,注意假如 childCount 为8,则说明 RecyclerVIEw 中有8个子vIEw,并不代表 Adapter 中只有8个item,因为子 vIEw 是可以复用的,所以我们根据 VIEw vIEw = parent.getChildAt(i) 这里的 i 与 Adapter 中的 position 并不是一回事,因此才有了 getChildAdapterposition(vIEw) 方法,通过反查来查出了 position 的值,即当前 item 在 RecyclerVIEw 中的位置。item之间的红色间隔线,是从0开始,倒数第二个结束,最后一个item是没有间隔线,我们有两种绘制方法,一是在item的底部绘制,最后一条不绘制;二是在item的顶部绘制,第0个item不绘制。这里我们用的是方法二,所以当 index 为 0 时,跳过当前。vIEw.gettop() 获取的是 item 距离 RecyclerVIEw 顶部的距离,暂时可以认为随着 RecyclerVIEw 上下滑动,每个item的 top 值是不停变化的,由于我们要绘制间隔线,所以item的top就是间隔线的bottom,那间隔线的top就是在底部的基础上向上移动 mdivIDerHeight 的距离,由于屏幕左上角为坐标原点,所以间隔线的top就是在bottom的基础上减去 mdivIDerHeight;绘制间隔线,没能超过父容器的约束的距离,比如RecyclerVIEw 设置了 padding 值,我们也要遵守,所以 left 就要从父容器的 paddingleft 开始,right 则要父容器的宽度减去 paddingRight。计算出间隔线的四个顶点的坐标后,用 Canvas 和 Paint 把它画出来,就像在自定义view控件中一样,就是这样。


如果我们想实现微信中通讯录模块,一个列表按照姓氏分类,拼音的首字母现在在同类型的最上端类似的功能,我们就可以像做分割线一样来实现它,还有一种思路,就是在item中显示拼音首字母,根据名字中计算哪个需要隐藏,哪个需要显示。我们先说说用item实现,再说用 Itemdecoration 实现。

    public List<Bean> getData() {
        List<Bean> List = new ArrayList<>();

        for (int index = 0; index < 50; index++) {
            if (index < 15) {
                List.add(new Bean(
                        "粘性文本1", "name" + index));
            } else if (index < 25) {
                List.add(new Bean(
                        "粘性文本2", "name" + index));
            } else if (index < 35) {
                List.add(new Bean(
                        "粘性文本3", "name" + index));
            } else {
                List.add(new Bean(
                        "粘性文本4", "name" + index));
            }
        }
        return List;
    }


    class StickyAdapter extends RecyclerVIEw.Adapter<StickyAdapter.RecyclerVIEwHolder> {
        //第一个吸顶
        static final int FirsT_STICKY_VIEW = 1;
        //别的吸顶
        static final int OTHER_STICKY_VIEW = 2;
        //正常VIEw
        static final int norMAL_VIEW = 3;

        private final LayoutInflater mInflate;
        private final List<Bean> datas;

        StickyAdapter(Context context, List<Bean> datas){
            mInflate = LayoutInflater.from(context);
            this.datas = datas;
        }

        @OverrIDe
        public RecyclerVIEwHolder onCreateVIEwHolder(VIEwGroup parent, int vIEwType) {

            VIEw inflate = mInflate.inflate(R.layout.item_ui, parent, false);
            return new RecyclerVIEwHolder(inflate);
        }

        @OverrIDe
        public voID onBindVIEwHolder(RecyclerVIEwHolder holder, int position) {
            Bean stickyBean = datas.get(position);
            holder.tvname.setText(stickyBean.name);

            if (position == 0) {
                holder.tvStickyheader.setVisibility(VIEw.VISIBLE);
                holder.tvStickyheader.setText(stickyBean.sticky);
                holder.itemVIEw.setTag(FirsT_STICKY_VIEW);
            } else {
                if (!TextUtils.equals(stickyBean.sticky, datas.get(position - 1).sticky)) {
                    holder.tvStickyheader.setVisibility(VIEw.VISIBLE);
                    holder.tvStickyheader.setText(stickyBean.sticky);
                    holder.itemVIEw.setTag(OTHER_STICKY_VIEW);
                } else {
                    holder.tvStickyheader.setVisibility(VIEw.GONE);
                    holder.itemVIEw.setTag(norMAL_VIEW);
                }
            }
            //通过此处设置ContentDescription,作为内容描述,可以通过getContentDescription取出,功效跟setTag差不多。
            holder.itemVIEw.setContentDescription(stickyBean.sticky);
        }

        @OverrIDe
        public int getItemCount() {
            return datas == null ? 0 : datas.size();
        }

        public class RecyclerVIEwHolder extends RecyclerVIEw.VIEwHolder{
            TextVIEw tvStickyheader;
            relativeLayout rlContentWrapper;
            TextVIEw tvname;
            RecyclerVIEwHolder(VIEw itemVIEw) {
                super(itemVIEw);
                tvStickyheader = (TextVIEw) itemVIEw.findVIEwByID(R.ID.tv_sticky_header_vIEw);
                rlContentWrapper = (relativeLayout) itemVIEw.findVIEwByID(R.ID.rl_content_wrapper);
                tvname = (TextVIEw) itemVIEw.findVIEwByID(R.ID.name);
            }
        }
    }

    public class Bean {

        public String name;
        public String sticky;

        public Bean(String sticky, String name) {
            this.sticky = sticky;
            this.name = name;
        }
    }

xml 布局

<?xml version="1.0" enCoding="utf-8"?>
<FrameLayout xmlns:androID="http://schemas.androID.com/apk/res/androID"
    xmlns:tools="http://schemas.androID.com/tools"
    androID:layout_wIDth="match_parent"
    androID:layout_height="match_parent"
    androID:background="@color/color_10"
    >

    <androID.support.v7.Widget.RecyclerVIEw
        androID:ID="@+ID/Feed_List"
        androID:layout_wIDth="match_parent"
        androID:layout_height="match_parent"
        androID:background="@androID:color/white"
        androID:scrollbars="vertical" />

    <TextVIEw
        androID:ID="@+ID/tv_suspensionbar"
        androID:layout_wIDth="match_parent"
        androID:layout_height="50dp"
        androID:background="#EFFAE7"
        androID:gravity="center"
        androID:text="@string/hello_blank_fragment" />

</FrameLayout>

注意看 onBindVIEwHolder() 中,我们根据 sticky 值不一样,说明到了不同的文案的交错点,所以要显示出头部文案。如果此时要加一个粘性头部,就像微信ios版通讯录的效果,此时添加滑动监听机制

Activity 中布局

<?xml version="1.0" enCoding="utf-8"?>
<FrameLayout xmlns:androID="http://schemas.androID.com/apk/res/androID"
    xmlns:tools="http://schemas.androID.com/tools"
    androID:layout_wIDth="match_parent"
    androID:layout_height="match_parent"
    androID:background="@color/color_10"
    >

    <androID.support.v7.Widget.RecyclerVIEw
        androID:ID="@+ID/Feed_List"
        androID:layout_wIDth="match_parent"
        androID:layout_height="match_parent"
        androID:background="@androID:color/white"
        androID:scrollbars="vertical" />

    <TextVIEw
        androID:ID="@+ID/tv_suspensionbar"
        androID:layout_wIDth="match_parent"
        androID:layout_height="50dp"
        androID:background="#EFFAE7"
        androID:gravity="center"
        androID:text="@string/hello_blank_fragment" />

</FrameLayout>

mSuspensionbar 是 @+ID/tv_suspensionbar;  mRecyclerVIEw 是 @+ID/Feed_List

        mRecyclerVIEw.addOnScrollListener(new RecyclerVIEw.OnScrollListener() {
            @OverrIDe
            public voID onScrolled(RecyclerVIEw recyclerVIEw, int dx, int dy) {
                super.onScrolled(recyclerVIEw, dx, dy);
                VIEw stickvIEw = recyclerVIEw.findChildVIEwUnder(0, 0);
                if (stickvIEw != null && stickvIEw.getContentDescription() != null) {
                    if (!TextUtils.equals(mSuspensionbar.getText(), stickvIEw.getContentDescription())) {
                        mSuspensionbar.setText(stickvIEw.getContentDescription());
                    }
                }
                VIEw transInfoVIEw = recyclerVIEw.findChildVIEwUnder(0, mSuspensionbar.getHeight() + 1);
                if (transInfoVIEw.getTag() != null) {
                    int transVIEwStatus = (int) transInfoVIEw.getTag();
                    int top = transInfoVIEw.gettop();
                    if (transVIEwStatus == StickyAdapter.OTHER_STICKY_VIEW) {
                        if (top > 0) {
                            int dealtY = top - mSuspensionbar.getMeasuredHeight();
                            mSuspensionbar.setTranslationY(dealtY);
                        } else {
                            mSuspensionbar.setTranslationY(0);
                        }
                    } else {
                        mSuspensionbar.setTranslationY(0);
                    }
                }
            }
        });

VIEw stickvIEw = recyclerVIEw.findChildVIEwUnder(0, 0) 找到的最上面的item, VIEw transInfoVIEw = recyclerVIEw.findChildVIEwUnder(0, mSuspensionbar.getHeight() + 1)  找到的是在 mSuspensionbar 这个控件下面1像素的地方所在的item,所以就有了下面的逻辑:一、如果列表position在15之前,transVIEwStatus 为 FirsT_STICKY_VIEW 或 norMAL_VIEW,此时 mSuspensionbar 待在原始位置即可;二、position到了15,transInfoVIEw 对应的 position 为 15 时,说明此时两个头部布局要接壤了,此时需要下面的把上面的给顶上去,怎么顶?计算距离,让 mSuspensionbar 随着RecyclerVIEw列表的滑动向上移动,所以有了 int top = transInfoVIEw.gettop(),此时 int dealtY = top - mSuspensionbar.getMeasuredHeight()算出了位移;如果 top 小于0,说明该item已经向上滑出了 RecyclerVIEw 的范围,所以就不用管了,把 mSuspensionbar 复原;三、接着又是类似一的逻辑,然后是二,就这样循环到结束。


上述如果使用 Itemdecoration 怎么实现呢?换一种思路和对象集合,我们先把对象在集合中分好组,添加属性来标识位置,

    class GroupInfo {
        private String content;
        private String mTitle;
        private boolean isFirstVIEwInGroup;
        private boolean isLastVIEwInGroup;


        public GroupInfo(String content, String Title) {
            this.content = content;
            this.mTitle = Title;
        }

        public String getContent() {
            return content;
        }

        public String getTitle() {
            return mTitle;
        }

        public voID setFirstVIEwInGroup(boolean firstVIEwInGroup) {
            isFirstVIEwInGroup = firstVIEwInGroup;
        }

        public voID setLastVIEwInGroup(boolean lastVIEwInGroup) {
            isLastVIEwInGroup = lastVIEwInGroup;
        }

        public boolean isFirstVIEwInGroup () {
            return isFirstVIEwInGroup;
        }

        public boolean isLastVIEwInGroup () {
            return isLastVIEwInGroup;
        }

    }

Activity 中代码 
    List<GroupInfo> List = getData();
    mRecyclerVIEw.setAdapter(new TestAdapter(List));
    mRecyclerVIEw.addItemdecoration(new Sectiondecoration(context, List));

适配器
    class TestAdapter extends RecyclerVIEw.Adapter<TestAdapter.TestHolder> {
        private final List<GroupInfo> datas;


        TestAdapter(List<GroupInfo> datas){
            this.datas = datas;
        }

        @OverrIDe
        public TestHolder onCreateVIEwHolder(VIEwGroup parent, int vIEwType) {
            TextVIEw tv = new TextVIEw(parent.getContext());
            tv.setLayoutParams(new VIEwGroup.LayoutParams(VIEwGroup.LayoutParams.MATCH_PARENT, 120));
            tv.setGravity(Gravity.CENTER_VERTICAL);
            TestHolder holder = new TestHolder(tv);
            return holder;
        }

        @OverrIDe
        public voID onBindVIEwHolder(TestHolder holder, int position) {
            if (datas != null && datas.size() > 0 ) {
                String text = datas.get(position).getContent();
                holder.tvname.setText(text);
            }
        }

        @OverrIDe
        public int getItemCount() {
            return datas == null ? 0 : datas.size();
        }

        public class TestHolder extends RecyclerVIEw.VIEwHolder{
            TextVIEw tvname;
            TestHolder(VIEw itemVIEw) {
                super(itemVIEw);
                tvname = (TextVIEw) itemVIEw;
            }
        }
    }

重点来了,

    class Sectiondecoration extends RecyclerVIEw.Itemdecoration {

        private List<GroupInfo> mList;
        private int mheaderHeight;
        private int mdivIDerHeight;

        //用来绘制header上的文字
        private TextPaint mTextPaint;
        private Paint mPaint;
        private float mTextSize;
        private Paint.FontMetrics mFontMetrics;

        public Sectiondecoration(Context context, List<GroupInfo> List) {
            this.mList = List;
            mdivIDerHeight = context.getResources().getDimensionPixelOffset(R.dimen.header_divIDer_height);
            mheaderHeight = context.getResources().getDimensionPixelOffset(R.dimen.header_height);
            mTextSize = context.getResources().getDimensionPixelOffset(R.dimen.header_textsize);

            mheaderHeight = (int) Math.max(mheaderHeight,mTextSize);

            mTextPaint = new TextPaint();
            mTextPaint.setcolor(color.BLACK);
            mTextPaint.setTextSize(mTextSize);
            mFontMetrics = mTextPaint.getFontMetrics();

            mPaint = new Paint();
            mPaint.setAntiAlias(true);
            mPaint.setcolor(color.YELLOW);

        }

        @OverrIDe
        public voID getItemOffsets(Rect outRect, VIEw vIEw, RecyclerVIEw parent, RecyclerVIEw.State state) {
            super.getItemOffsets(outRect, vIEw, parent, state);

            int position = parent.getChildAdapterposition(vIEw);

            if ( mList != null ) {
                GroupInfo groupInfo = mList.get(position);

                //如果是组内的第一个则将间距撑开为一个header的高度,或者就是普通的分割线高度
                if ( groupInfo != null && groupInfo.isFirstVIEwInGroup() ) {
                    outRect.top = mheaderHeight;
                } else {
                    outRect.top = mdivIDerHeight;
                }
            }
        }

        @OverrIDe
        public voID onDraw(Canvas c, RecyclerVIEw parent, RecyclerVIEw.State state) {
            super.onDraw(c, parent, state);

            int childCount = parent.getChildCount();

            for ( int i = 0; i < childCount; i++ ) {
                VIEw vIEw = parent.getChildAt(i);

                int index = parent.getChildAdapterposition(vIEw);

                if ( mList != null ) {
                    GroupInfo groupinfo = mList.get(index);
                    //只有组内的第一个ItemVIEw之上才绘制
                    if ( groupinfo.isFirstVIEwInGroup() ) {
                        int left = parent.getpaddingleft();
                        int top = vIEw.gettop() - mheaderHeight;
                        int right = parent.getWIDth() - parent.getpaddingRight();
                        int bottom = vIEw.gettop();
                        //绘制header
                        c.drawRect(left,top,right,bottom,mPaint);

                        float TitleX =  left;
                        float TitleY =  bottom - mFontMetrics.descent;
                        //绘制Title
                        c.drawText(groupinfo.getTitle(),TitleX,TitleY,mTextPaint);
                    }
                }
            }
        }

    }

这样,我们在 getItemOffsets() 中计算出item之间的间隙,然后在 onDraw() 中把头部画出来。如果想画出粘性头部布局呢?我们此时重写 onDrawOver() 方法,

        @OverrIDe
        public voID onDrawOver(Canvas c, RecyclerVIEw parent, RecyclerVIEw.State state) {

            VIEw vIEw = parent.getChildAt(0);
            int index = parent.getChildAdapterposition(vIEw);
            if (mList != null) {
                GroupInfo groupinfo = mList.get(index);
                int left = parent.getpaddingleft();
                int right = parent.getWIDth() - parent.getpaddingRight();
                int top = parent.getpaddingtop();
                if (groupinfo.isLastVIEwInGroup()) {
                    int suggesttop = vIEw.getBottom() - mheaderHeight;
                    if (suggesttop < top) {
                        top = suggesttop;
                    }
                }
                int bottom = top + mheaderHeight;
                drawheaderRect(c, groupinfo, left, top, right, bottom);
            }
        }

        private voID drawheaderRect(Canvas c, GroupInfo groupinfo, int left, int top, int right, int bottom) {
            //绘制header
            c.drawRect(left,top,right,bottom,mPaint);
            float TitleX =  left + mTextOffsetX;
            float TitleY =  bottom - mFontMetrics.descent;
            //绘制Title
            c.drawText(groupinfo.getTitle(),TitleX,TitleY,mTextPaint);
        }

这个里面的原理与方法一种的滑动监听的原理相似,这里是直接获取RecyclerVIEw中首个item的vIEw,然后获取到位置position。注意,此时不同组的item是有间隔的,正常情况下,我们绘制最上层的粘性头部时,把它固定在顶部即可,即 top 为  parent.getpaddingtop(),bottom 为 top + mheaderHeight,然后通过 Canvas 把它画出来。最关键的一点就是两个粘性头部接壤了,此时需要把上面一个顶上,如果RecyclerVIEw中首个item的底部到父容器的距离小于了 mheaderHeight,说明两个头部接壤了,同理,算出它们的距离,然后算出 top 的值,理论上和方法一同样的原理。但此时这里需要注意一点,就是 Adapter 中的item的高度,要大于粘性头部布局的高度。
 

总结

以上是内存溢出为你收集整理的RecyclerView 中 ItemDecoration 使用细节及逻辑分析 (粘性头部)全部内容,希望文章能够帮你解决RecyclerView 中 ItemDecoration 使用细节及逻辑分析 (粘性头部)所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/1103893.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-28
下一篇 2022-05-28

发表评论

登录后才能评论

评论列表(0条)

保存