2026-02

ViewGroup 开发指南

Android 自定义 ViewGroup 综合学习笔记

ViewGroup 自定义View 布局测量

目录导航

一、测量阶段(onMeasure)

方法 作用 注意事项
onMeasure() 测量子View并确定自身尺寸
  • 必须重写
  • 使用 measureChild()measureChildWithMargins()
  • 正确处理 MeasureSpec(EXACTLY / AT_MOST / UNSPECIFIED)
  • 高度/宽度别算错!(常见bug:用maxWidth当height)
setMeasuredDimension() 设置最终测量尺寸
  • 必须在 onMeasure 最后调用
  • 使用 resolveSize()处理父约束
典型实现逻辑
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // 1. 测量子View
    val childCount = childCount
    for (i in 0 until childCount) {
        measureChildWithMargins(childAt(i), widthMeasureSpec, 0, heightMeasureSpec, 0)
    }

    // 2. 计算自身尺寸
    var totalWidth = paddingLeft + paddingRight
    var totalHeight = paddingTop + paddingBottom

    // 3. 调用setMeasuredDimension
    val finalWidth = resolveSize(totalWidth, widthMeasureSpec)
    val finalHeight = resolveSize(totalHeight, heightMeasureSpec)
    setMeasuredDimension(finalWidth, finalHeight)
}

二、布局阶段(onLayout)

方法 作用 注意事项
onLayout() 定位每个子View的位置
  • 必须重写
  • 跳过 GONE 的子View
  • 考虑 padding
  • 若支持 margin,需读取 MarginLayoutParams
child.layout(l, t, r, b) 设置子View的边界
  • l,t,r,b 是相对于父View的坐标
  • r = l + child.getMeasuredWidth()
  • b = t + child.getMeasuredHeight()
典型实现逻辑
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    val parentWidth = r - l
    val parentHeight = b - t

    // 遍历所有子View
    var cumulativeHeight = paddingTop
    for (i in 0 until childCount) {
        val child = getChildAt(i)
        if (child.visibility != VISIBLE) continue

        // 获取 MarginLayoutParams
        val lp = child.layoutParams as MarginLayoutParams
        
        // 计算子View的边界(示例:垂直排列)
        val childLeft = paddingLeft + lp.leftMargin
        val childTop = cumulativeHeight + lp.topMargin
        val childRight = childLeft + child.measuredWidth
        val childBottom = childTop + child.measuredHeight

        child.layout(childLeft, childTop, childRight, childBottom)
        cumulativeHeight = childBottom + lp.bottomMargin
    }
}

三、绘制阶段(onDraw/dispatchDraw)

方法 作用 注意事项
onDraw() 绘制自身背景
  • ViewGroup 默认不调用 onDraw!
  • 若需绘制背景,必须调用 setWillNotDraw(false)
dispatchDraw() 绘制子View
  • 默认由系统调用
  • 可重写以添加额外绘制内容(如分隔线)
draw() 完整绘制流程
  • 不要直接重写此方法
  • 优先使用 onDraw 和 dispatchDraw
绘制分隔线示例
override fun dispatchDraw(canvas: Canvas) {
    super.dispatchDraw(canvas)
    // 在子View绘制后添加分隔线
    if (showDivider) {
        val paint = Paint().apply {
            color = dividerColor
            strokeWidth = dividerHeight.toFloat()
        }
        var y = paddingTop.toFloat()
        for (i in 0 until childCount - 1) {
            val child = getChildAt(i)
            if (child.visibility == VISIBLE) {
                y += child.measuredHeight + (child.layoutParams as MarginLayoutParams).bottomMargin
                canvas.drawLine(paddingLeft.toFloat(), y, (width - paddingRight).toFloat(), y, paint)
                y += dividerHeight
            }
        }
    }
}

四、自定义属性与XML配置

4.1 定义自定义属性

res/values/attrs.xml

<declare-styleable name="MyVerticalLayout">
    <attr name="dividerColor" format="color" />
    <attr name="dividerHeight" format="dimension" />
    <attr name="showDivider" format="boolean" />
