自定义View之HiSliderView商品分类展示

在这里插入图片描述

1.案例演示

在这里插入图片描述

2.问题分析

1.采用网格布局。

2.不同的组别它的条目数量不一定能填满网格布局,这就需要最后剩余的 item 把剩下的控件占满。

解决办法:

​ 1、通过假数据填充,使得多余的部分用空白数据填充,这样就能满足每一行都能占满,正常摆放。(不采用该方案)

​ 2、通过调整每个Group 的最后一个 item 所占用的 SpanSize 来让其占满当前行,使得下一组数据能另起一行。
在这里插入图片描述

3.关键代码

需要根据具体数据来设置不同的位置的 item 所占用的 spanSize。需要设置 GridLayoutManager#setSpanSizeLookup() 方法,复写 SpanSizeLookup 的 getSpanSize() 方法

	//子条目列表数据集合
    private val subcategoryList = mutableListOf<Subcategory>()
    //网格布局
    private val layoutManager = GridLayoutManager(this, SPAN_COUNT)
    //存储当前分组条目的偏移量之和,累加试的
    private val groupSpanSizeOffset = SparseIntArray()

	private val spanSizeLookUp = object : GridLayoutManager.SpanSizeLookup() {

        override fun getSpanSize(position: Int): Int {
            var spanSize = 1
            val groupName: String = subcategoryList[position].groupName
            val nextGroupName: String? =
                if (position + 1 < subcategoryList.size) subcategoryList[position + 1].groupName else null
            //当前位置 item 与 下一个 item 统一个分组,则当前 item 的 spanSize = 1
            if (TextUtils.equals(groupName, nextGroupName)) {
                spanSize = 1
            } else {
                //当前位置和 下一个位置 不再同一个分组,此时需要计算当前 item 需要将剩余的 spanCount 占完,比如当前 spanCount = 3,如果 item 位于第一个位置,则需要占用 3 列。
                //1.要拿到当前组 position (所在组)在 groupSpanSizeOffset 的索引下标
                //2.拿到 当前组前面一组 存储的 spanSizeOffset 偏移量
                //3.给当前组最后一个item 分配 spanSize count
                val indexOfKey = groupSpanSizeOffset.indexOfKey(position)
                val size = groupSpanSizeOffset.size()
                //上一个分组的偏移量
                val lastGroupOffset =
                if (size <= 0) 0
                else if (indexOfKey >= 0) {
                    //说明当前组的偏移量记录,已经存在了 groupSpanSizeOffset ,这个情况发生在上下滑动,
                    if (indexOfKey == 0) 0 else groupSpanSizeOffset.valueAt(indexOfKey - 1)
                } else {
                    //说明当前组的偏移量记录还没有存在于 groupSpanSizeOffset ,这个情况发生在第一次布局的时候,得到前面所有组的偏移量之和。
                    groupSpanSizeOffset.valueAt(size - 1)
                }
                //          3       -     (6     +    5               % 3  )第几列=0  ,1 ,2
                //当前 item 需要把当前分组的的最后一行占满,比如网格布局的一行为 3 个。此时这个 item 位于第一列,因为这一行只有一个 item了,所以需要占用三列。那么此时的偏移量为 2,总的偏移量为之前的偏移量之和。
                spanSize = SPAN_COUNT - (position + lastGroupOffset) % SPAN_COUNT
                if (indexOfKey < 0) {
                    //得到当前组 和前面所有组的spanSize 偏移量之和
                    val groupOffset = lastGroupOffset + spanSize - 1
                    groupSpanSizeOffset.put(position, groupOffset)
                }
            }
            return spanSize
        }
    }

4.封装HiSilderView

在这里插入图片描述

1.左侧为 MenuView,由一个 RecyclerView 来承载。

2.右侧为 ContentView,也由一个 RecyclerView 来承载。

3.默认 MenuView 的基本样式为一个指示器和文本。

