1. 优化HomePage加载逻辑,修复加载弹窗重复显示问题 2. 重构蓝牙分享页、重命名页的返回事件绑定逻辑 3. 优化iOS/Android扫码对焦功能,支持多后置摄像头、软触发对焦 4. 调整扫码预览布局与参数,提升扫码识别率与适配性 5. 删除冗余的Android状态栏管理脚本与无用文件 6. 新增返回回调调试日志
1126 lines
43 KiB
C#
1126 lines
43 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Runtime.InteropServices;
|
||
using Kill.UI.Components;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
|
||
namespace Kill.UI.Pages
|
||
{
|
||
/// <summary>
|
||
/// 二维码扫描器 - 调用摄像头扫描二维码
|
||
/// </summary>
|
||
public class ScanQRcode : MonoBehaviour
|
||
{
|
||
[Serializable]
|
||
public enum ScanType
|
||
{
|
||
ConnectDevice, // 连接设备
|
||
Other // 其他用途
|
||
}
|
||
[Header("UI组件")]
|
||
public RawImage cameraPreview; // 摄像头预览
|
||
[Header("扫描设置")]
|
||
float scanInterval = 0.05f; // 扫描间隔(秒),密集扫描提升远距离识别率
|
||
public ScanType scanType = ScanType.ConnectDevice; // 扫描类型
|
||
|
||
[Header("性能优化")]
|
||
[Tooltip("扫描处理线程数")]
|
||
bool useAsyncScan = true;
|
||
|
||
[Header("中心裁切放大")]
|
||
[Tooltip("截取画面中心区域的比例(0-1),值越小放越大。\n推荐:近距离 0.55~0.65;远距离扫码 0.8~0.9")]
|
||
[Range(0.3f, 1.0f)]
|
||
float centerCropRatio = 1f;
|
||
|
||
#if UNITY_IOS && !UNITY_EDITOR
|
||
[DllImport("__Internal")]
|
||
private static extern void _EnableContinuousAutoFocus();
|
||
|
||
[DllImport("__Internal")]
|
||
private static extern void _TriggerAutoFocus(float normalizedX, float normalizedY);
|
||
|
||
[DllImport("__Internal")]
|
||
private static extern void _TriggerContinuousRefocus();
|
||
#endif
|
||
|
||
#if UNITY_ANDROID && !UNITY_EDITOR
|
||
// 通过反射拿到的 WebCamTexture 内部 Camera 实例(Unity 2022 LTS 一般拿不到,详见 EnableAndroidContinuousAutoFocus)
|
||
private AndroidJavaObject androidCameraInstance;
|
||
// 标记是否成功接管对焦控制
|
||
private bool androidFocusControlAvailable = false;
|
||
#endif
|
||
|
||
// 摄像头相关
|
||
private WebCamTexture webCamTexture;
|
||
private WebCamDevice currentDevice;
|
||
private bool isScanning = false;
|
||
private bool isProcessing = false;
|
||
|
||
// 扫描结果回调
|
||
public event Action<string> OnQRCodeScanned;
|
||
|
||
void OnEnable()
|
||
{
|
||
StartScan();
|
||
}
|
||
|
||
void OnDisable()
|
||
{
|
||
StopScan();
|
||
}
|
||
|
||
// 手动重新对焦防抖时间:0.6s 内的重复点击只触发一次,避免过于频繁触发对焦
|
||
private float lastManualRefocusTime = -10f;
|
||
private const float ManualRefocusCooldown = 0.6f;
|
||
|
||
/// <summary>
|
||
/// 用户手动点击预览时调用:触发一次软重新对焦(无黑屏,体验好)。
|
||
/// 已接管 Camera 的设备走点对焦;未接管(Unity 2022 LTS 默认情况)走 autoFocusPoint 重新设置。
|
||
/// </summary>
|
||
public void TriggerRefocus()
|
||
{
|
||
if (!isScanning) return;
|
||
if (Time.unscaledTime - lastManualRefocusTime < ManualRefocusCooldown) return;
|
||
|
||
lastManualRefocusTime = Time.unscaledTime;
|
||
|
||
#if UNITY_IOS && !UNITY_EDITOR
|
||
// iOS:调用原生插件触发一次点对焦(1.5s 后原生自动恢复连续对焦,无黑屏)
|
||
_TriggerContinuousRefocus();
|
||
#elif UNITY_ANDROID && !UNITY_EDITOR
|
||
if (androidFocusControlAvailable && androidCameraInstance != null)
|
||
{
|
||
// 已接管:走正常的点对焦
|
||
TriggerAndroidAutoFocus(0.5f, 0.5f);
|
||
}
|
||
else
|
||
{
|
||
// 未接管:走软触发(无黑屏)
|
||
Debug.Log("[ScanQRcode] 用户点击预览,触发软重新对焦");
|
||
SoftTriggerAndroidFocus();
|
||
}
|
||
#else
|
||
// 编辑器/其他平台:无操作
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 开始扫描
|
||
/// </summary>
|
||
public void StartScan()
|
||
{
|
||
if (isScanning) return;
|
||
|
||
StartCoroutine(InitializeCamera());
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止扫描
|
||
/// </summary>
|
||
public void StopScan()
|
||
{
|
||
isScanning = false;
|
||
StopAllCoroutines();
|
||
|
||
if (webCamTexture != null && webCamTexture.isPlaying)
|
||
{
|
||
webCamTexture.Stop();
|
||
webCamTexture = null;
|
||
}
|
||
|
||
// 清理预览纹理
|
||
if (previewTexture != null)
|
||
{
|
||
Destroy(previewTexture);
|
||
previewTexture = null;
|
||
}
|
||
|
||
#if UNITY_ANDROID && !UNITY_EDITOR
|
||
// 反射拿到的 Camera 实例由 WebCamTexture 内部管理,仅清空引用即可
|
||
if (androidCameraInstance != null)
|
||
{
|
||
androidCameraInstance.Dispose();
|
||
androidCameraInstance = null;
|
||
}
|
||
androidFocusControlAvailable = false;
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭扫码界面
|
||
/// </summary>
|
||
public void Close()
|
||
{
|
||
StopScan();
|
||
gameObject.SetActive(false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化摄像头
|
||
/// </summary>
|
||
private IEnumerator InitializeCamera()
|
||
{
|
||
// Android 使用 Permission API 请求权限
|
||
#if UNITY_ANDROID && !UNITY_EDITOR
|
||
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera))
|
||
{
|
||
UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.Camera);
|
||
// 等待权限请求结果
|
||
yield return new WaitUntil(() => UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera));
|
||
}
|
||
|
||
if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.Camera))
|
||
{
|
||
UpdateStatus("100085");
|
||
yield break;
|
||
}
|
||
#else
|
||
// 其他平台使用传统方式
|
||
yield return Application.RequestUserAuthorization(UserAuthorization.WebCam);
|
||
|
||
if (!Application.HasUserAuthorization(UserAuthorization.WebCam))
|
||
{
|
||
UpdateStatus("100085");
|
||
yield break;
|
||
}
|
||
#endif
|
||
|
||
// 获取后置摄像头
|
||
WebCamDevice[] devices = WebCamTexture.devices;
|
||
if (devices.Length == 0)
|
||
{
|
||
UpdateStatus("100086");
|
||
yield break;
|
||
}
|
||
|
||
// 优先使用后置摄像头,并选择支持自动对焦的摄像头
|
||
string deviceName = devices[0].name;
|
||
for (int i = 0; i < devices.Length; i++)
|
||
{
|
||
if (!devices[i].isFrontFacing)
|
||
{
|
||
deviceName = devices[i].name;
|
||
currentDevice = devices[i];
|
||
break;
|
||
}
|
||
}
|
||
RectTransform cameraPreviewRect=cameraPreview.GetComponent<RectTransform>();
|
||
// 尝试更高分辨率(4K);WebCamTexture 在不支持时会自动回退到设备最大值
|
||
// 注意:高分辨率下解码 CPU 压力大,需要配合性能较好的设备
|
||
webCamTexture = new WebCamTexture(deviceName, 3840, 2160, 30);
|
||
|
||
// 开始播放
|
||
webCamTexture.Play();
|
||
|
||
// 等待摄像头启动
|
||
yield return new WaitUntil(() => webCamTexture.width > 100);
|
||
|
||
// 输出实际分辨率,方便确认设备用的最大值
|
||
Debug.Log($"[ScanQRcode] 摄像头实际分辨率: {webCamTexture.width}x{webCamTexture.height}");
|
||
|
||
// 等 0.3s 再触发对焦,避免系统未准备好
|
||
yield return new WaitForSeconds(0.3f);
|
||
|
||
#if UNITY_IOS && !UNITY_EDITOR
|
||
// iOS: 通过原生插件开启连续自动对焦(autoFocusPoint 在 iOS 无效)
|
||
_EnableContinuousAutoFocus();
|
||
// 周期维护对焦:每 3s 触发一次点对焦(无黑屏),让镜头持续对焦
|
||
StartCoroutine(IOsFocusMaintainCoroutine());
|
||
#elif UNITY_ANDROID && !UNITY_EDITOR
|
||
// Android:WebCamTexture 默认不会自动对焦,需主动通过 Camera API 触发
|
||
EnableAndroidContinuousAutoFocus();
|
||
// 仅当我们拿到 Camera 实例时才启动对焦维护协程(Unity 2022 LTS Camera2 后端一般拿不到)
|
||
if (androidFocusControlAvailable)
|
||
{
|
||
StartCoroutine(AndroidFocusMaintainCoroutine());
|
||
}
|
||
else
|
||
{
|
||
// 兜底:未接管到 Camera 时,周期性触发重新对焦(部分只对焦一次的设备)
|
||
StartCoroutine(AndroidFallbackFocusMaintainCoroutine());
|
||
}
|
||
#endif
|
||
webCamTexture.autoFocusPoint = new Vector2(0.5f, 0.5f);
|
||
// 调整预览画面比例
|
||
AdjustPreviewAspect();
|
||
|
||
isScanning = true;
|
||
// 开始预览更新和扫描
|
||
StartCoroutine(PreviewUpdateCoroutine());
|
||
StartCoroutine(ScanCoroutine());
|
||
}
|
||
|
||
|
||
// 预览纹理长边基准(按 RawImage 实际宽高比生成,避免变形)
|
||
// 保持 UI RectTransform 比例,内部纹理按比例放大到接近 1080 长边
|
||
// 但不超过相机实际分辨率的 1.5 倍,避免低端设备过度放大导致画质损失
|
||
private const int PreviewTextureLongSide = 1080;
|
||
private const float MaxUpscaleRatio = 1.5f;
|
||
|
||
/// <summary>
|
||
/// 调整预览画面比例 - 从相机画面中心截取并旋转,生成与 UI 框同比例的高清纹理。
|
||
/// 手机竖着拿,摄像头默认横向输出,需要旋转 90 度。
|
||
/// 预览纹理宽高比跟随 RawImage RectTransform 宽高比,长边固定 1080,
|
||
/// 既保证远距离扫码识别率,又避免画面变形。
|
||
/// 对低端设备(720p 摄像头)会自适应降低纹理尺寸,避免过度放大导致画质损失。
|
||
/// </summary>
|
||
private void AdjustPreviewAspect()
|
||
{
|
||
if (webCamTexture == null || cameraPreview == null) return;
|
||
|
||
// 获取相机分辨率(摄像头默认横向,如 3840x2160 / 1920x1080 / 1280x720)
|
||
int cameraWidth = webCamTexture.width;
|
||
int cameraHeight = webCamTexture.height;
|
||
|
||
// 获取预览区域尺寸(用于按比例生成纹理,避免变形)
|
||
RectTransform previewRect = cameraPreview.GetComponent<RectTransform>();
|
||
float rectW = Mathf.Max(1, previewRect.rect.width);
|
||
float rectH = Mathf.Max(1, previewRect.rect.height);
|
||
|
||
// 相机短边(旋转后对应预览宽边),用于限制过度放大
|
||
int cameraShortSide = Mathf.Min(cameraWidth, cameraHeight);
|
||
|
||
// 按 RectTransform 比例计算纹理尺寸(长边固定 1080)
|
||
int previewWidth, previewHeight;
|
||
if (rectW >= rectH)
|
||
{
|
||
previewWidth = PreviewTextureLongSide;
|
||
previewHeight = Mathf.Max(1, Mathf.RoundToInt(PreviewTextureLongSide * (rectH / rectW)));
|
||
}
|
||
else
|
||
{
|
||
previewHeight = PreviewTextureLongSide;
|
||
previewWidth = Mathf.Max(1, Mathf.RoundToInt(PreviewTextureLongSide * (rectW / rectH)));
|
||
}
|
||
|
||
// 自适应:如果纹理长边超过相机短边的 1.5 倍,按相机分辨率等比缩小(保护低端设备画质)
|
||
int previewLongSide = Mathf.Max(previewWidth, previewHeight);
|
||
int maxAllowedLong = Mathf.RoundToInt(cameraShortSide * MaxUpscaleRatio);
|
||
if (previewLongSide > maxAllowedLong)
|
||
{
|
||
float scale = (float)maxAllowedLong / previewLongSide;
|
||
previewWidth = Mathf.Max(1, Mathf.RoundToInt(previewWidth * scale));
|
||
previewHeight = Mathf.Max(1, Mathf.RoundToInt(previewHeight * scale));
|
||
}
|
||
|
||
// 创建预览纹理(按 UI 比例)
|
||
if (previewTexture == null || previewTexture.width != previewWidth || previewTexture.height != previewHeight)
|
||
{
|
||
if (previewTexture != null)
|
||
{
|
||
Destroy(previewTexture);
|
||
}
|
||
previewTexture = new Texture2D(previewWidth, previewHeight, TextureFormat.RGB24, false);
|
||
}
|
||
|
||
// 从相机画面中裁切并旋转
|
||
CutAndRotateCameraFrame(cameraWidth, cameraHeight, previewWidth, previewHeight);
|
||
|
||
// 设置预览纹理
|
||
cameraPreview.texture = previewTexture;
|
||
cameraPreview.uvRect = new Rect(0, 0, 1, 1);
|
||
|
||
// 重置旋转(因为已经在像素层面旋转了)
|
||
previewRect.localEulerAngles = Vector3.zero;
|
||
|
||
Debug.Log($"[ScanQRcode] 预览调整 - 相机: {cameraWidth}x{cameraHeight}, 预览: {previewWidth}x{previewHeight}");
|
||
}
|
||
|
||
#if UNITY_IOS && !UNITY_EDITOR
|
||
/// <summary>
|
||
/// iOS 对焦维护协程 — 每 3s 触发一次点对焦(无黑屏),让镜头持续重新对焦
|
||
/// 与 Android softTrigger 策略保持一致
|
||
/// </summary>
|
||
private IEnumerator IOsFocusMaintainCoroutine()
|
||
{
|
||
// 首次等待 3s 让初始对焦完成
|
||
yield return new WaitForSeconds(3f);
|
||
|
||
while (isScanning)
|
||
{
|
||
// 触发一次点对焦(无黑屏),1.5s 后原生会自动恢复连续对焦
|
||
_TriggerContinuousRefocus();
|
||
yield return new WaitForSeconds(3f);
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if UNITY_ANDROID && !UNITY_EDITOR
|
||
/// <summary>
|
||
/// 启用 Android 自动对焦。
|
||
/// Unity 2022 LTS 在 Android 上使用 Camera2 API 内部创建 CameraDevice,
|
||
/// 无法直接反射拿到 Camera 对象,本方法仅尝试拿到旧 Camera API 的实例。
|
||
/// 失败时仅打印日志,不阻塞扫码流程(依赖系统默认对焦)。
|
||
/// </summary>
|
||
private void EnableAndroidContinuousAutoFocus()
|
||
{
|
||
androidCameraInstance = GetAndroidCameraFromWebCamTexture(webCamTexture);
|
||
if (androidCameraInstance != null)
|
||
{
|
||
try
|
||
{
|
||
SetCameraFocusMode(androidCameraInstance, "FOCUS_MODE_CONTINUOUS_PICTURE");
|
||
androidFocusControlAvailable = true;
|
||
Debug.Log("[ScanQRcode] Android 自动对焦已接管(FOCUS_MODE_CONTINUOUS_PICTURE)");
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning($"[ScanQRcode] 设置 Android 自动对焦模式失败: {e.Message}");
|
||
androidFocusControlAvailable = false;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
androidFocusControlAvailable = false;
|
||
// 尝试让 WebCamTexture 自身使用自动对焦(部分 ROM 支持)
|
||
try { webCamTexture.autoFocusPoint = new Vector2(0.5f, 0.5f); } catch { }
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 触发 Android 单次自动对焦(指定点)
|
||
/// </summary>
|
||
private void TriggerAndroidAutoFocus(float normX, float normY)
|
||
{
|
||
if (!androidFocusControlAvailable || androidCameraInstance == null)
|
||
{
|
||
// 没接管到对焦,触摸对焦无效(保持安静,不打日志避免刷屏)
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
// 屏幕坐标 -> camera 坐标系(Camera API 的 focusAreas 使用 -1000~1000 坐标系)
|
||
int focusX = Mathf.RoundToInt((normX - 0.5f) * 2000f);
|
||
int focusY = Mathf.RoundToInt((normY - 0.5f) * 2000f);
|
||
|
||
using (var rectClass = new AndroidJavaClass("android.graphics.Rect"))
|
||
using (var area = rectClass.CallStatic<AndroidJavaObject>("new", focusX - 100, focusY - 100, focusX + 100, focusY + 100))
|
||
using (var areasList = new AndroidJavaObject("java.util.ArrayList"))
|
||
{
|
||
areasList.Call<bool>("add", area);
|
||
androidCameraInstance.Call("setFocusAreas", areasList);
|
||
androidCameraInstance.Call("setMeteringAreas", areasList);
|
||
}
|
||
|
||
// 先切到 AUTO 模式才能执行单次对焦,对焦完再切回 CONTINUOUS
|
||
SetCameraFocusMode(androidCameraInstance, "FOCUS_MODE_AUTO");
|
||
|
||
// AndroidJavaProxy 不是 IDisposable,不能 using;也不需要 Dispose,GC 即可回收
|
||
var callback = new AutoFocusCallbackProxy(this);
|
||
androidCameraInstance.Call("autoFocus", callback);
|
||
|
||
// 1.5s 后切回连续模式(对焦回调可能未触发,需要兜底)
|
||
StartCoroutine(RestoreContinuousFocusAfter(1.5f));
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning($"[ScanQRcode] Android 触摸对焦失败: {e.Message}");
|
||
}
|
||
}
|
||
|
||
private IEnumerator RestoreContinuousFocusAfter(float seconds)
|
||
{
|
||
yield return new WaitForSeconds(seconds);
|
||
if (androidFocusControlAvailable && androidCameraInstance != null)
|
||
{
|
||
SetCameraFocusMode(androidCameraInstance, "FOCUS_MODE_CONTINUOUS_PICTURE");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 周期性重新设置连续对焦,防止 Android 镜头因失焦后停止自动对焦
|
||
/// </summary>
|
||
private IEnumerator AndroidFocusMaintainCoroutine()
|
||
{
|
||
while (isScanning)
|
||
{
|
||
yield return new WaitForSeconds(2.5f);
|
||
if (androidFocusControlAvailable && androidCameraInstance != null)
|
||
{
|
||
try
|
||
{
|
||
SetCameraFocusMode(androidCameraInstance, "FOCUS_MODE_CONTINUOUS_PICTURE");
|
||
}
|
||
catch { /* 实例可能已被释放,下次循环重试 */ }
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 兜底对焦策略:未接管到 Camera 实例时(Unity 2022 LTS 默认情况),
|
||
/// 每 3s 通过 softTrigger(autoFocusPoint 重新设置,几乎无感)让镜头重新对焦。
|
||
/// 用户也可通过调用 TriggerRefocus() 主动触发。
|
||
/// </summary>
|
||
private IEnumerator AndroidFallbackFocusMaintainCoroutine()
|
||
{
|
||
// 首次等待 3s 让初始对焦完成
|
||
yield return new WaitForSeconds(3f);
|
||
|
||
while (isScanning && webCamTexture != null)
|
||
{
|
||
// 如果在运行中已经接管到对焦控制(理论上不会变),停止兜底
|
||
if (androidFocusControlAvailable) break;
|
||
|
||
// 软触发:重新设置 autoFocusPoint(无黑屏)
|
||
SoftTriggerAndroidFocus();
|
||
|
||
yield return new WaitForSeconds(3f);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 软触发对焦:通过重新设置 autoFocusPoint 让手机做一次快速对焦,无黑屏。
|
||
/// 部分手机响应后会重新对焦,部分手机忽略此属性(需要走硬重启兜底)。
|
||
/// </summary>
|
||
private void SoftTriggerAndroidFocus()
|
||
{
|
||
if (webCamTexture == null || !webCamTexture.isPlaying) return;
|
||
try
|
||
{
|
||
// 在 (0.5, 0.5) 和 (0.5, 0.51) 之间切换,强制触发 setter
|
||
// 相同值部分 ROM 不响应,需要做点变化
|
||
webCamTexture.autoFocusPoint = new Vector2(0.5f, 0.51f);
|
||
webCamTexture.autoFocusPoint = new Vector2(0.5f, 0.5f);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning($"[ScanQRcode] softTrigger 失败: {e.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 通过停止+重新播放 WebCamTexture 强制镜头重新对焦。
|
||
/// 注意:会有约 200~500ms 的预览黑屏闪烁,权衡使用。
|
||
/// </summary>
|
||
private void RestartWebCamTextureForFocus()
|
||
{
|
||
try
|
||
{
|
||
if (webCamTexture == null) return;
|
||
webCamTexture.Stop();
|
||
webCamTexture.Play();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning($"[ScanQRcode] 重启 WebCamTexture 失败: {e.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 通过反射从 WebCamTexture 拿到内部的 android.hardware.Camera 实例。
|
||
/// 注意:Unity 2022 LTS 在 Android 上使用 Camera2 API,此反射在 2022 LTS 上几乎拿不到值。
|
||
/// </summary>
|
||
private AndroidJavaObject GetAndroidCameraFromWebCamTexture(WebCamTexture tex)
|
||
{
|
||
try
|
||
{
|
||
string[] candidates = { "m_Camera", "m_NativeCamera", "camera", "mCamera" };
|
||
foreach (var name in candidates)
|
||
{
|
||
var f = typeof(WebCamTexture).GetField(name,
|
||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||
if (f != null)
|
||
{
|
||
var v = f.GetValue(tex);
|
||
if (v is AndroidJavaObject ajo) return ajo;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning($"[ScanQRcode] 反射获取 Camera 失败: {e.Message}");
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置 Camera 对焦模式(Camera API 官方推荐通过 setParameters 设置)
|
||
/// </summary>
|
||
private void SetCameraFocusMode(AndroidJavaObject camera, string modeName)
|
||
{
|
||
if (camera == null) return;
|
||
using (var parameters = camera.Call<AndroidJavaObject>("getParameters"))
|
||
{
|
||
parameters.Call("setFocusMode", modeName);
|
||
camera.Call("setParameters", parameters);
|
||
}
|
||
// 部分旧设备直接调用 setFocusMode(String) 也生效,兼容一下
|
||
camera.Call("setFocusMode", modeName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// AutoFocusCallback 的 C# 实现(AndroidJavaProxy)
|
||
/// </summary>
|
||
private class AutoFocusCallbackProxy : AndroidJavaProxy
|
||
{
|
||
private readonly ScanQRcode _owner;
|
||
public AutoFocusCallbackProxy(ScanQRcode owner) : base("android.hardware.Camera$AutoFocusCallback")
|
||
{
|
||
_owner = owner;
|
||
}
|
||
|
||
// Java 签名: void onAutoFocus(boolean success, Camera camera)
|
||
public void onAutoFocus(bool success, AndroidJavaObject camera)
|
||
{
|
||
try
|
||
{
|
||
if (_owner != null && _owner.androidFocusControlAvailable && _owner.androidCameraInstance != null)
|
||
{
|
||
_owner.SetCameraFocusMode(_owner.androidCameraInstance, "FOCUS_MODE_CONTINUOUS_PICTURE");
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
#endif
|
||
|
||
private Texture2D previewTexture;
|
||
|
||
/// <summary>
|
||
/// 从相机画面裁切并旋转,生成预览画面
|
||
/// 支持中心裁切放大:只截取画面中心区域,放大到预览尺寸
|
||
/// 性能优化:按行复用 float 增量,避免重复乘法
|
||
/// </summary>
|
||
private void CutAndRotateCameraFrame(int camW, int camH, int targetW, int targetH)
|
||
{
|
||
// 计算中心裁切区域大小
|
||
int sourceCropH = Mathf.Max(1, Mathf.RoundToInt(targetH * centerCropRatio));
|
||
int sourceCropW = Mathf.Max(1, Mathf.RoundToInt(targetW * centerCropRatio));
|
||
int cropY = (camW - sourceCropH) / 2;
|
||
int cropX = (camH - sourceCropW) / 2;
|
||
|
||
// 两个方向缩放一致,保证不变形
|
||
float scale = centerCropRatio;
|
||
|
||
// 获取相机像素数据
|
||
Color32[] cameraPixels = webCamTexture.GetPixels32();
|
||
Color32[] targetPixels = new Color32[targetW * targetH];
|
||
|
||
// 顺时针旋转90度 + 中心裁切 + 缩放
|
||
for (int y = 0; y < targetH; y++)
|
||
{
|
||
float srcXBase = cropY + (targetH - 1 - y) * scale; // 整行的 srcX
|
||
float srcYBase = cropX; // 起始 srcY
|
||
|
||
int rowStart = y * targetW;
|
||
for (int x = 0; x < targetW; x++)
|
||
{
|
||
float srcX = srcXBase;
|
||
float srcY = srcYBase + x * scale;
|
||
|
||
int sx = (int)srcX;
|
||
int sy = (int)srcY;
|
||
float fx = srcX - sx;
|
||
float fy = srcY - sy;
|
||
int sx1 = Mathf.Min(sx + 1, camW - 1);
|
||
int sy1 = Mathf.Min(sy + 1, camH - 1);
|
||
|
||
int idx00 = sy * camW + sx;
|
||
int idx10 = sy * camW + sx1;
|
||
int idx01 = sy1 * camW + sx;
|
||
int idx11 = sy1 * camW + sx1;
|
||
|
||
Color32 c00 = cameraPixels[idx00];
|
||
Color32 c10 = cameraPixels[idx10];
|
||
Color32 c01 = cameraPixels[idx01];
|
||
Color32 c11 = cameraPixels[idx11];
|
||
|
||
float r = (1-fx)*(1-fy)*c00.r + fx*(1-fy)*c10.r + (1-fx)*fy*c01.r + fx*fy*c11.r;
|
||
float g = (1-fx)*(1-fy)*c00.g + fx*(1-fy)*c10.g + (1-fx)*fy*c01.g + fx*fy*c11.g;
|
||
float b = (1-fx)*(1-fy)*c00.b + fx*(1-fy)*c10.b + (1-fx)*fy*c01.b + fx*fy*c11.b;
|
||
|
||
targetPixels[rowStart + x] = new Color32(
|
||
(byte)Mathf.Clamp(r, 0, 255),
|
||
(byte)Mathf.Clamp(g, 0, 255),
|
||
(byte)Mathf.Clamp(b, 0, 255),
|
||
255
|
||
);
|
||
}
|
||
}
|
||
|
||
// 设置像素到预览纹理
|
||
previewTexture.SetPixels32(targetPixels);
|
||
previewTexture.Apply();
|
||
}
|
||
|
||
[SerializeField] private float previewUpdateInterval = 0.03f; // 预览更新间隔(20fps)
|
||
|
||
/// <summary>
|
||
/// 预览更新协程 - 降低更新频率避免卡顿
|
||
/// </summary>
|
||
private IEnumerator PreviewUpdateCoroutine()
|
||
{
|
||
while (isScanning && webCamTexture != null && webCamTexture.isPlaying)
|
||
{
|
||
// 更新预览画面(限制频率)
|
||
if (previewTexture != null)
|
||
{
|
||
CutAndRotateCameraFrame(webCamTexture.width, webCamTexture.height, previewTexture.width, previewTexture.height);
|
||
}
|
||
yield return new WaitForSeconds(previewUpdateInterval);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 扫描协程
|
||
/// </summary>
|
||
private IEnumerator ScanCoroutine()
|
||
{
|
||
// 等待一帧确保摄像头已准备好
|
||
yield return null;
|
||
|
||
while (isScanning && webCamTexture != null && webCamTexture.isPlaying)
|
||
{
|
||
// 避免同时处理多帧
|
||
if (isProcessing)
|
||
{
|
||
yield return null;
|
||
continue;
|
||
}
|
||
|
||
isProcessing = true;
|
||
|
||
// 使用预览纹理进行扫描
|
||
if (previewTexture != null)
|
||
{
|
||
// 异步解析二维码以避免卡顿
|
||
if (useAsyncScan)
|
||
{
|
||
// 直接获取 RGB24 原始字节,无需手动转换
|
||
var rawData = previewTexture.GetRawTextureData<byte>();
|
||
byte[] rgbBytes = rawData.ToArray();
|
||
int texWidth = previewTexture.width;
|
||
int texHeight = previewTexture.height;
|
||
|
||
string result = null;
|
||
bool scanComplete = false;
|
||
|
||
// 在后台线程执行 ZXing 解码(纯计算,不涉及 Unity API)
|
||
System.Threading.ThreadPool.QueueUserWorkItem(_ =>
|
||
{
|
||
try
|
||
{
|
||
result = DecodeQRCodeFromBytes(rgbBytes, texWidth, texHeight);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"[ScanQRcode] 解码异常: {e.Message}");
|
||
}
|
||
finally
|
||
{
|
||
scanComplete = true;
|
||
}
|
||
});
|
||
|
||
// 等待解码完成
|
||
yield return new WaitUntil(() => scanComplete);
|
||
|
||
// 处理结果
|
||
if (!string.IsNullOrEmpty(result))
|
||
{
|
||
ProcessScanResult(result);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 同步解码
|
||
string result = DecodeQRCode(previewTexture);
|
||
|
||
if (!string.IsNullOrEmpty(result))
|
||
{
|
||
ProcessScanResult(result);
|
||
}
|
||
}
|
||
}
|
||
|
||
isProcessing = false;
|
||
yield return new WaitForSeconds(scanInterval);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理扫描结果
|
||
/// </summary>
|
||
private void ProcessScanResult(string result)
|
||
{
|
||
Debug.Log($"扫描结果: {result}");
|
||
// 根据扫描类型处理结果
|
||
if (scanType == ScanType.ConnectDevice)
|
||
{
|
||
// 从结果中提取 MAC 地址
|
||
string macAddress = ExtractMacAddress(result);
|
||
if (!string.IsNullOrEmpty(macAddress))
|
||
{
|
||
// 扫描成功,仅回传 MAC 地址
|
||
OnQRCodeScanned?.Invoke(macAddress);
|
||
OnQRCodeScanned-=OnQRCodeScanned;
|
||
}
|
||
else
|
||
{
|
||
// 二维码不符合要求,显示提示
|
||
UpdateStatus("100084");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (!string.IsNullOrEmpty(result))
|
||
{
|
||
// 其他用途,直接回传结果
|
||
OnQRCodeScanned?.Invoke(result);
|
||
}
|
||
else
|
||
{
|
||
// 二维码不符合要求,显示提示
|
||
UpdateStatus("100084");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取摄像头当前帧
|
||
/// </summary>
|
||
private Texture2D GetSnapshot()
|
||
{
|
||
if (webCamTexture == null || !webCamTexture.isPlaying) return null;
|
||
|
||
try
|
||
{
|
||
Texture2D snapshot = new Texture2D(webCamTexture.width, webCamTexture.height);
|
||
snapshot.SetPixels(webCamTexture.GetPixels());
|
||
snapshot.Apply();
|
||
|
||
return snapshot;
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 放大纹理(使用双线性插值)- 优化版本
|
||
/// </summary>
|
||
private Texture2D UpscaleTexture(Texture2D source, float factor)
|
||
{
|
||
// 限制放大倍数,避免过度消耗性能
|
||
factor = Mathf.Clamp(factor, 1f, 3f);
|
||
|
||
int newWidth = Mathf.RoundToInt(source.width * factor);
|
||
int newHeight = Mathf.RoundToInt(source.height * factor);
|
||
|
||
// 限制最大尺寸,避免内存问题
|
||
newWidth = Mathf.Min(newWidth, 1280);
|
||
newHeight = Mathf.Min(newHeight, 720);
|
||
|
||
// 如果尺寸没有变化,直接返回原图
|
||
if (newWidth <= source.width || newHeight <= source.height)
|
||
{
|
||
return source;
|
||
}
|
||
|
||
Texture2D result = new Texture2D(newWidth, newHeight, TextureFormat.RGB24, false);
|
||
|
||
// 使用 RenderTexture 进行高质量缩放
|
||
RenderTexture rt = RenderTexture.GetTemporary(newWidth, newHeight, 0, RenderTextureFormat.RGB565);
|
||
RenderTexture.active = rt;
|
||
|
||
Graphics.Blit(source, rt);
|
||
result.ReadPixels(new Rect(0, 0, newWidth, newHeight), 0, 0);
|
||
result.Apply();
|
||
|
||
RenderTexture.active = null;
|
||
RenderTexture.ReleaseTemporary(rt);
|
||
|
||
// 销毁原始纹理
|
||
Destroy(source);
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析二维码
|
||
/// </summary>
|
||
private string DecodeQRCode(Texture2D texture)
|
||
{
|
||
try
|
||
{
|
||
// 全图扫描
|
||
string result = TryDecodeRegion(texture, 0, 0, texture.width, texture.height);
|
||
if (!string.IsNullOrEmpty(result))
|
||
return result;
|
||
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.LogError($"二维码解析失败: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从原始字节数据解析二维码(不依赖 Unity API,可在后台线程调用)
|
||
/// </summary>
|
||
private string DecodeQRCodeFromBytes(byte[] rgbBytes, int width, int height)
|
||
{
|
||
try
|
||
{
|
||
var luminanceSource = new ZXing.RGBLuminanceSource(rgbBytes, width, height);
|
||
string result = TryDecodeWithBinarizer(luminanceSource);
|
||
if (!string.IsNullOrEmpty(result))
|
||
return result;
|
||
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.LogError($"二维码解析失败: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// 尝试解码指定区域
|
||
/// </summary>
|
||
private string TryDecodeRegion(Texture2D texture, int startX, int startY, int width, int height)
|
||
{
|
||
try
|
||
{
|
||
// 确保区域在有效范围内
|
||
startX = Mathf.Max(0, startX);
|
||
startY = Mathf.Max(0, startY);
|
||
width = Mathf.Min(width, texture.width - startX);
|
||
height = Mathf.Min(height, texture.height - startY);
|
||
|
||
// 获取全图像素数据
|
||
Color32[] allPixels = texture.GetPixels32();
|
||
|
||
// 提取指定区域的像素
|
||
Color32[] regionPixels = new Color32[width * height];
|
||
for (int y = 0; y < height; y++)
|
||
{
|
||
for (int x = 0; x < width; x++)
|
||
{
|
||
int srcIndex = (startY + y) * texture.width + (startX + x);
|
||
int dstIndex = y * width + x;
|
||
if (srcIndex < allPixels.Length)
|
||
{
|
||
regionPixels[dstIndex] = allPixels[srcIndex];
|
||
}
|
||
}
|
||
}
|
||
|
||
// 转换为 RGB 字节数组(ZXing 内部会自动处理灰度转换)
|
||
byte[] rgbBytes = new byte[regionPixels.Length * 3];
|
||
for (int i = 0; i < regionPixels.Length; i++)
|
||
{
|
||
rgbBytes[i * 3] = regionPixels[i].r;
|
||
rgbBytes[i * 3 + 1] = regionPixels[i].g;
|
||
rgbBytes[i * 3 + 2] = regionPixels[i].b;
|
||
}
|
||
|
||
// 使用 RGBLuminanceSource,让 ZXing 内部处理灰度转换
|
||
var luminanceSource = new ZXing.RGBLuminanceSource(rgbBytes, width, height);
|
||
|
||
// 尝试解码
|
||
string result = TryDecodeWithBinarizer(luminanceSource);
|
||
if (!string.IsNullOrEmpty(result))
|
||
return result;
|
||
|
||
return null;
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 尝试使用不同的二值化算法解码
|
||
/// </summary>
|
||
private string TryDecodeWithBinarizer(ZXing.LuminanceSource luminanceSource)
|
||
{
|
||
var hints = new Dictionary<ZXing.DecodeHintType, object>
|
||
{
|
||
{ ZXing.DecodeHintType.TRY_HARDER, true },
|
||
{ ZXing.DecodeHintType.POSSIBLE_FORMATS, new List<ZXing.BarcodeFormat> { ZXing.BarcodeFormat.QR_CODE } }
|
||
};
|
||
|
||
var reader = new ZXing.QrCode.QRCodeReader();
|
||
|
||
// 尝试 HybridBinarizer
|
||
try
|
||
{
|
||
var binarizer = new ZXing.Common.HybridBinarizer(luminanceSource);
|
||
var binaryBitmap = new ZXing.BinaryBitmap(binarizer);
|
||
var result = reader.decode(binaryBitmap, hints);
|
||
if (result != null)
|
||
return result.Text;
|
||
}
|
||
catch { }
|
||
|
||
// 尝试 GlobalHistogramBinarizer
|
||
try
|
||
{
|
||
var binarizer = new ZXing.Common.GlobalHistogramBinarizer(luminanceSource);
|
||
var binaryBitmap = new ZXing.BinaryBitmap(binarizer);
|
||
var result = reader.decode(binaryBitmap, hints);
|
||
if (result != null)
|
||
return result.Text;
|
||
}
|
||
catch { }
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 应用锐化滤镜(拉普拉斯算子)
|
||
/// 增强图像边缘,提高模糊二维码识别率
|
||
/// </summary>
|
||
private byte[] ApplySharpenFilter(byte[] input, int width, int height)
|
||
{
|
||
byte[] output = new byte[input.Length];
|
||
|
||
// 拉普拉斯锐化核
|
||
// 0 -1 0
|
||
// -1 5 -1
|
||
// 0 -1 0
|
||
int[] kernel = { 0, -1, 0, -1, 5, -1, 0, -1, 0 };
|
||
int kernelSize = 3;
|
||
int halfKernel = kernelSize / 2;
|
||
|
||
for (int y = 0; y < height; y++)
|
||
{
|
||
for (int x = 0; x < width; x++)
|
||
{
|
||
int sum = 0;
|
||
|
||
// 应用卷积核
|
||
for (int ky = -halfKernel; ky <= halfKernel; ky++)
|
||
{
|
||
for (int kx = -halfKernel; kx <= halfKernel; kx++)
|
||
{
|
||
int py = Mathf.Clamp(y + ky, 0, height - 1);
|
||
int px = Mathf.Clamp(x + kx, 0, width - 1);
|
||
|
||
int pixelIndex = py * width + px;
|
||
int kernelIndex = (ky + halfKernel) * kernelSize + (kx + halfKernel);
|
||
|
||
sum += input[pixelIndex] * kernel[kernelIndex];
|
||
}
|
||
}
|
||
|
||
// 限制在0-255范围内
|
||
output[y * width + x] = (byte)Mathf.Clamp(sum, 0, 255);
|
||
}
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从字符串中提取 MAC 地址
|
||
/// 支持格式:
|
||
/// 1. 带冒号格式:98:EA:A0:02:4E:06
|
||
/// 2. 不带冒号格式:98eaa002658e
|
||
/// 3. 从键值对格式提取:Name:xxx;MAC:98eaa002658e
|
||
/// </summary>
|
||
private string ExtractMacAddress(string input)
|
||
{
|
||
if (string.IsNullOrEmpty(input))
|
||
return null;
|
||
|
||
string macAddress = null;
|
||
|
||
// 尝试从键值对格式提取 MAC:xxx
|
||
// 匹配 MAC: 后面跟着 12 位十六进制字符(可能带冒号或不带)
|
||
System.Text.RegularExpressions.Regex macKeyValueRegex =
|
||
new System.Text.RegularExpressions.Regex(@"MAC:([0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2}:?[0-9A-Fa-f]{2})");
|
||
|
||
System.Text.RegularExpressions.Match kvMatch = macKeyValueRegex.Match(input);
|
||
if (kvMatch.Success)
|
||
{
|
||
macAddress = kvMatch.Groups[1].Value;
|
||
}
|
||
else
|
||
{
|
||
// 尝试匹配带冒号的标准 MAC 格式
|
||
System.Text.RegularExpressions.Regex macColonRegex =
|
||
new System.Text.RegularExpressions.Regex(@"[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}");
|
||
|
||
System.Text.RegularExpressions.Match colonMatch = macColonRegex.Match(input);
|
||
if (colonMatch.Success)
|
||
{
|
||
macAddress = colonMatch.Value;
|
||
}
|
||
else
|
||
{
|
||
// 尝试匹配不带冒号的 12 位十六进制字符
|
||
System.Text.RegularExpressions.Regex macNoColonRegex =
|
||
new System.Text.RegularExpressions.Regex(@"[0-9A-Fa-f]{12}");
|
||
|
||
System.Text.RegularExpressions.Match noColonMatch = macNoColonRegex.Match(input);
|
||
if (noColonMatch.Success)
|
||
{
|
||
macAddress = noColonMatch.Value;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(macAddress))
|
||
{
|
||
// 统一转换为带冒号的大写格式
|
||
return FormatMacAddress(macAddress);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将 MAC 地址统一格式化为带冒号的大写格式
|
||
/// 输入: 98eaa002658e 或 98:EA:A0:02:65:8E
|
||
/// 输出: 98:EA:A0:02:65:8E
|
||
/// </summary>
|
||
private string FormatMacAddress(string mac)
|
||
{
|
||
if (string.IsNullOrEmpty(mac))
|
||
return null;
|
||
|
||
// 移除所有冒号
|
||
string cleanMac = mac.Replace(":", "").Replace("-", "").ToUpper();
|
||
|
||
// 检查长度是否为 12
|
||
if (cleanMac.Length != 12)
|
||
return null;
|
||
|
||
// 格式化为 XX:XX:XX:XX:XX:XX
|
||
return string.Format("{0}:{1}:{2}:{3}:{4}:{5}",
|
||
cleanMac.Substring(0, 2),
|
||
cleanMac.Substring(2, 2),
|
||
cleanMac.Substring(4, 2),
|
||
cleanMac.Substring(6, 2),
|
||
cleanMac.Substring(8, 2),
|
||
cleanMac.Substring(10, 2));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新状态文字
|
||
/// </summary>
|
||
private void UpdateStatus(string code)
|
||
{
|
||
ToastUI.Show(code);
|
||
}
|
||
|
||
void OnDestroy()
|
||
{
|
||
StopScan();
|
||
gameObject.SetActive(false);
|
||
}
|
||
}
|
||
}
|