Jetpack Compose中Canvas绘制

简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.copy
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput


@Composable
fun DrawingBoard(modifier: Modifier) {
val drawPaths = remember { mutableStateListOf<DrawPath>() }
var currentPath by remember { mutableStateOf(Path()) }
val currentColor = Color.Black
val currentStrokeWidth = 2f

Box(modifier = modifier) {
Canvas(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
currentPath = Path().apply { moveTo(offset.x, offset.y) }
},
onDrag = { change, _ ->
currentPath.lineTo(change.position.x, change.position.y)
currentPath = currentPath.copy()
},
onDragEnd = {
drawPaths.add(
DrawPath(
currentPath,
currentColor,
currentStrokeWidth
)
)
currentPath = Path() // 重置当前路径
}
)
}
) {
// 绘制所有已完成的路径
drawPaths.forEach { drawPath ->
drawPath(
path = drawPath.path,
color = drawPath.color,
style = Stroke(width = drawPath.strokeWidth)
)
}
// 绘制当前正在绘制的路径
drawPath(
path = currentPath,
color = currentColor,
style = Stroke(width = currentStrokeWidth)
)
}
}
}

data class DrawPath(val path: Path, val color: Color, val strokeWidth: Float)

带控制类

如果我们想在组件内控制画板的绘制,比如回退、清空等。

这就需要添加控制类来实现。

画板类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.copy
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput

data class DrawPath(val path: Path, val color: Color, val strokeWidth: Float)
class DrawingController() {
val paths = mutableStateListOf<DrawPath>()

val undoList = mutableStateListOf<DrawPath>()

val currentColor = Color.Black
val currentStrokeWidth = 2f

// 撤销上一步(内部操作方法)
fun undo() {
if (paths.isNotEmpty()) {
undoList.add(paths.get(paths.size - 1))
paths.remove(paths.get(paths.size - 1))
}
}

fun redo() {
if (undoList.size > 0) {
val last = undoList.get(undoList.size - 1)
paths.add(last)
undoList.remove(last)
}
}

// 清空画板
fun clear() {
paths.clear()
}

fun addPath(path: DrawPath) {
paths.add(path)
undoList.clear()
}

// 获取当前路径(供绘制使用)
fun getPaths(): List<DrawPath> = paths.toList()
}


@Composable
fun DrawingBoard(controller: DrawingController, modifier: Modifier) {
var currentPath by remember { mutableStateOf(Path()) }
// 监听控制器数据变化,触发重绘
val paths by remember { derivedStateOf { controller.getPaths() } }
Box(modifier = modifier) {
Canvas(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
currentPath = Path().apply { moveTo(offset.x, offset.y) }
},
onDrag = { change, _ ->
currentPath.lineTo(change.position.x, change.position.y)
currentPath = currentPath.copy()
},
onDragEnd = {
controller.addPath(
DrawPath(
currentPath,
controller.currentColor,
controller.currentStrokeWidth
)
)

currentPath = Path() // 重置当前路径
}
)
}
) {
// 绘制所有已完成的路径
paths.forEach { drawPath ->
drawPath(
path = drawPath.path,
color = drawPath.color,
style = Stroke(width = drawPath.strokeWidth)
)
}
// 绘制当前正在绘制的路径
drawPath(
path = currentPath,
color = controller.currentColor,
style = Stroke(width = controller.currentStrokeWidth)
)
}
}
}

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Box(
Modifier.fillMaxSize()
) {
val drawController = remember { DrawingController() }
DrawingBoard(
drawController,
Modifier.fillMaxSize()
)

Row() {
Box(
Modifier
.size(20.dp)
.clickable {
drawController.undo()
}) {
ImgLocalBg(R.drawable.draw_undo)
}
Spacer(Modifier.width(4.dp))
Box(
Modifier
.size(20.dp)
.clickable {
drawController.redo()
}) {
ImgLocalBg(R.drawable.draw_redo)
}
Spacer(Modifier.width(4.dp))
Box(
Modifier
.size(20.dp)
.clickable {
drawController.clear()
}) {
ImgLocalBg(R.drawable.draw_clean)
}
}
}

