01-02-10 View开发最佳实践与优化完全指南

难度等级:⭐⭐⭐⭐
标签:View开发规范 性能优化 最佳实践 自定义View 架构设计

🎯 核心问题

如何系统性地开发高质量的自定义View?有哪些关键的开发流程和规范?如何避免常见的性能陷阱?如何优化View的测量、布局和绘制性能?

本文深入探讨自定义View的完整开发流程,从体系结构理解到构造函数设计,从开发规范到性能优化,提供全方位的最佳实践指南,帮助开发者构建高性能、可维护的自定义View。


💡 核心概念体系

View继承体系

Android View采用**组合模式(Composite Pattern)**设计,形成树形结构:

Object
  ↓
View (所有UI组件的基类)
  ↓
  ├─ TextView (文本显示)
  ├─ ImageView (图片显示)
  ├─ Button (按钮)
  └─ ViewGroup (容器)
       ↓
       ├─ LinearLayout (线性布局)
       ├─ RelativeLayout (相对布局)
       ├─ ConstraintLayout (约束布局)
       └─ FrameLayout (帧布局)

职责划分

  • View:负责绘制自己、处理事件、管理状态
  • ViewGroup:除View职责外,还负责管理子View、分发事件、布局控制

六步开发流程

1. 需求分析 → 确定功能和UI效果
2. 自定义属性 → 定义attrs.xml配置
3. 构造函数 → 处理属性初始化
4. 测量 → 实现onMeasure计算尺寸
5. 绘制 → 实现onDraw绘制内容
6. 交互 → 实现onTouchEvent处理事件

📝 完整开发实战

1. 构造函数设计与属性解析

View有4个构造函数,对应不同的创建场景。

构造函数调用链
View(Context)View(Context, AttributeSet?)View(Context, AttributeSet?, Int)View(Context, AttributeSet?, Int, Int)

调用场景

  • 单参数:代码创建View CustomView(context)
  • 双参数:XML布局加载(最常用)
  • 三参数:指定默认样式属性 CustomView(context, null, R.attr.customViewStyle)
  • 四参数:指定默认样式资源(API 21+)CustomView(context, null, 0, R.style.CustomViewStyle)
标准实现模板
class CustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = R.attr.customViewStyle,
    defStyleRes: Int = R.style.DefaultCustomViewStyle
) : View(context, attrs, defStyleAttr, defStyleRes) {

    // 自定义属性
    private var customText: String = ""
    private var customColor: Int = Color.BLUE
    private var customSize: Float = 14f
    private var customEnabled: Boolean = true

    // 画笔对象(复用,避免在onDraw中创建)
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG)

    init {
        initAttributes(attrs, defStyleAttr, defStyleRes)
        setupPaint()
    }

    private fun initAttributes(attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) {
        // 使用obtainStyledAttributes获取属性
        context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.CustomView,
            defStyleAttr,
            defStyleRes
        ).apply {
            try {
                customText = getString(R.styleable.CustomView_customText) ?: ""
                customColor = getColor(R.styleable.CustomView_customColor, Color.BLUE)
                customSize = getDimension(R.styleable.CustomView_customSize, 14f)
                customEnabled = getBoolean(R.styleable.CustomView_customEnabled, true)
            } finally {
                recycle() // ⚠️ 必须回收
            }
        }
    }

    private fun setupPaint() {
        paint.apply {
            color = customColor
            style = Paint.Style.FILL
        }

        textPaint.apply {
            color = Color.WHITE
            textSize = customSize
        }
    }
}
自定义属性定义
<!-- res/values/attrs.xml -->
<resources>
    <declare-styleable name="CustomView">
        <attr name="customText" format="string" />
        <attr name="customColor" format="color" />
        <attr name="customSize" format="dimension" />
        <attr name="customEnabled" format="boolean" />
        <attr name="customGravity" format="enum">
            <enum name="left" value="0" />
            <enum name="center" value="1" />
            <enum name="right" value="2" />
        </attr>
    </declare-styleable>

    <!-- 定义样式属性引用 -->
    <attr name="customViewStyle" format="reference" />
</resources>

<!-- res/values/styles.xml -->
<resources>
    <style name="DefaultCustomViewStyle">
        <item name="customColor">@color/primary</item>
        <item name="customSize">16sp</item>
        <item name="customEnabled">true</item>
    </style>
