Android中的Custom View — 绘图与事件处理

作者: IT Sectr 发布日期: 2026-07-20 阅读时间: 9 分钟

Custom View — 是通过继承View类或其子类(Button、TextView、ImageView)并重写关键方法(onDraw用于绘制、onMeasure用于测量尺寸、onTouchEvent用于处理触摸)创建的自定义界面组件。根据Google Android Developer Guide(2024),当标准Android SDK组件无法提供所需行为或外观时,就会使用Custom View — 例如,用于自定义动画、非标准形状、专业图表和游戏元素。

要点

  • Custom View — 通过继承View或其子类并重写onDraw、onMeasure、onTouchEvent创建。
  • 通过Canvas API进行绘制 — 几何图形、文本、路径、位图和动画。
  • onMeasure负责View的正确尺寸,考虑布局参数和内边距。
  • 自定义XML属性在res/values/attrs.xml中声明,在View构造函数中读取。
  • onTouchEvent触摸处理允许添加手势、拖放和多点触控支持。

Android中的Custom View是什么?

Custom View — 是一个继承android.view.View(或其子类之一)并重写系统方法以实现自己绘制、测量和事件处理逻辑的类。Custom View是Android创建独特UI组件的基本机制。

Android提供了两种创建Custom View的方法:继承View(通过onDraw完全自定义绘制)和继承现有的View子类(例如,重写Button或TextView以在保持基本行为的同时添加功能)。

基础构造函数Custom View必须接受Context和AttributeSet — 这允许系统从XML代码创建View。如果View仅从代码使用,带Context的构造函数就足够了。带样式的第三个构造函数用于支持Android主题。

kotlin
class CustomView
    @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.BLUE
        strokeWidth = 4f
        style = Paint.Style.FILL
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawCircle(width / 2f,
            height / 2f,
            50f, paint)
    }
}

最小Custom View由构造函数和onDraw组成。然而,为了在Android布局系统中正常工作,还需要重写onMeasure — 否则View可能以零尺寸显示,或对wrap_content布局参数做出错误反应。

根据Google I/O 2023,自定义View在Google Play前100名应用中的65%中使用。创建Custom View的最常见原因:自定义动画(35%)、自定义图表(25%)、专业控件元素(20%)和品牌组件(20%)。

onMeasure和onLayout:尺寸管理

onMeasure — 是View根据传入的MeasureSpec(父级约束)向系统传达其期望尺寸的方法。没有正确的onMeasure实现,View在wrap_content时可能高度为零,或在match_parent时占据整个屏幕区域。

MeasureSpec由模式(EXACTLY、AT_MOST、UNSPECIFIED)和值组成。EXACTLY — 父级指定了精确尺寸(match_parent或固定值)。AT_MOST — 父级指定了最大值(wrap_content)。UNSPECIFIED — 无限制(ScrollView、ListView)。

kotlin
override fun onMeasure(
    widthMeasureSpec: Int,
    heightMeasureSpec: Int
) {
    val desiredWidth = paddingLeft + paddingRight
        + DEFAULT_WIDTH
    val desiredHeight = paddingTop + paddingBottom
        + DEFAULT_HEIGHT

    val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)
    val modeWidth = MeasureSpec.getMode(widthMeasureSpec)

    val resultWidth = when (modeWidth) {
        MeasureSpec.EXACTLY -> measuredWidth
        MeasureSpec.AT_MOST ->
            desiredWidth.coerceAtMost(measuredWidth)
        else -> desiredWidth
    }

    val resultHeight = resolveSize(desiredHeight,
        heightMeasureSpec)

    setMeasuredDimension(resultWidth, resultHeight)
}

resolveSize — 是一个简化onMeasure实现的Android实用方法。它接收期望尺寸和MeasureSpec,并返回正确值:对于EXACTLY — 精确尺寸,对于AT_MOST — 期望值和最大值中的最小值,对于UNSPECIFIED — 期望尺寸。

onLayout对于View(非ViewGroup)通常不重写 — 由父级调用以定位子元素。对于ViewGroup,onLayout是必需的 — 其中必须为每个子View调用layout()。

onDraw和Canvas API:绘制

onDraw — Custom View的核心。每次View重绘时,Android都会调用此方法。它接收一个Canvas对象,通过它执行所有绘制:几何图形、文本、路径、位图和动画。

Canvas API为所有基本操作提供方法:drawCircle、drawRect、drawLine、drawPath、drawText、drawBitmap、drawArc、drawOval。用于配置样式的Paint对象定义颜色、粗细、填充样式、字体、阴影和效果。

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

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

    // 文本
    canvas.drawText("自定义视图",
        paddingLeft.toFloat(),
        paddingTop.toFloat() + textPaint.textSize,
        textPaint)

    // 圆形
    canvas.drawCircle(
        width / 2f,
        height / 2f,
        radius, circlePaint)

    // 路径(贝塞尔曲线)
    val path = Path().apply {
        moveTo(0f, height.toFloat())
        quadTo(width / 2f, 0f,
            width.toFloat(), height.toFloat())
    }
    canvas.drawPath(path, pathPaint)
}

Paint — 是绘制样式的配置器。抗锯齿(Paint.ANTI_ALIAS_FLAG)对于平滑边缘是必需的。亚像素文本(SUBPIXEL_TEXT_FLAG)可提高文本质量。Style.FILL、STROKE和FILL_AND_STROKE确定图形是填充、描边还是两者兼具。

重要提示:不要在onDraw中创建Paint、Path、Rect对象 — 这会导致绘制循环中的内存分配并触发垃圾回收,从而降低FPS。在构造函数或init方法中创建所有绘制对象。对于动画,请使用ValueAnimator或ObjectAnimator配合invalidate()触发重绘。

attrs.xml中的自定义XML属性

自定义属性允许像标准android:layout_width或android:background一样从XML代码配置Custom View。属性在res/values/attrs.xml文件中声明,包含名称、类型和可选的默认值。

属性类型:string、integer、float、boolean、color、dimension、enum、flag、fraction、reference(资源引用)。对于每种类型,Android自动从XML解析值并将其传递给TypedArray。

xml
<!-- res/values/attrs.xml -->
<resources>
    <declare-styleable name="CustomView">
        <attr name="circleColor"
            format="color" />
        <attr name="circleRadius"
            format="dimension" />
        <attr name="labelText"
            format="string" />
        <attr name="showAnimation"
            format="boolean" />
    </declare-styleable>
</resources>

在Custom View构造函数中,通过context.obtainStyledAttributes读取属性,它返回TypedArray。TypedArray提供类型化访问方法:getColor、getDimension、getString、getBoolean、getInt。读取后,必须在TypedArray上调用recycle()以释放资源。

kotlin
init {
    val typedArray = context.obtainStyledAttributes(
        attrs,
        R.styleable.CustomView
    )

    circleColor = typedArray.getColor(
        R.styleable.CustomView_circleColor,
        Color.BLUE
    )
    circleRadius = typedArray.getDimension(
        R.styleable.CustomView_circleRadius,
        50f
    )
    labelText = typedArray.getString(
        R.styleable.CustomView_labelText
    ) ?: "Default"

    typedArray.recycle()
}

在XML中使用:添加app命名空间(xmlns:app="http://schemas.android.com/apk/res-auto")并使用自定义属性如app:circleColor="@color/red"。如果attrs.xml中的声明正确,Android Studio将自动补全并检查属性类型。

使用onTouchEvent处理触摸

onTouchEvent — 每次触摸View时调用的方法。它接收一个MotionEvent对象,包含事件类型(ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL)、坐标、压力和手指数量(多点触控)的信息。

为了处理复杂手势(滑动、缩放、长按),请使用GestureDetector或ScaleGestureDetector与onTouchEvent结合。GestureDetector简化了onSingleTapUp、onFling、onLongPress、onDoubleTap和其他标准手势的识别。

kotlin
private val gestureDetector = GestureDetector(
    context, object : GestureDetector.SimpleOnGestureListener() {
    override fun onSingleTapUp(e: MotionEvent): Boolean {
        handleTap(e.x, e.y)
        return true
    }
})

override fun onTouchEvent(event: MotionEvent): Boolean {
    val handled = gestureDetector.onTouchEvent(event)

    when (event.action) {
        MotionEvent.ACTION_MOVE -> {
            currentX = event.x
            currentY = event.y
            invalidate()
            return true
        }
    }

    return handled || super.onTouchEvent(event)
}

命中测试 — 确定触摸是否命中View的特定区域。对于矩形区域,使用Rect.contains(x, y)。对于圆形 — 检查距中心的距离:sqrt(dx^2 + dy^2) < radius。对于任意形状 — Path.op()或Region.contains()。

多点触控通过MotionEvent.getPointerCount()和getPointerId(index)处理。每个手指获得一个从ACTION_DOWN到ACTION_POINTER_UP保持的唯一ID。ACTION_MOVE可能包含所有活动手指的数据 — 使用getHistoricalX/Y进行运动插值。

Custom View性能优化

Custom View如果onDraw被频繁调用(动画、滚动)或包含重型操作,可能成为性能瓶颈。Android提供了几种优化自定义组件绘制的机制。

复杂绘制的图层类型

View.setLayerType允许将View的渲染切换到软件(LAYER_TYPE_SOFTWARE)或硬件(LAYER_TYPE_HARDWARE)图层。软件图层适用于硬件加速不支持的复杂Canvas图形 — 例如drawTextOnPath或复杂Path效果。

仅使需要区域失效

invalidate(Rect) — 仅重绘View的指定区域,而非整个组件。这对于大型Custom View(图形、地图、绘图画布)至关重要,因为每帧的完整重绘会导致FPS下降。使用postInvalidateOnAnimation()与vsync同步。

kotlin
class EfficientCustomView
    @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : View(context, attrs) {

    // 在构造函数中一次性创建的对象
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val rect = Rect()
    private val position = PointF()

    fun updatePosition(x: Float, y: Float) {
        position.set(x, y)
        // 仅重绘点周围的区域
        rect.set(
            (x - 10).toInt(),
            (y - 10).toInt(),
            (x + 10).toInt(),
            (y + 10).toInt()
        )
        invalidate(rect)
    }
}

硬件加速在Android 3.0+(API 11+)上默认启用。drawCircle、drawRect、drawBitmap等Canvas操作是硬件加速的,在GPU上执行。然而,drawTextOnPath、drawVertices、复杂裁剪操作不加速 — 对它们使用LAYER_TYPE_SOFTWARE。

根据Android Performance Patterns(Google,2023),Custom View卡顿的主要原因包括:onDraw中的内存分配(每帧创建对象)、更改一个元素时重绘整个View以及静态内容缺少LayerType.HARDWARE。修复这三个问题在大多数场景中将FPS从30提升到60。

方法用途调用频率
onDraw绘制View内容每次invalidate()时
onMeasure确定View尺寸布局更改时
onLayout定位子ViewonMeasure之后
onTouchEvent处理触摸每次触摸时
onSizeChanged响应尺寸变化首次布局和变化时

常见问题

何时使用Custom View而非Compose?

Custom View(Canvas API)在需要最大自定义绘制性能(图形、视频编辑器、地图)、与现有基于View的代码集成或支持API 21以下Android版本时是合理的。Jetpack Compose是大多数新项目的现代方法,使用声明式UI,更适合动态界面。

是否必须重写onMeasure?

是的,如果View在XML代码中使用wrap_content。没有onMeasure,wrap_content将像match_parent一样工作,因为View.onMeasure的默认实现不设置默认大小。如果View始终具有固定尺寸或match_parent,onMeasure可以跳过。

如何制作Custom View动画?

使用ValueAnimator或ObjectAnimator更改View的属性(颜色、半径、位置),并在动画回调中调用invalidate()进行重绘。ValueAnimator在主线程上工作,与vsync同步。对于复杂动画(物理、粒子),使用Choreographer.FrameCallback或Android Animation Framework。

如何在Custom View中处理多点触控?

onTouchEvent中使用MotionEvent.getPointerCount()确定手指数量,使用getPointerId(i)通过唯一ID跟踪每个手指。ACTION_POINTER_DOWN / ACTION_POINTER_UP — 手指添加/移除事件。对于缩放,使用ScaleGestureDetector与onTouchEvent配合 — 它简化了捏合缩放的检测。

能否在Jetpack Compose中使用Custom View?

可以,通过AndroidView — 一个composable函数,将基于View的元素插入Compose层次结构。AndroidView接收创建View的工厂和用于状态同步的update回调。这允许在无需完全重写的情况下在Compose项目中重用现有的Custom View。

总结

  • Custom View — 通过继承View并重写onDraw、onMeasure和onTouchEvent进行自定义绘制和处理的自定义Android组件。
  • onDraw和Canvas API — 绘制的基础:几何图形、文本、位图、路径 — 通过Paint配置颜色、样式和字体。
  • onMeasure对wrap_content的正确工作很重要;使用MeasureSpec(EXACTLY、AT_MOST、UNSPECIFIED)和resolveSize计算尺寸。
  • 自定义属性通过attrs.xml和TypedArray允许从XML代码配置Custom View,在Android Studio中具有自动补全支持。
  • onTouchEvent与GestureDetector提供触摸、手势和多点触控处理,命中测试确定交互区域。
  • 性能:在构造函数中一次性创建对象,使用invalidate(Rect)进行部分重绘,为复杂效果使用LayerType。
  • Custom View通过AndroidView与Jetpack Compose集成,允许在Compose项目中重用现成的组件。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读