扫一扫

This commit is contained in:
2026-05-18 14:07:21 +08:00
parent 4c53c43f7b
commit a9fb39cfde
15 changed files with 102 additions and 529 deletions

View File

@@ -91,5 +91,12 @@ dependencies {
implementation("com.geyifeng.immersionbar:immersionbar-ktx:3.2.2")
implementation("com.wdullaer:materialdatetimepicker:4.2.3") // 经典稳定
implementation("androidx.core:core-ktx:1.12.0")
//二维码三方库
implementation("com.github.maning0303:MNMLKitScanner:V1.0.4")
implementation("com.google.mlkit:barcode-scanning:17.0.2")
implementation("androidx.camera:camera-core:1.0.2")
implementation("androidx.camera:camera-camera2:1.0.2")
implementation("androidx.camera:camera-lifecycle:1.0.2")
implementation("androidx.camera:camera-view:1.0.0-alpha25")
}

View File

@@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

View File

@@ -1,13 +1,19 @@
package com.example.defenseapplication.home
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Build
import android.graphics.Rect
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.util.Log
import android.util.TypedValue
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
@@ -30,6 +36,8 @@ import com.example.defenseapplication.utils.UserinfoUtil
import com.example.defenseapplication.view.DefenseDialogs
import com.example.defenseapplication.view.showDefenseDialog
import com.gyf.immersionbar.ImmersionBar
import com.maning.mlkitscanner.scan.MNScanManager
import com.maning.mlkitscanner.scan.callback.act.MNScanCallback
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -45,6 +53,85 @@ class HomeMenuFragment : Fragment() {
private var isFamilyExpanded = false
private lateinit var currentFamily: String
private val requestPermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val cameraGranted = permissions[Manifest.permission.CAMERA] ?: false
val storageGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissions[Manifest.permission.READ_MEDIA_IMAGES] ?: false
} else {
permissions[Manifest.permission.READ_EXTERNAL_STORAGE] ?: false
}
if (cameraGranted && storageGranted) {
startScan()
} else {
Toast.makeText(requireContext(), R.string.permission_denied, Toast.LENGTH_SHORT).show()
}
}
private fun checkPermissionsAndScan() {
val cameraPermission = ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.CAMERA
)
val storagePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.READ_MEDIA_IMAGES
)
} else {
ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
if (cameraPermission == PackageManager.PERMISSION_GRANTED &&
storagePermission == PackageManager.PERMISSION_GRANTED) {
startScan()
} else {
val permissionsToRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.READ_MEDIA_IMAGES
)
} else {
arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
requestPermissionsLauncher.launch(permissionsToRequest)
}
}
private fun startScan() {
MNScanManager.startScan(requireContext() as Activity, object: MNScanCallback {
override fun onActivityResult(resultCode: Int, data: Intent?) {
when (resultCode) {
MNScanManager.RESULT_SUCCESS -> {
val result = data?.getStringArrayListExtra(MNScanManager.INTENT_KEY_RESULT_SUCCESS)
if (!result.isNullOrEmpty()) {
ToastUtils.showToast(requireContext(), "扫码成功: $result")
} else {
ToastUtils.showToast(requireContext(), "扫码结果为空")
}
}
MNScanManager.RESULT_FAIL -> {
val error = data?.getStringExtra(MNScanManager.INTENT_KEY_RESULT_ERROR)
ToastUtils.showToast(requireContext(), "扫码失败: $error")
}
MNScanManager.RESULT_CANCLE -> {
ToastUtils.showToast(requireContext(), "扫码取消")
}
}
}
})
}
private val unreadCountChangeListener = object : MessageStatusManager.UnreadCountChangeListener {
override fun onUnreadCountChanged(count: Int) {
updateUnreadCountUI(count)
@@ -112,8 +199,7 @@ class HomeMenuFragment : Fragment() {
startActivity(intent)
}
binding.addFamily.setOnClickListener {
val intent = Intent(requireContext(), ScanActivity::class.java)
startActivity(intent)
checkPermissionsAndScan()
}
fetchFamilyList()

View File

@@ -1,295 +0,0 @@
package com.example.defenseapplication.home
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.ImageFormat
import android.graphics.Matrix
import android.graphics.RectF
import android.graphics.SurfaceTexture
import android.hardware.Camera
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
import android.view.animation.TranslateAnimation
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.example.defenseapplication.R
import com.example.defenseapplication.databinding.ScanActivityBinding
import com.google.zxing.*
import com.google.zxing.common.HybridBinarizer
import com.gyf.immersionbar.ImmersionBar
import java.util.*
//扫一扫页面
class ScanActivity : AppCompatActivity(), SurfaceHolder.Callback {
private lateinit var binding: ScanActivityBinding
private var camera: Camera? = null
private var isFlashlightOn = false
private var scanHandler: Handler? = null
private var scanRunnable: Runnable? = null
private val REQUEST_CODE_CAMERA = 100
private val REQUEST_CODE_ALBUM = 101
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ScanActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
ImmersionBar.with(this)
.statusBarDarkFont(true)
.titleBar(binding.btnBack)
.init()
initView()
initListener()
startScanLineAnimation()
}
private fun initView() {
binding.surfaceView.holder.addCallback(this)
}
private fun initListener() {
binding.btnBack.setOnClickListener {
finish()
}
binding.llAlbum.setOnClickListener {
openAlbum()
}
binding.llFlashlight.setOnClickListener {
toggleFlashlight()
}
}
private fun startScanLineAnimation() {
val animation = TranslateAnimation(
Animation.RELATIVE_TO_PARENT, 0f,
Animation.RELATIVE_TO_PARENT, 0f,
Animation.RELATIVE_TO_PARENT, 0f,
Animation.RELATIVE_TO_PARENT, 1f
)
animation.duration = 2000
animation.repeatCount = Animation.INFINITE
animation.interpolator = LinearInterpolator()
binding.scanLine.startAnimation(animation)
}
private fun openAlbum() {
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
startActivityForResult(intent, REQUEST_CODE_ALBUM)
}
private fun toggleFlashlight() {
if (camera == null) {
Toast.makeText(this, "相机未准备好", Toast.LENGTH_SHORT).show()
return
}
isFlashlightOn = !isFlashlightOn
val params = camera?.parameters
if (isFlashlightOn) {
params?.flashMode = Camera.Parameters.FLASH_MODE_TORCH
binding.ivFlashlight.setColorFilter(ContextCompat.getColor(this, R.color.green))
} else {
params?.flashMode = Camera.Parameters.FLASH_MODE_OFF
binding.ivFlashlight.clearColorFilter()
}
camera?.parameters = params
}
private fun checkCameraPermission(): Boolean {
return ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
}
private fun requestCameraPermission() {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CAMERA),
REQUEST_CODE_CAMERA
)
}
private fun openCamera() {
if (!checkCameraPermission()) {
requestCameraPermission()
return
}
try {
camera = Camera.open()
camera?.setDisplayOrientation(90)
val params = camera?.parameters
params?.previewFormat = ImageFormat.NV21
val sizes = params?.supportedPreviewSizes
val optimalSize = getOptimalPreviewSize(sizes, binding.surfaceView.width, binding.surfaceView.height)
if (optimalSize != null) {
params?.setPreviewSize(optimalSize.width, optimalSize.height)
}
camera?.parameters = params
camera?.setPreviewDisplay(binding.surfaceView.holder)
camera?.startPreview()
startScanDecode()
} catch (e: Exception) {
Toast.makeText(this, "无法打开相机", Toast.LENGTH_SHORT).show()
}
}
private fun getOptimalPreviewSize(sizes: List<Camera.Size>?, w: Int, h: Int): Camera.Size? {
if (sizes == null) return null
var optimalSize: Camera.Size? = null
val targetRatio = h.toDouble() / w.toDouble()
for (size in sizes) {
val ratio = size.width.toDouble() / size.height.toDouble()
if (Math.abs(ratio - targetRatio) < 0.01) {
optimalSize = size
break
}
}
if (optimalSize == null) {
var minDiff = Int.MAX_VALUE
for (size in sizes) {
val diff = Math.abs(size.width - w) + Math.abs(size.height - h)
if (diff < minDiff) {
optimalSize = size
minDiff = diff
}
}
}
return optimalSize
}
private fun startScanDecode() {
scanHandler = Handler(Looper.getMainLooper())
scanRunnable = object : Runnable {
override fun run() {
decodeQRCode()
scanHandler?.postDelayed(this, 100)
}
}
scanHandler?.post(scanRunnable!!)
}
private fun decodeQRCode() {
if (camera == null) return
try {
val parameters = camera?.parameters
val size = parameters?.previewSize
if (size == null) return
val data = ByteArray(size.width * size.height * 3 / 2)
camera?.setOneShotPreviewCallback { bytes, _ ->
val source = PlanarYUVLuminanceSource(
bytes, size.width, size.height, 0, 0,
size.width, size.height, false
)
val binarizer = HybridBinarizer(source)
val bitmap = BinaryBitmap(binarizer)
val reader = MultiFormatReader()
val hints = Hashtable<DecodeHintType, Any>()
hints[DecodeHintType.CHARACTER_SET] = "UTF-8"
reader.setHints(hints)
try {
val result = reader.decode(bitmap)
handleScanResult(result.text)
} catch (e: Exception) {
}
}
} catch (e: Exception) {
}
}
private fun handleScanResult(result: String) {
stopScanDecode()
camera?.stopPreview()
Toast.makeText(this, "扫描结果: $result", Toast.LENGTH_SHORT).show()
val intent = Intent(this, AddFamilyActivity::class.java)
intent.putExtra("device_code", result)
startActivity(intent)
finish()
}
private fun stopScanDecode() {
scanRunnable?.let { scanHandler?.removeCallbacks(it) }
scanHandler = null
scanRunnable = null
}
override fun surfaceCreated(holder: SurfaceHolder) {
openCamera()
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
if (camera != null) {
camera?.stopPreview()
try {
camera?.setPreviewDisplay(holder)
camera?.startPreview()
} catch (e: Exception) {
}
}
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
stopScanDecode()
camera?.stopPreview()
camera?.release()
camera = null
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_CAMERA) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
openCamera()
} else {
Toast.makeText(this, "需要相机权限", Toast.LENGTH_SHORT).show()
finish()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_ALBUM && resultCode == RESULT_OK && data != null) {
val uri = data.data
uri?.let {
Toast.makeText(this, "已选择图片", Toast.LENGTH_SHORT).show()
}
}
}
override fun onDestroy() {
super.onDestroy()
stopScanDecode()
camera?.stopPreview()
camera?.release()
camera = null
}
}

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<stroke
android:width="1dp"
android:color="@color/green" />
</shape>

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<stroke
android:width="3dp"
android:color="@color/green"
android:bottom="3dp"
android:left="3dp"
android:right="0dp"
android:top="0dp" />
</shape>

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<stroke
android:width="3dp"
android:color="@color/green"
android:bottom="3dp"
android:left="0dp"
android:right="3dp"
android:top="0dp" />
</shape>

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<stroke
android:width="3dp"
android:color="@color/green"
android:bottom="0dp"
android:left="3dp"
android:right="0dp"
android:top="3dp" />
</shape>

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<stroke
android:width="3dp"
android:color="@color/green"
android:bottom="0dp"
android:left="0dp"
android:right="3dp"
android:top="3dp" />
</shape>

