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/2839/merge
Oleksandr Karpovich
1 month 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 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.demo.shared.UseResources |
||||
import org.jetbrains.compose.resources.preloadFont |
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class) |
||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalResourceApi::class, InternalComposeUiApi::class) |
||||
fun main() { |
||||
configureWebResources { |
||||
// Not necessary - It's the same as the default. We add it here just to present this feature. |
||||
resourcePathMapping { path -> "./$path" } |
||||
} |
||||
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() |
||||
|
||||
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