</resources>

2. 完整案例:彩色标签View

展示从需求分析到最终实现的完整流程。

Step 1: 需求分析

设计一个彩色标签View,具备以下特性:

  • 可自定义文本、颜色、大小
  • 圆角背景
  • 支持点击反馈
  • 自动计算合适的尺寸
Step 2: 完整实现
class TagView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = R.attr.tagViewStyle,
    defStyleRes: Int = R.style.DefaultTagViewStyle
) : View(context, attrs, defStyleAttr, defStyleRes) {

    // 属性
    private var tagText: String = ""
    private var tagColor: Int = Color.BLUE
    private var tagTextSize: Float = 14f.sp
    private var tagPadding: Float = 8f.dp

    // 画笔(复用对象)
    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.FILL
    }

    private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.WHITE
        textAlign = Paint.Align.CENTER
    }

    // 缓存对象
    private val tempRect = RectF()

    // 状态
    private var isPressed = false

    init {
        initAttributes(attrs, defStyleAttr, defStyleRes)
        isClickable = true
    }

    private fun initAttributes(attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) {
        context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.TagView,
            defStyleAttr,
            defStyleRes
        ).apply {
            try {
                tagText = getString(R.styleable.TagView_tagText) ?: ""
                tagColor = getColor(R.styleable.TagView_tagColor, Color.BLUE)
                tagTextSize = getDimension(R.styleable.TagView_tagTextSize, 14f.sp)
                tagPadding = getDimension(R.styleable.TagView_tagPadding, 8f.dp)

                backgroundPaint.color = tagColor
                textPaint.textSize = tagTextSize
            } finally {
                recycle()
            }
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // 计算文本宽度
        val textWidth = textPaint.measureText(tagText)
        val fontMetrics = textPaint.fontMetrics
        val textHeight = fontMetrics.descent - fontMetrics.ascent

        // 加上padding计算总尺寸
        val desiredWidth = (textWidth + tagPadding * 2).toInt()
        val desiredHeight = (textHeight + tagPadding * 2).toInt()

        setMeasuredDimension(
            resolveSize(desiredWidth, widthMeasureSpec),
            resolveSize(desiredHeight, heightMeasureSpec)
        )
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        // 绘制圆角背景(按下时改变透明度)
        backgroundPaint.alpha = if (isPressed) 180 else 255
        tempRect.set(0f, 0f, width.toFloat(), height.toFloat())
        canvas.drawRoundRect(tempRect, height / 2f, height / 2f, backgroundPaint)

        // 绘制文本(居中)
        val fontMetrics = textPaint.fontMetrics
        val baseline = height / 2 + (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent
        canvas.drawText(tagText, width / 2f, baseline, textPaint)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                isPressed = true
                invalidate()
                return true
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                if (isPressed) {
                    isPressed = false
                    invalidate()
                    if (event.action == MotionEvent.ACTION_UP) {
                        performClick()
                    }
                }
            }
        }
        return super.onTouchEvent(event)
    }

    override fun performClick(): Boolean {
        return super.performClick()
    }

    // 公开API
    fun setTagText(text: String) {
        if (tagText != text) {
            tagText = text
            requestLayout() // 尺寸可能改变
        }
    }

    fun setTagColor(color: Int) {
        if (tagColor != color) {
            tagColor = color
            backgroundPaint.color = color
            invalidate() // 仅颜色改变
        }
    }

    // 状态保存与恢复
    override fun onSaveInstanceState(): Parcelable {
        val superState = super.onSaveInstanceState()
        return Bundle().apply {
            putParcelable("superState", superState)
            putString("tagText", tagText)
            putInt("tagColor", tagColor)
        }
    }

    override fun onRestoreInstanceState(state: Parcelable?) {
        if (state is Bundle) {
            tagText = state.getString("tagText", "")
            tagColor = state.getInt("tagColor", Color.BLUE)
            backgroundPaint.color = tagColor
            super.onRestoreInstanceState(state.getParcelable("superState"))
        } else {
            super.onRestoreInstanceState(state)
        }
    }

    // 可访问性支持
    init {
        contentDescription = "标签: $tagText"
        importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
    }

    // 扩展属性
    private val Float.dp: Float
        get() = this * context.resources.displayMetrics.density

    private val Float.sp: Float
        get() = this * context.resources.displayMetrics.scaledDensity
}
Step 3: XML使用
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="16dp">

    <com.example.TagView
        android:id="@+id/tagHot"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:tagText="热门"
        app:tagColor="#FF5722"
        app:tagTextSize="12sp"
        app:tagPadding="6dp" />

    <com.example.TagView
        android:id="@+id/tagNew"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        app:tagText="新品"
        app:tagColor="#4CAF50" />
