From 614fe78d7187ab4a7bc1fca433c30bec063319b0 Mon Sep 17 00:00:00 2001 From: Nikita Lipsky Date: Thu, 16 Feb 2023 19:38:19 +0200 Subject: [PATCH] Add painterResource utility method to resource library. (#2753) + respect density in remembering state for ImageVector --- .../resources/demo/shared/UseResources.kt | 8 ++ .../src/commonMain/resources/dir/vector.xml | 15 +++ components/resources/library/build.gradle.kts | 7 ++ .../compose/resources/Resource.commonbutjs.kt | 14 +++ .../resources/ComposeResource.common.kt | 94 +++++++++++++++---- .../compose/resources/Resource.js.kt | 7 +- 6 files changed, 128 insertions(+), 17 deletions(-) create mode 100644 components/resources/demo/shared/src/commonMain/resources/dir/vector.xml create mode 100644 components/resources/library/src/commonButJSMain/kotlin/org/jetbrains/compose/resources/Resource.commonbutjs.kt diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt index bc5f514132..b1fa10c534 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt @@ -5,7 +5,9 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.* @OptIn(ExperimentalResourceApi::class) @@ -23,6 +25,12 @@ internal fun UseResources() { ) Icon( imageVector = resource("vector.xml").rememberImageVector(LocalDensity.current).orEmpty(), + modifier = Modifier.size(150.dp), + contentDescription = null + ) + Icon( + painter = painterResource("dir/vector.xml"), + modifier = Modifier.size(150.dp), contentDescription = null ) } diff --git a/components/resources/demo/shared/src/commonMain/resources/dir/vector.xml b/components/resources/demo/shared/src/commonMain/resources/dir/vector.xml new file mode 100644 index 0000000000..abd196bb92 --- /dev/null +++ b/components/resources/demo/shared/src/commonMain/resources/dir/vector.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/components/resources/library/build.gradle.kts b/components/resources/library/build.gradle.kts index bd26802d18..7387edf05b 100644 --- a/components/resources/library/build.gradle.kts +++ b/components/resources/library/build.gradle.kts @@ -35,6 +35,9 @@ kotlin { implementation(kotlin("test")) } } + val commonButJSMain by creating { + dependsOn(commonMain) + } val skikoMain by creating { dependsOn(commonMain) } @@ -44,6 +47,7 @@ kotlin { val desktopMain by getting { dependsOn(skikoMain) dependsOn(jvmAndAndroidMain) + dependsOn(commonButJSMain) } val desktopTest by getting { dependencies { @@ -54,6 +58,7 @@ kotlin { } val androidMain by getting { dependsOn(jvmAndAndroidMain) + dependsOn(commonButJSMain) } val androidTest by getting { dependencies { @@ -62,6 +67,7 @@ kotlin { } val iosMain by getting { dependsOn(skikoMain) + dependsOn(commonButJSMain) } val iosTest by getting val iosSimulatorArm64Main by getting @@ -73,6 +79,7 @@ kotlin { } val macosMain by creating { dependsOn(skikoMain) + dependsOn(commonButJSMain) } val macosX64Main by getting { dependsOn(macosMain) diff --git a/components/resources/library/src/commonButJSMain/kotlin/org/jetbrains/compose/resources/Resource.commonbutjs.kt b/components/resources/library/src/commonButJSMain/kotlin/org/jetbrains/compose/resources/Resource.commonbutjs.kt new file mode 100644 index 0000000000..ce0cba09cd --- /dev/null +++ b/components/resources/library/src/commonButJSMain/kotlin/org/jetbrains/compose/resources/Resource.commonbutjs.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.resources + +import kotlinx.coroutines.runBlocking + +internal actual fun isSyncResourceLoadingSupported() = true + +@OptIn(ExperimentalResourceApi::class) +internal actual fun Resource.readBytesSync(): ByteArray = runBlocking { readBytes() } + diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt index c09f0f2b6a..bb63b7c6c6 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt @@ -7,7 +7,11 @@ package org.jetbrains.compose.resources import androidx.compose.runtime.* import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +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 org.jetbrains.compose.resources.vector.xmldom.Element import org.jetbrains.compose.resources.vector.parseVectorRoot @@ -19,13 +23,16 @@ private val emptyImageVector: ImageVector by lazy { ImageVector.Builder(defaultWidth = 1.dp, defaultHeight = 1.dp, viewportWidth = 1f, viewportHeight = 1f).build() } -@OptIn(ExperimentalResourceApi::class) +/** + * Get and remember resource. While loading and if resource not exists result will be null. + */ +@ExperimentalResourceApi @Composable -private fun Resource.rememberLoadingResource(fromByteArrayConverter: ByteArray.()->T): LoadState { - val state: MutableState> = remember(this) { mutableStateOf(LoadState.Loading()) } +fun Resource.rememberImageBitmap(): LoadState { + val state: MutableState> = remember(this) { mutableStateOf(LoadState.Loading()) } LaunchedEffect(this) { state.value = try { - LoadState.Success(readBytes().fromByteArrayConverter()) + LoadState.Success(readBytes().toImageBitmap()) } catch (e: Exception) { LoadState.Error(e) } @@ -38,16 +45,17 @@ private fun Resource.rememberLoadingResource(fromByteArrayConverter: ByteArr */ @ExperimentalResourceApi @Composable -fun Resource.rememberImageBitmap(): LoadState = - rememberLoadingResource { toImageBitmap() } - -/** - * Get and remember resource. While loading and if resource not exists result will be null. - */ -@ExperimentalResourceApi -@Composable -fun Resource.rememberImageVector(density: Density): LoadState = - rememberLoadingResource { toImageVector(density) } +fun Resource.rememberImageVector(density: Density): LoadState { + val state: MutableState> = remember(this, density) { mutableStateOf(LoadState.Loading()) } + LaunchedEffect(this, density) { + state.value = try { + LoadState.Success(readBytes().toImageVector(density)) + } catch (e: Exception) { + LoadState.Error(e) + } + } + return state.value +} private fun LoadState.orEmpty(emptyValue: T): T = when (this) { is LoadState.Loading -> emptyValue @@ -56,17 +64,71 @@ private fun LoadState.orEmpty(emptyValue: T): T = when (this) { } /** - * return current ImageBitmap or return empty while loading + * Return current ImageBitmap or return empty while loading. */ @ExperimentalResourceApi fun LoadState.orEmpty(): ImageBitmap = orEmpty(emptyImageBitmap) /** - * return current ImageVector or return empty while loading + * Return current ImageVector or return empty while loading. */ @ExperimentalResourceApi fun LoadState.orEmpty(): ImageVector = orEmpty(emptyImageVector) + +@OptIn(ExperimentalResourceApi::class) +@Composable +private fun Resource.rememberImageBitmapSync(): ImageBitmap = remember(this) { + readBytesSync().toImageBitmap() +} + +@OptIn(ExperimentalResourceApi::class) +@Composable +private fun Resource.rememberImageVectorSync(density: Density): ImageVector = remember(this, density) { + readBytesSync().toImageVector(density) +} + + +@OptIn(ExperimentalResourceApi::class) +@Composable +private fun painterResource( + res: String, + rememberImageBitmap: @Composable Resource.() -> ImageBitmap, + rememberImageVector: @Composable Resource.(Density) -> ImageVector +): Painter = + if (res.endsWith(".xml")) { + rememberVectorPainter(resource(res).rememberImageVector(LocalDensity.current)) + } else { + BitmapPainter(resource(res).rememberImageBitmap()) + } + +/** + * Return a Painter from the given resource path. + * Can load either a BitmapPainter for rasterized images (.png, .jpg) or + * a VectorPainter for XML Vector Drawables (.xml). + * + * XML Vector Drawables have the same format as for Android + * (https://developer.android.com/reference/android/graphics/drawable/VectorDrawable) + * except that external references to Android resources are not supported. + * + * Note that XML Vector Drawables are not supported for Web and native MacOS targets currently. + * + */ +@ExperimentalResourceApi +@Composable +fun painterResource(res: String): Painter = + if (isSyncResourceLoadingSupported()) { + painterResource(res, {rememberImageBitmapSync()}, {density->rememberImageVectorSync(density)}) + } else { + painterResource(res, {rememberImageBitmap().orEmpty()}, {density->rememberImageVector(density).orEmpty()}) + } + + +internal expect fun isSyncResourceLoadingSupported(): Boolean + +@OptIn(ExperimentalResourceApi::class) +internal expect fun Resource.readBytesSync(): ByteArray + internal expect fun ByteArray.toImageBitmap(): ImageBitmap internal expect fun parseXML(byteArray: ByteArray): Element diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt index b05f8a49ad..b071ef8e04 100644 --- a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt @@ -46,4 +46,9 @@ internal actual class MissingResourceException actual constructor(path: String) internal actual fun parseXML(byteArray: ByteArray): Element { throw UnsupportedOperationException("XML Vector Drawables are not supported for Web target") -} \ No newline at end of file +} + +internal actual fun isSyncResourceLoadingSupported() = false + +@OptIn(ExperimentalResourceApi::class) +internal actual fun Resource.readBytesSync(): ByteArray = throw UnsupportedOperationException() \ No newline at end of file