1 /*
<lambda>null2 * Copyright 2019 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.compose.ui.graphics
18
19 import androidx.compose.ui.geometry.Offset
20 import androidx.compose.ui.geometry.Rect
21 import androidx.compose.ui.unit.IntOffset
22 import androidx.compose.ui.unit.IntSize
23 import androidx.compose.ui.util.fastForEach
24
25 actual typealias NativeCanvas = android.graphics.Canvas
26
27 /** Create a new Canvas instance that targets its drawing commands to the provided [ImageBitmap] */
28 internal actual fun ActualCanvas(image: ImageBitmap): Canvas =
29 AndroidCanvas().apply { internalCanvas = android.graphics.Canvas(image.asAndroidBitmap()) }
30
<lambda>null31 fun Canvas(c: android.graphics.Canvas): Canvas = AndroidCanvas().apply { internalCanvas = c }
32
33 /**
34 * Holder class that is used to issue scoped calls to a [Canvas] from the framework equivalent
35 * canvas without having to allocate an object on each draw call
36 */
37 class CanvasHolder {
38 @PublishedApi internal val androidCanvas = AndroidCanvas()
39
drawIntonull40 inline fun drawInto(targetCanvas: android.graphics.Canvas, block: Canvas.() -> Unit) {
41 val previousCanvas = androidCanvas.internalCanvas
42 androidCanvas.internalCanvas = targetCanvas
43 androidCanvas.block()
44 androidCanvas.internalCanvas = previousCanvas
45 }
46 }
47
48 /** Return an instance of the native primitive that implements the Canvas interface */
49 actual val Canvas.nativeCanvas: NativeCanvas
50 get() = (this as AndroidCanvas).internalCanvas
51
52 // Stub canvas instance used to keep the internal canvas parameter non-null during its
53 // scoped usage and prevent unnecessary byte code null checks from being generated
54 private val EmptyCanvas = android.graphics.Canvas()
55
56 @PublishedApi
57 internal class AndroidCanvas() : Canvas {
58
59 // Keep the internal canvas as a var prevent having to allocate an AndroidCanvas
60 // instance on each draw call
61 @PublishedApi internal var internalCanvas: NativeCanvas = EmptyCanvas
62
63 private var srcRect: android.graphics.Rect? = null
64
65 private var dstRect: android.graphics.Rect? = null
66
67 /** @see Canvas.save */
savenull68 override fun save() {
69 internalCanvas.save()
70 }
71
72 /** @see Canvas.restore */
restorenull73 override fun restore() {
74 internalCanvas.restore()
75 }
76
77 /** @see Canvas.saveLayer */
78 @SuppressWarnings("deprecation")
saveLayernull79 override fun saveLayer(bounds: Rect, paint: Paint) {
80 @Suppress("DEPRECATION")
81 internalCanvas.saveLayer(
82 bounds.left,
83 bounds.top,
84 bounds.right,
85 bounds.bottom,
86 paint.asFrameworkPaint(),
87 android.graphics.Canvas.ALL_SAVE_FLAG
88 )
89 }
90
91 /** @see Canvas.translate */
translatenull92 override fun translate(dx: Float, dy: Float) {
93 internalCanvas.translate(dx, dy)
94 }
95
96 /** @see Canvas.scale */
scalenull97 override fun scale(sx: Float, sy: Float) {
98 internalCanvas.scale(sx, sy)
99 }
100
101 /** @see Canvas.rotate */
rotatenull102 override fun rotate(degrees: Float) {
103 internalCanvas.rotate(degrees)
104 }
105
106 /** @see Canvas.skew */
skewnull107 override fun skew(sx: Float, sy: Float) {
108 internalCanvas.skew(sx, sy)
109 }
110
111 /** @throws IllegalStateException if an arbitrary transform is provided */
concatnull112 override fun concat(matrix: Matrix) {
113 if (!matrix.isIdentity()) {
114 val frameworkMatrix = android.graphics.Matrix()
115 frameworkMatrix.setFrom(matrix)
116 internalCanvas.concat(frameworkMatrix)
117 }
118 }
119
120 @SuppressWarnings("deprecation")
clipRectnull121 override fun clipRect(left: Float, top: Float, right: Float, bottom: Float, clipOp: ClipOp) {
122 @Suppress("DEPRECATION")
123 internalCanvas.clipRect(left, top, right, bottom, clipOp.toRegionOp())
124 }
125
126 /** @see Canvas.clipPath */
clipPathnull127 override fun clipPath(path: Path, clipOp: ClipOp) {
128 @Suppress("DEPRECATION") internalCanvas.clipPath(path.asAndroidPath(), clipOp.toRegionOp())
129 }
130
toRegionOpnull131 fun ClipOp.toRegionOp(): android.graphics.Region.Op =
132 when (this) {
133 ClipOp.Difference -> android.graphics.Region.Op.DIFFERENCE
134 else -> android.graphics.Region.Op.INTERSECT
135 }
136
137 /** @see Canvas.drawLine */
drawLinenull138 override fun drawLine(p1: Offset, p2: Offset, paint: Paint) {
139 internalCanvas.drawLine(p1.x, p1.y, p2.x, p2.y, paint.asFrameworkPaint())
140 }
141
drawRectnull142 override fun drawRect(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) {
143 internalCanvas.drawRect(left, top, right, bottom, paint.asFrameworkPaint())
144 }
145
drawRoundRectnull146 override fun drawRoundRect(
147 left: Float,
148 top: Float,
149 right: Float,
150 bottom: Float,
151 radiusX: Float,
152 radiusY: Float,
153 paint: Paint
154 ) {
155 internalCanvas.drawRoundRect(
156 left,
157 top,
158 right,
159 bottom,
160 radiusX,
161 radiusY,
162 paint.asFrameworkPaint()
163 )
164 }
165
drawOvalnull166 override fun drawOval(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) {
167 internalCanvas.drawOval(left, top, right, bottom, paint.asFrameworkPaint())
168 }
169
170 /** @see Canvas.drawCircle */
drawCirclenull171 override fun drawCircle(center: Offset, radius: Float, paint: Paint) {
172 internalCanvas.drawCircle(center.x, center.y, radius, paint.asFrameworkPaint())
173 }
174
drawArcnull175 override fun drawArc(
176 left: Float,
177 top: Float,
178 right: Float,
179 bottom: Float,
180 startAngle: Float,
181 sweepAngle: Float,
182 useCenter: Boolean,
183 paint: Paint
184 ) {
185 internalCanvas.drawArc(
186 left,
187 top,
188 right,
189 bottom,
190 startAngle,
191 sweepAngle,
192 useCenter,
193 paint.asFrameworkPaint()
194 )
195 }
196
197 /** @see Canvas.drawPath */
drawPathnull198 override fun drawPath(path: Path, paint: Paint) {
199 internalCanvas.drawPath(path.asAndroidPath(), paint.asFrameworkPaint())
200 }
201
202 /** @see Canvas.drawImage */
drawImagenull203 override fun drawImage(image: ImageBitmap, topLeftOffset: Offset, paint: Paint) {
204 internalCanvas.drawBitmap(
205 image.asAndroidBitmap(),
206 topLeftOffset.x,
207 topLeftOffset.y,
208 paint.asFrameworkPaint()
209 )
210 }
211
212 /** @See Canvas.drawImageRect */
drawImageRectnull213 override fun drawImageRect(
214 image: ImageBitmap,
215 srcOffset: IntOffset,
216 srcSize: IntSize,
217 dstOffset: IntOffset,
218 dstSize: IntSize,
219 paint: Paint
220 ) {
221 // There is no framework API to draw a subset of a target bitmap
222 // that consumes only primitives so lazily allocate a src and dst
223 // rect to populate the dimensions and re-use across calls
224 if (srcRect == null) {
225 srcRect = android.graphics.Rect()
226 dstRect = android.graphics.Rect()
227 }
228 internalCanvas.drawBitmap(
229 image.asAndroidBitmap(),
230 srcRect!!.apply {
231 left = srcOffset.x
232 top = srcOffset.y
233 right = srcOffset.x + srcSize.width
234 bottom = srcOffset.y + srcSize.height
235 },
236 dstRect!!.apply {
237 left = dstOffset.x
238 top = dstOffset.y
239 right = dstOffset.x + dstSize.width
240 bottom = dstOffset.y + dstSize.height
241 },
242 paint.asFrameworkPaint()
243 )
244 }
245
246 /** @see Canvas.drawPoints */
drawPointsnull247 override fun drawPoints(pointMode: PointMode, points: List<Offset>, paint: Paint) {
248 when (pointMode) {
249 // Draw a line between each pair of points, each point has at most one line
250 // If the number of points is odd, then the last point is ignored.
251 PointMode.Lines -> drawLines(points, paint, 2)
252
253 // Connect each adjacent point with a line
254 PointMode.Polygon -> drawLines(points, paint, 1)
255
256 // Draw a point at each provided coordinate
257 PointMode.Points -> drawPoints(points, paint)
258 }
259 }
260
enableZnull261 override fun enableZ() {
262 CanvasUtils.enableZ(internalCanvas, true)
263 }
264
disableZnull265 override fun disableZ() {
266 CanvasUtils.enableZ(internalCanvas, false)
267 }
268
drawPointsnull269 private fun drawPoints(points: List<Offset>, paint: Paint) {
270 points.fastForEach { point ->
271 internalCanvas.drawPoint(point.x, point.y, paint.asFrameworkPaint())
272 }
273 }
274
275 /**
276 * Draw lines connecting points based on the corresponding step.
277 *
278 * ex. 3 points with a step of 1 would draw 2 lines between the first and second points and
279 * another between the second and third
280 *
281 * ex. 4 points with a step of 2 would draw 2 lines between the first and second and another
282 * between the third and fourth. If there is an odd number of points, the last point is ignored
283 *
284 * @see drawRawLines
285 */
drawLinesnull286 private fun drawLines(points: List<Offset>, paint: Paint, stepBy: Int) {
287 if (points.size >= 2) {
288 val frameworkPaint = paint.asFrameworkPaint()
289 var i = 0
290 while (i < points.size - 1) {
291 val p1 = points[i]
292 val p2 = points[i + 1]
293 internalCanvas.drawLine(p1.x, p1.y, p2.x, p2.y, frameworkPaint)
294 i += stepBy
295 }
296 }
297 }
298
299 /** @throws IllegalArgumentException if a non even number of points is provided */
drawRawPointsnull300 override fun drawRawPoints(pointMode: PointMode, points: FloatArray, paint: Paint) {
301 if (points.size % 2 != 0) {
302 throw IllegalArgumentException("points must have an even number of values")
303 }
304 when (pointMode) {
305 PointMode.Lines -> drawRawLines(points, paint, 2)
306 PointMode.Polygon -> drawRawLines(points, paint, 1)
307 PointMode.Points -> drawRawPoints(points, paint, 2)
308 }
309 }
310
drawRawPointsnull311 private fun drawRawPoints(points: FloatArray, paint: Paint, stepBy: Int) {
312 if (points.size % 2 == 0) {
313 val frameworkPaint = paint.asFrameworkPaint()
314 var i = 0
315 while (i < points.size - 1) {
316 val x = points[i]
317 val y = points[i + 1]
318 internalCanvas.drawPoint(x, y, frameworkPaint)
319 i += stepBy
320 }
321 }
322 }
323
324 /**
325 * Draw lines connecting points based on the corresponding step. The points are interpreted as
326 * x, y coordinate pairs in alternating index positions
327 *
328 * ex. 3 points with a step of 1 would draw 2 lines between the first and second points and
329 * another between the second and third
330 *
331 * ex. 4 points with a step of 2 would draw 2 lines between the first and second and another
332 * between the third and fourth. If there is an odd number of points, the last point is ignored
333 *
334 * @see drawLines
335 */
drawRawLinesnull336 private fun drawRawLines(points: FloatArray, paint: Paint, stepBy: Int) {
337 // Float array is treated as alternative set of x and y coordinates
338 // x1, y1, x2, y2, x3, y3, ... etc.
339 if (points.size >= 4 && points.size % 2 == 0) {
340 val frameworkPaint = paint.asFrameworkPaint()
341 var i = 0
342 while (i < points.size - 3) {
343 val x1 = points[i]
344 val y1 = points[i + 1]
345 val x2 = points[i + 2]
346 val y2 = points[i + 3]
347 internalCanvas.drawLine(x1, y1, x2, y2, frameworkPaint)
348 i += stepBy * 2
349 }
350 }
351 }
352
drawVerticesnull353 override fun drawVertices(vertices: Vertices, blendMode: BlendMode, paint: Paint) {
354 // TODO(njawad) align drawVertices blendMode parameter usage with framework
355 // android.graphics.Canvas#drawVertices does not consume a blendmode argument
356 internalCanvas.drawVertices(
357 vertices.vertexMode.toAndroidVertexMode(),
358 vertices.positions.size,
359 vertices.positions,
360 0, // TODO(njawad) figure out proper vertOffset)
361 vertices.textureCoordinates,
362 0, // TODO(njawad) figure out proper texOffset)
363 vertices.colors,
364 0, // TODO(njawad) figure out proper colorOffset)
365 vertices.indices,
366 0, // TODO(njawad) figure out proper indexOffset)
367 vertices.indices.size,
368 paint.asFrameworkPaint()
369 )
370 }
371 }
372