自定义View —> 项目中遇到的复杂UI
1.首先就是上图了
拿到UI图后,就要构思怎么去实现了
第一个想法是相当于九宫格的逻辑去实现,第一个左边占上下两格,右边两个上下各占一格,横向的最好就是按比例去实现
但是九宫格的逻辑基本都是只有一张图片或是其它简单的imageview或是textview之类的就很好管理和实现,但是这个明显有点复杂,不好实现管理
所以第二个想法就是使用recycleview,通过适配器adapter,最后在布局方式,传统的方式不能实现的,比如LinerLayoutManager,GridLayoutManager,或是瀑布流模式都是没办法实现,需要自定义
2、自定义view实现
先确定好逻辑,横向分为3格,第一个左边2x2,第二个第三个在第一个右边,上下模式各占一格1x1,之间的间隔是8dp
接下来就可以通过代码实现了:
class CustomGridLayoutManager(context: Context) : RecyclerView.LayoutManager() {private var spacing: Int = (8 * context.resources.displayMetrics.density).toInt()override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {return RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT)}override fun canScrollVertically(): Boolean = falseoverride fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {if (itemCount == 0) {detachAndScrapAttachedViews(recycler)return}detachAndScrapAttachedViews(recycler)val usableWidth = width - paddingLeft - paddingRightval smallSize = (usableWidth - 2 * spacing) / 3 // 直接使用整数除法val bigSize = smallSize * 2 + spacing// --- 第 0 个:左边大方块 ---if (itemCount > 0) {val view = recycler.getViewForPosition(0)addView(view)// 关键修复:使用 measureChild 而不是 measureChildWithMarginsmeasureChild(view, bigSize, bigSize)layoutDecorated(view,paddingLeft,paddingTop,paddingLeft + bigSize,paddingTop + bigSize)}// --- 第 1 个:右上小方块 ---if (itemCount > 1) {val view = recycler.getViewForPosition(1)addView(view)measureChild(view, smallSize, smallSize)layoutDecorated(view,paddingLeft + bigSize + spacing,paddingTop,paddingLeft + bigSize + spacing + smallSize,paddingTop + smallSize)}// --- 第 2 个:右下小方块 ---if (itemCount > 2) {val view = recycler.getViewForPosition(2)addView(view)measureChild(view, smallSize, smallSize)layoutDecorated(view,paddingLeft + bigSize + spacing,paddingTop + smallSize + spacing,paddingLeft + bigSize + spacing + smallSize,paddingTop + smallSize + spacing + smallSize)}}// 辅助方法:简化测量逻辑override fun measureChild(child: View, width: Int, height: Int) {val widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)val heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)child.measure(widthSpec, heightSpec)}override fun onMeasure(recycler: RecyclerView.Recycler, state: RecyclerView.State, widthSpec: Int, heightSpec: Int) {val width = View.MeasureSpec.getSize(widthSpec)val usableWidth = width - paddingLeft - paddingRightval smallSize = (usableWidth - 2 * spacing) / 3val bigSize = smallSize * 2 + spacing// 计算总高度(取大方块和右侧两个小方块的最大高度)val totalHeight = if (itemCount > 2) {max(bigSize, smallSize * 2 + spacing) + paddingTop + paddingBottom} else if (itemCount > 1) {max(bigSize, smallSize) + paddingTop + paddingBottom} else {bigSize + paddingTop + paddingBottom}setMeasuredDimension(width, totalHeight)}
}
最后直接使用就好
rv_list.apply {animation = nulladapter = mRvAdapterlayoutManager = CustomGridLayoutManager(requireContext())}
自定义view核心代码分析以及逻辑分析:
布局效果分析
这个布局管理器创建了一个 "一大两小" 的特殊网格布局:
text
┌───────────┬───────┐ │ │ 1 │ │ 0 ├───────┤ │ │ 2 │ └───────────┴───────┘
位置0:左边大方块 (2x2)
位置1:右上小方块 (1x1)
位置2:右下小方块 (1x1)
代码详细分析
1. 核心参数计算
kotlin
val usableWidth = width - paddingLeft - paddingRight val smallSize = (usableWidth - 2 * spacing) / 3 // 小方块尺寸 val bigSize = smallSize * 2 + spacing // 大方块尺寸计算逻辑:
可用宽度 = 总宽度 - 左右padding
3等分减去2个间距得到小方块尺寸
大方块 = 2个小方块 + 1个间距
2. 布局逻辑 (
onLayoutChildren
)位置0 - 左边大方块:
kotlin
layoutDecorated(view,paddingLeft, // leftpaddingTop, // top paddingLeft + bigSize, // rightpaddingTop + bigSize // bottom )位置1 - 右上小方块:
kotlin
layoutDecorated(view,paddingLeft + bigSize + spacing, // left (大方块右边 + 间距)paddingTop, // toppaddingLeft + bigSize + spacing + smallSize, // rightpaddingTop + smallSize // bottom )位置2 - 右下小方块:
kotlin
layoutDecorated(view,paddingLeft + bigSize + spacing, // leftpaddingTop + smallSize + spacing, // top (小方块下面 + 间距)paddingLeft + bigSize + spacing + smallSize, // rightpaddingTop + smallSize + spacing + smallSize // bottom )3. 测量优化
自定义
measureChild
方法:kotlin
override fun measureChild(child: View, width: Int, height: Int) {val widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)val heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)child.measure(widthSpec, heightSpec) }使用
EXACTLY
模式确保子View精确匹配指定尺寸。4. 高度计算 (
onMeasure
)根据item数量动态计算总高度:
3个item:
max(大方块高度, 2*小方块+间距)
2个item:
max(大方块高度, 小方块高度)
1个item:大方块高度