Browse Source
## Release Notes ### Features - Resources - Add new API to preload and cache font and image resources on web targets: `preloadFont`, `preloadImageBitmap`, `preloadImageVector` ____ Add a new experimental web-specific API to preload fonts and images: ```kotlin @Composable fun preloadFont( resource: FontResource, weight: FontWeight = FontWeight.Normal, style: FontStyle = FontStyle.Normal ): State<Font?> @Composable fun preloadImageBitmap( resource: DrawableResource, ): State<ImageBitmap?> @Composable fun preloadImageVector( resource: DrawableResource, ): State<ImageVector?> ``` Using this methods in advance, it's possible to avoid FOUT (flash of unstyled text), or flickering of images/icons. Usage example: ```kotlin val font1 by preloadFont(Res.font.Workbench_Regular) val font2 by preloadFont(Res.font.font_awesome, FontWeight.Normal, FontStyle.Normal) UseResources() // Main App that uses the above fonts if (font1 != null && font2 != null) { println("Fonts are ready") } else { Box(modifier = Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.8f)).clickable { }) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } println("Fonts are not ready yet") } ```pull/2336/merge
Oleksandr Karpovich
2 weeks ago
committed by
GitHub
11 changed files with 319 additions and 7 deletions
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
Binary file not shown.
@ -1,15 +1,57 @@ |
|||||||
import androidx.compose.ui.ExperimentalComposeUiApi |
import androidx.compose.foundation.background |
||||||
|
import androidx.compose.foundation.clickable |
||||||
|
import androidx.compose.foundation.layout.Box |
||||||
|
import androidx.compose.foundation.layout.fillMaxSize |
||||||
|
import androidx.compose.material3.CircularProgressIndicator |
||||||
|
import androidx.compose.runtime.* |
||||||
|
import androidx.compose.ui.* |
||||||
|
import androidx.compose.ui.graphics.Color |
||||||
|
import androidx.compose.ui.platform.LocalFontFamilyResolver |
||||||
|
import androidx.compose.ui.text.font.FontFamily |
||||||
|
import androidx.compose.ui.text.font.FontStyle |
||||||
|
import androidx.compose.ui.text.font.FontWeight |
||||||
import androidx.compose.ui.window.CanvasBasedWindow |
import androidx.compose.ui.window.CanvasBasedWindow |
||||||
|
import components.resources.demo.shared.generated.resources.* |
||||||
|
import components.resources.demo.shared.generated.resources.NotoColorEmoji |
||||||
|
import components.resources.demo.shared.generated.resources.Res |
||||||
|
import components.resources.demo.shared.generated.resources.Workbench_Regular |
||||||
|
import components.resources.demo.shared.generated.resources.font_awesome |
||||||
|
import org.jetbrains.compose.resources.ExperimentalResourceApi |
||||||
import org.jetbrains.compose.resources.configureWebResources |
import org.jetbrains.compose.resources.configureWebResources |
||||||
import org.jetbrains.compose.resources.demo.shared.UseResources |
import org.jetbrains.compose.resources.demo.shared.UseResources |
||||||
|
import org.jetbrains.compose.resources.preloadFont |
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class) |
@OptIn(ExperimentalComposeUiApi::class, ExperimentalResourceApi::class, InternalComposeUiApi::class) |
||||||
fun main() { |
fun main() { |
||||||
configureWebResources { |
configureWebResources { |
||||||
// Not necessary - It's the same as the default. We add it here just to present this feature. |
// Not necessary - It's the same as the default. We add it here just to present this feature. |
||||||
resourcePathMapping { path -> "./$path" } |
resourcePathMapping { path -> "./$path" } |
||||||
} |
} |
||||||
CanvasBasedWindow("Resources demo + K/Wasm") { |
CanvasBasedWindow("Resources demo + K/Wasm") { |
||||||
|
val font1 by preloadFont(Res.font.Workbench_Regular) |
||||||
|
val font2 by preloadFont(Res.font.font_awesome, FontWeight.Normal, FontStyle.Normal) |
||||||
|
val emojiFont = preloadFont(Res.font.NotoColorEmoji).value |
||||||
|
var fontsFallbackInitialiazed by remember { mutableStateOf(false) } |
||||||
|
|
||||||
UseResources() |
UseResources() |
||||||
|
|
||||||
|
if (font1 != null && font2 != null && emojiFont != null && fontsFallbackInitialiazed) { |
||||||
|
println("Fonts are ready") |
||||||
|
} else { |
||||||
|
Box(modifier = Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.8f)).clickable { }) { |
||||||
|
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) |
||||||
|
} |
||||||
|
println("Fonts are not ready yet") |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
val fontFamilyResolver = LocalFontFamilyResolver.current |
||||||
|
LaunchedEffect(fontFamilyResolver, emojiFont) { |
||||||
|
if (emojiFont != null) { |
||||||
|
// we have an emoji on Strings tab |
||||||
|
fontFamilyResolver.preload(FontFamily(listOf(emojiFont))) |
||||||
|
fontsFallbackInitialiazed = true |
||||||
|
} |
||||||
|
} |
||||||
} |
} |
||||||
} |
} |
@ -0,0 +1,76 @@ |
|||||||
|
package org.jetbrains.compose.resources |
||||||
|
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider |
||||||
|
import androidx.compose.runtime.getValue |
||||||
|
import androidx.compose.runtime.mutableStateOf |
||||||
|
import androidx.compose.runtime.setValue |
||||||
|
import androidx.compose.ui.test.ExperimentalTestApi |
||||||
|
import androidx.compose.ui.test.runComposeUiTest |
||||||
|
import androidx.compose.ui.text.font.Font |
||||||
|
import kotlinx.coroutines.CancellableContinuation |
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine |
||||||
|
import kotlin.io.encoding.Base64 |
||||||
|
import kotlin.io.encoding.ExperimentalEncodingApi |
||||||
|
import kotlin.test.Test |
||||||
|
import kotlin.test.assertEquals |
||||||
|
import kotlin.test.assertNotEquals |
||||||
|
|
||||||
|
@OptIn(ExperimentalTestApi::class, ExperimentalEncodingApi::class) |
||||||
|
class TestResourcePreloading { |
||||||
|
|
||||||
|
@Test |
||||||
|
fun testPreloadFont() = runComposeUiTest { |
||||||
|
var loadContinuation: CancellableContinuation<ByteArray>? = null |
||||||
|
|
||||||
|
val resLoader = object : ResourceReader { |
||||||
|
override suspend fun read(path: String): ByteArray { |
||||||
|
return suspendCancellableCoroutine { |
||||||
|
loadContinuation = it |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray { |
||||||
|
TODO("Not yet implemented") |
||||||
|
} |
||||||
|
|
||||||
|
override fun getUri(path: String): String { |
||||||
|
TODO("Not yet implemented") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var font: Font? = null |
||||||
|
var font2: Font? = null |
||||||
|
var condition by mutableStateOf(false) |
||||||
|
|
||||||
|
|
||||||
|
setContent { |
||||||
|
CompositionLocalProvider( |
||||||
|
LocalComposeEnvironment provides TestComposeEnvironment, |
||||||
|
LocalResourceReader provides resLoader |
||||||
|
) { |
||||||
|
font = preloadFont(TestFontResource("sometestfont")).value |
||||||
|
|
||||||
|
if (condition) { |
||||||
|
font2 = Font(TestFontResource("sometestfont")) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
waitForIdle() |
||||||
|
assertEquals(null, font) |
||||||
|
assertEquals(null, font2) |
||||||
|
|
||||||
|
assertNotEquals(null, loadContinuation) |
||||||
|
loadContinuation!!.resumeWith(Result.success(ByteArray(0))) |
||||||
|
loadContinuation = null |
||||||
|
|
||||||
|
waitForIdle() |
||||||
|
assertNotEquals(null, font) |
||||||
|
assertEquals(null, font2) // condition was false for now, so font2 should be not initialized |
||||||
|
|
||||||
|
condition = true |
||||||
|
waitForIdle() |
||||||
|
assertNotEquals(null, font) |
||||||
|
assertEquals(font, font2, "font2 is expected to be loaded from cache") |
||||||
|
assertEquals(null, loadContinuation, "expected no more ResourceReader usages") |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue