// // PopupWindow.swift // QuickLocation // // Created by 八条 on 2026/7/2. // import UIKit import RxSwift import RxCocoa import Lottie enum PopupType { /// 搜索位置 case search /// 注册 case register /// 会员 case vip } class PopupWindow: UIView { private static let shared = PopupWindow(frame: CGRect(origin: .zero, size: kScreenSize)) var disposeBag = DisposeBag() private var popupType: PopupType? { didSet { switch popupType { case .search: searchPopupView.isHidden = false earthLottieView.play() case .register: if Bool.random() { registerPopupView.isHidden = true register2PopupView.isHidden = false slideInRegister2Popup() } else { register2PopupView.isHidden = true registerPopupView.isHidden = false slideInRegisterPopup() } case .vip: slideInUnlockVip1Popup() default: break } } } static func show(popupType: PopupType) { guard let superView = kKeyWindow else { return } if PopupWindow.shared.superview != nil { PopupWindow.shared.removeFromSuperview() PopupWindow.shared.bgView.frame = .zero } PopupWindow.shared.resetPopupState() PopupWindow.shared.popupType = popupType PopupWindow.shared.bgView.alpha = 1 PopupWindow.shared.bgView.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight) superView.addSubview(PopupWindow.shared) superView.bringSubviewToFront(PopupWindow.shared) UIView.animate(withDuration: 0.25) { PopupWindow.shared.bgView.alpha = 1 } } private func resetPopupState() { stopRegisterStarFlash() stopRegister1LottieAlternate() stopUnlockVip1Animations() registerPopupView.transform = .identity register2PopupView.transform = .identity unlockVip1View.transform = .identity registerStar1.transform = .identity // register2Star1.transform = .identity searchPopupView.isHidden = true registerPopupView.isHidden = true register2PopupView.isHidden = true unlockVip1View.isHidden = true bgView.isUserInteractionEnabled = true } /// 关闭 static func dismiss() { guard PopupWindow.shared.superview != nil else { return } UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) { PopupWindow.shared.bgView.alpha = 0 } completion: { _ in PopupWindow.shared.resetPopupState() PopupWindow.shared.removeFromSuperview() } } private func setupRx() { registerCloseBtn.rx.tap .subscribe(onNext: { [weak self] _ in self?.activePopupView == 2 ? self?.slideOutRegister2Popup() : self?.slideOutRegisterPopup() }) .disposed(by: disposeBag) registerBtn.rx.tap .subscribe(onNext: { [weak self] _ in self?.activePopupView == 2 ? self?.slideOutRegister2Popup() : self?.slideOutRegisterPopup() AppRouter.push(Route.login) }) .disposed(by: disposeBag) register2CloseBtn.rx.tap .subscribe(onNext: { [weak self] _ in self?.slideOutRegister2Popup() }) .disposed(by: disposeBag) register2Btn.rx.tap .subscribe(onNext: { [weak self] _ in self?.slideOutRegister2Popup() AppRouter.push(Route.login) }) .disposed(by: disposeBag) vip1CloseBtn.rx.tap .subscribe(onNext: { [weak self] _ in self?.slideOutUnlockVip1Popup() }) .disposed(by: disposeBag) vip1UnlockBtn.rx.tap .subscribe(onNext: { [weak self] _ in self?.slideOutUnlockVip1Popup() }) .disposed(by: disposeBag) } private lazy var bgView: UIView = { let view = UIView() view.backgroundColor = .black.withAlphaComponent(0.5) return view }() // MARK: - 实时查位置 lazy var searchPopupView: UIView = { let view = UIView() view.backgroundColor = .clear view.isHidden = true let bgImgView = UIImageView(image: UIImage(named: "PopupWindow/search_bg")) bgImgView.contentMode = .scaleAspectFill view.addSubview(bgImgView) bgImgView.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(828/770) let btn = UIButton(type: .custom) btn.backgroundColor = .clear btn.setBackgroundImage(UIImage(named: "Group/close"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 100, bottom: 100, right: 100) btn.rx.tap.subscribe(onNext: { _ in PopupWindow.dismiss() }).disposed(by: disposeBag) view.addSubview(btn) btn.layoutChain .topToBottomOfView(bgImgView, offset: 0) .centerX() .width(22) .height(22) .bottom() // let peopleIcon = UIImageView(image: UIImage(named: "PopupWindow/search_people")) // peopleIcon.contentMode = .scaleAspectFill // view.addSubview(peopleIcon) // peopleIcon.layoutChain // .top(20) // .width(73) // .height(95) // .rightToCenterXOfView(view) // let eartIcon = UIImageView(image: UIImage(named: "PopupWindow/search_earth")) // eartIcon.contentMode = .scaleAspectFill // view.addSubview(eartIcon) // eartIcon.layoutChain // .topToView(peopleIcon, offset: 12) // .width(88) // .height(81) // .leftToCenterXOfView(view) view.addSubview(earthLottieView) earthLottieView.layoutChain .top() .edgesHorzontal(50) .heightToWidth(95/155) return view }() lazy var earthLottieView: LottieAnimationView = { let view = LottieAnimationView(name: "dialog_phone_search") view.loopMode = .loop return view }() // MARK: - 解锁会员 1 lazy var unlockVip1View: UIView = { let view = UIView() view.backgroundColor = .clear view.isHidden = true let bgImg = UIImageView(image: UIImage(named: "PopupWindow/vip_bg")) bgImg.contentMode = .scaleAspectFill view.addSubview(bgImg) bgImg.layoutChain .top(45) .edgesHorzontal() .bottom() view.addSubview(vip1HeartIcon) vip1HeartIcon.layoutChain .top() .left(21) .width(116) .height(99) let logoImg = UIImageView(image: UIImage(named: "PopupWindow/vip1_logo")) logoImg.contentMode = .scaleAspectFill view.addSubview(logoImg) logoImg.layoutChain .topToView(bgImg, offset: 40) .centerX() .width(125) .height(42) view.addSubview(vip1Tips2Img) vip1Tips2Img.layoutChain .topToBottomOfView(logoImg, offset: 30) .centerX() .width(71) .height(26) view.addSubview(vip1Tips1Img) vip1Tips1Img.layoutChain .rightToLeftOfView(vip1Tips2Img, offset: -24) .centerY(vip1Tips2Img) .width(71) .height(26) view.addSubview(vip1Tips3Img) vip1Tips3Img.layoutChain .leftToRightOfView(vip1Tips2Img, offset: 24) .centerY(vip1Tips2Img) .width(71) .height(26) let textLab = UILabel() textLab.text = "看看TA真实走过哪里" textLab.font = .systemFont(ofSize: 14, weight: .medium) textLab.textColor = UIColor(hexStr: "#16B3FF") view.addSubview(textLab) textLab.layoutChain .topToBottomOfView(vip1Tips2Img, offset: 20) .centerX() view.addSubview(vip1CloseBtn) vip1CloseBtn.layoutChain .topToView(bgImg, offset: 32) .right(17) view.addSubview(vip1UnlockBtn) vip1UnlockBtn.layoutChain .width(224) .height(50) .centerX() .bottom(kSafeBottomMargin + 15) return view }() lazy var vip1HeartIcon: UIImageView = { let view = UIImageView() view.image = UIImage(named: "PopupWindow/vip1_heart") view.contentMode = .scaleAspectFill return view }() lazy var vip1Tips1Img: UIImageView = { let view = UIImageView() view.image = UIImage(named: "PopupWindow/vip1_tips_1") view.contentMode = .scaleAspectFill return view }() lazy var vip1Tips2Img: UIImageView = { let view = UIImageView() view.image = UIImage(named: "PopupWindow/vip1_tips_2") view.contentMode = .scaleAspectFill return view }() lazy var vip1Tips3Img: UIImageView = { let view = UIImageView() view.image = UIImage(named: "PopupWindow/vip1_tips_3") view.contentMode = .scaleAspectFill return view }() lazy var vip1CloseBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "PopupWindow/close"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 30, left: 30, bottom: 30, right: 17) return btn }() lazy var vip1UnlockBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("立即解锁", for: .normal) btn.setTitleColor(.white, for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) btn.cornerRadius = 25 return btn }() private func startUnlockVip1Animations() { stopUnlockVip1Animations() let heartAnimation = CABasicAnimation(keyPath: "transform.scale") heartAnimation.fromValue = 1.0 heartAnimation.toValue = 1.2 heartAnimation.duration = 0.55 heartAnimation.autoreverses = true heartAnimation.repeatCount = .infinity heartAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) vip1HeartIcon.layer.add(heartAnimation, forKey: "vip1HeartBeat") addVip1TipSwingAnimation(to: vip1Tips1Img, from: -0.12, to: 0.12, duration: 0.55) addVip1TipSwingAnimation(to: vip1Tips2Img, from: -0.12, to: 0.12, duration: 0.55) addVip1TipSwingAnimation(to: vip1Tips3Img, from: -0.12, to: 0.12, duration: 0.55) } private func addVip1TipSwingAnimation(to imageView: UIImageView, from: CGFloat, to: CGFloat, duration: CFTimeInterval) { let animation = CABasicAnimation(keyPath: "transform.rotation.z") animation.fromValue = from animation.toValue = to animation.duration = duration animation.autoreverses = true animation.repeatCount = .infinity animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) imageView.layer.add(animation, forKey: "vip1TipSwing") } private func stopUnlockVip1Animations() { vip1HeartIcon.layer.removeAnimation(forKey: "vip1HeartBeat") vip1Tips1Img.layer.removeAnimation(forKey: "vip1TipSwing") vip1Tips2Img.layer.removeAnimation(forKey: "vip1TipSwing") vip1Tips3Img.layer.removeAnimation(forKey: "vip1TipSwing") } private let unlockVip1PopupHeight: CGFloat = 339 private func slideInUnlockVip1Popup() { stopUnlockVip1Animations() bgView.isUserInteractionEnabled = false unlockVip1View.isHidden = false unlockVip1View.layoutIfNeeded() unlockVip1View.transform = CGAffineTransform(translationX: 0, y: unlockVip1PopupHeight) UIView.animate(withDuration: 0.45, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: .curveEaseOut) { self.unlockVip1View.transform = .identity } completion: { _ in self.bgView.isUserInteractionEnabled = true self.startUnlockVip1Animations() } } private func slideOutUnlockVip1Popup() { stopUnlockVip1Animations() bgView.isUserInteractionEnabled = false UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) { self.unlockVip1View.transform = CGAffineTransform(translationX: 0, y: self.unlockVip1PopupHeight) } completion: { _ in Self.dismiss() } } // MARK: - 注册账号 1 lazy var registerPopupView: UIView = { let view = UIView() view.backgroundColor = .clear view.isHidden = true let bgImg = UIImageView(image: UIImage(named: "PopupWindow/register_bg")) bgImg.contentMode = .scaleAspectFill view.addSubview(bgImg) bgImg.layoutChain .top(51) .edgesHorzontal() .bottom() let logoImg = UIImageView(image: UIImage(named: "PopupWindow/register_logo")) logoImg.contentMode = .scaleAspectFill view.addSubview(logoImg) logoImg.layoutChain .top() .centerX() .width(250) .height(123) let textImg = UIImageView(image: UIImage(named: "PopupWindow/register_text")) textImg.contentMode = .scaleAspectFill view.addSubview(textImg) textImg.layoutChain .topToBottomOfView(logoImg, offset: 20) .width(272) .height(50) .centerX() view.addSubview(register1UpLottieView) register1UpLottieView.layoutChain .bottomToTopOfView(logoImg, offset: 120) .edgesHorzontal() .height(200) view.addSubview(register1DownLottieView) register1DownLottieView.layoutChain .topToBottomOfView(logoImg, offset: -140) .edgesHorzontal() .height(200) // view.addSubview(registerStar1) // registerStar1.layoutChain // .top(19) // .leftToView(logoImg, offset: -31) // .width(68) // .heightToWidth(1.0) view.addSubview(registerStar3) registerStar3.layoutChain .top(23) .rightToView(logoImg, offset: -9) .width(26) .height(20) view.addSubview(registerStar2) registerStar2.layoutChain .top(31) .rightToView(logoImg, offset: 20) .width(48) .height(44) view.addSubview(registerCloseBtn) registerCloseBtn.layoutChain .topToBottomOfView(textImg, offset: 30) .left(30) .height(50) view.addSubview(registerBtn) registerBtn.layoutChain .topToBottomOfView(textImg, offset: 30) .right(30) .height(50) .widthToView(registerCloseBtn) registerCloseBtn.layoutChain.rightToLeftOfView(registerBtn, offset: -10) return view }() lazy var register1UpLottieView: LottieAnimationView = { let view = LottieAnimationView(name: "dialog_register_tip1_1") return view }() lazy var register1DownLottieView: LottieAnimationView = { let view = LottieAnimationView(name: "dialog_register_tip1_2") return view }() lazy var registerStar1: UIImageView = { let view = UIImageView() view.image = UIImage(named: "PopupWindow/star_1") return view }() lazy var registerStar2: UIImageView = { let view = UIImageView() view.image = UIImage(named: "PopupWindow/star_2") return view }() lazy var registerStar3: UIImageView = { let view = UIImageView() view.image = UIImage(named: "PopupWindow/star_3") return view }() lazy var registerCloseBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("稍后再说", for: .normal) btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) btn.backgroundColor = .white btn.cornerRadius = 25 btn.borderWidth = 0.5 btn.borderColor = UIColor(hexStr: "#16B3FF") return btn }() lazy var registerBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("免费注册", for: .normal) btn.setTitleColor(.white, for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) btn.cornerRadius = 25 return btn }() private var register1LottieAlternating = false private func startRegister1LottieAlternate() { stopRegister1LottieAlternate() register1LottieAlternating = true playRegister1Up() } private func playRegister1Up() { guard register1LottieAlternating else { return } register1UpLottieView.isHidden = false register1DownLottieView.isHidden = true register1UpLottieView.play { [weak self] _ in self?.playRegister1Down() } } private func playRegister1Down() { guard register1LottieAlternating else { return } register1UpLottieView.isHidden = true register1DownLottieView.isHidden = false register1DownLottieView.play { [weak self] _ in self?.playRegister1Up() } } private func stopRegister1LottieAlternate() { register1LottieAlternating = false register1UpLottieView.stop() register1DownLottieView.stop() register1UpLottieView.isHidden = true register1DownLottieView.isHidden = true } private func startRegisterStarFlash() { stopRegisterStarFlash() let (star2, star3) = activePopupView == 2 ? (register2Star2, register2Star3) : (registerStar2, registerStar3) let animation2 = CABasicAnimation(keyPath: "opacity") animation2.fromValue = 1.0 animation2.toValue = 0.0 animation2.duration = 0.4 animation2.autoreverses = true animation2.repeatCount = .infinity star2.layer.add(animation2, forKey: "starFlash") let animation3 = CABasicAnimation(keyPath: "opacity") animation3.fromValue = 0.0 animation3.toValue = 1.0 animation3.duration = 0.4 animation3.autoreverses = true animation3.repeatCount = .infinity star3.layer.add(animation3, forKey: "starFlash") } private func stopRegisterStarFlash() { registerStar2.layer.removeAnimation(forKey: "starFlash") registerStar3.layer.removeAnimation(forKey: "starFlash") register2Star2.layer.removeAnimation(forKey: "starFlash") register2Star3.layer.removeAnimation(forKey: "starFlash") } private func animateRegisterStarSlideIn() { UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.6, options: .curveEaseOut) { self.registerStar1.transform = .identity } } private let registerPopupHeight: CGFloat = 308 private var activePopupView = 1 private func slideInRegisterPopup() { activePopupView = 1 stopRegisterStarFlash() bgView.isUserInteractionEnabled = false registerPopupView.isHidden = false registerPopupView.layoutIfNeeded() // registerStar1.transform = CGAffineTransform(translationX: -(registerStar1.frame.maxX + 30), y: 0) registerPopupView.transform = CGAffineTransform(translationX: 0, y: registerPopupHeight) UIView.animate(withDuration: 0.45, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: .curveEaseOut) { self.registerPopupView.transform = .identity } completion: { _ in self.bgView.isUserInteractionEnabled = true self.startRegisterStarFlash() self.startRegister1LottieAlternate() // self.animateRegisterStarSlideIn() } } private func slideOutRegisterPopup() { stopRegisterStarFlash() stopRegister1LottieAlternate() bgView.isUserInteractionEnabled = false UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) { self.registerPopupView.transform = CGAffineTransform(translationX: 0, y: self.registerPopupHeight) } completion: { _ in Self.dismiss() } } private func slideInRegister2Popup() { activePopupView = 2 stopRegisterStarFlash() bgView.isUserInteractionEnabled = false register2PopupView.isHidden = false register2PopupView.layoutIfNeeded() // register2Star1.transform = CGAffineTransform(translationX: -(register2Star1.frame.maxX + 30), y: 0) register2PopupView.transform = CGAffineTransform(translationX: 0, y: register2PopupView.bounds.height) UIView.animate(withDuration: 0.45, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: .curveEaseOut) { self.register2PopupView.transform = .identity } completion: { _ in self.bgView.isUserInteractionEnabled = true self.startRegisterStarFlash() self.register2LottieView.play() // self.animateRegister2StarSlideIn() } } private func slideOutRegister2Popup() { stopRegisterStarFlash() bgView.isUserInteractionEnabled = false UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) { self.register2PopupView.transform = CGAffineTransform(translationX: 0, y: self.register2PopupView.bounds.height) } completion: { _ in Self.dismiss() } } private func animateRegister2StarSlideIn() { UIView.animate(withDuration: 0.55, delay: 0.1, usingSpringWithDamping: 0.65, initialSpringVelocity: 0.4, options: .curveEaseOut) { self.register2Star1.transform = .identity } } // MARK: - 注册账号 2 lazy var register2PopupView: UIView = { let view = UIView() view.backgroundColor = .clear view.isHidden = true let bgImg = UIImageView(image: UIImage(named: "PopupWindow/register_bg")) bgImg.contentMode = .scaleAspectFill view.addSubview(bgImg) bgImg.layoutChain .top(58) .edgesHorzontal() .bottom() let logoImg = UIImageView(image: UIImage(named: "PopupWindow/register2_logo")) logoImg.contentMode = .scaleAspectFill view.addSubview(register2LottieView) view.addSubview(logoImg) logoImg.layoutChain .top() .centerX() .width(243) .height(97) let textLab = UILabel() textLab.text = "注册后即可保存历史轨迹、多人 共享地图、跨设备同步。" textLab.font = .systemFont(ofSize: 16, weight: .medium) textLab.textColor = UIColor(hexStr: "#16B3FF") textLab.numberOfLines = 0 view.addSubview(textLab) textLab.layoutChain .topToBottomOfView(logoImg, offset: 33) .edgesHorzontal(75) register2LottieView.layoutChain .top() .rightToLeftOfView(logoImg, offset: 45) .width(100) .heightToWidth(1.0) view.addSubview(register2Star3) register2Star3.layoutChain .top(25) .rightToView(logoImg, offset: -6) .width(26) .height(20) view.addSubview(register2Star2) register2Star2.layoutChain .top(32) .rightToView(logoImg, offset: 25) .width(48) .height(44) view.addSubview(register2Btn) register2Btn.layoutChain .topToBottomOfView(textLab, offset: 30) .width(224) .height(50) .centerX() view.addSubview(register2CloseBtn) register2CloseBtn.layoutChain .topToView(bgImg, offset: 32) .right(15) return view }() lazy var register2LottieView: LottieAnimationView = { let view = LottieAnimationView(name: "dialog_register_tip2") view.loopMode = .loop return view }() lazy var register2Star1: UIImageView = { let view = UIImageView() view.image = UIImage(named: "PopupWindow/register_heart") return view }() lazy var register2Star2: UIImageView = { let view = UIImageView() view.image = UIImage(named: "PopupWindow/star_2") return view }() lazy var register2Star3: UIImageView = { let view = UIImageView() view.image = UIImage(named: "PopupWindow/star_3") return view }() lazy var register2CloseBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "PopupWindow/close"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 30, left: 30, bottom: 30, right: 15) return btn }() lazy var register2Btn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("免费注册", for: .normal) btn.setTitleColor(.white, for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) btn.cornerRadius = 25 return btn }() // MARK: - Init override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .clear addSubview(bgView) bgView.addSubview(searchPopupView) bgView.addSubview(unlockVip1View) bgView.addSubview(registerPopupView) bgView.addSubview(register2PopupView) searchPopupView.layoutChain .centerY() .centerX() registerPopupView.layoutChain .edgesHorzontal() .bottom() .height(308) register2PopupView.layoutChain .edgesHorzontal() .bottom() .height(295) unlockVip1View.layoutChain .edgesHorzontal() .bottom() .height(unlockVip1PopupHeight) setupRx() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() } }