在App开发中,通常会运用RecyclerView来显现列表数据,并且通常会运用ItemDecoration来设置列表项之间的距离。

最近在开发公司新版App的过程中,遇到了一个特别的列表,Item的样式是统一的,但是在某两个特定的Item之间增加了分割线,列表分为了上下两部分,如下图:

Android — 自定义ItemDecoration实现特殊分割效果

当看到这个图的时分,我的第一反应是,运用两个RecyclerView别离显现一部分数据,在两个RecyclerView中心用一个View来当分割线。这样能够很简单的完成相同的作用,但是有几点问题:

  1. 当设备屏幕高度不够时,需求在外层再嵌套一个ScrollView来确保用户能够看到所有数据。
  2. 列表中的Item是单选的,跨两个RecyclerView完成单选需求额定的处理。
  3. 这样的方式有点low哈哈哈。

ItemDecoration

ItemDecorationRecyclerView的子类,作用是对RecyclerViewItemView增加边距或许制作特别的图形。

源代码如下:

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

  • 首要拆解需求。
  1. 每个Item之间的距离是16dp。
  2. Random项和2 Player项的距离为2倍,并且在两项中居中制作一条2dp高的分割线,分割线颜色为363636。
  • 接下来,自定义CustomItemDecoration继承ItemDecoration,依据上面拆解的需求,需求重写onDrawgetItemOffsets两个办法,代码如下:
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
            }
        }
    }
}

作用如图:

Android — 自定义ItemDecoration实现特殊分割效果
  • 办法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