Add camera functionality with CameraX, permissions, watermarks, and settings page
This commit is contained in:
@@ -37,6 +37,20 @@ dependencies {
|
|||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
|
||||||
|
// CameraX core library
|
||||||
|
def camerax_version = "1.3.0"
|
||||||
|
implementation "androidx.camera:camera-core:${camerax_version}"
|
||||||
|
implementation "androidx.camera:camera-camera2:${camerax_version}"
|
||||||
|
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
|
||||||
|
implementation "androidx.camera:camera-video:${camerax_version}"
|
||||||
|
|
||||||
|
// CameraX View class
|
||||||
|
implementation "androidx.camera:camera-view:${camerax_version}"
|
||||||
|
|
||||||
|
// CameraX Extensions (Effects) - Optional
|
||||||
|
implementation "androidx.camera:camera-extensions:${camerax_version}"
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
|
|||||||
@@ -1,17 +1,32 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.example.app">
|
package="com.example.app">
|
||||||
|
|
||||||
|
<!-- 相机权限 -->
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
|
<!-- 位置权限 -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
|
<!-- 硬件功能声明 -->
|
||||||
|
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
||||||
|
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.LogCam">
|
android:theme="@style/Theme.LogCam"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
|
tools:targetApi="q">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:screenOrientation="portrait">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
|||||||
283
app/src/main/java/com/example/app/CameraActivity.kt
Normal file
283
app/src/main/java/com/example/app/CameraActivity.kt
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
package com.example.app
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.location.Geocoder
|
||||||
|
import android.location.Location
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Looper
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.camera.core.ImageCapture
|
||||||
|
import androidx.camera.core.ImageCaptureException
|
||||||
|
import androidx.camera.core.Preview
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class CameraActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LogCam"
|
||||||
|
private const val REQUEST_CODE_PERMISSIONS = 10
|
||||||
|
private val REQUIRED_PERMISSIONS = arrayOf(
|
||||||
|
Manifest.permission.CAMERA,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var viewFinder: PreviewView
|
||||||
|
private lateinit var captureButton: ImageButton
|
||||||
|
private lateinit var settingsButton: ImageButton
|
||||||
|
private lateinit var photoPreviewLayout: LinearLayout
|
||||||
|
private var imageCapture: ImageCapture? = null
|
||||||
|
private lateinit var cameraExecutor: ExecutorService
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
// 初始化视图
|
||||||
|
viewFinder = findViewById(R.id.viewFinder)
|
||||||
|
captureButton = findViewById(R.id.captureButton)
|
||||||
|
settingsButton = findViewById(R.id.settingsButton)
|
||||||
|
photoPreviewLayout = findViewById(R.id.photoPreviewLayout)
|
||||||
|
|
||||||
|
// 请求权限
|
||||||
|
if (allPermissionsGranted()) {
|
||||||
|
startCamera()
|
||||||
|
} else {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置点击监听器
|
||||||
|
captureButton.setOnClickListener { takePhoto() }
|
||||||
|
settingsButton.setOnClickListener { openSettings() }
|
||||||
|
|
||||||
|
// 初始化线程池
|
||||||
|
cameraExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
|
||||||
|
ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startCamera() {
|
||||||
|
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
|
||||||
|
|
||||||
|
cameraProviderFuture.addListener({
|
||||||
|
val cameraProvider = cameraProviderFuture.get()
|
||||||
|
|
||||||
|
val preview = Preview.Builder()
|
||||||
|
.build()
|
||||||
|
.also {
|
||||||
|
it.setSurfaceProvider(viewFinder.surfaceProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
imageCapture = ImageCapture.Builder()
|
||||||
|
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
|
|
||||||
|
try {
|
||||||
|
cameraProvider.unbindAll()
|
||||||
|
cameraProvider.bindToLifecycle(
|
||||||
|
this, cameraSelector, preview, imageCapture
|
||||||
|
)
|
||||||
|
|
||||||
|
} catch (exc: Exception) {
|
||||||
|
Log.e(TAG, "相机绑定失败", exc)
|
||||||
|
}
|
||||||
|
}, ContextCompat.getMainExecutor(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun takePhoto() {
|
||||||
|
val imageCapture = imageCapture ?: return
|
||||||
|
|
||||||
|
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(
|
||||||
|
createImageUri()
|
||||||
|
).build()
|
||||||
|
|
||||||
|
imageCapture.takePicture(
|
||||||
|
outputFileOptions,
|
||||||
|
ContextCompat.getMainExecutor(this),
|
||||||
|
object : ImageCapture.OnImageSavedCallback {
|
||||||
|
override fun onError(exception: ImageCaptureException) {
|
||||||
|
Log.e(TAG, "照片拍摄失败: ${exception.message}", exception)
|
||||||
|
Toast.makeText(
|
||||||
|
baseContext,
|
||||||
|
"照片拍摄失败: ${exception.message}",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
|
||||||
|
// 添加水印到已保存的照片
|
||||||
|
val savedUri = output.savedUri ?: return
|
||||||
|
addWatermarkToImage(savedUri)
|
||||||
|
|
||||||
|
Toast.makeText(
|
||||||
|
baseContext,
|
||||||
|
"照片已保存",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
|
||||||
|
// 刷新相册
|
||||||
|
refreshGallery(savedUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createImageUri(): Uri {
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.Images.Media.DISPLAY_NAME, "巡检_${System.currentTimeMillis()}.jpg")
|
||||||
|
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
||||||
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
|
||||||
|
put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
?: throw IOException("无法创建图片URI")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addWatermarkToImage(imageUri: Uri) {
|
||||||
|
try {
|
||||||
|
val inputStream = contentResolver.openInputStream(imageUri)
|
||||||
|
val originalBitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
inputStream?.close()
|
||||||
|
|
||||||
|
val watermarkedBitmap = addWatermark(originalBitmap)
|
||||||
|
|
||||||
|
// 将带水印的图片保存回原URI
|
||||||
|
val outputStream = contentResolver.openOutputStream(imageUri, "rwt") // truncate and write
|
||||||
|
watermarkedBitmap.compress(Bitmap.CompressFormat.JPEG, 95, outputStream)
|
||||||
|
outputStream?.close()
|
||||||
|
|
||||||
|
// 回收位图以释放内存
|
||||||
|
if (watermarkedBitmap != originalBitmap) {
|
||||||
|
originalBitmap.recycle()
|
||||||
|
}
|
||||||
|
watermarkedBitmap.recycle()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "添加水印失败: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addWatermark(bitmap: Bitmap): Bitmap {
|
||||||
|
val watermarkedBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
|
||||||
|
val canvas = Canvas(watermarkedBitmap)
|
||||||
|
val paint = Paint().apply {
|
||||||
|
color = Color.WHITE
|
||||||
|
textSize = 48f
|
||||||
|
typeface = Typeface.DEFAULT_BOLD
|
||||||
|
textAlign = Paint.Align.LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制时间水印
|
||||||
|
val timestamp = formatDate(System.currentTimeMillis())
|
||||||
|
canvas.drawText(timestamp, 50f, watermarkedBitmap.height - 100f, paint)
|
||||||
|
|
||||||
|
// 绘制位置水印
|
||||||
|
getCurrentLocation()?.let { location ->
|
||||||
|
val address = getAddressFromLocation(location.latitude, location.longitude)
|
||||||
|
canvas.drawText(address, 50f, watermarkedBitmap.height - 50f, paint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return watermarkedBitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshGallery(uri: Uri) {
|
||||||
|
// 通知相册更新
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.Images.Media.IS_PENDING, 0)
|
||||||
|
}
|
||||||
|
contentResolver.update(uri, contentValues, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将时间戳转换为指定格式的日期时间字符串
|
||||||
|
fun formatDate(timestamp: Long): String {
|
||||||
|
val sdf = SimpleDateFormat("yyyy年-MM月-dd日 HH:mm:ss", Locale.getDefault())
|
||||||
|
return sdf.format(Date(timestamp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用Geocoder将经纬度转换为地址
|
||||||
|
private fun getAddressFromLocation(latitude: Double, longitude: Double): String? {
|
||||||
|
return try {
|
||||||
|
val geocoder = Geocoder(this, Locale.getDefault())
|
||||||
|
val addresses = geocoder.getFromLocation(latitude, longitude, 1)
|
||||||
|
if (addresses?.isEmpty() == false) {
|
||||||
|
val address = addresses[0]
|
||||||
|
val addressLine = address.getAddressLine(0)
|
||||||
|
addressLine ?: "${latitude}, ${longitude}"
|
||||||
|
} else {
|
||||||
|
"${latitude}, ${longitude}"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "无法解析地址: ${e.message}")
|
||||||
|
"${latitude}, ${longitude}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private fun getCurrentLocation(): Location? {
|
||||||
|
// 这里应该实现获取当前位置的逻辑
|
||||||
|
// 简化实现,返回null,实际应用中需要使用LocationManager或FusedLocationProviderClient
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openSettings() {
|
||||||
|
// 打开设置页面
|
||||||
|
val settingsFragment = SettingsFragment()
|
||||||
|
settingsFragment.show(supportFragmentManager, "SettingsFragment")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(
|
||||||
|
requestCode: Int,
|
||||||
|
permissions: Array<String>,
|
||||||
|
grantResults: IntArray
|
||||||
|
) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
if (requestCode == REQUEST_CODE_PERMISSIONS) {
|
||||||
|
if (allPermissionsGranted()) {
|
||||||
|
startCamera()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "权限被拒绝,相机功能不可用", Toast.LENGTH_SHORT).show()
|
||||||
|
// 可以在这里提供降级功能,比如手动输入位置信息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
cameraExecutor.shutdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.app
|
package com.example.app
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
|
||||||
@@ -7,5 +8,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
// 直接启动相机Activity
|
||||||
|
startActivity(Intent(this, CameraActivity::class.java))
|
||||||
|
finish() // 关闭MainActivity,避免返回键回到空白页面
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
121
app/src/main/java/com/example/app/SettingsFragment.kt
Normal file
121
app/src/main/java/com/example/app/SettingsFragment.kt
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package com.example.app
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.location.Location
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.RadioButton
|
||||||
|
import android.widget.RadioGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
|
||||||
|
class SettingsFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private lateinit var locationCalibrationRadioGroup: RadioGroup
|
||||||
|
private lateinit var manualLatInput: EditText
|
||||||
|
private lateinit var manualLngInput: EditText
|
||||||
|
private lateinit var distanceInput: EditText
|
||||||
|
private lateinit var saveButton: Button
|
||||||
|
private lateinit var cancelButton: Button
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_settings, container, false)
|
||||||
|
|
||||||
|
initViews(view)
|
||||||
|
setupClickListeners()
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initViews(view: View) {
|
||||||
|
locationCalibrationRadioGroup = view.findViewById(R.id.locationCalibrationRadioGroup)
|
||||||
|
manualLatInput = view.findViewById(R.id.manualLatInput)
|
||||||
|
manualLngInput = view.findViewById(R.id.manualLngInput)
|
||||||
|
distanceInput = view.findViewById(R.id.distanceInput)
|
||||||
|
saveButton = view.findViewById(R.id.saveButton)
|
||||||
|
cancelButton = view.findViewById(R.id.cancelButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupClickListeners() {
|
||||||
|
locationCalibrationRadioGroup.setOnCheckedChangeListener { _, checkedId ->
|
||||||
|
val selectedMethod = when (checkedId) {
|
||||||
|
R.id.latLngDistanceCalibration -> "经纬度+距离校准"
|
||||||
|
R.id.onlineQueryCalibration -> "联网查询校准"
|
||||||
|
else -> "默认"
|
||||||
|
}
|
||||||
|
Log.d("SettingsFragment", "选择了校准方式: $selectedMethod")
|
||||||
|
}
|
||||||
|
|
||||||
|
saveButton.setOnClickListener {
|
||||||
|
saveSettings()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelButton.setOnClickListener {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveSettings() {
|
||||||
|
// 获取选择的位置校准方式
|
||||||
|
val selectedMethodId = locationCalibrationRadioGroup.checkedRadioButtonId
|
||||||
|
val calibrationMethod = when (selectedMethodId) {
|
||||||
|
R.id.latLngDistanceCalibration -> "lat_lng_distance"
|
||||||
|
R.id.onlineQueryCalibration -> "online_query"
|
||||||
|
else -> "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存设置到SharedPreferences
|
||||||
|
val sharedPreferences = requireContext().getSharedPreferences("LogCamSettings", Context.MODE_PRIVATE)
|
||||||
|
val editor = sharedPreferences.edit()
|
||||||
|
editor.putString("calibration_method", calibrationMethod)
|
||||||
|
|
||||||
|
// 如果选择了手动输入经纬度,则保存经纬度和距离
|
||||||
|
if (selectedMethodId == R.id.latLngDistanceCalibration) {
|
||||||
|
try {
|
||||||
|
val latStr = manualLatInput.text.toString()
|
||||||
|
val lngStr = manualLngInput.text.toString()
|
||||||
|
val distanceStr = distanceInput.text.toString()
|
||||||
|
|
||||||
|
if (latStr.isNotEmpty() && lngStr.isNotEmpty()) {
|
||||||
|
val latitude = latStr.toDouble()
|
||||||
|
val longitude = lngStr.toDouble()
|
||||||
|
val distance = if (distanceStr.isNotEmpty()) distanceStr.toDouble() else 0.0
|
||||||
|
|
||||||
|
if (latitude in -90.0..90.0 && longitude in -180.0..180.0) {
|
||||||
|
editor.putFloat("manual_latitude", latitude.toFloat())
|
||||||
|
editor.putFloat("manual_longitude", longitude.toFloat())
|
||||||
|
editor.putFloat("calibration_distance", distance.toFloat())
|
||||||
|
Toast.makeText(context, "设置已保存", Toast.LENGTH_SHORT).show()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "经纬度超出有效范围", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
Toast.makeText(context, "请输入有效的数字", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val REQUEST_CODE_LOCATION_PERMISSION = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
8
app/src/main/res/drawable/circle_button_background.xml
Normal file
8
app/src/main/res/drawable/circle_button_background.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<solid android:color="#1C1B1F" />
|
||||||
|
<stroke
|
||||||
|
android:width="4dp"
|
||||||
|
android:color="@android:color/white" />
|
||||||
|
</shape>
|
||||||
13
app/src/main/res/drawable/ic_camera.xml
Normal file
13
app/src/main/res/drawable/ic_camera.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorOnPrimary">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z"/>
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable/ic_settings.xml
Normal file
10
app/src/main/res/drawable/ic_settings.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorOnPrimary">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.82,11.69 4.82,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||||
|
</vector>
|
||||||
@@ -4,30 +4,73 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_height"
|
||||||
tools:context=".MainActivity">
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
<TextView
|
<!-- CameraView for displaying the camera preview -->
|
||||||
android:id="@+id/titleText"
|
<androidx.camera.view.PreviewView
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/viewFinder"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:text="@string/app_name"
|
android:layout_height="0dp"
|
||||||
android:textSize="24sp"
|
app:layout_constraintBottom_toTopOf="@id/controlPanel"
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<!-- Control panel for camera controls -->
|
||||||
android:id="@+id/descriptionText"
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/controlPanel"
|
||||||
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Welcome to LogCam"
|
android:orientation="vertical"
|
||||||
android:textSize="16sp"
|
android:padding="16dp"
|
||||||
android:layout_marginTop="16dp"
|
android:background="@color/white"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
app:layout_constraintTop_toBottomOf="@id/titleText" />
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<!-- Capture button -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/captureButton"
|
||||||
|
android:layout_width="72dp"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:background="@drawable/circle_button_background"
|
||||||
|
android:src="@drawable/ic_camera"
|
||||||
|
android:contentDescription="Capture photo" />
|
||||||
|
|
||||||
|
<!-- Settings button -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/settingsButton"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_settings"
|
||||||
|
android:contentDescription="Settings" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Preview of captured photos -->
|
||||||
|
<HorizontalScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/photoPreviewLayout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal" />
|
||||||
|
|
||||||
|
</HorizontalScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
143
app/src/main/res/layout/fragment_settings.xml
Normal file
143
app/src/main/res/layout/fragment_settings.xml
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="设置"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
|
<!-- 地点校准方式 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="地点校准方式"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/locationCalibrationRadioGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/latLngDistanceCalibration"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="经纬度 + 距离校准"
|
||||||
|
android:checked="true" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/onlineQueryCalibration"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="联网查询校准" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<!-- 手动经纬度输入区域 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/manualInputLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="手动输入经纬度"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/manualLatInput"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="纬度 (-90 到 90)"
|
||||||
|
android:inputType="numberSigned|numberDecimal" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/manualLngInput"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="经度 (-180 到 180)"
|
||||||
|
android:inputType="numberSigned|numberDecimal" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/distanceInput"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="校准距离 (米)"
|
||||||
|
android:inputType="numberDecimal" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 水印样式设置 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="水印样式"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/watermarkStyleSpinner"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- 合成质量设置 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="合成质量"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/qualitySpinner"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- 按钮 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="end">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/cancelButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="取消"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/saveButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="保存" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
Reference in New Issue
Block a user