//
// 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)
}
}
}
悬浮视图
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
推荐阅读更多精彩内容
- 悬浮按钮: 悬浮图片: 悬浮Gif图: 悬浮轮播图: 悬浮视频: 使用: - (void)viewDidLoad ...
- 前言 最近在项目开发中有账号关联和切换账号的需求,并且还要有相应的显示和隐藏动画,这时就用到了布局动画 Layou...