如何优雅的实现仿京东、天猫的筛选列表

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_22090073/article/details/86623042

简介

现在越来越多的电商app都参照了京东和天猫风格的商品列表,商品列表页有一个侧滑筛选菜单,我们产品也不例外,在网上看大部分都是recyclerview嵌套gridview的方式实现的,这样会在一些低配的手机上运行非常卡顿,如果筛选项过多甚至会有一些不可预估的问题(如快速点击,造成数据索引错乱,直接奔溃),用户体验极差。

使用一个recyclerview实现京东筛选菜单

下面我们就来介绍如何使用一个recyclerview就可以 实现京东的筛选菜单。首先,我们先来看筛选菜单有哪些功能:

  1. 单选和多选 ,筛选项的每一个类目都有可能是单选或多选;
  2. 展开收起 ,每一个类目如果下面的选项大于6项,那么初始化就显示6项,其余的可点击以及标题进行展开和收起;
  3. 展示筛选项 ,用户点击某一筛选项时,在父级标题展示所选筛选项;
  4. 重置,点击重置按钮时,重置所有筛选项;
  5. 获取筛选项,点击确定按钮时,获取所选项,进行筛选。

screenshot

实现

说了这么多,我们先来看看是怎么实现的吧!

先来定义我们的筛选数据对象:

/**
  * 数据对象
  */
data class FilterDao(
		// 一级标题对象
        var filterParentDao: FilterParentDao? = null,
        // 筛选项集合
        var sub: List<Sub>? = 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<FilterDao>, val mContext: Context) :
        RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    private var mGroupItemStatus: MutableList<Boolean> = mutableListOf() // 保存一级标题的开关状态

    companion object {
    	// 子列表收起时,最少展示的数量, 默认是6
        private const val MIN_COUNT = 6
    }


    /**
     * 设置显示的数据
     *
     * @param dataListTrees
     */
    fun setData(dataListTrees: List<FilterDao>) {
        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中关键的几个方法:

  1. getItemCount() : 遍历我们传进来的list, itemCount++,这里是加上一个父标题,然后判断开关状态, 如果是展开状态,那么直接加上子级列表的数量,否则要判断子集列表的数量是否大于 “收起后剩余展示数量 ”, 再进行增加。
  2. getItemStatusByPosition(): 最重要的就是这个方法了, 首先遍历mGroupItemStatus, 其实就是遍历了父级对象集合, 如果itemCount刚好等于position时,代表此条目是一个父级, 那么就保存itemType和父级索引,结束掉循环;否则的话代表这个item是个子级, 然后计算此item的真实子索引,当遍历到最后一组的时候,将此item设置为子item,再计算真实子索引。
    在这里插入图片描述
    如上图所示: 当position=0时,组索引为0; 当pos=1是,组索引为0,子索引为0。。。以此类推。
    那么继续看while循环, 第一次pos=0;那么itemcount=pos,代表这是一个组索引;结束掉循环; 当走到第二个item时, pos为1, itemcount是0, 那么就计算itemcount的数量, 继续循环:
			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对象中了。

扫描二维码关注公众号,回复: 5055881 查看本文章

设置列表

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

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/mRlTitle"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/common_white">

    <LinearLayout
        android:id="@+id/rl_top"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="@dimen/space_6"
        android:layout_marginTop="@dimen/space_16"
        android:orientation="horizontal"
        android:paddingLeft="@dimen/space_10"
        android:paddingRight="@dimen/space_12">


        <TextView
            android:id="@+id/mTvFilter"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="0.2"
            android:text="筛选1"
            android:textColor="@color/text_color_light"
            android:textSize="@dimen/text_size_14" />


        <CheckBox
            android:id="@+id/mCbDesc"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginLeft="@dimen/space_6"
            android:layout_marginRight="@dimen/space_6"
            android:layout_weight="0.6"
            android:button="@null"
            android:clickable="false"
            android:drawableEnd="@drawable/cb_selector_more"
            android:drawablePadding="@dimen/space_6"
            android:drawableRight="@drawable/cb_selector_more"
            android:ellipsize="end"
            android:focusable="false"
            android:gravity="right"
            android:maxLines="1"
            android:paddingLeft="@dimen/space_6"
            android:paddingRight="@dimen/space_6"
            android:text="234234234234"
            android:textColor="@color/text_color_orange"
            android:textSize="@dimen/text_size_12" />

    </LinearLayout>

    <com.xfs.goods.weigets.grid.GridLayout
        android:id="@+id/mGridLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/rl_top"
        app:gridHorizontalSpace="@dimen/space_4"
        app:gridSpan="3"
        app:gridVerticalSpace="@dimen/space_4" />

</RelativeLayout>
goods_item_filter_sub.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:id="@+id/mRlSub"
    android:layout_height="wrap_content">

    <CheckBox
        android:id="@+id/mCbSub"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="false"
        android:layout_marginBottom="6dp"
        android:layout_marginLeft="6dp"
        android:layout_marginRight="6dp"
        android:background="@drawable/background_cb_filter"
        android:button="@null"
        android:ellipsize="end"
        android:gravity="center"
        android:maxEms="4"
        android:maxLines="1"
        android:paddingBottom="7dp"
        android:paddingTop="7dp"
        android:singleLine="true"
        android:textColor="@color/goods_text_color_cb_filter"
        android:textSize="@dimen/text_size_12" />

</RelativeLayout>

这样,一个仿京东、天猫的商品筛选列表就渲染出来了。

结语

我已经把adapter做了一层封装, 实现了数据和业务分离。现已经放到github上了,里面有一个完整的筛选列表的demo,欢迎大家star!

大家可以直接引入:compile ‘com.plumcookingwine.tree:TreeRvAdapter:0.0.1’;
具体使用方法附在github上。

github地址: https://github.com/plumcookingwine/TreeAdapter

猜你喜欢

转载自blog.csdn.net/qq_22090073/article/details/86623042