Article directory
1 Overview
I saw an Android Kotlin version of the RecyclerView package on an open source project. I found it very convenient, so I took out the package and recorded it for future use. This open source project is called DanDanPlayForAndroid. Click the link to view the specific open source project . code.
2. Operation renderings
3. Code implementation
3.1 Extend RecyclerView
We can extend the layout of RecycleView and set data and other functions through Kotlin's extension function, which is convenient for us to call. code show as below:
fun RecyclerView.vertical(
reverse: Boolean = false
): LinearLayoutManager {
return LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
reverse
)
}
fun RecyclerView.horizontal(
reverse: Boolean = false
): LinearLayoutManager {
return LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
reverse
)
}
fun RecyclerView.grid(
spanCount: Int
): GridLayoutManager {
return GridLayoutManager(context, spanCount)
}
fun RecyclerView.gridEmpty(spanCount: Int): GridLayoutManager {
return GridLayoutManager(context, spanCount).also {
it.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
if (position == RecyclerView.NO_POSITION) {
return 1
}
val viewType = adapter?.getItemViewType(position)
if (viewType != -1) {
return 1
}
return spanCount
}
}
}
}
fun RecyclerView.setData(itemData: List<Any>) {
(adapter as RVBaseAdapter).setData(itemData)
}
fun RecyclerView.requestIndexChildFocus(index: Int): Boolean {
scrollToPosition(index)
val targetTag = "tag_focusable_item"
val indexView = layoutManager?.findViewByPosition(index)
if (indexView != null) {
indexView.findViewWithTag<View>(targetTag)?.requestFocus()
return true
}
post {
layoutManager?.findViewByPosition(index)
?.findViewWithTag<View>(targetTag)
?.requestFocus()
}
return true
}
3.2 Extend Adapter
Before extending the Adapter, we need to first define an Adapter of our own, and then extend it based on our own Adapter. The code is as follows:
class RVBaseAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object{
// the data of empty layout
val EMPTY_ITEM = Any()
// view type of empty layout
const val VIEW_TYPE_EMPTY = -1
// number of max item
private const val NUMBER_OF_MAX_VIEW_TYPE = Int.MAX_VALUE -1
}
val itemData: MutableList<Any> = mutableListOf()
private val typeHolders =
SparseArrayCompat<BaseViewHolderCreator<out ViewDataBinding>>()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder {
return BaseViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
getHolderCreator(viewType).getResourceId(),
parent,
false
)
)
}
private fun getHolderCreator(viewType: Int):
BaseViewHolderCreator<out ViewDataBinding> {
return typeHolders.get(viewType)
?: throw java.lang.RuntimeException()
}
override fun getItemCount(): Int {
return itemData.size
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder,
position: Int) {
getHolderCreator(holder.itemViewType).apply {
initItemBinding(holder.itemView)
onBindViewHolder(itemData[position],position,this)
}
}
fun setData(dataList: List<Any>) {
itemData.clear()
itemData.addAll(dataList)
// show the empty layout when data is empty
if(itemData.isEmpty() && typeHolders.containsKey(VIEW_TYPE_EMPTY)){
itemData.add(EMPTY_ITEM)
}
notifyDataSetChanged()
}
fun register(creator: BaseViewHolderCreator<out ViewDataBinding>,
customViewType: Int? = null) {
apply {
var viewType = customViewType ?: typeHolders.size()
while (typeHolders.get(viewType) != null) {
viewType++
require(viewType < NUMBER_OF_MAX_VIEW_TYPE) {
"the number of view type has reached the maximum limit"
}
}
require(viewType < NUMBER_OF_MAX_VIEW_TYPE) {
"the number of view type has reached the maximum limit"
}
typeHolders.put(viewType, creator)
}
}
override fun getItemViewType(position: Int): Int {
if(itemData[position] == EMPTY_ITEM
&& typeHolders.containsKey(VIEW_TYPE_EMPTY)){
return VIEW_TYPE_EMPTY
}
// only one viewHolder
if(typeHolders.size() == 1){
return typeHolders.keyAt(0)
}
// more than one viewHolder
for (i in 0 until typeHolders.size()){
if(typeHolders.keyAt(i) == VIEW_TYPE_EMPTY){
continue
}
val holder = typeHolders.valueAt(i)
if(holder.isForViewType(itemData[position],position)){
return typeHolders.keyAt(i)
}
}
throw java.lang.IllegalStateException(
"no holder added that matches at position: $position in data source"
)
}
}
Abstract class associated with the above code:
class BaseViewHolder(binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
}
abstract class BaseViewHolderCreator<V : ViewDataBinding> {
abstract fun isForViewType(data: Any?, position: Int): Boolean
abstract fun getResourceId(): Int
abstract fun onBindViewHolder(
data: Any?,
position: Int,
creator: BaseViewHolderCreator<out ViewDataBinding>
)
lateinit var itemDataBinding: V
fun initItemBinding(itemView: View) {
this.itemDataBinding = DataBindingUtil.getBinding(itemView)!!
}
}
Implementation of abstract class:
class BaseViewHolderDSL<T : Any, V : ViewDataBinding>(
private val resourceId: Int,
private val clazz: KClass<T>
) : BaseViewHolderCreator<V>() {
private var checkViewType: ((data: Any, position: Int) -> Boolean)? = null
private var viewHolder: (
(data: T, position: Int, creator:
BaseViewHolderCreator<out ViewDataBinding>) -> Unit
)? = null
private var emptyViewHolder: (() -> Unit)? = null
override fun isForViewType(data: Any?, position: Int): Boolean {
if(data == null){
return false
}
if(checkViewType != null){
return checkViewType!!.invoke(data,position)
}
return clazz.isInstance(data)
}
/**
* judge the type of current item data according to position
*/
fun checkType(viewType:(data:Any,position:Int) ->Boolean){
this.checkViewType = viewType
}
fun initView(
holder:(
data:T,
position:Int,
holder:BaseViewHolderCreator<out ViewDataBinding>
)->Unit
){
this.viewHolder = holder
}
override fun getResourceId(): Int {
return resourceId
}
override fun onBindViewHolder(
data: Any?,
position: Int,
creator: BaseViewHolderCreator<out ViewDataBinding>
) {
// empty layout
if(data == RVBaseAdapter.EMPTY_ITEM){
emptyViewHolder?.invoke()
return
}
data ?: return
viewHolder?.invoke(data as T,position,creator)
}
}
Extension of RVBaseAdapter class
fun buildAdapter(init: RVBaseAdapter.() -> Unit): RVBaseAdapter {
return RVBaseAdapter().apply {
init()
}
}
inline fun <reified T : Any, V : ViewDataBinding> RVBaseAdapter.addItem(
resourceID: Int,
init: BaseViewHolderDSL<T, V>.() -> Unit
) {
register(
BaseViewHolderDSL<T, V>(resourceID, T::class).apply {
init() }
)
}
inline fun RVBaseAdapter.addEmptyView(
resourceID: Int,
init: (BaseViewHolderDSL<Any, LayoutEmptyBinding>.() -> Unit) = {
}
) {
register(
BaseViewHolderDSL<Any, LayoutEmptyBinding>(resourceID, Any::class)
.apply {
init()
},
customViewType = RVBaseAdapter.VIEW_TYPE_EMPTY
)
setData(listOf(RVBaseAdapter.EMPTY_ITEM))
}
3.3 RecyclerView decoration drawing
RecyclerView can inherit from the ItemDecoration class to draw the dividing lines and decorations you want. Here are a few examples. The code is as follows:
3.3.1 Use pictures to realize dividing lines
/**
* 分割线(以图片实现)
*/
class MyItemDecoration(divider: Drawable, dividerSize: Int) :
RecyclerView.ItemDecoration() {
private val mDivider = divider
private val mDividerSize = dividerSize
override fun onDraw(canvas: Canvas, parent: RecyclerView, state:
RecyclerView.State) {
canvas.save()
//居中显示
val top = (parent.height - mDividerSize) / 2
val bottom = top + mDividerSize
val mBounds = Rect()
//只在中间绘制
for (i in 0 until parent.childCount - 1) {
val child = parent.getChildAt(i)
parent.layoutManager!!.getDecoratedBoundsWithMargins(child, mBounds)
val right = mBounds.right + child.translationX.roundToInt()
val left = right - mDividerSize
mDivider.setBounds(left, top, right, bottom)
mDivider.draw(canvas)
}
canvas.restore()
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
outRect.set(0, 0, mDividerSize, 0)
}
}
3.3.2 Draw grid lines
class ItemGridDecorationDrawable : ItemDecoration {
private var leftRight: Int
private var topBottom: Int
private var mDivider: Drawable?
constructor(spacePx: Int) {
leftRight = spacePx
topBottom = spacePx
mDivider = ColorDrawable(Color.WHITE)
}
constructor(leftRight: Int, topBottom: Int) {
this.leftRight = leftRight
this.topBottom = topBottom
mDivider = ColorDrawable(Color.WHITE)
}
constructor(leftRight: Int, topBottom: Int, mColor: Int) {
this.leftRight = leftRight
this.topBottom = topBottom
mDivider = ColorDrawable(mColor)
}
override fun onDraw(
c: Canvas,
parent: RecyclerView,
state: RecyclerView.State
) {
val layoutManager = parent.layoutManager
as GridLayoutManager? ?: return
val lookup = layoutManager.spanSizeLookup
if (mDivider == null || layoutManager.childCount == 0) {
return
}
//判断总的数量是否可以整除
val spanCount = layoutManager.spanCount
var left: Int
var right: Int
var top: Int
var bottom: Int
val childCount = parent.childCount
if (layoutManager.orientation == GridLayoutManager.VERTICAL) {
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
//将带有颜色的分割线处于中间位置
val centerLeft =
((layoutManager.getLeftDecorationWidth(child) + layoutManager.getRightDecorationWidth(
child
)).toFloat()
* spanCount / (spanCount + 1) + 1 - leftRight) / 2
val centerTop =
(layoutManager.getBottomDecorationHeight(child)
+ 1 - topBottom) / 2f
//得到它在总数里面的位置
val position = parent.getChildAdapterPosition(child)
//获取它所占有的比重
val spanSize = lookup.getSpanSize(position)
//获取每排的位置
val spanIndex = lookup.getSpanIndex(position,
layoutManager.spanCount)
//判断是否为第一排
val isFirst =
layoutManager.spanSizeLookup.getSpanGroupIndex(position,
spanCount) == 0
//画上边的,第一排不需要上边的,只需要在最左边的那项的时候画一次就好
if (!isFirst && spanIndex == 0) {
left = layoutManager.getLeftDecorationWidth(child)
right = parent.width -
layoutManager.getLeftDecorationWidth(child)
top = (child.top - centerTop).toInt() - topBottom
bottom = top + topBottom
mDivider!!.setBounds(left, top, right, bottom)
mDivider!!.draw(c)
}
//最右边的一排不需要右边的
val isRight = spanIndex + spanSize == spanCount
if (!isRight) {
//计算右边的
left = (child.right + centerLeft).toInt()
right = left + leftRight
top = child.top
if (!isFirst) {
top -= centerTop.toInt()
}
bottom = (child.bottom + centerTop).toInt()
mDivider!!.setBounds(left, top, right, bottom)
mDivider!!.draw(c)
}
}
} else {
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
//将带有颜色的分割线处于中间位置
val centerLeft =
(layoutManager.getRightDecorationWidth(child)
+ 1 - leftRight) / 2f
val centerTop =
((layoutManager.getTopDecorationHeight(child) + layoutManager.getBottomDecorationHeight(
child
)).toFloat()
* spanCount / (spanCount + 1) - topBottom) / 2
//得到它在总数里面的位置
val position = parent.getChildAdapterPosition(child)
//获取它所占有的比重
val spanSize = lookup.getSpanSize(position)
//获取每排的位置
val spanIndex = lookup
.getSpanIndex(position, layoutManager.spanCount)
//判断是否为第一列
val isFirst =
layoutManager.spanSizeLookup
.getSpanGroupIndex(position, spanCount) == 0
//画左边的,第一排不需要左边的,只需要在最上边的那项的时候画一次就好
if (!isFirst && spanIndex == 0) {
left = (child.left - centerLeft).toInt() - leftRight
right = left + leftRight
top = layoutManager.getRightDecorationWidth(child)
bottom = parent.height - layoutManager.getTopDecorationHeight(child)
mDivider!!.setBounds(left, top, right, bottom)
mDivider!!.draw(c)
}
//最下的一排不需要下边的
val isRight = spanIndex + spanSize == spanCount
if (!isRight) {
//计算右边的
left = child.left
if (!isFirst) {
left -= centerLeft.toInt()
}
right = (child.right + centerTop).toInt()
top = (child.bottom + centerLeft).toInt()
bottom = top + leftRight
mDivider!!.setBounds(left, top, right, bottom)
mDivider!!.draw(c)
}
}
}
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val layoutManager = parent.layoutManager as GridLayoutManager? ?: return
val lp =
view.layoutParams as GridLayoutManager.LayoutParams
val childPosition = parent.getChildAdapterPosition(view)
val spanCount = layoutManager.spanCount
if (layoutManager.orientation == GridLayoutManager.VERTICAL) {
//判断是否在第一排
if (layoutManager.spanSizeLookup.getSpanGroupIndex(
childPosition,
spanCount
) == 0
) {
//第一排的需要上面
outRect.top = topBottom
}
outRect.bottom = topBottom
//这里忽略和合并项的问题,只考虑占满和单一的问题
if (lp.spanSize == spanCount) {
//占满
outRect.left = leftRight
outRect.right = leftRight
} else {
outRect.left =
((spanCount - lp.spanIndex).toFloat() / spanCount * leftRight).toInt()
outRect.right =
(leftRight.toFloat() * (spanCount + 1) / spanCount - outRect.left).toInt()
}
} else {
if (layoutManager.spanSizeLookup.getSpanGroupIndex(
childPosition,
spanCount
) == 0
) {
//第一排的需要left
outRect.left = leftRight
}
outRect.right = leftRight
//这里忽略和合并项的问题,只考虑占满和单一的问题
if (lp.spanSize == spanCount) {
//占满
outRect.top = topBottom
outRect.bottom = topBottom
} else {
outRect.top =
((spanCount - lp.spanIndex).toFloat() / spanCount * topBottom).toInt()
outRect.bottom =
(topBottom.toFloat() * (spanCount + 1) / spanCount - outRect.top).toInt()
}
}
}
}
3.3.3 Blank dividing line
/**
* 空白的分割线
*
*/
class ItemDecorationSpace : ItemDecoration {
private var top: Int
private var left: Int
private var right: Int
private var bottom: Int
private var spanCount: Int
constructor(space: Int) : this(space, space, space, space)
constructor(spaceLR: Int, spaceTB: Int) : this(spaceTB, spaceLR, spaceLR,
spaceTB)
constructor(top: Int, left: Int, right: Int, bottom: Int) {
this.top = top
this.left = left
this.right = right
this.bottom = bottom
spanCount = 0
}
constructor(top: Int, left: Int, right: Int, bottom: Int, spanCount: Int) {
this.top = top
this.left = left
this.right = right
this.bottom = bottom
this.spanCount = spanCount
}
override fun getItemOffsets(
outRect: Rect, view: View,
parent: RecyclerView, state: RecyclerView.State
) {
outRect.top = top
outRect.left = left
outRect.bottom = bottom
if (spanCount != 0) {
val position = parent.getChildLayoutPosition(view)
if ((position + 1) % spanCount == 0) {
outRect.right = 0
} else {
outRect.right = right
}
} else {
outRect.right = right
}
}
}
3.3.4 Dividing lines in different directions
/**
* 不同方向上的分割线
*/
class ItemDecorationOrientation : ItemDecoration {
private val dividerPx: Int
private val headerPx: Int
private val footerPx: Int
private val orientation: Int
constructor(dividerPx: Int, @RecyclerView.Orientation orientation: Int)
: this(
dividerPx,
dividerPx,
orientation
)
constructor(
dividerPx: Int,
headerFooterPx: Int,
@RecyclerView.Orientation orientation: Int
) : this(dividerPx, headerFooterPx, headerFooterPx, orientation)
constructor(
dividerPx: Int,
headerPx: Int,
footerPx: Int,
@RecyclerView.Orientation orientation: Int
) {
this.dividerPx = dividerPx
this.headerPx = headerPx
this.footerPx = footerPx
this.orientation = orientation
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
if (orientation == RecyclerView.VERTICAL) {
getItemOffsetsVertical(outRect, view, parent)
} else {
getItemOffsetsHorizontal(outRect, view, parent)
}
}
private fun getItemOffsetsVertical(outRect: Rect, view: View,
parent: RecyclerView) {
val itemCount = parent.adapter?.itemCount ?: return
val position = parent.getChildAdapterPosition(view)
if (position == 0) {
outRect.top = headerPx
} else {
outRect.top = position * dividerPx / itemCount
}
if (position == itemCount - 1) {
outRect.bottom = footerPx
} else {
outRect.bottom = dividerPx - (position + 1) * dividerPx / itemCount
}
}
private fun getItemOffsetsHorizontal(outRect: Rect, view: View, parent:
RecyclerView) {
val itemCount = parent.adapter?.itemCount ?: return
val position = parent.getChildAdapterPosition(view)
if (position == 0) {
outRect.left = headerPx
} else {
outRect.left = position * dividerPx / itemCount
}
if (position == itemCount - 1) {
outRect.right = footerPx
} else {
outRect.right = dividerPx - (position + 1) * dividerPx / itemCount
}
}
}
3.4 How to use
When using it, remove the corresponding comments in the code and experience various styles.
class RecyclerViewActivity : AppCompatActivity() {
private lateinit var dataBinding: ActivityRecyclerViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initDataBinding()
initRV()
val dataList = listOf<UserData>(
UserData("walt zhong", 21),
UserData("walt xian", 22),
UserData("walt jian", 31),
UserData("walt x", 22),
UserData("walt y", 41),
UserData("walt z", 26),
UserData("walt 2", 29),
)
// val dataList = emptyList<UserData>()
dataBinding.rvList.setData(dataList)
}
private fun initRV() {
dataBinding.rvList.apply {
// layoutManager = gridEmpty(3) //网格布局
// layoutManager = vertical(false) // 垂直布局
layoutManager = horizontal(false) // 水平布局
adapter = buildAdapter {
addEmptyView(R.layout.layout_empty)
addItem<UserData, RvItemBinding>(R.layout.rv_item) {
initView {
data, position, _ ->
itemDataBinding.apply {
tvName.text = data.name
tvAge.text = data.age.toString()
itemLayout.setOnClickListener {
Log.d("zhongxj", "click item: $position")
}
}
}
}
}
// val pxValue = dp2px(5)
//
// addItemDecoration(
// ItemGridDecorationDrawable(
// pxValue,
// pxValue,
// R.color.purple_200
// )
// )
// addItemDecoration(
// ItemDecorationSpace(
// pxValue
// )
// )
// addItemDecoration(
// ItemDecorationOrientation(
// dividerPx = pxValue,
// headerFooterPx = 0,
// orientation = RecyclerView.HORIZONTAL
// )
// )
val dividerSize = dp2px(16)
val divider = ContextCompat.getDrawable(context, R.drawable.ic_arrow)
if(divider != null){
addItemDecoration(
MyItemDecoration(
divider,
dividerSize
)
)
}
}
}
private fun initDataBinding() {
dataBinding = DataBindingUtil.setContentView(
this,
R.layout.activity_recycler_view
)
dataBinding.lifecycleOwner = this@RecyclerViewActivity
}
/**
* 单位转换,将DP转为PX
*/
fun dp2px(dpValue: Int): Int {
val scale = Resources.getSystem().displayMetrics.density
return (dpValue * scale + 0.5f).toInt()
}
}
data class UserData(var name:String,var age:Int)
Layout file:
RcyclerViewActivity layout
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<LinearLayout
android:background="#eeeeee"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".RecyclerViewActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</layout>
RecyclerView item layout
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<LinearLayout
android:background="@color/white"
android:padding="10dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="100dp"
android:id="@+id/item_layout"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="walt"
android:id="@+id/tv_name"/>
<TextView
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="24"
android:id="@+id/tv_age"/>
</LinearLayout>
</layout>
Empty layout when there is no data
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/empty_iv"
android:layout_width="200dp"
android:layout_height="200dp"
android:src="@mipmap/ic_empty_data"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.382" />
<TextView
android:id="@+id/empty_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="没有数据"
android:textColor="@color/black"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/empty_iv" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Readers can just find the corresponding pictures and replace them with the ones they like. This article is mainly for records, and the code is not difficult. Readers can follow it by themselves to deepen the image and become familiar with this encapsulation method, which can be used in other parts of the project later. Encapsulation.