// // 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: let roll = Int.random(in: 0...3) switch roll { case 0: unlockVip2View.isHidden = true unlockVip3View.isHidden = true unlockVip4View.isHidden = true unlockVip1View.isHidden = false slideInUnlockVip1Popup() case 1: unlockVip1View.isHidden = true unlockVip3View.isHidden = true unlockVip4View.isHidden = true unlockVip2View.isHidden = false slideInUnlockVip2Popup() case 2: unlockVip1View.isHidden = true unlockVip2View.isHidden = true unlockVip4View.isHidden = true unlockVip3View.isHidden = false slideInUnlockVip3Popup() default: unlockVip1View.isHidden = true unlockVip2View.isHidden = true unlockVip3View.isHidden = true unlockVip4View.isHidden = false slideInUnlockVip4Popup() } default: break } } } private static var dismissCompletion: (() -> Void)? static func show(popupType: PopupType, dismissCompletion: (() -> Void)? = nil) { Self.dismissCompletion = dismissCompletion 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() stopUnlockVip2Animations() stopUnlockVip3Animations() stopUnlockVip4Animations() registerPopupView.transform = .identity register2PopupView.transform = .identity unlockVip1View.transform = .identity unlockVip2View.transform = .identity unlockVip3View.transform = .identity unlockVip4View.transform = .identity registerStar1.transform = .identity // register2Star1.transform = .identity searchPopupView.isHidden = true registerPopupView.isHidden = true register2PopupView.isHidden = true unlockVip1View.isHidden = true unlockVip2View.isHidden = true unlockVip3View.isHidden = true unlockVip4View.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() let completion = Self.dismissCompletion Self.dismissCompletion = nil completion?() } } 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) unlockVip2CloseBtn.rx.tap .subscribe(onNext: { [weak self] _ in self?.slideOutUnlockVip2Popup() }) .disposed(by: disposeBag) unlockVip2Btn.rx.tap .subscribe(onNext: { [weak self] _ in self?.slideOutUnlockVip2Popup() }) .disposed(by: disposeBag) unlockVip3CloseBtn.rx.tap .subscribe(onNext: { [weak self] _ in self?.slideOutUnlockVip3Popup() }) .disposed(by: disposeBag) unlockVip3Btn.rx.tap .subscribe(onNext: { [weak self] _ in self?.slideOutUnlockVip3Popup() }) .disposed(by: disposeBag) unlockVip4CloseBtn.rx.tap .subscribe(onNext: { [weak self] _ in self?.slideOutUnlockVip4Popup() }) .disposed(by: disposeBag) unlockVip4Btn.rx.tap .subscribe(onNext: { [weak self] _ in self?.slideOutUnlockVip4Popup() }) .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 bgImgView.rx.tapGesture.subscribe(onNext: { _ in PopupWindow.dismiss() AppRouter.push(Route.searchLocation) }).disposed(by: disposeBag) 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: 16) .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: - 解锁会员 2 private let unlockVip2PopupHeight: CGFloat = 412 lazy var unlockVip2View: UIView = { let view = UIView() view.backgroundColor = .clear view.isHidden = true let bgImg = UIImageView(image: UIImage(named: "PopupWindow/vip2_bg")) bgImg.contentMode = .scaleAspectFill view.addSubview(bgImg) bgImg.layoutChain .top(84) .edgesHorzontal() .bottom() let titleLab = UILabel() let text = "会员升级人次" let attr = NSMutableAttributedString(string: text) let strokeAttrs: [NSAttributedString.Key: Any] = [ .strokeWidth: -5, .strokeColor: UIColor.black.cgColor, .foregroundColor: UIColor.white, .font: UIFont(name: "zihunbiantaoti", size: 20) ?? .systemFont(ofSize: 20) ] attr.setAttributes(strokeAttrs, range: NSRange(location: 0, length: text.count)) titleLab.attributedText = attr view.addSubview(titleLab) titleLab.layoutChain .top(106) .left(29) let countTitleLab = UILabel() let countText = "已突破" let countAttr = NSMutableAttributedString(string: countText) let countStrokeAttrs: [NSAttributedString.Key: Any] = [ .strokeWidth: -5, .strokeColor: UIColor.black.cgColor, .foregroundColor: UIColor.white, .font: UIFont(name: "zihunbiantaoti", size: 22) ?? .systemFont(ofSize: 22) ] countAttr.setAttributes(countStrokeAttrs, range: NSRange(location: 0, length: countText.count)) countTitleLab.attributedText = countAttr view.addSubview(countTitleLab) countTitleLab.layoutChain .topToBottomOfView(titleLab, offset: 22) .leftToView(titleLab) view.addSubview(unlockVip2CountLab) unlockVip2CountLab.layoutChain .leftToRightOfView(countTitleLab) .bottomToView(countTitleLab, offset: 3) view.addSubview(unlockVip2LogoImg) unlockVip2LogoImg.layoutChain .top() .right() .width(178) .height(199) let textImg = UIImageView(image: UIImage(named: "PopupWindow/vip2_text")) view.addSubview(textImg) textImg.layoutChain .topToBottomOfView(unlockVip2LogoImg, offset: 15) .centerX() .width(275) .height(41) view.addSubview(unlockVip2CloseBtn) unlockVip2CloseBtn.layoutChain .bottom(kSafeBottomMargin + 15) .left(30) .height(50) view.addSubview(unlockVip2Btn) unlockVip2Btn.layoutChain .bottomToView(unlockVip2CloseBtn) .right(30) .height(50) .widthToView(unlockVip2CloseBtn) unlockVip2CloseBtn.layoutChain.rightToLeftOfView(unlockVip2Btn, offset: -10) return view }() lazy var unlockVip2LogoImg: UIImageView = { let view = UIImageView() view.image = UIImage(named: "PopupWindow/vip2_logo") view.contentMode = .scaleAspectFill return view }() private var unlockVip2CountTarget: Int = 0 private var unlockVip2CountTimer: Timer? lazy var unlockVip2CountLab: UILabel = { let label = UILabel() unlockVip2CountTarget = Int.random(in: 10000...15000) let text = "0+" let attr = NSMutableAttributedString(string: text) let strokeAttrs: [NSAttributedString.Key: Any] = [ .strokeWidth: -5, .strokeColor: UIColor.black.cgColor, .foregroundColor: UIColor(hexStr: "#7CE7FC"), .font: UIFont(name: "zihunbiantaoti", size: 34) ?? .systemFont(ofSize: 34) ] attr.setAttributes(strokeAttrs, range: NSRange(location: 0, length: text.count)) label.attributedText = attr return label }() lazy var unlockVip2CloseBtn: 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 unlockVip2Btn: 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 startUnlockVip2Animations() { stopUnlockVip2Animations() let animation = CABasicAnimation(keyPath: "transform.rotation.z") animation.fromValue = -0.08 animation.toValue = 0.08 animation.duration = 0.7 animation.autoreverses = true animation.repeatCount = .infinity animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) unlockVip2LogoImg.layer.add(animation, forKey: "unlockVip2LogoSwing") startUnlockVip2CountAnimation() } private func stopUnlockVip2Animations() { unlockVip2LogoImg.layer.removeAnimation(forKey: "unlockVip2LogoSwing") unlockVip2CountTimer?.invalidate() unlockVip2CountTimer = nil } private func startUnlockVip2CountAnimation() { unlockVip2CountTimer?.invalidate() let target = unlockVip2CountTarget let duration: TimeInterval = 2.0 let interval: TimeInterval = 0.025 let totalSteps = Int(duration / interval) var step = 0 unlockVip2CountTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] timer in step += 1 let progress = min(Double(step) / Double(totalSteps), 1.0) let eased = 1.0 - pow(1.0 - progress, 3) let current = Int(Double(target) * eased) let text = "\(current)+" let attr = NSMutableAttributedString(string: text) let strokeAttrs: [NSAttributedString.Key: Any] = [ .strokeWidth: -3, .strokeColor: UIColor.black.cgColor, .foregroundColor: UIColor(hexStr: "#7CE7FC"), .font: UIFont(name: "zihunbiantaoti", size: 34) ?? .systemFont(ofSize: 34) ] attr.setAttributes(strokeAttrs, range: NSRange(location: 0, length: text.count)) self?.unlockVip2CountLab.attributedText = attr if progress >= 1.0 { timer.invalidate() self?.unlockVip2CountTimer = nil } } } private func slideInUnlockVip2Popup() { stopUnlockVip2Animations() unlockVip2CountTarget = Int.random(in: 10000...15000) let attr = NSMutableAttributedString(string: "0+") let strokeAttrs: [NSAttributedString.Key: Any] = [ .strokeWidth: -3, .strokeColor: UIColor.black.cgColor, .foregroundColor: UIColor(hexStr: "#7CE7FC"), .font: UIFont(name: "zihunbiantaoti", size: 34) ?? .systemFont(ofSize: 34) ] attr.setAttributes(strokeAttrs, range: NSRange(location: 0, length: 2)) unlockVip2CountLab.attributedText = attr bgView.isUserInteractionEnabled = false unlockVip2View.isHidden = false unlockVip2View.layoutIfNeeded() unlockVip2View.transform = CGAffineTransform(translationX: 0, y: unlockVip2PopupHeight) UIView.animate(withDuration: 0.45, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: .curveEaseOut) { self.unlockVip2View.transform = .identity } completion: { _ in self.bgView.isUserInteractionEnabled = true self.startUnlockVip2Animations() } } private func slideOutUnlockVip2Popup() { stopUnlockVip2Animations() bgView.isUserInteractionEnabled = false UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) { self.unlockVip2View.transform = CGAffineTransform(translationX: 0, y: self.unlockVip2PopupHeight) } completion: { _ in Self.dismiss() } } // MARK: - 解锁会员 3 private let unlockVip3PopupHeight: CGFloat = 385 lazy var unlockVip3View: UIView = { let view = UIView() view.backgroundColor = .clear view.isHidden = true let bgImg = UIImageView(image: UIImage(named: "PopupWindow/vip3_bg")) bgImg.contentMode = .scaleAspectFill view.addSubview(bgImg) bgImg.layoutChain .top(45) .edgesHorzontal() .bottom() view.addSubview(unlockVip3LottieView) unlockVip3LottieView.layoutChain .top(-120) .right(-110) .width(420) .height(370) view.addSubview(unlockVip3CloseBtn) unlockVip3CloseBtn.layoutChain .bottom(kSafeBottomMargin + 15) .left(30) .height(50) view.addSubview(unlockVip3Btn) unlockVip3Btn.layoutChain .bottomToView(unlockVip3CloseBtn) .right(30) .height(50) .widthToView(unlockVip3CloseBtn) unlockVip3CloseBtn.layoutChain.rightToLeftOfView(unlockVip3Btn, offset: -10) return view }() lazy var unlockVip3LottieView: LottieAnimationView = { let view = LottieAnimationView(name: "dialog_buy_vip_3") view.loopMode = .loop return view }() lazy var unlockVip3CloseBtn: 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 unlockVip3Btn: 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 stopUnlockVip3Animations() { unlockVip3LottieView.stop() } private func slideInUnlockVip3Popup() { stopUnlockVip3Animations() bgView.isUserInteractionEnabled = false unlockVip3View.isHidden = false unlockVip3View.layoutIfNeeded() unlockVip3View.transform = CGAffineTransform(translationX: 0, y: unlockVip3PopupHeight) UIView.animate(withDuration: 0.45, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: .curveEaseOut) { self.unlockVip3View.transform = .identity } completion: { _ in self.bgView.isUserInteractionEnabled = true self.unlockVip3LottieView.play() } } private func slideOutUnlockVip3Popup() { stopUnlockVip3Animations() bgView.isUserInteractionEnabled = false UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) { self.unlockVip3View.transform = CGAffineTransform(translationX: 0, y: self.unlockVip3PopupHeight) } completion: { _ in Self.dismiss() } } // MARK: - 解锁会员 4 private let unlockVip4PopupHeight: CGFloat = 368 lazy var unlockVip4View: UIView = { let view = UIView() view.backgroundColor = .clear view.isHidden = true let bgImg = UIImageView(image: UIImage(named: "PopupWindow/vip4_bg")) bgImg.contentMode = .scaleAspectFill view.addSubview(bgImg) bgImg.layoutChain .top() .edgesHorzontal() .bottom() view.addSubview(unlockVip4AnimateView) unlockVip4AnimateView.layoutChain .top(143) .rightToCenterXOfView(view, offset: 50) .width(60) .height(52) let label = UILabel() label.text = "终身会员" label.font = .systemFont(ofSize: 14, weight: .medium) view.addSubview(label) label.layoutChain .leftToRightOfView(unlockVip4AnimateView, offset: 17) .bottomToView(unlockVip4AnimateView) view.addSubview(unlockVip4DiscountLab) unlockVip4DiscountLab.layoutChain .bottomToView(label, offset: 10) .leftToRightOfView(label) let unitLab = UILabel() unitLab.text = "折" unitLab.font = .systemFont(ofSize: 14, weight: .medium) view.addSubview(unitLab) unitLab.layoutChain .bottomToView(label, offset: 0) .leftToRightOfView(unlockVip4DiscountLab) view.addSubview(unlockVip4CloseBtn) unlockVip4CloseBtn.layoutChain .top() .right(18) .width(22) .height(22) view.addSubview(unlockVip4Btn) unlockVip4Btn.layoutChain .centerX() .width(224) .height(50) .bottom(49) return view }() lazy var unlockVip4AnimateView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "PopupWindow/vip4_animate") return view }() lazy var unlockVip4DiscountLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 44, weight: .bold) label.textColor = UIColor(hexStr: "#FF4E41") return label }() lazy var unlockVip4CloseBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "PopupWindow/close"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 30, left: 30, bottom: 30, right: 18) return btn }() lazy var unlockVip4Btn: 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 startUnlockVip4Animations() { stopUnlockVip4Animations() let animateScale = CABasicAnimation(keyPath: "transform.scale") animateScale.fromValue = 1.0 animateScale.toValue = 1.15 animateScale.duration = 0.6 animateScale.autoreverses = true animateScale.repeatCount = .infinity animateScale.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) unlockVip4AnimateView.layer.add(animateScale, forKey: "unlockVip4AnimateScale") let btnScale = CABasicAnimation(keyPath: "transform.scale") btnScale.fromValue = 1.0 btnScale.toValue = 1.08 btnScale.duration = 0.8 btnScale.autoreverses = true btnScale.repeatCount = .infinity btnScale.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) unlockVip4Btn.layer.add(btnScale, forKey: "unlockVip4BtnScale") } private func stopUnlockVip4Animations() { unlockVip4AnimateView.layer.removeAnimation(forKey: "unlockVip4AnimateScale") unlockVip4Btn.layer.removeAnimation(forKey: "unlockVip4BtnScale") } private func slideInUnlockVip4Popup() { if let systemConfig = AppContextManager.shared.systemConfig, let timeConfig = systemConfig.timeConfig { unlockVip4DiscountLab.text = (String(format: "%.1f", timeConfig.min_discount/10)) } stopUnlockVip4Animations() bgView.isUserInteractionEnabled = false unlockVip4View.isHidden = false unlockVip4View.layoutIfNeeded() unlockVip4View.transform = CGAffineTransform(translationX: 0, y: unlockVip4PopupHeight) UIView.animate(withDuration: 0.45, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: .curveEaseOut) { self.unlockVip4View.transform = .identity } completion: { _ in self.bgView.isUserInteractionEnabled = true self.startUnlockVip4Animations() } } private func slideOutUnlockVip4Popup() { stopUnlockVip4Animations() bgView.isUserInteractionEnabled = false UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) { self.unlockVip4View.transform = CGAffineTransform(translationX: 0, y: self.unlockVip4PopupHeight) } 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(unlockVip2View) bgView.addSubview(unlockVip3View) bgView.addSubview(unlockVip4View) 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) unlockVip2View.layoutChain .edgesHorzontal() .bottom() .height(unlockVip2PopupHeight) unlockVip3View.layoutChain .edgesHorzontal() .bottom() .height(unlockVip3PopupHeight) unlockVip4View.layoutChain .edgesHorzontal() .bottom() .height(unlockVip4PopupHeight) setupRx() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() } }