</declare-styleable>
4.2 解析自定义属性

Kotlin 最佳实践:

class MyVerticalLayout(context: Context, attrs: AttributeSet?) : ViewGroup(context, attrs) {

    private var dividerColor = Color.GRAY
    private var dividerHeight = 2
    private var showDivider = true

    init {
        // 使用KTX的withStyledAttributes自动回收TypedArray
        context.withStyledAttributes(attrs, R.styleable.MyVerticalLayout) {
            dividerColor = getColor(R.styleable.MyVerticalLayout_dividerColor, Color.GRAY)
            dividerHeight = getDimensionPixelSize(R.styleable.MyVerticalLayout_dividerHeight, 2)
            showDivider = getBoolean(R.styleable.MyVerticalLayout_showDivider, true)
        }
        setWillNotDraw(false) // 启用onDraw
    }
}
4.3 XML中使用自定义属性
<com.example.MyVerticalLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:dividerColor="#FF0000"
    app:showDivider="true"
    app:dividerHeight="1dp">
    <!-- 子View -->
</com.example.MyVerticalLayout>

三、进阶能力与最佳实践

3.1 支持子View的Margin

必须重写三个方法:

class MyVerticalLayout : ViewGroup {
    // 支持Margin的关键三方法
    override fun generateLayoutParams(attrs: AttributeSet?) = MarginLayoutParams(context, attrs)
    override fun generateDefaultLayoutParams() = MarginLayoutParams(WRAP_CONTENT, WRAP_CONTENT)
    override fun checkLayoutParams(p: LayoutParams): Boolean = p is MarginLayoutParams

    // 测量子View时必须使用带margin的方法
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val childCount = childCount
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            val lp = child.layoutParams as MarginLayoutParams
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
        }
        // 计算自身尺寸(需包含所有子View的margin)
    }
}
3.2 性能优化技巧
优化原则:
  • 避免在 onMeasure/onLayout中创建对象(如 Paint、Rect)
  • 缓存计算结果(如总高度、最大宽度)
  • 使用 ViewGroup.resolveSize()正确处理父约束
优化示例:
class MyVerticalLayout : ViewGroup {
    // 缓存Paint对象
    private val backgroundPaint = Paint().apply {
        color = Color.GRAY
        style = Paint.Style.STROKE
        strokeWidth = 2f
    }

    // 复用Rect对象
    private val tempRect = Rect()

    override fun onDraw(canvas: Canvas) {
        // 使用缓存的Paint和Rect对象
        tempRect.set(0, 0, width, height)
        canvas.drawRect(tempRect, backgroundPaint)
    }
}
3.3 滚动支持
基础滚动实现:
class ScrollingLayout : ViewGroup {
    private var scrollY = 0
    private var lastY = 0f

    override fun dispatchDraw(canvas: Canvas) {
        // 先滚动画布,再绘制子View
        canvas.translate(0f, scrollY.toFloat())
        super.dispatchDraw(canvas)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> lastY = event.y
            MotionEvent.ACTION_MOVE -> {
                val dy = lastY - event.y
                scrollY += dy.toInt()
                // 限制滚动范围
                scrollY = Math.max(Math.min(scrollY, maxScrollY), 0)
                invalidate() // 请求重绘
            }
        }
        lastY = event.y
        return true
    }
}
高级滚动(带惯性):
// 需要配合Scroller实现fling效果
private val scroller = Scroller(context)

override fun computeScroll() {
    if (scroller.computeScrollOffset()) {
        scrollY = scroller.currY
        invalidate() // 更新滚动位置
    }
}

// 处理fling事件
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    if (ev.actionMasked == MotionEvent.ACTION_MOVE) {
        // 检测是否需要滚动
        return shouldIntercept
    }
    return false
}
3.4 状态保存与恢复
完整状态管理示例:
class MyVerticalLayout : ViewGroup {
    override fun onSaveInstanceState(): Parcelable {
        val superState = super.onSaveInstanceState()
        return SavedState(superState).apply {
            // 保存自定义状态
            currentScrollY = scrollY
        }
    }

