501 lines
19 KiB
Kotlin
501 lines
19 KiB
Kotlin
package com.assimilate.alltrans.viewui
|
|
|
|
|
|
import android.Manifest
|
|
import android.app.Activity
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.pm.PackageManager
|
|
import android.content.res.Configuration
|
|
import android.graphics.Bitmap
|
|
import android.net.Uri
|
|
import android.os.Bundle
|
|
import android.util.Log
|
|
import android.util.Pair
|
|
import android.view.View
|
|
import android.view.ViewTreeObserver
|
|
import android.view.inputmethod.InputMethodManager
|
|
import android.widget.Toast
|
|
import androidx.activity.enableEdgeToEdge
|
|
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 androidx.core.view.ViewCompat
|
|
import androidx.core.view.WindowInsetsCompat
|
|
import androidx.recyclerview.widget.LinearLayoutManager
|
|
import androidx.recyclerview.widget.RecyclerView
|
|
import com.assimilate.alltrans.R
|
|
import com.assimilate.alltrans.adapters.LanguageAdapter
|
|
import com.assimilate.alltrans.common.BitmapUtils
|
|
import com.assimilate.alltrans.keepmodel.Language
|
|
import com.assimilate.alltrans.keepmodel.LanguagesConstants
|
|
import com.assimilate.alltrans.keepmodel.PreferenceLanguageUtils
|
|
import com.assimilate.alltrans.common.TextRecognitionProcessor
|
|
import com.assimilate.alltrans.common.VisionImageProcessor
|
|
import com.assimilate.alltrans.common.Widget
|
|
import com.assimilate.alltrans.databinding.ActivityStillImageBinding
|
|
import com.google.android.gms.common.annotation.KeepName
|
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
import com.google.common.util.concurrent.ListenableFuture
|
|
import com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions
|
|
import com.google.mlkit.vision.text.devanagari.DevanagariTextRecognizerOptions
|
|
import com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions
|
|
import com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions
|
|
import com.google.mlkit.vision.text.latin.TextRecognizerOptions
|
|
import java.io.File
|
|
import java.io.IOException
|
|
import java.text.SimpleDateFormat
|
|
import java.util.Locale
|
|
import kotlin.math.max
|
|
import kotlin.math.min
|
|
|
|
@KeepName
|
|
class PhotoImageActivity : AppCompatActivity() {
|
|
|
|
private var selectedMode = TEXT_RECOGNITION_CHINESE
|
|
private var selectedSize: String? = SIZE_SCREEN
|
|
private var isLandScape = false
|
|
private var imageUri: Uri? = null
|
|
private var imageMaxWidth = 0
|
|
private var imageMaxHeight = 0
|
|
private var imageProcessor: VisionImageProcessor? = null
|
|
private lateinit var imageCapture: ImageCapture
|
|
private lateinit var outputDirectory: File
|
|
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
|
|
private var isFlashOn = false
|
|
|
|
private val REQUEST_CAMERA_PERMISSION = 100
|
|
private lateinit var bottomSheetDialog: BottomSheetDialog
|
|
private var chooseLanguage: Boolean = false
|
|
private lateinit var binding: ActivityStillImageBinding
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
enableEdgeToEdge()
|
|
binding = ActivityStillImageBinding.inflate(layoutInflater)
|
|
setContentView(binding.root)
|
|
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.root)) { v, insets ->
|
|
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
v.setPadding(0, systemBars.top + 26, 0, systemBars.bottom)
|
|
insets
|
|
}
|
|
|
|
Widget.makeSnackbar(this, "Photographing text for translation")
|
|
|
|
isLandScape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
|
savedInstanceState?.let {
|
|
imageUri = it.getParcelable(KEY_IMAGE_URI)
|
|
imageMaxWidth = it.getInt(KEY_IMAGE_MAX_WIDTH)
|
|
imageMaxHeight = it.getInt(KEY_IMAGE_MAX_HEIGHT)
|
|
selectedSize = it.getString(KEY_SELECTED_SIZE)
|
|
}
|
|
|
|
val rootView = findViewById<View>(R.id.root)
|
|
rootView.viewTreeObserver.addOnGlobalLayoutListener(object :
|
|
ViewTreeObserver.OnGlobalLayoutListener {
|
|
override fun onGlobalLayout() {
|
|
rootView.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
|
imageMaxWidth = rootView.width
|
|
imageMaxHeight = rootView.height - findViewById<View>(R.id.control).height
|
|
if (SIZE_SCREEN == selectedSize) {
|
|
tryReloadAndDetectInImage()
|
|
}
|
|
}
|
|
})
|
|
|
|
outputDirectory = getOutputDirectory()
|
|
// 检查并启动相机
|
|
checkCameraPermission()
|
|
initView()
|
|
initClick()
|
|
}
|
|
|
|
private fun checkCameraPermission() {
|
|
if (ContextCompat.checkSelfPermission(
|
|
this, Manifest.permission.CAMERA
|
|
) != PackageManager.PERMISSION_GRANTED
|
|
) {
|
|
ActivityCompat.requestPermissions(
|
|
this,
|
|
arrayOf(Manifest.permission.CAMERA),
|
|
REQUEST_CAMERA_PERMISSION
|
|
)
|
|
} else {
|
|
startCamera()
|
|
}
|
|
}
|
|
|
|
private fun initView() {
|
|
|
|
binding.photoPreview.visibility = View.VISIBLE
|
|
|
|
binding.stillSourceLanguage.text =
|
|
PreferenceLanguageUtils.getString("language_source")
|
|
binding.stillTargetLanguage.text =
|
|
PreferenceLanguageUtils.getString("language_target")
|
|
|
|
bottomSheetDialog = BottomSheetDialog(this, R.style.CustomBottomSheetDialogTheme)
|
|
bottomSheetDialog.setContentView(R.layout.bottomsheet_still_lan)
|
|
bottomSheetDialog.dismissWithAnimation = true
|
|
|
|
bottomSheetDialog.findViewById<androidx.appcompat.widget.SearchView>(R.id.ph_search)
|
|
?.let { searchView ->
|
|
// 设置关闭按钮一直显示
|
|
searchView.isIconified = false
|
|
searchView.clearFocus()
|
|
|
|
// 设置点击事件
|
|
searchView.setOnClickListener {
|
|
// 清空搜索框
|
|
searchView.setQuery("", false)
|
|
// 清除焦点
|
|
searchView.clearFocus()
|
|
|
|
// 隐藏软件盘
|
|
val imm =
|
|
this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
|
imm.hideSoftInputFromWindow(searchView.windowToken, 0)
|
|
|
|
// 保持关闭按钮显示
|
|
searchView.isIconified = false
|
|
}
|
|
|
|
// 设置查询文本监听器
|
|
searchView.setOnQueryTextListener(object :
|
|
androidx.appcompat.widget.SearchView.OnQueryTextListener {
|
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
return false
|
|
}
|
|
|
|
override fun onQueryTextChange(newText: String?): Boolean {
|
|
(bottomSheetDialog.findViewById<RecyclerView>(R.id.list_languages)?.adapter as LanguageAdapter).filter.filter(
|
|
newText
|
|
)
|
|
return true
|
|
}
|
|
})
|
|
|
|
// 设置关闭按钮点击事件
|
|
searchView.findViewById<View>(androidx.appcompat.R.id.search_close_btn)
|
|
?.setOnClickListener {
|
|
searchView.setQuery("", false)
|
|
searchView.clearFocus()
|
|
searchView.isIconified = false // 保持关闭按钮显示
|
|
}
|
|
}
|
|
|
|
initList()
|
|
}
|
|
|
|
private fun initList() {
|
|
val languages: ArrayList<Language> = LanguagesConstants.getInstance().getList(this)
|
|
if (languages.isNotEmpty()) {
|
|
val adapter = LanguageAdapter(this, languages) { _, language ->
|
|
if (!chooseLanguage) {
|
|
|
|
PreferenceLanguageUtils.putString("language_source", language.language)
|
|
selectedMode = PreferenceLanguageUtils.getString("language_source")
|
|
tryReloadAndDetectInImage()
|
|
bottomSheetDialog.dismiss()
|
|
} else {
|
|
|
|
PreferenceLanguageUtils.putString("language_target", language.language)
|
|
tryReloadAndDetectInImage()
|
|
bottomSheetDialog.dismiss()
|
|
|
|
}
|
|
|
|
binding.stillSourceLanguage.text =
|
|
PreferenceLanguageUtils.getString("language_source")
|
|
binding.stillTargetLanguage.text =
|
|
PreferenceLanguageUtils.getString("language_target")
|
|
}
|
|
bottomSheetDialog.findViewById<RecyclerView>(R.id.list_languages)?.layoutManager =
|
|
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
|
bottomSheetDialog.findViewById<RecyclerView>(R.id.list_languages)?.adapter = adapter
|
|
}
|
|
}
|
|
|
|
|
|
private fun initClick() {
|
|
binding.ivStillPic.setOnClickListener {
|
|
binding.photoPreview.visibility = View.INVISIBLE
|
|
startChooseImageIntentForResult()
|
|
}
|
|
binding.ivStillTake.setOnClickListener {
|
|
binding.photoPreview.visibility = View.VISIBLE
|
|
|
|
takePhoto()
|
|
}
|
|
binding.ivStillBack.setOnClickListener { onBackPressed() }
|
|
binding.ivStillBuling.setOnClickListener {
|
|
toggleFlash()
|
|
updateFlashButtonUI()
|
|
}
|
|
binding.stillSourceLanguage.setOnClickListener {
|
|
chooseLanguage = false
|
|
|
|
bottomSheetDialog.show()
|
|
}
|
|
binding.stillTargetLanguage.setOnClickListener {
|
|
chooseLanguage = true
|
|
bottomSheetDialog.show()
|
|
}
|
|
binding.stillExChange.setOnClickListener {
|
|
|
|
val currentSourceLanguage = PreferenceLanguageUtils.getString("language_source")
|
|
val currentTargetLanguage = PreferenceLanguageUtils.getString("language_target")
|
|
// 交换源语言和目标语言
|
|
PreferenceLanguageUtils.putString("language_source", currentTargetLanguage)
|
|
PreferenceLanguageUtils.putString("language_target", currentSourceLanguage)
|
|
// 更新界面显示
|
|
binding.stillSourceLanguage.text = currentTargetLanguage
|
|
binding.stillTargetLanguage.text = currentSourceLanguage
|
|
}
|
|
}
|
|
|
|
private fun toggleFlash() {
|
|
imageCapture.flashMode =
|
|
if (isFlashOn) ImageCapture.FLASH_MODE_OFF else ImageCapture.FLASH_MODE_ON
|
|
isFlashOn = !isFlashOn
|
|
}
|
|
|
|
private fun updateFlashButtonUI() {
|
|
binding.ivStillBuling.setImageResource(if (isFlashOn) R.drawable.ic_still_bulibuli else R.drawable.ic_still_notbuli)
|
|
}
|
|
|
|
private fun startCamera() {
|
|
val previewView = findViewById<PreviewView>(R.id.photo_preview)
|
|
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
|
|
|
|
cameraProviderFuture.addListener(Runnable {
|
|
val cameraProvider = cameraProviderFuture.get()
|
|
val preview = Preview.Builder().build().also {
|
|
it.setSurfaceProvider(previewView.surfaceProvider)
|
|
}
|
|
|
|
imageCapture = ImageCapture.Builder().setFlashMode(ImageCapture.FLASH_MODE_OFF).build()
|
|
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
|
|
|
try {
|
|
cameraProvider.unbindAll()
|
|
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
|
|
} catch (exc: Exception) {
|
|
Log.e(TAG, "Use case binding failed", exc)
|
|
}
|
|
}, ContextCompat.getMainExecutor(this))
|
|
}
|
|
|
|
private fun takePhoto() {
|
|
val photoFile = File(
|
|
outputDirectory,
|
|
SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis()) + ".jpg"
|
|
)
|
|
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
|
|
|
|
imageCapture.takePicture(
|
|
outputOptions,
|
|
ContextCompat.getMainExecutor(this),
|
|
object : ImageCapture.OnImageSavedCallback {
|
|
override fun onError(exc: ImageCaptureException) {
|
|
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
|
|
}
|
|
|
|
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
|
|
imageUri = Uri.fromFile(photoFile)
|
|
Log.d(TAG, "Photo capture succeeded: $imageUri")
|
|
tryReloadAndDetectInImage()
|
|
}
|
|
})
|
|
}
|
|
|
|
private fun getOutputDirectory(): File {
|
|
val mediaDir = externalMediaDirs.firstOrNull()?.let {
|
|
File(it, resources.getString(R.string.app_name)).apply { mkdirs() }
|
|
}
|
|
return if (mediaDir != null && mediaDir.exists()) mediaDir else filesDir
|
|
}
|
|
|
|
override fun onRequestPermissionsResult(
|
|
requestCode: Int,
|
|
permissions: Array<String>,
|
|
grantResults: IntArray
|
|
) {
|
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
when (requestCode) {
|
|
REQUEST_CAMERA_PERMISSION -> {
|
|
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
|
|
startCamera()
|
|
} else {
|
|
Toast.makeText(
|
|
this,
|
|
"Camera permission is required to use the camera",
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
Log.d(TAG, "onResume")
|
|
|
|
|
|
createImageProcessor()
|
|
tryReloadAndDetectInImage()
|
|
|
|
|
|
}
|
|
|
|
override fun onPause() {
|
|
super.onPause()
|
|
imageProcessor?.stop()
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
super.onDestroy()
|
|
imageProcessor?.stop()
|
|
if (::cameraProviderFuture.isInitialized) {
|
|
cameraProviderFuture.get().unbindAll()
|
|
}
|
|
}
|
|
|
|
override fun onSaveInstanceState(outState: Bundle) {
|
|
super.onSaveInstanceState(outState)
|
|
outState.putParcelable(KEY_IMAGE_URI, imageUri)
|
|
outState.putInt(KEY_IMAGE_MAX_WIDTH, imageMaxWidth)
|
|
outState.putInt(KEY_IMAGE_MAX_HEIGHT, imageMaxHeight)
|
|
outState.putString(KEY_SELECTED_SIZE, selectedSize)
|
|
}
|
|
|
|
private fun createImageProcessor() {
|
|
imageProcessor?.stop()
|
|
try {
|
|
imageProcessor = when (selectedMode) {
|
|
TEXT_RECOGNITION_CHINESE -> TextRecognitionProcessor(
|
|
this,
|
|
ChineseTextRecognizerOptions.Builder().build(), true, true
|
|
)
|
|
|
|
"Hindi", "Marathi", "Nepali", "Sanskrit" -> TextRecognitionProcessor(
|
|
this,
|
|
DevanagariTextRecognizerOptions.Builder().build(), true, true
|
|
)
|
|
|
|
TEXT_RECOGNITION_JAPANESE -> TextRecognitionProcessor(
|
|
this,
|
|
JapaneseTextRecognizerOptions.Builder().build(), true, true
|
|
)
|
|
|
|
TEXT_RECOGNITION_KOREAN -> TextRecognitionProcessor(
|
|
this,
|
|
KoreanTextRecognizerOptions.Builder().build(), true, true
|
|
)
|
|
|
|
else -> TextRecognitionProcessor(
|
|
this,
|
|
TextRecognizerOptions.Builder().build(), true, true
|
|
)
|
|
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Can not create image processor: $selectedMode", e)
|
|
Toast.makeText(
|
|
applicationContext,
|
|
"Can not create image processor: " + e.message,
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
}
|
|
}
|
|
|
|
private fun tryReloadAndDetectInImage() {
|
|
try {
|
|
if (imageUri == null) return
|
|
if (SIZE_SCREEN == selectedSize && imageMaxWidth == 0) return
|
|
|
|
val imageBitmap =
|
|
BitmapUtils.getBitmapFromContentUri(contentResolver, imageUri) ?: return
|
|
|
|
if (selectedSize == SIZE_SCREEN) {
|
|
val targetedSize = targetedWidthHeight
|
|
val scaleFactor = max(
|
|
imageBitmap.width.toFloat() / targetedSize.first.toFloat(),
|
|
imageBitmap.height.toFloat() / targetedSize.second.toFloat()
|
|
)
|
|
val resizedBitmap = Bitmap.createScaledBitmap(
|
|
imageBitmap,
|
|
(imageBitmap.width / scaleFactor).toInt(),
|
|
(imageBitmap.height / scaleFactor).toInt(),
|
|
true
|
|
)
|
|
binding.preview.setImageBitmap(resizedBitmap)
|
|
processImage(resizedBitmap)
|
|
} else {
|
|
binding.preview.setImageBitmap(imageBitmap)
|
|
processImage(imageBitmap)
|
|
}
|
|
} catch (e: IOException) {
|
|
Log.e(TAG, "Error retrieving saved image")
|
|
imageUri = null
|
|
}
|
|
}
|
|
|
|
private val targetedWidthHeight: Pair<Int, Int>
|
|
get() {
|
|
val targetWidth = if (isLandScape) max(imageMaxWidth, imageMaxHeight) else min(
|
|
imageMaxWidth,
|
|
imageMaxHeight
|
|
)
|
|
val targetHeight = if (isLandScape) min(imageMaxWidth, imageMaxHeight) else max(
|
|
imageMaxWidth,
|
|
imageMaxHeight
|
|
)
|
|
return Pair(targetWidth, targetHeight)
|
|
}
|
|
|
|
private fun processImage(bitmap: Bitmap) {
|
|
binding.graphicOverlay.clear()
|
|
imageProcessor?.processBitmap(bitmap, binding.graphicOverlay)
|
|
}
|
|
|
|
private fun startChooseImageIntentForResult() {
|
|
val intent = Intent().apply {
|
|
type = "image/*"
|
|
action = Intent.ACTION_GET_CONTENT
|
|
}
|
|
startActivityForResult(Intent.createChooser(intent, "Select Picture"), REQUEST_CHOOSE_IMAGE)
|
|
}
|
|
|
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
super.onActivityResult(requestCode, resultCode, data)
|
|
if (requestCode == REQUEST_CHOOSE_IMAGE && resultCode == Activity.RESULT_OK && data != null && data.data != null) {
|
|
imageUri = data.data
|
|
tryReloadAndDetectInImage()
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private const val TAG = "StillImageActivity"
|
|
private const val KEY_IMAGE_URI = "com.google.mlkit.demo.stillImage.KEY_IMAGE_URI"
|
|
private const val KEY_IMAGE_MAX_WIDTH =
|
|
"com.google.mlkit.demo.stillImage.KEY_IMAGE_MAX_WIDTH"
|
|
private const val KEY_IMAGE_MAX_HEIGHT =
|
|
"com.google.mlkit.demo.stillImage.KEY_IMAGE_MAX_HEIGHT"
|
|
private const val KEY_SELECTED_SIZE = "com.google.mlkit.demo.stillImage.KEY_SELECTED_SIZE"
|
|
private const val SIZE_SCREEN = "w:screen"
|
|
private const val TEXT_RECOGNITION_CHINESE = "Chinese, Simplified"
|
|
private const val TEXT_RECOGNITION_JAPANESE = "Japanese"
|
|
private const val TEXT_RECOGNITION_KOREAN = "Korean"
|
|
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
|
|
private const val REQUEST_CHOOSE_IMAGE = 1001
|
|
}
|
|
}
|
|
|