现在越来越多的电商app都参照了京东和天猫风格的商品列表,商品列表页有一个侧滑筛选菜单,我们产品也不例外,在网上看大部分都是recyclerview嵌套gridview的方式实现的,这样会在一些低配的手机上运行非常卡顿,如果筛选项过多甚至会有一些不可预估的问题(如快速点击,造成数据索引错乱,直接奔溃),用户体验极差。
下面我们就来介绍如何使用一个recyclerview就可以 实现京东的筛选菜单。首先,我们先来看筛选菜单有哪些功能:
说了这么多,我们先来看看是怎么实现的吧!
先来定义我们的筛选数据对象:
/**
* 数据对象
*/
data class FilterDao(
// 一级标题对象
var filterParentDao: FilterParentDao? = null,
// 筛选项集合
var sub: List? = null
) {
data class Sub(
var id: Int? = null,
var name: String? = null,
var desc: String? = null,
var isShow: Boolean = true,
var isCheck: Boolean = false
)
}
data class FilterParentDao(
var id: Int? = null,
var name: String? = null,
var desc: String? = null,
var isShow: Boolean = false
)
接下来创建一个ItemStatus对象,用于管理和计算我们的item状态:
class ItemStatus {
companion object {
// 父标题 itemType
val VIEW_TYPE_GROUP_ITEM = 0
// 子标题 itemtYPE
val VIEW_TYPE_SUB_ITEM = 1
}
var mViewType: Int = -1
var mGroupItemIndex: Int = -1
var mSubItemIndex: Int = -1
}
既然是列表,那么最重要的肯定就是我们的adapter了:
/**
* Created Kevin by on 2018/10/30.
*/
class GoodsFilterAdapter(private val mDataListTrees: MutableList, val mContext: Context) :
RecyclerView.Adapter() {
private var mGroupItemStatus: MutableList = mutableListOf() // 保存一级标题的开关状态
companion object {
// 子列表收起时,最少展示的数量, 默认是6
private const val MIN_COUNT = 6
}
/**
* 设置显示的数据
*
* @param dataListTrees
*/
fun setData(dataListTrees: List) {
this.mDataListTrees.clear()
this.mDataListTrees.addAll(dataListTrees)
initGroupItemStatus()
notifyDataSetChanged()
}
/**
* 初始化一级列表开关状态
*/
private fun initGroupItemStatus() {
mGroupItemStatus = ArrayList()
for (i in mDataListTrees.indices) {
mGroupItemStatus.add(false)
}
}
/**
* 根据item的位置,获取当前Item的状态
*
* @param position 当前item的位置(此position的计数包含groupItem和subItem合计)
* @return 当前Item的状态(此Item可能是groupItem,也可能是SubItem)
*/
private fun getItemStatusByPosition(position: Int): ItemStatus {
val itemStatus = ItemStatus()
var itemCount = 0
var i = 0
//轮询 groupItem 的开关状态
while (i < mGroupItemStatus.size) {
if (itemCount == position) { //position刚好等于计数时,item为groupItem
itemStatus.mViewType = ItemStatus.VIEW_TYPE_GROUP_ITEM
itemStatus.mGroupItemIndex = i
break
} else if (itemCount > position) { //position大于计数时,item为groupItem(i - 1)中的某个subItem
itemStatus.mViewType = ItemStatus.VIEW_TYPE_SUB_ITEM
itemStatus.mGroupItemIndex = i - 1 // 指定的position组索引
val subSize = mDataListTrees[i - 1].sub!!.size
// 计算指定的position前,统计的列表项和
val temp = itemCount - subSize
LogUtils.i("\ntemp === " + temp + ";itemcount === " + itemCount + ";subsize === " + subSize + ";pos === " + position)
// 指定的position的子项索引:即为position-之前统计的列表项和
if (!mGroupItemStatus[i - 1]) {
val ind = if (subSize > MIN_COUNT) {
position - temp - subSize + MIN_COUNT
} else {
position - temp
}
itemStatus.mSubItemIndex = ind
} else {
itemStatus.mSubItemIndex = position - temp
}
break
}
val subSize = mDataListTrees[i].sub!!.size
val realCount = if (subSize > MIN_COUNT) MIN_COUNT else subSize
itemCount += realCount + 1
if (mGroupItemStatus[i]) {
itemCount += if (subSize > MIN_COUNT) subSize - MIN_COUNT else 0
}
i++
}
// 轮询到最后一组时,未找到对应位置
if (i >= mGroupItemStatus.size) {
itemStatus.mViewType = ItemStatus.VIEW_TYPE_SUB_ITEM // 设置为二级标签类型
itemStatus.mGroupItemIndex = i - 1 // 设置一级标签为最后一组
val subSize = mDataListTrees[i - 1].sub!!.size
val temp = itemCount - subSize
LogUtils.i("\ntemp2 === " + temp + ";itemcount === " + itemCount + ";subsize === " + subSize + ";pos === " + position)
// 指定的position的子项索引:即为position-之前统计的列表项和
if (!mGroupItemStatus[i - 1]) {
val ind = if (subSize > MIN_COUNT) {
position - temp - subSize + MIN_COUNT
} else {
position - temp
}
itemStatus.mSubItemIndex = ind
} else {
itemStatus.mSubItemIndex = position - temp
}
}
return itemStatus
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view: View
val viewHolder: RecyclerView.ViewHolder
if (viewType == ItemStatus.VIEW_TYPE_GROUP_ITEM) {
view = LayoutInflater.from(mContext).inflate(R.layout.goods_item_filter, parent, false)
viewHolder = GroupItemViewHolder(view)
} else {
view = LayoutInflater.from(mContext).inflate(R.layout.goods_item_filter_sub, parent, false)
viewHolder = SubItemViewHolder(view)
}
return viewHolder
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val itemStatus = getItemStatusByPosition(position) // 获取列表项状态
val data = mDataListTrees[itemStatus.mGroupItemIndex]
if (itemStatus.mViewType == ItemStatus.VIEW_TYPE_GROUP_ITEM) { // 组类型
val groupItemViewHolder = holder as GroupItemViewHolder
groupItemViewHolder.mTvFilter.text = data.filterParentDao?.name
val groupIndex = itemStatus.mGroupItemIndex // 组索引
//点击父标题时进行展开和收起
groupItemViewHolder.itemView.setOnClickListener {
// initGroupItemStatus() // 如果想要实现只展开一个组的功能
mGroupItemStatus[groupIndex] = !mGroupItemStatus[groupIndex]
notifyDataSetChanged()
groupItemViewHolder.mCbDesc.isChecked = mGroupItemStatus[groupIndex]
}
} else if (itemStatus.mViewType == ItemStatus.VIEW_TYPE_SUB_ITEM) { // 子项类型
val subItemViewHolder = holder as SubItemViewHolder
subItemViewHolder.mCbSub.text = data.sub!![itemStatus.mSubItemIndex].desc
subItemViewHolder.mCbSub.setOnClickListener {
ToastUtils.showToast(itemStatus.mSubItemIndex.toString()
+ "\n"
+ mDataListTrees[itemStatus.mGroupItemIndex].sub!!.get(itemStatus.mSubItemIndex).desc.toString()
+ "\n"
+ position
)
}
}
}
/**
* 计算列表总item
*/
override fun getItemCount(): Int {
var itemCount = 0
if (0 == mGroupItemStatus.size) {
return itemCount
}
for (i in mDataListTrees.indices) {
itemCount++ // 每个一级标题项+1
val subSize = mDataListTrees[i].sub!!.size
if (mGroupItemStatus[i]) { // 二级标题展开时,再加上二级标题的数量
itemCount += subSize
} else { // 收起时,先判断二级标题数量是否大于最小展示数量
itemCount += if (subSize >= MIN_COUNT) MIN_COUNT else subSize
}
}
return itemCount
}
override fun getItemViewType(position: Int): Int {
return getItemStatusByPosition(position).mViewType
}
/**
* 组项ViewHolder
*/
internal class GroupItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var mTvFilter: TextView = itemView.findViewById(R.id.mTvFilter) as TextView
var mCbDesc: CheckBox = itemView.findViewById(R.id.mCbDesc) as CheckBox
}
/**
* 子项ViewHolder
*/
internal class SubItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val mCbSub: CheckBox = itemView.findViewById(R.id.mCbSub)
}
}
这样这个列表就完成了我们来看看这个adapter中关键的几个方法:
val subSize = mDataListTrees[i].sub!!.size
val realCount = if (subSize > MIN_COUNT) MIN_COUNT else subSize
itemCount += realCount + 1
if (mGroupItemStatus[i]) {
itemCount += if (subSize > MIN_COUNT) subSize - MIN_COUNT else 0
}
i++
根据图示: 这时 i=1;itemcount = 8,会走到else if (itemCount > position)判断;将其保存为一个子item,它的组索引是0,也就是i-1;
itemStatus.mViewType = ItemStatus.VIEW_TYPE_SUB_ITEM
itemStatus.mGroupItemIndex = i - 1 // 指定的position组索引
val subSize = mDataListTrees[i - 1].sub!!.size
// **计算指定的position前,统计的列表项和(这时,temp = 8-7 = 1)**
val temp = itemCount - subSize
LogUtils.i("\ntemp === " + temp + ";itemcount === " + itemCount + ";subsize === " + subSize + ";pos === " + position)
// 如果是收起状态
if (!mGroupItemStatus[i - 1]) {
val ind = if (subSize > MIN_COUNT) {
position - temp - subSize + MIN_COUNT
} else {
position - temp
}
itemStatus.mSubItemIndex = ind
} else {
// position - temp = (1- 1) = 0
itemStatus.mSubItemIndex = position - temp
}
这样就计算出子item的真实索引,保存到itemstatus对象中了。
adapter讲完了,那么如何使用呢?它跟我们平时使用是一样的:
mAdapter = GoodsFilterAdapter(mFilters, activity!!)
val manager = GridLayoutManager(activity, mSpanCount)
// 判断item的类型,如果是子类型,它占据1个item的宽度;如果是父类型;则占据3个item的宽度
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
if (mAdapter.getItemViewType(position) == ItemStatus.VIEW_TYPE_SUB_ITEM) {
return 1
}
return mSpanCount
}
}
mRvFilter.layoutManager = manager
mRvFilter.adapter = mAdapter
侧滑的话可以讲recyclerview放到drawerlayout或slidingmenu里面
这样就大功告成啦!
最后再把adapter的布局文件贴出来:
goods_item_filter.xml
goods_item_filter_sub.xml
这样,一个仿京东、天猫的商品筛选列表就渲染出来了。
我已经把adapter做了一层封装, 实现了数据和业务分离。现已经放到github上了,里面有一个完整的筛选列表的demo,欢迎大家star!
大家可以直接引入:compile ‘com.plumcookingwine.tree:TreeRvAdapter:0.0.1’;
具体使用方法附在github上。大家可以根据需要自行扩展
github地址:
https://github.com/plumcookingwine/TreeAdapter
DEMO地址:
https://download.csdn.net/download/qq_22090073/10942079