    override fun onRestoreInstanceState(state: Parcelable?) {
        val savedState = state as? SavedState ?: return super.onRestoreInstanceState(state)
        super.onRestoreInstanceState(savedState.superState)
        // 恢复自定义状态
        scrollY = savedState.currentScrollY
        invalidate() // 应用新状态
    }

    // 自定义状态类
    class SavedState : BaseSavedState {
        var currentScrollY = 0
        constructor(superState: Parcelable?) : super(superState)
        private constructor(parcel: Parcel) : super(parcel) {
            currentScrollY = parcel.readInt()
        }
        override fun writeToParcel(out: Parcel, flags: Int) {
            super.writeToParcel(out, flags)
            out.writeInt(currentScrollY)
        }

        companion object {
            @JvmField val CREATOR = object : Parcelable.Creator {
                override fun createFromParcel(parcel: Parcel) = SavedState(parcel)
                override fun newArray(size: Int) = arrayOfNulls(size)
            }
        }
    }
}

四、资源管理与对象回收规范

4.1 必须手动回收的对象
对象类型 回收方法 最佳实践
TypedArray recycle() 使用KTX的 .use { }自动回收
自建 Bitmap recycle() onDetachedFromWindow()中回收
Handler 回调 removeCallbacks() onDetachedFromWindow()中移除
Animator cancel() onDetachedFromWindow()中取消
TypedArray 回收示例:
// 错误写法(可能导致资源泄漏)
val ta = context.obtainStyledAttributes(attrs, R.styleable.MyView)
val color = ta.getColor(R.styleable.MyView_color, Color.BLACK)
// 忘记recycle() → Lint警告 + 潜在泄漏

// 正确写法(推荐KTX)
context.obtainStyledAttributes(attrs, R.styleable.MyView).use { ta ->
    val color = ta.getColor(R.styleable.MyView_color, Color.BLACK)
    // 自动调用ta.recycle()
}
自建 Bitmap 回收示例:
class MyView : View {
    private var customBitmap: Bitmap? = null

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        // 回收自建的Bitmap
        customBitmap?.recycle()
        customBitmap = null
    }
}
4.2 建议复用的对象
对象类型 复用策略 为什么重要
Paint 定义为成员变量并复用 频繁创建消耗内存,Paint 内部有原生资源
Rect 定义为成员变量并复用 高频绘制场景(如 onDraw)减少GC开销
Matrix 定义为成员变量并复用 避免频繁创建原生资源,提升性能
Paint 复用示例:
class MyView : View {
    // 缓存Paint对象
    private val textPaint = Paint().apply {
        color = Color.RED
        strokeWidth = 2f
        textSize = 16f * resources.displayMetrics.scaledDensity
    }

    override fun onDraw(canvas: Canvas) {
        // 直接使用缓存的Paint对象
        canvas.drawText("Hello", 0f, 0f, textPaint)
        // 不要在这里创建新的Paint对象!
    }
}
Rect 复用示例:
class MyView : View {
    // 定义成员变量
    private val tempRect = Rect()

    override fun onDraw(canvas: Canvas) {
        // 复用tempRect,而不是每次新建
        tempRect.set(0, 0, width, height)
        canvas.drawRect(tempRect, paint)
        // 下次使用时会重用同一个Rect对象
    }
}

五、无障碍支持(Accessibility)

5.1 必须实现的关键方法
方法 作用 注意事项
onInitializeAccessibilityNodeInfo() 初始化无障碍节点信息 必须设置contentDescription、setBoundsInScreen()、setClassName()
onInitializeAccessibilityEvent() 初始化无障碍事件 必须反映组件的可见性、处理焦点状态
无障碍实现示例:
class CustomViewGroup : ViewGroup {

