1. 想要的效果
左右两列和上下两行ItemView贴边,与RecyclerView边界之间没有间隙,如图所示
2. ItemView平分实现
在设置ItemView之间的间隙之前,我们先想想如何使得ItemView平分行
我们的ItemView的布局如下
当最外层布局为固定宽度或者为wrap_content
时,Recyclerview的效果如下:
wrap_content:
100dp:
当itemView
的最外层宽度为match_parent
时,效果如下:
所以我们想要使得ItemView
平分Recyclerview
,就要求ItemView
的宽度为match_parent
。
因此当我们在实际使用的时候,可以在真正的布局(紫色)部分外嵌套一层宽度为match_parent
的布局,紫色布局居中。
3. ItemDecoration自定义思路
想要实现ItemView
间的间隙效果,第一个想到的就是使用ItemDecoration
的getItemOffsets()
方法。想起原先写过的文章,为Recyclerview
的列表添加间隙就是使用ItemDecoration
。
简单介绍下,蓝色部分就是我们的ItemView
布局,想要和其他ItemView布局间形成间隙就是通过OutRect
来设置的。它是在当前ItemView
的四周扩展一部分作为间隙。
以三列为例,想要实现以下效果,有两种方案
这里我们采用方案二,方案一我在实际使用过程中遇到了一些问题,后面我们再讲讲.
3.1 创建类继承自RecyclerView.ItemDecoration()
class GridItemDecoration(leftAndRightSpace: Int = 0, topAndBottomSpace: Int = 0) :
RecyclerView.ItemDecoration() {
//总共的列数
var mSpanCount: Int = 0
//左右间隔
private val leftAndRightSpace = leftAndRightSpace
//上下间隔
private val topAndBottomSpace = topAndBottomSpace
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
}
}
这里我们创建的类GridItemDecoration
继承自RecyclerView.ItemDecoration()
leftAndRightSpace
和topAndBottomSpace
分别为ItemView
之间的左右间隔和上下间隔
3.2 重写getItemOffsets()方法
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
//总共的列数
mSpanCount = (parent.layoutManager as GridLayoutManager).spanCount
//当前View所属位置(从0计数),注意这里使用getChildAdapterPosition()而不是getChildLayoutPosition(),后面注意事项中单独说明
val itemPosition = parent.getChildAdapterPosition(view)
//当前所处行,从0计数
val currentRow = itemPosition / mSpanCount
//当前所处列,从0计数
val currentColumn = itemPosition % mSpanCount
//先默认所有的itemview的left和bottom为0
//set(left,top,right,bottom)
outRect.set(0, topAndBottomSpace, leftAndRightSpace, 0)
//当前行处于第0行,top偏移均为0
if (currentRow == 0) {
outRect.top = 0
}
//当前列处于最后一列,right偏移均为0
if (currentColumn == mSpanCount-1) {
outRect.right = 0
}
}
以上就完成了我们的ItemDecoration
4. 使用(包括移除操作)
4.1 创建适配器
这里就简单创建个适配器,如下所示:
class MyAdapter(private val data:MutableList<String>):RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
inner class MyViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
val tvName:TextView = itemView.findViewById(R.id.tvName)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view =LayoutInflater.from(parent.context).inflate(R.layout.item,parent,false)
return MyViewHolder(view)
}
override fun getItemCount(): Int {
return data.size
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.tvName.text = data[position]
}
}
4.2 使用GridItemDeocation
class MainActivity : AppCompatActivity() {
private val dataList = mutableListOf<String>()
private val mAdapter: MyAdapter by lazy {
MyAdapter(dataList) }
override fun onCreate(savedInstanceState: Bundle?) {
//填充数据
for (i in 0..20) {
dataList.add("Item$i")
}
//设置适配器
val layoutManager = GridLayoutManager(this, 3)
mRecyclerview.layoutManager = layoutManager
mRecyclerview.adapter = mAdapter
//使用自定义的ItemDeciration
mRecyclerview.addItemDecoration(GridItemDecoration(20, 10))
}
}
4.3 移除一项
这里我们默认测试一直删除下标position为4的那一项
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
btnRemove.setOnClickListener {
position = 4
if (position <= dataList.size - 1) {
mAdapter.notifyItemRemoved(position)
dataList.removeAt(position)
mAdapter.notifyItemRangeChanged(position, dataList.size - position)
}
}
}
}
注意,这里在调用notifyItemRemoved()
后,还需要将数据项从list
集合中去除,因为它并不会删除adapter
数据集中真实的元素。
可以看到后面我们在移除后,一直是item14,数据项个数没有变化
最后一定要调用notifyItemRangeChanged()
方法,因为notifyItemRemoved()
只是移除了视图,并没有进行重新绑定,因此position
还是之前的position
,那样我们的GridItemDecoration
内对位置的判断就会出现问题导致间隙错乱,所以需要对删除掉的位置及其后剩余项的所有itemview
进行更新。
看如下所示,如果没有更新后面的部分,在移除后,后面ItemView
之间的间隙就出现了问题。
5. 另外一种思路遇到的问题,待解决
其实一开始我使用的是思路一,每个ItemView
都是四个方向有间隔,然后边界的再单独处理
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
//总共的列数
mSpanCount = (parent.layoutManager as GridLayoutManager).spanCount
//当前View所属位置(从0计数)
val itemPosition = parent.getChildAdapterPosition(view)
//当前所处行,从0行计数
val currentRow = itemPosition / mSpanCount
//当前所处列,从0计数
val currentColumn = itemPosition % mSpanCount
//思路一
outRect.set(
leftAndRightSpace / 2,
topAndBottomSpace / 2,
leftAndRightSpace / 2,
topAndBottomSpace / 2
)
//总共的个数
val totalCount = (parent.layoutManager as GridLayoutManager).itemCount
//第一行
if (currentRow == 0) {
outRect.top = 0
}
//如果总共的个数<=当前view所在行的最大个数,则代表它在最后一行
if (totalCount <= (currentRow + 1) * mSpanCount) {
outRect.bottom = 0
}
//第0列
if (currentColumn == 0) {
outRect.left = 0
}
//最后一列
if (currentColumn == mSpanCount - 1) {
outRect.right = 0
}
}
但是在实际使用的时候发现问题,在移除到如图所示时,可以看到最后那两个ItemView
的最外层布局变高了,按道理我们的ItemDecoration
只是在ItemView
的外侧来偏移间隙,而且我们的ItemView
的布局是wrap_content
,内部紫色部分是固定高度,为什么它会变高?当我将itemview
布局设置为固定高度后还是会出现这样的状况,暂时还是想不通,所以就没有采用思路一。如果有大佬知道是什么问题的话,还请指明原因,感激不尽。
6. 注意事项
6.1 获取当前View的位置要使用getChildAdapterPosition
通过日志打印出在删除前的下标,可以看到getChildLayoutPosition()
和getChildAdapterPosition()
获取到的每个ItemView
的下标位置是相同的
getChildLayoutPosition为:0 getChildAdapterPosition为:0
getChildLayoutPosition为:1 getChildAdapterPosition为:1
getChildLayoutPosition为:2 getChildAdapterPosition为:2
getChildLayoutPosition为:3 getChildAdapterPosition为:3
getChildLayoutPosition为:4 getChildAdapterPosition为:4
getChildLayoutPosition为:5 getChildAdapterPosition为:5
getChildLayoutPosition为:6 getChildAdapterPosition为:6
getChildLayoutPosition为:7 getChildAdapterPosition为:7
getChildLayoutPosition为:8 getChildAdapterPosition为:8
getChildLayoutPosition为:9 getChildAdapterPosition为:9
getChildLayoutPosition为:10 getChildAdapterPosition为:10
getChildLayoutPosition为:11 getChildAdapterPosition为:11
getChildLayoutPosition为:12 getChildAdapterPosition为:12
getChildLayoutPosition为:13 getChildAdapterPosition为:13
getChildLayoutPosition为:14 getChildAdapterPosition为:14
getChildLayoutPosition为:15 getChildAdapterPosition为:15
删除掉下标为4的那一项后,打印日志,可以看到删除掉的那个ItemView
通过getChildAdapterPosition()
得到的下标为-1,而通过getChildLayoutPosition()
得到的下标还是为4
getChildLayoutPosition为:4 getChildAdapterPosition为:-1
getChildLayoutPosition为:12 getChildAdapterPosition为:11
getChildLayoutPosition为:13 getChildAdapterPosition为:12
getChildLayoutPosition为:14 getChildAdapterPosition为:13
getChildLayoutPosition为:15 getChildAdapterPosition为:14
getChildLayoutPosition为:4 getChildAdapterPosition为:4
getChildLayoutPosition为:5 getChildAdapterPosition为:5
getChildLayoutPosition为:6 getChildAdapterPosition为:6
getChildLayoutPosition为:7 getChildAdapterPosition为:7
getChildLayoutPosition为:8 getChildAdapterPosition为:8
getChildLayoutPosition为:9 getChildAdapterPosition为:9
getChildLayoutPosition为:10 getChildAdapterPosition为:10
这个东西在刚开始使用的时候我还没发现异常,因为在手机上显示是没有任何问题的,可是当我在模拟器上使用时出现了如图所示的现象:在移除一项后,右下角那一项的偏移明显发生了问题。
这是因为:
getAdapterPosition()
在数据刷新的时候提供一个-1的返回值,来告知视图其实正在重新绘制。
getLayoutPosition()
不会告诉你数据正在刷新,始终会返回一个位置值。这个位置值有可能是之前的视图item位置,也有可能是刷新视图后的item位置,所以不采用这个方法。
6.2 移除数据注意要数据刷新
要进行如下固定的三步操作
mAdapter.notifyItemRemoved(position)
dataList.removeAt(position)
mAdapter.notifyItemRangeChanged(position, dataList.size - position)
7. 总结
总体上思路是挺简单的,还是那一套对Rect的运用,用其按照逻辑偏移出相应的距离即可。但是细节上例如ItemView的下标位置,移除数据该怎么操作还是要多注意。
项目Github地址
以上就是全部内容,如果本文对你有帮助,请别忘记三连,如果有不恰当的地方也请提出来,下篇文章见。
8. 参考文章
android – 从RecyclerView.Adapter中删除项目后,RecyclerView.ItemDecoration不会更新
Android开发 RecyclerView.Adapter点击后的数组越界问题 与 getAdapterPosition() 与 getLayoutPosition() 的区别