// 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 AVFoundation import Foundation import UIKit /// The layout object used by `MessagesCollectionView` to determine the size of all /// framework provided `MessageCollectionViewCell` subclasses. open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: Lifecycle // MARK: - Initializers public override init() { super.init() setupView() setupObserver() } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupView() setupObserver() } deinit { NotificationCenter.default.removeObserver(self) } // MARK: Open open override class var layoutAttributesClass: AnyClass { MessagesCollectionViewLayoutAttributes.self } // MARK: - Cell Sizing lazy open var textMessageSizeCalculator = TextMessageSizeCalculator(layout: self) lazy open var attributedTextMessageSizeCalculator = TextMessageSizeCalculator(layout: self) lazy open var emojiMessageSizeCalculator: TextMessageSizeCalculator = { let sizeCalculator = TextMessageSizeCalculator(layout: self) sizeCalculator.messageLabelFont = UIFont.systemFont(ofSize: sizeCalculator.messageLabelFont.pointSize * 2) return sizeCalculator }() lazy open var photoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self) lazy open var videoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self) lazy open var locationMessageSizeCalculator = LocationMessageSizeCalculator(layout: self) lazy open var audioMessageSizeCalculator = AudioMessageSizeCalculator(layout: self) lazy open var contactMessageSizeCalculator = ContactMessageSizeCalculator(layout: self) lazy open var typingIndicatorSizeCalculator = TypingCellSizeCalculator(layout: self) lazy open var linkPreviewMessageSizeCalculator = LinkPreviewMessageSizeCalculator(layout: self) /// A method that by default checks if the section is the last in the /// `messagesCollectionView` and that `isTypingIndicatorViewHidden` /// is FALSE /// /// - Parameter section /// - Returns: A Boolean indicating if the TypingIndicator should be presented at the given section open func isSectionReservedForTypingIndicator(_ section: Int) -> Bool { !isTypingIndicatorViewHidden && section == messagesCollectionView.numberOfSections - 1 } // MARK: - Attributes open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { return nil } for attributes in attributesArray where attributes.representedElementCategory == .cell { let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath) cellSizeCalculator.configure(attributes: attributes) } return attributesArray } open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else { return nil } if attributes.representedElementCategory == .cell { let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath) cellSizeCalculator.configure(attributes: attributes) } return attributes } // MARK: - Layout Invalidation open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { collectionView?.bounds.width != newBounds.width } open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { let context = super.invalidationContext(forBoundsChange: newBounds) guard let flowLayoutContext = context as? UICollectionViewFlowLayoutInvalidationContext else { return context } flowLayoutContext.invalidateFlowLayoutDelegateMetrics = shouldInvalidateLayout(forBoundsChange: newBounds) return flowLayoutContext } /// Note: /// - If you override this method, remember to call MessageLayoutDelegate's /// customCellSizeCalculator(for:at:in:) method for MessageKind.custom messages, if necessary /// - If you are using the typing indicator be sure to return the `typingIndicatorSizeCalculator` /// when the section is reserved for it, indicated by `isSectionReservedForTypingIndicator` open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { if isSectionReservedForTypingIndicator(indexPath.section) { return typingIndicatorSizeCalculator } let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) switch message.kind { case .text: return messagesLayoutDelegate .textCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? textMessageSizeCalculator case .attributedText: return messagesLayoutDelegate.attributedTextCellSizeCalculator( for: message, at: indexPath, in: messagesCollectionView) ?? attributedTextMessageSizeCalculator case .emoji: return messagesLayoutDelegate .emojiCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? emojiMessageSizeCalculator case .photo: return messagesLayoutDelegate .photoCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? photoMessageSizeCalculator case .video: return messagesLayoutDelegate .videoCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? videoMessageSizeCalculator case .location: return messagesLayoutDelegate .locationCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? locationMessageSizeCalculator case .audio: return messagesLayoutDelegate .audioCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? audioMessageSizeCalculator case .contact: return messagesLayoutDelegate .contactCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) ?? contactMessageSizeCalculator case .linkPreview: return linkPreviewMessageSizeCalculator case .custom: return messagesLayoutDelegate.customCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) } } open func sizeForItem(at indexPath: IndexPath) -> CGSize { let calculator = cellSizeCalculatorForItem(at: indexPath) return calculator.sizeForItem(at: indexPath) } /// Get all `MessageSizeCalculator`s open func messageSizeCalculators() -> [MessageSizeCalculator] { [ textMessageSizeCalculator, attributedTextMessageSizeCalculator, emojiMessageSizeCalculator, photoMessageSizeCalculator, videoMessageSizeCalculator, locationMessageSizeCalculator, audioMessageSizeCalculator, contactMessageSizeCalculator, linkPreviewMessageSizeCalculator, ] } // MARK: Public public private(set) var isTypingIndicatorViewHidden = true /// The `MessagesCollectionView` that owns this layout object. public var messagesCollectionView: MessagesCollectionView { guard let messagesCollectionView = collectionView as? MessagesCollectionView else { fatalError(MessageKitError.layoutUsedOnForeignType) } return messagesCollectionView } /// The `MessagesDataSource` for the layout's collection view. public var messagesDataSource: MessagesDataSource { guard let messagesDataSource = messagesCollectionView.messagesDataSource else { fatalError(MessageKitError.nilMessagesDataSource) } return messagesDataSource } /// The `MessagesLayoutDelegate` for the layout's collection view. public var messagesLayoutDelegate: MessagesLayoutDelegate { guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { fatalError(MessageKitError.nilMessagesLayoutDelegate) } return messagesLayoutDelegate } public var itemWidth: CGFloat { guard let collectionView = collectionView else { return 0 } return collectionView.frame.width - sectionInset.left - sectionInset.right } /// Set `incomingAvatarSize` of all `MessageSizeCalculator`s public func setMessageIncomingAvatarSize(_ newSize: CGSize) { messageSizeCalculators().forEach { $0.incomingAvatarSize = newSize } } /// Set `outgoingAvatarSize` of all `MessageSizeCalculator`s public func setMessageOutgoingAvatarSize(_ newSize: CGSize) { messageSizeCalculators().forEach { $0.outgoingAvatarSize = newSize } } /// Set `incomingAvatarPosition` of all `MessageSizeCalculator`s public func setMessageIncomingAvatarPosition(_ newPosition: AvatarPosition) { messageSizeCalculators().forEach { $0.incomingAvatarPosition = newPosition } } /// Set `outgoingAvatarPosition` of all `MessageSizeCalculator`s public func setMessageOutgoingAvatarPosition(_ newPosition: AvatarPosition) { messageSizeCalculators().forEach { $0.outgoingAvatarPosition = newPosition } } /// Set `avatarLeadingTrailingPadding` of all `MessageSizeCalculator`s public func setAvatarLeadingTrailingPadding(_ newPadding: CGFloat) { messageSizeCalculators().forEach { $0.avatarLeadingTrailingPadding = newPadding } } /// Set `incomingMessagePadding` of all `MessageSizeCalculator`s public func setMessageIncomingMessagePadding(_ newPadding: UIEdgeInsets) { messageSizeCalculators().forEach { $0.incomingMessagePadding = newPadding } } /// Set `outgoingMessagePadding` of all `MessageSizeCalculator`s public func setMessageOutgoingMessagePadding(_ newPadding: UIEdgeInsets) { messageSizeCalculators().forEach { $0.outgoingMessagePadding = newPadding } } /// Set `incomingCellTopLabelAlignment` of all `MessageSizeCalculator`s public func setMessageIncomingCellTopLabelAlignment(_ newAlignment: LabelAlignment) { messageSizeCalculators().forEach { $0.incomingCellTopLabelAlignment = newAlignment } } /// Set `outgoingCellTopLabelAlignment` of all `MessageSizeCalculator`s public func setMessageOutgoingCellTopLabelAlignment(_ newAlignment: LabelAlignment) { messageSizeCalculators().forEach { $0.outgoingCellTopLabelAlignment = newAlignment } } /// Set `incomingCellBottomLabelAlignment` of all `MessageSizeCalculator`s public func setMessageIncomingCellBottomLabelAlignment(_ newAlignment: LabelAlignment) { messageSizeCalculators().forEach { $0.incomingCellBottomLabelAlignment = newAlignment } } /// Set `outgoingCellBottomLabelAlignment` of all `MessageSizeCalculator`s public func setMessageOutgoingCellBottomLabelAlignment(_ newAlignment: LabelAlignment) { messageSizeCalculators().forEach { $0.outgoingCellBottomLabelAlignment = newAlignment } } /// Set `incomingMessageTopLabelAlignment` of all `MessageSizeCalculator`s public func setMessageIncomingMessageTopLabelAlignment(_ newAlignment: LabelAlignment) { messageSizeCalculators().forEach { $0.incomingMessageTopLabelAlignment = newAlignment } } /// Set `outgoingMessageTopLabelAlignment` of all `MessageSizeCalculator`s public func setMessageOutgoingMessageTopLabelAlignment(_ newAlignment: LabelAlignment) { messageSizeCalculators().forEach { $0.outgoingMessageTopLabelAlignment = newAlignment } } /// Set `incomingMessageBottomLabelAlignment` of all `MessageSizeCalculator`s public func setMessageIncomingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) { messageSizeCalculators().forEach { $0.incomingMessageBottomLabelAlignment = newAlignment } } /// Set `outgoingMessageBottomLabelAlignment` of all `MessageSizeCalculator`s public func setMessageOutgoingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) { messageSizeCalculators().forEach { $0.outgoingMessageBottomLabelAlignment = newAlignment } } /// Set `incomingAccessoryViewSize` of all `MessageSizeCalculator`s public func setMessageIncomingAccessoryViewSize(_ newSize: CGSize) { messageSizeCalculators().forEach { $0.incomingAccessoryViewSize = newSize } } /// Set `outgoingAccessoryViewSize` of all `MessageSizeCalculator`s public func setMessageOutgoingAccessoryViewSize(_ newSize: CGSize) { messageSizeCalculators().forEach { $0.outgoingAccessoryViewSize = newSize } } /// Set `incomingAccessoryViewPadding` of all `MessageSizeCalculator`s public func setMessageIncomingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) { messageSizeCalculators().forEach { $0.incomingAccessoryViewPadding = newPadding } } /// Set `outgoingAccessoryViewPadding` of all `MessageSizeCalculator`s public func setMessageOutgoingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) { messageSizeCalculators().forEach { $0.outgoingAccessoryViewPadding = newPadding } } /// Set `incomingAccessoryViewPosition` of all `MessageSizeCalculator`s public func setMessageIncomingAccessoryViewPosition(_ newPosition: AccessoryPosition) { messageSizeCalculators().forEach { $0.incomingAccessoryViewPosition = newPosition } } /// Set `outgoingAccessoryViewPosition` of all `MessageSizeCalculator`s public func setMessageOutgoingAccessoryViewPosition(_ newPosition: AccessoryPosition) { messageSizeCalculators().forEach { $0.outgoingAccessoryViewPosition = newPosition } } // MARK: Internal // MARK: - Typing Indicator API /// Notifies the layout that the typing indicator will change state /// /// - Parameters: /// - isHidden: A Boolean value that is to be the new state of the typing indicator internal func setTypingIndicatorViewHidden(_ isHidden: Bool) { isTypingIndicatorViewHidden = isHidden } // MARK: Private // MARK: - Methods private func setupView() { sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) } private func setupObserver() { NotificationCenter.default.addObserver( self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: UIDevice.orientationDidChangeNotification, object: nil) } @objc private func handleOrientationChange(_: Notification) { invalidateLayout() } }