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

Android Camera问题:首次拍照后后续拍摄图片无法更新显示

问题:Android Compose相机拍照后AsyncImage无法更新显示

首次拍照能正常显示,但后续拍摄的照片无法刷新,始终停留在第一张;若每次点击都重新生成URI,第二次拍照后图片会变为空白。

问题根源

  1. 原代码中val uri = context.createImageFile()在Composable首次初始化时仅执行一次,所有拍照操作复用同一个URI——新照片会覆盖旧文件,但AsyncImage因URI未变化(或系统缓存)不会触发重新加载。
  2. 若直接在点击事件中生成URI,会导致相机启动时的URI与回调中更新的imageUri不匹配,或者AsyncImage因缓存机制无法识别新文件。

修复后的完整代码

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    MyApp()
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MyApp() {
    val context = LocalContext.current
    var imageUri by remember {
        mutableStateOf<Uri>(Uri.EMPTY)
    }

    // 相机拍照回调,确认成功后触发状态更新
    val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { success ->
        if (success) {
            // 强制触发状态变化,让AsyncImage感知到更新
            imageUri = imageUri.copy()
        }
    }

    // 相机权限申请回调
    val cameraPermissionLauncher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->
        if (granted) {
            Toast.makeText(context, "权限已授予", Toast.LENGTH_SHORT).show()
            // 生成新URI并启动相机
            val newUri = context.createImageFile()
            imageUri = newUri
            cameraLauncher.launch(newUri)
        } else {
            Toast.makeText(context, "权限被拒绝", Toast.LENGTH_SHORT).show()
        }
    }

    Scaffold (
        topBar = {
            CenterAlignedTopAppBar(
                title = {
                    Text(
                        text = stringResource(R.string.title),
                        fontWeight = FontWeight.Bold,
                        fontSize = 30.sp
                    )
                },
                colors = TopAppBarDefaults.largeTopAppBarColors(
                    containerColor = colorResource(R.color.theme)
                )
            )
        }
    ) { innerPadding ->
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
        ) {
            // 显示拍摄的照片,用key强制URI变化时重组组件
            if (imageUri != Uri.EMPTY) {
                AsyncImage(
                    model = imageUri,
                    modifier = Modifier.fillMaxWidth(),
                    contentDescription = stringResource(R.string.image_content_description),
                    contentScale = ContentScale.Fit,
                    key = imageUri
                )
            }
            Column(
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .padding(bottom = dimensionResource(R.dimen.padding_vertical))
            ) {
                Text(
                    text = stringResource(R.string.upload),
                    style = MaterialTheme.typography.titleLarge
                )
                Button(
                    onClick = {
                        val permissionCheckResult =
                            ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
                        if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
                            // 已有权限时生成新URI并启动相机
                            val newUri = context.createImageFile()
                            imageUri = newUri
                            cameraLauncher.launch(newUri)
                        } else {
                            cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
                        }
                    },
                    shape = RoundedCornerShape(dimensionResource(R.dimen.button_radius)),
                    colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.theme)),
                    modifier = Modifier
                        .width(dimensionResource(R.dimen.button_width))
                        .height(dimensionResource(R.dimen.button_height))
                        .padding(top = dimensionResource(R.dimen.button_padding))
                ) {
                    Image(
                        painter = painterResource(R.drawable.photo_camera),
                        contentDescription = null,
                        modifier = Modifier.weight(0.5f)
                    )
                    Text(
                        text = stringResource(R.string.take_photo),
                        style = MaterialTheme.typography.titleLarge,
                        color = Color.Black,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier.weight(1.5f)
                    )
                }
            }
        }
    }
}

private fun Context.createImageFile(): Uri {
    val directory = File(filesDir, "images")
    if (!directory.exists()) {
        directory.mkdirs()
    }
    val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
    val file = File.createTempFile(
        "image_${timeStamp}_",
        ".png",
        directory
    )
    return FileProvider.getUriForFile(
        this,
        packageName + ".FileProvider",
        file
    )
}

关键改动说明

  • 每次拍照生成新URI:将createImageFile()调用移到权限通过或已有权限的点击逻辑中,确保每次拍照都使用全新的文件URI,避免覆盖旧文件且触发状态更新。
  • 强制组件重组:给AsyncImage添加key = imageUri,确保URI变化时重新创建组件,绕过Glide(AsyncImage默认使用Glide)的缓存机制;相机回调中通过imageUri = imageUri.copy()触发状态变化。
  • 状态同步:启动相机前先更新imageUri为新生成的URI,确保回调能正确关联到刚拍摄的文件。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 12:53:14