Skip to content

Commit f31ab72

Browse files
authored
Merge pull request #4 from MarkYav/develop
v1.2.0
2 parents 4c2f718 + baa05a5 commit f31ab72

14 files changed

Lines changed: 336 additions & 145 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ This is the first multiplatform drawing library!
2222
- Different image rations
2323
- Filling tool
2424
- Optimizing rendering (convert drawn PATHes)
25-
- Migrate from Compose dependencies in DrawController
25+
- Migrate from Compose dependencies in [controller folder](drawbox/src/commonMain/kotlin/io/github/markyav/drawbox/controller)
2626

2727
## Demo
2828

buildSrc/src/main/kotlin/Library.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ object Library {
55

66
val group = "io.github.markyav.drawbox"
77
val artifact = "drawbox"
8-
val version = "1.1.0"
8+
val version = "1.2.0"
99

1010
object License {
1111
val name = "Apache-2.0"

drawbox/src/commonMain/kotlin/io/github/markyav/drawbox/box/DrawBox.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,34 @@ package io.github.markyav.drawbox.box
33
import androidx.compose.foundation.layout.Box
44
import androidx.compose.foundation.layout.fillMaxSize
55
import androidx.compose.runtime.Composable
6+
import androidx.compose.runtime.remember
7+
import androidx.compose.runtime.rememberCoroutineScope
68
import androidx.compose.ui.Modifier
7-
import io.github.markyav.drawbox.controller.DrawBoxBackground
9+
import io.github.markyav.drawbox.controller.DrawBoxSubscription
810
import io.github.markyav.drawbox.controller.DrawController
911
import io.github.markyav.drawbox.model.PathWrapper
12+
import kotlinx.coroutines.CoroutineScope
13+
import kotlinx.coroutines.flow.StateFlow
1014

1115
@Composable
1216
fun DrawBox(
1317
controller: DrawController,
1418
modifier: Modifier = Modifier.fillMaxSize(),
1519
) {
16-
val path: List<PathWrapper>? = controller.pathToDrawOnCanvas
17-
val background: DrawBoxBackground = controller.background
18-
val canvasAlpha: Float = controller.canvasOpacity
20+
val path: StateFlow<List<PathWrapper>> = remember {
21+
controller.getPathWrappersForDrawbox(DrawBoxSubscription.DynamicUpdate)
22+
}
1923

2024
Box(modifier = modifier) {
2125
DrawBoxBackground(
22-
background = background,
26+
background = controller.background.value,
2327
modifier = Modifier.fillMaxSize(),
2428
)
2529
DrawBoxCanvas(
26-
path = path ?: emptyList(),
27-
alpha = canvasAlpha,
30+
pathListWrapper = path,
31+
alpha = controller.canvasOpacity.value,
2832
onSizeChanged = controller::connectToDrawBox,
29-
onTap = controller::insertNewPath,
33+
onTap = controller::onTap,
3034
onDragStart = controller::insertNewPath,
3135
onDrag = controller::updateLatestPath,
3236
onDragEnd = controller::finalizePath,

drawbox/src/commonMain/kotlin/io/github/markyav/drawbox/box/DrawBoxCanvas.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import androidx.compose.foundation.Canvas
44
import androidx.compose.foundation.gestures.detectDragGestures
55
import androidx.compose.foundation.gestures.detectTapGestures
66
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.collectAsState
8+
import androidx.compose.runtime.getValue
79
import androidx.compose.runtime.remember
810
import androidx.compose.ui.Modifier
911
import androidx.compose.ui.draw.alpha
@@ -18,10 +20,11 @@ import androidx.compose.ui.layout.onSizeChanged
1820
import androidx.compose.ui.unit.IntSize
1921
import io.github.markyav.drawbox.model.PathWrapper
2022
import io.github.markyav.drawbox.util.createPath
23+
import kotlinx.coroutines.flow.StateFlow
2124

2225
@Composable
2326
fun DrawBoxCanvas(
24-
path: List<PathWrapper>,
27+
pathListWrapper: StateFlow<List<PathWrapper>>,
2528
alpha: Float,
2629
onSizeChanged: (IntSize) -> Unit,
2730
onTap: (Offset) -> Unit,
@@ -33,6 +36,7 @@ fun DrawBoxCanvas(
3336
val onDragMapper: (change: PointerInputChange, dragAmount: Offset) -> Unit = remember {
3437
{ change, _ -> onDrag(change.position) }
3538
}
39+
val path by pathListWrapper.collectAsState()
3640

3741
Canvas(modifier = modifier
3842
.onSizeChanged(onSizeChanged)
Lines changed: 109 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,194 @@
11
package io.github.markyav.drawbox.controller
22

33
import androidx.compose.runtime.*
4-
import androidx.compose.runtime.snapshots.SnapshotStateList
54
import androidx.compose.ui.geometry.Offset
65
import androidx.compose.ui.graphics.*
76
import androidx.compose.ui.unit.IntSize
87
import io.github.markyav.drawbox.model.PathWrapper
8+
import io.github.markyav.drawbox.util.addNotNull
9+
import io.github.markyav.drawbox.util.combineStates
910
import io.github.markyav.drawbox.util.createPath
10-
import io.github.markyav.drawbox.util.pop
11+
import io.github.markyav.drawbox.util.mapState
1112
import kotlinx.coroutines.CoroutineScope
13+
import kotlinx.coroutines.Dispatchers
14+
import kotlinx.coroutines.coroutineScope
1215
import kotlinx.coroutines.flow.*
16+
import kotlinx.coroutines.launch
17+
import kotlin.reflect.KProperty
1318

1419
/**
1520
* DrawController interacts with [DrawBox] and it allows you to control the canvas and all the components with it.
1621
*/
1722
class DrawController {
18-
private var state: DrawBoxConnectionState by mutableStateOf(DrawBoxConnectionState.Disconnected)
23+
private var state: MutableStateFlow<DrawBoxConnectionState> = MutableStateFlow(DrawBoxConnectionState.Disconnected)
1924

2025
/** A stateful list of [Path] that is drawn on the [Canvas]. */
21-
private val drawnPaths: SnapshotStateList<PathWrapper> = mutableStateListOf()
26+
private val drawnPaths: MutableStateFlow<List<PathWrapper>> = MutableStateFlow(emptyList())
2227

23-
private val completelyDrawnPaths: SnapshotStateList<PathWrapper> = mutableStateListOf()
24-
25-
/** A stateful list of [Path] that is drawn on the [Canvas] and is scaled for the connected bitmap. */
26-
internal val pathToDrawOnCanvas: List<PathWrapper>? by derivedStateOf {
27-
(state as? DrawBoxConnectionState.Connected)?.let {
28-
drawnPaths.scale(it.size.toFloat())
29-
}
30-
}
28+
private val activeDrawingPath: MutableStateFlow<List<Offset>?> = MutableStateFlow(null)
3129

3230
/** A stateful list of [Path] that was drawn on the [Canvas] but user retracted his action. */
33-
private val canceledPaths: SnapshotStateList<PathWrapper> = mutableStateListOf()
31+
private val canceledPaths: MutableStateFlow<List<PathWrapper>> = MutableStateFlow(emptyList())
3432

3533
/** An [canvasOpacity] of the [Canvas] in the [DrawBox] */
36-
var canvasOpacity: Float by mutableStateOf(1f)
34+
var canvasOpacity: MutableStateFlow<Float> = MutableStateFlow(1f)
3735

3836
/** An [opacity] of the stroke */
39-
var opacity: Float by mutableStateOf(1f)
37+
var opacity: MutableStateFlow<Float> = MutableStateFlow(1f)
4038

4139
/** A [strokeWidth] of the stroke */
42-
var strokeWidth: Float by mutableStateOf(10f)
40+
var strokeWidth: MutableStateFlow<Float> = MutableStateFlow(10f)
4341

4442
/** A [color] of the stroke */
45-
var color: Color by mutableStateOf(Color.Red)
43+
var color: MutableStateFlow<Color> = MutableStateFlow(Color.Red)
4644

4745
/** A [background] of the background of DrawBox */
48-
var background: DrawBoxBackground by mutableStateOf(DrawBoxBackground.NoBackground)
46+
var background: MutableStateFlow<DrawBoxBackground> = MutableStateFlow(DrawBoxBackground.NoBackground)
4947

5048
/** Indicate how many redos it is possible to do. */
51-
val undoCount: Int by derivedStateOf { drawnPaths.size }
49+
val undoCount = drawnPaths.mapState { it.size }
5250

5351
/** Indicate how many undos it is possible to do. */
54-
val redoCount: Int by derivedStateOf { canceledPaths.size }
52+
val redoCount = canceledPaths.mapState { it.size }
5553

5654
/** Executes undo the drawn path if possible. */
5755
fun undo() {
58-
if (drawnPaths.isNotEmpty()) {
59-
canceledPaths.add(drawnPaths.pop())
60-
finalizePath()
56+
if (drawnPaths.value.isNotEmpty()) {
57+
val _drawnPaths = drawnPaths.value.toMutableList()
58+
val _canceledPaths = canceledPaths.value.toMutableList()
59+
60+
_canceledPaths.add(_drawnPaths.removeLast())
61+
62+
drawnPaths.value = _drawnPaths
63+
canceledPaths.value = _canceledPaths
6164
}
6265
}
6366

6467
/** Executes redo the drawn path if possible. */
6568
fun redo() {
66-
if (canceledPaths.isNotEmpty()) {
67-
drawnPaths.add(canceledPaths.pop())
68-
finalizePath()
69+
if (canceledPaths.value.isNotEmpty()) {
70+
val _drawnPaths = drawnPaths.value.toMutableList()
71+
val _canceledPaths = canceledPaths.value.toMutableList()
72+
73+
_drawnPaths.add(_canceledPaths.removeLast())
74+
75+
drawnPaths.value = _drawnPaths
76+
canceledPaths.value = _canceledPaths
6977
}
7078
}
7179

7280
/** Clear drawn paths and the bitmap image. */
7381
fun reset() {
74-
drawnPaths.clear()
75-
canceledPaths.clear()
76-
completelyDrawnPaths.clear()
82+
drawnPaths.value = emptyList()
83+
canceledPaths.value = emptyList()
7784
}
7885

7986
/** Call this function when user starts drawing a path. */
8087
internal fun updateLatestPath(newPoint: Offset) {
81-
(state as? DrawBoxConnectionState.Connected)?.let {
82-
drawnPaths.last().points.add(newPoint.div(it.size.toFloat()))
88+
(state.value as? DrawBoxConnectionState.Connected)?.let {
89+
require(activeDrawingPath.value != null)
90+
val list = activeDrawingPath.value!!.toMutableList()
91+
list.add(newPoint.div(it.size.toFloat()))
92+
activeDrawingPath.value = list
8393
}
8494
}
8595

8696
/** When dragging call this function to update the last path. */
8797
internal fun insertNewPath(newPoint: Offset) {
88-
(state as? DrawBoxConnectionState.Connected)?.let {
89-
val pathWrapper = PathWrapper(
98+
(state.value as? DrawBoxConnectionState.Connected)?.let {
99+
require(activeDrawingPath.value == null)
100+
/*val pathWrapper = PathWrapper(
90101
points = mutableStateListOf(newPoint.div(it.size.toFloat())),
91-
strokeColor = color,
92-
alpha = opacity,
93-
strokeWidth = strokeWidth.div(it.size.toFloat()),
94-
)
95-
drawnPaths.add(pathWrapper)
96-
canceledPaths.clear()
102+
strokeColor = color.value,
103+
alpha = opacity.value,
104+
strokeWidth = strokeWidth.value.div(it.size.toFloat()),
105+
)*/
106+
activeDrawingPath.value = listOf(newPoint.div(it.size.toFloat()))
107+
canceledPaths.value = emptyList()
97108
}
98109
}
99110

100111
internal fun finalizePath() {
101-
completelyDrawnPaths.clear()
102-
completelyDrawnPaths.addAll(drawnPaths)
112+
(state.value as? DrawBoxConnectionState.Connected)?.let {
113+
require(activeDrawingPath.value != null)
114+
val _drawnPaths = drawnPaths.value.toMutableList()
115+
116+
val pathWrapper = PathWrapper(
117+
points = activeDrawingPath.value!!,
118+
strokeColor = color.value,
119+
alpha = opacity.value,
120+
strokeWidth = strokeWidth.value.div(it.size.toFloat()),
121+
)
122+
_drawnPaths.add(pathWrapper)
123+
124+
drawnPaths.value = _drawnPaths
125+
activeDrawingPath.value = null
126+
}
103127
}
104128

105129
/** Call this function to connect to the [DrawBox]. */
106130
internal fun connectToDrawBox(size: IntSize) {
107131
if (
108132
size.width > 0 &&
109133
size.height > 0 &&
110-
size.width == size.height //&&
111-
//state is DrawBoxConnectionState.Disconnected
134+
size.width == size.height
112135
) {
113-
state = DrawBoxConnectionState.Connected(size = size.width)
136+
state.value = DrawBoxConnectionState.Connected(size = size.width)
114137
}
115138
}
116139

140+
internal fun onTap(newPoint: Offset) {
141+
insertNewPath(newPoint)
142+
finalizePath()
143+
}
144+
117145
private fun List<PathWrapper>.scale(size: Float): List<PathWrapper> {
118146
return this.map { pw ->
119147
val t = pw.points.map { it.times(size) }
120148
pw.copy(
121-
points = SnapshotStateList<Offset>().also { it.addAll(t) },
149+
points = mutableListOf<Offset>().also { it.addAll(t) },
122150
strokeWidth = pw.strokeWidth * size
123151
)
124152
}
125153
}
126154

127-
fun getBitmap(size: Int, coroutineScope: CoroutineScope, subscription: DrawBoxSubscription): StateFlow<ImageBitmap> {
155+
fun getDrawPath(subscription: DrawBoxSubscription): StateFlow<List<PathWrapper>> {
156+
return when (subscription) {
157+
is DrawBoxSubscription.DynamicUpdate -> getDynamicUpdateDrawnPath()
158+
is DrawBoxSubscription.FinishDrawingUpdate -> drawnPaths
159+
}
160+
}
161+
162+
private fun getDynamicUpdateDrawnPath(): StateFlow<List<PathWrapper>> {
163+
return combineStates(drawnPaths, activeDrawingPath) { a, b ->
164+
val _a = a.toMutableList()
165+
(state.value as? DrawBoxConnectionState.Connected)?.let {
166+
val pathWrapper = PathWrapper(
167+
points = activeDrawingPath.value ?: emptyList(),
168+
strokeColor = color.value,
169+
alpha = opacity.value,
170+
strokeWidth = strokeWidth.value.div(it.size.toFloat()),
171+
)
172+
_a.addNotNull(pathWrapper)
173+
}
174+
_a
175+
}
176+
}
177+
178+
internal fun getPathWrappersForDrawbox(subscription: DrawBoxSubscription): StateFlow<List<PathWrapper>> {
179+
return combineStates(getDrawPath(subscription), state) { paths, st ->
180+
val size = (st as? DrawBoxConnectionState.Connected)?.size ?: 1
181+
paths.scale(size.toFloat())
182+
}
183+
}
184+
185+
fun getBitmap(size: Int, subscription: DrawBoxSubscription): StateFlow<ImageBitmap> {
128186
val initialBitmap = ImageBitmap(size, size, ImageBitmapConfig.Argb8888)
129-
return flow {
187+
val path = getDrawPath(subscription)
188+
return path.mapState {
130189
val bitmap = ImageBitmap(size, size, ImageBitmapConfig.Argb8888)
131190
val canvas = Canvas(bitmap)
132-
val path = when (subscription) {
133-
is DrawBoxSubscription.DynamicUpdate -> drawnPaths
134-
is DrawBoxSubscription.FinishDrawingUpdate -> completelyDrawnPaths
135-
}
136-
path.scale(size.toFloat()).forEach { pw ->
191+
it.scale(size.toFloat()).forEach { pw ->
137192
canvas.drawPath(
138193
createPath(pw.points),
139194
paint = Paint().apply {
@@ -146,7 +201,7 @@ class DrawController {
146201
}
147202
)
148203
}
149-
emit(bitmap)
150-
}.stateIn(coroutineScope, started = SharingStarted.Eagerly, initialValue = initialBitmap)
204+
bitmap
205+
}
151206
}
152207
}

drawbox/src/commonMain/kotlin/io/github/markyav/drawbox/model/PathWrapper.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import androidx.compose.ui.geometry.Offset
55
import androidx.compose.ui.graphics.Color
66

77
data class PathWrapper(
8-
var points: SnapshotStateList<Offset>,
8+
var points: List<Offset>,
99
val strokeWidth: Float = 5f,
1010
val strokeColor: Color,
1111
val alpha: Float = 1f

drawbox/src/commonMain/kotlin/io/github/markyav/drawbox/util/SnapshotStateListUtil.kt

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)