1 /*
2 * Copyright 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.graphics.path
18
19 import android.graphics.Bitmap
20 import android.graphics.Color
21 import android.graphics.Paint
22 import android.graphics.Path
23 import android.graphics.PointF
24 import android.graphics.RectF
25 import android.os.Build
26 import androidx.core.graphics.applyCanvas
27 import androidx.core.graphics.createBitmap
28 import androidx.test.ext.junit.runners.AndroidJUnit4
29 import androidx.test.filters.SmallTest
30 import kotlin.math.abs
31 import org.junit.Assert.assertEquals
32 import org.junit.Assert.assertFalse
33 import org.junit.Assert.assertTrue
34 import org.junit.Assert.fail
35 import org.junit.Test
36 import org.junit.runner.RunWith
37
assertPointsEqualsnull38 private fun assertPointsEquals(p1: PointF, p2: PointF) {
39 assertEquals(p1.x, p2.x, 1e-6f)
40 assertEquals(p1.y, p2.y, 1e-6f)
41 }
42
assertPointsEqualsnull43 private fun assertPointsEquals(p1: FloatArray, offset: Int, p2: PointF) {
44 assertEquals(p1[0 + offset * 2], p2.x, 1e-6f)
45 assertEquals(p1[1 + offset * 2], p2.y, 1e-6f)
46 }
47
compareBitmapsnull48 private fun compareBitmaps(b1: Bitmap, b2: Bitmap) {
49 val epsilon: Int =
50 if (Build.VERSION.SDK_INT != 23) {
51 1
52 } else {
53 // There is more AA variability between conics and cubics on API 23, leading
54 // to failures on relatively small visual differences. Increase the error
55 // value for just this release to avoid erroneous bitmap comparison failures.
56 32
57 }
58
59 assertEquals(b1.width, b2.width)
60 assertEquals(b1.height, b2.height)
61
62 val p1 = IntArray(b1.width * b1.height)
63 b1.getPixels(p1, 0, b1.width, 0, 0, b1.width, b1.height)
64
65 val p2 = IntArray(b2.width * b2.height)
66 b2.getPixels(p2, 0, b2.width, 0, 0, b2.width, b2.height)
67
68 for (x in 0 until b1.width) {
69 for (y in 0 until b2.width) {
70 val index = y * b1.width + x
71
72 val c1 = p1[index]
73 val c2 = p2[index]
74
75 assertTrue(abs(Color.red(c1) - Color.red(c2)) <= epsilon)
76 assertTrue(abs(Color.green(c1) - Color.green(c2)) <= epsilon)
77 assertTrue(abs(Color.blue(c1) - Color.blue(c2)) <= epsilon)
78 }
79 }
80 }
81
82 @SmallTest
83 @RunWith(AndroidJUnit4::class)
84 class PathIteratorTest {
85 @Test
emptyIteratornull86 fun emptyIterator() {
87 val path = Path()
88
89 val iterator = path.iterator()
90 assertFalse(iterator.hasNext())
91 val firstSegment = iterator.next()
92 assertEquals(PathSegment.Type.Done, firstSegment.type)
93
94 var count = 0
95 for (segment in path) {
96 count++
97 }
98
99 assertEquals(0, count)
100 }
101
102 @Test
emptyPeeknull103 fun emptyPeek() {
104 val path = Path()
105 val iterator = path.iterator()
106 assertEquals(PathSegment.Type.Done, iterator.peek())
107 }
108
109 @Test
nonEmptyIteratornull110 fun nonEmptyIterator() {
111 val path =
112 Path().apply {
113 moveTo(1.0f, 1.0f)
114 lineTo(2.0f, 2.0f)
115 close()
116 }
117
118 val iterator = path.iterator()
119 assertTrue(iterator.hasNext())
120
121 val types =
122 arrayOf(
123 PathSegment.Type.Move,
124 PathSegment.Type.Line,
125 PathSegment.Type.Close,
126 PathSegment.Type.Done
127 )
128 val points = arrayOf(PointF(1.0f, 1.0f), PointF(2.0f, 2.0f))
129
130 var count = 0
131 for (segment in path) {
132 assertEquals(types[count], segment.type)
133 when (segment.type) {
134 PathSegment.Type.Move -> {
135 assertEquals(points[count], segment.points[0])
136 }
137 PathSegment.Type.Line -> {
138 assertEquals(points[count - 1], segment.points[0])
139 assertEquals(points[count], segment.points[1])
140 }
141 else -> {}
142 }
143 // TODO: remove condition and just auto-increment count when platform change is
144 // checked in which ignores DONE during iteration
145 if (segment.type != PathSegment.Type.Done) count++
146 }
147
148 assertEquals(3, count)
149 }
150
151 @Test
peeknull152 fun peek() {
153 val path =
154 Path().apply {
155 moveTo(1.0f, 1.0f)
156 lineTo(2.0f, 2.0f)
157 close()
158 }
159
160 val iterator = path.iterator()
161 assertEquals(PathSegment.Type.Move, iterator.peek())
162 }
163
164 @Test
peekBeyondnull165 fun peekBeyond() {
166 val path = Path()
167 assertEquals(PathSegment.Type.Done, path.iterator().peek())
168
169 path.apply {
170 moveTo(1.0f, 1.0f)
171 lineTo(2.0f, 2.0f)
172 close()
173 }
174
175 val iterator = path.iterator()
176 while (iterator.hasNext()) iterator.next()
177 assertEquals(PathSegment.Type.Done, iterator.peek())
178 }
179
180 @Test
iteratorStylesnull181 fun iteratorStyles() {
182 val path =
183 Path().apply {
184 moveTo(1.0f, 1.0f)
185 lineTo(2.0f, 2.0f)
186 cubicTo(3.0f, 3.0f, 4.0f, 4.0f, 5.0f, 5.0f)
187 quadTo(7.0f, 7.0f, 8.0f, 8.0f)
188 moveTo(10.0f, 10.0f)
189 // addRoundRect() will generate conic curves on certain API levels
190 addRoundRect(RectF(12.0f, 12.0f, 36.0f, 36.0f), 8.0f, 8.0f, Path.Direction.CW)
191 close()
192 }
193
194 iteratorStylesImpl(path, PathIterator.ConicEvaluation.AsConic)
195 iteratorStylesImpl(path, PathIterator.ConicEvaluation.AsQuadratics)
196 }
197
iteratorStylesImplnull198 private fun iteratorStylesImpl(path: Path, conicEvaluation: PathIterator.ConicEvaluation) {
199 val iterator1 = path.iterator(conicEvaluation)
200 val iterator2 = path.iterator(conicEvaluation)
201 val iterator3 = path.iterator(conicEvaluation)
202
203 val points = FloatArray(8)
204 val points2 = FloatArray(16)
205
206 while (iterator1.hasNext() || iterator2.hasNext() || iterator3.hasNext()) {
207 val segment = iterator1.next()
208 val type = iterator2.next(points)
209 val type2 = iterator3.next(points2, 8)
210
211 assertEquals(type, segment.type)
212 assertEquals(type2, segment.type)
213
214 when (type) {
215 PathSegment.Type.Move -> {
216 assertPointsEquals(points, 0, segment.points[0])
217 assertPointsEquals(points2, 4, segment.points[0])
218 }
219 PathSegment.Type.Line -> {
220 assertPointsEquals(points, 0, segment.points[0])
221 assertPointsEquals(points, 1, segment.points[1])
222 assertPointsEquals(points2, 4, segment.points[0])
223 assertPointsEquals(points2, 5, segment.points[1])
224 }
225 PathSegment.Type.Quadratic -> {
226 assertPointsEquals(points, 0, segment.points[0])
227 assertPointsEquals(points, 1, segment.points[1])
228 assertPointsEquals(points, 2, segment.points[2])
229 assertPointsEquals(points2, 4, segment.points[0])
230 assertPointsEquals(points2, 5, segment.points[1])
231 assertPointsEquals(points2, 6, segment.points[2])
232 }
233 PathSegment.Type.Conic -> {
234 assertPointsEquals(points, 0, segment.points[0])
235 assertPointsEquals(points, 1, segment.points[1])
236 assertPointsEquals(points, 2, segment.points[2])
237 // Weight is stored after all of the points
238 assertEquals(points[6], segment.weight)
239
240 assertPointsEquals(points2, 4, segment.points[0])
241 assertPointsEquals(points2, 5, segment.points[1])
242 assertPointsEquals(points2, 6, segment.points[2])
243 // Weight is stored after all of the points
244 assertEquals(points2[14], segment.weight)
245 }
246 PathSegment.Type.Cubic -> {
247 assertPointsEquals(points, 0, segment.points[0])
248 assertPointsEquals(points, 1, segment.points[1])
249 assertPointsEquals(points, 2, segment.points[2])
250 assertPointsEquals(points, 3, segment.points[3])
251
252 assertPointsEquals(points2, 4, segment.points[0])
253 assertPointsEquals(points2, 5, segment.points[1])
254 assertPointsEquals(points2, 6, segment.points[2])
255 assertPointsEquals(points2, 7, segment.points[3])
256 }
257 PathSegment.Type.Close -> {}
258 PathSegment.Type.Done -> {}
259 }
260 }
261 }
262
263 @Test
donenull264 fun done() {
265 val path = Path().apply { close() }
266
267 val segment = path.iterator().next()
268
269 assertEquals(PathSegment.Type.Done, segment.type)
270 assertEquals(0, segment.points.size)
271 assertEquals(0.0f, segment.weight)
272 }
273
274 @Test
closenull275 fun close() {
276 val path =
277 Path().apply {
278 lineTo(10.0f, 12.0f)
279 close()
280 }
281
282 val iterator = path.iterator()
283 // Swallow the move
284 iterator.next()
285 // Swallow the line
286 iterator.next()
287
288 val segment = iterator.next()
289
290 assertEquals(PathSegment.Type.Close, segment.type)
291 assertEquals(0, segment.points.size)
292 assertEquals(0.0f, segment.weight)
293 }
294
295 @Test
moveTonull296 fun moveTo() {
297 val path = Path().apply { moveTo(10.0f, 12.0f) }
298
299 val segment = path.iterator().next()
300
301 assertEquals(PathSegment.Type.Move, segment.type)
302 assertEquals(1, segment.points.size)
303 assertPointsEquals(PointF(10.0f, 12.0f), segment.points[0])
304 assertEquals(0.0f, segment.weight)
305 }
306
307 @Test
lineTonull308 fun lineTo() {
309 val path =
310 Path().apply {
311 moveTo(4.0f, 6.0f)
312 lineTo(10.0f, 12.0f)
313 }
314
315 val iterator = path.iterator()
316 // Swallow the move
317 iterator.next()
318
319 val segment = iterator.next()
320
321 assertEquals(PathSegment.Type.Line, segment.type)
322 assertEquals(2, segment.points.size)
323 assertPointsEquals(PointF(4.0f, 6.0f), segment.points[0])
324 assertPointsEquals(PointF(10.0f, 12.0f), segment.points[1])
325 assertEquals(0.0f, segment.weight)
326 }
327
328 @Test
quadraticTonull329 fun quadraticTo() {
330 val path =
331 Path().apply {
332 moveTo(4.0f, 6.0f)
333 quadTo(10.0f, 12.0f, 20.0f, 24.0f)
334 }
335
336 val iterator = path.iterator()
337 // Swallow the move
338 iterator.next()
339
340 val segment = iterator.next()
341
342 assertEquals(PathSegment.Type.Quadratic, segment.type)
343 assertEquals(3, segment.points.size)
344 assertPointsEquals(PointF(4.0f, 6.0f), segment.points[0])
345 assertPointsEquals(PointF(10.0f, 12.0f), segment.points[1])
346 assertPointsEquals(PointF(20.0f, 24.0f), segment.points[2])
347 assertEquals(0.0f, segment.weight)
348 }
349
350 @Test
cubicTonull351 fun cubicTo() {
352 val path =
353 Path().apply {
354 moveTo(4.0f, 6.0f)
355 cubicTo(10.0f, 12.0f, 20.0f, 24.0f, 30.0f, 36.0f)
356 }
357
358 val iterator = path.iterator()
359 // Swallow the move
360 iterator.next()
361
362 val segment = iterator.next()
363
364 assertEquals(PathSegment.Type.Cubic, segment.type)
365 assertEquals(4, segment.points.size)
366 assertPointsEquals(PointF(4.0f, 6.0f), segment.points[0])
367 assertPointsEquals(PointF(10.0f, 12.0f), segment.points[1])
368 assertPointsEquals(PointF(20.0f, 24.0f), segment.points[2])
369 assertPointsEquals(PointF(30.0f, 36.0f), segment.points[3])
370 assertEquals(0.0f, segment.weight)
371 }
372
373 @Test
conicTonull374 fun conicTo() {
375 if (Build.VERSION.SDK_INT >= 25) {
376 val path =
377 Path().apply {
378 addRoundRect(RectF(12.0f, 12.0f, 24.0f, 24.0f), 8.0f, 8.0f, Path.Direction.CW)
379 }
380
381 val iterator = path.iterator(PathIterator.ConicEvaluation.AsConic)
382 // Swallow the move
383 iterator.next()
384
385 val segment = iterator.next()
386
387 assertEquals(PathSegment.Type.Conic, segment.type)
388 assertEquals(3, segment.points.size)
389
390 assertPointsEquals(PointF(12.0f, 18.0f), segment.points[0])
391 assertPointsEquals(PointF(12.0f, 12.0f), segment.points[1])
392 assertPointsEquals(PointF(18.0f, 12.0f), segment.points[2])
393 assertEquals(0.70710677f, segment.weight)
394 }
395 }
396
397 @Test
conicAsQuadraticsnull398 fun conicAsQuadratics() {
399 val path =
400 Path().apply {
401 addRoundRect(RectF(12.0f, 12.0f, 24.0f, 24.0f), 8.0f, 8.0f, Path.Direction.CW)
402 }
403
404 for (segment in path) {
405 if (segment.type == PathSegment.Type.Conic) fail("Found conic, none expected: $segment")
406 }
407 }
408
409 @Test
convertedConicsnull410 fun convertedConics() {
411 val path1 =
412 Path().apply {
413 addRoundRect(RectF(12.0f, 12.0f, 64.0f, 64.0f), 12.0f, 12.0f, Path.Direction.CW)
414 }
415
416 val path2 = Path()
417 for (segment in path1) {
418 when (segment.type) {
419 PathSegment.Type.Move -> path2.moveTo(segment.points[0].x, segment.points[0].y)
420 PathSegment.Type.Line -> path2.lineTo(segment.points[1].x, segment.points[1].y)
421 PathSegment.Type.Quadratic ->
422 path2.quadTo(
423 segment.points[1].x,
424 segment.points[1].y,
425 segment.points[2].x,
426 segment.points[2].y
427 )
428 PathSegment.Type.Conic -> fail("Unexpected conic! $segment")
429 PathSegment.Type.Cubic ->
430 path2.cubicTo(
431 segment.points[1].x,
432 segment.points[1].y,
433 segment.points[2].x,
434 segment.points[2].y,
435 segment.points[3].x,
436 segment.points[3].y
437 )
438 PathSegment.Type.Close -> path2.close()
439 PathSegment.Type.Done -> {}
440 }
441 }
442
443 // Now with smaller error tolerance
444 val path3 = Path()
445 for (segment in
446 path1.iterator(conicEvaluation = PathIterator.ConicEvaluation.AsQuadratics, 0.001f)) {
447 when (segment.type) {
448 PathSegment.Type.Move -> path3.moveTo(segment.points[0].x, segment.points[0].y)
449 PathSegment.Type.Line -> path3.lineTo(segment.points[1].x, segment.points[1].y)
450 PathSegment.Type.Quadratic ->
451 path3.quadTo(
452 segment.points[1].x,
453 segment.points[1].y,
454 segment.points[2].x,
455 segment.points[2].y
456 )
457 PathSegment.Type.Conic -> fail("Unexpected conic! $segment")
458 PathSegment.Type.Cubic ->
459 path3.cubicTo(
460 segment.points[1].x,
461 segment.points[1].y,
462 segment.points[2].x,
463 segment.points[2].y,
464 segment.points[3].x,
465 segment.points[3].y
466 )
467 PathSegment.Type.Close -> path3.close()
468 PathSegment.Type.Done -> {}
469 }
470 }
471
472 val b1 =
473 createBitmap(76, 76).applyCanvas {
474 drawARGB(255, 255, 255, 255)
475 drawPath(
476 path1,
477 Paint().apply {
478 color = argb(1.0f, 0.0f, 0.0f, 1.0f)
479 strokeWidth = 2.0f
480 isAntiAlias = true
481 style = Paint.Style.STROKE
482 }
483 )
484 }
485
486 val b2 =
487 createBitmap(76, 76).applyCanvas {
488 drawARGB(255, 255, 255, 255)
489 drawPath(
490 path2,
491 Paint().apply {
492 color = argb(1.0f, 0.0f, 0.0f, 1.0f)
493 strokeWidth = 2.0f
494 isAntiAlias = true
495 style = Paint.Style.STROKE
496 }
497 )
498 }
499
500 compareBitmaps(b1, b2)
501 // Note: b1-vs-b3 is not a valid comparison; default Skia rendering does not use an
502 // error tolerance that low. The test for fine-precision in path3 was just to
503 // ensure that the system could handle the extra data and operations required
504 }
505
506 @Test
sizesnull507 fun sizes() {
508 val path = Path()
509 var iterator: PathIterator = path.iterator()
510
511 assertEquals(0, iterator.calculateSize())
512
513 path.addRoundRect(RectF(12.0f, 12.0f, 64.0f, 64.0f), 8.0f, 8.0f, Path.Direction.CW)
514
515 // Skia converted
516 if (Build.VERSION.SDK_INT > 22) {
517 // Preserve conics and count
518 iterator = path.iterator(PathIterator.ConicEvaluation.AsConic)
519 assertEquals(10, iterator.calculateSize())
520 assertEquals(iterator.calculateSize(false), iterator.calculateSize())
521 }
522
523 // Convert conics and count
524 iterator = path.iterator(PathIterator.ConicEvaluation.AsQuadratics)
525 if (Build.VERSION.SDK_INT > 22) {
526 // simple size, not including conic conversion
527 assertEquals(10, iterator.calculateSize(false))
528 } else {
529 // round rects pre-API22 used line/quad/quad for each corner
530 assertEquals(14, iterator.calculateSize(false))
531 }
532 // now get the size with converted conics
533 assertEquals(14, iterator.calculateSize())
534 }
535 }
536
argbnull537 fun argb(alpha: Float, red: Float, green: Float, blue: Float) =
538 ((alpha * 255.0f + 0.5f).toInt() shl 24) or
539 ((red * 255.0f + 0.5f).toInt() shl 16) or
540 ((green * 255.0f + 0.5f).toInt() shl 8) or
541 (blue * 255.0f + 0.5f).toInt()
542