123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- /*
- MIT License
- Copyright (c) 2017-2019 MessageKit
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
- import UIKit
- /// A subclass of `MessageCollectionViewCell` used to display text, media, and location messages.
- open class MessageContentCell: MessageCollectionViewCell {
- /// The image view displaying the avatar.
- open var avatarView = AvatarView()
- /// The container used for styling and holding the message's content view.
- open var messageContainerView: MessageContainerView = {
- let containerView = MessageContainerView()
- containerView.clipsToBounds = true
- containerView.layer.masksToBounds = true
- return containerView
- }()
- /// The top label of the cell.
- open var cellTopLabel: InsetLabel = {
- let label = InsetLabel()
- label.numberOfLines = 0
- label.textAlignment = .center
- return label
- }()
-
- /// The bottom label of the cell.
- open var cellBottomLabel: InsetLabel = {
- let label = InsetLabel()
- label.numberOfLines = 0
- label.textAlignment = .center
- return label
- }()
-
- /// The top label of the messageBubble.
- open var messageTopLabel: InsetLabel = {
- let label = InsetLabel()
- label.numberOfLines = 0
- return label
- }()
- /// The bottom label of the messageBubble.
- open var messageBottomLabel: InsetLabel = {
- let label = InsetLabel()
- label.numberOfLines = 0
- return label
- }()
- // Should only add customized subviews - don't change accessoryView itself.
- open var accessoryView: UIView = UIView()
- /// The `MessageCellDelegate` for the cell.
- open weak var delegate: MessageCellDelegate?
- public override init(frame: CGRect) {
- super.init(frame: frame)
- contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
- setupSubviews()
- }
- required public init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
- setupSubviews()
- }
- open func setupSubviews() {
- contentView.addSubview(accessoryView)
- contentView.addSubview(cellTopLabel)
- contentView.addSubview(messageTopLabel)
- contentView.addSubview(messageBottomLabel)
- contentView.addSubview(cellBottomLabel)
- contentView.addSubview(messageContainerView)
- contentView.addSubview(avatarView)
- }
- open override func prepareForReuse() {
- super.prepareForReuse()
- cellTopLabel.text = nil
- cellBottomLabel.text = nil
- messageTopLabel.text = nil
- messageBottomLabel.text = nil
- }
- // MARK: - Configuration
- open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
- super.apply(layoutAttributes)
- guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return }
- // Call this before other laying out other subviews
- layoutMessageContainerView(with: attributes)
- layoutMessageBottomLabel(with: attributes)
- layoutCellBottomLabel(with: attributes)
- layoutCellTopLabel(with: attributes)
- layoutMessageTopLabel(with: attributes)
- layoutAvatarView(with: attributes)
- layoutAccessoryView(with: attributes)
- }
- /// Used to configure the cell.
- ///
- /// - Parameters:
- /// - message: The `MessageType` this cell displays.
- /// - indexPath: The `IndexPath` for this cell.
- /// - messagesCollectionView: The `MessagesCollectionView` in which this cell is contained.
- open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
- guard let dataSource = messagesCollectionView.messagesDataSource else {
- fatalError(MessageKitError.nilMessagesDataSource)
- }
- guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else {
- fatalError(MessageKitError.nilMessagesDisplayDelegate)
- }
- delegate = messagesCollectionView.messageCellDelegate
- let messageColor = displayDelegate.backgroundColor(for: message, at: indexPath, in: messagesCollectionView)
- let messageStyle = displayDelegate.messageStyle(for: message, at: indexPath, in: messagesCollectionView)
- displayDelegate.configureAvatarView(avatarView, for: message, at: indexPath, in: messagesCollectionView)
- displayDelegate.configureAccessoryView(accessoryView, for: message, at: indexPath, in: messagesCollectionView)
- messageContainerView.backgroundColor = messageColor
- messageContainerView.style = messageStyle
- let topCellLabelText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath)
- let bottomCellLabelText = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath)
- let topMessageLabelText = dataSource.messageTopLabelAttributedText(for: message, at: indexPath)
- let bottomMessageLabelText = dataSource.messageBottomLabelAttributedText(for: message, at: indexPath)
- cellTopLabel.attributedText = topCellLabelText
- cellBottomLabel.attributedText = bottomCellLabelText
- messageTopLabel.attributedText = topMessageLabelText
- messageBottomLabel.attributedText = bottomMessageLabelText
- }
- /// Handle tap gesture on contentView and its subviews.
- open override func handleTapGesture(_ gesture: UIGestureRecognizer) {
- let touchLocation = gesture.location(in: self)
- switch true {
- case messageContainerView.frame.contains(touchLocation) && !cellContentView(canHandle: convert(touchLocation, to: messageContainerView)):
- delegate?.didTapMessage(in: self)
- case avatarView.frame.contains(touchLocation):
- delegate?.didTapAvatar(in: self)
- case cellTopLabel.frame.contains(touchLocation):
- delegate?.didTapCellTopLabel(in: self)
- case cellBottomLabel.frame.contains(touchLocation):
- delegate?.didTapCellBottomLabel(in: self)
- case messageTopLabel.frame.contains(touchLocation):
- delegate?.didTapMessageTopLabel(in: self)
- case messageBottomLabel.frame.contains(touchLocation):
- delegate?.didTapMessageBottomLabel(in: self)
- case accessoryView.frame.contains(touchLocation):
- delegate?.didTapAccessoryView(in: self)
- default:
- delegate?.didTapBackground(in: self)
- }
- }
- /// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame
- open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
- let touchPoint = gestureRecognizer.location(in: self)
- guard gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) else { return false }
- return messageContainerView.frame.contains(touchPoint)
- }
- /// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture
- open func cellContentView(canHandle touchPoint: CGPoint) -> Bool {
- return false
- }
- // MARK: - Origin Calculations
- /// Positions the cell's `AvatarView`.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutAvatarView(with attributes: MessagesCollectionViewLayoutAttributes) {
- var origin: CGPoint = .zero
- let padding = attributes.avatarLeadingTrailingPadding
- switch attributes.avatarPosition.horizontal {
- case .cellLeading:
- origin.x = padding
- case .cellTrailing:
- origin.x = attributes.frame.width - attributes.avatarSize.width - padding
- case .natural:
- fatalError(MessageKitError.avatarPositionUnresolved)
- }
- switch attributes.avatarPosition.vertical {
- case .messageLabelTop:
- origin.y = messageTopLabel.frame.minY
- case .messageTop: // Needs messageContainerView frame to be set
- origin.y = messageContainerView.frame.minY
- case .messageBottom: // Needs messageContainerView frame to be set
- origin.y = messageContainerView.frame.maxY - attributes.avatarSize.height
- case .messageCenter: // Needs messageContainerView frame to be set
- origin.y = messageContainerView.frame.midY - (attributes.avatarSize.height/2)
- case .cellBottom:
- origin.y = attributes.frame.height - attributes.avatarSize.height
- default:
- break
- }
- avatarView.frame = CGRect(origin: origin, size: attributes.avatarSize)
- }
- /// Positions the cell's `MessageContainerView`.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutMessageContainerView(with attributes: MessagesCollectionViewLayoutAttributes) {
- var origin: CGPoint = .zero
- switch attributes.avatarPosition.vertical {
- case .messageBottom:
- origin.y = attributes.size.height - attributes.messageContainerPadding.bottom - attributes.cellBottomLabelSize.height - attributes.messageBottomLabelSize.height - attributes.messageContainerSize.height - attributes.messageContainerPadding.top
- case .messageCenter:
- if attributes.avatarSize.height > attributes.messageContainerSize.height {
- let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical
- origin.y = (attributes.size.height / 2) - (messageHeight / 2)
- } else {
- fallthrough
- }
- default:
- if attributes.accessoryViewSize.height > attributes.messageContainerSize.height {
- let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical
- origin.y = (attributes.size.height / 2) - (messageHeight / 2)
- } else {
- origin.y = attributes.cellTopLabelSize.height + attributes.messageTopLabelSize.height + attributes.messageContainerPadding.top
- }
- }
- let avatarPadding = attributes.avatarLeadingTrailingPadding
- switch attributes.avatarPosition.horizontal {
- case .cellLeading:
- origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left + avatarPadding
- case .cellTrailing:
- origin.x = attributes.frame.width - attributes.avatarSize.width - attributes.messageContainerSize.width - attributes.messageContainerPadding.right - avatarPadding
- case .natural:
- fatalError(MessageKitError.avatarPositionUnresolved)
- }
- messageContainerView.frame = CGRect(origin: origin, size: attributes.messageContainerSize)
- }
- /// Positions the cell's top label.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutCellTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
- cellTopLabel.frame = CGRect(origin: .zero, size: attributes.cellTopLabelSize)
- }
-
- /// Positions the cell's bottom label.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutCellBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
- cellBottomLabel.textAlignment = attributes.cellBottomLabelAlignment.textAlignment
- cellBottomLabel.textInsets = attributes.cellBottomLabelAlignment.textInsets
-
- let y = messageBottomLabel.frame.maxY
- let origin = CGPoint(x: 0, y: y)
-
- cellBottomLabel.frame = CGRect(origin: origin, size: attributes.cellBottomLabelSize)
- }
-
- /// Positions the message bubble's top label.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutMessageTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
- messageTopLabel.textAlignment = attributes.messageTopLabelAlignment.textAlignment
- messageTopLabel.textInsets = attributes.messageTopLabelAlignment.textInsets
- let y = messageContainerView.frame.minY - attributes.messageContainerPadding.top - attributes.messageTopLabelSize.height
- let origin = CGPoint(x: 0, y: y)
-
- messageTopLabel.frame = CGRect(origin: origin, size: attributes.messageTopLabelSize)
- }
- /// Positions the message bubble's bottom label.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutMessageBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) {
- messageBottomLabel.textAlignment = attributes.messageBottomLabelAlignment.textAlignment
- messageBottomLabel.textInsets = attributes.messageBottomLabelAlignment.textInsets
- let y = messageContainerView.frame.maxY + attributes.messageContainerPadding.bottom
- let origin = CGPoint(x: 0, y: y)
- messageBottomLabel.frame = CGRect(origin: origin, size: attributes.messageBottomLabelSize)
- }
- /// Positions the cell's accessory view.
- /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell.
- open func layoutAccessoryView(with attributes: MessagesCollectionViewLayoutAttributes) {
-
- var origin: CGPoint = .zero
-
- // Accessory view is set at the side space of the messageContainerView
- switch attributes.accessoryViewPosition {
- case .messageLabelTop:
- origin.y = messageTopLabel.frame.minY
- case .messageTop:
- origin.y = messageContainerView.frame.minY
- case .messageBottom:
- origin.y = messageContainerView.frame.maxY - attributes.accessoryViewSize.height
- case .messageCenter:
- origin.y = messageContainerView.frame.midY - (attributes.accessoryViewSize.height / 2)
- case .cellBottom:
- origin.y = attributes.frame.height - attributes.accessoryViewSize.height
- default:
- break
- }
- // Accessory view is always on the opposite side of avatar
- switch attributes.avatarPosition.horizontal {
- case .cellLeading:
- origin.x = messageContainerView.frame.maxX + attributes.accessoryViewPadding.left
- case .cellTrailing:
- origin.x = messageContainerView.frame.minX - attributes.accessoryViewPadding.right - attributes.accessoryViewSize.width
- case .natural:
- fatalError(MessageKitError.avatarPositionUnresolved)
- }
- accessoryView.frame = CGRect(origin: origin, size: attributes.accessoryViewSize)
- }
- }
|