    override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
        super.onInitializeAccessibilityNodeInfo(info)
        // 设置内容描述
        info.contentDescription = "自定义垂直布局"
        info.setBoundsInScreen(Rect())
        info.className = javaClass.name
        // 添加点击操作支持
        info.addAction(AccessibilityNodeInfo.ACTION_CLICK)
        info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
    }

    override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) {
        super.onInitializeAccessibilityEvent(event)
        // 确保事件包含正确的信息
        event.className = javaClass.name
        event.text.add("自定义垂直布局")
    }

    // 为可聚焦子View设置焦点
    override fun addFocusables(views: ArrayList, direction: Int, previouslyHasFocus: Boolean) {
        // 遍历子View并添加可聚焦的子View
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (child.isFocusable) {
                views.add(child)
            }
        }
    }
}

六、动画与过渡效果

6.1 布局动画(LayoutTransition)
启用默认动画:

<com.example.MyVerticalLayout
    android:animateLayoutChanges="true"
    ... other attributes ...>
</com.example.MyVerticalLayout>

// 或在代码中启用
layoutTransition = LayoutTransition()
自定义动画类型:
val transition = LayoutTransition()

// 设置入场动画
transition.setAnimator(LayoutTransition.APPEARING, ObjectAnimator.ofFloat(null, "alpha", 0f, 1f))

// 设置消失动画
transition.setAnimator(LayoutTransition.DISAPPEARING, ObjectAnimator.ofFloat(null, "alpha", 1f, 0f))

// 设置布局改变时的动画
transition.setAnimator(LayoutTransition.CHANGE_APPEARING, ObjectAnimator.ofFloat(null, "scaleX", 1f, 1.1f, 1f))
transition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, ObjectAnimator.ofFloat(null, "scaleX", 1f, 0.9f, 1f))

// 应用到ViewGroup
this.layoutTransition = transition
6.2 子View入场动画
自定义入场动画示例:
class MyVerticalLayout : ViewGroup {
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        super.onLayout(changed, l, t, r, b)
        if (changed) {
            // 遍历所有子View并添加入场动画
            for (i in 0 until childCount) {
                val child = getChildAt(i)
                if (child.visibility == View.VISIBLE) {
                    child.translationY = child.measuredHeight.toFloat()
                    child.alpha = 0f
                    ObjectAnimator.ofFloat(child, "translationY", 0f).apply {
                        duration = 300
                        interpolator = AccelerateDecelerateInterpolator()
                        start()
                    }
                    ObjectAnimator.ofFloat(child, "alpha", 1f).apply {
                        duration = 300
                        interpolator = AccelerateDecelerateInterpolator()
                        start()
                    }
                }
            }
        }
    }
}

七、调试与测试技巧

7.1 使用Layout Inspector调试
调试要点:
  • 检查 measuredWidth/Height 是否符合预期
  • 验证 padding/margin 是否生效
  • 确认子View的 layout bounds 是否正确
  • 检查 AccessibilityNodeInfo 是否正确暴露
操作步骤:
  1. 在 Android Studio 中打开 Layout Inspector 工具
  2. 选择设备和界面
  3. 点击 MyVerticalLayout 查看其测量尺寸和布局边界
  4. 检查子View的 margin 和 padding 是否正确应用
7.2 单元测试与UI测试
单元测试示例:
// 使用Robolectric测试onMeasure逻辑
@Test
fun testOnMeasureWithWrapContent() {
    val layout = MyVerticalLayout(ApplicationProvider.getApplicationContext())
    val childView = TextView(ApplicationProvider.getApplicationContext())
    childView.layoutParams = MarginLayoutParams(100, 200)
    
    layout.addView(childView)
    layout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
    
    assertEquals(100 + layout.paddingLeft + layout.paddingRight, layout.measuredWidth)
    assertEquals(200 + layout.paddingTop + layout.paddingBottom, layout.measuredHeight)
}
UI测试示例:
// 使用Espresso验证分隔线显示
@Test
fun testDividerVisibility() {
    launchActivity()
    // 确保分隔线可见
    onView(withId(R.id.divider)).check(matches(isDisplayed()))
    // 验证分隔线颜色
    onView(withId(R.id.divider)).check(matches(hasBackgroundColor(dividerColor)))
}

八、向官方源码学习