</LinearLayout>

🚀 性能优化核心策略

1. 避免对象分配

原则:永远不要在onDraw中创建对象。

// ❌ 错误示例:每次绘制都创建新对象
override fun onDraw(canvas: Canvas) {
    val paint = Paint() // 每次都创建!
    val rect = RectF()  // 每次都创建!
    paint.color = Color.RED
    canvas.drawRect(rect, paint)
}

// ✅ 正确示例:复用Paint对象
class OptimizedView : View {
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG) // 在init或构造函数中创建
    private val tempRect = RectF() // 缓存对象

    override fun onDraw(canvas: Canvas) {
        paint.color = Color.RED
        tempRect.set(0f, 0f, width.toFloat(), height.toFloat())
        canvas.drawRect(tempRect, paint)
    }
}

2. 减少不必要的刷新

// ❌ 错误示例:不检查就刷新
fun setColor(color: Int) {
    this.color = color
    invalidate() // 即使颜色相同也刷新
}

// ✅ 正确示例:检查是否真的需要刷新
fun setColor(color: Int) {
    if (this.color != color) {
        this.color = color
        invalidate() // 只在变化时刷新
    }
}

// ✅ 区分invalidate和requestLayout
fun setText(text: String) {
    if (this.text != text) {
        this.text = text

        // 如果文本长度改变,可能影响尺寸
        if (this.text.length != text.length) {
            requestLayout() // 触发measure → layout → draw
        } else {
            invalidate() // 仅触发draw
        }
    }
}

3. 使用Canvas裁剪

override fun onDraw(canvas: Canvas) {
    // 裁剪到可见区域
    canvas.clipRect(dirtyRect)

    // 只绘制可见内容
    items.forEach { item ->
        if (item.rect.intersects(dirtyRect)) {
            drawItem(canvas, item)
        }
    }
}

4. 缓存复杂图形

class ComplexView : View {
    private var cachedBitmap: Bitmap? = null
    private var isDirty = true

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        // 尺寸变化时重建缓存
        cachedBitmap?.recycle()
        cachedBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
        isDirty = true
    }

    override fun onDraw(canvas: Canvas) {
        // 只在内容变化时重绘到缓存
        if (isDirty) {
            val cacheCanvas = Canvas(cachedBitmap!!)
            drawComplexContent(cacheCanvas) // 复杂的绘制操作
            isDirty = false
        }

        // 直接绘制缓存的Bitmap
        cachedBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) }
    }

    fun updateContent() {
        isDirty = true
        invalidate()
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        cachedBitmap?.recycle()
        cachedBitmap = null
    }
}

5. 优化measure和layout

class OptimizedMeasureView : View {
    // 缓存计算结果
    private var cachedWidth = 0
    private var cachedHeight = 0
    private var measureDirty = true

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)

        // 如果尺寸已确定且未变化,直接返回
        if (widthMode == MeasureSpec.EXACTLY &&
            heightMode == MeasureSpec.EXACTLY &&
            !measureDirty) {
            setMeasuredDimension(
                MeasureSpec.getSize(widthMeasureSpec),
                MeasureSpec.getSize(heightMeasureSpec)
            )
            return
        }

        // 只在需要时重新计算
        if (measureDirty) {
            cachedWidth = calculateWidth() // 耗时操作
            cachedHeight = calculateHeight()
            measureDirty = false
        }

        setMeasuredDimension(
            resolveSize(cachedWidth, widthMeasureSpec),
            resolveSize(cachedHeight, heightMeasureSpec)
        )
    }

    fun updateContent() {
        measureDirty = true
        requestLayout()
    }
}

6. 减少过度绘制

