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