EmotionBoardView.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. //
  2. // EmotionBoardView.swift
  3. // TUILiveKit
  4. //
  5. // Created by krabyu on 2024/4/7.
  6. //
  7. import Foundation
  8. import UIKit
  9. protocol EmotionBoardViewDelegate: AnyObject {
  10. func emotionView(emotionBoardView: EmotionBoardView, didSelectEmotion emotion: Emotion, atIndex index: Int)
  11. func emotionViewDidSelectDeleteButton(emotionBoardView: EmotionBoardView)
  12. }
  13. protocol EmotionPageViewDelegate: AnyObject {
  14. func emotionPageView(emotionPageView: EmotionPageView, didSelectEmotion emotion: Emotion, atIndex index: Int)
  15. func emotionPageViewDidLayoutEmotions(emotionPageView: EmotionPageView)
  16. }
  17. class EmotionPageView: UIView {
  18. private let buttonWidth = 35
  19. private let buttonHeight = 30
  20. weak var delegate: EmotionPageViewDelegate?
  21. let emotionSelectedBackgroundView: UIView = {
  22. let view = UIView()
  23. view.isUserInteractionEnabled = false
  24. view.backgroundColor = UIColor(red: 0 / 255.0, green: 0 / 255.0, blue: 0 / 255.0, alpha: 0.16)
  25. view.layer.cornerRadius = 3
  26. view.alpha = 0
  27. return view
  28. }()
  29. var deleteButton: UIButton = UIButton()
  30. var deleteButtonOffset: CGPoint = CGPointZero
  31. var emotionLayers: [CALayer] = []
  32. var emotions: [Emotion] = []
  33. // Record the rect of the clickable area of all emoticons in the current pageView, updated in drawRect: and used in tap events
  34. var emotionHittingRects: [NSValue] = []
  35. var padding = UIEdgeInsets()
  36. var numberOfRows: Int = 4
  37. // The size of the drawing area for each expression
  38. var emotionSize = CGSize(width: 30, height: 30)
  39. var emotionSelectedBackgroundExtension = UIEdgeInsets()
  40. var minimumEmotionHorizontalSpacing: CGFloat = 16
  41. var needsLayoutEmotions: Bool = true
  42. var previousLayoutFrame = CGRect()
  43. override init(frame: CGRect) {
  44. super.init(frame: frame)
  45. backgroundColor = UIColor(red: 34 / 255.0, green: 38 / 255.0, blue: 46 / 255.0, alpha: 1)
  46. addSubview(emotionSelectedBackgroundView)
  47. let tap = UITapGestureRecognizer(target: self, action: #selector(handleTapGestureRecognizer))
  48. addGestureRecognizer(tap)
  49. }
  50. required init?(coder: NSCoder) {
  51. fatalError("init(coder:) has not been implemented")
  52. }
  53. private func frameForDeleteButton(deleteButton: UIView) -> CGRect {
  54. var rect = deleteButton.frame
  55. let x = CGRectGetWidth(bounds) - padding.right - CGRectGetWidth(deleteButton.frame) -
  56. (emotionSize.width - CGRectGetWidth(deleteButton.frame)) / 2.0 + deleteButtonOffset.x
  57. let y = CGRectGetHeight(bounds) - padding.bottom - CGRectGetHeight(deleteButton.frame) -
  58. (emotionSize.height - CGRectGetHeight(deleteButton.frame)) / 2.0 + deleteButtonOffset.y
  59. rect.origin = CGPoint(x: x, y: y)
  60. return rect
  61. }
  62. override func layoutSubviews() {
  63. super.layoutSubviews()
  64. if deleteButton.superview == self {
  65. // The Delete button must be placed in the position of the last expression,
  66. // and is centered left and right above and below the expression
  67. deleteButton.frame = frameForDeleteButton(deleteButton: deleteButton)
  68. }
  69. let isSizeChanged = !CGSizeEqualToSize(previousLayoutFrame.size, frame.size)
  70. previousLayoutFrame = frame
  71. if isSizeChanged {
  72. setNeedsLayoutEmotions()
  73. }
  74. layoutEmotionsIfNeeded()
  75. }
  76. func setNeedsLayoutEmotions() {
  77. needsLayoutEmotions = true
  78. }
  79. func setEmotions(emotions: [Emotion]) {
  80. if self.emotions == emotions { return }
  81. self.emotions = emotions
  82. setNeedsLayoutEmotions()
  83. setNeedsLayout()
  84. }
  85. func layoutEmotionsIfNeeded() {
  86. if !needsLayoutEmotions { return }
  87. needsLayoutEmotions = false
  88. emotionHittingRects.removeAll()
  89. let contentSize = bounds.inset(by: padding).size
  90. let emotionCountPerRow = (contentSize.width + minimumEmotionHorizontalSpacing) / (emotionSize.width + minimumEmotionHorizontalSpacing)
  91. let emotionHorizontalSpacing = (contentSize.width - emotionCountPerRow * emotionSize.width) / (emotionCountPerRow - 1)
  92. let emotionVerticalSpacing = Int(contentSize.height - CGFloat(numberOfRows) * emotionSize.height) / Int(numberOfRows - 1)
  93. emotionSelectedBackgroundExtension = UIEdgeInsets(top: CGFloat(-emotionVerticalSpacing) / 2,
  94. left: -emotionHorizontalSpacing / 2,
  95. bottom: CGFloat(-emotionVerticalSpacing) / 2,
  96. right: -emotionHorizontalSpacing / 2)
  97. var emotionOrigin = CGPointZero
  98. let emotionCount = emotions.count
  99. for i in stride(from: 0, to: emotionCount, by: 1) {
  100. var emotionLayer: CALayer
  101. if i < emotionLayers.count {
  102. emotionLayer = emotionLayers[i]
  103. } else {
  104. emotionLayer = CALayer()
  105. emotionLayer.contentsScale = UIScreen.main.scale
  106. emotionLayers.append(emotionLayer)
  107. layer.addSublayer(emotionLayer)
  108. }
  109. emotionLayer.contents = emotions[i].image.cgImage
  110. let row = i / Int(emotionCountPerRow)
  111. emotionOrigin.x = padding.left + (emotionSize.width + emotionHorizontalSpacing) * CGFloat(i % Int(emotionCountPerRow))
  112. emotionOrigin.y = padding.top + (emotionSize.height + CGFloat(emotionVerticalSpacing)) * CGFloat(row)
  113. let emotionRect = CGRect(x: emotionOrigin.x, y: emotionOrigin.y, width: emotionSize.width, height: emotionSize.height)
  114. let emotionHittingRect = emotionRect.inset(by: emotionSelectedBackgroundExtension)
  115. emotionHittingRects.append(NSValue(cgRect: emotionHittingRect))
  116. emotionLayer.frame = emotionRect
  117. emotionLayer.isHidden = false
  118. }
  119. if emotionLayers.count > emotionCount {
  120. for i in emotionLayers.count - emotionCount ..< emotionLayers.count {
  121. emotionLayers[i].isHidden = true
  122. }
  123. }
  124. delegate?.emotionPageViewDidLayoutEmotions(emotionPageView: self)
  125. }
  126. @objc func handleTapGestureRecognizer(_ gestureRecognizer: UITapGestureRecognizer) {
  127. let location = gestureRecognizer.location(in: self)
  128. for i in 0 ..< emotionHittingRects.count {
  129. let rect = emotionHittingRects[i].cgRectValue
  130. if rect.contains(location) {
  131. let layer = emotionLayers[i]
  132. if layer.opacity < 0.2 { return }
  133. let emotion = emotions[i]
  134. emotionSelectedBackgroundView.frame = rect
  135. UIView.animate(withDuration: 0.08, animations: { [weak self] in
  136. guard let self = self else { return }
  137. self.emotionSelectedBackgroundView.alpha = 1
  138. }, completion: { [weak self] _ in
  139. guard let self = self else { return }
  140. UIView.animate(withDuration: 0.08, animations: {
  141. self.emotionSelectedBackgroundView.alpha = 0
  142. }, completion: nil)
  143. })
  144. delegate?.emotionPageView(emotionPageView: self, didSelectEmotion: emotion, atIndex: i)
  145. return
  146. }
  147. }
  148. }
  149. func verticalSizeThatFits(size: CGSize, emotionVerticalSpacing: CGFloat) -> CGSize {
  150. let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
  151. let contentSize = rect.inset(by: padding).size
  152. let emotionCountPerRow = (contentSize.width + minimumEmotionHorizontalSpacing) / (emotionSize.width + minimumEmotionHorizontalSpacing)
  153. let row = ceil(CGFloat(emotions.count) / (emotionCountPerRow * 1.0))
  154. let height = (emotionSize.height + emotionVerticalSpacing) * row - emotionVerticalSpacing + (padding.top + padding.bottom)
  155. return CGSize(width: size.width, height: height)
  156. }
  157. func updateDeleteButton(deleteButton: UIButton) {
  158. self.deleteButton = deleteButton
  159. addSubview(deleteButton)
  160. }
  161. func setDeleteButtonOffset(deleteButtonOffset: CGPoint) {
  162. self.deleteButtonOffset = deleteButtonOffset
  163. setNeedsLayout()
  164. }
  165. }
  166. class EmotionVerticalScrollView: UIScrollView {
  167. let pageView: EmotionPageView = {
  168. let pageView = EmotionPageView()
  169. pageView.deleteButton.isHidden = true
  170. return pageView
  171. }()
  172. override init(frame: CGRect) {
  173. super.init(frame: frame)
  174. addSubview(pageView)
  175. }
  176. required init?(coder: NSCoder) {
  177. fatalError("init(coder:) has not been implemented")
  178. }
  179. func setEmotions(emotions: [Emotion],
  180. emotionSize: CGSize,
  181. minimumEmotionHorizontalSpacing: CGFloat,
  182. emotionVerticalSpacing: CGFloat,
  183. emotionSelectedBackgroundExtension: UIEdgeInsets,
  184. paddingInPage: UIEdgeInsets) {
  185. let pageView = self.pageView
  186. pageView.emotions = emotions
  187. pageView.padding = paddingInPage
  188. let contentSize = CGSize(width: bounds.size.width - edgeInsetsGetHorizontalValue(insets: paddingInPage),
  189. height: bounds.size.height - edgeInsetsGetVerticalValue(insets: paddingInPage))
  190. let emotionCountPerRow = (contentSize.width + minimumEmotionHorizontalSpacing) / (emotionSize.width + minimumEmotionHorizontalSpacing)
  191. pageView.numberOfRows = Int(ceil(CGFloat(emotions.count) / emotionCountPerRow))
  192. pageView.emotionSize = emotionSize
  193. pageView.emotionSelectedBackgroundExtension = emotionSelectedBackgroundExtension
  194. pageView.minimumEmotionHorizontalSpacing = minimumEmotionHorizontalSpacing
  195. pageView.setNeedsLayout()
  196. let size = pageView.verticalSizeThatFits(size: bounds.size, emotionVerticalSpacing: emotionVerticalSpacing)
  197. self.pageView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
  198. self.contentSize = size
  199. }
  200. func adjustEmotionsAlpha(withFloatingRect floatingRect: CGRect) {
  201. let contentSize = CGSize(width: contentSize.width - edgeInsetsGetHorizontalValue(insets: pageView.padding),
  202. height: contentSize.height - edgeInsetsGetVerticalValue(insets: pageView.padding))
  203. let emotionCountPerRow = (contentSize.width + pageView.minimumEmotionHorizontalSpacing) /
  204. ((pageView.emotionSize.width) + (pageView.minimumEmotionHorizontalSpacing))
  205. let emotionVerticalSpacing = Int(contentSize.height - CGFloat(pageView.numberOfRows) *
  206. (pageView.emotionSize.height)) / Int(CGFloat(pageView.numberOfRows) - 1)
  207. let emotionHorizontalSpacing = (contentSize.width - emotionCountPerRow * (pageView.emotionSize.width)) / (emotionCountPerRow - 1)
  208. let columnIndexLeft = ceil((floatingRect.origin.x - (pageView.padding.left)) / ((pageView.emotionSize.width) +
  209. emotionHorizontalSpacing)) - 1
  210. let columnIndexRight = emotionCountPerRow - 1
  211. let rowIndexTop = ((floatingRect.origin.y - (pageView.padding.top)) / ((pageView.emotionSize.height) +
  212. CGFloat(emotionVerticalSpacing))) - 1
  213. for i in 0 ..< pageView.emotionLayers.count {
  214. let row = i / Int(emotionCountPerRow)
  215. let column = i % Int(emotionCountPerRow)
  216. CATransaction.begin()
  217. CATransaction.setDisableActions(true)
  218. if column >= Int(columnIndexLeft) && column <= Int(columnIndexRight) && row > Int(rowIndexTop) {
  219. if row == Int(ceil(rowIndexTop)) {
  220. let intersectAreaHeight = floatingRect.origin.y - pageView.emotionLayers[i].frame.origin.y
  221. let percent = intersectAreaHeight / pageView.emotionSize.height
  222. pageView.emotionLayers[i].opacity = Float(percent * percent)
  223. } else {
  224. pageView.emotionLayers[i].opacity = 0
  225. }
  226. } else {
  227. pageView.emotionLayers[i].opacity = 1.0
  228. }
  229. CATransaction.commit()
  230. }
  231. }
  232. func edgeInsetsGetVerticalValue(insets: UIEdgeInsets) -> CGFloat {
  233. return insets.top + insets.bottom
  234. }
  235. func edgeInsetsGetHorizontalValue(insets: UIEdgeInsets) -> CGFloat {
  236. return insets.left + insets.right
  237. }
  238. }
  239. class EmotionBoardView: UIView {
  240. private let buttonWidth = 35
  241. private let buttonHeight = 30
  242. var emotions: [Emotion] = []
  243. weak var delegate: EmotionBoardViewDelegate?
  244. var deleteButtonMargins = UIEdgeInsets(top: 0, left: 0, bottom: 18, right: 18)
  245. var pagedEmotions: [Emotion] = []
  246. let emotionVerticalSpacing = 16
  247. let paddingInPage = UIEdgeInsets(top: 18, left: 18, bottom: 65, right: 18)
  248. let numberOfRowsPerPage: Int = 4
  249. let emotionSize = CGSize(width: 34, height: 34)
  250. let emotionSelectedBackgroundExtension = UIEdgeInsets(top: -3, left: -3, bottom: -3, right: 03)
  251. let minimumEmotionHorizontalSpacing: CGFloat = 16
  252. let deleteButtonOffset: CGPoint = CGPointZero
  253. let pageControlMarginBottom: CGFloat = 22
  254. lazy var verticalScrollView: EmotionVerticalScrollView = {
  255. let scrollView = EmotionVerticalScrollView()
  256. scrollView.contentInsetAdjustmentBehavior = .never
  257. scrollView.delegate = self
  258. return scrollView
  259. }()
  260. lazy var deleteButton: UIButton = {
  261. let button = UIButton()
  262. button.setImage(UIImage(named: "room_floatchat_delete", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
  263. button.addTarget(self, action: #selector(didSelectDeleteButton), for: .touchUpInside)
  264. button.layer.cornerRadius = 4
  265. button.backgroundColor = .white
  266. return button
  267. }()
  268. lazy var topLineView: UIView = {
  269. let view = UIView()
  270. view.frame = CGRect(x: 0, y: 0, width: CGRectGetWidth(bounds), height: 1 / UIScreen.main.scale)
  271. view.backgroundColor = UIColor(red: 34 / 255.0, green: 38 / 255.0, blue: 46 / 255.0, alpha: 1)
  272. return view
  273. }()
  274. override init(frame: CGRect) {
  275. super.init(frame: frame)
  276. didInitialized(withFrame: frame)
  277. }
  278. required init?(coder: NSCoder) {
  279. super.init(coder: coder)
  280. didInitialized(withFrame: CGRectZero)
  281. }
  282. func didInitialized(withFrame frame: CGRect) {
  283. addSubview(verticalScrollView)
  284. addSubview(deleteButton)
  285. addSubview(topLineView)
  286. }
  287. func setEmotions(emotions: [Emotion]) {
  288. self.emotions = emotions
  289. setNeedsLayout()
  290. }
  291. override func layoutSubviews() {
  292. super.layoutSubviews()
  293. deleteButton.frame = CGRect(x: Int(Double(Int(bounds.width) - Int(deleteButtonMargins.right) - buttonWidth)),
  294. y: Int(Double(bounds.height - safeAreaInsets.bottom) - deleteButtonMargins.bottom) - buttonHeight,
  295. width: buttonWidth, height: buttonHeight)
  296. var paddingInPage = paddingInPage
  297. paddingInPage.bottom = paddingInPage.bottom + safeAreaInsets.bottom
  298. let verticalScrollViewFrame = bounds.inset(by: .zero)
  299. verticalScrollView.frame = verticalScrollViewFrame
  300. verticalScrollView.setEmotions(emotions: emotions,
  301. emotionSize: emotionSize,
  302. minimumEmotionHorizontalSpacing: minimumEmotionHorizontalSpacing,
  303. emotionVerticalSpacing: CGFloat(emotionVerticalSpacing),
  304. emotionSelectedBackgroundExtension: emotionSelectedBackgroundExtension,
  305. paddingInPage: paddingInPage)
  306. verticalScrollView.pageView.delegate = self
  307. topLineView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 1 / UIScreen.main.scale)
  308. }
  309. func adjustEmotionsAlpha() {
  310. let x = deleteButton.frame.origin.x
  311. let y = deleteButton.frame.origin.y
  312. let width = deleteButton.frame.maxX
  313. let height = deleteButton.frame.maxY - deleteButton.frame.minY
  314. let buttonGroupRect = CGRect(x: x, y: y, width: width, height: height)
  315. let floatingRect = verticalScrollView.convert(buttonGroupRect, from: self)
  316. verticalScrollView.adjustEmotionsAlpha(withFloatingRect: floatingRect)
  317. }
  318. @objc func didSelectDeleteButton() {
  319. delegate?.emotionViewDidSelectDeleteButton(emotionBoardView: self)
  320. }
  321. }
  322. extension EmotionBoardView: EmotionPageViewDelegate {
  323. func emotionPageView(emotionPageView: EmotionPageView, didSelectEmotion emotion: Emotion, atIndex index: Int) {
  324. let index = emotions.firstIndex(of: emotion) ?? -1
  325. delegate?.emotionView(emotionBoardView: self, didSelectEmotion: emotion, atIndex: index)
  326. }
  327. func emotionPageViewDidLayoutEmotions(emotionPageView: EmotionPageView) {
  328. adjustEmotionsAlpha()
  329. }
  330. }
  331. extension EmotionBoardView: UIScrollViewDelegate {
  332. func scrollViewDidScroll(_ scrollView: UIScrollView) {
  333. if scrollView == verticalScrollView {
  334. adjustEmotionsAlpha()
  335. }
  336. }
  337. }