Android 实现分组标题吸顶效果,支持上下左右padding
Posted 安卓开发-顺
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 实现分组标题吸顶效果,支持上下左右padding相关的知识,希望对你有一定的参考价值。
先上gif效果图:
技术方案:RecycleView + ItemDecoration
具体实现:
第一步:先实现相关业务代码,让数据加载出来
Activity:
/**
* 实现吸顶效果 演示
*/
class RecyclerViewActivity : Activity()
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recyclerview)
val data: MutableList<DataBean> = getData()
val adapter = RVAdapter(data)
act_recyclerview_rv.adapter = adapter
act_recyclerview_rv.layoutManager = LinearLayoutManager(this)
val divider = DividerItemDecoration(this,DividerItemDecoration.VERTICAL)
divider.setDrawable(getDrawable(R.drawable.shape_divider)!!)
// act_recyclerview_rv.addItemDecoration(divider)
//自定义itemDecoration 实现吸顶效果
act_recyclerview_rv.addItemDecoration(MyItemDecoration())
private fun getData(): MutableList<DataBean>
val data: MutableList<DataBean> = mutableListOf()
for (i in 0..2)
for (j in 0..9)
if (i == 0)
data.add(DataBean("曹操$i$j","曹操分组"))
else if (i == 1)
data.add(DataBean("刘备$i$j","刘备分组"))
else if (i == 2)
data.add(DataBean("孙权$i$j","孙权分组"))
return data
R.layout.activity_recyclerview
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:layout_margin="10dp"
android:padding="20dp"
android:id="@+id/act_recyclerview_rv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
RVAdapter
class RVAdapter : RecyclerView.Adapter<RVViewHolder>
var data: MutableList<DataBean> = mutableListOf()
constructor(data: MutableList<DataBean>)
this.data = data
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RVViewHolder
val view =
LayoutInflater.from(parent.context).inflate(R.layout.item_act_rec_rv, parent, false)
view.setOnClickListener
ToastUtil.showShortToast("you click $view.findViewById<TextView>(R.id.act_rec_rv_item_tv).text")
return RVViewHolder(view)
override fun onBindViewHolder(holder: RVViewHolder, position: Int)
holder.name!!.text = data[position].name
override fun getItemCount(): Int
return data.size
/**
* 判断此位置是否是每一组的第一个view
*/
fun isFirstGroupView(childLayoutPos: Int): Boolean
if (childLayoutPos == 0)
return true
if (data[childLayoutPos].groupName != data[childLayoutPos - 1].groupName)
return true
return false
fun getGroupName(childLayoutPosition: Int): String
return data[childLayoutPosition].groupName
RVViewHolder
class RVViewHolder : RecyclerView.ViewHolder
var name: TextView? = null
constructor(view: View) : super(view)
name = view.findViewById(R.id.act_rec_rv_item_tv)
DataBean
data class DataBean(
var name: String,
var groupName: String
)
ZSConstants
object ZSConstants
val TITLE_TEXT_SIZE: Int = 18
val DIVIDER_HEIGHT: Int = 10
//此变量和布局文件中设置的高度保持一致
val ITEM_HEIGHT: Int = 60
val GROUP_HEIGHT: Int = 40
val GROUP_NAME_MARGIN: Int = 10
第二步:利用自定义ItemDecoration来实现吸顶效果,并处理RecycleView的各种padding
相关说明都写在了注释里面,代码如下:
/**
* 自定义分割线实现分类标题自动吸顶效果
* 如果 需求是分组标题支持点击的话 当前是不满足的,就得切换实现思路了,思路如下:
* (1)group标题直接使用item实现并且实现点击事件,这种情况在getItemOffsets里面就没有必要在预留那么大的空间了,因为不需要onDraw来绘制分组信息了
* (2)吸顶时还是要通过onDrawOver来绘制悬浮到顶部,此时的点击事件比较麻烦,需要通过RecycleView的onTouch事件来根据点击位置来处理
* 点击时的顶部的这个区域就是当前的吸顶布局了,然后做处理就可以了,(要记录下现在哪个分组在顶部)
*/
class MyItemDecoration : RecyclerView.ItemDecoration
private val headPaint = Paint()
private val headPaint2 = Paint()
private val textPaint = Paint()
private val groupHeight: Float = DensityUtil.dp2px(ZSConstants.GROUP_HEIGHT).toFloat()
private val dividerHeight: Float = DensityUtil.dp2px(ZSConstants.DIVIDER_HEIGHT).toFloat()
private val groupNameMargin: Float = DensityUtil.dp2px(ZSConstants.GROUP_NAME_MARGIN).toFloat()
constructor()
headPaint.color = Color.parseColor("#ff0000")
headPaint.style = Paint.Style.FILL
headPaint2.color = Color.parseColor("#00ff00")
headPaint2.style = Paint.Style.FILL
textPaint.color = Color.BLACK
textPaint.isDither = true
textPaint.isAntiAlias = true
textPaint.textSize = DensityUtil.dp2px(ZSConstants.TITLE_TEXT_SIZE).toFloat()
/**
* 此方法绘制的内容在RecyclerView item下面,因此可能会被item挡住
*/
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State)
super.onDraw(c, parent, state)
//在预留出的空间中 绘制分组标题
val adapter = parent.adapter
val left: Float = parent.paddingLeft.toFloat()
val right: Float = parent.width.toFloat() - parent.paddingRight
if (adapter is RVAdapter)
//获取可见view的个数
val childCount = parent.childCount
//循环遍历去绘制
for (i in 0 until childCount)
c.save()
//得到屏幕上显示的view
val view = parent.getChildAt(i)
//得到该view在整个列表布局中的位置
val childLayoutPosition = parent.getChildLayoutPosition(view)
//判断该位置是否是每组view的第一个
val isFirstGroupView = adapter.isFirstGroupView(childLayoutPosition)
if (isFirstGroupView &&
//头部屏蔽没有必要的绘制
view.top - groupHeight - parent.paddingTop >= 0 &&
//底部屏蔽没有必要的绘制
view.top <= parent.measuredHeight - parent.paddingBottom + groupHeight
)
// 最底部的分割线需要c.clip一下
if (view.top.toFloat() > parent.measuredHeight - parent.paddingBottom)
val rect = Rect(
left.toInt(),
view.top - groupHeight.toInt(),
right.toInt(),
parent.measuredHeight - parent.paddingBottom
)
c.clipRect(rect)
//绘制分组矩形背景
c.drawRect(
left,
view.top - groupHeight,
right,
view.top.toFloat(),
headPaint
)
//绘制标题文本
val text: String = adapter.getGroupName(childLayoutPosition)
c.drawText(
text,
left + groupNameMargin,
view.top - groupHeight / 2 + abs(textPaint.fontMetrics.ascent) / 2 - textPaint.fontMetrics.descent / 2,
textPaint
)
else if (
//头部屏蔽没有必要的绘制
view.top - groupHeight - parent.paddingTop >= 0 &&
//底部屏蔽没有必要的绘制
view.top <= parent.measuredHeight - parent.paddingBottom + dividerHeight
)
//绘制分割线
if (i == childCount - 1)
log("parent height - parent.paddingBottom = $parent.measuredHeight - parent.paddingBottom view.top=$view.top")
//最底部的分割线需要c.clip一下
if (view.top.toFloat() > parent.measuredHeight - parent.paddingBottom)
val rect = Rect(
left.toInt(),
view.top - dividerHeight.toInt(),
right.toInt(),
parent.measuredHeight - parent.paddingBottom
)
c.clipRect(rect)
c.drawRect(
left,
view.top.toFloat() - dividerHeight.toInt(),
right,
view.top.toFloat(),
headPaint
)
c.restore()
/**
* 此方法绘制的内容在RecyclerView item上面,因此会在最外层显示,可以挡住item
*/
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State)
super.onDrawOver(c, parent, state)
val adapter = parent.adapter
val left: Float = parent.paddingLeft.toFloat()
val top: Float = parent.paddingTop.toFloat()
val right: Float = parent.width.toFloat() - parent.paddingRight
if (adapter is RVAdapter)
//拿到第一个可见的view
val firstVisiblePos =
(parent.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
val viewHolder = parent.findViewHolderForLayoutPosition(firstVisiblePos)
val itemView = viewHolder!!.itemView
//日志打印
// val textView = itemView.findViewById<TextView>(R.id.act_rec_rv_item_tv)
// log("$textView.text firstVisiblePos = $firstVisiblePos")
//判断当前位置的下一个是否是分组的第一个view
//为甚是下一个,因为当前的那个被onDrawOver位置的常驻标题挡住了
//所以如果下一个是分组第一个的话,刚好开始执行推动的效果
val isFirstGroupView = adapter.isFirstGroupView(firstVisiblePos + 1)
if (isFirstGroupView)
//慢慢往上推动
// log("$itemView.top itemView.bottom = $itemView.bottom")
// log("top-$top itemView.top=$itemView.top itemView.bottom = $itemView.bottom")
val bottom = min(groupHeight, itemView.bottom.toFloat() - top) + top
c.drawRect(left, top, right, bottom, headPaint2)
val y =
bottom - groupHeight / 2 + abs(textPaint.fontMetrics.ascent) / 2 - textPaint.fontMetrics.descent / 2
val rect = Rect(0, top.toInt(), right.toInt(), bottom.toInt())
c.clipRect(rect)
val text: String = adapter.getGroupName(firstVisiblePos)
c.drawText(
text,
left + groupNameMargin,
y,
textPaint
)
else
//标题常驻在顶部
c.drawRect(left, top, right, top + groupHeight, headPaint2)
val y =
top + groupHeight - groupHeight / 2 + abs(textPaint.fontMetrics.ascent) / 2 - textPaint.fontMetrics.descent / 2
val text: String = adapter.getGroupName(firstVisiblePos)
c.drawText(
text,
left + groupNameMargin,
y,
textPaint
)
/**
* 通过此方法来设置item的预留区间,进而给ItemDecoration留出位置
* 只绘制可见部分,滚动到屏幕内的则进行绘制
*/
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
)
//拿到对应的adapter
val adapter = parent.adapter
if (adapter is RVAdapter)
//拿到当前view所在的位置
val childLayoutPos = parent.getChildLayoutPosition(view)
//判断此view是否是每一组的第一个view
if (adapter.isFirstGroupView(childLayoutPos))
outRect.set(0, groupHeight.toInt(), 0, 0)
else
outRect.set(0, dividerHeight.toInt(), 0, 0)
//日志打印
val textView = view.findViewById<TextView>(R.id.act_rec_rv_item_tv)
// log("$textView.text childLayoutPos = $childLayoutPos")
注意:涉及到具体的尺寸计算,特别是bottom、top之类的要十分细心小心,可以自己画画图来理解,也可以把工程跑起来,根据效果一点一点去理解。
难点就在于两个标题靠在一起时上面的标题慢慢被顶上去,这里的实现思路是在onDrawOver方法里面不断绘制上面的标题空间,让bottom不断减小(减小就是往上走),标题文字的绘制也要跟着往上走,然后还要通过canvas的clipRect方法去裁剪绘制区域,要不然会绘制到RecycleView paddingTop区域。
以上是关于Android 实现分组标题吸顶效果,支持上下左右padding的主要内容,如果未能解决你的问题,请参考以下文章
Android 实现分组标题吸顶效果,支持上下左右padding
Android 自定义ItemDecoration-实现分组吸顶效果
RecyclerView 悬浮吸顶效果实现,支持数据绑定及Touch事件
RecyclerView 悬浮吸顶效果实现,支持数据绑定及Touch事件