跳到主要内容

拍照模块

📋 模块概述

拍照模块提供独立的拍照功能,支持自定义文字叠加(设备ID+GPS位置)、自动上传等特性。

核心类:

  • android.znhaas.service.RecordingService (服务层)
  • android.znhaas.recoder.VideoRecorderAPI (API层)

🎯 主要功能

  • ✅ 独立拍照(不需要正在录制)
  • ✅ 自定义文字叠加(设备ID + GPS坐标)
  • ✅ 自动上传功能(可配置)
  • ✅ 文件自动命名(IMG_yyyyMMdd_HHmmss.jpg)
  • ✅ 拍照状态回调
  • ✅ 相机资源自动管理

📚 核心API

1. 通过RecordingService拍照

takePhoto(): Boolean

最常用的拍照方式,通过服务统一管理。

流程:

  1. 检查录制器是否初始化,未初始化则自动重新初始化
  2. 更新自定义文字(设备ID + GPS位置)
  3. 等待100ms确保文字更新完成
  4. 调用底层拍照API
  5. 根据配置决定是否自动上传

使用示例:

class MainActivity : AppCompatActivity() {

private var recordingService: RecordingService? = null

private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as RecordingService.LocalBinder
recordingService = binder.service

// 设置设备信息和位置服务
recordingService?.setDeviceInfo(deviceId, locationService)

// 设置监听器
recordingService?.setRecordingServiceListener(recordingListener)
}

override fun onServiceDisconnected(name: ComponentName?) {
recordingService = null
}
}

private val recordingListener = object : RecordingService.RecordingServiceListener {
override fun onPhotoTaken(filePath: String) {
Log.d(TAG, "拍照成功: $filePath")
runOnUiThread {
Toast.makeText(this@MainActivity,
"照片已保存", Toast.LENGTH_SHORT).show()
// 显示照片预览
showPhotoPreview(filePath)
}
}

override fun onPhotoError(errorMessage: String) {
Log.e(TAG, "拍照失败: $errorMessage")
runOnUiThread {
Toast.makeText(this@MainActivity,
"拍照失败: $errorMessage", Toast.LENGTH_SHORT).show()
}
}

// 其他回调方法...
override fun onRecordingStarted(filePath: String) {}
override fun onRecordingStopped(filePath: String) {}
override fun onRecordingError(errorMessage: String) {}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// 绑定服务
val intent = Intent(this, RecordingService::class.java)
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)

// 拍照按钮
photoButton.setOnClickListener {
takePhoto()
}
}

private fun takePhoto() {
val success = recordingService?.takePhoto()
if (success == true) {
Log.d(TAG, "拍照请求已发送")
} else {
Log.e(TAG, "拍照请求失败")
Toast.makeText(this, "拍照失败,请稍后重试", Toast.LENGTH_SHORT).show()
}
}

private fun showPhotoPreview(filePath: String) {
val bitmap = BitmapFactory.decodeFile(filePath)
imageView.setImageBitmap(bitmap)
}

override fun onDestroy() {
super.onDestroy()
if (isBound) {
unbindService(serviceConnection)
}
}
}

2. 通过VideoRecorderAPI拍照

更底层的拍照API,适合自定义场景。

val recorder = VideoRecorderFactory.createVideoRecorderAPI(context)

// 初始化
val config = RecordingUtils.createHDConfig()
recorder.initialize(context, config)

// 拍照
recorder.takePhoto(object : VideoRecorderAPI.PhotoListener {
override fun onPhotoTaken(filePath: String) {
Log.d(TAG, "照片保存到: $filePath")
}

override fun onPhotoError(error: String) {
Log.e(TAG, "拍照错误: $error")
}
})

🔧 自定义文字叠加

文字内容格式

拍照时自动叠加的文字格式:

{设备ID}
{纬度}, {经度}

示例:

HELMET_12345
22.547000, 114.085000

实现原理

服务层代码(RecordingService.java):

