[go: nahoru, domu]

blob: c4b52b1b325df6f1e9ab345d70b929c993e2f3f7 [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.ui.graphics.vector
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionContext
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
/**
* Default identifier for the root group if a Vector graphic
*/
const val RootGroupName = "VectorRootGroup"
/**
* Create a [VectorPainter] with the Vector defined by the provided
* sub-composition
*
* @param [defaultWidth] Intrinsic width of the Vector in [Dp]
* @param [defaultHeight] Intrinsic height of the Vector in [Dp]
* @param [viewportWidth] Width of the viewport space. The viewport is the virtual canvas where
* paths are drawn on.
* This parameter is optional. Not providing it will use the [defaultWidth] converted to pixels
* @param [viewportHeight] Height of the viewport space. The viewport is the virtual canvas where
* paths are drawn on.
* This parameter is optional. Not providing it will use the [defaultHeight] converted to pixels
* @param [name] optional identifier used to identify the root of this vector graphic
* @param [tintColor] optional color used to tint the root group of this vector graphic
* @param [tintBlendMode] BlendMode used in combination with [tintColor]
* @param [content] Composable used to define the structure and contents of the vector graphic
*/
@Composable
fun rememberVectorPainter(
defaultWidth: Dp,
defaultHeight: Dp,
viewportWidth: Float = Float.NaN,
viewportHeight: Float = Float.NaN,
name: String = RootGroupName,
tintColor: Color = Color.Unspecified,
tintBlendMode: BlendMode = BlendMode.SrcIn,
content: @Composable (viewportWidth: Float, viewportHeight: Float) -> Unit
): VectorPainter {
val density = LocalDensity.current
val widthPx = with(density) { defaultWidth.toPx() }
val heightPx = with(density) { defaultHeight.toPx() }
val vpWidth = if (viewportWidth.isNaN()) widthPx else viewportWidth
val vpHeight = if (viewportHeight.isNaN()) heightPx else viewportHeight
val painter = remember { VectorPainter() }.apply {
// This assignment is thread safe as the internal Size parameter is
// backed by a mutableState object
size = Size(widthPx, heightPx)
RenderVector(name, vpWidth, vpHeight, content)
}
SideEffect {
// Initialize the intrinsic color filter if a tint color is provided on the
// vector itself. Note this tint can be overridden by an explicit ColorFilter
// provided on the Modifier.paint call
painter.intrinsicColorFilter = if (tintColor != Color.Unspecified) {
ColorFilter.tint(tintColor, tintBlendMode)
} else {
null
}
}
return painter
}
/**
* Create a [VectorPainter] with the given [ImageVector]. This will create a
* sub-composition of the vector hierarchy given the tree structure in [ImageVector]
*
* @param [image] ImageVector used to create a vector graphic sub-composition
*/
@Composable
fun rememberVectorPainter(image: ImageVector) =
rememberVectorPainter(
defaultWidth = image.defaultWidth,
defaultHeight = image.defaultHeight,
viewportWidth = image.viewportWidth,
viewportHeight = image.viewportHeight,
name = image.name,
tintColor = image.tintColor,
tintBlendMode = image.tintBlendMode,
content = { _, _ -> RenderVectorGroup(group = image.root) }
)
/**
* [Painter] implementation that abstracts the drawing of a Vector graphic.
* This can be represented by either a [ImageVector] or a programmatic
* composition of a vector
*/
class VectorPainter internal constructor() : Painter() {
internal var size by mutableStateOf(Size.Zero)
/**
* configures the intrinsic tint that may be defined on a VectorPainter
*/
internal var intrinsicColorFilter: ColorFilter?
get() = vector.intrinsicColorFilter
set(value) {
vector.intrinsicColorFilter = value
}
private val vector = VectorComponent().apply {
invalidateCallback = {
isDirty = true
}
}
private var composition: Composition? = null
private fun composeVector(
parent: CompositionContext,
composable: @Composable (viewportWidth: Float, viewportHeight: Float) -> Unit
): Composition {
val existing = composition
val next = if (existing == null || existing.isDisposed) {
Composition(
VectorApplier(vector.root),
parent
)
} else {
existing
}
composition = next
next.setContent {
composable(vector.viewportWidth, vector.viewportHeight)
}
return next
}
private var isDirty by mutableStateOf(true)
@Composable
internal fun RenderVector(
name: String,
viewportWidth: Float,
viewportHeight: Float,
content: @Composable (viewportWidth: Float, viewportHeight: Float) -> Unit
) {
vector.apply {
this.name = name
this.viewportWidth = viewportWidth
this.viewportHeight = viewportHeight
}
val composition = composeVector(
rememberCompositionContext(),
content
)
DisposableEffect(composition) {
onDispose {
composition.dispose()
}
}
}
private var currentAlpha: Float = 1.0f
private var currentColorFilter: ColorFilter? = null
override val intrinsicSize: Size
get() = size
override fun DrawScope.onDraw() {
with(vector) {
draw(currentAlpha, currentColorFilter ?: intrinsicColorFilter)
}
// This conditional is necessary to obtain invalidation callbacks as the state is
// being read here which adds this callback to the snapshot observation
if (isDirty) {
isDirty = false
}
}
override fun applyAlpha(alpha: Float): Boolean {
currentAlpha = alpha
return true
}
override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
currentColorFilter = colorFilter
return true
}
}
/**
* Returns all the properties of PathComponent or GroupComponent that can be overridden for
* animation. This can be passed to [RenderVectorGroup] to override some property values when the
* [VectorGroup] is rendered.
*/
internal interface VectorOverride {
/**
* Overrides the 'rotation' attribute for a vector group.
*/
fun obtainRotation(rotation: Float): Float = rotation
/**
* Overrides the 'pivotX' attribute for a vector group.
*/
fun obtainPivotX(pivotX: Float): Float = pivotX
/**
* Overrides the 'pivotY' attribute for a vector group.
*/
fun obtainPivotY(pivotY: Float): Float = pivotY
/**
* Overrides the 'scaleX' attribute for a vector group.
*/
fun obtainScaleX(scaleX: Float): Float = scaleX
/**
* Overrides the 'scaleY' attribute for a vector group.
*/
fun obtainScaleY(scaleY: Float): Float = scaleY
/**
* Overrides the 'translateX' attribute for a vector group.
*/
fun obtainTranslateX(translateX: Float): Float = translateX
/**
* Overrides the 'translateY' attribute for a vector group.
*/
fun obtainTranslateY(translateY: Float): Float = translateY
/**
* Overrides the 'pathData' attribute for a vector path or a clip path.
*/
fun obtainPathData(pathData: List<PathNode>): List<PathNode> = pathData
/**
* Overrides the 'fill' attribute for a vector path.
*/
fun obtainFill(fill: Brush?): Brush? = fill
/**
* Overrides the 'fillAlpha' attribute for a vector path.
*/
fun obtainFillAlpha(fillAlpha: Float): Float = fillAlpha
/**
* Overrides the 'stroke' attribute for a vector path.
*/
fun obtainStroke(stroke: Brush?): Brush? = stroke
/**
* Overrides the 'strokeWidth' attribute for a vector path.
*/
fun obtainStrokeWidth(strokeWidth: Float): Float = strokeWidth
/**
* Overrides the 'strokeAlpha' attribute for a vector path.
*/
fun obtainStrokeAlpha(strokeAlpha: Float): Float = strokeAlpha
/**
* Overrides the 'trimPathStart' attribute for a vector path.
*/
fun obtainTrimPathStart(trimPathStart: Float): Float = trimPathStart
/**
* Overrides the 'trimPathEnd' attribute for a vector path.
*/
fun obtainTrimPathEnd(trimPathEnd: Float): Float = trimPathEnd
/**
* Overrides the 'trimPathOffset' attribute for a vector path.
*/
fun obtainTrimPathOffset(trimPathOffset: Float): Float = trimPathOffset
}
private object DefaultVectorOverride : VectorOverride
/**
* Recursive method for creating the vector graphic composition by traversing
* the tree structure
*/
@Composable
internal fun RenderVectorGroup(
group: VectorGroup,
overrides: Map<String, VectorOverride> = emptyMap()
) {
for (vectorNode in group) {
if (vectorNode is VectorPath) {
val override = overrides[vectorNode.name] ?: DefaultVectorOverride
Path(
pathData = override.obtainPathData(vectorNode.pathData),
pathFillType = vectorNode.pathFillType,
name = vectorNode.name,
fill = override.obtainFill(vectorNode.fill),
fillAlpha = override.obtainFillAlpha(vectorNode.fillAlpha),
stroke = override.obtainStroke(vectorNode.stroke),
strokeAlpha = override.obtainStrokeAlpha(vectorNode.strokeAlpha),
strokeLineWidth = override.obtainStrokeWidth(vectorNode.strokeLineWidth),
strokeLineCap = vectorNode.strokeLineCap,
strokeLineJoin = vectorNode.strokeLineJoin,
strokeLineMiter = vectorNode.strokeLineMiter,
trimPathStart = override.obtainTrimPathStart(vectorNode.trimPathStart),
trimPathEnd = override.obtainTrimPathEnd(vectorNode.trimPathEnd),
trimPathOffset = override.obtainTrimPathOffset(vectorNode.trimPathOffset)
)
} else if (vectorNode is VectorGroup) {
val override = overrides[vectorNode.name] ?: DefaultVectorOverride
Group(
name = vectorNode.name,
rotation = override.obtainRotation(vectorNode.rotation),
scaleX = override.obtainScaleX(vectorNode.scaleX),
scaleY = override.obtainScaleY(vectorNode.scaleY),
translationX = override.obtainTranslateX(vectorNode.translationX),
translationY = override.obtainTranslateY(vectorNode.translationY),
pivotX = override.obtainPivotX(vectorNode.pivotX),
pivotY = override.obtainPivotY(vectorNode.pivotY),
clipPathData = override.obtainPathData(vectorNode.clipPathData)
) {
RenderVectorGroup(group = vectorNode, overrides = overrides)
}
}
}
}