实现这样的效果只需要一个装饰器就可以了。如果对ItemDecoration不太了解。那就请看这篇教程,这位大神写的十分详细。
《RecyclerView 之 ItemDecoration 讲解及高级特性实践》开门就到
这个效果只需要重写onDrawOver()
方法就可以了。此处还是要啰嗦一嘴,onDraw()
和onDrawOver()
这两个方法。
onDraw()
绘制的内容将会显示在条目的下层。
onDrawOver()
绘制的内容将会显示在条目的上层。
item的绘制就在它们中间,就像夹心饼干一样。只不过这是有正反面的,最上层是onDrawOver()
,其次是item,最底层是onDraw()
。
接下来创建SuspensionDecoration
类。准备重写onDrawOver()
方法:
import android.graphics.*
import androidx.recyclerview.widget.RecyclerView
/**
* 带有悬浮效果的装饰器
*/
class SuspensionDecoration : RecyclerView.ItemDecoration() {
/**
* 绘制悬浮层(绘制的图像将会呈现在条目的上层)
*/
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
}
}
稍微改造下SuspensionDecoration
这个类。改造后发现多传了两个参数,这两个参数一个是用来查找分组条目的标题,另一个是用来控制悬浮头绘制的高度。这个高度就是是分组条目的高度。
import android.graphics.*
import androidx.annotation.IdRes
import androidx.recyclerview.widget.RecyclerView
/**
* 带有悬浮效果的装饰器
*/
class SuspensionDecoration(@IdRes private val titleResId: Int, private val headHeight: Int) :
RecyclerView.ItemDecoration() {
//添加默认悬浮物
private val defaultKey = "default"
//保存当前绘制的头部
private var nowHeaderKey: String = defaultKey
//保存当前展示的列表分组条目快照
private val headDic = mutableMapOf<String, Bitmap>().apply {
put(nowHeaderKey, Bitmap.createBitmap(headHeight, headHeight, Bitmap.Config.RGB_565))
}
//保存标题
private val headLabel = mutableListOf<String>().apply { add(defaultKey) }
/**
* 绘制悬浮层(绘制的图像将会呈现在每个条目的上层)
*/
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
//通过改变偏移量到达悬浮头部的动态效果
val offset = 0
//绘制悬浮头
val stickyRect = Rect(0, offset, parent.right, headHeight)//绘制图片显示的区域
val drawRect = Rect(0, 0, parent.right, headHeight - offset)//当前悬浮头可绘制区域
c.drawBitmap(headDic[nowHeaderKey]!!, stickyRect, drawRect, null)
}
}
listView.addItemDecoration(
SuspensionDecoration(
R.id.headTitle,
SizeUtils.dp2px(30f)
)
)
运行效果如下,图中黑色方框是默认的悬浮层。此处为了对比就先保留这个的悬浮物。
悬浮物有了,那么如何让悬浮物展示分组条目的内容呢?此处的实现思路是,当列表向上滑动的时候先找出第一个分组条目,然后将它的截图保存到一个map中。随后在悬浮层中绘制这个分组条目的截图,而且还有一个问题,那就是第一个条目不一定是分组条目。这样就需要做一点小处理,当列表首位条目不是分组条目的时候需要加载一张默认图,也就是上面的那个小黑方块。
import android.graphics.*
import android.view.View
import android.widget.TextView
import androidx.annotation.IdRes
import androidx.recyclerview.widget.RecyclerView
/**
* 带有悬浮效果的装饰器
*/
class SuspensionDecoration(@IdRes private val titleResId: Int, private val headHeight: Int) :
RecyclerView.ItemDecoration() {
//添加默认悬浮物
private val defaultKey = "default"
//保存当前绘制的头部
private var nowHeaderKey: String = defaultKey
//保存当前展示的列表分组条目快照
private val headDic = mutableMapOf<String, Bitmap>().apply {
put(nowHeaderKey, Bitmap.createBitmap(headHeight, headHeight, Bitmap.Config.RGB_565))
}
//保存标题
private val headLabel = mutableListOf<String>().apply { add(defaultKey) }
/**
* 绘制悬浮层(绘制的图像将会呈现在每个条目的上层)
*/
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
//列表不一定有头部 所以循坏遍历查询第一个头部
var headerItemView: View? = null
for (position in 0 until parent.childCount) {
if (null != parent.getChildAt(position).findHeaderView()) {
break
}
}
if (null == headerItemView) {
headerItemView = parent.getChildAt(0)
}
//处理没有子控件的情况
if (null == headerItemView) {
return
}
//寻找标题头
val headerView = headerItemView.findHeaderView()
//头部绘制偏移量
if (headerView != null) {
//保存截图
nowHeaderKey = headerView.text.toString()
if (!headDic.containsKey(nowHeaderKey)) {
headLabel.add(nowHeaderKey)//保存标题
headDic[nowHeaderKey] = getViewBitmap(headerItemView)
}
}
if (headDic.isEmpty()) {
return
}
//通过改变偏移量到达悬浮头部的动态效果
val offset = 0
//绘制悬浮头
val stickyRect = Rect(0, offset, parent.right, headHeight)//绘制图片显示的区域
val drawRect = Rect(0, 0, parent.right, headHeight - offset)//当前悬浮头可绘制区域
c.drawBitmap(headDic[nowHeaderKey]!!, stickyRect, drawRect, null)
}
/**
* 快照
*/
private fun getViewBitmap(view: View): Bitmap {
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.RGB_565)
val canvas = Canvas(bitmap)
view.draw(canvas)
return bitmap
}
/**
* 获取头部
*/
private fun View.findHeaderView() = findViewById<TextView>(titleResId)
}
改动的稍微有点多,此处代码就都贴过来了。随后来看下效果:
接下来去掉群组条目。再来看下效果:
在上一步中,悬浮头的内容已经绘制了出来,接下来通过处理列表的向上滑动来达到,下一个分组条目向上推上一个分组条目的效果。这里的实现思路是在列表向上滑动的时候寻找紧邻的下一个分组条目,通过这个分组条目控件距离顶部的距离,计算出悬浮物绘制的高度。从而达到从下往上推的效果。
observeNextHeader()
方法的具体实现:
/**
* 观察下一个标题头
*/
private fun observeNextHeader(parent: RecyclerView): Int {
var nextHeaderView: View? = null
for (index in (1 until parent.childCount)) {
val childView = parent.getChildAt(index)
if (null != childView.findHeaderView()) {
nextHeaderView = childView
break
}
}
if (null == nextHeaderView) {
return 0
}
//距离顶部高度
val top = nextHeaderView.top
//计算偏移量
if (top in 0 until headHeight) {
return headHeight - top
}
return 0
}
效果如下:
向上推是推上去了,但是在列表向下滑动的时候还需要将上一分组条目拉下来。实现思路就是获取屏幕中可显示的第一个分组条目,判断它是否有上一个分组条目,如果有就绘制出上一个分组条目的图片。
observeLastHeader()
方法的具体实现:
/**
* 观察上一个标题头
*/
private fun observeLastHeader(parent: RecyclerView) {
//找出当前显示的标题(只找第一个)
for (index in (0 until parent.childCount)) {
val childView = parent.getChildAt(index)
val headText = childView.findHeaderView()
if (null != headText) {
val position = headLabel.indexOf(headText.text)//获取标题的索引
val offset = childView.top//获取偏移量
if (offset > 0 && position > 0) {
nowHeaderKey = headLabel[position - 1]
}
return
}
}
}
效果如下:
效果还行就是这个黑色的方框看着难受,此处小改一下。
因为RGB_565不带透明度,所以此处改为ARGB_8888。这样创建的Bitmap默认就是透明的。
put(nowHeaderKey, Bitmap.createBitmap(headHeight, headHeight, Bitmap.Config.ARGB_8888))
虽然功能实现了,但是还有个问题。这个问题会在与字母导航条联动的时候遇到。