4.右侧 ContentView 的基本样式就采用 图片加文本的样式。

1.定义 HiSliderView 左侧 MenuView 样式及解析

    <!--    HiSliderView 样式-->
    <declare-styleable name="HiSliderView">
        //左侧 menuItem 基本样式
        <attr name="menuItemWidth" format="dimension" />
        <attr name="menuItemHeight" format="dimension" />
        <attr name="menuItemTextSize" format="dimension" />
        <attr name="menuItemSelectTextSize" format="dimension" />
        <attr name="menuItemIndicator" format="reference" />
        <attr name="menuItemTextColor" format="reference" />
        <attr name="menuItemBackGroundColor" format="color" />
        <attr name="menuItemSelectBackGroundColor" format="color" />

    </declare-styleable>

解析HiSliderView样式,通常将解析到的属性封装成一个数据类,方便管理

/**
 *     author : shengping.tian
 *     time   : 2021/09/10
 *     desc   : Slider 样式资源解析器
 *     version: 1.0
 */
internal object SliderAttrsParse {

    //提供一些默认属性
    private val MENU_WIDTH = HiDisplayUtil.dp2px(100f)
    private val MENU_HEIGHT = HiDisplayUtil.dp2px(45f)
    private val MENU_TEXT_SIZE = HiDisplayUtil.sp2px(14f)

    private val TEXT_COLOR_NORMAL = HiRes.getColor(R.color.color_666)//Color.parseColor("#666666")
    private val TEXT_COLOR_SELECT = HiRes.getColor(R.color.color_127)//Color.parseColor("#DD3127")

    private val BG_COLOR_NORMAL = HiRes.getColor(R.color.color_8f9)//Color.parseColor("#F7F8F9")
    private val BG_COLOR_SELECT = HiRes.getColor(R.color.color_white)//Color.parseColor("#ffffff")


    fun parseMenuItemAttr(context: Context, attrs: AttributeSet?): MenuItemAttr {
        val typeArray = context.obtainStyledAttributes(attrs, R.styleable.HiSliderView)
        //左侧菜单 item 的宽度
        val menuItemWidth = typeArray.getDimensionPixelOffset(
            R.styleable.HiSliderView_menuItemWidth,
            MENU_WIDTH
        )
        //左侧菜单 item 的高度
        val menuItemHeight =
            typeArray.getDimensionPixelOffset(R.styleable.HiSliderView_menuItemHeight, MENU_HEIGHT)

        //左侧菜单 item 的文字大小
        val menuItemTextSize = typeArray.getDimensionPixelOffset(
            R.styleable.HiSliderView_menuItemTextSize,
            MENU_TEXT_SIZE
        )

        //左侧菜单 item 被选中时的文字大小
        val menuItemSelectTextSize = typeArray.getDimensionPixelOffset(
            R.styleable.HiSliderView_menuItemSelectTextSize,
            MENU_TEXT_SIZE
        )
        //左侧菜单 item 的文字的颜色
        val menuItemTextColor =
            typeArray.getColorStateList(R.styleable.HiSliderView_menuItemTextColor)
                ?: generateColorStateList()

        //左侧菜单 item 的指示器
        val menuItemIndicator = typeArray.getDrawable(R.styleable.HiSliderView_menuItemIndicator)
            ?: ContextCompat.getDrawable(context, R.drawable.shape_hi_slider_indicator)


        val menuItemBackgroundColor =
            typeArray.getColor(R.styleable.HiSliderView_menuItemBackGroundColor, BG_COLOR_NORMAL)

        val menuItemBackgroundSelectColor = typeArray.getColor(
            R.styleable.HiSliderView_menuItemSelectBackGroundColor,
            BG_COLOR_SELECT
        )
        typeArray.recycle()
        //将所有的参数封装成数据类,方便传递
        return MenuItemAttr(
            menuItemWidth,
            menuItemHeight,
            menuItemTextColor,
            menuItemBackgroundSelectColor,
            menuItemBackgroundColor,
            menuItemTextSize,
            menuItemSelectTextSize,
            menuItemIndicator
        )
    }

