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.

341 lines
12 KiB

import UIKit
/// 悬浮窗实现类
/// 使用 UIWindow 创建应用内悬浮窗口
class FloatingWindow: UIWindow {
// MARK: - 属性
private weak var delegate: FloatingWindowDelegate?
private var floatingViewController: FloatingViewController?
private var isMinimized = true
private var lastPosition = CGPoint(x: 100, y: 100)
// MARK: - 初始化
init(delegate: FloatingWindowDelegate) {
self.delegate = delegate
// 创建窗口
super.init(frame: CGRect(x: 100, y: 100, width: 300, height: 120))
setupWindow()
setupViewController()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - 私有方法
/// 设置窗口属性
private func setupWindow() {
// 设置窗口层级,确保在最顶层显示
windowLevel = UIWindow.Level.alert + 1
backgroundColor = UIColor.clear
// 设置窗口为可见但不成为主窗口
isHidden = true
// 设置圆角
layer.cornerRadius = 12
layer.masksToBounds = true
}
/// 设置视图控制器
private func setupViewController() {
floatingViewController = FloatingViewController()
floatingViewController?.delegate = self
rootViewController = floatingViewController
}
// MARK: - 公共方法
/// 显示悬浮窗
func show() {
isHidden = false
makeKeyAndVisible()
// 添加显示动画
alpha = 0
UIView.animate(withDuration: 0.3) {
self.alpha = 1
}
delegate?.floatingWindowDidChangeState(isMinimized ? "minimized" : "expanded")
}
/// 隐藏悬浮窗
func hide() {
UIView.animate(withDuration: 0.3, animations: {
self.alpha = 0
}) { _ in
self.isHidden = true
self.resignKey()
}
delegate?.floatingWindowDidChangeState("hidden")
}
/// 更新内容
func updateContent(_ data: FloatingWindowContentData) {
floatingViewController?.updateContent(data)
}
/// 设置位置
func setPosition(x: Double, y: Double) {
let newPosition = CGPoint(x: x, y: y)
lastPosition = newPosition
UIView.animate(withDuration: 0.3) {
self.frame.origin = newPosition
}
}
/// 切换最小化/展开状态
func toggleMinimized() {
isMinimized.toggle()
let newSize: CGSize
if isMinimized {
newSize = CGSize(width: 120, height: 60)
} else {
newSize = CGSize(width: 300, height: 120)
}
UIView.animate(withDuration: 0.3) {
self.frame.size = newSize
}
floatingViewController?.setMinimized(isMinimized)
delegate?.floatingWindowDidChangeState(isMinimized ? "minimized" : "expanded")
}
}
// MARK: - FloatingViewControllerDelegate
extension FloatingWindow: FloatingViewControllerDelegate {
func floatingViewControllerDidTap() {
toggleMinimized()
delegate?.floatingWindowDidClick()
}
func floatingViewControllerDidPan(translation: CGPoint) {
let newOrigin = CGPoint(
x: lastPosition.x + translation.x,
y: lastPosition.y + translation.y
)
// 限制在屏幕范围内
let screenBounds = UIScreen.main.bounds
let constrainedX = max(0, min(newOrigin.x, screenBounds.width - frame.width))
let constrainedY = max(0, min(newOrigin.y, screenBounds.height - frame.height))
frame.origin = CGPoint(x: constrainedX, y: constrainedY)
}
func floatingViewControllerDidEndPan() {
lastPosition = frame.origin
}
func floatingViewControllerDidRequestClose() {
hide()
delegate?.floatingWindowDidClose()
}
}
/// 悬浮窗视图控制器
private class FloatingViewController: UIViewController {
// MARK: - 属性
weak var delegate: FloatingViewControllerDelegate?
// UI 组件
private let containerView = UIView()
private let sourceTextLabel = UILabel()
private let translatedTextLabel = UILabel()
private let languageInfoLabel = UILabel()
private let timestampLabel = UILabel()
private let closeButton = UIButton(type: .system)
// 手势识别器
private var panGestureRecognizer: UIPanGestureRecognizer!
private var tapGestureRecognizer: UITapGestureRecognizer!
private var isMinimized = true
// MARK: - 生命周期
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupGestures()
}
// MARK: - 私有方法
/// 设置 UI
private func setupUI() {
view.backgroundColor = UIColor.clear
// 容器视图
containerView.backgroundColor = UIColor.black.withAlphaComponent(0.8)
containerView.layer.cornerRadius = 12
containerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(containerView)
// 源文本标签
sourceTextLabel.textColor = .white
sourceTextLabel.font = UIFont.systemFont(ofSize: 14)
sourceTextLabel.numberOfLines = 2
sourceTextLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(sourceTextLabel)
// 翻译文本标签
translatedTextLabel.textColor = .systemBlue
translatedTextLabel.font = UIFont.systemFont(ofSize: 14, weight: .medium)
translatedTextLabel.numberOfLines = 2
translatedTextLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(translatedTextLabel)
// 语言信息标签
languageInfoLabel.textColor = .lightGray
languageInfoLabel.font = UIFont.systemFont(ofSize: 10)
languageInfoLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(languageInfoLabel)
// 时间戳标签
timestampLabel.textColor = .lightGray
timestampLabel.font = UIFont.systemFont(ofSize: 10)
timestampLabel.textAlignment = .right
timestampLabel.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(timestampLabel)
// 关闭按钮
closeButton.setTitle("×", for: .normal)
closeButton.setTitleColor(.white, for: .normal)
closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .bold)
closeButton.translatesAutoresizingMaskIntoConstraints = false
closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside)
containerView.addSubview(closeButton)
setupConstraints()
}
/// 设置约束
private func setupConstraints() {
NSLayoutConstraint.activate([
// 容器视图
containerView.topAnchor.constraint(equalTo: view.topAnchor),
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
// 关闭按钮
closeButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 4),
closeButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -4),
closeButton.widthAnchor.constraint(equalToConstant: 20),
closeButton.heightAnchor.constraint(equalToConstant: 20),
// 源文本标签
sourceTextLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8),
sourceTextLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
sourceTextLabel.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: -4),
// 翻译文本标签
translatedTextLabel.topAnchor.constraint(equalTo: sourceTextLabel.bottomAnchor, constant: 4),
translatedTextLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
translatedTextLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8),
// 语言信息标签
languageInfoLabel.topAnchor.constraint(equalTo: translatedTextLabel.bottomAnchor, constant: 4),
languageInfoLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8),
languageInfoLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -4),
// 时间戳标签
timestampLabel.topAnchor.constraint(equalTo: translatedTextLabel.bottomAnchor, constant: 4),
timestampLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8),
timestampLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -4)
])
}
/// 设置手势识别器
private func setupGestures() {
// 拖拽手势
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
view.addGestureRecognizer(panGestureRecognizer)
// 点击手势
tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
view.addGestureRecognizer(tapGestureRecognizer)
}
// MARK: - 手势处理
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: view.superview)
switch gesture.state {
case .changed:
delegate?.floatingViewControllerDidPan(translation: translation)
case .ended, .cancelled:
delegate?.floatingViewControllerDidEndPan()
default:
break
}
}
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
delegate?.floatingViewControllerDidTap()
}
@objc private func closeButtonTapped() {
delegate?.floatingViewControllerDidRequestClose()
}
// MARK: - 公共方法
/// 更新内容
func updateContent(_ data: FloatingWindowContentData) {
sourceTextLabel.text = data.sourceText.isEmpty ? "正在识别..." : data.sourceText
translatedTextLabel.text = data.translatedText.isEmpty ? "翻译中..." : data.translatedText
languageInfoLabel.text = "\(data.sourceLanguage) → \(data.targetLanguage)"
// 格式化时间戳
if data.timestamp > 0 {
let date = Date(timeIntervalSince1970: TimeInterval(data.timestamp / 1000))
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
timestampLabel.text = formatter.string(from: date)
}
// 中间结果的视觉提示
translatedTextLabel.alpha = data.isIntermediate ? 0.7 : 1.0
}
/// 设置最小化状态
func setMinimized(_ minimized: Bool) {
isMinimized = minimized
// 在最小化状态下隐藏部分元素
sourceTextLabel.isHidden = minimized
languageInfoLabel.isHidden = minimized
timestampLabel.isHidden = minimized
closeButton.isHidden = minimized
// 调整翻译文本标签的约束
if minimized {
translatedTextLabel.numberOfLines = 1
translatedTextLabel.font = UIFont.systemFont(ofSize: 12, weight: .medium)
} else {
translatedTextLabel.numberOfLines = 2
translatedTextLabel.font = UIFont.systemFont(ofSize: 14, weight: .medium)
}
}
}
/// 悬浮窗视图控制器代理协议
private protocol FloatingViewControllerDelegate: AnyObject {
func floatingViewControllerDidTap()
func floatingViewControllerDidPan(translation: CGPoint)
func floatingViewControllerDidEndPan()
func floatingViewControllerDidRequestClose()
}