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

Android(Kotlin)图片EXIF获取GPS与时间数据及空值问题排查

解决CameraX拍照后EXIF GPS坐标为空的问题并保存坐标

问题根源

CameraX的ImageCapture默认不会自动将设备GPS坐标写入照片EXIF元数据,所以你读取的TAG_GPS_LATITUDE和TAG_GPS_LONGITUDE自然为null。此外还需确保已正确获取定位权限。

解决步骤

1. 配置权限

在AndroidManifest.xml中添加必要权限:

<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 精确定位权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 声明依赖GPS硬件 -->
<uses-feature android:name="android.hardware.location.gps" />

在Activity/Fragment中动态申请危险权限:

private val requiredPermissions = arrayOf(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION)
private const val REQUEST_CODE_PERMISSIONS = 100

// 初始化相机前执行权限检查
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
    ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this, requiredPermissions, REQUEST_CODE_PERMISSIONS)
}

2. 获取当前GPS位置

使用Fused Location Provider获取可靠的当前位置,先添加依赖到build.gradle(Module):

implementation "com.google.android.gms:play-services-location:21.0.1"

封装位置获取逻辑:

class LocationHelper(private val context: Context) {
    private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)

    fun getCurrentLocation(onSuccess: (Location) -> Unit, onFailure: (Exception) -> Unit) {
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            onFailure(SecurityException("定位权限未授权"))
            return
        }

        fusedLocationClient.lastLocation
            .addOnSuccessListener { location ->
                location?.let(onSuccess) ?: onFailure(IllegalStateException("无法获取当前位置"))
            }
            .addOnFailureListener(onFailure)
    }
}

3. 修改拍照逻辑,写入GPS到EXIF

更新CameraFileUtils,在拍照后获取位置并写入照片EXIF:

object CameraFileUtils {
    private lateinit var locationHelper: LocationHelper

    fun init(context: Context) {
        locationHelper = LocationHelper(context)
    }

    fun takePicture(
        cameraController: CameraController,
        context: Context,
        executor: ExecutorService,
        onImageCaptured: (Uri, PhotoMetadata?) -> Unit,
        onError: (ImageCaptureException) -> Unit
    ) {
        val photoFile = createPhotoFile(context)
        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

        cameraController.takePicture(outputOptions,
            executor,
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                    locationHelper.getCurrentLocation(
                        onSuccess = { location ->
                            writeGpsToExif(photoFile, location)
                            val savedUri = savePhotoToGallery(context, photoFile)
                            val metadata = getPhotoMetadata(photoFile)
                            onImageCaptured(savedUri, metadata)
                        },
                        onFailure = { e ->
                            Log.e("Location", "获取位置失败: ${e.message}")
                            val savedUri = savePhotoToGallery(context, photoFile)
                            onImageCaptured(savedUri, null)
                        }
                    )
                }

                override fun onError(exception: ImageCaptureException) {
                    onError(exception)
                }
            })
    }

    // 写入GPS坐标到EXIF
    private fun writeGpsToExif(photoFile: File, location: Location) {
        val exif = ExifInterface(photoFile)
        // 转换为EXIF要求的度分秒格式
        val latDegrees = Location.convert(location.latitude, Location.FORMAT_SECONDS)
        val lngDegrees = Location.convert(location.longitude, Location.FORMAT_SECONDS)
        // 设置GPS方向标签
        exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE, latDegrees)
        exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, if (location.latitude >= 0) "N" else "S")
        exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, lngDegrees)
        exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, if (location.longitude >= 0) "E" else "W")
        // 设置拍摄时间
        exif.setAttribute(ExifInterface.TAG_DATETIME, SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.UK).format(Date()))
        exif.saveAttributes()
    }

    // 读取并转换照片元数据
    private fun getPhotoMetadata(photoFile: File): PhotoMetadata? {
        return try {
            val exif = ExifInterface(photoFile)
            val latStr = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)
            val latRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF)
            val lngStr = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE)
            val lngRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF)
            val dateTime = exif.getAttribute(ExifInterface.TAG_DATETIME)

            PhotoMetadata(
                convertExifGpsToDecimal(latStr, latRef),
                convertExifGpsToDecimal(lngStr, lngRef),
                dateTime
            )
        } catch (e: Exception) {
            Log.e("Exif", "读取元数据失败: ${e.message}")
            null
        }
    }

    // 转换EXIF度分秒坐标为十进制
    private fun convertExifGpsToDecimal(gpsStr: String?, ref: String?): Double? {
        if (gpsStr.isNullOrEmpty() || ref.isNullOrEmpty()) return null
        val parts = gpsStr.split(",")
        if (parts.size != 3) return null
        return try {
            val degrees = parts[0].toDouble()
            val minutes = parts[1].toDouble()
            val seconds = parts[2].toDouble()
            var decimal = degrees + (minutes / 60) + (seconds / 3600)
            if (ref == "S" || ref == "W") decimal *= -1
            decimal
        } catch (e: NumberFormatException) {
            null
        }
    }

    // 元数据数据类,用于保存坐标和时间
    data class PhotoMetadata(val latitude: Double?, val longitude: Double?, val captureTime: String?)

    // 保存照片到相册
    private fun savePhotoToGallery(context: Context, photoFile: File): Uri {
        val contentResolver = context.contentResolver
        val fileName = photoFileName()
        val contentValues = ContentValues().apply {
            put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraXPhotos")
            }
        }
        val newImageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
            ?: throw IOException("无法创建媒体存储URI")

        contentResolver.openOutputStream(newImageUri).use { outputStream ->
            FileInputStream(photoFile).use { inputStream ->
                inputStream.copyTo(outputStream!!)
            }
        }
        return newImageUri
    }

    // 以下为原有工具方法
    private fun createPhotoFile(context: Context): File {
        val outputDirectory = getOutputDirectory(context)
        return File(outputDirectory, photoFileName()).apply {
            parentFile?.mkdirs()
        }
    }

    private fun photoFileName() =
        SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.UK).format(System.currentTimeMillis()) + ".jpg"

    private fun getOutputDirectory(context: Context): File {
        return context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) ?: context.filesDir
    }
}

4. 使用坐标进行验证和路线规划

在拍照回调中获取PhotoMetadata,即可保存坐标用于后续操作:

// 初始化工具类
CameraFileUtils.init(this)
// 调用拍照
CameraFileUtils.takePicture(
    cameraController = cameraController,
    context = this,
    executor = Executors.newSingleThreadExecutor(),
    onImageCaptured = { uri, metadata ->
        metadata?.let {
            // 保存坐标到ViewModel或全局变量
            viewModel.addPhotoLocation(it.latitude!!, it.longitude!!, it.captureTime)
            // 验证坐标
            Log.d("Photo Data", "纬度: ${it.latitude}, 经度: ${it.longitude}, 时间: ${it.captureTime}")
            // 路线规划示例:启动Google Maps导航
            val navUri = Uri.parse("google.navigation:q=${it.latitude},${it.longitude}")
            val navIntent = Intent(Intent.ACTION_VIEW, navUri).apply {
                setPackage("com.google.android.apps.maps")
            }
            startActivity(navIntent)
        }
    },
    onError = { exception ->
        Log.e("Camera", "拍照失败: ${exception.message}")
    }
)

关键注意事项

  • 必须确保相机和定位权限均已授权,否则无法获取位置或完成拍照。
  • 建议在拍照前预获取位置,避免拍照后位置延迟导致的坐标偏差。
  • EXIF中的GPS为度分秒格式,需转换为十进制才能直接用于地图导航等场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 01:37:37