    data class MenuItemAttr(
        val width: Int,
        val height: Int,
        val textColor: ColorStateList,
        val selectBackgroundColor: Int,
        val normalBackgroundColor: Int,
        val textSize: Int,
        val selectTextSize: Int,
        val indicator: Drawable?
    )

    /**
     * 构建Selector选择器,相当于在 drawable 中设置的背景选择器
     */
    private fun generateColorStateList(): ColorStateList {
        val states = Array(2) { IntArray(2) }
        val colors = IntArray(2)
        //被选中时候的颜色
        colors[0] = TEXT_COLOR_SELECT
        //未被选中时的颜色
        colors[1] = TEXT_COLOR_NORMAL
        //被选择的状态
        states[0] = IntArray(1) { android.R.attr.state_selected }
        //其他
        states[1] = IntArray(1)
        return ColorStateList(states, colors)
    }
}

2.构建 HiSliderView 控件

1.继承自 LinearLayout,水平方向布局,左侧和右侧为 RecyclerView

2.提供方法 bindMenuView 和 bindContentView 供具具体场景去绑定数据和点击事件。

3.提供默认的 RecyclerView 的样式

/**
 *     author : shengping.tian
 *     time   : 2021/09/10
 *     desc   : HiSlideView 自定义view,左边为一个垂直的 RecyclerView显示menu菜单
 *              右边一个 RecyclerView 显示菜单对应的内容
 *     version: 1.0
 */
