美文网首页Android Class各种viewandroid_recyclerview
【Android】RecyclerView:打造悬浮效果

【Android】RecyclerView:打造悬浮效果

作者: 带心情去旅行 | 来源:发表于2017-06-12 19:12 被阅读16911次

悬浮效果

先看个效果

效果
这是一个City列表,每个City都有所属的Province,需要在滑动的时候,将对应的Province悬浮在顶部。悬浮顶部的Province需要根据列表的滑动而适当改变位置,实现“顶上去”的效果。

实现思路:

  • 利用RecyclerView.ItemDecoration绘制Province(就像绘制分割线一样)
  • 同一组的City,只绘制一个Province
  • 计算偏移,将当前Province固定在顶部
  • 根据列表滑动,实现偏移效果

ItemDecoration

既然是利用RecyclerView.ItemDecoration实现的悬浮效果,那么有必要了解下它。
ItemDecoration字面意思:Item的装饰。是的!是装饰!不只是画分割线。
其实ItemDecoration的功能非常强大,而我们平时只是用它来实现分割线的效果(至少我是这样)。因此,可能很多同学认为ItemDecoration就是用来绘制分割线的。其实不然,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);
    }
}

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

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

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

层级关系

绘制分割线

先看看一般的分割线绘制。

  • 定义分割线高度
private int mHeight = 5; //分割线高度
  • 通过getItemOffsets预留空间
@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 (position != 0) {
        //第一个item预留空间
        outRect.top = mHeight;
    }
}
  • 然后在onDraw中绘制
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDraw(c, parent, state);
    final int left = parent.getLeft();
    final int right = parent.getRight();
    final int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View childView = parent.getChildAt(i);
        final int bottom = childView.getTop();
        final int top = bottom - mHeight;
        c.drawRect(left, top, right, bottom, mPaint);
    }
}

获取当前RecyclerViewItem数量,遍历每个Item。在对应的位置绘制一个高度为mHeight的矩形 ,从而实现分割线的效果。

效果
(详情代码见底部链接)

打造悬浮效果

这是一个城市列表,根据省份分组,相同的城市只会显示一个省份。滚动城市列表时,省份会悬浮在顶部。效果如下:


效果1

实现
由于需要悬浮效果,所以需要在onDrawOver中绘制分组。

  • 定义一个interface,根据position通过接口方法getGroupName获取当前省名(由Activity实现)
public interface GroupListener {
    String getGroupName(int position);
}
  • 创建StickyDecoration继承RecyclerView.ItemDecoration
  • 定义isFirstInGroup方法。根据前一个省份,判断当前是否为新的省份
//判断是不是组中的第一个位置
//根据前一个组名,判断当前是否为新的组
private boolean isFirstInGroup(int pos) {
      if (pos == 0) {
        return true;
      } else {
        String prevGroupId = getGroupName(pos - 1);
        String groupId = getGroupName(pos);
        return !TextUtils.equals(prevGroupId, groupId);
      }
}

通过position,对比上一个省份名称,判断当前省是否为第一个

  • 重写getItemOffsets方法,为悬浮栏预留空间
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
      super.getItemOffsets(outRect, view, parent, state);
      int pos = parent.getChildAdapterPosition(view);
      String groupId = getGroupName(pos);
      if (groupId == null) return;
      //只有是同一组的第一个才显示悬浮栏
      if (pos == 0 || isFirstInGroup(pos)) {
        outRect.top = mGroupHeight;
      }
}

只有第一个Item或者新的省份才为悬浮栏预留空间

  • 重写onDrawOver方法(重点)
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDrawOver(c, parent, state);
    final int itemCount = state.getItemCount();
    final int childCount = parent.getChildCount();
    final int left = parent.getLeft() + parent.getPaddingLeft();
    final int right = parent.getRight() - parent.getPaddingRight();
    String preGroupName;      //标记上一个item对应的Group
    String currentGroupName = null;       //当前item对应的Group
    for (int i = 0; i < childCount; i++) {
        View view = parent.getChildAt(i);
        int position = parent.getChildAdapterPosition(view);
        preGroupName = currentGroupName;
        currentGroupName = getGroupName(position);
        if (currentGroupName == null || TextUtils.equals(currentGroupName, preGroupName))
            continue;
        int viewBottom = view.getBottom();
        float top = Math.max(mGroupHeight, view.getTop());//top 决定当前顶部第一个悬浮Group的位置
        if (position + 1 < itemCount) {
            //获取下个GroupName
            String nextGroupName = getGroupName(position + 1);
            //下一组的第一个View接近头部
            if (!currentGroupName.equals(nextGroupName) && viewBottom < top) {
                top = viewBottom;
            }
        }
        //根据top绘制group
        c.drawRect(left, top - mGroupHeight, right, top, mGroutPaint);
        Paint.FontMetrics fm = mTextPaint.getFontMetrics();
        //文字竖直居中显示
        float baseLine = top - (mGroupHeight - (fm.bottom - fm.top)) / 2 - fm.bottom;
        c.drawText(currentGroupName, left + mLeftMargin, baseLine, mTextPaint);
      }
}

