LNRoomOrderGuideView.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. //
  2. // LNRoomOrderGuideView.swift
  3. // Gami
  4. //
  5. // Created by OneeChan on 2026/3/22.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. class LNRoomOrderGuideView: UIView {
  11. static weak var joinView: UIView?
  12. static weak var mainSeatView: UIView?
  13. static weak var playmateView: UIView?
  14. static weak var orderCardView: UIView?
  15. private static weak var currentGuide: LNRoomOrderGuideView?
  16. private enum Step: Int, CaseIterable {
  17. case join
  18. case demand
  19. case playmate
  20. case order
  21. var cardSize: CGSize {
  22. switch self {
  23. case .join, .playmate, .order:
  24. .init(width: 314, height: self == .join ? 136 : 120)
  25. case .demand:
  26. .init(width: 320, height: 136)
  27. }
  28. }
  29. var focusInset: UIEdgeInsets {
  30. switch self {
  31. case .join:
  32. .init(top: -4, left: -4, bottom: -4, right: -4)
  33. case .demand:
  34. .init(top: -8, left: -4, bottom: -8, right: -4)
  35. case .playmate:
  36. .init(top: -16, left: -4, bottom: -16, right: -4)
  37. case .order:
  38. .init(top: -2, left: -2, bottom: -2, right: -2)
  39. }
  40. }
  41. var focusCornerRadius: CGFloat {
  42. switch self {
  43. case .join:
  44. 15
  45. case .demand, .playmate:
  46. 12
  47. case .order:
  48. 12
  49. }
  50. }
  51. var progressText: String {
  52. "(\(rawValue + 1)/\(Self.allCases.count))"
  53. }
  54. }
  55. private let dimLayer = CAShapeLayer()
  56. private let lineLayer = CAShapeLayer()
  57. private let dotView = UIView()
  58. private let dotInnerView = UIView()
  59. private let cardView = UIView()
  60. private let cardGlowView = UIImageView(image: .primary_6)
  61. private let titleLabel = UILabel()
  62. private let descLabel = UILabel()
  63. private let nextButton = UIButton(type: .custom)
  64. private let nextTitleLabel = UILabel()
  65. private let progressLabel = UILabel()
  66. private weak var profileCard: LNRoomProfileCardPanel?
  67. private var currentStep: Step = .join {
  68. didSet {
  69. updateTexts()
  70. setNeedsLayout()
  71. }
  72. }
  73. static func show(_ container: UIView) {
  74. guard currentGuide == nil else { return }
  75. let guide = LNRoomOrderGuideView()
  76. currentGuide = guide
  77. container.addSubview(guide)
  78. guide.snp.makeConstraints { make in
  79. make.edges.equalToSuperview()
  80. }
  81. }
  82. override init(frame: CGRect) {
  83. super.init(frame: frame)
  84. setupViews()
  85. updateTexts()
  86. }
  87. required init?(coder: NSCoder) {
  88. fatalError("init(coder:) has not been implemented")
  89. }
  90. override func layoutSubviews() {
  91. super.layoutSubviews()
  92. dimLayer.frame = bounds
  93. lineLayer.frame = bounds
  94. updateLayout()
  95. }
  96. }
  97. private extension LNRoomOrderGuideView {
  98. var currentAnchorView: UIView? {
  99. switch currentStep {
  100. case .join:
  101. Self.joinView
  102. case .demand:
  103. Self.mainSeatView
  104. case .playmate:
  105. Self.playmateView
  106. case .order:
  107. Self.orderCardView
  108. }
  109. }
  110. func setupViews() {
  111. backgroundColor = .clear
  112. dimLayer.fillColor = UIColor.black.withAlphaComponent(0.8).cgColor
  113. dimLayer.fillRule = .evenOdd
  114. layer.addSublayer(dimLayer)
  115. lineLayer.strokeColor = UIColor.fill.cgColor
  116. lineLayer.lineWidth = 1
  117. lineLayer.fillColor = UIColor.clear.cgColor
  118. layer.addSublayer(lineLayer)
  119. dotView.backgroundColor = UIColor.fill.withAlphaComponent(0.2)
  120. dotView.layer.cornerRadius = 7.5
  121. addSubview(dotView)
  122. dotInnerView.backgroundColor = .fill
  123. dotInnerView.layer.cornerRadius = 3.5
  124. dotView.addSubview(dotInnerView)
  125. dotInnerView.snp.makeConstraints { make in
  126. make.center.equalToSuperview()
  127. make.width.height.equalTo(7)
  128. }
  129. cardView.backgroundColor = .fill
  130. cardView.layer.cornerRadius = 20
  131. cardView.clipsToBounds = true
  132. addSubview(cardView)
  133. cardView.frame = .init(x: 0, y: 0, width: 50, height: 50)
  134. cardGlowView.alpha = 0.6
  135. cardGlowView.contentMode = .scaleToFill
  136. cardView.addSubview(cardGlowView)
  137. cardGlowView.snp.makeConstraints { make in
  138. make.edges.equalToSuperview()
  139. }
  140. titleLabel.font = .heading_h3
  141. titleLabel.textColor = .text_5
  142. titleLabel.numberOfLines = 0
  143. cardView.addSubview(titleLabel)
  144. titleLabel.snp.makeConstraints { make in
  145. make.top.equalToSuperview().offset(20)
  146. make.leading.equalToSuperview().offset(15)
  147. make.trailing.equalToSuperview().offset(-15)
  148. }
  149. descLabel.font = .body_m
  150. descLabel.textColor = .text_4
  151. descLabel.numberOfLines = 0
  152. cardView.addSubview(descLabel)
  153. descLabel.snp.makeConstraints { make in
  154. make.top.equalTo(titleLabel.snp.bottom).offset(8)
  155. make.leading.equalToSuperview().offset(15)
  156. make.trailing.equalToSuperview().offset(-15)
  157. }
  158. nextButton.setBackgroundImage(.primary_7, for: .normal)
  159. nextButton.layer.cornerRadius = 16.5
  160. nextButton.clipsToBounds = true
  161. nextButton.addAction(UIAction(handler: { [weak self] _ in
  162. self?.handleNext()
  163. }), for: .touchUpInside)
  164. cardView.addSubview(nextButton)
  165. nextButton.snp.makeConstraints { make in
  166. make.trailing.equalToSuperview().offset(-15)
  167. make.bottom.equalToSuperview().offset(-13)
  168. make.height.equalTo(33)
  169. }
  170. let buttonStack = UIStackView()
  171. buttonStack.axis = .horizontal
  172. buttonStack.spacing = 2
  173. buttonStack.alignment = .center
  174. buttonStack.isUserInteractionEnabled = false
  175. nextButton.addSubview(buttonStack)
  176. buttonStack.snp.makeConstraints { make in
  177. make.leading.equalToSuperview().offset(12)
  178. make.trailing.equalToSuperview().offset(-12)
  179. make.centerY.equalToSuperview()
  180. }
  181. nextTitleLabel.font = .heading_h3
  182. nextTitleLabel.textColor = .text_1
  183. buttonStack.addArrangedSubview(nextTitleLabel)
  184. progressLabel.font = .body_xs
  185. progressLabel.textColor = .text_1
  186. buttonStack.addArrangedSubview(progressLabel)
  187. }
  188. func updateTexts() {
  189. nextTitleLabel.text = .init(key: "A00373")
  190. progressLabel.text = currentStep.progressText
  191. switch currentStep {
  192. case .join:
  193. titleLabel.text = .init(key: "A00374")
  194. descLabel.attributedText = nil
  195. descLabel.text = .init(key: "A00375")
  196. case .demand:
  197. titleLabel.text = .init(key: "A00376")
  198. descLabel.attributedText = highlightedDemandText()
  199. case .playmate:
  200. titleLabel.text = .init(key: "A00381")
  201. descLabel.attributedText = nil
  202. descLabel.text = .init(key: "A00382")
  203. case .order:
  204. titleLabel.text = .init(key: "A00383")
  205. descLabel.attributedText = nil
  206. descLabel.text = .init(key: "A00384")
  207. }
  208. }
  209. func highlightedDemandText() -> NSAttributedString {
  210. let text = String(key: "A00378")
  211. let attributed = NSMutableAttributedString(
  212. string: text,
  213. attributes: [
  214. .font: UIFont.body_m,
  215. .foregroundColor: UIColor.text_4
  216. ]
  217. )
  218. [String(key: "A00379"), String(key: "A00380"), String(key: "A00385")].forEach { target in
  219. let nsText = text as NSString
  220. let range = nsText.range(of: target)
  221. if range.location != NSNotFound {
  222. attributed.addAttributes([
  223. .foregroundColor: UIColor.text_6
  224. ], range: range)
  225. }
  226. }
  227. return attributed
  228. }
  229. func updateLayout() {
  230. guard let focusRect = currentFocusRect() else {
  231. dimLayer.path = UIBezierPath(rect: bounds).cgPath
  232. lineLayer.path = nil
  233. dotView.isHidden = true
  234. cardView.isHidden = true
  235. return
  236. }
  237. cardView.isHidden = false
  238. dotView.isHidden = false
  239. let cardFrame = currentCardFrame(for: focusRect)
  240. cardView.frame = cardFrame
  241. let maskPath = UIBezierPath(rect: bounds)
  242. maskPath.append(UIBezierPath(roundedRect: focusRect, cornerRadius: currentStep.focusCornerRadius))
  243. dimLayer.path = maskPath.cgPath
  244. let isCardAbove = cardFrame.maxY <= focusRect.minY
  245. let dotCenter = CGPoint(
  246. x: currentStep == .demand ? focusRect.maxX - 42 : focusRect.midX,
  247. y: isCardAbove ? focusRect.minY - 7.5 : focusRect.maxY + 7.5
  248. )
  249. dotView.bounds = CGRect(x: 0, y: 0, width: 15, height: 15)
  250. dotView.center = dotCenter
  251. let path = UIBezierPath()
  252. path.move(to: CGPoint(x: dotCenter.x, y: isCardAbove ? cardFrame.maxY : cardFrame.minY))
  253. path.addLine(to: dotCenter)
  254. lineLayer.path = path.cgPath
  255. }
  256. func currentFocusRect() -> CGRect? {
  257. guard let anchor = currentAnchorView,
  258. anchor.window != nil else {
  259. return nil
  260. }
  261. let rect = anchor.convert(anchor.bounds, to: self)
  262. let inset = currentStep.focusInset
  263. let focusedRect = rect.inset(by: inset)
  264. let visibleRect = focusedRect.intersection(bounds)
  265. guard !visibleRect.isNull, !visibleRect.isEmpty else { return nil }
  266. return visibleRect
  267. }
  268. func currentCardFrame(for focusRect: CGRect) -> CGRect {
  269. let size = currentStep.cardSize
  270. switch currentStep {
  271. case .join:
  272. let x = bounds.width - 12 - size.width
  273. let y = focusRect.minY - 56 - size.height
  274. return .init(x: x, y: max(100, y), width: size.width, height: size.height)
  275. case .demand:
  276. let x = bounds.width - 12 - size.width
  277. return .init(x: x, y: focusRect.maxY + 61, width: size.width, height: size.height)
  278. case .playmate:
  279. return .init(x: (bounds.width - size.width) / 2, y: focusRect.maxY + 59, width: size.width, height: size.height)
  280. case .order:
  281. let y = focusRect.minY - 66 - size.height
  282. return .init(x: 10, y: max(100, y), width: size.width, height: size.height)
  283. }
  284. }
  285. func handleNext() {
  286. switch currentStep {
  287. case .join:
  288. currentStep = .demand
  289. case .demand:
  290. currentStep = .playmate
  291. case .playmate:
  292. showOrderStepIfPossible()
  293. case .order:
  294. dismiss()
  295. profileCard?.dismiss(animated: false)
  296. }
  297. }
  298. func showOrderStepIfPossible() {
  299. let panel = LNRoomProfileCardPanel()
  300. panel.toBeExample()
  301. panel.popup(superview, animated: false)
  302. superview?.bringSubviewToFront(self)
  303. profileCard = panel
  304. currentStep = .order
  305. }
  306. func dismiss() {
  307. if Self.currentGuide === self {
  308. Self.currentGuide = nil
  309. }
  310. UIView.animate(withDuration: 0.2, animations: {
  311. self.alpha = 0
  312. }, completion: { [weak self] _ in
  313. self?.removeFromSuperview()
  314. })
  315. }
  316. }