RecyclerView 悬浮/粘性头部——StickyHeaderDecoration

转载至:https://blog.csdn.net/qian520ao/article/details/76167193

ItemDecoration是recyclerView拓展的一个很好工具,支持我们在recyclerView上面做各种操作,而且耦合性低,容易添加。这篇我们先用ItemDecoration来做悬浮/粘性头部,后面还可以用ItemDecoration做时间轴,手机通讯录联系人右侧字母导航栏。


老规矩,先上图。




集成方式

github地址:https://github.com/qdxxxx/StickyHeaderDecoration
天气热,本github已安装空调,star即可免费享用~~

  • 注入依赖
    Step 1. Add the JitPack repository to your build file
    Step 2. Add the dependency
    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
    dependencies {
       compile 'com.github.qdxxxx:StickyHeaderDecoration:1.0.1'
    }
   
   
   
   
  • 1
  • 2
  • 3

Activity里面集成代码

  • 分组头部
        NormalDecoration decoration = new NormalDecoration() {
            @Override
            public String getHeaderName(int pos) {
                return //返回每个分组头部名称;
            }
        };
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 自定义头部/悬浮头部layout】【自定义头部加载图片请用 loadImage()方法】

    decoration.setOnDecorationHeadDraw(new NormalDecoration.OnDecorationHeadDraw() {
        @Override
        public View getHeaderView(int pos) {
            return //返回自定义头部view;
        }
    });
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 头部点击事件

  •     decoration.setOnHeaderClickListener(new NormalDecoration.OnHeaderClickListener() {
            @Override
            public void headerClick(int pos) {
            }
        });
       
       
       
       
    • 1
    • 2
    • 3
    • 4
    • 5

    GridLayoutManager请配合GridDecoration使用。


    方法及属性介绍


    name format 中文解释
    setHeaderHeight integer 分组头部高度
    setTextPaddingLeft integer 普通分组头部【只含文字】文字左边距
    setTextSize integer 普通分组头部【只含文字】文字大小
    setTextColor integer 普通分组头部【只含文字】文字颜色
    setHeaderContentColor integer 普通分组头部【只含文字】文字背景颜色
    onDestory 清空数据集合/监听等
    *loadImage String,integer,ImageView 用来加载并刷新图片到分组头部【自定义头部很重要的方法!】



    实现解刨

    又要开始漫天代码的解刨了,非专业战斗人员…请务必耐着性子看。
    首先我们来划分几个主要的功能模块

    • 预留不同分组的头部空间
    • 绘制不同分组头部
    • 绘制悬浮头部
    • 悬浮头部粘性效果(上推效果)
    • 头部点击处理
    • 自定义layout的头部
    • GridLayoutManager的适配


    进击的ItemDecoration

    以下一个段落引用【带心情去旅行】的简书,写的很具体。

    先看下RecyclerView.ItemDecoration的源码(部分):

    public static abstract class ItemDecoration {
        ...
        public void onDraw(Canvas c, RecyclerView parent, State state) {
            onDraw(c, parent);
        }
        public void onDrawOver(Canvas c, RecyclerView parent, State state) {
            onDrawOver(c, parent);
        }
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
            getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                    parent);
        }
    }
       
       
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    里面是我们常用的三个方法:

    • getItemOffsets:通过Rect为每个Item设置偏移,用于绘制Decoration。
    • onDraw:通过该方法,在Canvas上绘制内容,在绘制Item之前调用。(如果没有通过getItemOffsets设置偏移的话,Item的内容会将其覆盖)
    • onDrawOver:通过该方法,在Canvas上绘制内容,在Item之后调用。(画的内容会覆盖在item的上层)

    RecyclerView 的背景、onDraw绘制的内容、Item、onDrawOver绘制的内容,各层级关系如下:

    RecyclerView 悬浮/粘性头部——StickyHeaderDecoration_第1张图片

    表示感谢【带心情去旅行】


    预留不同分组的头部空间

    我们为每个不同头部名称的第一个item设置头部高度

    根据上面的讲解,我们用getItemOffsets()方法设置分组的item头部,我们只要判断当前item和上一个item是否属于同一个group即可。

            /*我们为每个不同头部名称的第一个item设置头部高度*/
            int pos = parent.getChildAdapterPosition(itemView); //获取当前itemView的位置
            String curHeaderName = getHeaderName(pos);         //根据pos获取分组头部名
    
            if (pos == 0 || !curHeaderName.equals(getHeaderName(pos - 1))) {//如果当前位置为0,或者与上一个item头部名不同的,都腾出头部空间
                outRect.top = headerHeight;                                 //设置itemView PaddingTop的距离
            }
       
       
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7



    绘制不同分组头部

    onDrawOver()来绘制分组头部,相当于绘制在item的界面之上(因为item已经设置了偏移)
    和上述方法一样,我们先获得每个分组的位置,然后绘制文字即可(自定义layout亦是如此)

    • 我们先获取当前屏幕所有recyclerView显示的item
    • 如果头部距离顶部==2*headerHeight时,悬浮头部就要向上偏移(上推效果)
    • 头部距离顶部==headerHeight时,悬浮头部偏移headerHeight(推离屏幕效果)
      RecyclerView 悬浮/粘性头部——StickyHeaderDecoration_第2张图片
            int childCount = recyclerView.getChildCount();//获取屏幕上可见的item数量
    
       
       
       
       
    • 1
    • 2
            for (int i = 0; i < childCount; i++) {
                View childView = recyclerView.getChildAt(i);
                int pos = recyclerView.getChildAdapterPosition(childView); //获取当前view在Adapter里的pos
                String curHeaderName = getHeaderName(pos);                 //根据pos获取要悬浮的头部名
                int viewTop = childView.getTop() + recyclerView.getPaddingTop();
                if (pos == 0 || !curHeaderName.equals(getHeaderName(pos - 1))) {//如果当前位置为0,或者与上一个item头部名不同的,都腾出头部空间
                    //绘制每个组头【奥迪上头的a(阿尔法罗密欧上头就不用绘制a),本田上头的b】
    
                canvas.drawRect(left, viewTop - headerHeight, right, viewTop, mHeaderContentPaint);//绘制头部背景
                canvas.drawText(curHeaderName, left + textPaddingLeft, viewTop - headerHeight / 2 + txtYAxis, mHeaderTxtPaint);//绘制文字,文字的基线可以看我的自定义菜单,有说到
    
                    if (headerHeight < viewTop && viewTop <= 2 * headerHeight) { //此判断是刚好2个头部碰撞,悬浮头部就要偏移
                        translateTop = viewTop - 2 * headerHeight;//悬浮头部需要偏移的距离(y轴方向)
                    }
    
    stickyHeaderPosArray.put(pos, viewTop);//将头部信息放进array,【头部点击处理有讲解】
                }
            }
    
       
       
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19



    绘制悬浮头部

    通过上面的方法,我们就能绘制出每个分组的头部。最后我们绘制一次悬浮的头部

            canvas.save();
            canvas.translate(0, translateTop);
            canvas.drawRect(left, 0, right, headerHeight, mHeaderContentPaint);
            canvas.drawText(firstHeaderName, left + textPaddingLeft, headerHeight / 2 + txtYAxis, mHeaderTxtPaint);
    //      canvas.drawLine(0, headerHeight / 2, right, headerHeight / 2, mHeaderTxtPaint);//画条线看看文字居中不
            canvas.restore();
    
       
       
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7



    头部点击处理

    头部点击这个一开始的确有点棘手,因为这个分组的头部是我们额外绘制上的,就必须要通过自己的计算和存储头部信息。
    我们在绘制头部的时候,通过SparseArray将头部信息存储集合里,但是每onDrawOver的时候都要clear一下,确保头部数据正确。
    最后通过GestureDetector来处理用户触摸事件,根据用户触摸的y轴位置来判断SparseArray是否包含该位置。

            @Override//单击事件
            public boolean onSingleTapUp(MotionEvent e) {
                for (int i = 0; i < stickyHeaderPosArray.size(); i++) {
                    int value = stickyHeaderPosArray.valueAt(i);
                    float y = e.getY();
                    if (value - headerHeight <= y && y <= value) {//如果点击到分组头
                        if (headerClickEvent != null) {
                            headerClickEvent.headerClick(stickyHeaderPosArray.keyAt(i));
                        }
                        return true;
                    }
                }
                return false;
            }
       
       
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14



    自定义layout的头部

    绘制自定义layout的头部有2个要点

    • 如何将layout布局绘制到canvas上
    • 如果layout里有图片,图片加载完成后需要通知canvas刷新,以显示头部图片(否则需要用户滑动才能更新图片)
    绘制view到canvas

    我们可以通过view.setDrawingCacheEnabled(true)方法,通过cache将view转化为bitmap,在用headerView.getDrawingCache()获取bitmap对象。

        View headerView = headerDrawEvent.getHeaderView(firstPos);
        headerView.measure(//measure布局
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
        headerView.setDrawingCacheEnabled(true);
        headerView.layout(0, 0, right, headerHeight);//布局layout
        canvas.drawBitmap(headerView.getDrawingCache(), left, 0, null);
       
       
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    但是如果view里面包含图片的话,图片不太可能是我们事先存储好的,而是通过网络请求获得的图片url,然后再加载。所以这也是一个难点之一。


    通过url绘制图片到头部
    • 如果图片暂未加载完成,通过Glide加载,加载完成后通过map集合来存储图片。
    • 图片加载完成后用mRecyclerView.postInvalidate(),从而间接性的手动调用onDrawOver()方法,重新绘制已经加载好的图片。
    public void loadImage(final String url, final int pos, ImageView imageView) {
            if (imgDrawableMap.get(url) != null) {//如果图片已经加载过了,并且已经存储
                imageView.setImageDrawable(imgDrawableMap.get(url));
            } else {
                Glide.with(mRecyclerView.getContext()).load(url).into(new SimpleTarget() {
                    @Override
                    public void onResourceReady(Drawable resource, Transitionsuper Drawable> transition) {
    
                        headViewMap.remove(pos);//删除,重新更新
                        imgDrawableMap.put(url, resource);
                        mRecyclerView.postInvalidate();
                    }
                });
            }
    
        }
       
       
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    更多详细功能请移步NormalDecoration,并配合onDrawOver()解析。
    所以自定义layout有图片请务必使用loadImage()方法,以便及时讲加载完的图片绘制到界面上。




    GridLayoutManager的适配

    GridGridDecoration也有2个难点突破

    • 设置item的getItemOffsets,不仅仅是分组头
    • 设置当前分组的最后一个item的Span.
    • 其它的就不需要我们设置了,normalDecoration已经帮我们完成了【自信回头】

    RecyclerView 悬浮/粘性头部——StickyHeaderDecoration_第3张图片

    public abstract class GridDecoration extends NormalDecoration {
        private int itemTotalCount;
    
        public GridDecoration(int itemTotalCount, int span) {
            this.itemTotalCount = itemTotalCount;
            for (int pos = 0; pos < itemTotalCount; pos++) {
                /*我们为每个不同头部名称的第一个item设置头部高度*/
                String curHeaderName = getRealHeaderName(pos);         //根据j获取要悬浮的头部名
                if (!headerPaddingSet.contains(pos) && (pos == 0 || !curHeaderName.equals(getRealHeaderName(pos - 1)))) {//如果是分组头部
                    groupHeadPos.add(pos);
                    for (int i = 0; i < span; i++) {
                        headerPaddingSet.add(pos + i);
                        if (!curHeaderName.equals(getRealHeaderName(pos + i + 1))) {//如果下一个分组名称不一致,pass
                            break;
                        }
                    }
                }
                if (!curHeaderName.equals(getRealHeaderName(pos + 1)) && groupHeadPos.size() > 0) {
                    int preHeadPos = (int) ((TreeSet) (groupHeadPos)).last();
                    int padSpan = span - (pos - preHeadPos) % span;
                    headerSpanArray.put(pos, padSpan);
                }
            }
        }
    
        private Set headerPaddingSet = new TreeSet<>();                //用来记录每个头部的paddintTop信息
        private Set groupHeadPos = new TreeSet<>();                    //记录每个分组第一个头部的pos【用于计算当前组最后一个item的span】
        private SparseArray headerSpanArray = new SparseArray<>();     //用来记录每个分组最后一个item的span
        private GridLayoutManager.SpanSizeLookup lookup;
    
        @Override
        public void getItemOffsets(Rect outRect, View itemView, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, itemView, parent, state);
            if (lookup == null) {
                lookup = new GridLayoutManager.SpanSizeLookup() {//相当于weight
                    @Override
                    public int getSpanSize(int position) {
                        int returnSpan = 1;
                        int index = headerSpanArray.indexOfKey(position);
                        if (index >= 0) {
                            returnSpan = headerSpanArray.valueAt(headerSpanArray.indexOfKey(position));   //设置itemView PaddingTop的距离
                        }
    
                        return returnSpan;
                    }
                };
                final GridLayoutManager gridLayoutManager = (GridLayoutManager) parent.getLayoutManager();
                gridLayoutManager.setSpanSizeLookup(lookup);
            }
    
    
            /*我们为每个不同头部名称的第一个item设置头部高度*/
            int pos = parent.getChildAdapterPosition(itemView); //获取当前itemView的位置
            if (headerPaddingSet.contains(pos)) {
                outRect.top = headerHeight;   //设置itemView PaddingTop的距离
            }
        }
    
    }
       
       
       
       
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59


    总结

    至此我们的功能都已经描述结束,做了这个小功能的确收货不少,比较多的耗时在GridDecoration的设计,因为不清楚能够动态的设置Span,一开始是通过设置itemOffsets的paddingRight去计算的,然后还要计算下一个分组的头部,各种问题,所以以后做功能时候先看看有没有api可以操作的,这样来的更方便和容易。最后附上github望小伙伴们多多点赞哈。有建议和意见还望在评论出提出~~


    https://github.com/qdxxxx/StickyHeaderDecoration

    阅读更多
    
    
    	
    • aguan1992
      aguan1992: 这个有bug哈,我用在fragemnt里面,fragment切换,那个头标题会重复绘制,切换多次产生多个头空位(1年前#1楼)查看回复(1)举报回复
      • qian520ao
        凶残的程序员回复 aguan1992: 我估计是你多次调用recyclerView.addItemDecoration(decoration);了,你看看日志,decoration的方法调用了几次。(1年前)举报回复
    • 上一页
    • 1
    • 下一页

    Android中头部悬浮StickyListHeader的简单使用 - Tangsan6666的博客

    11-20 502

    一、实现效果:头部停留 不断一直保持在顶部

    二、实现步骤:
    1、导入Libary:
    compile
    ‘se.emilsjolander:stickylistheaders:2.7.0’
    2…

    来自: Tangsan6666的博客



    		
    
    
    
    
    		

     sticy_headers_recylerview教程


    来自: 不忘初心



    
    			
    			
    
    
    		
    
    
    
    
    		

    &lt;方式一&gt;添加外部Header实现方式

    &lt;方式二&gt;绘制ItemDecoration实现方式

    二者实现效果一样,下面分析下…

    来自: little762的博客



    		
    
    
    
    
    		
    
    
    
    
    		

    Github管理地址:timehop/st…

    来自: 写点什么捏



    早知道腰椎疼这么简单就能好,还做什么手术啊! 召家 · 燨燚
    		
    
    
    		
    
    
    		

    sticky-headers-recyclerview
    2,compile 'com.timehop.stickyheadersrecyclerview:library:[0.4.3…

    来自: vily_luky的博客



            
    王永迪
    王永迪

    关注 81篇文章

    小_源
    小_源

    关注 118篇文章

    阁楼猫
    阁楼猫

    关注 201篇文章

    换一批

    今天让我 使用
    ItemDecoration 来完成 可推动的悬浮导航栏的效果,最终实现的效果如下图:

    具体实现步骤如下:

    根据我…

    来自: cjm2484836553的博客



    		

     Currently, NOT alr…

    来自: sunbinkang的博客



    		
    
    
    东门股王8年追涨停铁律“1272”曝光,震惊众人! 美易互动 · 燨燚
    		

    吸顶效果在很多APP都可以看见,现在可以借助RecyclerView的ItemDecoration 来简单实现一个吸顶效果直接上效果图:

    首先实现一个Mo…

    来自: qq_34240569的博客



    		
    
    
    
    
    		
    
    
    
    
    		
    
    
    
    
    		
    
    
    东门27岁刘某辞去保安工作,半年存款惊呆众人! 天傲 · 燨燚
    		
    
    
    
    
    		

    以下的几个类 直接拷贝到项目中 备用。

    import java.lang.refl…

    来自: shaoyezhangliwei的专栏



    		
    
    
    
    
    		
    
    
    
    
    		

    该文详细的介绍了RecyclerView.ItemDecoration实现分组粘性头部的功能,让我们自己生产代码,告别代码搬运工的时代.另外文末附有完整Demo的连接.看下效果:



    来自: 那个人_的博客



    股市奇才17年不亏之谜,方法令人意想不到 陕西信息科技 · 燨燚
    		

    http://blog.csdn.net/user11223344abc?viewmode=contents
    出自【蛟-blog】
    0.介绍0.1:先看下效果图吧。
    请…

    来自: 千里逐梦



    		
    
    
    
    
    			
    
    
    
    
    		

        - (void)scrollViewDidScroll:(U…

    来自: x1198928367的博客



    		

    StikkyHeader——为滚动视图添加粘性头部(Stick Header)的Android类库


    来自: qq_27439299的博客



    喝水都会胖竟因为这个,教你一招瘦到90斤! 振辉生物 · 燨燚
    		

    在项目中有时会需要recyclerview滑动式时某个view滑出后会固定在头部显示,比较常用的比如手机联系人界面、地区选择界面等。 StickHeaderRecyclerView就是实现这…

    来自: hx3971的博客



    		
    
    
    
    
    		
    
    
    
    
    		
    
    
    
    
    		

    另外感谢上边连接作者.其实代码没多少…

    来自: yu_bug的博客



    11月起,东门开通初高中学历读本科通道,1.5年课程,毕业即本科 爱华国际 · 顶新
    		

    import android.content.Context;
    import android.graph…

    来自: qq_28898075的博客



    		
    
    
    
    
    		
    
    
    
    
    		

    控制器是UIViewController或者UICollectionViewController都可以使用,已经封装好了,直接可是在工程中使用。
    1…

    来自: zhaocanrong的博客



    		

                大多数项目中都会使用到viewpager + tablayout,还有的项目需求是带有头部而且头部可以伸缩效果,在这里

    一、自定义tablayout选…

    来自: 王永迪的专栏



    27岁东门妹子通过网络平台赚钱,爆赚成网红!!! 元昌盈投资 · 燨燚
    		
    
    
    
    
    		
    
    
    
    
    		

    感谢

    【Android】RecyclerView:打造悬浮效果

    RecyclerView分组悬浮列表

    上图来自于网络,上图的列表中有一个悬浮的粘性头部的效果,现在这种效果的需求比较常见了,像…

    来自: 禽兽先生不禽兽



    		

    就是下面的效果

    QQ浏览器玩了半天,分析出了一点东西。
    之前用ItemDecoration给RecyclerView绘制item过分割线…

    来自: liangfeng093的博客



    		
    
    

    你可能感兴趣的:(【Android,进阶部分】)