private String getCurrentLocationText() {
if (mLocationService == null) {
return mHasValidLocation ?
String.format("%.6f , %.6f", mLastValidLatitude, mLastValidLongitude) : "";
}

NativeLocationService.LocationResult location = mLocationService.getLastLocation();
long currentTime = System.currentTimeMillis();

if (location != null && location.getLatitude() != 0.0) {
// 更新缓存
mLastValidLatitude = location.getLatitude();
mLastValidLongitude = location.getLongitude();
mHasValidLocation = true;
mLastLocationUpdateTime = currentTime;
return String.format("%.6f , %.6f", mLastValidLatitude, mLastValidLongitude);
} else if (mHasValidLocation &&
(currentTime - mLastLocationUpdateTime) < LOCATION_CACHE_VALIDITY_MS) {
// 使用缓存的位置(5秒内有效)
return String.format("%.6f , %.6f", mLastValidLatitude, mLastValidLongitude);
}

return "";
}

public boolean takePhoto() {
// ... 初始化检查 ...

// 设置自定义文字
String customText = buildCustomText();
mRecorder.setCustomText(customText);

// 等待100ms确保文字更新完成
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

// 执行拍照
return mRecorder.takePhoto(photoListener);
}

private String buildCustomText() {
StringBuilder sb = new StringBuilder();

// 设备ID
if (mDeviceId != null && !mDeviceId.isEmpty()) {
sb.append(mDeviceId).append("\n");
}

// GPS位置
String locationText = getCurrentLocationText();
if (!locationText.isEmpty()) {
sb.append(" ").append(locationText);
}

return sb.toString();
}

位置缓存机制

  • 缓存有效期: 5秒
  • 缓存策略: 在有效期内,即使GPS无法获取新位置,也会使用缓存的位置
  • 超时处理: 超过有效期且无新位置,则不显示位置信息

📤 自动上传功能

配置上传

通过SharedPreferences配置是否自动上传:

val settings = getSharedPreferences("settings", Context.MODE_PRIVATE)
settings.edit().putBoolean("photoUploadAlarm", true).apply()

上传实现

// RecordingService.java
private void handlePhotoSuccess(String filePath) {
SharedPreferences settingPerf = getSharedPreferences("settings", Context.MODE_PRIVATE);
boolean photoUploadAlarm = settingPerf.getBoolean("photoUploadAlarm", false);

if (photoUploadAlarm && mUploadUtil != null) {
File photoFile = new File(filePath);
if (photoFile.exists()) {
mUploadUtil.uploadPhoto(photoFile, new UploadUtil.UploadCallback() {
@Override
public void onUploadSuccess(String fileName, int seqNo) {
Log.d(TAG, "照片上传成功: " + fileName);
}

@Override
public void onUploadFailure(String error, int seqNo) {
Log.e(TAG, "照片上传失败: " + error);
}
});
}
}
}

设置上传工具

recordingService?.setUploadUtil(uploadUtil)

📁 文件管理

文件命名

自动生成文件名格式:

IMG_yyyyMMdd_HHmmss.jpg

示例:

IMG_20250119_143052.jpg

保存路径

照片保存在应用专属目录:

/storage/emulated/0/Android/data/android.znhaas/files/

获取照片

val photosDir = File(context.getExternalFilesDir(null), "photos")
val photos = photosDir.listFiles { file ->
file.name.startsWith("IMG_") && file.name.endsWith(".jpg")
}

⚠️ 注意事项

1. 权限要求

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

2. 相机资源管理

  • 拍照不需要正在录制
  • 拍照后可能释放相机资源
  • 下次拍照会自动重新初始化
  • 录制时相机资源始终保持

3. 线程安全

  • 拍照操作是异步的
  • 回调在主线程执行
  • 可以直接更新UI

4. 文字更新延迟

拍照前会等待100ms确保文字叠加完成:

// 等待100ms确保文字更新完成
Thread.sleep(100);

5. 初始化检查

如果相机未初始化,会自动重新初始化:

if (!reinitializeResources()) {
return false;
}

🔗 相关资源

  • 源码:

    • app/src/main/java/android/znhaas/service/RecordingService.java
    • app/src/main/java/android/znhaas/recoder/VideoRecorderAPI.java
  • 相关模块:

📚 扩展阅读