“虞渠成” a3b29d5414 refactor(scanQRcode): 调整预览纹理的声明位置
将预览纹理字段移到类成员顶部,统一管理资源清理逻辑,移除重复注释
2026-07-23 15:08:11 +08:00

518 lines
17 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>
/// 二维码扫描器 - 调用摄像头扫描二维码
/// Android: 使用 WebCamTexture + ZXing 解码
/// iOS: 使用原生 AVCaptureSession连续对焦 + 原生 QR 检测)
/// </summary>
public class ScanQRcode : MonoBehaviour
{
[Serializable]
public enum ScanType
{
ConnectDevice, // 连接设备
Other // 其他用途
}
[Header("UI组件")]
public RawImage cameraPreview; // 摄像头预览
[Header("扫描设置")]
float scanInterval = 0.2f; // 扫描间隔(秒)
public ScanType scanType = ScanType.ConnectDevice; // 扫描类型
[Header("自动对焦")]
[Tooltip("启用对焦")]
public bool enableAutoFocus = true;
#if UNITY_IOS && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void _StartNativeCamera(string gameObjectName);
[DllImport("__Internal")]
private static extern void _StopNativeCamera();
#endif
// 摄像头相关
private WebCamTexture webCamTexture;
private WebCamDevice currentDevice;
private bool isScanning = false;
private bool isProcessing = false;
// 扫描结果回调
public event Action<string> OnQRCodeScanned;
// 预览纹理(所有平台共用清理逻辑)
private Texture2D previewTexture;
void OnEnable()
{
StartScan();
}
void OnDisable()
{
StopScan();
}
/// <summary>
/// 开始扫描
/// </summary>
public void StartScan()
{
if (isScanning) return;
#if UNITY_IOS && !UNITY_EDITOR
// iOS: 使用原生 AVCaptureSession连续对焦 + 原生 QR 检测)
if (cameraPreview != null)
cameraPreview.gameObject.SetActive(false);
_StartNativeCamera(gameObject.name);
isScanning = true;
#else
// Android/其他: 使用 WebCamTexture + ZXing
StartCoroutine(InitializeCamera());
#endif
}
/// <summary>
/// 停止扫描
/// </summary>
public void StopScan()
{
isScanning = false;
StopAllCoroutines();
#if UNITY_IOS && !UNITY_EDITOR
_StopNativeCamera();
if (cameraPreview != null)
cameraPreview.gameObject.SetActive(true);
#endif
if (webCamTexture != null && webCamTexture.isPlaying)
{
webCamTexture.Stop();
webCamTexture = null;
}
if (previewTexture != null)
{
Destroy(previewTexture);
previewTexture = null;
}
}
/// <summary>
/// 关闭扫码界面
/// </summary>
public void Close()
{
StopScan();
gameObject.SetActive(false);
}
// ──── iOS 原生回调 ────
/// <summary>
/// 由 iOS 原生插件调用(通过 UnitySendMessage
/// </summary>
public void OnNativeScanResult(string result)
{
if (string.IsNullOrEmpty(result))
return;
if (result == "CAMERA_READY")
{
Debug.Log("[ScanQRcode] 原生摄像头已就绪");
return;
}
if (result.StartsWith("ERROR:"))
{
Debug.LogError($"[ScanQRcode] 原生摄像头错误: {result}");
ToastUI.Show("100086");
return;
}
Debug.Log($"[ScanQRcode] 原生扫描结果: {result}");
ProcessScanResult(result);
}
// ──── Android / 编辑器的 WebCamTexture 方案 ────
#if !UNITY_IOS || UNITY_EDITOR
/// <summary>
/// 初始化摄像头WebCamTexture 方案)
/// </summary>
private IEnumerator InitializeCamera()
{
#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))
{
ToastUI.Show("100085");
yield break;
}
#else
yield return Application.RequestUserAuthorization(UserAuthorization.WebCam);
if (!Application.HasUserAuthorization(UserAuthorization.WebCam))
{
ToastUI.Show("100085");
yield break;
}
#endif
WebCamDevice[] devices = WebCamTexture.devices;
if (devices.Length == 0)
{
ToastUI.Show("100086");
yield break;
}
// 优先后置主摄
string deviceName = devices[0].name;
currentDevice = devices[0];
for (int i = 0; i < devices.Length; i++)
{
if (!devices[i].isFrontFacing)
{
string lowerName = devices[i].name.ToLower();
if (!lowerName.Contains("telephoto") && !lowerName.Contains("ultra wide"))
{
deviceName = devices[i].name;
currentDevice = devices[i];
break;
}
if (deviceName == devices[0].name)
{
deviceName = devices[i].name;
currentDevice = devices[i];
}
}
}
Debug.Log($"[ScanQRcode] 选中摄像头: {deviceName}");
webCamTexture = new WebCamTexture(deviceName, 2160, 30);
webCamTexture.Play();
yield return new WaitUntil(() => webCamTexture.width > 100);
AdjustPreviewAspect();
isScanning = true;
StartCoroutine(PreviewUpdateCoroutine());
StartCoroutine(ScanCoroutine());
if (enableAutoFocus)
{
StartCoroutine(AutoFocusCoroutine());
}
}
/// <summary>
/// 自动对焦维护协程
/// </summary>
private IEnumerator AutoFocusCoroutine()
{
while (isScanning)
{
if (webCamTexture != null && webCamTexture.isPlaying)
{
webCamTexture.autoFocusPoint = new Vector2(0.5f, 0.5f);
}
yield return new WaitForSeconds(1f);
}
}
/// <summary>
/// 调整预览画面比例
/// </summary>
private void AdjustPreviewAspect()
{
if (webCamTexture == null || cameraPreview == null) return;
int cameraWidth = webCamTexture.width;
int cameraHeight = webCamTexture.height;
RectTransform previewRect = cameraPreview.GetComponent<RectTransform>();
int previewWidth = (int)previewRect.rect.width;
int previewHeight = (int)previewRect.rect.height;
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;
}
/// <summary>
/// 从相机画面裁切并旋转,生成预览画面
/// </summary>
private void CutAndRotateCameraFrame(int camW, int camH, int targetW, int targetH)
{
int cropY = (camW - targetH) / 2;
Color32[] cameraPixels = webCamTexture.GetPixels32();
Color32[] targetPixels = new Color32[targetW * targetH];
for (int y = 0; y < targetH; y++)
{
for (int x = 0; x < targetW; x++)
{
int srcX = cropY + (targetH - 1 - y);
int srcY = x;
if (srcX >= 0 && srcX < camW && srcY >= 0 && srcY < camH)
{
int srcIndex = srcY * camW + srcX;
int dstIndex = y * targetW + x;
targetPixels[dstIndex] = cameraPixels[srcIndex];
}
}
}
previewTexture.SetPixels32(targetPixels);
previewTexture.Apply();
}
[SerializeField] private float previewUpdateInterval = 0.03f;
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>
/// 扫描协程ZXing 解码)
/// </summary>
private IEnumerator ScanCoroutine()
{
yield return null;
while (isScanning && webCamTexture != null && webCamTexture.isPlaying)
{
if (isProcessing)
{
yield return null;
continue;
}
isProcessing = true;
if (previewTexture != null)
{
Color32[] pixels = previewTexture.GetPixels32();
int texWidth = previewTexture.width;
int texHeight = previewTexture.height;
byte[] rgbBytes = new byte[pixels.Length * 3];
for (int i = 0; i < pixels.Length; i++)
{
rgbBytes[i * 3] = pixels[i].r;
rgbBytes[i * 3 + 1] = pixels[i].g;
rgbBytes[i * 3 + 2] = pixels[i].b;
}
string result = null;
bool scanComplete = false;
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);
}
}
isProcessing = false;
yield return new WaitForSeconds(scanInterval);
}
}
/// <summary>
/// 从原始字节数据解析二维码(后台线程安全)
/// </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;
}
catch (Exception ex)
{
Debug.LogError($"二维码解析失败: {ex.Message}");
}
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();
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 { }
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;
}
#endif // !UNITY_IOS || UNITY_EDITOR
// ──── 公共处理方法 ────
/// <summary>
/// 处理扫描结果
/// </summary>
private void ProcessScanResult(string result)
{
Debug.Log($"[ScanQRcode] 扫描结果: {result}");
if (scanType == ScanType.ConnectDevice)
{
string macAddress = ExtractMacAddress(result);
if (!string.IsNullOrEmpty(macAddress))
{
OnQRCodeScanned?.Invoke(macAddress);
OnQRCodeScanned = null;
}
else
{
ToastUI.Show("100084");
}
}
else
{
if (!string.IsNullOrEmpty(result))
{
OnQRCodeScanned?.Invoke(result);
}
else
{
ToastUI.Show("100084");
}
}
}
/// <summary>
/// 从字符串中提取 MAC 地址
/// </summary>
private string ExtractMacAddress(string input)
{
if (string.IsNullOrEmpty(input))
return null;
var 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})");
var kvMatch = macKeyValueRegex.Match(input);
if (kvMatch.Success)
return FormatMacAddress(kvMatch.Groups[1].Value);
var 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}");
var colonMatch = macColonRegex.Match(input);
if (colonMatch.Success)
return FormatMacAddress(colonMatch.Value);
var macNoColonRegex = new System.Text.RegularExpressions.Regex(@"[0-9A-Fa-f]{12}");
var noColonMatch = macNoColonRegex.Match(input);
if (noColonMatch.Success)
return FormatMacAddress(noColonMatch.Value);
return null;
}
/// <summary>
/// 格式化 MAC 地址为 XX:XX:XX:XX:XX:XX
/// </summary>
private string FormatMacAddress(string mac)
{
if (string.IsNullOrEmpty(mac))
return null;
string cleanMac = mac.Replace(":", "").Replace("-", "").ToUpper();
if (cleanMac.Length != 12)
return null;
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));
}
void OnDestroy()
{
StopScan();
gameObject.SetActive(false);
}
}
}