美文网首页
用装饰者模式为RecyclerView实现无入侵的HeaderA

用装饰者模式为RecyclerView实现无入侵的HeaderA

作者: 猫爸iYao | 来源:发表于2018-11-16 20:12 被阅读0次

    前言

    曾经有幸看到过鸿洋大神使用装饰者模式实现的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<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>(), AdapterWrapper {
    
        companion object {
            val CHANGE_NONE_CONTENT = Any()
        }
    
        lateinit var client: RecyclerView.Adapter<VH>
        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<Any>) {
            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<VH> = 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<VH : RecyclerView.ViewHolder> : AbsAdapterWrapper<VH>() {
    
    
        private val headers : SparseArray<VH> = SparseArray()
        private val footers : SparseArray<VH> = 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<VH : RecyclerView.ViewHolder> : AbsAdapterWrapper<VH>() {
    
        private val checkedIds: LongSparseArray<Int> = 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<Any>) {
            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<VH : RecyclerView.ViewHolder> : AbsAdapterWrapper<VH>() {
    
        companion object {
            const val STATUS_NORMAL = -1
        }
        private val statusViewHolders = SparseArray<VH>()
        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<Any>) {
            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<String>() {
    
                                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<TextView>(android.R.id.text1)?.text = get(position)
                                }
    
                                override fun onBindViewHolder(holder: CacheViewHolder, position: Int, payloads: MutableList<Any>) {
                                    holder.childView<TextView>(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<TextView>(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<TextView>(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<TextView>(android.R.id.text1)?.text = "Footer: 女孩感冒右腿截肢"
                                        })
                    }
                    addStatusView(-2, CacheViewHolder(layoutInflater.inflate(R.layout.layout_data_empty, recycler_view, false)))
                }
                addOnItemClickListener { _, viewHolder ->
                    adapter.takeIsInstance<CachedMultipleChoiceWrapper>()?.run {
                        adapter.getWrappedPosition(this, viewHolder.adapterPosition).run {
                            setItemChecked(this, !isItemChecked(this))
                        }
                    }
                    adapter.takeIsInstance<CachedStatusWrapper>()?.run {
                        when (viewHolder.itemViewType) {
                            -2 -> setCurrentStatusIf(-2, { takeIsInstance<CachedAutoRefreshAdapter<String>>()?.itemCount == 0 })
                            0 -> Unit
                            else -> setCurrentStatus(-2)
                        }
                    }
                }
    
                addOnItemLongClickListener { _, viewHolder ->
                    adapter.takeIsInstance<CachedStatusWrapper>()?.run {
                        when (viewHolder.itemViewType) {
                            -2 -> setCurrentStatusIf(-2, { takeIsInstance<CachedAutoRefreshAdapter<String>>()?.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实现无入侵的HeaderA

          本文链接:https://www.haomeiwen.com/subject/cbdkoftx.html