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
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()
|
|
}
|
|
|