// // LockedAppPopView.swift // QuickLocation // import UIKit import RxSwift final class LockedAppPopView: UIView { private static let shared = LockedAppPopView(frame: .zero) private static var hostWindow: UIWindow? private static var showRetryCount = 0 private var record: PhoneLockRecord? private var timer: Timer? private var disposeBag = DisposeBag() private let digitLabels = (0..<4).map { _ in LockedAppTimeDigitLabel() } static func show(_ record: PhoneLockRecord) { let popup = LockedAppPopView.shared popup.stopTimer() popup.configure(record) guard let window = attachedWindow() else { guard showRetryCount < 10 else { return } showRetryCount += 1 DispatchQueue.main.async { show(record) } return } showRetryCount = 0 popup.frame = window.bounds popup.autoresizingMask = [.flexibleWidth, .flexibleHeight] if popup.superview !== window { popup.removeFromSuperview() window.addSubview(popup) } window.isHidden = false popup.layoutIfNeeded() popup.startTimer() popup.startStruggleAnimation() popup.overlayView.alpha = 0 popup.cardView.transform = CGAffineTransform(scaleX: 0.92, y: 0.92) popup.cardView.alpha = 0 UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseOut]) { popup.overlayView.alpha = 1 popup.cardView.alpha = 1 popup.cardView.transform = .identity } } static func dismiss() { let popup = LockedAppPopView.shared popup.stopTimer() popup.stopStruggleAnimation() UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseIn]) { popup.overlayView.alpha = 0 popup.cardView.alpha = 0 popup.cardView.transform = CGAffineTransform(scaleX: 0.94, y: 0.94) } completion: { _ in popup.removeFromSuperview() popup.cardView.transform = .identity popup.record = nil hostWindow?.isHidden = true hostWindow = nil } } private static func attachedWindow() -> UIWindow? { if let hostWindow { hostWindow.frame = hostWindow.windowScene?.coordinateSpace.bounds ?? UIScreen.main.bounds return hostWindow } guard let scene = activeWindowScene() else { return nil } let window = UIWindow(windowScene: scene) window.windowLevel = .alert + 1 window.backgroundColor = .clear window.isHidden = false hostWindow = window return window } private static func activeWindowScene() -> UIWindowScene? { let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } return scenes.first { $0.activationState == .foregroundActive } ?? scenes.first { $0.activationState == .foregroundInactive } ?? scenes.first } private let overlayView: UIView = { let view = UIView() view.backgroundColor = UIColor.black.withAlphaComponent(0.62) return view }() private let cardView: LockedAppGradientView = { let view = LockedAppGradientView() view.layer.cornerRadius = 40 view.clipsToBounds = true return view }() private let titleLab: UILabel = { let lab = UILabel() lab.text = "已锁" lab.font = FontManager.boboBold(32) lab.textColor = UIColor(hexStr: "#293445") lab.textAlignment = .center return lab }() private let avatarLockView: UIView = { let view = UIView() view.clipsToBounds = false return view }() private let wallpaperView: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFill view.clipsToBounds = true view.layer.cornerRadius = 28 view.layer.borderWidth = 4 view.layer.borderColor = UIColor.white.cgColor return view }() private let overlayIcon: UIImageView = { let view = UIImageView(image: UIImage(named: "LockDistract/app_locked_overlay")) view.contentMode = .scaleAspectFit return view }() private let requestBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("请求TA解锁", for: .normal) btn.setTitleColor(.white, for: .normal) btn.titleLabel?.font = FontManager.boboBold(18) btn.backgroundColor = UIColor(hexStr: "#293445") btn.layer.cornerRadius = 20 return btn }() private let ignoreBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("无所谓", for: .normal) btn.setTitleColor(UIColor(hexStr: "#A8AFBA"), for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) return btn }() override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupUI() { addSubview(overlayView) addSubview(cardView) overlayView.layoutChain.edges() cardView.layoutChain .centerX() .centerY() .width(315) .height(430) cardView.addSubview(titleLab) titleLab.layoutChain.top(28).centerX() let colon = UILabel() colon.text = ":" colon.font = FontManager.boboBold(22) colon.textColor = UIColor(hexStr: "#293445") colon.textAlignment = .center let timeStack = UIStackView(arrangedSubviews: [ digitLabels[0], digitLabels[1], colon, digitLabels[2], digitLabels[3] ]) timeStack.axis = .horizontal timeStack.alignment = .center timeStack.spacing = 4 cardView.addSubview(timeStack) timeStack.layoutChain .topToBottomOfView(titleLab, offset: 16) .centerX() .height(36) digitLabels.forEach { label in label.layoutChain.width(28).height(36) } cardView.addSubview(avatarLockView) avatarLockView.addSubview(wallpaperView) avatarLockView.addSubview(overlayIcon) avatarLockView.layoutChain .topToBottomOfView(timeStack, offset: 22) .centerX() .width(168) .height(168) wallpaperView.layoutChain .centerX() .centerY() .width(148) .height(148) overlayIcon.layoutChain.edges() cardView.addSubview(requestBtn) requestBtn.layoutChain .topToBottomOfView(avatarLockView, offset: 28) .edgesHorzontal(20) .height(48) cardView.addSubview(ignoreBtn) ignoreBtn.layoutChain .topToBottomOfView(requestBtn, offset: 10) .centerX() requestBtn.addTarget(self, action: #selector(tapRequest), for: .touchUpInside) ignoreBtn.addTarget(self, action: #selector(tapIgnore), for: .touchUpInside) } private func configure(_ record: PhoneLockRecord) { self.record = record wallpaperView.setHeadPic(AppContextManager.shared.head_pic) refreshElapsedTime() } private func startTimer() { refreshElapsedTime() let timer = Timer(timeInterval: 1, repeats: true) { [weak self] _ in self?.refreshElapsedTime() } RunLoop.main.add(timer, forMode: .common) self.timer = timer } private func stopTimer() { timer?.invalidate() timer = nil } private func startStruggleAnimation() { stopStruggleAnimation() guard !UIAccessibility.isReduceMotionEnabled else { return } let cycle: CFTimeInterval = 1.25 let keyTimes: [NSNumber] = [0, 0.08, 0.16, 0.26, 0.36, 0.44, 0.52, 1] let timing = CAMediaTimingFunction(name: .easeInEaseOut) let avatarRotate = CAKeyframeAnimation(keyPath: "transform.rotation.z") avatarRotate.values = [0, 0.12, -0.14, 0.11, -0.09, 0.05, 0, 0] avatarRotate.keyTimes = keyTimes avatarRotate.duration = cycle avatarRotate.repeatCount = .infinity avatarRotate.timingFunction = timing let avatarMoveX = CAKeyframeAnimation(keyPath: "transform.translation.x") avatarMoveX.values = [0, 5, -5, 4, -3, 2, 0, 0] avatarMoveX.keyTimes = keyTimes avatarMoveX.duration = cycle avatarMoveX.repeatCount = .infinity avatarMoveX.timingFunction = timing let avatarMoveY = CAKeyframeAnimation(keyPath: "transform.translation.y") avatarMoveY.values = [0, -3, 2, -2, 3, -1, 0, 0] avatarMoveY.keyTimes = keyTimes avatarMoveY.duration = cycle avatarMoveY.repeatCount = .infinity avatarMoveY.timingFunction = timing wallpaperView.layer.add(avatarRotate, forKey: "struggle.rotate") wallpaperView.layer.add(avatarMoveX, forKey: "struggle.moveX") wallpaperView.layer.add(avatarMoveY, forKey: "struggle.moveY") let chainRotate = CAKeyframeAnimation(keyPath: "transform.rotation.z") chainRotate.values = [0, -0.05, 0.05, -0.04, 0.035, -0.02, 0, 0] chainRotate.keyTimes = keyTimes chainRotate.duration = cycle chainRotate.repeatCount = .infinity chainRotate.timingFunction = timing let chainScale = CAKeyframeAnimation(keyPath: "transform.scale") chainScale.values = [1, 1.03, 1.01, 1.045, 1.02, 1.01, 1, 1] chainScale.keyTimes = keyTimes chainScale.duration = cycle chainScale.repeatCount = .infinity chainScale.timingFunction = timing overlayIcon.layer.add(chainRotate, forKey: "struggle.rotate") overlayIcon.layer.add(chainScale, forKey: "struggle.scale") } private func stopStruggleAnimation() { wallpaperView.layer.removeAnimation(forKey: "struggle.rotate") wallpaperView.layer.removeAnimation(forKey: "struggle.moveX") wallpaperView.layer.removeAnimation(forKey: "struggle.moveY") overlayIcon.layer.removeAnimation(forKey: "struggle.rotate") overlayIcon.layer.removeAnimation(forKey: "struggle.scale") wallpaperView.transform = .identity overlayIcon.transform = .identity } private func refreshElapsedTime() { guard let record else { return } let elapsed = max(0, Int(Date().timeIntervalSince(record.lockStartDate))) let capped = min(elapsed, 99 * 60 + 59) let minutes = capped / 60 let seconds = capped % 60 let digits = String(format: "%02d%02d", minutes, seconds) for (index, label) in digitLabels.enumerated() { let stringIndex = digits.index(digits.startIndex, offsetBy: index) label.text = String(digits[stringIndex]) } } @objc private func tapRequest() { let locks = PhoneLockSession.currentLocks let source = record ?? locks.first let groupKey = source?.groupKey ?? "" let os = { let value = source?.os.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return value.isEmpty ? "ios" : value }() let tokens = locks.flatMap(\.tokens) guard !groupKey.isEmpty, !tokens.isEmpty else { DLToast.show(text: "锁定数据缺失") return } DLToast.showLoading() disposeBag = DisposeBag() UserService.requestPhoneUnlock(os: os, groupKey: groupKey, tokens: tokens) .subscribe(onNext: { _ in DLToast.show(text: "已向圈主发送解锁请求") Self.dismiss() }, onError: { error in DLToast.show(text: error.gatewayMessage ?? "发送失败") }) .disposed(by: disposeBag) } @objc private func tapIgnore() { Self.dismiss() } } private final class LockedAppTimeDigitLabel: UILabel { override init(frame: CGRect) { super.init(frame: frame) backgroundColor = UIColor(hexStr: "#293445") textColor = .white font = FontManager.boboBold(20) textAlignment = .center layer.cornerRadius = 8 clipsToBounds = true text = "0" } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } private final class LockedAppGradientView: UIView { override class var layerClass: AnyClass { CAGradientLayer.self } override init(frame: CGRect) { super.init(frame: frame) guard let layer = layer as? CAGradientLayer else { return } layer.colors = [ UIColor(hexStr: "#91DEFA").cgColor, UIColor(hexStr: "#D9F4FD").cgColor, UIColor.white.cgColor ] layer.locations = [0, 0.58, 1] layer.startPoint = CGPoint(x: 0.5, y: 0) layer.endPoint = CGPoint(x: 0.5, y: 1) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }