悬浮视图

//
//  PTPetShowManager.swift
//  PTPet
//
//  Created by junqi on 2025/6/9.
//

import UIKit
import SceneKit

class PassthroughWindow: UIWindow {
    var passthroughViews: [UIView] = []
    
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        for view in passthroughViews {
            let convertedPoint = view.convert(point, from: self)
            if view.bounds.contains(convertedPoint) {
                return super.hitTest(point, with: event)
            }
        }
        return nil
    }
}

class PTPetShowManager: NSObject, UIGestureRecognizerDelegate {
    static let shared = PTPetShowManager()
    private let containerView = UIView()
    private let scnView = SCNView()
    
    private var startLocation: CGPoint = .zero
    private var lastLocation: CGPoint = .zero
    private var velocity: CGPoint = .zero
    private var isBeingDragged: Bool = false
    
    private var animator: UIViewPropertyAnimator?
    
    private weak var window: UIWindow?
    private var floatingWindow: PassthroughWindow?
    
    private var isViewAdded = false
    private var lastBringToFrontTime: Date?
    
    private override init() {
        super.init()
        setupContainerView()
        setupGestures()
        
        NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive), name: UIApplication.willResignActiveNotification, object: nil)
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
    @objc private func applicationDidBecomeActive() {
        bringToFront()
    }
    
    @objc private func applicationWillResignActive() {}
    
    private func setupContainerView() {
        containerView.backgroundColor = .red
        containerView.isUserInteractionEnabled = true
        containerView.isHidden = true
        containerView.frame = CGRect(x: 0, y: 0, width: 150, height: 150)
        containerView.layer.shouldRasterize = true
        containerView.layer.rasterizationScale = UIScreen.main.scale
    }
    
    private func setupGestures() {
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        containerView.addGestureRecognizer(panGesture)
        
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
        containerView.addGestureRecognizer(tapGesture)
        
        panGesture.delegate = self
        tapGesture.delegate = self
    }
    
    func setAnimalVisible(_ visible: Bool) {
        if !isViewAdded {
            addViewToWindow()
        }
        containerView.isHidden = !visible
        if visible {
            bringToFront()
        }
    }
    
    func setPosition(_ point: CGPoint) {
        if !isViewAdded {
            addViewToWindow()
        }
        containerView.center = constrainToScreenBounds(point)
        bringToFront()
    }
    
    private func addViewToWindow() {
        if #available(iOS 13.0, *) {
            if let scene = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first(where: { $0.activationState == .foregroundActive }) {
                let window = PassthroughWindow(windowScene: scene)
                window.windowLevel = .alert + 1
                window.backgroundColor = .clear
                window.rootViewController = UIViewController()
                window.rootViewController?.view.backgroundColor = .clear
                window.isHidden = false
                window.passthroughViews = [containerView]
                floatingWindow = window
                window.addSubview(containerView)
                containerView.center = window.center
                isViewAdded = true
                self.window = window
                print("浮窗添加成功")
            }
        } else {
            guard let legacyWindow = UIApplication.shared.keyWindow else { return }
            legacyWindow.addSubview(containerView)
            containerView.center = legacyWindow.center
            self.window = legacyWindow
            isViewAdded = true
            print("添加到旧版本 keyWindow")
        }
    }
    
    func bringToFront(force: Bool = false) {
        if !isViewAdded {
            addViewToWindow()
        }
        
        let now = Date()
        if !force, let lastTime = lastBringToFrontTime, now.timeIntervalSince(lastTime) < 0.5 {
            return
        }
        lastBringToFrontTime = now
        
        if let window = window {
            if window.subviews.last != containerView {
                containerView.removeFromSuperview()
                window.addSubview(containerView)
                print("视图已置于最上层")
            }
        }
    }
    
    @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
        switch gesture.state {
        case .began:
            animator?.stopAnimation(true)
            animator = nil
            startLocation = gesture.location(in: containerView.superview)
            lastLocation = startLocation
            isBeingDragged = true
            bringToFront(force: true)
            
        case .changed:
            let currentLocation = gesture.location(in: containerView.superview)
            let translation = CGPoint(x: currentLocation.x - lastLocation.x, y: currentLocation.y - lastLocation.y)
            var newCenter = CGPoint(x: containerView.center.x + translation.x, y: containerView.center.y + translation.y)
            newCenter = constrainToScreenBounds(newCenter)
            
            UIView.animate(withDuration: 0.1, delay: 0, options: [.curveLinear, .allowUserInteraction]) {
                self.containerView.center = newCenter
            }
            
            velocity = CGPoint(x: translation.x * 10, y: translation.y * 10)
            lastLocation = currentLocation
            
        case .ended, .cancelled:
            isBeingDragged = false
            velocity = gesture.velocity(in: containerView.superview)
            print("Pan ended with velocity: \(velocity)")
            applyFlingEffect(shouldStickToEdge: false)
            
        default:
            break
        }
    }
    
    @objc private func handleTapGesture(_ gesture: UITapGestureRecognizer) {
        print("Pet view tapped")
        bringToFront(force: true)
    }
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
    
    private func applyFlingEffect(shouldStickToEdge: Bool) {
        let maxDistance: CGFloat = 300
        let distanceX = min(maxDistance, max(-maxDistance, velocity.x * 0.05))
        let distanceY = min(maxDistance, max(-maxDistance, velocity.y * 0.05))
        
        var targetX = containerView.center.x + distanceX
        var targetY = containerView.center.y + distanceY
        
        guard let superview = containerView.superview else { return }
        let maxX = superview.bounds.width - containerView.bounds.width / 2
        let minX = containerView.bounds.width / 2
        let maxY = superview.bounds.height - containerView.bounds.height / 2
        let minY = containerView.bounds.height / 2
        
        targetX = max(minX, min(maxX, targetX))
        targetY = max(minY, min(maxY, targetY))
        
        animator = UIViewPropertyAnimator(duration: 0.5, dampingRatio: 0.7) {
            self.containerView.center = CGPoint(x: targetX, y: targetY)
        }
        
        animator?.addCompletion { [weak self] _ in
            if shouldStickToEdge {
                self?.adjustPositionToEdge()
            }
        }
        
        bringToFront(force: true)
        animator?.startAnimation()
    }
    
    private func adjustPositionToEdge() {}
    
    private func constrainToScreenBounds(_ point: CGPoint) -> CGPoint {
        guard let superview = containerView.superview else { return point }
        let maxX = superview.bounds.width - containerView.bounds.width / 2
        let minX = containerView.bounds.width / 2
        let maxY = superview.bounds.height - containerView.bounds.height / 2
        let minY = containerView.bounds.height / 2
        let constrainedX = max(minX, min(maxX, point.x))
        let constrainedY = max(minY, min(maxY, point.y))
        return CGPoint(x: constrainedX, y: constrainedY)
    }
    
    func add3DModel(named modelName: String) {
        if let modelURL = Bundle.main.url(forResource: modelName, withExtension: "scn"),
           let node = SCNReferenceNode(url: modelURL) {
            node.load()
            scnView.scene?.rootNode.addChildNode(node)
            bringToFront(force: true)
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容