通过变量preGroupIdcurrentGroupId来保存当前分组名和上一个分组名。当前Item与上一个Item为同一个分组时,跳过该Item的绘制。
其中代码:

float top = Math.max(mGroupHeight, view.getTop());

根据当前Item的位置确定绘制分组的位置。top将在mGroupHeightview.getTop()中取最大值,也就是说top将不会小于mGroupHeight,这样就能实现吸顶效果。
其中代码:

if (!currentGroupName.equals(nextGroupName) && viewBottom < top) {
            top = viewBottom;
        }

当下个分组的顶部(当前Item的底部viewBottom可近似认为下个Item的顶部)距离RecyclerView顶部小于top时,偏移当前分组位置。实现下一组上滑时候,当前分组上移;上一组下滑的时候,当前分组下移。
最后计算baseLine,并绘制背景和文字。

到目前为止,一个带有悬浮功能的列表就实现了。
(详细代码见底部链接)


--

进阶

当我们利用ItemDecoration实现文字的悬浮的时候,是不是还可以搞点事情~ ~我有个大胆的想法
只有文字的悬浮怎么行!我还希望可以再来个icon?再来几个TextView?来个自定义布局?那就来个自定义布局吧。

实现

实现的原理跟上面一样,由于需要自定义布局,所以需要在接口中添加一个获取View的方法。

  • 定义PowerGroupListener
public interface PowerGroupListener {
    String getGroupName(int position);
    View getGroupView(int position);
}

相比之前,多了个getGroupView方法,用来获取View

  • onDrawOver中绘制
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
    ...
    for (int i = 0; i < childCount; i++) {
        ...
        //根据position获取View
        View groupView = mGroupListener.getGroupView(position);
        if (groupView == null) return;
        ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, mGroupHeight);
groupView.setLayoutParams(layoutParams);
        groupView.setDrawingCacheEnabled(true);
        groupView.measure(
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
        //指定高度、宽度的groupView
        groupView.layout(0, 0, right, mGroupHeight);
        groupView.buildDrawingCache();
        Bitmap bitmap = groupView.getDrawingCache();
        c.drawBitmap(bitmap, left, top - mGroupHeight, null);
    }
}

在原来的基础上做了点修改,通过接口的getGroupView方法获取需要绘制的分组View,将得到的View绘制到指定位置。

效果:


效果2

(详细代码见底部链接)

源码

已封装成库,欢迎来提Issues

repositories {
    jcenter()// If not already there
}
dependencies {
    compile 'com.gavin.com.library:stickyDecoration:x.x.x'
}

链接

支持

  • LinearLayoutManager
  • GridLayoutManager
  • 点击事件
  • 分割线

详细用法级源码请看Github

参考

Android-使用RecyclerView的ItemDecoration 实现炫酷的 吸顶效果
RecycleView的学习(一篇不错的博客,没有投稿,所以看得人不多)
Android RecyclerView 使用完全解析 体验艺术般的控件

相关文章

