Directorio de artículos
1. Información general
Vi una versión de Android Kotlin del paquete RecyclerView en un proyecto de código abierto. Personalmente, lo encontré muy conveniente, así que saqué el paquete y lo grabé para usarlo en el futuro. Este proyecto de código abierto se llama DanDanPlayForAndroid. Haga clic en el enlace para verlo. el código específico del proyecto de código abierto.
2. Representaciones de operación
3. Implementación del código
3.1 Ampliar RecyclerView
Podemos ampliar el diseño de RecycleView y configurar datos y otras funciones a través de la función de extensión de Kotlin, que nos resulta conveniente llamar. El código se muestra a continuación:
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 Extender el adaptador
Antes de extender el Adaptador, primero debemos definir nuestro propio Adaptador y luego extenderlo en función de nuestro propio Adaptador. El código es el siguiente:
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"
)
}
}
Clase abstracta asociada con el código anterior:
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)!!
}
}
Implementación de clase abstracta:
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)
}
}
Extensión de la clase RVBaseAdapter
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 Dibujo de decoración de RecyclerView
RecyclerView puede heredar de la clase ItemDecoration para dibujar las líneas divisorias y decoraciones que desees. A continuación se muestran algunos ejemplos. El código es el siguiente:
3.3.1 Utilice imágenes para realizar líneas divisorias
/**
* 分割线(以图片实现)
*/
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 Dibujar líneas de cuadrícula
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 Línea divisoria en blanco
/**
* 空白的分割线
*
*/
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 Líneas divisorias en diferentes direcciones
/**
* 不同方向上的分割线
*/
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 Cómo utilizar
Al usarlo, elimine los comentarios correspondientes en el código y experimente varios estilos.
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)
Archivo de diseño:
diseño RcyclerViewActivity
<?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>
Diseño del elemento RecyclerView
<?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>
Diseño vacío cuando no hay datos
<?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>
Los lectores pueden simplemente encontrar las imágenes correspondientes y reemplazarlas con las que les gusten. Este artículo es principalmente para registros y el código no es difícil. Los lectores pueden seguirlo por sí mismos para profundizar la imagen y familiarizarse con este método de empaquetado, que puede ser utilizado en otras partes del proyecto más adelante.