diff --git a/app/src/main/assets/best_embedding_model_mobile.pt b/app/src/main/assets/best_embedding_model_mobile.pt index 5ccae40..ec53847 100644 Binary files a/app/src/main/assets/best_embedding_model_mobile.pt and b/app/src/main/assets/best_embedding_model_mobile.pt differ diff --git a/app/src/main/java/com/sw/dualscreen/MyApp.kt b/app/src/main/java/com/sw/dualscreen/MyApp.kt index 5aadbc9..83b9582 100644 --- a/app/src/main/java/com/sw/dualscreen/MyApp.kt +++ b/app/src/main/java/com/sw/dualscreen/MyApp.kt @@ -9,10 +9,12 @@ import timber.log.Timber class MyApp : App() { companion object { const val DEBUG: Boolean = true + var instance: MyApp?=null } override fun onCreate() { super.onCreate() + instance = this Timber.plant(Timber.DebugTree()) var deviceId = AppUtil.getUDID(this) Timber.d("UDID = ${AppUtil.getUDID(this)}") diff --git a/app/src/main/java/com/sw/dualscreen/activity/FoodCollectionActivity.kt b/app/src/main/java/com/sw/dualscreen/activity/FoodCollectionActivity.kt index 78d9af1..f2f420c 100644 --- a/app/src/main/java/com/sw/dualscreen/activity/FoodCollectionActivity.kt +++ b/app/src/main/java/com/sw/dualscreen/activity/FoodCollectionActivity.kt @@ -3,12 +3,17 @@ package com.sw.dualscreen.activity import android.annotation.SuppressLint import android.content.Context import android.content.Intent +import android.graphics.Bitmap import android.graphics.Typeface import android.net.Uri +import android.view.View import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.activity.viewModels +import androidx.camera.view.PreviewView +import androidx.core.net.toUri +import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import com.example.utils.FloatBase64Utils @@ -19,7 +24,9 @@ import com.sw.dualscreen.adapter.GridSpacingItemDecoration import com.sw.dualscreen.adapter.dpToPx import com.sw.dualscreen.databinding.ActivityFoodCollectionBinding import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding +import com.sw.dualscreen.databinding.LayoutCameraPreviewBinding import com.sw.dualscreen.ext.clickWithDebounce +import com.sw.dualscreen.ext.dp import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.objbox.Food import com.sw.dualscreen.objbox.FoodCollectionBean @@ -28,6 +35,7 @@ import com.sw.dualscreen.objbox.ObjectBox import com.sw.dualscreen.utils.BitmapCropper import com.sw.dualscreen.utils.BitmapSaver import com.sw.dualscreen.utils.CameraHelper +import com.sw.dualscreen.utils.CameraUtils import com.sw.dualscreen.utils.Debouncer import com.sw.dualscreen.utils.ImageUtil import com.sw.dualscreen.viewmodel.BaseViewModel @@ -46,17 +54,19 @@ class FoodCollectionActivity : BaseActivity() { private val viewModel by viewModels() private var selectedFoodId: String? = "" private var selectedFoodName: String? = "" - private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f) - private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f) - private var module: Module? = null private var box: Box? = null private val foodCollectionList: MutableList = mutableListOf() - private lateinit var cameraHelper: CameraHelper +// private lateinit var cameraHelper: CameraHelper private lateinit var foodAdapter: GenericItemAdapter + private val foodList = mutableListOf() // 适配器内部维护的数据列表 private val debouncer = Debouncer(2000) + private lateinit var previewView: PreviewView + private val cameraUtils: CameraUtils by lazy { + CameraUtils(this) + } private val collectionAdapter: FoodCollectionAdapter by lazy { FoodCollectionAdapter(foodCollectionList).apply { @@ -67,9 +77,15 @@ class FoodCollectionActivity : BaseActivity() { // } // } addOnItemChildClickListener(R.id.ivDelete) { _, _, position -> - list.removeAt(position) - collectionAdapter.notifyItemRemoved(position) - collectionAdapter.notifyItemRangeChanged(position, list.size) +// list.removeAt(position) +// collectionAdapter.notifyItemRemoved(position) +// collectionAdapter.notifyItemRangeChanged(position, list.size) + foodCollectionList[position].let { + it.bitmap = null + it.isShowCamera = true + it.isFinish = false + } + notifyItemChanged(position) } } } @@ -82,54 +98,77 @@ class FoodCollectionActivity : BaseActivity() { return ActivityFoodCollectionBinding.inflate(layoutInflater) } - private fun cameraCallback(uri:Uri){ + private val cameraCallback: (Uri) -> Unit = { uri -> val index = foodCollectionList.indexOfFirst { it.bitmap == null } if (index == -1) { ToastUtils.showToast("每次只允许保存6条数据") - return + rerurn@ cameraCallback } ImageUtil.uriToBitmap(this, uri)?.let { bitmap -> - val cropBitmap = BitmapCropper.cropCenter( - original = bitmap, - targetWidth = 1300, targetHeight = 900, - //offsetX = 30, offsetY = 100 - ) +// val cropBitmap = BitmapCropper.cropCenter( +// original = bitmap, +// targetWidth = 1300, targetHeight = 900, +// //offsetX = 30, offsetY = 100 +// ) val file = BitmapSaver.saveToAppFilesDir( - cropBitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg" + bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg" ) Timber.d("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}") foodCollectionList[index].let { - it.bitmap = cropBitmap + it.bitmap = bitmap it.isShowCamera = false + it.imageUri = file?.toUri() } collectionAdapter.notifyItemChanged(index) } } - override fun initialize() { - // 初始化 CameraHelper - cameraHelper = CameraHelper( - context = this, - caller = this, - authority = "${packageName}.fileprovider" - ) { uri, path -> - //if (foodCollectionList.size < 6) { - // val insertIndex = if (foodCollectionList.isEmpty()) 0 else foodCollectionList.size - 1 - // foodCollectionList.add(insertIndex, FoodCollectionBean(imageUri = uri)) - //} else { - // foodCollectionList[foodCollectionList.size - 1] = FoodCollectionBean(imageUri = uri) - //} - //collectionAdapter.notifyDataSetChanged() - cameraCallback(uri) + @SuppressLint("NotifyDataSetChanged") + private fun takePhoto() { + val count = foodCollectionList.count { it.bitmap != null } + if (count == 6) { + ToastUtils.showToast("每次只允许保存6条数据") + return } + cameraUtils.takePhoto(cameraCallback) + } + + override fun initialize() { + cameraUtils.initCamera() + val previewBinding = + LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview) + previewView = previewBinding.previewView.also { + it.updateLayoutParams { + width = 180.dp + height = 180.dp + } + } + cameraUtils.setPreviewController(previewView) + // 初始化 CameraHelper +// cameraHelper = CameraHelper( +// context = this, +// caller = this, +// authority = "${packageName}.fileprovider" +// ) { uri, path -> +// //if (foodCollectionList.size < 6) { +// // val insertIndex = if (foodCollectionList.isEmpty()) 0 else foodCollectionList.size - 1 +// // foodCollectionList.add(insertIndex, FoodCollectionBean(imageUri = uri)) +// //} else { +// // foodCollectionList[foodCollectionList.size - 1] = FoodCollectionBean(imageUri = uri) +// //} +// //collectionAdapter.notifyDataSetChanged() +// cameraCallback(uri) +// } binding.ivBack.setOnClickListener { val intent = Intent(this, MainActivity::class.java) startActivity(intent) finish() } - foodCollectionList.add(FoodCollectionBean(isShowCamera = true)) + repeat(6) { + foodCollectionList.add(FoodCollectionBean(isShowCamera = true)) + } binding.rvFoodCollection.let { it.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false) it.adapter = collectionAdapter @@ -164,15 +203,17 @@ class FoodCollectionActivity : BaseActivity() { } binding.btnCollectedGoods.setOnClickListener { - + startActivity(Intent(this, CollectedDataActivity::class.java)) } binding.btnTakePhoto.clickWithDebounce { + binding.btnTakePhoto.text = "拍照" val count = foodCollectionList.count { it.bitmap != null } if (count == 6) { ToastUtils.showToast("每次只允许保存6条数据") return@clickWithDebounce } - cameraHelper.openCamera() +// cameraHelper.openCamera() + takePhoto() } binding.btnClearData.setOnClickListener { clearData() } foodAdapter = createAdapter() @@ -192,11 +233,11 @@ class FoodCollectionActivity : BaseActivity() { //Loading.show(this) showWaitingDialog("加载中……") Thread { - foodCollectionList.forEachIndexed { index, it -> - - if (!it.isShowCamera) - image2VectorTask(it.imageUri!!, index) - } + foodCollectionList + .filter { it.bitmap != null } + .forEachIndexed { index, it -> + image2VectorTask(it, index) + } runOnUiThread { window.decorView.postDelayed({ //Loading.dismiss() @@ -206,44 +247,33 @@ class FoodCollectionActivity : BaseActivity() { }.start() } - private fun image2VectorTask(imageUri: Uri, position: Int) { - if (module == null) { - module = - Module.load(FoodModule.copyAssetToCache(this, "best_embedding_model_mobile.pt")) - } + private fun initBox() { if (box == null) { box = ObjectBox.boxStore.boxFor(Food::class) } - ImageUtil.uriToBitmap(this, imageUri)?.let { bitmap -> - //val bitmap2 = BitmapCropper.cropCenter(bitmap!!, 500, 500) - val inputTensor = TensorImageUtils.bitmapToFloat32Tensor( - bitmap, - NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB - NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB - ) - val outputTensor = module!!.forward(IValue.from(inputTensor)).toTensor() + } - val base64Str = FloatBase64Utils.floatArrayToBase64(outputTensor.dataAsFloatArray) - Timber.tag("mzf1").e(base64Str) - val imageVector = outputTensor.dataAsFloatArray - box?.put(Food(name = selectedFoodName, foodIdx = 0, foodVector = imageVector)) - foodCollectionList[position].let { - it.imageVector = imageVector - it.isFinish = true + private fun image2VectorTask(item: FoodCollectionBean, position: Int) { + initBox() + val imageVector = FoodModule.bitmap2FloatArray(item.bitmap!!) - Timber - viewModel.postImageData( - context, - foodId = selectedFoodId.toString(), - foodName = selectedFoodName.toString(), - foodVector = base64Str, - uri = imageUri - ) - } - runOnUiThread { - collectionAdapter.notifyItemChanged(position) - } - Timber.tag("registerDataChange").e("box.all.size=${box?.all?.size}") + val base64Str = FloatBase64Utils.floatArrayToBase64(imageVector) + Timber.tag("mzf1").e(base64Str) +// viewModel.postImageData( +// context, +// foodId = selectedFoodId.toString(), +// foodName = selectedFoodName.toString(), +// foodVector = base64Str, +// uri = item.imageUri!! +// ) + box?.put(Food(name = checkedItem!!.foodName, foodIdx = 0, foodVector = imageVector)) + foodCollectionList[position].let { + it.imageVector = imageVector + it.isFinish = true + } + + runOnUiThread { + collectionAdapter.notifyItemChanged(position) } } @@ -258,6 +288,8 @@ class FoodCollectionActivity : BaseActivity() { super.registerDataChange() lifecycleScope.launch { viewModel.searchFoodInfoList.collect { +// foodList.clear() +// foodList.addAll(it) foodAdapter.updateData(it) } } @@ -294,7 +326,9 @@ class FoodCollectionActivity : BaseActivity() { } ) } + private var clickIndex = -1 + @SuppressLint("NotifyDataSetChanged") private fun clearData() { foodCollectionList.forEach { @@ -306,9 +340,27 @@ class FoodCollectionActivity : BaseActivity() { clickIndex = -1 binding.editFoodName.setText("") - foodAdapter.updateData(mutableListOf()) - foodAdapter.notifyDataSetChanged() +// foodList.clear() +// foodAdapter.updateData(mutableListOf()) +// foodAdapter.notifyDataSetChanged() //loadEmptyView() } + override fun onResume() { + super.onResume() + cameraUtils.bind() + binding.llCameraFlag.run { + visibility = View.VISIBLE + postDelayed({ + visibility = View.GONE + }, 3000) + } + } + + override fun onPause() { + super.onPause() + cameraUtils.unbind() + binding.llCameraFlag.visibility = View.VISIBLE + } + } \ No newline at end of file diff --git a/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt b/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt index ac29b77..2f405fc 100644 --- a/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt +++ b/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt @@ -235,15 +235,15 @@ class MainActivity : BaseActivity() { ImageUtil.uriToBitmap(this, photoUri)?.let { bitmap -> Timber.d("registerDataChange photoUri 拿到bitmap") - val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900) +// val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900) Timber.d("registerDataChange photoUri bitmap裁剪完成") val file = BitmapSaver.saveToAppFilesDir( - bmp, + bitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg" ) Timber.d("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}") - val resultList = FoodModule.queryFood(bmp) + val resultList = FoodModule.queryFood(bitmap) Timber.d("registerDataChange photoUri 拿到识别数据") val foodName = resultList.joinToString(separator = ",") Timber.d("registerDataChange photoUri 识别数据名称:$foodName") diff --git a/app/src/main/java/com/sw/dualscreen/adapter/FoodCollectionAdapter.kt b/app/src/main/java/com/sw/dualscreen/adapter/FoodCollectionAdapter.kt index affe7c5..306112c 100644 --- a/app/src/main/java/com/sw/dualscreen/adapter/FoodCollectionAdapter.kt +++ b/app/src/main/java/com/sw/dualscreen/adapter/FoodCollectionAdapter.kt @@ -33,7 +33,8 @@ class FoodCollectionAdapter (var list: MutableList) : setImageResource(R.drawable.ic_camera256) } else { scaleType = ImageView.ScaleType.FIT_CENTER - setImageURI(it.imageUri) + //setImageURI(it.imageUri) + setImageBitmap(it.bitmap) } } } diff --git a/app/src/main/java/com/sw/dualscreen/dialog/BaseDialog.kt b/app/src/main/java/com/sw/dualscreen/dialog/BaseDialog.kt index 16b259a..88cbd21 100644 --- a/app/src/main/java/com/sw/dualscreen/dialog/BaseDialog.kt +++ b/app/src/main/java/com/sw/dualscreen/dialog/BaseDialog.kt @@ -13,7 +13,7 @@ import com.sw.dualscreen.ext.hideKeyboard abstract class BaseDialog( context: Context, var defWidth: Int = 400.dp, - var defHeight: Int = 300.dp + var defHeight: Int = 260.dp ) : Dialog(context, R.style.DialogTheme) { override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt b/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt index ab8a526..b6530ff 100644 --- a/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt +++ b/app/src/main/java/com/sw/dualscreen/objbox/FoodModule.kt @@ -1,10 +1,13 @@ package com.sw.dualscreen.objbox +import com.sw.dualscreen.MyApp +import com.sw.dualscreen.utils.AssetsTool import android.content.Context import android.graphics.Bitmap +import android.net.Uri import com.google.gson.Gson import com.google.gson.reflect.TypeToken -import com.sw.dualscreen.utils.AssetsTool +import com.sw.dualscreen.utils.ImageUtil import io.objectbox.Box import io.objectbox.kotlin.boxFor import io.objectbox.query.Query @@ -19,34 +22,64 @@ import java.io.InputStream object FoodModule { - private const val THRESHOLD = 0.8 private lateinit var module_mobile: Module private lateinit var box: Box private lateinit var embeddingsList: List> private lateinit var labelsList: IntArray private lateinit var classInfo: FoodClassInfo - private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f) - private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f) + val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f) + val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f) + val DEFAULT_FOOD_INDEX = -1 fun init(context: Context) { - module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt")) - box = ObjectBox.boxStore.boxFor(Food::class) -// if (box.all.isNotEmpty()) { -// box.removeAll() -// } -// if (box.all.isEmpty()) { -// initFoodData(context) -// } + Thread { + module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt")) + box = ObjectBox.boxStore.boxFor(Food::class) + //if (box.all.isNotEmpty()) { + // box.removeAll() + //} + //if (box.all.isEmpty()) { + // initDefFoodData(context) + //} + }.start() } - fun queryFood(bitmap: Bitmap, queryCount: Int = 15): List { + fun uri2FloatArray(uri: Uri): FloatArray? { + return MyApp.instance?.let { context -> + ImageUtil.uriToBitmap(context, uri)?.let { + bitmap2FloatArray(it) + } + } + } + + fun bitmap2FloatArray(bitmap: Bitmap): FloatArray { val inputTensor = TensorImageUtils.bitmapToFloat32Tensor( bitmap, NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB ) val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor() + return outputTensor.dataAsFloatArray + } + + fun queryFood(uri: Uri, queryCount: Int = 15): List? { + return uri2FloatArray(uri)?.let { + queryFood(it, queryCount) + } + } + + fun queryFood(bitmap: Bitmap, queryCount: Int = 15): List { + val inputTensor = + TensorImageUtils.bitmapToFloat32Tensor( + bitmap, + NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB + NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB + ) + val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor() val floatArray = outputTensor.dataAsFloatArray + if (bitmap.isRecycled.not()) { + bitmap.recycle() + } return queryFood(floatArray, queryCount) } @@ -56,24 +89,35 @@ object FoodModule { //查询比较分数 // val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" } val map = mutableMapOf() - query.findIdsWithScores().forEach { + val idScoreList = query.findIdsWithScores() + val nameScoreList = mutableListOf() + idScoreList.forEach { + nameScoreList.add(IdNameScore(id = it.id, name = box.get(it.id).name?:"", score = it.score)) + + Timber.tag("registerDataChange").d("queryFood数据:${box.get(it.id).name}===score=${it.score}") + } +// Timber.tag("FoodModule").d("queryFood数据:${nameScoreList.toJsonString()}") + idScoreList.filter { it.score < 0.20 }.forEach { val food = box.get(it.id) - Timber.d("${food.name}|${food.foodIdx}|${it.score}") - if (1 - it.score >= THRESHOLD) { - //FoodQueryResult(id = it.id, name = food.name, foodIdx = food.foodIdx, score = it.score) - food.name?.let { key -> - val count = map[key] ?: 0 - map.put(key, count + 1) - } + //FoodQueryResult(id = it.id, name = food.name, foodIdx = food.foodIdx, score = it.score) + food.name?.let { key -> + val count = map[key] ?: 0 + map[key] = count + 1 } } val list = map.entries.sortedByDescending { it.value }.map { it.key } return list } + data class IdNameScore( + val id:Long, + val name:String, + val score: Double + ) - private fun initFoodData(context: Context) { - if (box.all.isNotEmpty()) { + fun initDefFoodData(context: Context, action:()-> Unit={}) { + val count = box.all.count { it.foodIdx == DEFAULT_FOOD_INDEX } + if (count > 0) { return } val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json") @@ -91,8 +135,9 @@ object FoodModule { val classIdx = labelsList[index] val foodName = foodMap["$classIdx"] val array = floatList.toFloatArray() - box.put(Food(name = foodName, foodVector = array, foodIdx = index)) + box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX)) } + action() } diff --git a/app/src/main/java/com/sw/dualscreen/utils/BitmapSaver.kt b/app/src/main/java/com/sw/dualscreen/utils/BitmapSaver.kt index 20ab354..d608c3b 100644 --- a/app/src/main/java/com/sw/dualscreen/utils/BitmapSaver.kt +++ b/app/src/main/java/com/sw/dualscreen/utils/BitmapSaver.kt @@ -29,7 +29,8 @@ object BitmapSaver { format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, quality: Int = 100 ): File? { - val dir = context.getExternalFilesDir(null) + //val dir = context.getExternalFilesDir(null) + val dir = context.cacheDir return saveBitmap(bitmap, File(dir, fileName), format, quality) } diff --git a/app/src/main/java/com/sw/dualscreen/utils/CameraUtils.kt b/app/src/main/java/com/sw/dualscreen/utils/CameraUtils.kt new file mode 100644 index 0000000..fe6deb3 --- /dev/null +++ b/app/src/main/java/com/sw/dualscreen/utils/CameraUtils.kt @@ -0,0 +1,81 @@ +package com.sw.dualscreen.utils + +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.camera.core.CameraSelector +import androidx.camera.view.CameraController +import androidx.camera.view.LifecycleCameraController +import androidx.camera.view.PreviewView + +class CameraUtils(private var activity: ComponentActivity) { + + private var cameraController: LifecycleCameraController? = null + private var photoCaptureHelper: PhotoCaptureHelper? = null + + // private var isCameraReady = false + fun takePhoto(callback: (Uri) -> Unit) { + cameraController?.let { + if (photoCaptureHelper == null) { + initCaptureHelper() + } + } + photoCaptureHelper?.let { + it.addSuccessCallback(callback) + it.bindCameraCallback { + bind() + } + it.takePhoto() + } + } + + private fun initCaptureHelper() { + photoCaptureHelper = PhotoCaptureHelper( + context = activity, + cameraController = cameraController!!, + onSuccess = {}, + onError = { msg -> + //toast(msg) + } + ) + } + + fun setPreviewController(previewView: PreviewView?) { + if (previewView?.controller == null) { + previewView?.controller = cameraController + } + } + + fun initCamera() { + if (cameraController == null) { + cameraController = LifecycleCameraController(activity).apply { + // 必须设置有效的用例 + setEnabledUseCases( + CameraController.IMAGE_CAPTURE + // or CameraController.VIDEO_CAPTURE + ) + cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + } + bind() + } + +// if (isCameraReady.not()) { +// try { +// cameraController!!.initializationFuture.addListener({ +// isCameraReady = true +// Timber.d("Camera initialized successfully") +// }, ContextCompat.getMainExecutor(this)) +// } catch (e: Exception) { +// Timber.d("Camera initialized error = ${e.message}") +// } +// } + } + + fun bind() { + cameraController?.bindToLifecycle(activity) + } + + fun unbind() { + cameraController?.unbind() + } + +} diff --git a/app/src/main/java/com/sw/dualscreen/utils/PhotoCaptureHelper.kt b/app/src/main/java/com/sw/dualscreen/utils/PhotoCaptureHelper.kt new file mode 100644 index 0000000..5544464 --- /dev/null +++ b/app/src/main/java/com/sw/dualscreen/utils/PhotoCaptureHelper.kt @@ -0,0 +1,92 @@ +package com.sw.dualscreen.utils + +import android.content.Context +import android.net.Uri +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.view.CameraController +import androidx.core.content.ContextCompat +import timber.log.Timber +import java.io.File + +/** + * 拍照工具类 + * @param context Context 上下文 + * @param cameraController CameraController 相机控制器 + * @param onSuccess (Uri) -> Unit 拍照成功回调 + * @param onError (String) -> Unit 拍照失败回调 + */ +class PhotoCaptureHelper( + private val context: Context, + private val cameraController: CameraController, + private val onSuccess: (Uri) -> Unit = {}, + private val onError: (String) -> Unit = {} +) { + private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf() + fun addSuccessCallback(callback:(Uri) -> Unit) { + if (callbackList.contains(callback).not()) { + callbackList.add(callback) + } + } + + private var bindCamera:(()->Unit)?=null + fun bindCameraCallback(callback:()->Unit) { + this.bindCamera = callback + } + + /** + * 拍照方法 + * @param fileNamePrefix 文件名前缀,默认为"IMG_" + * @param fileExtension 文件扩展名,默认为".jpg" + */ + fun takePhoto( + fileNamePrefix: String = "IMG_", + fileExtension: String = ".jpg" + ) { + Timber.d("开始拍照采集") + + try { + val executor = ContextCompat.getMainExecutor(context) + val cacheDir = context.cacheDir + val photoFile = File.createTempFile( + "${fileNamePrefix}${System.currentTimeMillis()}", + fileExtension, + cacheDir + ) + + val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() + + cameraController.takePicture( + outputOptions, + executor, + object : ImageCapture.OnImageSavedCallback { + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile) + Timber.d("照片保存成功: $photoUri") + onSuccess(photoUri) + callbackList.forEach { + it(photoUri) + } + } + + override fun onError(exception: ImageCaptureException) { + val errorMsg = "拍照失败: ${exception.message}" + if (errorMsg.contains("Not bound to a valid Camera")) { + if (bindCamera != null) { + bindCamera?.invoke() + //takePhoto() + } + + } + Timber.e(exception, errorMsg) + onError(errorMsg) + } + } + ) + } catch (e: Exception) { + val errorMsg = "创建临时文件失败: ${e.message}" + Timber.e(e, errorMsg) + onError(errorMsg) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/dualscreen/viewmodel/BaseViewModel.kt b/app/src/main/java/com/sw/dualscreen/viewmodel/BaseViewModel.kt index 99593a1..6ea394f 100644 --- a/app/src/main/java/com/sw/dualscreen/viewmodel/BaseViewModel.kt +++ b/app/src/main/java/com/sw/dualscreen/viewmodel/BaseViewModel.kt @@ -65,6 +65,7 @@ abstract class BaseViewModel() : ViewModel() { fun parseEquipmentInfo(equipmentInfo: EquipmentInfo) { GlobalData.appBaseUrl = equipmentInfo.appPackageUrl!! + GlobalData.appBaseUrl="http://192.168.1.210/gateway/local" GlobalData.sdkKey = equipmentInfo.arcsoftSdkKey!! GlobalData.appId = equipmentInfo.arcsoftAppId!! // GlobalData.activeKey = equipmentInfo.arcsoftActiveKey!! diff --git a/app/src/main/res/layout/activity_collected_data.xml b/app/src/main/res/layout/activity_collected_data.xml index dd3cd5c..f324dcf 100644 --- a/app/src/main/res/layout/activity_collected_data.xml +++ b/app/src/main/res/layout/activity_collected_data.xml @@ -7,12 +7,12 @@ android:layout_height="match_parent"> diff --git a/app/src/main/res/layout/activity_food_collection.xml b/app/src/main/res/layout/activity_food_collection.xml index 354079a..18d317e 100644 --- a/app/src/main/res/layout/activity_food_collection.xml +++ b/app/src/main/res/layout/activity_food_collection.xml @@ -8,14 +8,15 @@ + android:layout_marginBottom="24dp"> @@ -79,32 +81,32 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_marginStart="20dp" - android:orientation="vertical" + android:orientation="horizontal" android:gravity="center_vertical">