安卓Canvas绘图指南:从基础到高级技巧

发布时间:2026/7/18 19:54:17
安卓Canvas绘图指南:从基础到高级技巧 1. 安卓画布基础概念解析在安卓开发中Canvas画布是2D图形绘制的核心组件它就像一块虚拟的画板开发者可以在上面绘制各种图形元素。Canvas类提供了丰富的绘图方法包括绘制点、线、矩形、圆形、文本等基本图形也支持更复杂的路径绘制和位图操作。1.1 Canvas与Paint的关系Canvas和Paint是安卓绘图系统中的黄金搭档它们各司其职Canvas负责画什么what - 定义要绘制的形状和位置Paint负责怎么画how - 定义绘制的样式和外观这种职责分离的设计让绘图逻辑更加清晰。例如当你想绘制一个红色的圆形和一个蓝色的矩形时可以这样操作val redPaint Paint().apply { color Color.RED style Paint.Style.FILL } val bluePaint Paint().apply { color Color.BLUE style Paint.Style.FILL } override fun onDraw(canvas: Canvas) { canvas.drawCircle(100f, 100f, 50f, redPaint) canvas.drawRect(200f, 200f, 300f, 300f, bluePaint) }1.2 坐标系系统理解Canvas的坐标系是绘图的基础。安卓Canvas采用数学中常见的笛卡尔坐标系但有两点关键区别原点(0,0)位于视图的左上角Y轴方向向下为正方向这种设计源于屏幕渲染的惯例但在处理数学图形时需要注意坐标转换。例如要绘制一个位于视图中心的圆override fun onDraw(canvas: Canvas) { val centerX width / 2f val centerY height / 2f canvas.drawCircle(centerX, centerY, 100f, paint) }1.3 绘图的基本流程一个完整的自定义视图绘图流程通常包括以下步骤创建Paint对象并设置样式通常在构造函数或初始化方法中完成在onSizeChanged()中计算与视图尺寸相关的参数在onDraw()中执行实际的绘制操作需要更新视图时调用invalidate()或postInvalidate()这种流程确保了绘图操作的高效性避免了在每次绘制时重复初始化资源。2. 核心绘图方法与技巧2.1 基本图形绘制Canvas提供了一系列绘制基本图形的方法掌握这些方法是进行复杂绘图的基础绘制线条canvas.drawLine(startX, startY, endX, endY, paint)绘制矩形// 通过四个坐标值定义 canvas.drawRect(left, top, right, bottom, paint) // 通过RectF对象定义 val rect RectF(left, top, right, bottom) canvas.drawRect(rect, paint)绘制圆形canvas.drawCircle(centerX, centerY, radius, paint)绘制椭圆val oval RectF(left, top, right, bottom) canvas.drawOval(oval, paint)绘制圆弧canvas.drawArc( rectF, // 定义椭圆边界 startAngle, // 起始角度0度为3点钟方向 sweepAngle, // 扫过的角度 useCenter, // 是否连接到中心点 paint )2.2 路径(Path)绘制对于更复杂的图形Path类提供了强大的支持。Path可以记录一系列直线和曲线形成任意形状val path Path().apply { moveTo(100f, 100f) // 移动到起点 lineTo(200f, 100f) // 画直线到(200,100) quadTo(250f, 150f, 200f, 200f) // 画二次贝塞尔曲线 cubicTo(150f, 250f, 100f, 250f, 100f, 200f) // 画三次贝塞尔曲线 close() // 闭合路径 } canvas.drawPath(path, paint)Path的高级用法包括布尔运算union, intersect等路径效果PathEffect测量路径长度和位置沿路径绘制文本2.3 文本绘制Canvas提供了灵活的文本绘制能力// 基本文本绘制 canvas.drawText(Hello Android, x, y, paint) // 沿路径绘制文本 val path Path().apply { addCircle(centerX, centerY, radius, Path.Direction.CW) } canvas.drawTextOnPath(Circular Text, path, 0f, 0f, paint)文本绘制的关键点在于基线(baseline)的理解。Y坐标参数指定的是文本基线的位置而不是文本顶部或底部。要精确控制文本位置可以使用Paint的getFontMetrics()方法获取字体度量信息。2.4 位图绘制Canvas可以绘制位图并支持各种变换val bitmap BitmapFactory.decodeResource(resources, R.drawable.my_image) canvas.drawBitmap(bitmap, left, top, paint) // 使用矩阵变换 val matrix Matrix().apply { postScale(0.5f, 0.5f) // 缩放50% postRotate(45f) // 旋转45度 } canvas.drawBitmap(bitmap, matrix, paint)对于性能敏感的绘图操作建议使用Bitmap.createBitmap()创建合适大小的位图并在其上绘制最后一次性绘制到Canvas上。3. 高级绘图技术与优化3.1 图层(Layer)操作Canvas支持图层操作可以实现复杂的混合效果val saveCount canvas.saveLayer( left, top, right, bottom, paint, Canvas.ALL_SAVE_FLAG ) try { // 在图层上绘制 canvas.drawCircle(centerX, centerY, radius, paint) } finally { canvas.restoreToCount(saveCount) // 恢复图层 }图层操作虽然强大但会消耗较多内存应谨慎使用。在Android 8.0及以上版本可以考虑使用RenderNode替代部分图层操作。3.2 硬件加速与软件绘制安卓绘图系统支持两种渲染模式硬件加速默认启用使用GPU进行绘制性能更高软件绘制使用CPU进行绘制兼容性更好某些Canvas操作在硬件加速模式下不受支持或表现不同。可以通过以下方式控制!-- 在AndroidManifest.xml中全局控制 -- application android:hardwareAcceleratedtrue !-- 在Activity级别控制 -- activity android:hardwareAcceleratedfalse !-- 在View级别控制 -- myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null)3.3 绘图性能优化高效的绘图对应用流畅性至关重要以下是一些优化建议避免在onDraw()中分配对象这会导致频繁的垃圾回收引起卡顿。应该在初始化时创建Paint等对象并复用。使用clipRect()限制绘制区域只绘制需要更新的部分减少不必要的绘制操作。考虑使用SurfaceView或TextureView对于频繁更新的复杂绘图如游戏这些视图类型在独立线程中绘制不会阻塞UI线程。合理使用invalidate()只重绘需要更新的区域可以使用invalidate(left, top, right, bottom)指定脏区域。预计算绘制参数在onSizeChanged()中计算与尺寸相关的参数避免在onDraw()中重复计算。3.4 现代绘图技术Android 12引入了RenderEffect为视图添加各种图形效果// 添加模糊效果 view.setRenderEffect( RenderEffect.createBlurEffect( radiusX, radiusY, Shader.TileMode.CLAMP ) ) // 组合效果 val blur RenderEffect.createBlurEffect(10f, 10f, Shader.TileMode.CLAMP) val colorFilter RenderEffect.createColorFilterEffect( ColorMatrixColorFilter(colorMatrix) ) view.setRenderEffect(RenderEffect.createChainEffect(blur, colorFilter))这些效果可以应用于整个视图层次结构实现复杂的视觉效果而无需自定义绘制代码。4. 实战案例创建自定义图表视图4.1 需求分析与设计假设我们需要创建一个自定义的饼图视图要求显示多个数据项的比例支持点击切换选中项显示当前选中项的标签有平滑的动画效果4.2 实现步骤4.2.1 数据结构设计data class PieChartItem( val label: String, val value: Float, ColorInt val color: Int, var startAngle: Float 0f, var endAngle: Float 0f, var selected: Boolean false )4.2.2 初始化与尺寸计算class PieChartView JvmOverloads constructor( context: Context, attrs: AttributeSet? null, defStyleAttr: Int 0 ) : View(context, attrs, defStyleAttr) { private val items mutableListOfPieChartItem() private val bounds RectF() private var centerX 0f private var centerY 0f private var radius 0f private val textPaint Paint(Paint.ANTI_ALIAS_FLAG).apply { textSize 48f color Color.BLACK } private val selectedPaint Paint(Paint.ANTI_ALIAS_FLAG).apply { style Paint.Style.STROKE strokeWidth 8f color Color.WHITE } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) val padding 20f val size min(width, height) - 2 * padding centerX width / 2f centerY height / 2f radius size / 2f bounds.set( centerX - radius, centerY - radius, centerX radius, centerY radius ) calculateAngles() } private fun calculateAngles() { val total items.sumOf { it.value.toDouble() }.toFloat() var startAngle 0f items.forEach { item - val sweepAngle 360f * item.value / total item.startAngle startAngle item.endAngle startAngle sweepAngle startAngle sweepAngle } } }4.2.3 绘制实现override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (items.isEmpty()) return val selectedItem items.find { it.selected } ?: items[0] // 绘制饼图切片 items.forEach { item - val paint Paint(Paint.ANTI_ALIAS_FLAG).apply { color item.color style Paint.Style.FILL } canvas.drawArc( bounds, item.startAngle - 90, // 从12点方向开始 item.endAngle - item.startAngle, true, paint ) // 绘制选中效果 if (item.selected) { canvas.drawArc( bounds, item.startAngle - 90, item.endAngle - item.startAngle, true, selectedPaint ) } } // 绘制选中项标签 val label ${selectedItem.label}: ${selectedItem.value} val textWidth textPaint.measureText(label) canvas.drawText( label, centerX - textWidth / 2, centerY radius 60, textPaint ) }4.2.4 交互处理override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN - { handleTouch(event.x, event.y) return true } } return super.onTouchEvent(event) } private fun handleTouch(x: Float, y: Float) { // 转换为相对于圆心的坐标 val touchX x - centerX val touchY y - centerY val distance sqrt(touchX * touchX touchY * touchY) // 检查是否点击在饼图范围内 if (distance radius) return // 计算点击角度 var angle Math.toDegrees(atan2(touchY.toDouble(), touchX.toDouble())).toFloat() if (angle 0) angle 360f // 找到对应的数据项 val adjustedAngle (angle 90) % 360 // 转换为从12点方向开始计算 items.forEach { item - item.selected adjustedAngle item.startAngle adjustedAngle item.endAngle } invalidate() // 触发重绘 }4.2.5 添加动画效果private var animator: ValueAnimator? null fun setData(newItems: ListPieChartItem) { // 取消正在进行的动画 animator?.cancel() // 保存当前角度状态 val oldItems items.toList() items.clear() items.addAll(newItems) calculateAngles() // 设置动画 animator ValueAnimator.ofFloat(0f, 1f).apply { duration 500 interpolator AccelerateDecelerateInterpolator() addUpdateListener { animation - val progress animation.animatedValue as Float // 插值计算中间角度状态 oldItems.forEachIndexed { index, oldItem - val newItem items[index] items[index] newItem.copy( startAngle lerp(oldItem.startAngle, newItem.startAngle, progress), endAngle lerp(oldItem.endAngle, newItem.endAngle, progress) ) } invalidate() } start() } } private fun lerp(start: Float, end: Float, progress: Float): Float { return start (end - start) * progress }4.3 使用示例val pieChart findViewByIdPieChartView(R.id.pie_chart) val items listOf( PieChartItem(Android, 40f, Color.BLUE), PieChartItem(iOS, 30f, Color.RED), PieChartItem(Web, 20f, Color.GREEN), PieChartItem(Other, 10f, Color.GRAY) ) pieChart.setData(items)4.4 性能优化建议避免过度绘制在自定义视图中确保只绘制必要的内容。可以使用Canvas的quickReject()方法提前判断是否需要绘制某些区域。使用缓存对于复杂的静态内容可以考虑使用Bitmap缓存绘制结果。优化动画使用属性动画而非自己处理时间逻辑利用硬件加速层优化动画性能。减少透明度使用透明度和混合模式会增加GPU的工作量在可能的情况下尽量避免。使用合适的视图类型对于频繁更新的内容考虑使用SurfaceView或TextureView替代普通View。通过这个完整的案例我们展示了如何从零开始创建一个功能完善的自定义图表视图涵盖了Canvas绘图的各个方面包括基本图形绘制、交互处理、动画效果和性能优化等关键知识点。