“虞渠成” 432b80dfa3 fix: 修复多处UI返回逻辑、优化扫码对焦与UI布局
1.  优化HomePage加载逻辑,修复加载弹窗重复显示问题
2.  重构蓝牙分享页、重命名页的返回事件绑定逻辑
3.  优化iOS/Android扫码对焦功能,支持多后置摄像头、软触发对焦
4.  调整扫码预览布局与参数,提升扫码识别率与适配性
5.  删除冗余的Android状态栏管理脚本与无用文件
6.  新增返回回调调试日志
2026-09-01 15:47:57 +08:00

1126 lines
43 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>();
// 尝试更高分辨率4KWebCamTexture 在不支持时会自动回退到设备最大值
// 注意:高分辨率下解码 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
// AndroidWebCamTexture 默认不会自动对焦,需主动通过 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也不需要 DisposeGC 即可回收
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 通过 softTriggerautoFocusPoint 重新设置,几乎无感)让镜头重新对焦。
/// 用户也可通过调用 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);
}
}
}