【Android】RecyclerView:打造悬浮效果

悬浮效果

先看个效果

效果

这是一个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 使用完全解析 体验艺术般的控件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容