实现一个简单的画图APP,通过自定义View 实现
相关官网文档参考:https://blog.youkuaiyun.com/whjk20/article/details/115666165
目录
1. Canvas基本概念 (Canvas/Bitmap/Paint/Clip)
onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int)
1. Canvas基本概念 (Canvas/Bitmap/Paint/Clip)
- Canvas(画布,帆布) Paint(涂料)
- 官方文档介绍:
https://developer.android.com/reference/android/graphics/Canvas.html
The Canvas class holds the "draw" calls. To draw something, you need 4 basic components:
A Bitmap to hold the pixels, a Canvas to host the draw calls (writing into the bitmap),
a drawing primitive (e.g. Rect, Path, text, Bitmap), and a paint (to describe the colors and styles for the drawing).
Bitmap 保存像素点, Canvas 定义在屏幕画的形状, Paint 定义画的颜色、风格、fund?, clip定义哪部分是可见的
主要方法
onDraw(Canvas canvas)
在画布上绘制,需要缓存所画的内容,以提升性能 (缓存canvas , bitmap); 调用invalidate() 则会回调方法以更新界面。
onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int)
屏幕分辩率变化时回调,在这里创建基于新尺寸的Bitmap 以及 Canvas 已经一些初始化
Path()
画的轨迹 : https://developer.android.com/reference/kotlin/android/graphics/Path.html
onTouchEvent()
记录触摸的x, y坐标,响应按下(ACTION_DOWN) / 移动(ACTION_MOVE)/抬起 (ACTION_UP) 操作
2. 问题
(1) 无法全屏(仍有状态栏statusbar) myCanvasView.systemUiVisibility = SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
package com.example.minipaint
import android.content.Context
import android.graphics.*
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.core.content.res.ResourcesCompat
import kotlin.math.abs
private const val STROKE_WIDTH = 12f // has to be float
class MyCanvasView(context: Context) : View(context) {
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f
private var currentX = 0f
private var currentY = 0f
//阈值:大于这个值才认为是滑动,进而画出轨迹
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop
//显示一个边框, 实际上超出这个边界也是可以画,仅仅是显示边框而已
private lateinit var frame: Rect
// Set up the paint with which to draw. 画笔的样式
private val paint = Paint().apply {
color = drawColor
// Smooths out edges of what is drawn without affecting shape.
isAntiAlias = true
// Dithering affects how colors with higher-precision than the device are down-sampled.
isDither = true
style = Paint.Style.STROKE // default: FILL
strokeJoin = Paint.Join.ROUND // default: MITER
strokeCap = Paint.Cap.ROUND // default: BUTT
strokeWidth = STROKE_WIDTH // default: Hairline-width (really thin)
}
private var path = Path() //轨迹
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// 因为每次大小变化,都需要创建新的bitmap, 但是旧的也要回收,否则内存泄露
if (::extraBitmap.isInitialized) extraBitmap.recycle()
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
extraCanvas = Canvas(extraBitmap)
extraCanvas.drawColor(backgroundColor)
// Calculate a rectangular frame around the picture.
val inset = 40
frame = Rect(inset, inset, width - inset, height - inset) // todo 为何左边没有设置,都会有框??
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
motionTouchEventX = event.x
motionTouchEventY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> touchStart()
MotionEvent.ACTION_MOVE -> touchMove()
MotionEvent.ACTION_UP -> touchUp()
}
return true
}
private fun touchStart() {
path.reset()
path.moveTo(motionTouchEventX, motionTouchEventY)
currentX = motionTouchEventX
currentY = motionTouchEventY
}
private fun touchMove() {
val dx = abs(motionTouchEventX - currentX)
val dy = abs(motionTouchEventY - currentY)
if (dx >= touchTolerance || dy >= touchTolerance) {
// QuadTo() adds a quadratic bezier from the last point,
// approaching control point (x1,y1), and ending at (x2,y2).
path.quadTo(
currentX,
currentY,
(motionTouchEventX + currentX) / 2,
(motionTouchEventY + currentY) / 2
) //低阶法计算数值积分 todo
currentX = motionTouchEventX
currentY = motionTouchEventY
extraCanvas.drawPath(path, paint)
}
invalidate()
}
private fun touchUp() {
// Reset the path so it doesn't get drawn again.
path.reset()
}
}
其中,
<color name="colorBackground">#FFFF5500</color>
<color name="colorPaint">#FFFFEB3B</color>
<string name="canvasContentDescription">Mini Paint is a simple line drawing app.
Drag your fingers to draw. Rotate the phone to clear.</string>
extraCanvas 缓存画布, 如何绘制(draw)到当前视图(View)上??
----> 通过Bitmap, 即不断的往Bitmap中添加内容(使用一个缓存的extraCanvas),然后在View.onDraw 时,使用Canvas 绘制出来
仅仅是在onSizeChanged 时调用 extraCanvas.drawColor, 响应触摸时,调用extraCanvas.drawPath(path, paint)
那么在 onDraw() 时调用 canvas.drawBitmap(extraBitmap, 0f,0f, null) 这个有什么用??? ---> 如果没有,则无背景颜色
3. 日志分析
//第一次启动进入,此时的缓存extraCanvas
04-13 09:48:55.759 I MyCanvasView: onSizeChanged extraCanvas=android.graphics.Canvas@981f35b
//此时的onDraw回调的canvas
04-13 09:48:55.782 I MyCanvasView: onDraw Call canvas=android.graphics.RecordingCanvas@7dc87f8
//之后触摸屏幕后,extraCanvas.drawPath()完成后,刷新界面
//此时回调onDraw 传进来的canvas, 既不是缓存canvas, 也不是第一次回调onDraw的canvas (但是后面触摸后回调onDraw,传入的是同一个Canvas对象)
04-13 09:49:05.992 I MyCanvasView: onDraw Call canvas=android.graphics.RecordingCanvas@6ec4b0e
04-13 09:49:06.026 I MyCanvasView: onDraw Call canvas=android.graphics.RecordingCanvas@6ec4b0e
4. 结论:
1. 需要一个保存像素的位图 (缓存的Bitmap)
2. 在回调onDraw(canvas:Canvas) 时,通过这个canvas 画出这个位图 (Canvas.drawBitmap), 同时可以其它 drawXXX 画形状操作
3. 画笔(Paint) 可以装饰画的属性(颜色、风格等)
4. 创建的缓存Canvas(通过一个缓存Bitmap创建), 只要有绘制内容,在调用 View 的invalidate()时,就回执行步骤2