class HiSliderView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

    //左侧菜单的item 布局样式
    private val MENU_ITEM_LAYOUT_RES_ID = R.layout.hi_slider_menu_item

    //右侧内容区的 item 样式
    private val COTENT_ITEM_LAYOUT_RES_ID = R.layout.hi_slider_content_item

    val menuView = RecyclerView(context)

    val contentView = RecyclerView(context)

    private var menuItemAttr: SliderAttrsParse.MenuItemAttr =
        SliderAttrsParse.parseMenuItemAttr(context, attrs)


    init {
        orientation = HORIZONTAL

        menuView.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)
        //去掉 menuView 滑动到顶部和底部的动画
        menuView.overScrollMode = View.OVER_SCROLL_NEVER
        menuView.itemAnimator = null

        contentView.layoutParams =
            LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
        contentView.overScrollMode = View.OVER_SCROLL_NEVER
        contentView.itemAnimator = null
        //水平布局,将两个 recyclerView 水平摆放
        addView(menuView)
        addView(contentView)
    }

    /**
     * 为 menuView 绑定回调和参数
     *  @layoutRes menuItem 资源样式
     *  @itemCount 数量
     *  @onBindView 绑定数据的时候回调出去,有具体业务去实现具体绑定逻辑
     *  @onItemClick 将点击事件回调出去
     */
    fun bindMenuView(
        layoutRes: Int = MENU_ITEM_LAYOUT_RES_ID,
        itemCount: Int,
        onBindView: (HiViewHolder, Int) -> Unit,
        onItemClick: (HiViewHolder, Int) -> Unit
    ) {
        menuView.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
        menuView.adapter = MenuAdapter(layoutRes, itemCount, onBindView, onItemClick)
    }

    /**
     * 绑定内容取数的数据及点击事件
     * @param layoutRes 右侧内容区域资源样式
     * @param itemCount  数据条目
     * @param itemDecoration  自定义的 ItemDecoration
     * @param layoutManager  布局布局方式
     * @param onBindView 绑定数据的回调
     * @param onItemClick 点击事件回调
     * 每次点击左侧菜单按钮,右侧内容区都会变化
     */
    fun bindContentView(
        layoutRes: Int = COTENT_ITEM_LAYOUT_RES_ID,
        itemCount: Int,
        itemDecoration: RecyclerView.ItemDecoration?,
        layoutManager: RecyclerView.LayoutManager,
        onBindView: (HiViewHolder, Int) -> Unit,
        onItemClick: (HiViewHolder, Int) -> Unit
    ) {
        if (contentView.layoutManager == null) {
            contentView.layoutManager = layoutManager
            contentView.adapter = ContentAdapter(layoutRes)
            itemDecoration?.let {
                contentView.addItemDecoration(it)
            }
        }
        val contentAdapter = contentView.adapter as ContentAdapter
        contentAdapter.update(itemCount, onBindView, onItemClick)
        contentAdapter.notifyDataSetChanged()
        contentView.scrollToPosition(0)
    }

    /**
     * content 区域的数据是会实时改变的,每个条目的布局及数目可能不一样,所以需要动态绑定数据
     * @property layoutRes Int
     * @constructor
     */
    inner class ContentAdapter(private val layoutRes: Int) : RecyclerView.Adapter<HiViewHolder>() {
        private lateinit var onItemClick: (HiViewHolder, Int) -> Unit
        private lateinit var onBindView: (HiViewHolder, Int) -> Unit
        private var count: Int = 0


        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HiViewHolder {
            val itemView = LayoutInflater.from(context).inflate(layoutRes, parent, false)
            return HiViewHolder(itemView)
        }

        override fun onBindViewHolder(holder: HiViewHolder, position: Int) {
            //让具体的业务场景去实现
            onBindView(holder, position)
            holder.itemView.setOnClickListener {
                onItemClick(holder, position)
            }
        }

        override fun getItemCount(): Int {
            return count
        }

        fun update(
            itemCount: Int,
            onBindView: (HiViewHolder, Int) -> Unit,
            onItemClick: (HiViewHolder, Int) -> Unit
        ) {
            this.onItemClick = onItemClick
            this.count = itemCount
            this.onBindView = onBindView
        }

        /**
         * 当视图与窗口绑定时候,如果是网格布局等,不同的条目可能占用的 spanSize 不一样
         * @param holder HiViewHolder
         */
        override fun onViewAttachedToWindow(holder: HiViewHolder) {
            super.onViewAttachedToWindow(holder)
            //右侧内容区域剩余的宽度
            val remainSpace = width - paddingLeft - paddingRight - menuItemAttr.width
            val layoutManager = contentView.layoutManager
            var spanCount = 0
            if (layoutManager is GridLayoutManager) {
                spanCount = layoutManager.spanCount
            } else if (layoutManager is StaggeredGridLayoutManager) {
                spanCount = layoutManager.spanCount
            }
            if (spanCount > 0) {
                //创建content itemView  ,设置它的layoutParams 的原因,是防止图片未加载出来之前,列表滑动时 上下闪动的效果,提前根据 spanCount 设置每个条目的宽度设置大小
                val itemWith = remainSpace / spanCount
                val layoutParams = holder.itemView.layoutParams
                layoutParams.width = itemWith
                layoutParams.height = itemWith
                holder.itemView.layoutParams = layoutParams
            }
        }
    }


    /**
     * 左侧 RecyclerView 的 适配器
     */
    inner class MenuAdapter(
        val layoutRes: Int,
        val count: Int,
        val onBindView: (HiViewHolder, Int) -> Unit,
        val onItemClick: (HiViewHolder, Int) -> Unit
    ) : RecyclerView.Adapter<HiViewHolder>() {

        //本次选中的 item 位置
        private var currentSelectIndex = 0

        //上一次选中的item的位置
        private var lastSelectIndex = 0

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HiViewHolder {
            val itemView = LayoutInflater.from(context).inflate(layoutRes, parent, false)
            val params = RecyclerView.LayoutParams(menuItemAttr.width, menuItemAttr.height)
            itemView.layoutParams = params
            itemView.setBackgroundColor(menuItemAttr.normalBackgroundColor)
            return HiViewHolder(itemView)
        }

        override fun onBindViewHolder(holder: HiViewHolder, position: Int) {
            holder.findViewById<TextView>(R.id.menu_item_title)
                ?.setTextColor(menuItemAttr.textColor)
            holder.findViewById<ImageView>(R.id.menu_item_indicator)
                ?.setImageDrawable(menuItemAttr.indicator)

            //初次绑定数据的时候没有点击事件,需要有一个默认的选择
            holder.itemView.setOnClickListener {
                currentSelectIndex = position
                notifyItemChanged(position)
                notifyItemChanged(lastSelectIndex)
                //如果直接在这里更改样式,会有一种延迟效果,导致左侧 item 短暂时间内有两个item的被选中
            }

            //初次绑定的时候有个默认被选中的条目,回调给使用者自己处理。当点击item条目时候,
            if (currentSelectIndex == position) {
                onItemClick(holder, position)
                lastSelectIndex = currentSelectIndex
            }
            applyItemAttr(position, holder)
            onBindView(holder, position)
        }

        override fun getItemCount(): Int {
            return count
        }

        /**
         * 选中后更新 menuItem 的样式
         */
        private fun applyItemAttr(position: Int, holder: HiViewHolder) {
            val selected = position == currentSelectIndex
            val titleView: TextView? = holder.findViewById(R.id.menu_item_title)
            val indicatorView: ImageView? = holder.findViewById(R.id.menu_item_indicator)
            indicatorView?.visibility = if (selected) View.VISIBLE else View.GONE
            titleView?.setTextSize(
                TypedValue.COMPLEX_UNIT_PX,
                if (selected) menuItemAttr.selectTextSize.toFloat()
                else menuItemAttr.textSize.toFloat()
            )
            holder.itemView.setBackgroundColor(if (selected) menuItemAttr.selectBackgroundColor else menuItemAttr.normalBackgroundColor)
            titleView?.isSelected = selected
        }
    }
}

