1 /*
2 * Copyright 2020 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.vector
18
19 import androidx.compose.runtime.Composable
20 import androidx.compose.runtime.ComposableOpenTarget
21 import androidx.compose.runtime.Composition
22 import androidx.compose.runtime.DisposableEffect
23 import androidx.compose.runtime.getValue
24 import androidx.compose.runtime.mutableIntStateOf
25 import androidx.compose.runtime.mutableStateOf
26 import androidx.compose.runtime.remember
27 import androidx.compose.runtime.rememberCompositionContext
28 import androidx.compose.runtime.setValue
29 import androidx.compose.ui.geometry.Size
30 import androidx.compose.ui.graphics.BlendMode
31 import androidx.compose.ui.graphics.Brush
32 import androidx.compose.ui.graphics.Color
33 import androidx.compose.ui.graphics.ColorFilter
34 import androidx.compose.ui.graphics.ImageBitmapConfig
35 import androidx.compose.ui.graphics.drawscope.DrawScope
36 import androidx.compose.ui.graphics.drawscope.scale
37 import androidx.compose.ui.graphics.isSpecified
38 import androidx.compose.ui.graphics.painter.Painter
39 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
40 import androidx.compose.ui.platform.LocalDensity
41 import androidx.compose.ui.unit.Density
42 import androidx.compose.ui.unit.Dp
43 import androidx.compose.ui.unit.LayoutDirection
44 import androidx.compose.ui.util.packFloats
45
46 /** Default identifier for the root group if a Vector graphic */
47 const val RootGroupName = "VectorRootGroup"
48
49 /**
50 * Create a [VectorPainter] with the Vector defined by the provided sub-composition
51 *
52 * @param [defaultWidth] Intrinsic width of the Vector in [Dp]
53 * @param [defaultHeight] Intrinsic height of the Vector in [Dp]
54 * @param [viewportWidth] Width of the viewport space. The viewport is the virtual canvas where
55 * paths are drawn on. This parameter is optional. Not providing it will use the [defaultWidth]
56 * converted to pixels
57 * @param [viewportHeight] Height of the viewport space. The viewport is the virtual canvas where
58 * paths are drawn on. This parameter is optional. Not providing it will use the [defaultHeight]
59 * converted to pixels
60 * @param [name] optional identifier used to identify the root of this vector graphic
61 * @param [tintColor] optional color used to tint the root group of this vector graphic
62 * @param [tintBlendMode] BlendMode used in combination with [tintColor]
63 * @param [content] Composable used to define the structure and contents of the vector graphic
64 */
65 @Deprecated(
66 "Replace rememberVectorPainter graphicsLayer that consumes the auto mirror flag",
67 replaceWith =
68 ReplaceWith(
69 "rememberVectorPainter(defaultWidth, defaultHeight, viewportWidth, " +
70 "viewportHeight, name, tintColor, tintBlendMode, false, content)",
71 "androidx.compose.ui.graphics.vector"
72 )
73 )
74 @Composable
75 @ComposableOpenTarget(-1)
rememberVectorPainternull76 fun rememberVectorPainter(
77 defaultWidth: Dp,
78 defaultHeight: Dp,
79 viewportWidth: Float = Float.NaN,
80 viewportHeight: Float = Float.NaN,
81 name: String = RootGroupName,
82 tintColor: Color = Color.Unspecified,
83 tintBlendMode: BlendMode = BlendMode.SrcIn,
84 content: @Composable @VectorComposable (viewportWidth: Float, viewportHeight: Float) -> Unit
85 ): VectorPainter =
86 rememberVectorPainter(
87 defaultWidth,
88 defaultHeight,
89 viewportWidth,
90 viewportHeight,
91 name,
92 tintColor,
93 tintBlendMode,
94 false,
95 content
96 )
97
98 /**
99 * Create a [VectorPainter] with the Vector defined by the provided sub-composition.
100 *
101 * Inside [content] use the [Group] and [Path] composables to define the vector.
102 *
103 * @param [defaultWidth] Intrinsic width of the Vector in [Dp]
104 * @param [defaultHeight] Intrinsic height of the Vector in [Dp]
105 * @param [viewportWidth] Width of the viewport space. The viewport is the virtual canvas where
106 * paths are drawn on. This parameter is optional. Not providing it will use the [defaultWidth]
107 * converted to pixels
108 * @param [viewportHeight] Height of the viewport space. The viewport is the virtual canvas where
109 * paths are drawn on. This parameter is optional. Not providing it will use the [defaultHeight]
110 * converted to pixels
111 * @param [name] optional identifier used to identify the root of this vector graphic
112 * @param [tintColor] optional color used to tint the root group of this vector graphic
113 * @param [tintBlendMode] BlendMode used in combination with [tintColor]
114 * @param [autoMirror] Determines if the contents of the Vector should be mirrored for right to left
115 * layouts.
116 * @param [content] Composable used to define the structure and contents of the vector graphic
117 */
118 @Composable
119 @ComposableOpenTarget(-1)
120 fun rememberVectorPainter(
121 defaultWidth: Dp,
122 defaultHeight: Dp,
123 viewportWidth: Float = Float.NaN,
124 viewportHeight: Float = Float.NaN,
125 name: String = RootGroupName,
126 tintColor: Color = Color.Unspecified,
127 tintBlendMode: BlendMode = BlendMode.SrcIn,
128 autoMirror: Boolean = false,
129 content: @Composable @VectorComposable (viewportWidth: Float, viewportHeight: Float) -> Unit
130 ): VectorPainter {
131 val density = LocalDensity.current
132 val defaultSize = density.obtainSizePx(defaultWidth, defaultHeight)
133 val viewport = obtainViewportSize(defaultSize, viewportWidth, viewportHeight)
134 val intrinsicColorFilter =
135 remember(tintColor, tintBlendMode) { createColorFilter(tintColor, tintBlendMode) }
136 return remember { VectorPainter() }
137 .apply {
138 configureVectorPainter(
139 defaultSize = defaultSize,
140 viewportSize = viewport,
141 name = name,
142 intrinsicColorFilter = intrinsicColorFilter,
143 autoMirror = autoMirror
144 )
145 val compositionContext = rememberCompositionContext()
146 val composition =
147 remember(viewportWidth, viewportHeight, content) {
148 val curComp = this.composition
149 val next =
150 if (curComp == null || curComp.isDisposed) {
151 Composition(VectorApplier(this.vector.root), compositionContext)
152 } else {
153 curComp
154 }
155 next.setContent { content(viewport.width, viewport.height) }
156 next
157 }
158 this.composition = composition
159 DisposableEffect(this) { onDispose { composition.dispose() } }
160 }
161 }
162
163 /**
164 * Create a [VectorPainter] with the given [ImageVector]. This will create a sub-composition of the
165 * vector hierarchy given the tree structure in [ImageVector]
166 *
167 * @param [image] ImageVector used to create a vector graphic sub-composition
168 */
169 @Composable
rememberVectorPainternull170 fun rememberVectorPainter(image: ImageVector): VectorPainter {
171 val density = LocalDensity.current
172 val key = packFloats(image.genId.toFloat(), density.density)
173 return remember(key) {
174 createVectorPainterFromImageVector(
175 density,
176 image,
177 GroupComponent().apply { createGroupComponent(image.root) }
178 )
179 }
180 }
181
182 /**
183 * [Painter] implementation that abstracts the drawing of a Vector graphic. This can be represented
184 * by either a [ImageVector] or a programmatic composition of a vector
185 */
186 class VectorPainter internal constructor(root: GroupComponent = GroupComponent()) : Painter() {
187
188 internal var size by mutableStateOf(Size.Zero)
189
190 internal var autoMirror by mutableStateOf(false)
191
192 /** configures the intrinsic tint that may be defined on a VectorPainter */
193 internal var intrinsicColorFilter: ColorFilter?
194 get() = vector.intrinsicColorFilter
195 set(value) {
196 vector.intrinsicColorFilter = value
197 }
198
199 internal var viewportSize: Size
200 get() = vector.viewportSize
201 set(value) {
202 vector.viewportSize = value
203 }
204
205 internal var name: String
206 get() = vector.name
207 set(value) {
208 vector.name = value
209 }
210
211 internal val vector =
<lambda>null212 VectorComponent(root).apply {
213 invalidateCallback = {
214 if (drawCount == invalidateCount) {
215 invalidateCount++
216 }
217 }
218 }
219
220 internal val bitmapConfig: ImageBitmapConfig
221 get() = vector.cacheBitmapConfig
222
223 internal var composition: Composition? = null
224
225 // TODO replace with mutableStateOf(Unit, neverEqualPolicy()) after b/291647821 is addressed
226 private var invalidateCount by mutableIntStateOf(0)
227
228 private var currentAlpha: Float = 1.0f
229 private var currentColorFilter: ColorFilter? = null
230
231 override val intrinsicSize: Size
232 get() = size
233
234 private var drawCount = -1
235
onDrawnull236 override fun DrawScope.onDraw() {
237 with(vector) {
238 val filter = currentColorFilter ?: intrinsicColorFilter
239 if (autoMirror && layoutDirection == LayoutDirection.Rtl) {
240 mirror { draw(currentAlpha, filter) }
241 } else {
242 draw(currentAlpha, filter)
243 }
244 }
245 // This assignment is necessary to obtain invalidation callbacks as the state is
246 // being read here which adds this callback to the snapshot observation
247 drawCount = invalidateCount
248 }
249
applyAlphanull250 override fun applyAlpha(alpha: Float): Boolean {
251 currentAlpha = alpha
252 return true
253 }
254
applyColorFilternull255 override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
256 currentColorFilter = colorFilter
257 return true
258 }
259 }
260
mirrornull261 private inline fun DrawScope.mirror(block: DrawScope.() -> Unit) {
262 scale(-1f, 1f, block = block)
263 }
264
265 /**
266 * Represents one of the properties for PathComponent or GroupComponent that can be overwritten when
267 * it is composed and drawn with [RenderVectorGroup].
268 */
269 sealed class VectorProperty<T> {
270 object Rotation : VectorProperty<Float>()
271
272 object PivotX : VectorProperty<Float>()
273
274 object PivotY : VectorProperty<Float>()
275
276 object ScaleX : VectorProperty<Float>()
277
278 object ScaleY : VectorProperty<Float>()
279
280 object TranslateX : VectorProperty<Float>()
281
282 object TranslateY : VectorProperty<Float>()
283
284 object PathData : VectorProperty<List<PathNode>>()
285
286 object Fill : VectorProperty<Brush?>()
287
288 object FillAlpha : VectorProperty<Float>()
289
290 object Stroke : VectorProperty<Brush?>()
291
292 object StrokeLineWidth : VectorProperty<Float>()
293
294 object StrokeAlpha : VectorProperty<Float>()
295
296 object TrimPathStart : VectorProperty<Float>()
297
298 object TrimPathEnd : VectorProperty<Float>()
299
300 object TrimPathOffset : VectorProperty<Float>()
301 }
302
303 /**
304 * Holds a set of values that overwrite the original property values of an [ImageVector]. This
305 * allows you to dynamically change any of the property values provided as [VectorProperty]. This
306 * can be passed to [RenderVectorGroup] to alter some property values when the [VectorGroup] is
307 * rendered.
308 */
309 @JvmDefaultWithCompatibility
310 interface VectorConfig {
getOrDefaultnull311 fun <T> getOrDefault(property: VectorProperty<T>, defaultValue: T): T {
312 return defaultValue
313 }
314 }
315
obtainSizePxnull316 private fun Density.obtainSizePx(defaultWidth: Dp, defaultHeight: Dp) =
317 Size(defaultWidth.toPx(), defaultHeight.toPx())
318
319 /**
320 * Helper method to calculate the viewport size. If the viewport width/height are not specified this
321 * falls back on the default size provided
322 */
323 private fun obtainViewportSize(defaultSize: Size, viewportWidth: Float, viewportHeight: Float) =
324 Size(
325 if (viewportWidth.isNaN()) defaultSize.width else viewportWidth,
326 if (viewportHeight.isNaN()) defaultSize.height else viewportHeight
327 )
328
329 /**
330 * Helper method to conditionally create a ColorFilter to tint contents if [tintColor] is specified,
331 * that is [Color.isSpecified] returns true
332 */
333 private fun createColorFilter(tintColor: Color, tintBlendMode: BlendMode): ColorFilter? =
334 if (tintColor.isSpecified) {
335 ColorFilter.tint(tintColor, tintBlendMode)
336 } else {
337 null
338 }
339
340 /** Helper method to configure the properties of a VectorPainter that maybe re-used */
configureVectorPainternull341 internal fun VectorPainter.configureVectorPainter(
342 defaultSize: Size,
343 viewportSize: Size,
344 name: String = RootGroupName,
345 intrinsicColorFilter: ColorFilter?,
346 autoMirror: Boolean = false,
347 ): VectorPainter = apply {
348 this.size = defaultSize
349 this.autoMirror = autoMirror
350 this.intrinsicColorFilter = intrinsicColorFilter
351 this.viewportSize = viewportSize
352 this.name = name
353 }
354
355 /** Helper method to create a VectorPainter instance from an ImageVector */
createVectorPainterFromImageVectornull356 internal fun createVectorPainterFromImageVector(
357 density: Density,
358 imageVector: ImageVector,
359 root: GroupComponent
360 ): VectorPainter {
361 val defaultSize = density.obtainSizePx(imageVector.defaultWidth, imageVector.defaultHeight)
362 val viewport =
363 obtainViewportSize(defaultSize, imageVector.viewportWidth, imageVector.viewportHeight)
364 return VectorPainter(root)
365 .configureVectorPainter(
366 defaultSize = defaultSize,
367 viewportSize = viewport,
368 name = imageVector.name,
369 intrinsicColorFilter =
370 createColorFilter(imageVector.tintColor, imageVector.tintBlendMode),
371 autoMirror = imageVector.autoMirror
372 )
373 }
374
375 /**
376 * statically create a a GroupComponent from the VectorGroup representation provided from an
377 * [ImageVector] instance
378 */
createGroupComponentnull379 internal fun GroupComponent.createGroupComponent(currentGroup: VectorGroup): GroupComponent {
380 for (index in 0 until currentGroup.size) {
381 val vectorNode = currentGroup[index]
382 if (vectorNode is VectorPath) {
383 val pathComponent =
384 PathComponent().apply {
385 pathData = vectorNode.pathData
386 pathFillType = vectorNode.pathFillType
387 name = vectorNode.name
388 fill = vectorNode.fill
389 fillAlpha = vectorNode.fillAlpha
390 stroke = vectorNode.stroke
391 strokeAlpha = vectorNode.strokeAlpha
392 strokeLineWidth = vectorNode.strokeLineWidth
393 strokeLineCap = vectorNode.strokeLineCap
394 strokeLineJoin = vectorNode.strokeLineJoin
395 strokeLineMiter = vectorNode.strokeLineMiter
396 trimPathStart = vectorNode.trimPathStart
397 trimPathEnd = vectorNode.trimPathEnd
398 trimPathOffset = vectorNode.trimPathOffset
399 }
400 insertAt(index, pathComponent)
401 } else if (vectorNode is VectorGroup) {
402 val groupComponent =
403 GroupComponent().apply {
404 name = vectorNode.name
405 rotation = vectorNode.rotation
406 scaleX = vectorNode.scaleX
407 scaleY = vectorNode.scaleY
408 translationX = vectorNode.translationX
409 translationY = vectorNode.translationY
410 pivotX = vectorNode.pivotX
411 pivotY = vectorNode.pivotY
412 clipPathData = vectorNode.clipPathData
413 createGroupComponent(vectorNode)
414 }
415 insertAt(index, groupComponent)
416 }
417 }
418 return this
419 }
420
421 /**
422 * Recursively creates the vector graphic composition by traversing the tree structure.
423 *
424 * @param group The vector group to render.
425 * @param configs An optional map of [VectorConfig] to provide animation values. The keys are the
426 * node names. The values are [VectorConfig] for that node.
427 */
428 @Composable
RenderVectorGroupnull429 fun RenderVectorGroup(group: VectorGroup, configs: Map<String, VectorConfig> = emptyMap()) {
430 for (vectorNode in group) {
431 if (vectorNode is VectorPath) {
432 val config = configs[vectorNode.name] ?: object : VectorConfig {}
433 Path(
434 pathData = config.getOrDefault(VectorProperty.PathData, vectorNode.pathData),
435 pathFillType = vectorNode.pathFillType,
436 name = vectorNode.name,
437 fill = config.getOrDefault(VectorProperty.Fill, vectorNode.fill),
438 fillAlpha = config.getOrDefault(VectorProperty.FillAlpha, vectorNode.fillAlpha),
439 stroke = config.getOrDefault(VectorProperty.Stroke, vectorNode.stroke),
440 strokeAlpha =
441 config.getOrDefault(VectorProperty.StrokeAlpha, vectorNode.strokeAlpha),
442 strokeLineWidth =
443 config.getOrDefault(VectorProperty.StrokeLineWidth, vectorNode.strokeLineWidth),
444 strokeLineCap = vectorNode.strokeLineCap,
445 strokeLineJoin = vectorNode.strokeLineJoin,
446 strokeLineMiter = vectorNode.strokeLineMiter,
447 trimPathStart =
448 config.getOrDefault(VectorProperty.TrimPathStart, vectorNode.trimPathStart),
449 trimPathEnd =
450 config.getOrDefault(VectorProperty.TrimPathEnd, vectorNode.trimPathEnd),
451 trimPathOffset =
452 config.getOrDefault(VectorProperty.TrimPathOffset, vectorNode.trimPathOffset)
453 )
454 } else if (vectorNode is VectorGroup) {
455 val config = configs[vectorNode.name] ?: object : VectorConfig {}
456 Group(
457 name = vectorNode.name,
458 rotation = config.getOrDefault(VectorProperty.Rotation, vectorNode.rotation),
459 scaleX = config.getOrDefault(VectorProperty.ScaleX, vectorNode.scaleX),
460 scaleY = config.getOrDefault(VectorProperty.ScaleY, vectorNode.scaleY),
461 translationX =
462 config.getOrDefault(VectorProperty.TranslateX, vectorNode.translationX),
463 translationY =
464 config.getOrDefault(VectorProperty.TranslateY, vectorNode.translationY),
465 pivotX = config.getOrDefault(VectorProperty.PivotX, vectorNode.pivotX),
466 pivotY = config.getOrDefault(VectorProperty.PivotY, vectorNode.pivotY),
467 clipPathData = config.getOrDefault(VectorProperty.PathData, vectorNode.clipPathData)
468 ) {
469 RenderVectorGroup(group = vectorNode, configs = configs)
470 }
471 }
472 }
473 }
474