| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- //
- // JCChatViewLayout.swift
- // JChat
- //
- // Created by deng on 2017/2/28.
- // Copyright © 2017年 HXHG. All rights reserved.
- //
- import UIKit
- @objc open class JCChatViewLayout: UICollectionViewFlowLayout {
-
- public override init() {
- super.init()
- _commonInit()
- }
- public required init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- _commonInit()
- }
-
- internal weak var _chatView: JCChatView?
-
- open override class var layoutAttributesClass: AnyClass {
- return JCChatViewLayoutAttributes.self
- }
-
- open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
- let attributes = super.layoutAttributesForItem(at: indexPath)
- if let attributes = attributes as? JCChatViewLayoutAttributes, attributes.info == nil {
- attributes.info = layoutAttributesInfoForItem(at: indexPath)
- }
- return attributes
- }
- open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
- let arr = super.layoutAttributesForElements(in: rect)
- arr?.forEach({
- guard let attributes = $0 as? JCChatViewLayoutAttributes, attributes.info == nil else {
- return
- }
- attributes.info = layoutAttributesInfoForItem(at: attributes.indexPath)
- })
- return arr
- }
-
- open func layoutAttributesInfoForItem(at indexPath: IndexPath) -> JCChatViewLayoutAttributesInfo? {
- guard let collectionView = collectionView, let _ = collectionView.delegate as? JCChatViewLayoutDelegate, let message = _message(at: indexPath) else {
- return nil
- }
- let size = CGSize(width: collectionView.frame.width, height: .greatestFiniteMagnitude)
- if let info = _allLayoutAttributesInfo[message.identifier] {
- // 这不是合理的做法
- if !message.updateSizeIfNeeded {
- return info
- }
- }
- let options = message.options
-
- var allRect: CGRect = .zero
- var allBoxRect: CGRect = .zero
-
- var cardRect: CGRect = .zero
- var cardBoxRect: CGRect = .zero
-
- var avatarRect: CGRect = .zero
- var avatarBoxRect: CGRect = .zero
-
- var bubbleRect: CGRect = .zero
- var bubbleBoxRect: CGRect = .zero
-
- var contentRect: CGRect = .zero
- var contentBoxRect: CGRect = .zero
-
- var tipsRect: CGRect = .zero
- var tipsBoxRect: CGRect = .zero
-
- // 计算的时候以左对齐为基准
-
- // +---------------------------------------+ r0
- // |+---------------------------------+ r1 |
- // ||+---+ <NAME> | |
- // ||| A | +---------------------\ r4 | |
- // ||+---+ |+---------------+ r5 | | |
- // || || CONTENT | | | |
- // || |+---------------+ | | |
- // || \---------------------/ | | +---+ r6
- // |+---------------------------------+ <-|- | ! |
- // +---------------------------------------+ +---+
-
- let edg0 = _inset(with: options.style, for: .all)
- var r0 = CGRect(x: 0, y: 0, width: size.width, height: .greatestFiniteMagnitude)
- var r1 = r0.inset(by: edg0)
-
- var x1 = r1.minX
- var y1 = r1.minY
- var x2 = r1.maxX
- var y2 = r1.maxY
-
- if options.showsAvatar {
- let edg = _inset(with: options.style, for: .avatar)
- let size = _size(with: options.style, for: .avatar)
-
- let box = CGRect(x: x1, y: y1, width: edg.left + size.width + edg.right, height: edg.top + size.height + edg.bottom)
- let rect = box.inset(by: edg)
-
- avatarRect = rect
- avatarBoxRect = box
-
- x1 = box.maxX
- }
-
- if options.showsCard {
- let edg = _inset(with: options.style, for: .card)
- let size = _size(with: options.style, for: .card)
-
- let box = CGRect(x: x1, y: y1, width: x2 - x1, height: edg.top + size.height + edg.bottom)
- let rect = box.inset(by: edg)
-
- cardRect = rect
- cardBoxRect = box
-
- y1 = box.maxY
- }
- if options.showsBubble {
- let edg = _inset(with: options.style, for: .bubble)
-
- let box = CGRect(x: x1, y: y1, width: x2 - x1, height: y2 - y1)
- let rect = box.inset(by: edg)
-
- bubbleRect = rect
- bubbleBoxRect = box
-
- x1 = rect.minX
- x2 = rect.maxX
- y1 = rect.minY
- y2 = rect.maxY
- }
- if true {
- let edg0 = _inset(with: options.style, for: .content)
- let edg1 = message.content.layoutMargins
- //
- let edg = UIEdgeInsets(top: edg0.top + edg1.top, left: edg0.left + edg1.left, bottom: edg0.bottom + edg1.bottom, right: edg0.right + edg1.right)
-
- var box = CGRect(x: x1, y: y1, width: x2 - x1, height: y2 - y1)
- var rect = box.inset(by: edg)
-
- // calc content size
- let size = message.content.sizeThatFits(rect.size)
-
- // restore offset
- box.size.width = edg.left + size.width + edg.right
- box.size.height = edg.top + size.height + edg.bottom
- rect.size.width = size.width
- rect.size.height = size.height
-
- contentRect = rect
- contentBoxRect = box
-
- x1 = box.maxX
- y1 = box.maxY
- }
- if options.showsBubble {
- let edg = _inset(with: options.style, for: .bubble)
-
- bubbleRect.size.width = contentBoxRect.width
- bubbleRect.size.height = contentBoxRect.height
-
- bubbleBoxRect.size.width = edg.left + contentBoxRect.width + edg.right
- bubbleBoxRect.size.height = edg.top + contentBoxRect.height + edg.bottom
- }
-
- if options.showsTips {
- let edg = _inset(with: options.style, for: .tips)
- let size = _size(with: options.style, for: .tips)
-
- 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)
- let rect = box.inset(by: edg)
-
- tipsRect = rect
- tipsBoxRect = box
-
- x1 = box.maxX
- }
-
- // adjust
- r1.size.width = x1 - r1.minX
- r1.size.height = y1 - r1.minY
- r0.size.width = x1
- r0.size.height = y1 + edg0.bottom
-
- allRect = r1
- allBoxRect = r0
-
- // algin
- switch options.alignment {
- case .right:
- // to right
- allRect.origin.x = size.width - allRect.maxX
- allBoxRect.origin.x = size.width - allBoxRect.maxX
-
- cardRect.origin.x = size.width - cardRect.maxX
- cardBoxRect.origin.x = size.width - cardBoxRect.maxX
-
- avatarRect.origin.x = size.width - avatarRect.maxX
- avatarBoxRect.origin.x = size.width - avatarBoxRect.maxX
-
- bubbleRect.origin.x = size.width - bubbleRect.maxX
- bubbleBoxRect.origin.x = size.width - bubbleBoxRect.maxX
-
- contentRect.origin.x = size.width - contentRect.maxX
- contentBoxRect.origin.x = size.width - contentBoxRect.maxX
-
- tipsRect.origin.x = size.width - tipsRect.maxX
- tipsBoxRect.origin.x = size.width - tipsBoxRect.maxX
-
- case .center:
- allRect.origin.x = (size.width - allRect.width) / 2
- allBoxRect.origin.x = (size.width - allBoxRect.width) / 2
-
- bubbleRect.origin.x = (size.width - bubbleRect.width) / 2
- bubbleBoxRect.origin.x = (size.width - bubbleBoxRect.width) / 2
-
- contentRect.origin.x = (size.width - contentRect.width) / 2
- contentBoxRect.origin.x = (size.width - contentBoxRect.width) / 2
-
- case .left:
- break
- }
- // save
- let rects: [JCChatViewLayoutItem: CGRect] = [
- .all: allRect,
- .card: cardRect,
- .avatar: avatarRect,
- .bubble: bubbleRect,
- .content: contentRect,
- .tips: tipsRect
- ]
- let boxRects: [JCChatViewLayoutItem: CGRect] = [
- .all: allBoxRect,
- .card: cardBoxRect,
- .avatar: avatarBoxRect,
- .bubble: bubbleBoxRect,
- .content: contentBoxRect,
- .tips: tipsBoxRect
- ]
- let info = JCChatViewLayoutAttributesInfo(message: message, size: size, rects: rects, boxRects: boxRects)
- _allLayoutAttributesInfo[message.identifier] = info
- return info
- }
-
- private func _size(with style: JCMessageStyle, for item: JCChatViewLayoutItem) -> CGSize {
- let key = "\(style.rawValue)-\(item.rawValue)"
- if let size = _cachedAllLayoutSize[key] {
- return size // hit cache
- }
- var size: CGSize?
- if let collectionView = collectionView, let delegate = collectionView.delegate as? JCChatViewLayoutDelegate {
- switch item {
- case .all: size = .zero
- case .card: size = delegate.collectionView?(collectionView, layout: self, sizeForItemCardOf: style)
- case .avatar: size = delegate.collectionView?(collectionView, layout: self, sizeForItemAvatarOf: style)
- case .bubble: size = .zero
- case .content: size = .zero
- case .tips: size = delegate.collectionView?(collectionView, layout: self, sizeForItemTipsOf: style)
- }
- }
- _cachedAllLayoutSize[key] = size ?? .zero
- return size ?? .zero
- }
- private func _inset(with style: JCMessageStyle, for item: JCChatViewLayoutItem) -> UIEdgeInsets {
- let key = "\(style.rawValue)-\(item.rawValue)"
- if let edg = _cachedAllLayoutInset[key] {
- return edg // hit cache
- }
- var edg: UIEdgeInsets?
- if let collectionView = collectionView, let delegate = collectionView.delegate as? JCChatViewLayoutDelegate {
- switch item {
- case .all: edg = delegate.collectionView?(collectionView, layout: self, insetForItemOf: style)
- case .card: edg = delegate.collectionView?(collectionView, layout: self, insetForItemCardOf: style)
- case .tips: edg = delegate.collectionView?(collectionView, layout: self, insetForItemTipsOf: style)
- case .avatar: edg = delegate.collectionView?(collectionView, layout: self, insetForItemAvatarOf: style)
- case .bubble: edg = delegate.collectionView?(collectionView, layout: self, insetForItemBubbleOf: style)
- case .content: edg = delegate.collectionView?(collectionView, layout: self, insetForItemContentOf: style)
- }
- }
- _cachedAllLayoutInset[key] = edg ?? .zero
- return edg ?? .zero
- }
- private func _message(at indexPath: IndexPath) -> JCMessageType? {
- guard let collectionView = collectionView, let delegate = collectionView.delegate as? JCChatViewLayoutDelegate else {
- return nil
- }
- return delegate.collectionView(collectionView, layout: self, itemAt: indexPath)
- }
-
- private func _commonInit() {
- minimumLineSpacing = 0
- minimumInteritemSpacing = 0
- }
-
- private lazy var _cachedAllLayoutSize: [String: CGSize] = [:]
- private lazy var _cachedAllLayoutInset: [String: UIEdgeInsets] = [:]
-
- private lazy var _allLayoutAttributesInfo: [UUID: JCChatViewLayoutAttributesInfo] = [:]
- }
- @objc public protocol JCChatViewLayoutDelegate: UICollectionViewDelegateFlowLayout {
-
- func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, itemAt indexPath: IndexPath) -> JCMessageType
-
- @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemCardOf style: JCMessageStyle) -> CGSize
- @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemTipsOf style: JCMessageStyle) -> CGSize
- @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAvatarOf style: JCMessageStyle) -> CGSize
-
- @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemOf style: JCMessageStyle) -> UIEdgeInsets
- @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemCardOf style: JCMessageStyle) -> UIEdgeInsets
- @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemTipsOf style: JCMessageStyle) -> UIEdgeInsets
- @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemAvatarOf style: JCMessageStyle) -> UIEdgeInsets
- @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemBubbleOf style: JCMessageStyle) -> UIEdgeInsets
- @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemContentOf style: JCMessageStyle) -> UIEdgeInsets
- }
|