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

Android Jetpack Compose中WebView拍照选图后无响应问题求助

问题排查与修复方案

核心问题分析

  1. onShowFileChooser 提前终止流程:你在方法开头调用了filePathCallback?.onReceiveValue(null),这会直接告诉WebView"用户取消了选择",后续再调用onReceiveValue已无效。
  2. 状态丢失问题:
    • takePictureUri和takePictureFile是Composable局部变量,Compose重组时会被重置,导致拍照成功后无法正确传递Uri。
    • grantState仅在首次组合时计算,权限申请成功后不会自动更新,导致后续点击拍照按钮仍会触发权限申请。
  3. fileChooserCallback 未持久化:未用rememberSaveable保存,配置变化(如旋转屏幕)会丢失回调引用。

修复后的代码

1. WebView 相关代码修复

// 用rememberSaveable持久化回调,防止配置变化丢失
var fileChooserCallback by rememberSaveable {
    mutableStateOf<ValueCallback<Array<Uri>>?>(null)
}
var selectImage by remember {
    mutableStateOf(false)
}

...

override fun onShowFileChooser(
    webView: WebView?,
    filePathCallback: ValueCallback<Array<Uri>>?,
    fileChooserParams: FileChooserParams?
): Boolean {
    // 移除提前调用onReceiveValue(null)的代码
    filePathCallback?.let { callback ->
        this.fileChooserCallback = callback
        selectImage = true
    }
    return true
}

...

if (selectImage) {
    ImageSelectionBottomSheet { uri ->
        fileChooserCallback?.onReceiveValue(uri?.let { arrayOf(it) } ?: emptyArray())
        // 无论选择成功还是取消,都重置状态
        selectImage = false
        // 清空回调,避免重复调用
        fileChooserCallback = null
    }
}

2. 图片选择底部弹窗修复

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ImageSelectionBottomSheet(
    onDismiss: (Uri?) -> Unit
) {
    val permissions = arrayOf(Manifest.permission.CAMERA)
    val context = LocalContext.current
    
    // 用remember保存拍照相关的文件和Uri,避免重组丢失
    var takePictureFile by remember { mutableStateOf<File?>(null) }
    var takePictureUri by remember { mutableStateOf<Uri?>(null) }

    // 实时检查权限状态
    val grantState by remember {
        mutableStateOf(permissions.all {
            ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        })
    }

    // 权限申请后更新状态
    val permissionLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestMultiplePermissions(),
        onResult = { results ->
            val allGranted = results.values.all { it }
            if (allGranted) {
                takePictureFile = createImageCaptureFile(context)
                takePictureUri = FileProvider.getUriForFile(
                    context,
                    "${context.packageName}.fileprovider",
                    takePictureFile!!
                )
                cameraLauncher.launch(takePictureUri)
            } else {
                // 权限拒绝通知回调
                onDismiss.invoke(null)
            }
        }
    )

    val singlePhotoPickerLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.PickVisualMedia(),
        onResult = { uri ->
            Timber.tag("ok").d("ImageSelectionBottomSheet: %s", uri)
            onDismiss.invoke(uri)
        }
    )

    val cameraLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.TakePicture()
    ) { success ->
        Timber.tag("ok").d("ImageSelectionBottomSheet: %s $success", takePictureUri)
        if (success) {
            onDismiss.invoke(takePictureUri)
        } else {
            // 拍照失败也通知回调
            onDismiss.invoke(null)
        }
        // 重置拍照状态
        takePictureFile = null
        takePictureUri = null
    }

    ModalBottomSheet(
        content = {
            Column(modifier = Modifier.padding(16.dp)) {
                ActionButton(
                    iconDrawableRes = com.liquidity.core.ui.R.drawable.ic_camera,
                    text = stringResource(id = R.string.open_gallery),
                    onClick = {
                        singlePhotoPickerLauncher.launch(
                            PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
                        )
                    },
                )
                Spacer(modifier = Modifier.height(8.dp))
                ActionButton(
                    iconDrawableRes = com.liquidity.core.ui.R.drawable.ic_camera,
                    text = stringResource(id = R.string.take_a_new_photo),
                    onClick = {
                        val currentGrantState = permissions.all {
                            ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
                        }
                        if (currentGrantState) {
                            takePictureFile = createImageCaptureFile(context)
                            takePictureUri = FileProvider.getUriForFile(
                                context,
                                "${context.packageName}.fileprovider",
                                takePictureFile!!
                            )
                            cameraLauncher.launch(takePictureUri)
                        } else {
                            permissionLauncher.launch(permissions)
                        }
                    },
                )
                Spacer(modifier = Modifier.height(30.dp))
            }
        },
        shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
        onDismissRequest = {
            onDismiss.invoke(null)
        }
    )
}

private fun createImageCaptureFile(context: Context): File {
    val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
    val fileName = "JPEG_${timeStamp}.jpg"
    val cacheDir = context.cacheDir
    return File(cacheDir, fileName)
}

关键修复点说明

  1. 移除提前终止代码:删除onShowFileChooser中的filePathCallback?.onReceiveValue(null),确保WebView等待选择结果。
  2. 持久化状态:
    • 用rememberSaveable保存fileChooserCallback,避免配置变化丢失。
    • 用remember保存takePictureFile和takePictureUri,防止Compose重组时重置。
  3. 实时检查权限:点击拍照按钮时实时检查权限状态,避免依赖静态的grantState。
  4. 完善回调逻辑:无论选择成功、失败还是取消,都调用onDismiss并重置状态,避免内存泄漏和重复调用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 02:45:26