jsdw_ios/QuickLocation/Section/Home/SearchLocation/SearchLocationView.swift

544 lines
18 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// SearchLocationView.swift
// QuickLocation
//
// Created by on 2026/6/27.
//
import UIKit
import RxSwift
import RxCocoa
import AVFoundation
import Lottie
class SearchLocationView: UIView {
var disposeBag = DisposeBag()
let onVideoComplete = PublishSubject<Void>()
private var player: AVPlayer?
private var timeObserver: Any?
private func setupRx() {
backBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.shared.popOrDismiss()
}).disposed(by: disposeBag)
phoneInputTF.rx.text
.map { text -> String? in
guard let text = text else { return nil }
return String(text.prefix(11))
}.bind(to: phoneInputTF.rx.text)
.disposed(by: disposeBag)
phoneInputTF.rx.text.orEmpty.map { phone -> Bool in
if phone.count == 11 {
return true
} else {
return false
}
}
.bind(to: searchBtn.rx.isEnabled)
.disposed(by: disposeBag)
phoneInputTF.rx.controlEvent(.editingDidEndOnExit)
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
self.phoneInputTF.resignFirstResponder()
})
.disposed(by: disposeBag)
}
private func setupUI() {
addSubview(navBgView)
addSubview(navBarView)
navBarView.addSubview(navTitleLabel)
navBarView.addSubview(backBtn)
addSubview(scrollView)
addSubview(videoView)
addSubview(searchProgressView)
videoView.layoutChain.edges()
searchProgressView.layoutChain
.edgesHorzontal(15)
.bottom(kSafeBottomMargin + 20)
.height(221)
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(160/375)
navBarView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
navTitleLabel.layoutChain
.top(kStatusBarHeight + 12)
.centerY(backBtn)
.centerX()
backBtn.layoutChain
.centerY(navTitleLabel)
.left(15)
.width(24)
.height(24)
scrollView.layoutChain
.topToBottomOfView(navBarView)
.edges(excludingEdge: .top)
}
// MARK: - Marquee
private func randomPhoneNumber() -> String {
// 3
let prefixes = [
"134","135","136","137","138","139","147","150","151","152","157","158","159","178","182","183","184","187","188","198",
"130","131","132","145","155","156","166","175","176","185","186","196",
"133","149","153","173","177","180","181","189","191","199"
]
//
let prefix = prefixes.randomElement()!
// 8
var suffix = ""
for _ in 0..<8 {
suffix.append("\(Int.random(in: 0...9))")
}
return prefix + suffix
}
private func setupMarquee() {
let surnames = ["", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", ""]
let container = UIView()
container.backgroundColor = .clear
marqueeScrollView.addSubview(container)
var prevView: UIView?
for _ in 0..<10 {
let iconIndex = Int.random(in: 1...15)
let surname = surnames.randomElement() ?? ""
let maskedName = "\(surname)**"
let phone = randomPhoneNumber()
let maskedPhone = phone.prefix(3) + "******" + phone.suffix(2)
let itemView = UIView()
itemView.backgroundColor = UIColor(hexStr: "#EFF9FF")
itemView.cornerRadius = 6
container.addSubview(itemView)
let avatar = UIImageView(image: UIImage(named: "UserIcon/\(iconIndex)"))
avatar.contentMode = .scaleAspectFill
avatar.cornerRadius = 12
avatar.clipsToBounds = true
itemView.addSubview(avatar)
let label = UILabel()
label.font = .systemFont(ofSize: 13, weight: .medium)
let fullText = "\(maskedName) 定位到了 \(maskedPhone)"
let attr = NSMutableAttributedString(string: fullText)
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"),
range: NSRange(fullText.range(of: maskedName)!, in: fullText))
if let phoneRange = fullText.range(of: maskedPhone) {
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"),
range: NSRange(phoneRange, in: fullText))
}
label.attributedText = attr
itemView.addSubview(label)
avatar.layoutChain
.left(10).centerY()
.width(24).height(24)
label.layoutChain
.leftToRightOfView(avatar, offset: 6)
.centerY().right(10)
itemView.layoutChain
.centerY()
.height(30)
if let prev = prevView {
itemView.layoutChain.leftToRightOfView(prev, offset: 20)
} else {
itemView.layoutChain.left()
}
prevView = itemView
}
if let last = prevView {
container.layoutChain
.edges().heightToView(marqueeScrollView)
.rightToView(last)
}
}
func startMarqueeAnimation() {
marqueeScrollView.layer.removeAllAnimations()
marqueeScrollView.contentOffset.x = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
let contentW = self.marqueeScrollView.contentSize.width
guard contentW > self.marqueeScrollView.bounds.width else { return }
let duration = contentW / 100
UIView.animate(withDuration: duration, delay: 0, options: [.curveLinear, .repeat]) {
self.marqueeScrollView.contentOffset.x = contentW - self.marqueeScrollView.bounds.width
}
}
}
override func willMove(toWindow newWindow: UIWindow?) {
super.willMove(toWindow: newWindow)
if newWindow != nil {
startMarqueeAnimation()
}
}
// MARK: - Views
lazy var navBgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill
return iv
}()
lazy var navBarView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var navTitleLabel: UILabel = {
let label = UILabel()
label.text = "找人助手"
label.font = .systemFont(ofSize: 18, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.textAlignment = .center
return label
}()
lazy var backBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/back"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100)
return btn
}()
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.backgroundColor = .clear
view.showsVerticalScrollIndicator = false
view.bounces = false
let contentView = UIView()
contentView.backgroundColor = .clear
view.addSubview(contentView)
contentView.layoutChain.edges().widthToView(view)
let bgImgView = UIImageView()
bgImgView.image = UIImage(named: "SearchLocation/bg_1")
contentView.addSubview(bgImgView)
bgImgView.layoutChain
.top(15)
.edgesHorzontal(56.5)
.heightToWidth(626/524)
contentView.addSubview(titleLab)
titleLab.layoutChain
.topToBottomOfView(bgImgView, offset: 0)
.centerX()
contentView.addSubview(marqueeScrollView)
marqueeScrollView.layoutChain
.topToBottomOfView(titleLab, offset: 10)
.edgesHorzontal()
.height(30)
contentView.addSubview(searchInputView)
searchInputView.layoutChain
.topToBottomOfView(marqueeScrollView, offset: 20)
.edgesHorzontal(15)
.bottom(kSafeBottomMargin + 30)
return view
}()
lazy var videoView: UIView = {
let v = UIView()
v.backgroundColor = .black
v.isHidden = true
return v
}()
func playVideo(completion: (() -> Void)? = nil) {
guard let path = Bundle.main.path(forResource: "search_interlude", ofType: "mp4") ?? Bundle.main.path(forResource: "search_interlude", ofType: "mp4", inDirectory: "video") else {
print("[video] file not found in bundle: \(Bundle.main.resourcePath ?? "")")
return
}
videoView.isHidden = false
videoView.layoutIfNeeded()
let player = AVPlayer(url: URL(fileURLWithPath: path))
self.player = player
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = videoView.bounds.isEmpty ? UIScreen.main.bounds : videoView.bounds
playerLayer.videoGravity = .resizeAspectFill
videoView.layer.addSublayer(playerLayer)
//
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: 600), queue: .main) { [weak self] time in
guard let self = self, let duration = self.player?.currentItem?.duration.seconds, duration > 0 else { return }
self.updateVideoProgress(Float(time.seconds / duration))
}
NotificationCenter.default.rx.notification(.AVPlayerItemDidPlayToEndTime, object: player.currentItem)
.take(1)
.subscribe(onNext: { _ in
self.updateVideoProgress(1)
completion?()
self.onVideoComplete.onNext(())
}).disposed(by: disposeBag)
player.play()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self = self else { return }
playerLayer.frame = self.videoView.bounds
}
}
override func layoutSubviews() {
super.layoutSubviews()
if let playerLayer = videoView.layer.sublayers?.compactMap({ $0 as? AVPlayerLayer }).first {
playerLayer.frame = videoView.bounds
}
}
lazy var titleLab: UILabel = {
let label = UILabel()
label.text = "\(Int.random(in: 1000...10000)) 人正在使用此功能"
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.textAlignment = .center
return label
}()
lazy var marqueeScrollView: UIScrollView = {
let sv = UIScrollView()
sv.backgroundColor = .clear
sv.showsHorizontalScrollIndicator = false
sv.isScrollEnabled = false
return sv
}()
lazy var searchInputView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#EFF9FF")
view.cornerRadius = 10
let titleLab = UILabel()
titleLab.text = "TA在哪输入号码就知道"
titleLab.font = .systemFont(ofSize: 16, weight: .semibold)
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
view.addSubview(titleLab)
titleLab.layoutChain
.top(18)
.left(15)
let inputView = UIView()
inputView.backgroundColor = .white
inputView.cornerRadius = 4
view.addSubview(inputView)
inputView.layoutChain
.topToBottomOfView(titleLab, offset: 20)
.edgesHorzontal(15)
.height(40)
let contactsBtn = UIButton()
contactsBtn.setTitle(" 通讯录导入", for: .normal)
contactsBtn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
contactsBtn.titleLabel?.font = .systemFont(ofSize: 12, weight: .regular)
contactsBtn.setImage(UIImage(named: "SearchLocation/contacts"), for: .normal)
contactsBtn.extendEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 15)
contactsBtn.rx.tap.subscribe(onNext: { _ in
//TODO: -
}).disposed(by: disposeBag)
inputView.addSubview(contactsBtn)
contactsBtn.layoutChain
.right(15)
.width(90)
.edgesVertical()
contactsBtn.sizeToFit()
inputView.addSubview(phoneInputTF)
phoneInputTF.layoutChain
.edgesVertical(10)
.left(15)
.rightToLeftOfView(contactsBtn, offset: -8)
let tipsLab = UILabel()
tipsLab.text = "我们不会存储和泄露你导入的通讯录隐私"
tipsLab.font = .systemFont(ofSize: 12, weight: .regular)
tipsLab.textColor = ThemeManager.shared.color.contentColor
view.addSubview(tipsLab)
tipsLab.layoutChain
.topToBottomOfView(inputView, offset: 6)
.right(15)
view.addSubview(searchBtn)
searchBtn.layoutChain
.topToBottomOfView(tipsLab, offset: 35)
.edgesHorzontal(52)
.height(50)
.bottom(30)
return view
}()
lazy var phoneInputTF: UITextField = {
let textField = UITextField()
textField.font = .systemFont(ofSize: 14, weight: .medium)
textField.placeholder = "请输入要查找的号码"
textField.keyboardType = .numberPad
textField.returnKeyType = .done
return textField
}()
lazy var searchBtn: 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
}()
lazy var searchProgressView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.8)
view.cornerRadius = 16
view.borderWidth = 0.5
view.borderColor = .white
view.isHidden = true
view.addSubview(searchPhoneLottieView)
searchPhoneLottieView.layoutChain
.top(30)
.left(15)
.width(120)
.heightToWidth(1)
let steps = ["分析用户号码", "正在找 CGI 蜂窝参数", "SS7 信息交流", "号码已获授权", "处理完成"]
var prevStepView: UIView?
for (i, text) in steps.enumerated() {
let stepView = UIView()
view.addSubview(stepView)
let icon = UIImageView(image: UIImage(named: "SearchLocation/done_off"))
icon.contentMode = .scaleAspectFit
stepView.addSubview(icon)
progressIcons.append(icon)
let label = UILabel()
label.text = text
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = .white
stepView.addSubview(label)
icon.layoutChain
.left().centerY()
.width(18).height(18)
label.layoutChain
.leftToRightOfView(icon, offset: 8)
.centerY().right()
stepView.layoutChain
.leftToRightOfView(searchPhoneLottieView, offset: 10)
.right(10)
.height(26)
if let prev = prevStepView {
stepView.layoutChain.topToBottomOfView(prev, offset: 4)
} else {
stepView.layoutChain.top(20)
}
prevStepView = stepView
}
// +
progressBar.layer.cornerRadius = 4
progressBar.clipsToBounds = true
progressBar.trackTintColor = .white.withAlphaComponent(0.2)
progressBar.progressTintColor = UIColor(hexStr: "#16B3FF")
view.addSubview(progressBar)
progressLab.textColor = .white
progressLab.font = .systemFont(ofSize: 12, weight: .medium)
progressLab.textAlignment = .right
view.addSubview(progressLab)
progressBar.layoutChain
.topToBottomOfView(prevStepView!, offset: 15)
.left(20)
.height(8)
progressLab.layoutChain
.leftToRightOfView(progressBar, offset: 8)
.centerY(progressBar)
.right(20)
return view
}()
private var progressIcons: [UIImageView] = []
private lazy var progressBar: UIProgressView = {
let p = UIProgressView()
return p
}()
private lazy var progressLab: UILabel = {
let l = UILabel()
return l
}()
/// (0~1)
func updateVideoProgress(_ progress: Float) {
let clamped = max(0, min(1, progress))
progressBar.progress = clamped
progressLab.text = "\(Int(clamped * 100))%"
let stepCount = progressIcons.count
let stepProgress = 1.0 / Float(stepCount)
for (i, icon) in progressIcons.enumerated() {
let stepStart = stepProgress * Float(i)
icon.image = UIImage(named: clamped >= stepStart + stepProgress * 0.5 ? "SearchLocation/done" : "SearchLocation/done_off")
}
}
lazy var searchPhoneLottieView: LottieAnimationView = {
let view = LottieAnimationView(name: "phone_search_interlude")
view.loopMode = .loop
return view
}()
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .white
setupUI()
setupRx()
setupMarquee()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}