其中:hi_slider_menu_item 和 hi_slider_content_item 如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="@dimen/dp_45">

    <ImageView
        android:id="@+id/menu_item_indicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        tools:src="@drawable/shape_hi_slider_indicator" />

    <TextView
        android:id="@+id/menu_item_title"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:gravity="center"
        android:textColor="@color/color_white"
        android:textSize="@dimen/sp_14"
        tools:text="热门分类" />
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="7dp">


    <ImageView
        android:id="@+id/content_item_image"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:scaleType="fitCenter"
        tools:background="#ff00000" />


    <TextView
        android:id="@+id/content_item_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="6dp"
        android:gravity="center"
        android:textColor="@color/color_000"
        android:textSize="@dimen/sp_12"
        tools:text="西装" />
</LinearLayout>

5.具体使用

1.为 ContenView 的 RecyclerView 设置 ItemDecoration

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WKjPHPJX-1632384516015)(../../pic/image-20210923155158774.png)]

/**
 *     author : shengping.tian
 *     time   : 2021/09/10
 *     desc   : contentView 的 ItemDecoration
 *     version: 1.0
 */
class CategoryItemDecoration(
    val callback: (Int) -> String,
    private val spanCount: Int
) : RecyclerView.ItemDecoration() {
    private val groupFirstPositions = mutableMapOf<String, Int>()
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

    init {
        paint.style = Paint.Style.FILL
        paint.color = Color.BLACK
        paint.isFakeBoldText = true
        paint.textSize = HiDisplayUtil.dp2px(15f).toFloat()
    }

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {

        //1. 根据 view对象,找到他在列表中处于的位置 adapterPosition
        val adapterPosition = parent.getChildAdapterPosition(view)
        if (adapterPosition >= parent.adapter!!.itemCount || adapterPosition < 0) return

        //2.拿到当前位置 adapterPosition 对应的 groupName
        val groupName = callback(adapterPosition)
        //3.拿到前面一个位置的 groupName
        val preGroupName = if (adapterPosition > 0) callback(adapterPosition - 1) else null

        val sameGroup = TextUtils.equals(groupName, preGroupName)

        if (!sameGroup && !groupFirstPositions.containsKey(groupName)) {
            //就说明当前位置 adapterPosition 对应的 item 是当前组的第一个位置。
            //此时存储起来,记录下来,目的是为了方便后面计算,计算后面 item 是否是第一行
            groupFirstPositions[groupName] = adapterPosition
        }

        val firstRowPosition = groupFirstPositions[groupName] ?: 0
        val samRow = adapterPosition - firstRowPosition in 0 until spanCount  //3

        //不是同一组,或者是同一行,就需要给 ItemDecoration 设置一个高度
        if (!sameGroup || samRow) {
            outRect.set(0, HiDisplayUtil.dp2px(40f), 0, 0)
            return
        }
        outRect.set(0, 0, 0, 0)
    }

    /**
     * 在提供给 RecyclerView 的 Canvas 中绘制任何适当的装饰。
     * 使用此方法绘制的任何内容都将在绘制项目视图之后绘制,因此会出现在视图之上。
     * @param c Canvas
     * @param parent RecyclerView
     * @param state State
     */
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        val childCount = parent.childCount
        for (index in 0 until childCount) {
            val view = parent.getChildAt(index)
            val adapterPosition = parent.getChildAdapterPosition(view)
            if (adapterPosition >= parent.adapter!!.itemCount || adapterPosition < 0) continue
            val groupName = callback(adapterPosition)
            //判断当前位置 是不是分组的第一个位置
            //如果是,在他的位置上绘制标题
            val groupFirstPosition = groupFirstPositions[groupName]
            if (groupFirstPosition == adapterPosition) {
                val decorationBounds = Rect()
                //为了拿到当前item 的 左上右下的坐标信息 包含了 margin 和 padding 空间的
                parent.getDecoratedBoundsWithMargins(view, decorationBounds)
                val textBounds = Rect()
                paint.getTextBounds(groupName, 0, groupName.length, textBounds)
                //将 GroupName 文字画上去
                c.drawText(
                    groupName,
                    HiDisplayUtil.dp2px(16f).toFloat(),
                    (decorationBounds.top + 2 * textBounds.height()).toFloat(),
                    paint
                )
            }
        }
    }

    fun clear() {
        groupFirstPositions.clear()
    }
}