class OptimizedOverdrawView : View {
    override fun onDraw(canvas: Canvas) {
        // ✅ 使用clipOut排除重叠区域
        canvas.save()

        // 先绘制背景
        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), bgPaint)

        // 排除前景区域
        canvas.clipOutRect(foregroundRect)

        canvas.restore()

        // 绘制前景
        canvas.drawRect(foregroundRect, fgPaint)
    }
}

7. 使用硬件加速

class HardwareAcceleratedView : View {
    init {
        // 启用硬件加速(API 11+)
        setLayerType(LAYER_TYPE_HARDWARE, null)
    }

    override fun onDraw(canvas: Canvas) {
        // 某些操作不支持硬件加速时,临时切换
        if (needSoftwareRendering) {
            setLayerType(LAYER_TYPE_SOFTWARE, null)
            // 执行不支持的操作
            setLayerType(LAYER_TYPE_HARDWARE, null)
        }

        // 正常绘制
        drawContent(canvas)
    }
}

8. 使用SurfaceView处理高频绘制

对于游戏、视频等高频刷新场景,使用SurfaceView。

class GameView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {
    private var drawingThread: DrawingThread? = null

    init {
        holder.addCallback(this)
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        drawingThread = DrawingThread(holder).apply {
            isRunning = true
            start()
        }
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        drawingThread?.let {
            it.isRunning = false
            it.join()
        }
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}

    private class DrawingThread(private val holder: SurfaceHolder) : Thread() {
        var isRunning = false

        override fun run() {
            while (isRunning) {
                var canvas: Canvas? = null
                try {
                    canvas = holder.lockCanvas()
                    canvas?.let { drawFrame(it) }
                } finally {
                    canvas?.let { holder.unlockCanvasAndPost(it) }
                }
            }
        }

        private fun drawFrame(canvas: Canvas) {
            // 绘制操作
        }
    }
}

9. 异步加载资源

class AsyncImageView : View {
    private var bitmap: Bitmap? = null
    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

    fun loadImage(url: String) {
        scope.launch {
            val loadedBitmap = withContext(Dispatchers.IO) {
                // 在IO线程加载图片
                loadBitmapFromUrl(url)
            }
            // 回到主线程更新UI
            bitmap = loadedBitmap
            invalidate()
        }
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        scope.cancel()
    }
}

10. 性能监控

class PerformanceMonitorView : View {
    private var frameCount = 0
    private var totalTime = 0L

    override fun onDraw(canvas: Canvas) {
        val startTime = System.nanoTime()

        // 绘制操作
        drawContent(canvas)

        val duration = (System.nanoTime() - startTime) / 1_000_000 // ms

        frameCount++
        totalTime += duration

        if (duration > 16) { // 超过1帧时间(60fps)
            Log.w("Performance", "Frame drop! onDraw took ${duration}ms")
        }

        if (frameCount >= 60) {
            val avgTime = totalTime / frameCount
            Log.d("Performance", "Average frame time: ${avgTime}ms")
            frameCount = 0
            totalTime = 0
        }
    }
}

📋 开发规范清单

构造函数规范

// ✅ 推荐:使用@JvmOverloads
class MyView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr)

// ❌ 不推荐:手动重载
class MyView : View {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    // ...
}

资源管理规范

// ✅ 及时回收TypedArray
context.obtainStyledAttributes(attrs, R.styleable.MyView).apply {
    // 读取属性
    recycle() // ⚠️ 必须调用
}

// ✅ 复用对象
private val paint = Paint()
private val tempRect = RectF()
private val path = Path()

// ❌ 不推荐
override fun onDraw(canvas: Canvas) {
    val paint = Paint() // 每次创建新对象
}

状态保存规范

override fun onSaveInstanceState(): Parcelable {
    val superState = super.onSaveInstanceState()
    return Bundle().apply {
        putParcelable("superState", superState)
        putInt("customColor", customColor)
        putString("customText", customText)
    }
}

override fun onRestoreInstanceState(state: Parcelable?) {
    if (state is Bundle) {
        customColor = state.getInt("customColor")
        customText = state.getString("customText", "")
        super.onRestoreInstanceState(state.getParcelable("superState"))
    } else {
        super.onRestoreInstanceState(state)
    }
}

可访问性规范

init {
    // 设置内容描述
    contentDescription = "自定义按钮"

    // 设置重要性
    importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
}

override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) {
    super.onInitializeAccessibilityEvent(event)
    event.className = MyView::class.java.name
}

⚡ 关键要点总结

开发流程

  1. 需求分析 → 确定功能和UI效果
  2. 定义属性 → 创建attrs.xml,支持XML配置
  3. 实现构造函数 → 使用@JvmOverloads,解析属性
  4. 测量 → onMeasure计算尺寸,使用resolveSize
  5. 绘制 → onDraw绘制内容,复用Paint对象
  6. 交互 → onTouchEvent处理事件,调用performClick
  7. 状态保存 → 实现onSaveInstanceState/onRestoreInstanceState
  8. 可访问性 → 添加contentDescription

性能优化

优化点 方法 收益
对象复用 在init中创建Paint等对象 避免GC,提升流畅度
减少刷新 检查状态变化再invalidate 减少不必要的重绘
缓存图形 使用Bitmap缓存复杂绘制 大幅提升复杂View性能
局部刷新 使用invalidate(Rect) 减少绘制区域
硬件加速 setLayerType(HARDWARE) GPU加速,性能提升明显
测量优化 缓存计算结果 避免重复计算
异步加载 协程加载资源 避免阻塞主线程
SurfaceView 高频绘制场景 独立绘制线程,不阻塞UI

常见陷阱

  1. onDraw中创建对象 → 导致频繁GC
  2. 不区分invalidate和requestLayout → 触发不必要的measure/layout
  3. 忘记recycle TypedArray → 内存泄漏
  4. 过度绘制 → 性能下降
  5. measure中耗时计算 → 影响布局性能
  6. 不处理状态保存 → 屏幕旋转后数据丢失

🔗 相关知识点

  • View绘制流程:measure、layout、draw三大步骤
  • 事件分发机制:onTouchEvent、dispatchTouchEvent
  • LayoutParams:自定义布局参数
  • ViewGroup实现:自定义容器布局
  • 属性动画:动态修改View属性
  • Canvas绘制:Path、Matrix、Shader等高级特性

📖 实战建议

开发流程

  1. 先确定需求:明确View的功能和交互方式
  2. 设计API:定义公开的属性和方法
  3. 实现核心逻辑:measure、draw、touch
  4. 测试边界情况:不同尺寸、不同数据、横竖屏切换
  5. 性能优化:使用Profiler工具分析性能瓶颈
  6. 文档和示例:提供清晰的使用文档和示例代码

调试技巧

// 开启布局边界显示
Settings → Developer Options → Show layout bounds

// 开启GPU过度绘制检测
Settings → Developer Options → Debug GPU overdraw

// 使用Hierarchy Viewer分析View树
Android Studio → Tools → Layout Inspector

// 性能监控
class DebugView : View {
    override fun onDraw(canvas: Canvas) {
        Trace.beginSection("MyView.onDraw")
        try {
            drawContent(canvas)
        } finally {
            Trace.endSection()
        }
    }
}

常见问题

Q: 为什么自定义View不显示?
A: 检查以下几点:

  1. onMeasure是否正确调用setMeasuredDimension
  2. onDraw是否有绘制内容
  3. 布局参数是否设置为0dp或GONE
  4. View是否在可见区域内

Q: 如何实现子View居中?
A: 在onLayout中计算偏移量:

val offsetX = (width - child.measuredWidth) / 2
val offsetY = (height - child.measuredHeight) / 2
child.layout(offsetX, offsetY,
             offsetX + child.measuredWidth,
             offsetY + child.measuredHeight)

Q: 何时使用invalidate,何时使用requestLayout?
A:

  • 仅内容改变(颜色、文本内容)→ invalidate()
  • 尺寸改变(文本长度、View大小)→ requestLayout()

Q: 如何支持dp和sp?
A: 使用扩展属性转换:

private val Float.dp: Float
    get() = this * context.resources.displayMetrics.density

private val Float.sp: Float
    get() = this * context.resources.displayMetrics.scaledDensity

📚 扩展阅读

  1. View源码分析:深入理解View的实现原理
  2. Canvas与Paint详解:掌握高级绘制技巧
  3. 硬件加速原理:理解GPU渲染机制
  4. Material Design规范:学习优秀的UI设计
  5. Jetpack Compose:了解声明式UI的未来
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