画板保存

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import android.graphics.Bitmap
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.asAndroidPath
import androidx.compose.ui.graphics.copy
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged

data class DrawPath(val path: Path, val color: Color, val strokeWidth: Float)
class DrawingController() {
val paths = mutableStateListOf<DrawPath>()

val undoList = mutableStateListOf<DrawPath>()

// 画板的尺寸
var boardSize = mutableStateOf(Pair(0, 0))

val currentColor = Color.Black
val currentStrokeWidth = 2f

// 撤销上一步(内部操作方法)
fun undo() {
if (paths.isNotEmpty()) {
undoList.add(paths.get(paths.size - 1))
paths.remove(paths.get(paths.size - 1))
}
}

fun redo() {
if (undoList.size > 0) {
val last = undoList.get(undoList.size - 1)
paths.add(last)
undoList.remove(last)
}
}

// 清空画板
fun clear() {
paths.clear()
}

fun addPath(path: DrawPath) {
paths.add(path)
undoList.clear()
}

// 获取当前路径(供绘制使用)
fun getPaths(): List<DrawPath> = paths.toList()

// 生成Bitmap(核心方法)
fun createBitmap(): Bitmap? {
val width = boardSize.value.first
val height = boardSize.value.second

if (width <= 0 || height <= 0) return null

return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
val canvas = android.graphics.Canvas(this)
canvas.drawColor(android.graphics.Color.WHITE) // 白色背景
getPaths().forEach { pathItem ->
// 绘制所有路径
val paint = android.graphics.Paint().apply {
color = pathItem.color.toArgb()
strokeWidth = pathItem.strokeWidth
style = android.graphics.Paint.Style.STROKE
isAntiAlias = true
}
canvas.drawPath(pathItem.path.asAndroidPath(), paint)
}
}
}
}


@Composable
fun DrawingBoard(controller: DrawingController, modifier: Modifier) {
var currentPath by remember { mutableStateOf(Path()) }
// 监听控制器数据变化,触发重绘
val paths by remember { derivedStateOf { controller.getPaths() } }
Box(modifier = modifier) {
Canvas(
modifier = Modifier
.fillMaxSize()
.onSizeChanged { size ->
// 记录画板实际尺寸
controller.boardSize.value = Pair(size.width, size.height)
}
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
currentPath = Path().apply { moveTo(offset.x, offset.y) }
},
onDrag = { change, _ ->
currentPath.lineTo(change.position.x, change.position.y)
currentPath = currentPath.copy()
},
onDragEnd = {
controller.addPath(
DrawPath(
currentPath,
controller.currentColor,
controller.currentStrokeWidth
)
)
currentPath = Path() // 重置当前路径
}
)
},
) {
// 绘制所有已完成的路径
paths.forEach { drawPath ->
drawPath(
path = drawPath.path,
color = drawPath.color,
style = Stroke(width = drawPath.strokeWidth)
)
}
// 绘制当前正在绘制的路径
drawPath(
path = currentPath,
color = controller.currentColor,
style = Stroke(width = controller.currentStrokeWidth)
)
}
}
}

注意

Composable组件不能获取图片,所以只能使用Path重新绘制来获取。

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Box(
Modifier.fillMaxSize()
) {
val drawController = remember { DrawingController() }
DrawingBoard(
drawController,
Modifier.fillMaxSize()
)

val image = remember { mutableStateOf<Bitmap?>(null) }

Row() {
Box(
Modifier
.size(20.dp)
.clickable {
val bmp = drawController.createBitmap()
bmp?.let {
image.value = bmp
}

}) {
ImgLocalBg(R.drawable.draw_clean)
}

image.value?.let {
Box(Modifier.size(100.dp)) {
Image(
bitmap = it.asImageBitmap(),
contentDescription = "",
modifier = Modifier.fillMaxSize()
)
}
}
}
}

注意

DrawingController的示例要添加remember{},否则组件重绘,实例就清空了。