1
+ package org.zhiwei.compose.screen.graphics
2
+
3
+ import androidx.compose.animation.core.LinearEasing
4
+ import androidx.compose.animation.core.RepeatMode
5
+ import androidx.compose.animation.core.animateFloat
6
+ import androidx.compose.animation.core.infiniteRepeatable
7
+ import androidx.compose.animation.core.rememberInfiniteTransition
8
+ import androidx.compose.animation.core.tween
9
+ import androidx.compose.foundation.Canvas
10
+ import androidx.compose.foundation.background
11
+ import androidx.compose.foundation.layout.Column
12
+ import androidx.compose.foundation.layout.fillMaxSize
13
+ import androidx.compose.foundation.layout.height
14
+ import androidx.compose.foundation.layout.padding
15
+ import androidx.compose.foundation.rememberScrollState
16
+ import androidx.compose.foundation.verticalScroll
17
+ import androidx.compose.material3.Slider
18
+ import androidx.compose.material3.Text
19
+ import androidx.compose.runtime.Composable
20
+ import androidx.compose.runtime.getValue
21
+ import androidx.compose.runtime.mutableFloatStateOf
22
+ import androidx.compose.runtime.mutableStateOf
23
+ import androidx.compose.runtime.remember
24
+ import androidx.compose.runtime.setValue
25
+ import androidx.compose.ui.Modifier
26
+ import androidx.compose.ui.draw.shadow
27
+ import androidx.compose.ui.geometry.Offset
28
+ import androidx.compose.ui.geometry.Size
29
+ import androidx.compose.ui.graphics.Color
30
+ import androidx.compose.ui.graphics.Path
31
+ import androidx.compose.ui.graphics.PathEffect
32
+ import androidx.compose.ui.graphics.StampedPathEffectStyle
33
+ import androidx.compose.ui.graphics.drawscope.Stroke
34
+ import androidx.compose.ui.text.font.FontWeight
35
+ import androidx.compose.ui.tooling.preview.Preview
36
+ import androidx.compose.ui.unit.dp
37
+ import androidx.compose.ui.unit.sp
38
+ import kotlin.math.roundToInt
39
+
40
+ @Composable
41
+ internal fun CanvasPathEffect_Screen (modifier : Modifier = Modifier ) {
42
+ Column (
43
+ modifier
44
+ .fillMaxSize()
45
+ .verticalScroll(rememberScrollState())
46
+ ) {
47
+ Text (
48
+ " dashedPathEffect" ,
49
+ fontWeight = FontWeight .Bold ,
50
+ fontSize = 20 .sp,
51
+ modifier = Modifier .padding(8 .dp)
52
+ )
53
+ DashedEffectExample ()
54
+ DashPathEffectAnimatedExample ()
55
+
56
+ Text (
57
+ " cornerPathEffect" ,
58
+ fontWeight = FontWeight .Bold ,
59
+ fontSize = 20 .sp,
60
+ modifier = Modifier .padding(8 .dp)
61
+ )
62
+ CornerPathEffectExample ()
63
+
64
+ Text (
65
+ " chainPathEffect" ,
66
+ fontWeight = FontWeight .Bold ,
67
+ fontSize = 20 .sp,
68
+ modifier = Modifier .padding(8 .dp)
69
+ )
70
+ ChainPathEffectExample ()
71
+ Text (
72
+ " stompedPathEffect" ,
73
+ fontWeight = FontWeight .Bold ,
74
+ fontSize = 20 .sp,
75
+ modifier = Modifier .padding(8 .dp)
76
+ )
77
+ StompedPathEffectExample ()
78
+ }
79
+ }
80
+
81
+
82
+ @Composable
83
+ private fun DashedEffectExample () {
84
+
85
+ var onInterval by remember { mutableFloatStateOf(20f ) }
86
+ var offInterval by remember { mutableFloatStateOf(20f ) }
87
+ var phase by remember { mutableFloatStateOf(10f ) }
88
+
89
+ val pathEffect = PathEffect .dashPathEffect(
90
+ intervals = floatArrayOf(onInterval, offInterval),
91
+ phase = phase
92
+ )
93
+
94
+ DrawPathEffect (pathEffect = pathEffect)
95
+
96
+ Text (text = " onInterval ${onInterval.roundToInt()} " )
97
+ Slider (
98
+ value = onInterval,
99
+ onValueChange = { onInterval = it },
100
+ valueRange = 0f .. 100f ,
101
+ )
102
+
103
+
104
+ Text (text = " offInterval ${offInterval.roundToInt()} " )
105
+ Slider (
106
+ value = offInterval,
107
+ onValueChange = { offInterval = it },
108
+ valueRange = 0f .. 100f ,
109
+ )
110
+
111
+ Text (text = " phase ${phase.roundToInt()} " )
112
+ Slider (
113
+ value = phase,
114
+ onValueChange = { phase = it },
115
+ valueRange = 0f .. 100f ,
116
+ )
117
+ }
118
+
119
+ @Composable
120
+ private fun DashPathEffectAnimatedExample () {
121
+
122
+ val transition = rememberInfiniteTransition(label = " path effect" )
123
+
124
+ val phase by transition.animateFloat(
125
+ initialValue = 0f ,
126
+ targetValue = 40f ,
127
+ animationSpec = infiniteRepeatable(
128
+ animation = tween(
129
+ durationMillis = 500 ,
130
+ easing = LinearEasing
131
+ ),
132
+ repeatMode = RepeatMode .Restart
133
+ ), label = " path effect"
134
+ )
135
+
136
+ val pathEffect = PathEffect .dashPathEffect(
137
+ intervals = floatArrayOf(20f , 20f ),
138
+ phase = phase
139
+ )
140
+
141
+ DrawPathEffect (pathEffect = pathEffect)
142
+ }
143
+
144
+ @Composable
145
+ private fun CornerPathEffectExample () {
146
+
147
+ var cornerRadius by remember { mutableFloatStateOf(20f ) }
148
+
149
+ val pathEffect = PathEffect .cornerPathEffect(cornerRadius)
150
+ DrawRect (pathEffect)
151
+
152
+ Text (text = " cornerRadius ${cornerRadius.roundToInt()} " )
153
+ Slider (
154
+ value = cornerRadius,
155
+ onValueChange = { cornerRadius = it },
156
+ valueRange = 0f .. 100f ,
157
+ )
158
+ }
159
+
160
+ @Composable
161
+ private fun ChainPathEffectExample () {
162
+
163
+ var onInterval1 by remember { mutableFloatStateOf(20f ) }
164
+ var offInterval1 by remember { mutableFloatStateOf(20f ) }
165
+ var phase1 by remember { mutableFloatStateOf(10f ) }
166
+
167
+ var cornerRadius by remember { mutableFloatStateOf(20f ) }
168
+
169
+ val pathEffect1 = PathEffect .dashPathEffect(
170
+ intervals = floatArrayOf(onInterval1, offInterval1),
171
+ phase = phase1
172
+ )
173
+
174
+ val pathEffect2 = PathEffect .cornerPathEffect(cornerRadius)
175
+ val pathEffect = PathEffect .chainPathEffect(outer = pathEffect1, inner = pathEffect2)
176
+
177
+ DrawRect (pathEffect)
178
+
179
+ Text (text = " onInterval1 ${onInterval1.roundToInt()} " )
180
+ Slider (
181
+ value = onInterval1,
182
+ onValueChange = { onInterval1 = it },
183
+ valueRange = 0f .. 100f ,
184
+ )
185
+
186
+
187
+ Text (text = " offInterval1 ${offInterval1.roundToInt()} " )
188
+ Slider (
189
+ value = offInterval1,
190
+ onValueChange = { offInterval1 = it },
191
+ valueRange = 0f .. 100f ,
192
+ )
193
+
194
+ Text (text = " phase1 ${phase1.roundToInt()} " )
195
+ Slider (
196
+ value = phase1,
197
+ onValueChange = { phase1 = it },
198
+ valueRange = 0f .. 100f ,
199
+ )
200
+
201
+ Text (text = " cornerRadius ${cornerRadius.roundToInt()} " )
202
+ Slider (
203
+ value = cornerRadius,
204
+ onValueChange = { cornerRadius = it },
205
+ valueRange = 0f .. 100f ,
206
+ )
207
+ }
208
+
209
+ @Composable
210
+ private fun StompedPathEffectExample () {
211
+
212
+ var stompedPathEffectStyle by remember {
213
+ mutableStateOf(StampedPathEffectStyle .Translate )
214
+ }
215
+
216
+ var advance by remember { mutableFloatStateOf(20f ) }
217
+ var phase by remember { mutableFloatStateOf(20f ) }
218
+
219
+ val path = remember {
220
+ Path ().apply {
221
+ moveTo(10f , 0f )
222
+ lineTo(20f , 10f )
223
+ lineTo(10f , 20f )
224
+ lineTo(0f , 10f )
225
+ }
226
+ }
227
+
228
+ val pathEffect = PathEffect .stampedPathEffect(
229
+ shape = path,
230
+ advance = advance,
231
+ phase = phase,
232
+ style = stompedPathEffectStyle
233
+ )
234
+
235
+ DrawPathEffect (pathEffect = pathEffect)
236
+
237
+ Text (text = " advance ${advance.roundToInt()} " )
238
+ Slider (
239
+ value = advance,
240
+ onValueChange = { advance = it },
241
+ valueRange = 0f .. 100f ,
242
+ )
243
+
244
+
245
+ Text (text = " phase ${phase.roundToInt()} " )
246
+ Slider (
247
+ value = phase,
248
+ onValueChange = { phase = it },
249
+ valueRange = 0f .. 100f ,
250
+ )
251
+
252
+ ExposedSelectionMenu (title = " StompedEffect Style" ,
253
+ index = when (stompedPathEffectStyle) {
254
+ StampedPathEffectStyle .Translate -> 0
255
+ StampedPathEffectStyle .Rotate -> 1
256
+ else -> 2
257
+ },
258
+ options = listOf (" Translate" , " Rotate" , " Morph" ),
259
+ onSelected = {
260
+ println (" STOKE CAP $it " )
261
+ stompedPathEffectStyle = when (it) {
262
+ 0 -> StampedPathEffectStyle .Translate
263
+ 1 -> StampedPathEffectStyle .Rotate
264
+ else -> StampedPathEffectStyle .Morph
265
+ }
266
+ }
267
+ )
268
+
269
+ }
270
+
271
+
272
+ @Composable
273
+ private fun DrawRect (pathEffect : PathEffect ) {
274
+ Canvas (modifier = canvasModifier) {
275
+ val horizontalCenter = size.width / 2
276
+ val verticalCenter = size.height / 2
277
+ val radius = size.height / 3
278
+ drawRect(
279
+ Color .Black ,
280
+ topLeft = Offset (horizontalCenter - radius, verticalCenter - radius),
281
+ size = Size (radius * 2 , radius * 2 ),
282
+ style = Stroke (
283
+ width = 2 .dp.toPx(),
284
+ pathEffect = pathEffect
285
+
286
+ )
287
+ )
288
+ }
289
+ }
290
+
291
+ @Composable
292
+ private fun DrawPathEffect (pathEffect : PathEffect ) {
293
+ Canvas (modifier = canvasModifier) {
294
+
295
+ val canvasWidth = size.width
296
+ val canvasHeight = size.height
297
+
298
+ val radius = (canvasHeight / 4 ).coerceAtMost(canvasWidth / 6 )
299
+ val space = (canvasWidth - 4 * radius) / 3
300
+
301
+ drawRect(
302
+ topLeft = Offset (space, (canvasHeight - 2 * radius) / 2 ),
303
+ size = Size (radius * 2 , radius * 2 ),
304
+ color = Color .Black ,
305
+ style = Stroke (
306
+ width = 2 .dp.toPx(),
307
+ pathEffect = pathEffect
308
+
309
+ )
310
+ )
311
+
312
+ drawCircle(
313
+ Color .Black ,
314
+ center = Offset (space * 2 + radius * 3 , canvasHeight / 2 ),
315
+ radius = radius,
316
+ style = Stroke (width = 2 .dp.toPx(), pathEffect = pathEffect)
317
+ )
318
+
319
+ drawLine(
320
+ color = Color .Black ,
321
+ start = Offset (50f , canvasHeight - 50f ),
322
+ end = Offset (canvasWidth - 50f , canvasHeight - 50f ),
323
+ strokeWidth = 2 .dp.toPx(),
324
+ pathEffect = pathEffect
325
+ )
326
+
327
+ }
328
+ }
329
+
330
+ private val canvasModifier = Modifier
331
+ .padding(8 .dp)
332
+ .shadow(1 .dp)
333
+ .background(Color .White )
334
+ .fillMaxSize()
335
+ .height(200 .dp)
336
+
337
+
338
+ @Preview
339
+ @Composable
340
+ private fun PreviewPathEffect () {
341
+ CanvasPathEffect_Screen ()
342
+ }
0 commit comments