网友评论

  • 时光不止我爱她:我这遇到了个小问题,悬浮view点击事件会穿透到下层item的点击事件,导致悬浮点击事件无效,看了你github那两种似乎也不能解决
    带心情去旅行:@唔开心 应该是不可以的,因为悬浮的内容是我绘制上去的,并没有处理事件分发。我里面有个例子,就是用来处理这种点击事件穿透的
    唔开心:我看了下,不是穿透,是悬浮的view没写点击事件,所有传给下层的item了。悬浮的加上点击事件应该会消耗点击事件,就不会下传了
  • 阿犇专用:您好,在使用该库时,发现会出现屏幕抖动的情况,请问是因为ItemDecoration中取GroupId的操作影响的呢?
    带心情去旅行:@阿犇专用 你的头部是否会比较复杂?
    阿犇专用:滑动时候抖动,在开发者模式看了一下GPU绘制条形图,发现页面在绘制的时候耗时较长,调试过程中,发现在获取GroupId等操作时导致的该问题,只要draw和drawover绘制较为复杂,就会出现此问题,itemDecoration的绘制貌似不会跟着recyclerview进行recycle操作,从而导致绘制事件过长,尝试了itemDecration的简单使用,也发现绘制的事件较长,但不致于出现屏幕抖动,有没有方案能够优化,目前在小米手机没此问题,华为,三星,锤子等均复现该问题
    带心情去旅行:请问是滑动的时候抖动呢,还是刷新或者加载更多的时候抖动?
  • db828a93df61:您好,请问一下,GridLayoutManager悬浮item的上面一个item会居中显示,请问怎么让它左侧对齐?就像您GitHub里的连云港、舟山这种如何设置左对齐?
    带心情去旅行:可以在Adapter中得到屏幕width,然后根据你的页面去设置item的宽度
    db828a93df61:@AndroidByXy 不过不设置match的话,就都靠左显示了。。不太好弄啊…
    db828a93df61:呃 ,好吧,我把item的宽度改成wrap_content就行了,不好意思问了个蠢问题 😂
  • Zcyang:dalao问一下,我每次刷新列表顶部都会出现一个和悬浮item一样大小的空白,每次都会多一个同样大小的item,请问怎么解决
    Android攻城狮小明:你的这个问题,解决了没啊!我也遇到了,同样的问题!
    带心情去旅行:@Zcyang 你应该是多次添加Decoration了
  • 134a90e9e0c1:感谢作者提供的想法,不过个人浅见 觉得for循环效率低了点,我的做法是只在第一个itemView上绘制一个悬浮效果,其他的则用多布局代替,是不是节省了运算效率也不好说,感觉是节省了一些吧。
    带心情去旅行:@阮易初 这样对RecyclerView的入侵会不会太大了~~
    134a90e9e0c1:@带心情去旅行 啊,其实就是根据type创建不同布局,然后复写SpanSizeLookUp的getSpanSize方法让标签布局占一行,不过我写出来发现有点bug 还在修😂
    带心情去旅行:@阮易初 这是个好想法,不知道你说的“多布局代替”指的是?
  • 216935a3ee38:你好 自定义的时候 现在有一个复用的问题就是 每当我滑倒最后一条需要加载更多的时候 就会有一块空白的区域 我觉得应该是悬浮栏复用的问题 不知道有没有遇到过呢?
    带心情去旅行:@216935a3ee38 方便的话,去github提个issue吧。把图传上来,我看下。
    216935a3ee38:@带心情去旅行 1.3.1的
    带心情去旅行:@216935a3ee38 你现在用是什么版本,我试了下,没有发现你说的那个问题。
  • Mr禽兽:你好,请问适用于 GridLayoutManager 的 RecyclerView 吗,我貌似出了点问题,一列三个的话,后面两个 item 的布局出现了问题,而且向上滑动时,悬浮列表是被遮盖,而不是被顶上去
    带心情去旅行:@Mr禽兽 因为本来就没考虑GridLayoutManager,所以出问题也是意料之中:joy: :joy: :joy:
    Mr禽兽:@带心情去旅行 谢谢了,不知道你有没有理解我刚才说的情况,如果需要的话我可以截图给你看,我 QQ 1075134332,感谢大神,我也会研究下你的源码
    带心情去旅行:@Mr禽兽 目前是没考虑GridLayoutManager的,我周末看看能不能把GridLayoutManager加进去
  • Dragon_Boat:lz你好,最近看到你的文章,实践时候发现,自定义view在view.draw(canvas)时,传入的布局中的一些排版,如居中对齐之类的会失效,不知道有没有这个情况?怎么解决
    Dragon_Boat:@带心情去旅行 我把实例变量viewholder改为了map<pos,viewholder>,createViewHolder的时候就进行数据绑定,之后在measure和layout,目前已解决此问题
    Dragon_Boat:@带心情去旅行 这个问题可能找到原因了:在creatViewHolder方法里对view进行了measure和layout,之后draw的时候,它只会按照最初的尺寸去绘制,不会动态调整,这导致显示出的结果,只会和布局文件中预览的一致。简单的验证是,如果布局文件中textView不设置默认值,则永远无法显示文本,即使赋值,文本也无法超过最初的长度限制。 暂时没找到解决方法
    带心情去旅行:@Dragon_Boat 对齐什么的暂时没发现什么问题。不过自定义的那个layout的宽度是没法跟布局里面那样全屏,所以我只好采用居左居右的方式来处理。
  • afa_9418:老铁,如果我想点击省,收缩省下的市,你有思路没有。我的思路是点击省,动态的移除省下的数据,然后局部刷新。可这样头也没了,回不去了。
    afa_9418:@带心情去旅行 根据某个条件动态去改变Item LayoutParams的高度,会有空白出现。控件不流程。
    带心情去旅行:换个思路试试。不要移除,而是把高度变为0?
  • 渔船Mr_Liu:if (position + 1 < itemCount) {
    //获取下个GroupName
    String nextGroupName = getGroupName(position + 1);
    //下一组的第一个View接近头部
    if (!currentGroupName.equals(nextGroupName) && viewBottom < top) {
    top = viewBottom;
    }
    }
    这段代码在mGroupHeight > 每个item的height的时候会有问题,这时group栏可能遮挡住多个item,那么当下一个group栏在屏幕顶挨着上一个group栏时,上一个group栏要等到这一组最后一个的top出屏幕范围了才会向上动。可以在groupListener添加一个查找某组最后一个item位置的方法,判断下一个时应该判断这一组的最后一个item的bottom(+分割线高度,如果有分割线,并且这个item目前在屏幕中)<mGroupHeight。虽然感觉mGroupHeight一般小于等于item的height,但这样改了可以更通用
    带心情去旅行:@丨浪漫情怀 是的,感谢你的建议,我找个时间完善下
  • 此刻灬暗殇:github的demo我怎么运行不起来啊
  • 唐三藏丶:楼主,我在github提了bug有空看一下哦~
    唐三藏丶:@带心情去旅行 好的,谢谢~
    带心情去旅行:@段_4fd7 我到了,我找个时间修复下。最近有点事
  • KunMinX:我以为是什么呢,这不叫悬浮,叫黏性头部 :joy:
  • 付凯强:没有人下载github然后报错吗?
    ccd0ac6e3bf8:将楼主的"appcompat-v7" : "com.android.support:appcompat-v7:22.2.1"提升至v7:24.2.1,就可以完美运行了。
    ccd0ac6e3bf8:我也报错,是关于v4冲突的,我找了好久都没有找到重复v4jar
    被时光移动的城:完美运行,没错误
  • 0ddc1f7125e0:楼主,能不能同时添加普通分割线,感觉有点麻烦
    付凯强:你们没有人下载gtihub上的demo,运行报错吗?
    带心情去旅行:添加分割线的话,需要修改里面的一些判断,然后添加分割线,这个我以后继续完善。你暂时可以先把分割线放在Item里面~~
  • kinkyboots:感谢分享
  • 曹_YouXing:楼主,这个能不能做到点击头部,然后让该组的内容收起或者是展开,因为我现在有这个需求,如果你有思路的话麻烦告诉我一下,谢谢谢谢
    0578a6742bfc:如果你要做那种效果就没有必要用这个啦 安卓原生有那种ListView的 item折叠的那种
    0ddc1f7125e0:首先要获取到点击事件,然后就简单了
  • shunxir:实现顶操作的原理是什么
  • 一个冬季:我服,可以,学习了
  • InnerNight:当时做这个吸顶,是用了顶部一个view配合recyclerView的滑动来做。题主用RecyclerView的ItemDecoration来做,就有点高级了。
  • 大脑好饿:`top将在mGroupHeight和view.getTop()中取最小值`
    ```
    float top = Math.max(mGroupHeight, view.getTop());
    ```
    是取最大值吧
    带心情去旅行:确实。已更正,谢谢
  • 5aa43c7d9fd5:以star
  • 蜀汉玫瑰:讲的挺清楚的
  • x耶律:以前只用过一个一样功能的listview的开源项目-StickyListview。现在你把recyclerView的写出来了,不错,赞一个!:+1:
  • _SOLID:有个缺点就是不能做点击事件
    带心情去旅行:@_SOLID 是的,没考虑到
    _SOLID:@带心情去旅行 直接在创建那里添加时不行的,毕竟你绘制的只是View的bitmap缓存,并不是这个View
    带心情去旅行:@_SOLID 你用自定义布局的时候是可以添加点击事件的
  • 正规程序员:请问动图是怎么上传的,怎么录制的?
    正规程序员:@带心情去旅行 nice
    带心情去旅行:用的是licecap
    带心情去旅行:@King工作室 录制的gif

本文标题:【Android】RecyclerView:打造悬浮效果

本文链接:https://www.haomeiwen.com/subject/hguwfxtx.html