Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[resources] Support SVG drawables for non android platforms #4605

Merged
merged 2 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ android.useAndroidX=true

#Versions
kotlin.version=1.9.23
compose.version=1.6.10-dev1566
compose.version=1.6.10-dev1575
agp.version=8.1.2

#Compose
Expand Down
13 changes: 12 additions & 1 deletion components/resources/demo/shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,32 @@ kotlin {
}
}

applyDefaultHierarchyTemplate()
sourceSets {
all {
languageSettings {
optIn("org.jetbrains.compose.resources.ExperimentalResourceApi")
}
}
val desktopMain by getting
val wasmJsMain by getting

commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.material3)
implementation(project(":resources:library"))
}
val desktopMain by getting
desktopMain.dependencies {
implementation(compose.desktop.common)
}

val nonAndroidMain by creating {
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
dependsOn(commonMain.get())
wasmJsMain.dependsOn(this)
desktopMain.dependsOn(this)
nativeMain.get().dependsOn(this)
jsMain.get().dependsOn(this)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.jetbrains.compose.resources.demo.shared

import androidx.compose.runtime.Composable

@Composable
actual fun SvgShowcase() {
//Android platform doesn't support SVG resources
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import components.resources.demo.shared.generated.resources.Res
import components.resources.demo.shared.generated.resources.*
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.imageResource
import org.jetbrains.compose.resources.vectorResource
import org.jetbrains.compose.resources.painterResource
Expand All @@ -22,6 +23,7 @@ fun ImagesRes(contentPadding: PaddingValues) {
Column(
modifier = Modifier.padding(contentPadding).verticalScroll(rememberScrollState()),
) {
SvgShowcase()
OutlinedCard(modifier = Modifier.padding(8.dp)) {
Column(
modifier = Modifier.padding(16.dp).fillMaxWidth().fillMaxWidth(),
Expand Down Expand Up @@ -175,4 +177,7 @@ fun ImagesRes(contentPadding: PaddingValues) {
}
}
}
}
}

@Composable
expect fun SvgShowcase()
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.jetbrains.compose.resources.demo.shared

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import components.resources.demo.shared.generated.resources.Res
import components.resources.demo.shared.generated.resources.sailing
import org.jetbrains.compose.resources.painterResource

@Composable
actual fun SvgShowcase() {
OutlinedCard(modifier = Modifier.padding(8.dp)) {
Column(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
modifier = Modifier.size(100.dp),
painter = painterResource(Res.drawable.sailing),
contentDescription = null
)
Text(
"""
Image(
painter = painterResource(Res.drawable.sailing)
)
""".trimIndent()
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ package org.jetbrains.compose.resources
import android.graphics.BitmapFactory
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.Density

internal actual fun ByteArray.toImageBitmap(): ImageBitmap =
BitmapFactory.decodeByteArray(this, 0, size).asImageBitmap()
BitmapFactory.decodeByteArray(this, 0, size).asImageBitmap()

internal actual class SvgElement

internal actual fun ByteArray.toSvgElement(): SvgElement {
error("Android platform doesn't support SVG format.")
}

internal actual fun SvgElement.toSvgPainter(density: Density): Painter {
error("Android platform doesn't support SVG format.")
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
Expand Down Expand Up @@ -49,9 +50,10 @@ fun DrawableResource(path: String): DrawableResource = DrawableResource(
fun painterResource(resource: DrawableResource): Painter {
val environment = LocalComposeEnvironment.current.rememberEnvironment()
val filePath = remember(resource, environment) { resource.getResourceItemByEnvironment(environment).path }
val isXml = filePath.endsWith(".xml", true)
if (isXml) {
if (filePath.endsWith(".xml", true)) {
return rememberVectorPainter(vectorResource(resource))
} else if (filePath.endsWith(".svg", true)) {
return svgPainter(resource)
} else {
return BitmapPainter(imageResource(resource))
}
Expand All @@ -69,7 +71,7 @@ private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) }
@Composable
fun imageResource(resource: DrawableResource): ImageBitmap {
val resourceReader = LocalResourceReader.current
val imageBitmap by rememberResourceState(resource, { emptyImageBitmap }) { env ->
val imageBitmap by rememberResourceState(resource, resourceReader, { emptyImageBitmap }) { env ->
val path = resource.getResourceItemByEnvironment(env).path
val cached = loadImage(path, resourceReader) {
ImageCache.Bitmap(it.toImageBitmap())
Expand All @@ -94,7 +96,7 @@ private val emptyImageVector: ImageVector by lazy {
fun vectorResource(resource: DrawableResource): ImageVector {
val resourceReader = LocalResourceReader.current
val density = LocalDensity.current
val imageVector by rememberResourceState(resource, { emptyImageVector }) { env ->
val imageVector by rememberResourceState(resource, resourceReader, density, { emptyImageVector }) { env ->
val path = resource.getResourceItemByEnvironment(env).path
val cached = loadImage(path, resourceReader) {
ImageCache.Vector(it.toXmlElement().toImageVector(density))
Expand All @@ -104,12 +106,34 @@ fun vectorResource(resource: DrawableResource): ImageVector {
return imageVector
}

internal expect class SvgElement
internal expect fun SvgElement.toSvgPainter(density: Density): Painter

private val emptySvgPainter: Painter by lazy { BitmapPainter(emptyImageBitmap) }

@OptIn(ExperimentalResourceApi::class)
@Composable
private fun svgPainter(resource: DrawableResource): Painter {
val resourceReader = LocalResourceReader.current
val density = LocalDensity.current
val svgPainter by rememberResourceState(resource, resourceReader, density, { emptySvgPainter }) { env ->
val path = resource.getResourceItemByEnvironment(env).path
val cached = loadImage(path, resourceReader) {
ImageCache.Svg(it.toSvgElement().toSvgPainter(density))
} as ImageCache.Svg
cached.painter
}
return svgPainter
}

internal expect fun ByteArray.toImageBitmap(): ImageBitmap
internal expect fun ByteArray.toXmlElement(): Element
internal expect fun ByteArray.toSvgElement(): SvgElement

private sealed interface ImageCache {
class Bitmap(val bitmap: ImageBitmap) : ImageCache
class Vector(val vector: ImageVector) : ImageCache
class Svg(val painter: Painter) : ImageCache
}

private val imageCacheMutex = Mutex()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.jetbrains.compose.resources

import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.ImageBitmapConfig
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toSize

/**
* Creates a drawing environment that directs its drawing commands to an [ImageBitmap]
* which can be drawn directly in another [DrawScope] instance. This is useful to cache
* complicated drawing commands across frames especially if the content has not changed.
* Additionally some drawing operations such as rendering paths are done purely in
* software so it is beneficial to cache the result and render the contents
* directly through a texture as done by [DrawScope.drawImage]
*/
internal class DrawCache {

@PublishedApi internal var mCachedImage: ImageBitmap? = null
private var cachedCanvas: Canvas? = null
private var scopeDensity: Density? = null
private var layoutDirection: LayoutDirection = LayoutDirection.Ltr
private var size: IntSize = IntSize.Zero
private var config: ImageBitmapConfig = ImageBitmapConfig.Argb8888

private val cacheScope = CanvasDrawScope()

/**
* Draw the contents of the lambda with receiver scope into an [ImageBitmap] with the provided
* size. If the same size is provided across calls, the same [ImageBitmap] instance is
* re-used and the contents are cleared out before drawing content in it again
*/
fun drawCachedImage(
config: ImageBitmapConfig,
size: IntSize,
density: Density,
layoutDirection: LayoutDirection,
block: DrawScope.() -> Unit
) {
this.scopeDensity = density
this.layoutDirection = layoutDirection
var targetImage = mCachedImage
var targetCanvas = cachedCanvas
if (targetImage == null ||
targetCanvas == null ||
size.width > targetImage.width ||
size.height > targetImage.height ||
this.config != config
) {
targetImage = ImageBitmap(size.width, size.height, config = config)
targetCanvas = Canvas(targetImage)

mCachedImage = targetImage
cachedCanvas = targetCanvas
this.config = config
}
this.size = size
cacheScope.draw(density, layoutDirection, targetCanvas, size.toSize()) {
clear()
block()
}
targetImage.prepareToDraw()
}

/**
* Draw the cached content into the provided [DrawScope] instance
*/
fun drawInto(
target: DrawScope,
alpha: Float = 1.0f,
colorFilter: ColorFilter? = null
) {
val targetImage = mCachedImage
check(targetImage != null) {
"drawCachedImage must be invoked first before attempting to draw the result " +
"into another destination"
}
target.drawImage(targetImage, srcSize = size, alpha = alpha, colorFilter = colorFilter)
}

/**
* Helper method to clear contents of the draw environment from the given bounds of the
* DrawScope
*/
private fun DrawScope.clear() {
drawRect(color = Color.Black, blendMode = BlendMode.Clear)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
package org.jetbrains.compose.resources

import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.unit.Density
import org.jetbrains.skia.Data
import org.jetbrains.skia.Image
import org.jetbrains.skia.svg.SVGDOM

internal actual fun ByteArray.toImageBitmap(): ImageBitmap =
Image.makeFromEncoded(this).toComposeImageBitmap()
Image.makeFromEncoded(this).toComposeImageBitmap()

internal actual class SvgElement(val svgdom: SVGDOM)

internal actual fun ByteArray.toSvgElement(): SvgElement =
SvgElement(SVGDOM(Data.makeFromBytes(this)))

internal actual fun SvgElement.toSvgPainter(density: Density): Painter =
SvgPainter(svgdom, density)
Loading