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
/**
* 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
}
}
}