2.Activity 中使用

1.在 xml 中引入自定义控件 HiSliderView

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.tsp.test.slider.SliderTestActivity">


    <com.tsp.android.hiui.slider.HiSliderView
        android:id="@+id/slider_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</RelativeLayout>

2.数据绑定

class SliderTestActivity : AppCompatActivity() {

    private val SPAN_COUNT = 3

    lateinit var viewModel: SliderViewModel

    private lateinit var sliderView: HiSliderView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_slider_test)
        sliderView = findViewById(R.id.slider_view)
        viewModel = ViewModelProvider(this)[SliderViewModel::class.java]
        observer()
    }

    private fun observer() {
        viewModel.queryCategoryList().observe(this) {
            if (it == null) return@observe

            sliderView.bindMenuView(
                itemCount = it.size,
                onBindView = { holder, position ->
                    val category = it[position]
                    holder.findViewById<TextView>(R.id.menu_item_title)?.text =
                        category.categoryName
                },
                onItemClick = { holder, position ->
                    val category = it[position]
                    val categoryId = category.categoryId
                    querySubcategoryList(categoryId)
//                    Toast.makeText(this," touch $category",Toast.LENGTH_SHORT).show()
                }
            )
        }
    }

    private fun querySubcategoryList(categoryId: String) {
        viewModel.querySubcategoryList(categoryId).observe(this) {
            if (it == null) return@observe
            decoration.clear()
            groupSpanSizeOffset.clear()
            subcategoryList.clear()
            subcategoryList.addAll(it)
            if (layoutManager.spanSizeLookup != spanSizeLookUp) {
                layoutManager.spanSizeLookup = spanSizeLookUp
            }
            sliderView.bindContentView(
                itemCount = it.size,
                itemDecoration = decoration,
                layoutManager = layoutManager,
                onBindView = { holder, position ->
                    val subcategory = it[position]
                    holder.findViewById<ImageView>(R.id.content_item_image)
                        ?.loadUrl(subcategory.subcategoryIcon)
                    holder.findViewById<TextView>(R.id.content_item_title)?.text =
                        subcategory.subcategoryName
                },
                onItemClick = { holder, position ->
                    //是应该跳转到类目的商品列表页的
                    val subcategory = it[position]
                    Toast.makeText(
                        this,
                        " touch ${subcategory.subcategoryName}",
                        Toast.LENGTH_SHORT
                    ).show()
                }
            )

        }
    }


    private val decoration = CategoryItemDecoration({ position ->
        subcategoryList[position].groupName
    }, SPAN_COUNT)

    //子条目列表数据集合
    private val subcategoryList = mutableListOf<Subcategory>()

    //网格布局
    private val layoutManager = GridLayoutManager(this, SPAN_COUNT)

    //存储当前分组条目的偏移量之和,累加试的
    private val groupSpanSizeOffset = SparseIntArray()

    private val spanSizeLookUp = object : GridLayoutManager.SpanSizeLookup() {

        override fun getSpanSize(position: Int): Int {
            var spanSize = 1
            val groupName: String = subcategoryList[position].groupName
            val nextGroupName: String? =
                if (position + 1 < subcategoryList.size) subcategoryList[position + 1].groupName else null
            //当前位置 item 与 下一个 item 统一个分组,则当前 item 的 spanSize = 1
            if (TextUtils.equals(groupName, nextGroupName)) {
                spanSize = 1
            } else {
                //当前位置和 下一个位置 不再同一个分组,此时需要计算当前 item 需要将剩余的 spanCount 占完,比如当前 spanCount = 3,如果 item 位于第一个位置,则需要占用 3 列。
                //1.要拿到当前组 position (所在组)在 groupSpanSizeOffset 的索引下标
                //2.拿到 当前组前面一组 存储的 spanSizeOffset 偏移量
                //3.给当前组最后一个item 分配 spanSize count
                val indexOfKey = groupSpanSizeOffset.indexOfKey(position)
                val size = groupSpanSizeOffset.size()
                //上一个分组的偏移量
                val lastGroupOffset =
                if (size <= 0) 0
                else if (indexOfKey >= 0) {
                    //说明当前组的偏移量记录,已经存在了 groupSpanSizeOffset ,这个情况发生在上下滑动,
                    if (indexOfKey == 0) 0 else groupSpanSizeOffset.valueAt(indexOfKey - 1)
                } else {
                    //说明当前组的偏移量记录还没有存在于 groupSpanSizeOffset ,这个情况发生在第一次布局的时候,得到前面所有组的偏移量之和。
                    groupSpanSizeOffset.valueAt(size - 1)
                }
                //          3       -     (6     +    5               % 3  )第几列=0  ,1 ,2
                //当前 item 需要把当前分组的的最后一行占满,比如网格布局的一行为 3 个。此时这个 item 位于第一列,因为这一行只有一个 item了,所以需要占用三列。那么此时的偏移量为 2,总的偏移量为之前的偏移量之和。
                spanSize = SPAN_COUNT - (position + lastGroupOffset) % SPAN_COUNT
                if (indexOfKey < 0) {
                    //得到当前组 和前面所有组的spanSize 偏移量之和
                    val groupOffset = lastGroupOffset + spanSize - 1
                    groupSpanSizeOffset.put(position, groupOffset)
                }
            }
            return spanSize
        }
    }
}

6.GitHub仓库完整代码

完整代码地址仓库

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值