用装饰者模式为RecyclerView实现无入侵的HeaderAndFooter,EmptyView,MultipleChoice

前言

曾经有幸看到过鸿洋大神使用装饰者模式实现的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的唯一性

你可能感兴趣的:(用装饰者模式为RecyclerView实现无入侵的HeaderAndFooter,EmptyView,MultipleChoice)