JCChatViewLayout.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. //
  2. // JCChatViewLayout.swift
  3. // JChat
  4. //
  5. // Created by deng on 2017/2/28.
  6. // Copyright © 2017年 HXHG. All rights reserved.
  7. //
  8. import UIKit
  9. @objc open class JCChatViewLayout: UICollectionViewFlowLayout {
  10. public override init() {
  11. super.init()
  12. _commonInit()
  13. }
  14. public required init?(coder aDecoder: NSCoder) {
  15. super.init(coder: aDecoder)
  16. _commonInit()
  17. }
  18. internal weak var _chatView: JCChatView?
  19. open override class var layoutAttributesClass: AnyClass {
  20. return JCChatViewLayoutAttributes.self
  21. }
  22. open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
  23. let attributes = super.layoutAttributesForItem(at: indexPath)
  24. if let attributes = attributes as? JCChatViewLayoutAttributes, attributes.info == nil {
  25. attributes.info = layoutAttributesInfoForItem(at: indexPath)
  26. }
  27. return attributes
  28. }
  29. open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
  30. let arr = super.layoutAttributesForElements(in: rect)
  31. arr?.forEach({
  32. guard let attributes = $0 as? JCChatViewLayoutAttributes, attributes.info == nil else {
  33. return
  34. }
  35. attributes.info = layoutAttributesInfoForItem(at: attributes.indexPath)
  36. })
  37. return arr
  38. }
  39. open func layoutAttributesInfoForItem(at indexPath: IndexPath) -> JCChatViewLayoutAttributesInfo? {
  40. guard let collectionView = collectionView, let _ = collectionView.delegate as? JCChatViewLayoutDelegate, let message = _message(at: indexPath) else {
  41. return nil
  42. }
  43. let size = CGSize(width: collectionView.frame.width, height: .greatestFiniteMagnitude)
  44. if let info = _allLayoutAttributesInfo[message.identifier] {
  45. // 这不是合理的做法
  46. if !message.updateSizeIfNeeded {
  47. return info
  48. }
  49. }
  50. let options = message.options
  51. var allRect: CGRect = .zero
  52. var allBoxRect: CGRect = .zero
  53. var cardRect: CGRect = .zero
  54. var cardBoxRect: CGRect = .zero
  55. var avatarRect: CGRect = .zero
  56. var avatarBoxRect: CGRect = .zero
  57. var bubbleRect: CGRect = .zero
  58. var bubbleBoxRect: CGRect = .zero
  59. var contentRect: CGRect = .zero
  60. var contentBoxRect: CGRect = .zero
  61. var tipsRect: CGRect = .zero
  62. var tipsBoxRect: CGRect = .zero
  63. // 计算的时候以左对齐为基准
  64. // +---------------------------------------+ r0
  65. // |+---------------------------------+ r1 |
  66. // ||+---+ <NAME> | |
  67. // ||| A | +---------------------\ r4 | |
  68. // ||+---+ |+---------------+ r5 | | |
  69. // || || CONTENT | | | |
  70. // || |+---------------+ | | |
  71. // || \---------------------/ | | +---+ r6
  72. // |+---------------------------------+ <-|- | ! |
  73. // +---------------------------------------+ +---+
  74. let edg0 = _inset(with: options.style, for: .all)
  75. var r0 = CGRect(x: 0, y: 0, width: size.width, height: .greatestFiniteMagnitude)
  76. var r1 = r0.inset(by: edg0)
  77. var x1 = r1.minX
  78. var y1 = r1.minY
  79. var x2 = r1.maxX
  80. var y2 = r1.maxY
  81. if options.showsAvatar {
  82. let edg = _inset(with: options.style, for: .avatar)
  83. let size = _size(with: options.style, for: .avatar)
  84. let box = CGRect(x: x1, y: y1, width: edg.left + size.width + edg.right, height: edg.top + size.height + edg.bottom)
  85. let rect = box.inset(by: edg)
  86. avatarRect = rect
  87. avatarBoxRect = box
  88. x1 = box.maxX
  89. }
  90. if options.showsCard {
  91. let edg = _inset(with: options.style, for: .card)
  92. let size = _size(with: options.style, for: .card)
  93. let box = CGRect(x: x1, y: y1, width: x2 - x1, height: edg.top + size.height + edg.bottom)
  94. let rect = box.inset(by: edg)
  95. cardRect = rect
  96. cardBoxRect = box
  97. y1 = box.maxY
  98. }
  99. if options.showsBubble {
  100. let edg = _inset(with: options.style, for: .bubble)
  101. let box = CGRect(x: x1, y: y1, width: x2 - x1, height: y2 - y1)
  102. let rect = box.inset(by: edg)
  103. bubbleRect = rect
  104. bubbleBoxRect = box
  105. x1 = rect.minX
  106. x2 = rect.maxX
  107. y1 = rect.minY
  108. y2 = rect.maxY
  109. }
  110. if true {
  111. let edg0 = _inset(with: options.style, for: .content)
  112. let edg1 = message.content.layoutMargins
  113. //
  114. let edg = UIEdgeInsets(top: edg0.top + edg1.top, left: edg0.left + edg1.left, bottom: edg0.bottom + edg1.bottom, right: edg0.right + edg1.right)
  115. var box = CGRect(x: x1, y: y1, width: x2 - x1, height: y2 - y1)
  116. var rect = box.inset(by: edg)
  117. // calc content size
  118. let size = message.content.sizeThatFits(rect.size)
  119. // restore offset
  120. box.size.width = edg.left + size.width + edg.right
  121. box.size.height = edg.top + size.height + edg.bottom
  122. rect.size.width = size.width
  123. rect.size.height = size.height
  124. contentRect = rect
  125. contentBoxRect = box
  126. x1 = box.maxX
  127. y1 = box.maxY
  128. }
  129. if options.showsBubble {
  130. let edg = _inset(with: options.style, for: .bubble)
  131. bubbleRect.size.width = contentBoxRect.width
  132. bubbleRect.size.height = contentBoxRect.height
  133. bubbleBoxRect.size.width = edg.left + contentBoxRect.width + edg.right
  134. bubbleBoxRect.size.height = edg.top + contentBoxRect.height + edg.bottom
  135. }
  136. if options.showsTips {
  137. let edg = _inset(with: options.style, for: .tips)
  138. let size = _size(with: options.style, for: .tips)
  139. let box = CGRect(x: x1 + 3, y: y1 - size.height - edg0.bottom, width: edg.left + size.width + edg.right, height: edg.top + size.height + edg.bottom)
  140. let rect = box.inset(by: edg)
  141. tipsRect = rect
  142. tipsBoxRect = box
  143. x1 = box.maxX
  144. }
  145. // adjust
  146. r1.size.width = x1 - r1.minX
  147. r1.size.height = y1 - r1.minY
  148. r0.size.width = x1
  149. r0.size.height = y1 + edg0.bottom
  150. allRect = r1
  151. allBoxRect = r0
  152. // algin
  153. switch options.alignment {
  154. case .right:
  155. // to right
  156. allRect.origin.x = size.width - allRect.maxX
  157. allBoxRect.origin.x = size.width - allBoxRect.maxX
  158. cardRect.origin.x = size.width - cardRect.maxX
  159. cardBoxRect.origin.x = size.width - cardBoxRect.maxX
  160. avatarRect.origin.x = size.width - avatarRect.maxX
  161. avatarBoxRect.origin.x = size.width - avatarBoxRect.maxX
  162. bubbleRect.origin.x = size.width - bubbleRect.maxX
  163. bubbleBoxRect.origin.x = size.width - bubbleBoxRect.maxX
  164. contentRect.origin.x = size.width - contentRect.maxX
  165. contentBoxRect.origin.x = size.width - contentBoxRect.maxX
  166. tipsRect.origin.x = size.width - tipsRect.maxX
  167. tipsBoxRect.origin.x = size.width - tipsBoxRect.maxX
  168. case .center:
  169. allRect.origin.x = (size.width - allRect.width) / 2
  170. allBoxRect.origin.x = (size.width - allBoxRect.width) / 2
  171. bubbleRect.origin.x = (size.width - bubbleRect.width) / 2
  172. bubbleBoxRect.origin.x = (size.width - bubbleBoxRect.width) / 2
  173. contentRect.origin.x = (size.width - contentRect.width) / 2
  174. contentBoxRect.origin.x = (size.width - contentBoxRect.width) / 2
  175. case .left:
  176. break
  177. }
  178. // save
  179. let rects: [JCChatViewLayoutItem: CGRect] = [
  180. .all: allRect,
  181. .card: cardRect,
  182. .avatar: avatarRect,
  183. .bubble: bubbleRect,
  184. .content: contentRect,
  185. .tips: tipsRect
  186. ]
  187. let boxRects: [JCChatViewLayoutItem: CGRect] = [
  188. .all: allBoxRect,
  189. .card: cardBoxRect,
  190. .avatar: avatarBoxRect,
  191. .bubble: bubbleBoxRect,
  192. .content: contentBoxRect,
  193. .tips: tipsBoxRect
  194. ]
  195. let info = JCChatViewLayoutAttributesInfo(message: message, size: size, rects: rects, boxRects: boxRects)
  196. _allLayoutAttributesInfo[message.identifier] = info
  197. return info
  198. }
  199. private func _size(with style: JCMessageStyle, for item: JCChatViewLayoutItem) -> CGSize {
  200. let key = "\(style.rawValue)-\(item.rawValue)"
  201. if let size = _cachedAllLayoutSize[key] {
  202. return size // hit cache
  203. }
  204. var size: CGSize?
  205. if let collectionView = collectionView, let delegate = collectionView.delegate as? JCChatViewLayoutDelegate {
  206. switch item {
  207. case .all: size = .zero
  208. case .card: size = delegate.collectionView?(collectionView, layout: self, sizeForItemCardOf: style)
  209. case .avatar: size = delegate.collectionView?(collectionView, layout: self, sizeForItemAvatarOf: style)
  210. case .bubble: size = .zero
  211. case .content: size = .zero
  212. case .tips: size = delegate.collectionView?(collectionView, layout: self, sizeForItemTipsOf: style)
  213. }
  214. }
  215. _cachedAllLayoutSize[key] = size ?? .zero
  216. return size ?? .zero
  217. }
  218. private func _inset(with style: JCMessageStyle, for item: JCChatViewLayoutItem) -> UIEdgeInsets {
  219. let key = "\(style.rawValue)-\(item.rawValue)"
  220. if let edg = _cachedAllLayoutInset[key] {
  221. return edg // hit cache
  222. }
  223. var edg: UIEdgeInsets?
  224. if let collectionView = collectionView, let delegate = collectionView.delegate as? JCChatViewLayoutDelegate {
  225. switch item {
  226. case .all: edg = delegate.collectionView?(collectionView, layout: self, insetForItemOf: style)
  227. case .card: edg = delegate.collectionView?(collectionView, layout: self, insetForItemCardOf: style)
  228. case .tips: edg = delegate.collectionView?(collectionView, layout: self, insetForItemTipsOf: style)
  229. case .avatar: edg = delegate.collectionView?(collectionView, layout: self, insetForItemAvatarOf: style)
  230. case .bubble: edg = delegate.collectionView?(collectionView, layout: self, insetForItemBubbleOf: style)
  231. case .content: edg = delegate.collectionView?(collectionView, layout: self, insetForItemContentOf: style)
  232. }
  233. }
  234. _cachedAllLayoutInset[key] = edg ?? .zero
  235. return edg ?? .zero
  236. }
  237. private func _message(at indexPath: IndexPath) -> JCMessageType? {
  238. guard let collectionView = collectionView, let delegate = collectionView.delegate as? JCChatViewLayoutDelegate else {
  239. return nil
  240. }
  241. return delegate.collectionView(collectionView, layout: self, itemAt: indexPath)
  242. }
  243. private func _commonInit() {
  244. minimumLineSpacing = 0
  245. minimumInteritemSpacing = 0
  246. }
  247. private lazy var _cachedAllLayoutSize: [String: CGSize] = [:]
  248. private lazy var _cachedAllLayoutInset: [String: UIEdgeInsets] = [:]
  249. private lazy var _allLayoutAttributesInfo: [UUID: JCChatViewLayoutAttributesInfo] = [:]
  250. }
  251. @objc public protocol JCChatViewLayoutDelegate: UICollectionViewDelegateFlowLayout {
  252. func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, itemAt indexPath: IndexPath) -> JCMessageType
  253. @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemCardOf style: JCMessageStyle) -> CGSize
  254. @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemTipsOf style: JCMessageStyle) -> CGSize
  255. @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAvatarOf style: JCMessageStyle) -> CGSize
  256. @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemOf style: JCMessageStyle) -> UIEdgeInsets
  257. @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemCardOf style: JCMessageStyle) -> UIEdgeInsets
  258. @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemTipsOf style: JCMessageStyle) -> UIEdgeInsets
  259. @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemAvatarOf style: JCMessageStyle) -> UIEdgeInsets
  260. @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemBubbleOf style: JCMessageStyle) -> UIEdgeInsets
  261. @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemContentOf style: JCMessageStyle) -> UIEdgeInsets
  262. }