View File

@@ -1,176 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#B2000000" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#B2000000" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="250dp"
android:layout_weight="1"
android:background="#B2000000" />
<FrameLayout
android:layout_width="250dp"
android:layout_height="250dp">
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_scan_frame" />
<View
android:id="@+id/scanLine"
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/green" />
<View
android:layout_width="16dp"
android:layout_height="16dp"
android:background="@drawable/corner_top_left"
android:layout_gravity="top|left" />
<View
android:layout_width="16dp"
android:layout_height="16dp"
android:background="@drawable/corner_top_right"
android:layout_gravity="top|right" />
<View
android:layout_width="16dp"
android:layout_height="16dp"
android:background="@drawable/corner_bottom_left"
android:layout_gravity="bottom|left" />
<View
android:layout_width="16dp"
android:layout_height="16dp"
android:background="@drawable/corner_bottom_right"
android:layout_gravity="bottom|right" />
</FrameLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="250dp"
android:layout_weight="1"
android:background="#B2000000" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#B2000000" />
</LinearLayout>
<Button
android:id="@+id/btnBack"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="40dp"
android:layout_marginLeft="16dp"
android:background="@drawable/back2"
android:contentDescription="返回" />
<TextView
android:id="@+id/tvScanTip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="550dp"
android:text="@string/scan_device_qr"
android:textColor="@color/white"
android:textSize="14sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="80dp"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/llAlbum"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginRight="80dp"
android:background="@drawable/bg_button_circle"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/ivAlbum"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/album" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/album"
android:textColor="@color/white"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/llFlashlight"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginLeft="80dp"
android:background="@drawable/bg_button_circle"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/ivFlashlight"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/flashlight" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/flashlight"
android:textColor="@color/white"
android:textSize="12dp" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>

View File

@@ -53,11 +53,10 @@
android:layout_margin="16dp"
android:padding="16dp">
<!-- 虚线边框 -->
<!-- 虚线边框 android:background="@drawable/bg_scan_frame"-->
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_scan_frame" />
android:layout_height="match_parent" />
<!-- 人体模型图片 -->
<ImageView

View File

@@ -353,6 +353,7 @@
<string name="camera_not_ready">相机未准备好</string>
<string name="cannot_open_camera">无法打开相机</string>
<string name="camera_permission_needed">需要相机权限</string>
<string name="permission_denied">需要获取相机和存储权限</string>
<string name="image_selected">已选择图片</string>
<string name="please_try_later">请稍后再试</string>
<string name="phone_format_invalid">手机号格式不正确</string>

View File

@@ -339,6 +339,7 @@
<string name="camera_not_ready">Camera not ready</string>
<string name="cannot_open_camera">Cannot open camera</string>
<string name="camera_permission_needed">Camera permission is required</string>
<string name="permission_denied">Camera and storage permissions are required</string>
<string name="image_selected">Image selected</string>
<string name="please_try_later">Please try again later</string>
<string name="phone_format_invalid">Invalid phone number format</string>