391 lines
13 KiB
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")
|
|
}
|
|
}
|