QSLPopView.swift 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. //
  2. // QSLPopView.swift
  3. // QuickSearchLocation
  4. //
  5. // Created by Destiny on 2024/12/4.
  6. //
  7. import UIKit
  8. class QSLPopView: UIView {
  9. private var items: [(image: UIImage?, title: String)] = []
  10. private let arrowView = UIView()
  11. private let tableView = UITableView()
  12. private var didSelectItem: ((Int) -> Void)?
  13. var offsetX: CGFloat = 0.0
  14. private let arrowWidth = 20.0
  15. private let radius = 8.0
  16. var arrowHeight: CGFloat = 10.0
  17. var rowHeight: CGFloat = 50.0
  18. var contentWidth: CGFloat = 200.0
  19. var alertBackgroundColor: UIColor? {
  20. didSet {
  21. arrowView.backgroundColor = alertBackgroundColor
  22. }
  23. }
  24. var textColor: UIColor?
  25. var popMaskViewBackgroundColor = UIColor.black.withAlphaComponent(0.0)
  26. // 新增属性,控制是否显示遮罩层
  27. var isShowMaskView: Bool = false
  28. // 遮罩层视图
  29. private var popMaskView: UIView?
  30. init(
  31. items: [(image: UIImage?, title: String)],
  32. didSelectItem: ((Int) -> Void)?
  33. ) {
  34. super.init(frame: .zero)
  35. alertBackgroundColor = .hexStringColor(hexString: "#4B4B4B", alpha: 0.95)
  36. textColor = .white
  37. self.items = items
  38. self.didSelectItem = didSelectItem
  39. setupView()
  40. }
  41. required init?(coder: NSCoder) {
  42. fatalError("init(coder:) has not been implemented")
  43. }
  44. private func setupView() {
  45. self.backgroundColor = .clear
  46. // self.layer.shadowColor = UIColor.black.cgColor
  47. // self.layer.shadowOpacity = 0.2
  48. // self.layer.shadowOffset = CGSize(width: 0, height: 2)
  49. // self.layer.shadowRadius = 4
  50. arrowView.backgroundColor = alertBackgroundColor
  51. addSubview(arrowView)
  52. tableView.backgroundColor = .clear
  53. tableView.delegate = self
  54. tableView.dataSource = self
  55. tableView.register(QSLPopViewCell.self, forCellReuseIdentifier: "QSLPopViewCell")
  56. tableView.separatorStyle = .none
  57. tableView.layer.cornerRadius = radius
  58. tableView.clipsToBounds = true
  59. tableView.isScrollEnabled = false
  60. addSubview(tableView)
  61. }
  62. override func layoutSubviews() {
  63. super.layoutSubviews()
  64. let tableHeight = CGFloat(items.count) * rowHeight
  65. tableView.frame = CGRect(x: 0, y: arrowHeight, width: contentWidth, height: tableHeight)
  66. updateArrowPosition()
  67. }
  68. private func updateArrowPosition() {
  69. let arrowCenterX = contentWidth - 20.rpx - offsetX
  70. arrowView.frame = CGRect(x: arrowCenterX - arrowWidth/2, y: 0, width: arrowWidth, height: arrowHeight)
  71. arrowView.layer.mask = arrowMaskLayer()
  72. }
  73. private func arrowMaskLayer() -> CAShapeLayer {
  74. let path = UIBezierPath()
  75. path.move(to: CGPoint(x: 0, y: arrowHeight))
  76. path.addLine(to: CGPoint(x: arrowWidth, y: arrowHeight))
  77. path.addLine(to: CGPoint(x: arrowWidth/2, y: 0))
  78. path.close()
  79. let shapeLayer = CAShapeLayer()
  80. shapeLayer.path = path.cgPath
  81. return shapeLayer
  82. }
  83. // 新增方法,添加遮罩层
  84. private func addMaskView(to targetSuperview: UIView) {
  85. popMaskView = UIView(frame: targetSuperview.bounds)
  86. popMaskView?.backgroundColor = popMaskViewBackgroundColor
  87. popMaskView?.isUserInteractionEnabled = true
  88. targetSuperview.addSubview(popMaskView!)
  89. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(maskViewTapped))
  90. popMaskView?.addGestureRecognizer(tapGesture)
  91. }
  92. // 遮罩层点击事件
  93. @objc private func maskViewTapped() {
  94. // hide()
  95. hide(animate: true)
  96. }
  97. // 显示弹窗
  98. func show(from button: UIButton, isWindow: Bool = false) {
  99. guard let superview = button.superview else { return }
  100. let maxOffsetX = (contentWidth / 2) - (arrowWidth / 2) - radius
  101. offsetX = min(max(-maxOffsetX, offsetX), maxOffsetX)
  102. let targetSuperview: UIView
  103. if isWindow {
  104. if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
  105. let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) {
  106. targetSuperview = keyWindow
  107. } else {
  108. targetSuperview = superview
  109. }
  110. } else {
  111. targetSuperview = superview
  112. }
  113. if isShowMaskView {
  114. addMaskView(to: targetSuperview) // 添加遮罩层
  115. }
  116. targetSuperview.addSubview(self)
  117. let buttonFrame = button.convert(button.bounds, to: targetSuperview)
  118. let originY = buttonFrame.maxY + 5
  119. var originX = buttonFrame.midX - contentWidth / 2 + offsetX
  120. let screenWidth = UIScreen.main.bounds.width
  121. if originX < 0 { originX = 0 }
  122. if originX + contentWidth > screenWidth {
  123. originX = screenWidth - contentWidth
  124. }
  125. self.frame = CGRect(x: originX, y: originY, width: contentWidth, height: CGFloat(items.count) * rowHeight + arrowHeight)
  126. updateArrowPosition()
  127. }
  128. func show(from button: UIButton, selfVC:UIViewController) {
  129. let maxOffsetX = (contentWidth / 2) - (arrowWidth / 2) - radius
  130. offsetX = min(max(-maxOffsetX, offsetX), maxOffsetX)
  131. let targetSuperview: UIView
  132. targetSuperview = selfVC.view
  133. if isShowMaskView {
  134. addMaskView(to: targetSuperview) // 添加遮罩层
  135. }
  136. targetSuperview.addSubview(self)
  137. let buttonFrame = button.convert(button.bounds, to: targetSuperview)
  138. let originY = buttonFrame.maxY + 5
  139. var originX = buttonFrame.midX - contentWidth / 2 + offsetX
  140. let screenWidth = UIScreen.main.bounds.width
  141. if originX < 0 { originX = 0 }
  142. if originX + contentWidth > screenWidth {
  143. originX = screenWidth - contentWidth
  144. }
  145. self.frame = CGRect(x: originX - 25.rpx, y: originY - 1.rpx, width: contentWidth, height: CGFloat(items.count) * rowHeight + arrowHeight)
  146. updateArrowPosition()
  147. }
  148. func hide(animate: Bool = false) {
  149. if animate {
  150. // 记录原始的frame
  151. let tempFrame = self.frame
  152. // 计算动态锚点位置
  153. let anchorX = calculateAnchorPointX(from: offsetX)
  154. // 设置锚点为动态计算值
  155. self.layer.anchorPoint = CGPoint(x: anchorX, y: 0)
  156. // 恢复原始的frame
  157. self.frame = tempFrame
  158. // 使用动画将视图缩小到右上角并隐藏
  159. UIView.animate(withDuration: 0.2, animations: {
  160. self.alpha = 0.0
  161. self.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
  162. }) { _ in
  163. // 动画结束后移除视图和遮罩层
  164. self.removeFromSuperview()
  165. self.popMaskView?.removeFromSuperview()
  166. // 恢复视图的初始状态
  167. self.alpha = 1.0
  168. self.transform = .identity
  169. self.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) // 恢复锚点为默认中心
  170. }
  171. } else {
  172. // 无动画,直接移除视图和遮罩层
  173. self.removeFromSuperview()
  174. self.popMaskView?.removeFromSuperview()
  175. self.alpha = 1.0
  176. }
  177. }
  178. func calculateAnchorPointX(from offsetX: CGFloat) -> CGFloat {
  179. // 计算合适的锚点X值
  180. let maxOffsetX = (contentWidth / 2) - (arrowWidth / 2) - radius
  181. let normalizedOffsetX = min(max(-maxOffsetX, offsetX), maxOffsetX) // 限制 offsetX 范围
  182. // 当offsetX为负时,锚点应该往右
  183. // 当offsetX为正时,锚点应该往左
  184. let anchorX = 0.5 - (normalizedOffsetX / contentWidth)
  185. return anchorX
  186. }
  187. }
  188. extension QSLPopView: UITableViewDelegate, UITableViewDataSource {
  189. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  190. return items.count
  191. }
  192. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  193. guard let cell = tableView.dequeueReusableCell(withIdentifier: "QSLPopViewCell", for: indexPath) as? QSLPopViewCell else {
  194. return UITableViewCell()
  195. }
  196. let item = items[indexPath.row]
  197. cell.selectionStyle = .none
  198. cell.backgroundColor = alertBackgroundColor
  199. cell.iconImageView.image = item.image
  200. cell.titleLabel.textColor = textColor
  201. cell.titleLabel.text = item.title
  202. return cell
  203. }
  204. func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  205. return rowHeight
  206. }
  207. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  208. didSelectItem?(indexPath.row)
  209. hide(animate: true)
  210. }
  211. }