diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-01-09 00:06:47 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-01-09 00:06:47 +0000 |
commit | 5119b8203f8556d77f7cf7b90e1681d59102a8a1 (patch) | |
tree | 80ae0b7358e1b5287d64cb3011086801ca5cb617 | |
parent | 6dc685be02316455881d22b69d0bb8adbe768c4f (diff) | |
parent | 1794a549616ac082f7aad22e9fa5f3048a3a58ae (diff) | |
download | support-5119b8203f8556d77f7cf7b90e1681d59102a8a1.tar.gz |
Merge cherrypicks of ['android-review.googlesource.com/2865106'] into androidx-compose-release.
Change-Id: Id51533bd8f40a7b4d82feeefc9a43a4e69fe87bf
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. * |