Jetpack Compose 用画板讲述组件渲染机制

前言

组件触发重新渲染需要满足两个前提

  • 变量是mutableState,并且发生改变
  • 组件内部使用到了这值

讲解

问题示例

先看一个示例

这个示例会有问题,当拖动的时候不会渲染,只有拖动结束才会渲染。

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
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.geometry.Offset
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
import androidx.compose.ui.unit.dp


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

Scaffold(modifier = modifier) { padding ->
Canvas(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
currentPath = Path().apply { moveTo(offset.x, offset.y) }
},
onDrag = { change, _ ->
currentPath.lineTo(change.position.x, change.position.y)
},
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

这个在拖动的时候不会渲染,是因为currentPath是个对象,对象的指向没有变化,所以不会被监听到变化。

只要修改这里就能触发渲染

1
2
3
4
onDrag = { change, _ ->
currentPath.lineTo(change.position.x, change.position.y)
currentPath = currentPath.copy()
},

解决方式2

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
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.geometry.Offset
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
import androidx.compose.ui.unit.dp


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

var drawConunter by remember { mutableIntStateOf(0) }

Scaffold(modifier = modifier) { padding ->
Canvas(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
currentPath = Path().apply { moveTo(offset.x, offset.y) }
},
onDrag = { change, _ ->
currentPath.lineTo(change.position.x, change.position.y)
drawConunter++
},
onDragEnd = {
drawPaths.add(
DrawPath(
currentPath,
currentColor,
currentStrokeWidth
)
)
currentPath = Path() // 重置当前路径
}
)
}
) {
@Suppress("UNUSED_EXPRESSION")
drawConunter
// 绘制所有已完成的路径
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)

我们也可以新增一个变量来触发渲染。

注意

虽然组件内不需要,也要在组件内使用drawConunter,否则不会触发渲染。

当然这样也行

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
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.geometry.Offset
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
import androidx.compose.ui.unit.dp


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

val drawConunter = remember { mutableIntStateOf(0) }

Scaffold(modifier = modifier) { padding ->
Canvas(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
currentPath = Path().apply { moveTo(offset.x, offset.y) }
},
onDrag = { change, _ ->
currentPath.lineTo(change.position.x, change.position.y)
drawConunter.intValue++
},
onDragEnd = {
drawPaths.add(
DrawPath(
currentPath,
currentColor,
currentStrokeWidth
)
)
currentPath = Path() // 重置当前路径
}
)
}
) {
drawConunter.intValue
// 绘制所有已完成的路径
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)

我们把var drawConunter by remember { mutableIntStateOf(0) }更换为了val drawConunter = remember { mutableIntStateOf(0) }

组件内也必须是drawConunter.value,否则不会渲染

区别

那么var drawConunter by remember { mutableIntStateOf(0) }val drawConunter = remember { mutableIntStateOf(0) }有什么区别呢?

1
var drawCounter by remember { mutableIntStateOf(0) }

写法1

var drawCounter by remember { mutableIntStateOf(0) }

这种写法使用了 Kotlin 的委托属性(delegated properties)特性。

mutableIntStateOf(0) 会创建一个 MutableState<Int> 对象,而 by 关键字则将 drawCounter 委托给这个 MutableState<Int> 对象。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

fun main() {
var drawCounter by remember { mutableIntStateOf(0) }
// 修改 drawCounter 的值
drawCounter = 1
// 获取 drawCounter 的值
println(drawCounter)
}

解释

  • 使用方式:当你使用 drawCounter 时,实际上是在调用 MutableState 对象的 getValue()setValue() 方法。

    当你给 drawCounter 赋值时,会调用 setValue() 方法更新状态;当你读取 drawCounter 的值时,会调用 getValue() 方法获取状态的值。

  • Compose 中的应用:在 Jetpack Compose 里,这种方式非常有用,因为 Compose 可以自动追踪 MutableState 的变化,并在状态改变时重新组合受影响的部分。

    例如,当 drawCounter 的值改变时,Compose 会知道状态有更新,进而重新执行依赖于 drawCounter 的组合函数。

写法2

var drawCounter = remember { mutableIntStateOf(0) }

这种写法只是简单地将 mutableIntStateOf(0) 返回的 MutableState<Int> 对象赋值给 drawCounter 变量。

代码示例

1
2
3
4
5
6
7
8
9
10
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember

fun main() {
var drawCounter = remember { mutableIntStateOf(0) }
// 修改 drawCounter 的值需要显式调用 value 属性
drawCounter.value = 1
// 获取 drawCounter 的值也需要显式调用 value 属性
println(drawCounter.value)
}

解释

  • 使用方式:要访问或修改状态的值,你需要显式地使用 drawCounter.value

    因为 drawCounter 现在是一个 MutableState<Int> 对象,而不是一个简单的 Int 类型变量。

  • Compose 中的应用:虽然 MutableState 仍然可以被 Compose 追踪,但在使用时会更繁琐,因为每次访问或修改状态值都要带上 .value,而且代码的可读性也会降低。

总结

在 Jetpack Compose 中,通常推荐使用 var drawCounter by remember { mutableIntStateOf(0) } 这种写法,因为它更简洁,使用起来更自然,并且能让 Compose 更方便地追踪状态变化,从而实现自动重新组合。