悬浮效果
先看个效果
这是一个
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
绘制的内容、Item
、onDrawOver
绘制的内容,各层级关系如下:
绘制分割线
先看看一般的分割线绘制。
- 定义分割线高度
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);
}
}
获取当前RecyclerView
的Item
数量,遍历每个Item
。在对应的位置绘制一个高度为mHeight
的矩形 ,从而实现分割线的效果。
(详情代码见底部链接)
打造悬浮效果
这是一个城市列表,根据省份分组,相同的城市只会显示一个省份。滚动城市列表时,省份会悬浮在顶部。效果如下:
实现
由于需要悬浮效果,所以需要在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);
}
}
通过变量preGroupId
和currentGroupId
来保存当前分组名和上一个分组名。当前Item
与上一个Item
为同一个分组时,跳过该Item的绘制。
其中代码:
float top = Math.max(mGroupHeight, view.getTop());
根据当前Item
的位置确定绘制分组的位置。top
将在mGroupHeight
和view.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绘制到指定位置。
效果:
(详细代码见底部链接)
源码
已封装成库,欢迎来提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 使用完全解析 体验艺术般的控件