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

388 lines
13 KiB
Swift

//
// SearchLocationView.swift
// QuickLocation
//
import UIKit
import RxSwift
import RxCocoa
import AVFoundation
import Lottie
import Contacts
import ContactsUI
import URLNavigator
class SearchLocationView: UIView {
var disposeBag = DisposeBag()
let onVideoComplete = PublishSubject<Void>()
private var player: AVPlayer?
private var timeObserver: Any?
let headerView = SearchLocationHeaderView()
private func setupRx() {
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 { $0.count == 11 }
.bind(to: searchBtn.rx.isEnabled)
.disposed(by: disposeBag)
phoneInputTF.rx.controlEvent(.editingDidEndOnExit)
.subscribe(onNext: { [weak self] in
self?.phoneInputTF.resignFirstResponder()
})
.disposed(by: disposeBag)
}
private func setupUI() {
addSubview(navBgView)
addSubview(navView)
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)
navView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
scrollView.layoutChain
.topToBottomOfView(navView)
.edges(excludingEdge: .top)
}
func startMarqueeAnimation() {
headerView.startMarqueeAnimation()
}
override func willMove(toWindow newWindow: UIWindow?) {
super.willMove(toWindow: newWindow)
if newWindow != nil {
startMarqueeAnimation()
}
}
lazy var navBgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill
iv.isHidden = true
return iv
}()
lazy var navView: BaseNavigationView = {
BaseNavigationView(title: " ")
}()
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)
contentView.addSubview(headerView)
headerView.layoutChain
.top(12)
.edgesHorzontal()
.widthToView(contentView)
contentView.addSubview(searchInputView)
searchInputView.layoutChain
.topToBottomOfView(headerView, 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 searchInputView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#EFFAFF")
view.cornerRadius = 40
let inputView = UIView()
inputView.backgroundColor = .white
inputView.cornerRadius = 14
view.addSubview(inputView)
inputView.layoutChain
.top(20)
.edgesHorzontal(15)
.height(44)
let contactsBtn = UIButton()
contactsBtn.setTitle("通讯录导入", for: .normal)
contactsBtn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
contactsBtn.titleLabel?.font = .systemFont(ofSize: 12, weight: .regular)
contactsBtn.rx.tap.subscribe(onNext: { [weak self] in
guard let self = self else { return }
self.phoneInputTF.resignFirstResponder()
let status = CNContactStore.authorizationStatus(for: .contacts)
switch status {
case .notDetermined:
CNContactStore().requestAccess(for: .contacts) { granted, _ in
DispatchQueue.main.async { if granted { self.presentContactPicker() } }
}
case .authorized:
self.presentContactPicker()
case .denied, .restricted:
DLToast.show(text: "请在设置中开启通讯录权限")
default:
break
}
}).disposed(by: disposeBag)
inputView.addSubview(contactsBtn)
contactsBtn.layoutChain.edgesVertical().width(60).right(12)
inputView.addSubview(phoneInputTF)
phoneInputTF.layoutChain
.edgesVertical(10)
.left(15)
.rightToLeftOfView(contactsBtn, offset: -8)
let warningIcon = UIImageView(image: UIImage(named: "SearchLocation/warning"))
warningIcon.contentMode = .scaleAspectFit
view.addSubview(warningIcon)
warningIcon.layoutChain
.topToBottomOfView(inputView, offset: 10)
.left(15)
.width(14)
.height(14)
let tipsLab = UILabel()
tipsLab.text = "我们不会存储和泄露你导入的通讯录隐私"
tipsLab.font = .systemFont(ofSize: 12, weight: .regular)
tipsLab.textColor = UIColor(hexStr: "#999999")
view.addSubview(tipsLab)
tipsLab.layoutChain
.leftToRightOfView(warningIcon, offset: 4)
.centerY(warningIcon)
.right(15)
view.addSubview(searchBtn)
searchBtn.layoutChain
.topToBottomOfView(warningIcon, offset: 24)
.edgesHorzontal(20)
.height(50)
.bottom(24)
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 = FontManager.boboBold(16)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 20
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 text in steps {
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 = {
UIProgressView()
}()
private lazy var progressLab: UILabel = {
UILabel()
}()
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()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func presentContactPicker() {
let picker = CNContactPickerViewController()
picker.delegate = self
picker.displayedPropertyKeys = [CNContactPhoneNumbersKey]
UIViewController.topMost?.present(picker, animated: true)
}
}
extension SearchLocationView: CNContactPickerDelegate {
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
if let phone = contact.phoneNumbers.first?.value.stringValue {
phoneInputTF.text = phone.filter { $0.isNumber }
phoneInputTF.sendActions(for: .editingChanged)
}
}
}