jsdw_ios/QuickLocation/Section/Home/LockedAppPopView.swift

391 lines
13 KiB
Swift

//
// 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")
}
}