You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose中通过Uri读取本地文件加载字体的问题

问题描述

我知道怎么从资源里加载字体,但不想给应用打包太多字体,所以想通过Uri使用本地存储里的字体。拿到文件Uri后,尝试用它作为字体时出现加载错误,相关代码和报错如下:

获取Uri与文件的代码

// use the standard font if null
val currentFont = remember { mutableStateOf<String?>(null) }

val fontPath = remember { mutableStateOf<String?>(null) }
val fontFile = remember { mutableStateOf<File?>(null) }

val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
    if (uri != null) {
        fontPath.value = uri.path
        if (fontPath.value != null) {
            if (fontPath.value!!.contains("document/raw:")) {
                fontPath.value = fontPath.value!!.replace("/document/raw:","");
            }
            fontFile.value = File(fontPath.value)
            currentFont.value = "name_in_future"
        }
    }
}

//In UI 
...
RegularButton(title = "Button") { launcher.launch(arrayOf("*/*")) }
...

UI代码

CustomTextField(
                        value = framesText[page.value.toInt() - 1],
                        onValueChange = { framesText[page.value.toInt() - 1] = it },
                        modifier = Modifier
                            .weight(1f)
                            .fillMaxWidth()
                            .padding(horizontal = 5.dp)
                            .border(1.dp, Color.Black),
                        fontSize = project.value.fontSize.sp,
                        // WORK_HERE ->
                        font = if (fontFile.value != null) Font(fontFile.value!!) else Font(R.font.jet_brains_mono),
                        contentAlignment = Alignment.TopStart
                    )

自定义TextField代码

@Composable
fun CustomTextField(
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier,
   fontSize: TextUnit = 20.sp,
   font: Font = Font(R.font.jet_brains_mono),
   contentAlignment: Alignment = Alignment.CenterStart,
   keyboardActions: KeyboardActions = KeyboardActions {},
   keyboardOptions: KeyboardOptions = KeyboardOptions.Default
) {

   BasicTextField(
       value = value,
       onValueChange = { onValueChange(it) },
       modifier = modifier,
       textStyle = TextStyle(
           fontSize = fontSize,
           lineHeight = fontSize,
           color = MaterialTheme.colorScheme.primary,
           fontFamily = FontFamily(font)
       ),
       cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
       keyboardActions = keyboardActions,
       keyboardOptions = keyboardOptions
   ) { innerTextField ->
       Box(modifier = Modifier
           .background(
               color = MaterialTheme.colorScheme.onPrimary,
               shape = RoundedCornerShape(4.dp)
           )
           .padding(4.dp)
           .fillMaxHeight(),
           contentAlignment = contentAlignment,
           content = { innerTextField() }
       )
   }
}

报错信息

Error mapping font file /storage/emulated/0/Download/RobotoMono-VariableFont_wght.ttf
FATAL EXCEPTION: main
java.lang.IllegalStateException: Could not load font

//And the same with "/document/raw:":
Error mapping font file /document/raw:/storage/emulated/0/Download/RobotoMono-VariableFont_wght.ttf
解决方案

核心问题是ActivityResultContracts.OpenDocument()返回的Uri是内容Uri,不是本地文件路径,直接转File会导致权限失效或路径无效。正确做法是通过ContentResolver获取文件输入流,再加载字体。

步骤1:修改Uri处理逻辑

保留原始Uri,通过输入流加载字体,同时申请永久访问权限避免重启后失效:

val context = LocalContext.current
val selectedFontUri = remember { mutableStateOf<Uri?>(null) }
val customFontFamily = remember { mutableStateOf<FontFamily?>(null) }

val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
    if (uri != null) {
        selectedFontUri.value = uri
        // 申请永久访问权限
        context.contentResolver.takePersistableUriPermission(
            uri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION
        )
        // 协程中执行IO操作加载字体
        lifecycleScope.launch {
            runCatching {
                context.contentResolver.openInputStream(uri)?.use { inputStream ->
                    val font = Font(inputStream)
                    customFontFamily.value = FontFamily(font)
                }
            }.onFailure {
                Log.e("FontLoad", "加载字体失败", it)
            }
        }
    }
}

步骤2:更新UI调用代码

直接使用加载好的FontFamily替代原有的Font参数:

CustomTextField(
    value = framesText[page.value.toInt() - 1],
    onValueChange = { framesText[page.value.toInt() - 1] = it },
    modifier = Modifier
        .weight(1f)
        .fillMaxWidth()
        .padding(horizontal = 5.dp)
        .border(1.dp, Color.Black),
    fontSize = project.value.fontSize.sp,
    fontFamily = customFontFamily.value ?: FontFamily(Font(R.font.jet_brains_mono)),
    contentAlignment = Alignment.TopStart
)

步骤3:调整CustomTextField参数

将font参数替换为fontFamily,更贴合实际使用场景:

@Composable
fun CustomTextField(
    value: String,
    onValueChange: (String) -> Unit,
    modifier: Modifier = Modifier,
    fontSize: TextUnit = 20.sp,
    fontFamily: FontFamily = FontFamily(Font(R.font.jet_brains_mono)),
    contentAlignment: Alignment = Alignment.CenterStart,
    keyboardActions: KeyboardActions = KeyboardActions {},
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default
) {

    BasicTextField(
        value = value,
        onValueChange = { onValueChange(it) },
        modifier = modifier,
        textStyle = TextStyle(
            fontSize = fontSize,
            lineHeight = fontSize,
            color = MaterialTheme.colorScheme.primary,
            fontFamily = fontFamily
        ),
        cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
        keyboardActions = keyboardActions,
        keyboardOptions = keyboardOptions
    ) { innerTextField ->
        Box(modifier = Modifier
            .background(
                color = MaterialTheme.colorScheme.onPrimary,
                shape = RoundedCornerShape(4.dp)
            )
            .padding(4.dp)
            .fillMaxHeight(),
            contentAlignment = contentAlignment,
            content = { innerTextField() }
        )
    }
}

关键说明

  • OpenDocument返回的Uri属于系统内容提供器,必须通过ContentResolver.openInputStream()访问,不能直接用File类读取。
  • takePersistableUriPermission能让应用在重启后仍保留文件访问权限,否则权限会随进程结束失效。
  • 字体加载属于IO操作,必须放在协程中执行,避免阻塞主线程引发ANR。

内容的提问来源于stack exchange,提问作者CosmicTea

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 06:46:01