8.1 精读官方ViewGroup源码
源码类 学习重点 关键实现
LinearLayout 权重分配、baseline对齐、margin处理 weightSum的计算、measureWithBaseline的实现、computeChildWidth的逻辑
FrameLayout 简单叠加、foreground支持 子View按Gravity对齐、foreground的绘制时机、setForeground的实现
CoordinatorLayout Behavior机制、交互解耦 Behavior接口设计、layoutChild的实现、onInterceptTouchEvent的逻辑
学习建议:
  1. 在 Android Studio 中使用 Ctrl + 左键跳转到源码
  2. 重点关注 onMeasureonLayoutonDraw的实现
  3. 学习如何处理边界条件(如 GONE 子View、margin冲突)
  4. 分析性能优化技巧(如缓存计算结果、减少GC开销)

九、常见误区与Bug清单

9.1 高频Bug自查
问题 解决方案 影响
onMeasure中高度计算错误 使用 resolveSize(totalHeight, heightMeasureSpec) 布局错乱,尺寸不符合预期
ViewGroup默认不调用onDraw 在构造函数中调用 setWillNotDraw(false) 背景无法绘制
忘记TypedArray recycle() 使用KTX的 .use { }或 try-finally 回收 资源泄漏,导致OOM或崩溃
不支持子View的margin 重写 generateLayoutParams并返回 MarginLayoutParams margin属性无效
滚动内容无法显示全 计算 maxScrollY并限制滚动范围 内容被截断,无法滑动到底部
9.2 性能优化检查点
问题 解决方案 影响
在onDraw中频繁创建Paint对象 定义为成员变量并复用 增加GC压力,可能导致卡顿
未复用Rect等临时对象 定义为成员变量并重用 增加GC压力,导致内存波动
未处理GONE状态的子View 在onLayout中跳过GONE子View 错误计算尺寸,导致布局错乱
未正确处理padding 在测量和布局时考虑padding 内容被padding遮挡,无法正确显示

十、完整示例代码(垂直布局+分隔线+滚动)

// 自定义垂直布局,支持分隔线和滚动
class VerticalLayout(context: Context, attrs: AttributeSet?) : ViewGroup(context, attrs) {
    // 自定义属性
    private var dividerColor = Color.GRAY
    private var dividerHeight = 2
    private var showDivider = true
    private val dividerPaint = Paint()

    // 滚动相关变量
    private var scrollY = 0
    private var lastY = 0f
    private var contentHeight = 0
    private val maxScrollY get() = Math.max(0, contentHeight - height)

    // 临时对象复用
    private val tempRect = Rect()

    init {
        // 解析自定义属性
        context.withStyledAttributes(attrs, R.styleable.VerticalLayout) {
            dividerColor = getColor(R.styleable.VerticalLayout_dividerColor, Color.GRAY)
            dividerHeight = getDimensionPixelSize(R.styleable.VerticalLayout_dividerHeight, 2)
            showDivider = getBoolean(R.styleable.VerticalLayout_showDivider, true)
        }

        // 初始化画笔
        dividerPaint.color = dividerColor
        dividerPaint.strokeWidth = dividerHeight.toFloat()

        // 启用滚动
        setWillNotDraw(false) // 启用onDraw
        setClipChildren(false) // 允许内容超出边界

        // 启用布局动画
        layoutTransition = LayoutTransition()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val childCount = childCount
        var totalWidth = paddingLeft + paddingRight
        var totalHeight = paddingTop + paddingBottom

        // 测量子View
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            val lp = child.layoutParams as MarginLayoutParams
            
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
            
            val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
            val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin
            
            totalWidth = Math.max(totalWidth, childWidth)
            totalHeight += childHeight
        }

        // 计算自身尺寸
        val finalWidth = resolveSize(totalWidth, widthMeasureSpec)
        val finalHeight = resolveSize(totalHeight, heightMeasureSpec)
        setMeasuredDimension(finalWidth, finalHeight)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        var currentY = paddingTop
        
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            val lp = child.layoutParams as MarginLayoutParams
            
            if (child.visibility != View.VISIBLE) continue

