1
1
package com.smarttoolfactory.tutorial1_1basics.chapter6_graphics
2
2
3
+ import android.graphics.Bitmap
4
+ import android.graphics.PorterDuff
3
5
import androidx.compose.foundation.Canvas
4
6
import androidx.compose.foundation.Image
5
7
import androidx.compose.foundation.background
6
- import androidx.compose.foundation.border
7
8
import androidx.compose.foundation.layout.Arrangement
8
- import androidx.compose.foundation.layout.Box
9
+ import androidx.compose.foundation.layout.BoxWithConstraints
9
10
import androidx.compose.foundation.layout.Column
10
11
import androidx.compose.foundation.layout.Row
12
+ import androidx.compose.foundation.layout.aspectRatio
11
13
import androidx.compose.foundation.layout.fillMaxSize
12
14
import androidx.compose.foundation.layout.fillMaxWidth
13
15
import androidx.compose.foundation.layout.padding
@@ -24,21 +26,31 @@ import androidx.compose.runtime.remember
24
26
import androidx.compose.runtime.setValue
25
27
import androidx.compose.runtime.snapshots.SnapshotStateList
26
28
import androidx.compose.ui.Modifier
29
+ import androidx.compose.ui.draw.clipToBounds
30
+ import androidx.compose.ui.draw.drawWithContent
27
31
import androidx.compose.ui.geometry.Offset
32
+ import androidx.compose.ui.geometry.Size
33
+ import androidx.compose.ui.graphics.BlendMode
34
+ import androidx.compose.ui.graphics.Canvas
28
35
import androidx.compose.ui.graphics.Color
36
+ import androidx.compose.ui.graphics.ImageBitmap
37
+ import androidx.compose.ui.graphics.Paint
38
+ import androidx.compose.ui.graphics.PaintingStyle
29
39
import androidx.compose.ui.graphics.Path
30
40
import androidx.compose.ui.graphics.PathEffect
41
+ import androidx.compose.ui.graphics.asImageBitmap
31
42
import androidx.compose.ui.graphics.drawscope.Stroke
43
+ import androidx.compose.ui.graphics.nativeCanvas
32
44
import androidx.compose.ui.layout.ContentScale
33
- import androidx.compose.ui.res.painterResource
45
+ import androidx.compose.ui.res.imageResource
34
46
import androidx.compose.ui.tooling.preview.Preview
47
+ import androidx.compose.ui.unit.IntSize
35
48
import androidx.compose.ui.unit.dp
36
49
import com.smarttoolfactory.tutorial1_1basics.R
37
50
import com.smarttoolfactory.tutorial1_1basics.chapter5_gesture.gesture.MotionEvent
38
51
import com.smarttoolfactory.tutorial1_1basics.chapter5_gesture.gesture.pointerMotionEvents
39
52
import com.smarttoolfactory.tutorial1_1basics.ui.Blue400
40
53
import com.smarttoolfactory.tutorial1_1basics.ui.backgroundColor
41
- import com.smarttoolfactory.tutorial1_1basics.ui.components.getRandomColor
42
54
import kotlin.math.sqrt
43
55
44
56
@Preview
@@ -82,6 +94,12 @@ private fun PolygonDrawingApp() {
82
94
*/
83
95
var currentPolygon by remember { mutableStateOf(Polygon (), policy = neverEqualPolicy()) }
84
96
97
+ val imageBitmap = ImageBitmap .imageResource(R .drawable.landscape4)
98
+
99
+ var crop by remember {
100
+ mutableStateOf(false )
101
+ }
102
+
85
103
LaunchedEffect (mode, motionEvent) {
86
104
if (mode == Mode .Touch && motionEvent == MotionEvent .Idle ) {
87
105
@@ -116,6 +134,7 @@ private fun PolygonDrawingApp() {
116
134
mode = if (mode == Mode .Draw ) {
117
135
Mode .Touch
118
136
} else Mode .Draw
137
+ crop = false
119
138
}
120
139
) {
121
140
Text (" Mode: $mode " )
@@ -125,16 +144,27 @@ private fun PolygonDrawingApp() {
125
144
onClick = {
126
145
currentPolygon = Polygon ()
127
146
mode = Mode .Draw
147
+ crop = false
128
148
}
129
149
) {
130
150
Text (" Reset" )
131
151
}
152
+
153
+ Button (
154
+ onClick = {
155
+ crop = true
156
+ }
157
+ ) {
158
+ Text (" Crop" )
159
+ }
132
160
}
133
161
162
+ val sizeModifier = Modifier
163
+ .padding(16 .dp)
164
+ .fillMaxWidth()
165
+ .aspectRatio(4 / 3f )
134
166
135
167
val drawModifier = Modifier
136
- .border(2 .dp, getRandomColor())
137
- .fillMaxSize()
138
168
.pointerMotionEvents(
139
169
onDown = { pointerInputChange ->
140
170
@@ -274,18 +304,87 @@ private fun PolygonDrawingApp() {
274
304
delayAfterDownInMillis = 20
275
305
)
276
306
277
- Box {
307
+ BoxWithConstraints (sizeModifier) {
308
+ val imageWidth = constraints.maxWidth
309
+ val imageHeight = constraints.maxHeight
310
+
311
+ val erasedBitmap: ImageBitmap = remember {
312
+ Bitmap .createBitmap(imageWidth, imageHeight, Bitmap .Config .ARGB_8888 )
313
+ .asImageBitmap()
314
+ }
315
+
316
+ val canvas: Canvas = remember {
317
+ Canvas (erasedBitmap)
318
+ }
319
+
320
+ val paint = remember {
321
+ Paint ()
322
+ }
323
+
324
+ val erasePaint = remember {
325
+ Paint ().apply {
326
+ blendMode = BlendMode .SrcIn
327
+ this .style = PaintingStyle .Fill
328
+ }
329
+ }
330
+
331
+ canvas.apply {
332
+
333
+ val canvasWidth = nativeCanvas.width.toFloat()
334
+ val canvasHeight = nativeCanvas.height.toFloat()
335
+
336
+ with (canvas.nativeCanvas) {
337
+ drawColor(android.graphics.Color .TRANSPARENT , PorterDuff .Mode .CLEAR )
338
+
339
+ // Destination
340
+ drawPath(path = currentPolygon.path, paint)
341
+
342
+ // Source
343
+ drawImageRect(
344
+ image = imageBitmap,
345
+ dstSize = IntSize (canvasWidth.toInt(), canvasHeight.toInt()),
346
+ paint = erasePaint
347
+ )
348
+ }
349
+ }
350
+
278
351
Image (
279
- modifier = Modifier .fillMaxSize(),
280
- painter = painterResource(R .drawable.landscape6),
352
+ modifier = Modifier .matchParentSize()
353
+ .clipToBounds()
354
+ .drawWithContent {
355
+ val width = this .size.width
356
+ val height = this .size.height
357
+
358
+ val checkerWidth = 10 .dp.toPx()
359
+ val checkerHeight = 10 .dp.toPx()
360
+
361
+ val horizontalSteps = (width / checkerWidth).toInt()
362
+ val verticalSteps = (height / checkerHeight).toInt()
363
+
364
+ for (y in 0 .. verticalSteps) {
365
+ for (x in 0 .. horizontalSteps) {
366
+ val isGrayTile = ((x + y) % 2 == 1 )
367
+ drawRect(
368
+ color = if (isGrayTile) Color .LightGray else Color .White ,
369
+ topLeft = Offset (x * checkerWidth, y * checkerHeight),
370
+ size = Size (checkerWidth, checkerHeight)
371
+ )
372
+ }
373
+ }
374
+
375
+ drawContent()
376
+ }
377
+ .matchParentSize(),
378
+ bitmap = if (crop) erasedBitmap else imageBitmap,
281
379
contentDescription = null ,
282
380
contentScale = ContentScale .Crop
283
381
)
284
382
285
- Canvas (modifier = drawModifier) {
383
+
384
+ Canvas (modifier = drawModifier.matchParentSize()) {
286
385
when (motionEvent) {
287
386
MotionEvent .Move -> {
288
- if (mode != Mode .Touch ) {
387
+ if (mode == Mode .Draw ) {
289
388
drawLine(
290
389
color = Color .Black ,
291
390
start = firstTouchPoint.position,
@@ -328,7 +427,6 @@ private fun PolygonDrawingApp() {
328
427
)
329
428
}
330
429
331
-
332
430
val path = currentPolygon.path
333
431
334
432
drawPath(
@@ -343,7 +441,7 @@ private fun PolygonDrawingApp() {
343
441
@Immutable
344
442
data class Polygon (
345
443
val path : Path = Path (),
346
- val lines : SnapshotStateList <Line > = mutableStateListOf()
444
+ val lines : SnapshotStateList <Line > = mutableStateListOf(),
347
445
) {
348
446
val firstPoint: Point ? = lines.firstOrNull()?.start
349
447
val lastPoint: Point ? = lines.lastOrNull()?.end
@@ -352,7 +450,7 @@ data class Polygon(
352
450
@Immutable
353
451
data class Line (
354
452
val start : Point ,
355
- val end : Point
453
+ val end : Point ,
356
454
) {
357
455
358
456
fun isPointExist (offset : Offset ): Boolean {
@@ -365,11 +463,11 @@ data class Line(
365
463
366
464
class Point (
367
465
var position : Offset ,
368
- var isTouched : Boolean = false
466
+ var isTouched : Boolean = false ,
369
467
)
370
468
371
469
private enum class Mode {
372
- Draw , Touch
470
+ Draw , Touch , ERASE
373
471
}
374
472
375
473
private fun calculateDistanceFromCenter (center : Offset , position : Offset ): Float {
0 commit comments