aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2024-01-09 00:06:47 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2024-01-09 00:06:47 +0000
commit5119b8203f8556d77f7cf7b90e1681d59102a8a1 (patch)
tree80ae0b7358e1b5287d64cb3011086801ca5cb617
parent6dc685be02316455881d22b69d0bb8adbe768c4f (diff)
parent1794a549616ac082f7aad22e9fa5f3048a3a58ae (diff)
downloadsupport-5119b8203f8556d77f7cf7b90e1681d59102a8a1.tar.gz
Merge cherrypicks of ['android-review.googlesource.com/2865106'] into androidx-compose-release.
Change-Id: Id51533bd8f40a7b4d82feeefc9a43a4e69fe87bf
-rw-r--r--compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/graphics/vector/CreateVectorPainterBenchmark.kt43
-rw-r--r--compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt45
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt21
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt54
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt243
5 files changed, 296 insertions, 110 deletions
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/graphics/vector/CreateVectorPainterBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/graphics/vector/CreateVectorPainterBenchmark.kt
index e96dac558ff..00182f8a854 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/graphics/vector/CreateVectorPainterBenchmark.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/graphics/vector/CreateVectorPainterBenchmark.kt
@@ -52,6 +52,13 @@ class CreateVectorPainterBenchmark {
RecreateVectorPainterTestCase()
}, assertOneRecomposition = false)
}
+
+ @Test
+ fun renderVectorWithDifferentSizes() {
+ benchmarkRule.toggleStateBenchmarkDraw({
+ ResizeVectorPainter()
+ }, assertOneRecomposition = false)
+ }
}
private class RecreateVectorPainterTestCase : ComposeTestCase, ToggleableTestCase {
@@ -80,3 +87,39 @@ private class RecreateVectorPainterTestCase : ComposeTestCase, ToggleableTestCas
}
}
}
+
+private class ResizeVectorPainter : ComposeTestCase, ToggleableTestCase {
+
+ private var alpha by mutableStateOf(1f)
+
+ @Composable
+ override fun Content() {
+ Column {
+ Box(modifier = Modifier.wrapContentSize()) {
+ Image(
+ painter = painterResource(R.drawable.ic_hourglass),
+ contentDescription = null,
+ modifier = Modifier.size(100.dp),
+ alpha = alpha
+ )
+ }
+
+ Box(modifier = Modifier.wrapContentSize()) {
+ Image(
+ painter = painterResource(R.drawable.ic_hourglass),
+ contentDescription = null,
+ modifier = Modifier.size(200.dp),
+ alpha = alpha
+ )
+ }
+ }
+ }
+
+ override fun toggleState() {
+ if (alpha == 1.0f) {
+ alpha = 0.5f
+ } else {
+ alpha = 1.0f
+ }
+ }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
index 0e0a75b6088..812dabd1632 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/graphics/vector/VectorTest.kt
@@ -36,6 +36,7 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -175,6 +176,50 @@ class VectorTest {
}
}
+ @Test
+ fun testVectorSkipsRecompositionOnNoChange() {
+ val state = mutableIntStateOf(0)
+ var composeCount = 0
+ var vectorComposeCount = 0
+
+ val composeVector: @Composable @VectorComposable (Float, Float) -> Unit = {
+ viewportWidth, viewportHeight ->
+
+ vectorComposeCount++
+ Path(
+ fill = SolidColor(Color.Blue),
+ pathData = PathData {
+ lineTo(viewportWidth, 0f)
+ lineTo(viewportWidth, viewportHeight)
+ lineTo(0f, viewportHeight)
+ close()
+ }
+ )
+ }
+
+ rule.setContent {
+ composeCount++
+ // Arbitrary read to force composition here and verify the subcomposition below skips
+ state.value
+ val vectorPainter = rememberVectorPainter(
+ defaultWidth = 10.dp,
+ defaultHeight = 10.dp,
+ autoMirror = false,
+ content = composeVector
+ )
+ Image(
+ vectorPainter,
+ null,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+
+ state.value = 1
+ rule.waitForIdle()
+ assertEquals(2, composeCount) // Arbitrary state read should compose twice
+ assertEquals(1, vectorComposeCount) // Vector is identical so should compose once
+ }
+
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun testVectorInvalidation() {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt
index b18bbaa6ac0..82c5d37ab54 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt
@@ -77,7 +77,13 @@ class ImageVector internal constructor(
/**
* Determines if the vector asset should automatically be mirrored for right to left locales
*/
- val autoMirror: Boolean
+ val autoMirror: Boolean,
+
+ /**
+ * Identifier used to disambiguate between different ImageVector instances in a more efficient
+ * manner than equality. This can be used as a key for caching instances of ImageVectors.
+ */
+ internal val genId: Int = generateImageVectorId(),
) {
/**
* Builder used to construct a Vector graphic tree.
@@ -401,10 +407,15 @@ class ImageVector internal constructor(
)
}
- /**
- * Provide an empty companion object to hang platform-specific companion extensions onto.
- */
- companion object { } // ktlint-disable no-empty-class-body
+ companion object {
+ private var imageVectorCount = 0
+
+ internal fun generateImageVectorId(): Int {
+ synchronized(this) {
+ return imageVectorCount++
+ }
+ }
+ }
override fun equals(other: Any?): Boolean {
if (this === other) return true
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
index def8917592f..fd2d8fbb032 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
@@ -19,6 +19,7 @@ package androidx.compose.ui.graphics.vector
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.Size.Companion.Unspecified
import androidx.compose.ui.graphics.BlendMode
@@ -36,6 +37,7 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.isUnspecified
@@ -92,20 +94,15 @@ sealed class VNode {
abstract fun DrawScope.draw()
}
-internal class VectorComponent : VNode() {
- val root = GroupComponent().apply {
- pivotX = 0.0f
- pivotY = 0.0f
- invalidateListener = {
+internal class VectorComponent(val root: GroupComponent) : VNode() {
+
+ init {
+ root.invalidateListener = {
doInvalidate()
}
}
- var name: String
- get() = root.name
- set(value) {
- root.name = value
- }
+ var name: String = DefaultGroupName
private fun doInvalidate() {
isDirty = true
@@ -131,11 +128,18 @@ internal class VectorComponent : VNode() {
private var previousDrawSize = Unspecified
+ private var rootScaleX = 1f
+ private var rootScaleY = 1f
+
/**
* Cached lambda used to avoid allocating the lambda on each draw invocation
*/
private val drawVectorBlock: DrawScope.() -> Unit = {
- with(root) { draw() }
+ with(root) {
+ scale(rootScaleX, rootScaleY, pivot = Offset.Zero) {
+ draw()
+ }
+ }
}
fun DrawScope.draw(alpha: Float, colorFilter: ColorFilter?) {
@@ -155,8 +159,8 @@ internal class VectorComponent : VNode() {
} else {
null
}
- root.scaleX = size.width / viewportSize.width
- root.scaleY = size.height / viewportSize.height
+ rootScaleX = size.width / viewportSize.width
+ rootScaleY = size.height / viewportSize.height
cacheDrawScope.drawCachedImage(
targetImageConfig,
IntSize(ceil(size.width).toInt(), ceil(size.height).toInt()),
@@ -266,29 +270,23 @@ internal class PathComponent : VNode() {
var trimPathStart = DefaultTrimPathStart
set(value) {
- if (field != value) {
- field = value
- isTrimPathDirty = true
- invalidate()
- }
+ field = value
+ isTrimPathDirty = true
+ invalidate()
}
var trimPathEnd = DefaultTrimPathEnd
set(value) {
- if (field != value) {
- field = value
- isTrimPathDirty = true
- invalidate()
- }
+ field = value
+ isTrimPathDirty = true
+ invalidate()
}
var trimPathOffset = DefaultTrimPathOffset
set(value) {
- if (field != value) {
- field = value
- isTrimPathDirty = true
- invalidate()
- }
+ field = value
+ isTrimPathDirty = true
+ invalidate()
}
private var isPathDirty = true
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
index 5fd7095b894..0198dd7dad1 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
@@ -19,8 +19,6 @@ package androidx.compose.ui.graphics.vector
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ComposableOpenTarget
import androidx.compose.runtime.Composition
-import androidx.compose.runtime.CompositionContext
-import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -35,9 +33,11 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmapConfig
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.scale
+import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.internal.JvmDefaultWithCompatibility
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
@@ -127,26 +127,35 @@ fun rememberVectorPainter(
content: @Composable @VectorComposable (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 defaultSize = density.obtainSizePx(defaultWidth, defaultHeight)
+ val viewport = obtainViewportSize(defaultSize, viewportWidth, viewportHeight)
val intrinsicColorFilter = remember(tintColor, tintBlendMode) {
- if (tintColor != Color.Unspecified) {
- ColorFilter.tint(tintColor, tintBlendMode)
- } else {
- null
- }
+ createColorFilter(tintColor, tintBlendMode)
}
-
return remember { VectorPainter() }.apply {
- // These assignments are thread safe as parameters are backed by a mutableState object
- size = Size(widthPx, heightPx)
- this.autoMirror = autoMirror
- this.intrinsicColorFilter = intrinsicColorFilter
- RenderVector(name, vpWidth, vpHeight, content)
+ configureVectorPainter(
+ defaultSize = defaultSize,
+ viewportSize = viewport,
+ name = name,
+ intrinsicColorFilter = intrinsicColorFilter,
+ autoMirror = autoMirror
+ )
+ val compositionContext = rememberCompositionContext()
+ this.composition = remember(viewportWidth, viewportHeight, content) {
+ val curComp = this.composition
+ val next = if (curComp == null || curComp.isDisposed) {
+ Composition(
+ VectorApplier(this.vector.root),
+ compositionContext
+ )
+ } else {
+ curComp
+ }
+ next.setContent {
+ content(viewport.width, viewport.height)
+ }
+ next
+ }
}
}
@@ -157,25 +166,25 @@ fun rememberVectorPainter(
* @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,
- autoMirror = image.autoMirror,
- content = { _, _ -> RenderVectorGroup(group = image.root) }
- )
+fun rememberVectorPainter(image: ImageVector): VectorPainter {
+ val density = LocalDensity.current
+ return remember(image.genId, density) {
+ createVectorPainterFromImageVector(
+ density,
+ image,
+ GroupComponent().apply {
+ createGroupComponent(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() {
+class VectorPainter internal constructor(root: GroupComponent = GroupComponent()) : Painter() {
internal var size by mutableStateOf(Size.Zero)
@@ -190,7 +199,19 @@ class VectorPainter internal constructor() : Painter() {
vector.intrinsicColorFilter = value
}
- private val vector = VectorComponent().apply {
+ internal var viewportSize: Size
+ get() = vector.viewportSize
+ set(value) {
+ vector.viewportSize = value
+ }
+
+ internal var name: String
+ get() = vector.name
+ set(value) {
+ vector.name = value
+ }
+
+ internal val vector = VectorComponent(root).apply {
invalidateCallback = {
if (drawCount == invalidateCount) {
invalidateCount++
@@ -201,54 +222,11 @@ class VectorPainter internal constructor() : Painter() {
internal val bitmapConfig: ImageBitmapConfig
get() = vector.cacheBitmapConfig
- 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.viewportSize.width, vector.viewportSize.height)
- }
- return next
- }
+ internal var composition: Composition? = null
// TODO replace with mutableStateOf(Unit, neverEqualPolicy()) after b/291647821 is addressed
private var invalidateCount by mutableIntStateOf(0)
- @Composable
- internal fun RenderVector(
- name: String,
- viewportWidth: Float,
- viewportHeight: Float,
- content: @Composable (viewportWidth: Float, viewportHeight: Float) -> Unit
- ) {
- vector.apply {
- this.name = name
- this.viewportSize = Size(viewportWidth, viewportHeight)
- }
- val composition = composeVector(
- rememberCompositionContext(),
- content
- )
-
- DisposableEffect(composition) {
- onDispose {
- composition.dispose()
- }
- }
- }
-
private var currentAlpha: Float = 1.0f
private var currentColorFilter: ColorFilter? = null
@@ -324,6 +302,117 @@ interface VectorConfig {
}
}
+private fun Density.obtainSizePx(defaultWidth: Dp, defaultHeight: Dp) =
+ Size(defaultWidth.toPx(), defaultHeight.toPx())
+
+/**
+ * Helper method to calculate the viewport size. If the viewport width/height are not specified
+ * this falls back on the default size provided
+ */
+private fun obtainViewportSize(
+ defaultSize: Size,
+ viewportWidth: Float,
+ viewportHeight: Float
+) = Size(
+ if (viewportWidth.isNaN()) defaultSize.width else viewportWidth,
+ if (viewportHeight.isNaN()) defaultSize.height else viewportHeight
+ )
+
+/**
+ * Helper method to conditionally create a ColorFilter to tint contents if [tintColor] is
+ * specified, that is [Color.isSpecified] returns true
+ */
+private fun createColorFilter(tintColor: Color, tintBlendMode: BlendMode): ColorFilter? =
+ if (tintColor.isSpecified) {
+ ColorFilter.tint(tintColor, tintBlendMode)
+ } else {
+ null
+ }
+
+/**
+ * Helper method to configure the properties of a VectorPainter that maybe re-used
+ */
+internal fun VectorPainter.configureVectorPainter(
+ defaultSize: Size,
+ viewportSize: Size,
+ name: String = RootGroupName,
+ intrinsicColorFilter: ColorFilter?,
+ autoMirror: Boolean = false,
+): VectorPainter = apply {
+ this.size = defaultSize
+ this.autoMirror = autoMirror
+ this.intrinsicColorFilter = intrinsicColorFilter
+ this.viewportSize = viewportSize
+ this.name = name
+ }
+
+/**
+ * Helper method to create a VectorPainter instance from an ImageVector
+ */
+internal fun createVectorPainterFromImageVector(
+ density: Density,
+ imageVector: ImageVector,
+ root: GroupComponent
+): VectorPainter {
+ val defaultSize = density.obtainSizePx(imageVector.defaultWidth, imageVector.defaultHeight)
+ val viewport = obtainViewportSize(
+ defaultSize,
+ imageVector.viewportWidth,
+ imageVector.viewportHeight
+ )
+ return VectorPainter(root).configureVectorPainter(
+ defaultSize = defaultSize,
+ viewportSize = viewport,
+ name = imageVector.name,
+ intrinsicColorFilter = createColorFilter(imageVector.tintColor, imageVector.tintBlendMode),
+ autoMirror = imageVector.autoMirror
+ )
+}
+
+/**
+ * statically create a a GroupComponent from the VectorGroup representation provided from
+ * an [ImageVector] instance
+ */
+internal fun GroupComponent.createGroupComponent(currentGroup: VectorGroup): GroupComponent {
+ for (index in 0 until currentGroup.size) {
+ val vectorNode = currentGroup[index]
+ if (vectorNode is VectorPath) {
+ val pathComponent = PathComponent().apply {
+ pathData = vectorNode.pathData
+ pathFillType = vectorNode.pathFillType
+ name = vectorNode.name
+ fill = vectorNode.fill
+ fillAlpha = vectorNode.fillAlpha
+ stroke = vectorNode.stroke
+ strokeAlpha = vectorNode.strokeAlpha
+ strokeLineWidth = vectorNode.strokeLineWidth
+ strokeLineCap = vectorNode.strokeLineCap
+ strokeLineJoin = vectorNode.strokeLineJoin
+ strokeLineMiter = vectorNode.strokeLineMiter
+ trimPathStart = vectorNode.trimPathStart
+ trimPathEnd = vectorNode.trimPathEnd
+ trimPathOffset = vectorNode.trimPathOffset
+ }
+ insertAt(index, pathComponent)
+ } else if (vectorNode is VectorGroup) {
+ val groupComponent = GroupComponent().apply {
+ name = vectorNode.name
+ rotation = vectorNode.rotation
+ scaleX = vectorNode.scaleX
+ scaleY = vectorNode.scaleY
+ translationX = vectorNode.translationX
+ translationY = vectorNode.translationY
+ pivotX = vectorNode.pivotX
+ pivotY = vectorNode.pivotY
+ clipPathData = vectorNode.clipPathData
+ createGroupComponent(vectorNode)
+ }
+ insertAt(index, groupComponent)
+ }
+ }
+ return this
+}
+
/**
* Recursively creates the vector graphic composition by traversing the tree structure.
*