            // 计算子View位置
            val childLeft = paddingLeft + lp.leftMargin
            val childTop = currentY + lp.topMargin
            val childRight = childLeft + child.measuredWidth
            val childBottom = childTop + child.measuredHeight

            // 应用布局(考虑滚动偏移)
            child.layout(childLeft, childTop - scrollY, childRight, childBottom - scrollY)

            // 更新累计高度
            currentY += child.measuredHeight + lp.topMargin + lp.bottomMargin
        }

        // 记录内容高度
        contentHeight = currentY
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        
        // 绘制分隔线
        if (showDivider) {
            var y = paddingTop.toFloat()
            for (i in 0 until childCount - 1) {
                val child = getChildAt(i)
                if (child.visibility == View.VISIBLE) {
                    val lp = child.layoutParams as MarginLayoutParams
                    y += child.measuredHeight + lp.topMargin + lp.bottomMargin
                    canvas.drawLine(paddingLeft.toFloat(), y, (width - paddingRight).toFloat(), y, dividerPaint)
                }
            }
        }
    }

    override fun dispatchDraw(canvas: Canvas) {
        // 先滚动画布
        canvas.translate(0f, scrollY.toFloat())
        super.dispatchDraw(canvas)
    }

    // 支持Margin的关键方法
    override fun generateLayoutParams(attrs: AttributeSet?) = MarginLayoutParams(context, attrs)
    override fun generateDefaultLayoutParams() = MarginLayoutParams(WRAP_CONTENT, WRAP_CONTENT)
    override fun checkLayoutParams(p: LayoutParams): Boolean = p is MarginLayoutParams

    // 处理触摸事件
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> lastY = event.y
            MotionEvent.ACTION_MOVE -> {
                val dy = lastY - event.y
                scrollY += dy.toInt()
                // 限制滚动范围
                scrollY = Math.max(Math.min(scrollY, maxScrollY), 0)
                invalidate() // 请求重绘
            }
        }
        lastY = event.y
        return true
    }

    // 无障碍支持
    override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
        super.onInitializeAccessibilityNodeInfo(info)
        info.contentDescription = "垂直布局"
        info.className = javaClass.name
        info.isVisibleToUser = true
        info.setBoundsInScreen(Rect())
    }

    // 资源回收
    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        // 取消所有动画
        layoutTransition?.cancel()
    }
}

十一、总结与最佳实践

自定义ViewGroup的完整流程:
  1. 定义自定义属性
  2. 重写 onMeasure测量子View并确定自身尺寸
  3. 重写 onLayout定位所有子View
  4. 必要时重写 onDraw绘制自身背景
  5. 添加对 margin 的支持(重写 generateLayoutParams
  6. 实现滚动支持(处理触摸事件,更新 scrollY)
  7. 添加动画效果(使用 LayoutTransition或自定义动画)
  8. 支持无障碍(重写 onInitializeAccessibilityNodeInfo
  9. 处理资源回收(在 onDetachedFromWindow中释放资源)
关键原则:
  • 资源管理:始终遵循"获取→使用→回收"的流程,避免资源泄漏
  • 性能优化:在 onMeasure/onLayout中避免创建对象,使用缓存机制
  • 代码结构:保持核心方法(onMeasure/onLayout)的简洁性,复杂逻辑拆分到辅助函数
  • 调试技巧:使用 Layout Inspector 验证测量和布局结果,确保符合预期
  • 无障碍支持:确保所有交互元素都正确暴露给无障碍服务,提升用户体验
下一步行动建议:
  1. 尝试实现一个支持 weight 属性的水平布局
  2. 添加对 gravity 的支持,允许子View在容器内对齐
  3. 实现更复杂的滚动逻辑,如惯性滚动和边界反弹
  4. 尝试实现一个 Behavior 类,让自定义 ViewGroup 与其他组件协同工作
  5. 使用 LeakCanary 检测内存泄漏,验证资源回收逻辑是否有效
箴言:会写 onMeasure/onLayout是入门,能写出媲美系统 ViewGroup 的容器,才是高手。
学习路径总结: