android 自定义view 详解

Android 自定义 View 详解

自定义 View 是 Android 开发的核心技能之一,本文从原理到实践,系统梳理自定义 View 的完整知识体系。


一、自定义 View 的三种方式

方式 适用场景 核心工作
继承现有控件 (如 TextView、ImageView) 在已有控件基础上扩展功能 复用绘制逻辑,重写关键方法
继承 View 完全自定义绘制内容(如饼图、进度条) 重写 onMeasure + onDraw
继承 ViewGroup 自定义布局管理器(如流式布局、瀑布流) 重写 onMeasure + onLayout

二、核心生命周期方法

1. 构造方法(Constructor)

kotlin

复制代码
class CustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    init {
        // 读取自定义属性
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView)
        val color = typedArray.getColor(R.styleable.CustomView_circleColor, Color.RED)
        typedArray.recycle()
    }
}

三个构造方法的区别:

  • View(Context):代码中直接 new

  • View(Context, AttributeSet):XML 中使用

  • View(Context, AttributeSet, Int):XML 中使用 + 指定 style

2. onMeasure ------ 测量尺寸

kotlin

复制代码
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    
    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
    
    // 三种测量模式
    when (widthMode) {
        MeasureSpec.EXACTLY -> { /* match_parent 或具体数值 */ }
        MeasureSpec.AT_MOST -> { /* wrap_content */ }
        MeasureSpec.UNSPECIFIED -> { /* 父布局不限制,如 ScrollView 内 */ }
    }
    
    // 设置最终测量结果
    setMeasuredDimension(resolveSize(desiredWidth, widthMeasureSpec),
                         resolveSize(desiredHeight, heightMeasureSpec))
}

测量模式速查:

Mode 触发条件 处理方式
EXACTLY match_parent / 具体 dp 直接使用给定尺寸
AT_MOST wrap_content 计算内容所需尺寸,不超过上限
UNSPECIFIED 父布局不限制 按内容实际需要设置

3. onSizeChanged ------ 尺寸变化

kotlin

复制代码
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    // 初始化与尺寸相关的对象(如 Rect、Path、Shader)
    centerX = w / 2f
    centerY = h / 2f
    radius = min(w, h) / 2f - padding
}

4. onDraw ------ 绘制内容(核心)

kotlin

复制代码
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    
    // 1. 绘制背景(系统已处理,通常不需要手动调用)
    
    // 2. 绘制内容
    canvas.drawCircle(centerX, centerY, radius, paint)
    canvas.drawRect(rect, paint)
    canvas.drawText("Hello", x, y, textPaint)
    
    // 3. 使用 Path 绘制复杂图形
    val path = Path().apply {
        moveTo(100f, 100f)
        lineTo(200f, 200f)
        quadTo(300f, 100f, 400f, 200f) // 二次贝塞尔曲线
        close()
    }
    canvas.drawPath(path, paint)
}

Canvas 常用绘制 API:

复制代码
drawCircle()      drawRect()      drawOval()
drawArc()         drawLine()      drawPoint()
drawText()        drawBitmap()    drawPath()

5. onLayout(仅 ViewGroup)

kotlin

复制代码
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    for (i in 0 until childCount) {
        val child = getChildAt(i)
        // 计算子 View 的位置
        child.layout(left, top, right, bottom)
    }
}

三、Paint 详解 ------ 画笔配置

kotlin

复制代码
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    color = Color.RED                    // 颜色
    strokeWidth = 8f                     // 描边宽度
    style = Paint.Style.STROKE           // FILL / STROKE / FILL_AND_STROKE
    strokeCap = Paint.Cap.ROUND          // 线帽:BUTT / ROUND / SQUARE
    strokeJoin = Paint.Join.ROUND        // 连接处:MITER / ROUND / BEVEL
    isAntiAlias = true                   // 抗锯齿
    isDither = true                      // 防抖动(颜色过渡更平滑)
    
    // 高级效果
    shader = LinearGradient(...)         // 渐变
    maskFilter = BlurMaskFilter(...)     // 模糊效果
    pathEffect = DashPathEffect(...)     // 虚线效果
}

四、完整示例:圆形进度条

1. 自定义属性(res/values/attrs.xml)

xml

复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleProgressView">
        <attr name="progressColor" format="color"/>
        <attr name="bgColor" format="color"/>
        <attr name="strokeWidth" format="dimension"/>
        <attr name="maxProgress" format="integer"/>
        <attr name="currentProgress" format="integer"/>
    </declare-styleable>
</resources>

2. 完整 View 代码

kotlin

复制代码
class CircleProgressView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private var progressColor = Color.parseColor("#2196F3")
    private var bgColor = Color.parseColor("#E0E0E0")
    private var strokeWidth = 20f
    private var maxProgress = 100
    private var currentProgress = 0
    
    private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val rectF = RectF()
    
    init {
        context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView).apply {
            progressColor = getColor(R.styleable.CircleProgressView_progressColor, progressColor)
            bgColor = getColor(R.styleable.CircleProgressView_bgColor, bgColor)
            strokeWidth = getDimension(R.styleable.CircleProgressView_strokeWidth, strokeWidth)
            maxProgress = getInt(R.styleable.CircleProgressView_maxProgress, maxProgress)
            currentProgress = getInt(R.styleable.CircleProgressView_currentProgress, currentProgress)
            recycle()
        }
        
        bgPaint.apply {
            color = bgColor
            this.strokeWidth = this@CircleProgressView.strokeWidth
            style = Paint.Style.STROKE
            strokeCap = Paint.Cap.ROUND
        }
        
        progressPaint.apply {
            color = progressColor
            this.strokeWidth = this@CircleProgressView.strokeWidth
            style = Paint.Style.STROKE
            strokeCap = Paint.Cap.ROUND
        }
    }
    
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val size = resolveSize(200.dpToPx(), widthMeasureSpec)
        setMeasuredDimension(size, size)
    }
    
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        val padding = strokeWidth / 2
        rectF.set(padding, padding, w - padding, h - padding)
    }
    
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        
        // 绘制背景圆环
        canvas.drawArc(rectF, 0f, 360f, false, bgPaint)
        
        // 绘制进度圆弧
        val sweepAngle = 360f * currentProgress / maxProgress
        canvas.drawArc(rectF, -90f, sweepAngle, false, progressPaint)
        
        // 绘制进度文字
        val text = "$currentProgress%"
        val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
            color = progressColor
            textSize = width / 4f
            textAlign = Paint.Align.CENTER
        }
        val baseline = height / 2f - (textPaint.descent() + textPaint.ascent()) / 2
        canvas.drawText(text, width / 2f, baseline, textPaint)
    }
    
    fun setProgress(progress: Int) {
        currentProgress = progress.coerceIn(0, maxProgress)
        invalidate() // 请求重绘
    }
    
    private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).toInt()
}

3. XML 中使用

xml

复制代码
<com.example.CircleProgressView
    android:layout_width="120dp"
    android:layout_height="120dp"
    app:progressColor="@color/purple_500"
    app:bgColor="#F5F5F5"
    app:strokeWidth="12dp"
    app:maxProgress="100"
    app:currentProgress="75"/>

五、触摸事件处理

kotlin

复制代码
override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            parent.requestDisallowInterceptTouchEvent(true)
            // 处理按下逻辑
            return true // 消费事件
        }
        MotionEvent.ACTION_MOVE -> {
            // 处理滑动逻辑
        }
        MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
            parent.requestDisallowInterceptTouchEvent(false)
            // 处理抬起逻辑
        }
    }
    return super.onTouchEvent(event)
}

事件分发要点:

  • dispatchTouchEvent → 事件入口

  • onInterceptTouchEvent(仅 ViewGroup)→ 是否拦截

  • onTouchEvent → 实际处理

  • 返回 true 表示消费事件,不再向上传递


六、动画与属性更新

kotlin

复制代码
// 方式1:属性动画
ValueAnimator.ofInt(0, 100).apply {
    duration = 2000
    interpolator = DecelerateInterpolator()
    addUpdateListener { animator ->
        setProgress(animator.animatedValue as Int)
    }
    start()
}

// 方式2:配合 ObjectAnimator
var progress: Int = 0
    set(value) {
        field = value
        invalidate()
    }

ObjectAnimator.ofInt(this, "progress", 0, 100).apply {
    duration = 1500
    start()
}

七、性能优化建议

优化项 说明
避免在 onDraw 中创建对象 所有 Paint、Path、Rect 应在 init 或 onSizeChanged 中初始化
使用 invalidate(Rect) 局部重绘 只重绘变化区域,减少绘制工作量
开启硬件加速 android:hardwareAccelerated="true",但注意部分 API 不支持
减少过度绘制 避免多层重叠绘制,使用 clipRect 裁剪
复杂图形使用 Bitmap 缓存 静态内容先绘制到 Bitmap,后续直接 drawBitmap
使用 requestLayout() 谨慎 会触发完整测量-布局-绘制流程,开销大

八、常见问题排查

问题 原因 解决方案
自定义 View 不显示 未重写 onMeasure 且父布局为 wrap_content 重写 onMeasure 设置默认尺寸
wrap_content 无效 未处理 AT_MOST 模式 在 onMeasure 中处理
文字绘制位置偏移 未计算 baseline 使用 Paint.FontMetrics 计算
触摸事件不响应 onTouchEvent 返回 false 返回 true 消费事件
动画卡顿 主线程阻塞或过度绘制 使用 Choreographer,减少绘制层级

九、进阶方向

  1. 自定义 Drawable :实现 Drawable 接口,可复用于多个 View

  2. RenderThread / RenderNode:Android 10+ 的硬件渲染优化

  3. Compose 自定义 :使用 Canvas Modifier 或 Layout Composable

  4. SVG Path 动画 :AnimatedVectorDrawable 实现复杂路径动画

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work4 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work4 天前
ch21 签名、校验与发版
kotlin
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui