Custom View — 是通过继承View类或其子类(Button、TextView、ImageView)并重写关键方法(onDraw用于绘制、onMeasure用于测量尺寸、onTouchEvent用于处理触摸)创建的自定义界面组件。根据Google Android Developer Guide(2024),当标准Android SDK组件无法提供所需行为或外观时,就会使用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主题。
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 — 是View根据传入的MeasureSpec(父级约束)向系统传达其期望尺寸的方法。没有正确的onMeasure实现,View在wrap_content时可能高度为零,或在match_parent时占据整个屏幕区域。
MeasureSpec由模式(EXACTLY、AT_MOST、UNSPECIFIED)和值组成。EXACTLY — 父级指定了精确尺寸(match_parent或固定值)。AT_MOST — 父级指定了最大值(wrap_content)。UNSPECIFIED — 无限制(ScrollView、ListView)。
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 — Custom View的核心。每次View重绘时,Android都会调用此方法。它接收一个Canvas对象,通过它执行所有绘制:几何图形、文本、路径、位图和动画。
Canvas API为所有基本操作提供方法:drawCircle、drawRect、drawLine、drawPath、drawText、drawBitmap、drawArc、drawOval。用于配置样式的Paint对象定义颜色、粗细、填充样式、字体、阴影和效果。
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()触发重绘。
自定义属性允许像标准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。
<!-- 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()以释放资源。
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 — 每次触摸View时调用的方法。它接收一个MotionEvent对象,包含事件类型(ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL)、坐标、压力和手指数量(多点触控)的信息。
为了处理复杂手势(滑动、缩放、长按),请使用GestureDetector或ScaleGestureDetector与onTouchEvent结合。GestureDetector简化了onSingleTapUp、onFling、onLongPress、onDoubleTap和其他标准手势的识别。
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如果onDraw被频繁调用(动画、滚动)或包含重型操作,可能成为性能瓶颈。Android提供了几种优化自定义组件绘制的机制。
View.setLayerType允许将View的渲染切换到软件(LAYER_TYPE_SOFTWARE)或硬件(LAYER_TYPE_HARDWARE)图层。软件图层适用于硬件加速不支持的复杂Canvas图形 — 例如drawTextOnPath或复杂Path效果。
invalidate(Rect) — 仅重绘View的指定区域,而非整个组件。这对于大型Custom View(图形、地图、绘图画布)至关重要,因为每帧的完整重绘会导致FPS下降。使用postInvalidateOnAnimation()与vsync同步。
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 | 定位子View | onMeasure之后 |
| onTouchEvent | 处理触摸 | 每次触摸时 |
| onSizeChanged | 响应尺寸变化 | 首次布局和变化时 |
常见问题
Custom View(Canvas API)在需要最大自定义绘制性能(图形、视频编辑器、地图)、与现有基于View的代码集成或支持API 21以下Android版本时是合理的。Jetpack Compose是大多数新项目的现代方法,使用声明式UI,更适合动态界面。
是的,如果View在XML代码中使用wrap_content。没有onMeasure,wrap_content将像match_parent一样工作,因为View.onMeasure的默认实现不设置默认大小。如果View始终具有固定尺寸或match_parent,onMeasure可以跳过。
使用ValueAnimator或ObjectAnimator更改View的属性(颜色、半径、位置),并在动画回调中调用invalidate()进行重绘。ValueAnimator在主线程上工作,与vsync同步。对于复杂动画(物理、粒子),使用Choreographer.FrameCallback或Android Animation Framework。
在onTouchEvent中使用MotionEvent.getPointerCount()确定手指数量,使用getPointerId(i)通过唯一ID跟踪每个手指。ACTION_POINTER_DOWN / ACTION_POINTER_UP — 手指添加/移除事件。对于缩放,使用ScaleGestureDetector与onTouchEvent配合 — 它简化了捏合缩放的检测。
可以,通过AndroidView — 一个composable函数,将基于View的元素插入Compose层次结构。AndroidView接收创建View的工厂和用于状态同步的update回调。这允许在无需完全重写的情况下在Compose项目中重用现有的Custom View。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。