前言
曾经有幸看到过鸿洋大神使用装饰者模式实现的HeaderAndFooterWrapper为RecyclerView优雅的添加Header和Footer的项目。它的优势已经无需赘述。我在使用过程中发现几个经常需要处理的问题就是
1,在装饰者模式嵌套后,对WrappedAdapter的position的处理;
2,EmptyWrapper未考虑二次设置EmptyView的问题;
3,EmptyWrapper包装HeaderAndFooterWrapper,当需要在数据为空时,隐藏Header和Footer,只显示EmptyView不得不重写HeaderAndFooterWrapper的问题。
4,由于RecycerView真实Adapter是WrapperAdapter,导致WrappedAdaper中调用直接notifyXXXX()等方法不能刷新UI,只能通过RecyclerView调用getAdapter()刷新,这种方法同样导致问题1发生。
虽然这几种问题都有解决办法,但是处理起来颇为繁琐。所以我才萌生了自己重新封装的想法。
思路
问题1:WrapperAdaper中提供position相关API。
问题2,3:采用依赖注入将相关的API暴露出来。
问题4:为WrappedAdapter注册RecyclerView.AdapterDataObserver,并在WrapperAdapter中响应更新。
另外,添加一个Wrapper处理多选。
实现
AdapterWrapper提供position相关API
package com.iyao.recyclerviewhelper.adapter
import android.support.v7.widget.RecyclerView
abstract class AbsAdapterWrapper : RecyclerView.Adapter(), AdapterWrapper {
companion object {
val CHANGE_NONE_CONTENT = Any()
}
lateinit var client: RecyclerView.Adapter
private val observer = object : RecyclerView.AdapterDataObserver() {
override fun onChanged()
= notifyItemRangeChanged(getWrapperAdapterPosition(0), client.itemCount)
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?)
= notifyItemRangeChanged(getWrapperAdapterPosition(positionStart), itemCount, payload)
override fun onItemRangeChanged(positionStart: Int, itemCount: Int)
= notifyItemRangeChanged(getWrapperAdapterPosition(positionStart), itemCount)
override fun onItemRangeInserted(positionStart: Int, itemCount: Int)
= notifyItemRangeInserted(getWrapperAdapterPosition(positionStart), itemCount)
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int)
= notifyItemMoved(getWrapperAdapterPosition(fromPosition), getWrapperAdapterPosition(toPosition))
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
notifyItemRangeRemoved(getWrapperAdapterPosition(positionStart), itemCount)
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
check(::client.isInitialized) {"client is null : ${javaClass.simpleName}"}
client.onAttachedToRecyclerView(recyclerView)
client.registerAdapterDataObserver(observer)
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
client.onDetachedFromRecyclerView(recyclerView)
client.unregisterAdapterDataObserver(observer)
}
override fun onViewAttachedToWindow(holder: VH) {
check(::client.isInitialized) {"client is null : ${javaClass.simpleName}"}
client.onViewAttachedToWindow(holder)
}
override fun onViewDetachedFromWindow(holder: VH) {
client.onViewDetachedFromWindow(holder)
}
override fun onFailedToRecycleView(holder: VH): Boolean {
return client.onFailedToRecycleView(holder)
}
override fun onViewRecycled(holder: VH) {
client.onViewRecycled(holder)
}
override fun getItemCount() = if (::client.isInitialized) client.itemCount else 0
override fun getItemViewType(position: Int): Int {
return client.getItemViewType(getWrappedPosition(position))
}
override fun onBindViewHolder(holder: VH, position: Int, payloads: MutableList) {
when {
payloads.isNotEmpty() && payloads[0] == CHANGE_NONE_CONTENT -> Unit
payloads.isNotEmpty() -> client.onBindViewHolder(holder, getWrappedPosition(position), payloads)
else -> onBindViewHolder(holder, position)
}
}
override fun getWrappedAdapter(): RecyclerView.Adapter = client
}
HeaderAndFooterWrapper添加任意数量的Header和Footer
package com.iyao.recyclerviewhelper.adapter
import android.support.v7.widget.RecyclerView
import android.util.SparseArray
import android.view.ViewGroup
open class HeaderAndFooterWrapper : AbsAdapterWrapper() {
private val headers : SparseArray = SparseArray()
private val footers : SparseArray = SparseArray()
override fun getItemCount() = headers.size() + super.getItemCount() + footers.size()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
return when {
headers.indexOfKey(viewType) >= 0 -> headers[viewType]
footers.indexOfKey(viewType) >= 0 -> footers[viewType]
else -> client.onCreateViewHolder(parent, viewType)
}
}
override fun onBindViewHolder(holder: VH, position: Int) {
if (position in headers.size() until itemCount - footers.size()) {
client.onBindViewHolder(holder, getWrappedPosition(position))
}
}
override fun getItemViewType(position: Int): Int {
return when(position){
in 0 until headers.size() -> headers.keyAt(position)
in headers.size() until itemCount - footers.size() -> client.getItemViewType(getWrappedPosition(position))
else -> footers.keyAt(position - headers.size() - client.itemCount)
}
}
override fun getWrappedPosition(wrapperPosition: Int) = wrapperPosition.minus(headers.size())
override fun getWrapperAdapterPosition(wrappedPosition: Int) = wrappedPosition.plus(headers.size())
fun addHeader(viewType: Int, holder: VH) = headers.put(viewType, holder)
fun addFooter(viewType: Int, holder: VH) = footers.put(viewType, holder)
fun removeHeader(viewType: Int) = headers.indexOfKey(viewType).run {
headers.removeAt(this)
notifyItemRemoved(this)
}
fun removeFooter(viewType: Int) = headers.indexOfKey(viewType).run {
footers.removeAt(this)
notifyItemRemoved(headers.size().plus(client.itemCount).plus(this))
}
}
MultipleChoiceWrapper处理单选和多选
package com.iyao.recyclerviewhelper.adapter
import android.support.annotation.MainThread
import android.support.v4.util.LongSparseArray
import android.support.v7.widget.RecyclerView
import android.util.SparseBooleanArray
import android.view.ViewGroup
import android.widget.Checkable
open class MultipleChoiceWrapper : AbsAdapterWrapper() {
private val checkedIds: LongSparseArray = LongSparseArray()
private val checkedStates: SparseBooleanArray = SparseBooleanArray()
private var checkedCount = 0
private val observer = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
getWrapperAdapterPosition(positionStart).run {
(getItemCount() - 1 downTo this + itemCount).forEach {
setItemChecked(it, isItemChecked(it - itemCount))
}.also {
(this until this.plus(itemCount)).forEach {
setItemChecked(it, false)
}
}
}
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
getWrapperAdapterPosition(fromPosition).also { positionFrom ->
getWrapperAdapterPosition(toPosition).also { positionTo ->
isItemChecked(positionFrom).run {
when {
positionFrom < positionTo -> (positionFrom until positionTo).forEach {
setItemChecked(it, isItemChecked(it + 1))
}
else -> (positionFrom downTo positionTo + 1).forEach {
setItemChecked(it, isItemChecked(it - 1))
}
}
setItemChecked(positionTo, this)
}
}
}
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
getWrapperAdapterPosition(positionStart).run {
(this until getItemCount()).forEach {
setItemChecked(it, isItemChecked(it + itemCount))
}.also {
(getItemCount() until getItemCount() + itemCount).forEach {
setItemChecked(it, false)
}
}
}
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
client.registerAdapterDataObserver(observer)
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
client.unregisterAdapterDataObserver(observer)
}
override fun getItemId(position: Int) = client.getItemId(position)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH = client.onCreateViewHolder(parent, viewType)
override fun onBindViewHolder(holder: VH, position: Int, payloads: MutableList) {
when {
payloads.isNotEmpty() && payloads[0] == MULTIPLE_CHOICE_PAYLOAD -> {
(holder.itemView as? Checkable)?.isChecked = isItemChecked(position)
}
else -> super.onBindViewHolder(holder, position, payloads)
}
}
override fun onBindViewHolder(holder: VH, position: Int) = client.onBindViewHolder(holder, getWrappedPosition(position)).also {
(holder.itemView as? Checkable)?.isChecked = isItemChecked(position)
}
override fun getWrappedPosition(wrapperPosition: Int) = wrapperPosition
override fun getWrapperAdapterPosition(wrappedPosition: Int) = wrappedPosition
@MainThread
fun setItemChecked(position: Int, checked: Boolean) {
if (position in 0 until itemCount && checked != isItemChecked(position)) {
checkedStates.put(position, checked)
checkedCount = if (checked) checkedCount + 1 else checkedCount - 1
notifyItemChanged(position, MULTIPLE_CHOICE_PAYLOAD)
if (hasStableIds()) {
getItemId(position).let {
checked.run {
when {
this -> checkedIds.put(it, position)
else -> checkedIds.delete(it)
}
}
}
}
} else Unit
}
fun isItemChecked(position: Int) = checkedStates[position]
fun getItemCheckedCount() = checkedCount
fun getCheckedItemIds() = LongArray(getItemCheckedCount()) { position -> checkedIds.keyAt(position) }
@MainThread
fun clearChoices() {
if (hasStableIds()) {
checkedIds.apply {
(size() - 1 downTo 0).forEach { it ->
valueAt(it)?.run {
setItemChecked(this, false)
}
}
}
} else {
(0 until checkedStates.size()).forEach {
setItemChecked(checkedStates.keyAt(it), false)
}
}
checkedStates.clear()
}
private companion object {
const val MULTIPLE_CHOICE_PAYLOAD = "multiple_choice"
}
}
StatusWrapper实现各种空视图
package com.iyao.recyclerviewhelper.adapter
import android.support.annotation.IntRange
import android.support.v7.widget.RecyclerView
import android.util.SparseArray
import android.view.ViewGroup
import java.lang.IllegalArgumentException
class StatusWrapper : AbsAdapterWrapper() {
companion object {
const val STATUS_NORMAL = -1
}
private val statusViewHolders = SparseArray()
var currentStatus : Int = STATUS_NORMAL
override fun getItemCount() = if (currentStatus == STATUS_NORMAL) super.getItemCount() else 1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
return if (currentStatus != STATUS_NORMAL) {
statusViewHolders.get(currentStatus)
} else {
client.onCreateViewHolder(parent, viewType)
}
}
override fun onBindViewHolder(holder: VH, position: Int, payloads: MutableList) {
when (currentStatus) {
STATUS_NORMAL -> super.onBindViewHolder(holder, position, payloads)
else -> onBindViewHolder(holder, position)
}
}
override fun onBindViewHolder(holder: VH, position: Int) {
if (currentStatus == STATUS_NORMAL) {
client.onBindViewHolder(holder, getWrappedPosition(position))
}
}
override fun getItemViewType(position: Int): Int {
return if (currentStatus != STATUS_NORMAL) currentStatus else client.getItemViewType(getWrappedPosition(position))
}
override fun getWrappedPosition(wrapperPosition: Int) = wrapperPosition
override fun getWrapperAdapterPosition(wrappedPosition: Int) = wrappedPosition
fun addStatusView(@IntRange(from = -100, to = -2) viewType: Int, holder : VH) {
check(viewType in -2 downTo -100, {"status must not be negative integer"}).run {
statusViewHolders.put(viewType, holder)
}
}
fun setCurrentStatus(@IntRange(from = -100, to = -2) viewType: Int) = statusViewHolders[viewType]?.apply {
currentStatus = viewType
notifyDataSetChanged()
} ?: throw IllegalArgumentException("Invalid viewType: $viewType")
fun setCurrentStatusIf(@IntRange(from = -100, to = -2) status: Int, predicate: (VH) -> Boolean) {
statusViewHolders[status]?.takeIf {
predicate.invoke(it)
}.run {
currentStatus = this?.let { status } ?: STATUS_NORMAL
notifyDataSetChanged()
}
}
}
使用
recycler_view.apply {
layoutManager = GridLayoutManager(this@MainActivity, 5).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (adapter.getItemViewType(position)) {
//statusView
in -100..-1 -> spanCount
in android.R.layout.simple_list_item_multiple_choice + 1..android.R.layout.simple_list_item_multiple_choice + 3 ->
spanCount
else -> 1
}
}
}
}
itemTouchHelper.attachToRecyclerView(this)
addItemDecoration(GridLayoutItemDecoration().apply {
startAndEndDecoration = 50
topAndBottomDecoration = 30
horizontalMiddleDecoration = 30
verticalMiddleDecoration = 30
decorateFullItem = true
})
adapter = CachedStatusWrapper().apply {
client = CachedHeaderAndFooterWrapper().apply {
client = CachedMultipleChoiceWrapper().apply {
setHasStableIds(true)
client = object : CachedAutoRefreshAdapter() {
override fun getItemId(position: Int) = if (position in 0 until itemCount) position.toLong() else -1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CacheViewHolder {
return layoutInflater
.inflate(android.R.layout.simple_list_item_multiple_choice, parent, false)
.let {
it.setBackgroundColor(Color.WHITE)
CacheViewHolder(it)
}
}
override fun onBindViewHolder(holder: CacheViewHolder, position: Int) {
holder.childView(android.R.id.text1)?.text = get(position)
}
override fun onBindViewHolder(holder: CacheViewHolder, position: Int, payloads: MutableList) {
holder.childView(android.R.id.text1)?.run {
when {
payloads.isEmpty() -> super.onBindViewHolder(holder, position, payloads)
}
}
}
}
}
addHeader(android.R.layout.simple_list_item_multiple_choice + 1,
CacheViewHolder(layoutInflater
.inflate(android.R.layout.simple_list_item_multiple_choice,
recycler_view,
false))
.apply {
itemView.setBackgroundColor(Color.WHITE)
childView(android.R.id.text1)?.text = "Header: 菲利普亲王入院"
})
addHeader(android.R.layout.simple_list_item_multiple_choice + 2,
CacheViewHolder(layoutInflater
.inflate(android.R.layout.simple_list_item_multiple_choice,
recycler_view,
false))
.apply {
itemView.setBackgroundColor(Color.WHITE)
childView(android.R.id.text1)?.text = "Header: 美国公布征税清单"
})
addFooter(android.R.layout.simple_list_item_multiple_choice + 3,
CacheViewHolder(layoutInflater
.inflate(android.R.layout.simple_list_item_multiple_choice,
recycler_view,
false))
.apply {
itemView.setBackgroundColor(Color.WHITE)
childView(android.R.id.text1)?.text = "Footer: 女孩感冒右腿截肢"
})
}
addStatusView(-2, CacheViewHolder(layoutInflater.inflate(R.layout.layout_data_empty, recycler_view, false)))
}
addOnItemClickListener { _, viewHolder ->
adapter.takeIsInstance()?.run {
adapter.getWrappedPosition(this, viewHolder.adapterPosition).run {
setItemChecked(this, !isItemChecked(this))
}
}
adapter.takeIsInstance()?.run {
when (viewHolder.itemViewType) {
-2 -> setCurrentStatusIf(-2, { takeIsInstance>()?.itemCount == 0 })
0 -> Unit
else -> setCurrentStatus(-2)
}
}
}
addOnItemLongClickListener { _, viewHolder ->
adapter.takeIsInstance()?.run {
when (viewHolder.itemViewType) {
-2 -> setCurrentStatusIf(-2, { takeIsInstance>()?.itemCount == 0 })
in android.R.layout.simple_list_item_multiple_choice + 1
..android.R.layout.simple_list_item_multiple_choice + 3 -> setCurrentStatus(-2)
0 -> itemTouchHelper.startDrag(viewHolder)
else -> Unit
}
}
}
}
GitHub
注意事项
由于不想提供更多的API用于处理viewType,使用的时候需要注意保持viewType的唯一性