You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

213 lines
9.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import SwiftUI
import AVFoundation
// MARK: -
struct CodePositionOverlay: View {
let detectedCodes: [DetectedCode]
let previewLayer: AVCaptureVideoPreviewLayer?
let onCodeSelected: (DetectedCode) -> Void
let onRescan: () -> Void
var body: some View {
GeometryReader { geometry in
ZStack {
ForEach(detectedCodes) { code in
CodePositionMarker(
code: code,
screenSize: geometry.size,
previewLayer: previewLayer,
onCodeSelected: onCodeSelected
)
}
//
#if DEBUG
ForEach(detectedCodes) { code in
let position = calculateDebugPosition(code: code, screenSize: geometry.size)
Rectangle()
.stroke(Color.red, lineWidth: 1)
.frame(width: 80, height: 80)
.position(x: position.x, y: position.y)
.opacity(0.3)
}
#endif
// -
VStack {
HStack {
Spacer()
Button(action: {
logInfo("🔄 用户点击重新扫描按钮", className: "CodePositionOverlay")
//
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
impactFeedback.impactOccurred()
onRescan()
}) {
HStack(spacing: 8) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 18, weight: .semibold))
.rotationEffect(.degrees(0))
.animation(.easeInOut(duration: 0.3), value: true)
Text("rescan_button".localized)
.font(.system(size: 15, weight: .semibold))
}
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color.blue.opacity(0.9))
.shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 2)
)
}
.buttonStyle(RescanButtonStyle())
.padding(.trailing, 25)
.padding(.top, 25)
//
#if DEBUG
Button(action: {
logInfo("🔍 调试:检查扫描会话状态", className: "CodePositionOverlay")
//
}) {
Image(systemName: "info.circle")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.yellow)
.padding(8)
.background(Color.black.opacity(0.6))
.clipShape(Circle())
}
.padding(.trailing, 10)
.padding(.top, 25)
#endif
}
Spacer()
}
}
}
.allowsHitTesting(true)
.contentShape(Rectangle()) //
.zIndex(1000) //
}
//
private func calculateDebugPosition(code: DetectedCode, screenSize: CGSize) -> CGPoint {
guard let previewLayer = previewLayer else {
return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2)
}
let metadataObject = code.bounds
let convertedPoint = previewLayer.layerPointConverted(fromCaptureDevicePoint: CGPoint(
x: metadataObject.midX,
y: metadataObject.midY
))
let clampedX = max(40, min(screenSize.width - 40, convertedPoint.x))
let clampedY = max(40, min(screenSize.height - 40, convertedPoint.y))
return CGPoint(x: clampedX, y: clampedY)
}
}
// MARK: -
struct CodePositionMarker: View {
let code: DetectedCode
let screenSize: CGSize
let previewLayer: AVCaptureVideoPreviewLayer?
let onCodeSelected: (DetectedCode) -> Void
var body: some View {
GeometryReader { geometry in
let position = calculatePosition(screenSize: geometry.size)
ZStack {
//
Circle()
.fill(Color.clear)
.frame(width: 80, height: 80)
.contentShape(Circle())
.onTapGesture {
logDebug("🎯 CodePositionMarker 被点击!", className: "CodePositionMarker")
logDebug(" 条码ID: \(code.id)", className: "CodePositionMarker")
logDebug(" 条码类型: \(code.type)", className: "CodePositionMarker")
logDebug(" 条码内容: \(code.content)", className: "CodePositionMarker")
logDebug(" 点击位置: x=\(position.x), y=\(position.y)", className: "CodePositionMarker")
//
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
impactFeedback.impactOccurred()
onCodeSelected(code)
}
//
Circle()
.stroke(Color.green, lineWidth: 3)
.frame(width: 40, height: 40)
//
Circle()
.fill(Color.green.opacity(0.3))
.frame(width: 20, height: 20)
//
Circle()
.fill(Color.green)
.frame(width: 6, height: 6)
}
.position(x: position.x, y: position.y)
.zIndex(1001) //
.onAppear {
logDebug("CodePositionMarker appeared at: x=\(position.x), y=\(position.y)", className: "CodePositionMarker")
logDebug("Screen size: \(geometry.size)", className: "CodePositionMarker")
logDebug("Code bounds: \(code.bounds)", className: "CodePositionMarker")
}
}
}
private func calculatePosition(screenSize: CGSize) -> CGPoint {
guard let previewLayer = previewLayer else {
logWarning("No preview layer available, using screen center", className: "CodePositionMarker")
return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2)
}
guard previewLayer.session?.isRunning == true else {
logWarning("Preview layer session not running, using screen center", className: "CodePositionMarker")
return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2)
}
let metadataObject = code.bounds
let convertedPoint = previewLayer.layerPointConverted(fromCaptureDevicePoint: CGPoint(
x: metadataObject.midX,
y: metadataObject.midY
))
guard convertedPoint.x.isFinite && convertedPoint.y.isFinite else {
logWarning("Invalid converted point: \(convertedPoint), using screen center", className: "CodePositionMarker")
return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2)
}
let clampedX = max(20, min(screenSize.width - 20, convertedPoint.x))
let clampedY = max(20, min(screenSize.height - 20, convertedPoint.y))
logDebug("AVFoundation bounds: \(code.bounds)", className: "CodePositionMarker")
logDebug("Converted point: \(convertedPoint)", className: "CodePositionMarker")
logDebug("Screen size: \(screenSize)", className: "CodePositionMarker")
logDebug("Clamped: x=\(clampedX), y=\(clampedY)", className: "CodePositionMarker")
return CGPoint(x: clampedX, y: clampedY)
}
}
// MARK: -
struct RescanButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
.opacity(configuration.isPressed ? 0.8 : 1.0)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
}
}