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
相关产品推荐
相关产品推荐

