在App开发中,通常会运用RecyclerView
来显现列表数据,并且通常会运用ItemDecoration
来设置列表项之间的距离。
最近在开发公司新版App的过程中,遇到了一个特别的列表,Item的样式是统一的,但是在某两个特定的Item之间增加了分割线,列表分为了上下两部分,如下图:
当看到这个图的时分,我的第一反应是,运用两个RecyclerView
别离显现一部分数据,在两个RecyclerView
中心用一个View
来当分割线。这样能够很简单的完成相同的作用,但是有几点问题:
- 当设备屏幕高度不够时,需求在外层再嵌套一个
ScrollView
来确保用户能够看到所有数据。 - 列表中的Item是单选的,跨两个
RecyclerView
完成单选需求额定的处理。 - 这样的方式有点low哈哈哈。
ItemDecoration
ItemDecoration
是RecyclerView
的子类,作用是对RecyclerView
的ItemView
增加边距或许制作特别的图形。
源代码如下:
public abstract static class ItemDecoration {
// 在制作ItemView之前制作所需图形,显现在ItemView基层
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
onDraw(c, parent);
}
@Deprecated
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {
}
// 在制作ItemView之后制作所需图形,显现在ItemView上层
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,@NonNull State state) {
onDrawOver(c, parent);
}
@Deprecated
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) {
}
@Deprecated
public void getItemOffsets(@NonNull Rect outRect, int itemPosition, @NonNull RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
// 获取ItemView的偏移参数,outRect用于设置left、top、right、bottom四个方向的距离,默以为0
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,@NonNull RecyclerView parent, @NonNull State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),parent);
}
}
自定义ItemDecoration
- 首要拆解需求。
- 每个Item之间的距离是16dp。
- Random项和2 Player项的距离为2倍,并且在两项中居中制作一条2dp高的分割线,分割线颜色为363636。
- 接下来,自定义
CustomItemDecoration
继承ItemDecoration
,依据上面拆解的需求,需求重写onDraw
和getItemOffsets
两个办法,代码如下:
class CustomItemDecoration(
private val space: Int,
private val dividerLinePosition: Int,
private val dividerLineHeight: Int,
@ColorInt private val dividerLineColor: Int
) : ItemDecoration() {
private val paint = Paint().apply {
color = dividerLineColor
style = Paint.Style.FILL
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
parent.getChildLayoutPosition(view).let {
// 指定方位底部距离设置为2倍
if (it == dividerLinePosition) {
outRect.bottom = space * 2
} else {
outRect.bottom = space
}
}
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
// 依据position找到需求制作分割线的图形
parent.getChildAt(dividerLinePosition)?.run {
c.drawRect(parent.paddingStart.toFloat(), (bottom + space - dividerLineHeight / 2).toFloat(), (parent.width - parent.paddingEnd).toFloat(), (bottom + space + dividerLineHeight / 2).toFloat(), paint)
}
}
}
Activity和Adapter代码如下:
// Activity
class CustomItemDecorationExampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = LayoutCustomItemDecorationExampleActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.run {
includeTitle.tvTitle.setTextColor(ContextCompat.getColor(this@CustomItemDecorationExampleActivity, R.color.white))
includeTitle.tvTitle.text = "Custom ItemDecoration Example"
val adapter = ItemDecorationExampleAdapter()
rvExampleDataContainer.addItemDecoration(CustomItemDecoration(DensityUtil.dp2Px(16), 6, DensityUtil.dp2Px(2), ContextCompat.getColor(this@CustomItemDecorationExampleActivity, R.color.color_black_363636)))
rvExampleDataContainer.adapter = adapter
adapter.setNewData(arrayListOf(
ItemDecorationExampleDataEntity(0, "Home", R.mipmap.icon_tag_all, true),
ItemDecorationExampleDataEntity(1, "Recently Played", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(2, "New Games", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(3, "Trending Now", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(4, "Updated", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(5, "The Game Blog", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(6, "Random", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(7, "2 Player", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(8, "Adventure", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(9, "Action", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(10, "Strategy", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(11, "Casual", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(12, ".io", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(13, "Horror", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(14, "3d", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(15, "Driving", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(16, "Shoting", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(17, "Puzzel", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(18, "Beauty", R.mipmap.icon_tag_all, false),
ItemDecorationExampleDataEntity(19, "Parkour", R.mipmap.icon_tag_all, false),
))
}
}
}
// Adapter
class ItemDecorationExampleAdapter : RecyclerView.Adapter<ItemDecorationExampleAdapter.ItemDecorationExampleViewHolder>() {
private val containerData = ArrayList<ItemDecorationExampleDataEntity>()
private var lastSelectedItem = -1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemDecorationExampleViewHolder {
return ItemDecorationExampleViewHolder(LayoutCustomItemDecorationExampleItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun getItemCount(): Int {
return containerData.size
}
override fun onBindViewHolder(holder: ItemDecorationExampleViewHolder, position: Int) {
containerData[position].run {
if (lastSelectedItem == -1 && selected) {
lastSelectedItem = holder.bindingAdapterPosition
}
holder.itemViewBinding.ivTagIcon.setImageDrawable(ContextCompat.getDrawable(holder.itemView.context, icon))
holder.itemViewBinding.tvTagText.text = name
holder.itemViewBinding.ctlContainer.isSelected = selected
holder.itemView.setOnClickListener { selectItem(position) }
}
}
fun setNewData(newData: ArrayList<ItemDecorationExampleDataEntity>?) {
val currentItemCount = itemCount
if (currentItemCount != 0) {
containerData.clear()
notifyItemRangeRemoved(0, currentItemCount)
}
if (!newData.isNullOrEmpty()) {
containerData.addAll(newData)
notifyItemRangeChanged(0, itemCount)
}
}
private fun selectItem(position: Int) {
if (lastSelectedItem != position) {
if (lastSelectedItem != -1) {
containerData[lastSelectedItem].selected = false
notifyItemChanged(lastSelectedItem)
lastSelectedItem = position
}
if (position >= 0) {
containerData[position].selected = true
notifyItemChanged(position)
}
}
}
class ItemDecorationExampleViewHolder(val itemViewBinding: LayoutCustomItemDecorationExampleItemBinding) : RecyclerView.ViewHolder(itemViewBinding.root)
}
作用如图:
能够看到,初始状态下成功在希望的方位制作了分割线,但是滑动时分割线并没有一向显现在Random项和2Player项中心。
发现问题
经过调试,发现在onDraw
中,运用parent.getChildAt
获取的ItemView
是从可见的ItemView
开始计数的,因而同一个position并不是一向对应同一个ItemView
,所以终究作用才会如上图。那么现在的问题就是怎么确保分割线总是在Random项下方制作。
改善办法
- 办法1:
在onDraw
顶用循环和parent.getChildAt
遍历获取所有的ItemView
,并运用parent.getChildAdapterPosition
获取ItemView
在适配器中正确的position,判别当ItemView
的正确position与设定的position相同时再制作分割线,代码调整如下:
class CustomItemDecoration(...) : ItemDecoration() {
...
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
for (index in 0 until parent.childCount) {
val itemView = parent.getChildAt(index)
// 确保itemView在适配器中的position是需求制作分割线的position
if (dividerLinePosition == parent.getChildAdapterPosition(itemView)) {
itemView.run {
c.drawRect(parent.paddingStart.toFloat(), (bottom + space - dividerLineHeight / 2).toFloat(), (parent.width - parent.paddingEnd).toFloat(), (bottom + space + dividerLineHeight / 2).toFloat(), paint)
}
break
}
}
}
}
作用如图:
- 办法2:
在onDraw
中运用循环会对性能有一定的影响,因而想了一种不运用循环的办法,在getItemOffsets
时获取并保存Random项的ItemView
,在onDraw
中运用保存的ItemView
进行制作,代码调整如下:
class CustomItemDecoration(...) : ItemDecoration() {
...
private var specialItemView: View? = null
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
parent.getChildLayoutPosition(view).let {
if (it == dividerLinePosition) {
specialItemView = view
...
} else {
...
}
}
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
specialItemView?.run {
// 防止View被复用
if (dividerLinePosition == parent.getChildAdapterPosition(this)) {
c.drawRect(parent.paddingStart.toFloat(), (bottom + space - dividerLineHeight / 2).toFloat(), (parent.width - parent.paddingEnd).toFloat(), (bottom + space + dividerLineHeight / 2).toFloat(), paint)
}
}
}
}
作用如图:
示例
演示代码已在示例Demo中增加。
ExampleDemo github
ExampleDemo gitee