MessageContentCell.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. /*
  2. MIT License
  3. Copyright (c) 2017-2019 MessageKit
  4. Permission is hereby granted, free of charge, to any person obtaining a copy
  5. of this software and associated documentation files (the "Software"), to deal
  6. in the Software without restriction, including without limitation the rights
  7. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. copies of the Software, and to permit persons to whom the Software is
  9. furnished to do so, subject to the following conditions:
  10. The above copyright notice and this permission notice shall be included in all
  11. copies or substantial portions of the Software.
  12. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  13. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  14. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  15. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  16. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  17. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  18. SOFTWARE.
  19. */
  20. import UIKit
  21. /// A subclass of `MessageCollectionViewCell` used to display text, media, and location messages.
  22. open class MessageContentCell: MessageCollectionViewCell {
  23. /// The image view displaying the avatar.
  24. open var avatarView = AvatarView()
  25. /// The container used for styling and holding the message's content view.
  26. open var messageContainerView: MessageContainerView = {
  27. let containerView = MessageContainerView()
  28. containerView.clipsToBounds = true
  29. containerView.layer.masksToBounds = true
  30. return containerView
  31. }()
  32. /// The top label of the cell.
  33. open var cellTopLabel: InsetLabel = {
  34. let label = InsetLabel()
  35. label.numberOfLines = 0
  36. label.textAlignment = .center
  37. return label
  38. }()
  39. /// The bottom label of the cell.
  40. open var cellBottomLabel: InsetLabel = {
  41. let label = InsetLabel()
  42. label.numberOfLines = 0
  43. label.textAlignment = .center
  44. return label
  45. }()
  46. /// The top label of the messageBubble.
  47. open var messageTopLabel: InsetLabel = {
  48. let label = InsetLabel()
  49. label.numberOfLines = 0
  50. return label
  51. }()
  52. /// The bottom label of the messageBubble.
  53. open var messageBottomLabel: InsetLabel = {
  54. let label = InsetLabel()
  55. label.numberOfLines = 0
  56. return label
  57. }()
  58. // Should only add customized subviews - don't change accessoryView itself.
  59. open var accessoryView: UIView = UIView()
  60. /// The `MessageCellDelegate` for the cell.
  61. open weak var delegate: MessageCellDelegate?
  62. public override init(frame: CGRect) {
  63. super.init(frame: frame)
  64. contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  65. setupSubviews()
  66. }
  67. required public init?(coder aDecoder: NSCoder) {
  68. super.init(coder: aDecoder)
  69. contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  70. setupSubviews()
  71. }
  72. open func setupSubviews() {
  73. contentView.addSubview(accessoryView)
  74. contentView.addSubview(cellTopLabel)
  75. contentView.addSubview(messageTopLabel)
  76. contentView.addSubview(messageBottomLabel)
  77. contentView.addSubview(cellBottomLabel)
  78. contentView.addSubview(messageContainerView)
  79. contentView.addSubview(avatarView)
  80. }
  81. open override func prepareForReuse() {
  82. super.prepareForReuse()
  83. cellTopLabel.text = nil
  84. cellBottomLabel.text = nil
  85. messageTopLabel.text = nil
  86. messageBottomLabel.text = nil
  87. }
  88. // MARK: - Configuration
  89. open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
  90. super.apply(layoutAttributes)
  91. guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return }
  92. // Call this before other laying out other subviews
  93. layoutMessageContainerView(with: attributes)
  94. layoutMessageBottomLabel(with: attributes)
  95. layoutCellBottomLabel(with: attributes)
  96. layoutCellTopLabel(with: attributes)
  97. layoutMessageTopLabel(with: attributes)
  98. layoutAvatarView(with: attributes)
  99. layoutAccessoryView(with: attributes)
  100. }
  101. /// Used to configure the cell.
  102. ///
  103. /// - Parameters:
  104. /// - message: The `MessageType` this cell displays.
  105. /// - indexPath: The `IndexPath` for this cell.
  106. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell is contained.
  107. open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
  108. guard let dataSource = messagesCollectionView.messagesDataSource else {
  109. fatalError(MessageKitError.nilMessagesDataSource)
  110. }
  111. guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
  112. fatalError(MessageKitError.nilMessagesDisplayDelegate)
  113. }
  114. delegate = messagesCollectionView.messageCellDelegate
  115. let messageColor = displayDelegate.backgroundColor(for: message, at: indexPath, in: messagesCollectionView)
  116. let messageStyle = displayDelegate.messageStyle(for: message, at: indexPath, in: messagesCollectionView)
  117. displayDelegate.configureAvatarView(avatarView, for: message, at: indexPath, in: messagesCollectionView)
  118. displayDelegate.configureAccessoryView(accessoryView, for: message, at: indexPath, in: messagesCollectionView)
  119. messageContainerView.backgroundColor = messageColor
  120. messageContainerView.style = messageStyle
  121. let topCellLabelText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath)
  122. let bottomCellLabelText = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath)
  123. let topMessageLabelText = dataSource.messageTopLabelAttributedText(for: message, at: indexPath)
  124. let bottomMessageLabelText = dataSource.messageBottomLabelAttributedText(for: message, at: indexPath)
  125. cellTopLabel.attributedText = topCellLabelText
  126. cellBottomLabel.attributedText = bottomCellLabelText
  127. messageTopLabel.attributedText = topMessageLabelText
  128. messageBottomLabel.attributedText = bottomMessageLabelText
  129. }
  130. /// Handle tap gesture on contentView and its subviews.
  131. open override func handleTapGesture(_ gesture: UIGestureRecognizer) {
  132. let touchLocation = gesture.location(in: self)
  133. switch true {
  134. case messageContainerView.frame.contains(touchLocation) && !cellContentView(canHandle: convert(touchLocation, to: messageContainerView)):
  135. delegate?.didTapMessage(in: self)
  136. case avatarView.frame.contains(touchLocation):
  137. delegate?.didTapAvatar(in: self)
  138. case cellTopLabel.frame.contains(touchLocation):
  139. delegate?.didTapCellTopLabel(in: self)
  140. case cellBottomLabel.frame.contains(touchLocation):
  141. delegate?.didTapCellBottomLabel(in: self)
  142. case messageTopLabel.frame.contains(touchLocation):
  143. delegate?.didTapMessageTopLabel(in: self)
  144. case messageBottomLabel.frame.contains(touchLocation):
  145. delegate?.didTapMessageBottomLabel(in: self)
  146. case accessoryView.frame.contains(touchLocation):
  147. delegate?.didTapAccessoryView(in: self)
  148. default:
  149. delegate?.didTapBackground(in: self)
  150. }
  151. }
  152. /// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame
  153. open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
  154. let touchPoint = gestureRecognizer.location(in: self)
  155. guard gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) else { return false }
  156. return messageContainerView.frame.contains(touchPoint)
  157. }
  158. /// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture
  159. open func cellContentView(canHandle touchPoint: CGPoint) -> Bool {
  160. return false
  161. }
  162. // MARK: - Origin Calculations
  163. /// Positions the cell's `AvatarView`.
  164. /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
  165. open func layoutAvatarView(with attributes: MessagesCollectionViewLayoutAttributes) {
  166. var origin: CGPoint = .zero
  167. let padding = attributes.avatarLeadingTrailingPadding
  168. switch attributes.avatarPosition.horizontal {
  169. case .cellLeading:
  170. origin.x = padding
  171. case .cellTrailing:
  172. origin.x = attributes.frame.width - attributes.avatarSize.width - padding
  173. case .natural:
  174. fatalError(MessageKitError.avatarPositionUnresolved)
  175. }
  176. switch attributes.avatarPosition.vertical {
  177. case .messageLabelTop:
  178. origin.y = messageTopLabel.frame.minY
  179. case .messageTop: // Needs messageContainerView frame to be set
  180. origin.y = messageContainerView.frame.minY
  181. case .messageBottom: // Needs messageContainerView frame to be set
  182. origin.y = messageContainerView.frame.maxY - attributes.avatarSize.height
  183. case .messageCenter: // Needs messageContainerView frame to be set
  184. origin.y = messageContainerView.frame.midY - (attributes.avatarSize.height/2)
  185. case .cellBottom:
  186. origin.y = attributes.frame.height - attributes.avatarSize.height
  187. default:
  188. break
  189. }
  190. avatarView.frame = CGRect(origin: origin, size: attributes.avatarSize)
  191. }
  192. /// Positions the cell's `MessageContainerView`.
  193. /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
  194. open func layoutMessageContainerView(with attributes: MessagesCollectionViewLayoutAttributes) {
  195. var origin: CGPoint = .zero
  196. switch attributes.avatarPosition.vertical {
  197. case .messageBottom:
  198. origin.y = attributes.size.height - attributes.messageContainerPadding.bottom - attributes.cellBottomLabelSize.height - attributes.messageBottomLabelSize.height - attributes.messageContainerSize.height - attributes.messageContainerPadding.top
  199. case .messageCenter:
  200. if attributes.avatarSize.height > attributes.messageContainerSize.height {
  201. let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical
  202. origin.y = (attributes.size.height / 2) - (messageHeight / 2)
  203. } else {
  204. fallthrough
  205. }
  206. default:
  207. if attributes.accessoryViewSize.height > attributes.messageContainerSize.height {
  208. let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical
  209. origin.y = (attributes.size.height / 2) - (messageHeight / 2)
  210. } else {
  211. origin.y = attributes.cellTopLabelSize.height + attributes.messageTopLabelSize.height + attributes.messageContainerPadding.top
  212. }
  213. }
  214. let avatarPadding = attributes.avatarLeadingTrailingPadding
  215. switch attributes.avatarPosition.horizontal {
  216. case .cellLeading:
  217. origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left + avatarPadding
  218. case .cellTrailing:
  219. origin.x = attributes.frame.width - attributes.avatarSize.width - attributes.messageContainerSize.width - attributes.messageContainerPadding.right - avatarPadding
  220. case .natural:
  221. fatalError(MessageKitError.avatarPositionUnresolved)
  222. }
  223. messageContainerView.frame = CGRect(origin: origin, size: attributes.messageContainerSize)
  224. }
  225. /// Positions the cell's top label.
  226. /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
  227. open func layoutCellTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
  228. cellTopLabel.frame = CGRect(origin: .zero, size: attributes.cellTopLabelSize)
  229. }
  230. /// Positions the cell's bottom label.
  231. /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
  232. open func layoutCellBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
  233. cellBottomLabel.textAlignment = attributes.cellBottomLabelAlignment.textAlignment
  234. cellBottomLabel.textInsets = attributes.cellBottomLabelAlignment.textInsets
  235. let y = messageBottomLabel.frame.maxY
  236. let origin = CGPoint(x: 0, y: y)
  237. cellBottomLabel.frame = CGRect(origin: origin, size: attributes.cellBottomLabelSize)
  238. }
  239. /// Positions the message bubble's top label.
  240. /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
  241. open func layoutMessageTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
  242. messageTopLabel.textAlignment = attributes.messageTopLabelAlignment.textAlignment
  243. messageTopLabel.textInsets = attributes.messageTopLabelAlignment.textInsets
  244. let y = messageContainerView.frame.minY - attributes.messageContainerPadding.top - attributes.messageTopLabelSize.height
  245. let origin = CGPoint(x: 0, y: y)
  246. messageTopLabel.frame = CGRect(origin: origin, size: attributes.messageTopLabelSize)
  247. }
  248. /// Positions the message bubble's bottom label.
  249. /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
  250. open func layoutMessageBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
  251. messageBottomLabel.textAlignment = attributes.messageBottomLabelAlignment.textAlignment
  252. messageBottomLabel.textInsets = attributes.messageBottomLabelAlignment.textInsets
  253. let y = messageContainerView.frame.maxY + attributes.messageContainerPadding.bottom
  254. let origin = CGPoint(x: 0, y: y)
  255. messageBottomLabel.frame = CGRect(origin: origin, size: attributes.messageBottomLabelSize)
  256. }
  257. /// Positions the cell's accessory view.
  258. /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
  259. open func layoutAccessoryView(with attributes: MessagesCollectionViewLayoutAttributes) {
  260. var origin: CGPoint = .zero
  261. // Accessory view is set at the side space of the messageContainerView
  262. switch attributes.accessoryViewPosition {
  263. case .messageLabelTop:
  264. origin.y = messageTopLabel.frame.minY
  265. case .messageTop:
  266. origin.y = messageContainerView.frame.minY
  267. case .messageBottom:
  268. origin.y = messageContainerView.frame.maxY - attributes.accessoryViewSize.height
  269. case .messageCenter:
  270. origin.y = messageContainerView.frame.midY - (attributes.accessoryViewSize.height / 2)
  271. case .cellBottom:
  272. origin.y = attributes.frame.height - attributes.accessoryViewSize.height
  273. default:
  274. break
  275. }
  276. // Accessory view is always on the opposite side of avatar
  277. switch attributes.avatarPosition.horizontal {
  278. case .cellLeading:
  279. origin.x = messageContainerView.frame.maxX + attributes.accessoryViewPadding.left
  280. case .cellTrailing:
  281. origin.x = messageContainerView.frame.minX - attributes.accessoryViewPadding.right - attributes.accessoryViewSize.width
  282. case .natural:
  283. fatalError(MessageKitError.avatarPositionUnresolved)
  284. }
  285. accessoryView.frame = CGRect(origin: origin, size: attributes.accessoryViewSize)
  286. }
  287. }