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