版权声明
本文内容均为搬运,目的只为更方便的学习Telegram编码思维。
如需查阅原作者文章,附赠原文章机票
Bubbles是一类UI展示方式,几乎是我们日常生活中不可或缺的一部分。如果消息只是一段纯文本或一个图像文件,事情将会很简单。但是Telegram中的情形很复杂,因为有许多消息样式,例如文本,带样式的文本,markdown文本,图片,相册,视频,文件,网页,位置等。一条消息几乎可以包含任意类型的多个样式,因此很具挑战性。本文将阐述Telegram-iOS如何在其异步UI框架上构建消息bubbles。
预览使用到的类
ChatControllerImpl是管理消息列表用户界面的核心控制器。它的内容ChatControllerNode由以下主要node组成UI结构:
class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
...
let backgroundNode: WallpaperBackgroundNode // background wallpaper
let historyNode: ChatHistoryListNode // message list
let loadingNode: ChatLoadingNode // loading UI
...
private var textInputPanelNode: ChatTextInputPanelNode? // text input
private var inputMediaNode: ChatMediaInputNode? // media input
let navigateButtons: ChatHistoryNavigationButtons // the navi button at the bottom right
}
作为ListView的子类,ChatHistoryListNode渲染消息列表以及其他信息node。它有两种UI模式:bubbles和list。bubbles
模式用于普通聊天,list
用于在聊天框信息详情按媒体,文件,语音等列出每种类型的聊天历史记录。本篇文章仅讨论bubbles
模式。
其核心数据属性items可以采用三种类型的ListViewItem。每个item都实现nodeConfiguredForParams
方法以返回相应的UI node。
public protocol ListViewItem {
...
func nodeConfiguredForParams(
async: @escaping (@escaping () -> Void) -> Void,
params: ListViewItemLayoutParams,
synchronousLoads: Bool,
previousItem: ListViewItem?,
nextItem: ListViewItem?,
completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void
)
}
ChatMessageItem表示一条聊天消息或一组聊天消息。ChatMessageItemView的四个子类是不同类型bubble的容器节点。ChatMessageBubbleItemNode实现了一种机制,用于呈现具有多个内容元素的消息气泡,这些内容元素是ChatMessageBubbleContentNode的子类。
列表翻转
聊天消息列表将最新消息放在底部,垂直滚动指示器也从底部开始。实际上,这是iOS上通用列表UI的一种翻转。Telegram-iOS使用类似在AsyncDisplayKit
里ASTableNode的UI变换伎俩。ChatHistoryListNode
利用ASDisplayNode
的transform
属性旋转了180° ,所以所有的content node 都被翻转了180°。
// rotate the list node
public final class ChatHistoryListNode: ListView, ChatHistoryNode {
public init(...) {
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
}
}
// rotate content nodes
public class ChatMessageItemView: ListViewItemNode {
public init(...) {
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
}
}
final class ChatMessageShadowNode: ASDisplayNode {
override init() {
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
}
}
final class ChatMessageDateHeaderNode: ListViewItemHeaderNode {
init() {
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
}
}
...
下面的屏幕截图演示了应用逐步转换后的样子:
ListView Items
- ChatBotInfoItem。如果Peer是Telegram机器人,则将机器人标识插入到items的第一个位置。
- ChatUnreadItem。区分未读消息和已读消息的标识。
- ChatMessageItem。它将聊天消息建模如下:
public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
...
let chatLocation: ChatLocation
let controllerInteraction: ChatControllerInteraction
let content: ChatMessageItemContent
...
}
public enum ChatLocation: Equatable {
case peer(PeerId)
}
public enum ChatMessageItemContent: Sequence {
case message(
message: Message,
read: Bool,
selection: ChatHistoryMessageSelection,
attributes: ChatMessageEntryAttributes)
case group(
messages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes)])
}
ChatControllerInteraction是一个维护了ChatControllerImpl
77个操作回调数据类。它通过项传递,以使它们能够在不引用控制器的情况下触发回调。
ChatMessageItemContent的结构很有趣。它是一个枚举,可以是一个消息或一组消息。在我看来,它可以被简化成只是.group
作为.message
可以由一组与一个元素来表达。
Message通过两个协议MessageAttribute和Media描述消息中的内容元素。
public final class Message {
....
public let author: Peer?
public let text: String
public let attributes: [MessageAttribute]
public let media: [Media]
...
}
public protocol MessageAttribute: class, PostboxCoding { ... }
public protocol Media: class, PostboxCoding {
var id: MediaId? { get }
...
}
实例Message
始终具有一个text
描述和一些可选的MessageAttribute
。如果attributes
entry为TextEntitiesMessageAttribute,则可以通过构造属性字符串stringWithAppliedEntities。然后,可以在bubble内呈现格式丰富的文本。
// For example, this one states the entities inside a text
public class TextEntitiesMessageAttribute: MessageAttribute, Equatable {
public let entities: [MessageTextEntity]
}
public struct MessageTextEntity: PostboxCoding, Equatable {
public let range: Range
public let type: MessageTextEntityType
}
public enum MessageTextEntityType: Equatable {
public typealias CustomEntityType = Int32
case Unknown
case Mention
case Hashtag
case Url
case Email
case Bold
case Italic
case Code
...
case Strikethrough
case BlockQuote
case Underline
case BankCard
case Custom(type: CustomEntityType)
}
协议Media
及其类的实现描述了一组丰富的媒体类型,如TelegramMediaImage,TelegramMediaFile,TelegramMediaMap等。
总而言之,Message
基本上是带有几个媒体附件的属性字符串,而ChatMessageItem
实际上是一组Message
实例。这种设计非常灵活,可以表示复杂的消息内容,并可以轻松保持向后兼容性。例如,将组图表示为具有多个消息的item,而每个消息的媒体为TelegramMediaImage。
Bubble Nodes
ChatMessageItem
实现nodeConfiguredForParams以匹配数据设置bubble nodes。如果我们看一下代码,会发现它对item结构有一些规则。
- 如果第一条消息的animated sticker媒体文件小于128 KB,ChatMessageAnimatedStickerItemNode则选择使用贴纸渲染气泡。该item中的其他消息和媒体数据将被忽略。
- 默认情况下,large emoji支持的设置在应用程序中处于打开状态。如果一条消息只有一个emoji字符或所有字符都是emojis,
ChatMessageAnimatedStickerItemNode
或者ChatMessageStickerItemNode用于实现较大的渲染效果而不是纯文本。
- 如果item的第一条消息具有即时圆形视频文件,ChatMessageInstantVideoItemNode则选择显示该圆形视频,其他内容将被忽略。
- ChatMessageBubbleItemNode处理结构化消息。
ChatMessageBubbleItemNode
通过item数据sub-nodes,总共有16个ChatMessageBubbleContentNode的子类。contentNodeMessagesAndClassesForItem是核心的匹配不同类型的方法。
private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [(Message, AnyClass, ChatMessageEntryAttributes)] {
var result: [(Message, AnyClass, ChatMessageEntryAttributes)] = []
...
outer: for (message, itemAttributes) in item.content {
inner: for media in message.media {
if let _ = media as? TelegramMediaImage {
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes))
} else if {...}
}
var messageText = message.text
if !messageText.isEmpty ... {
result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes))
}
}
...
}
Layout
bubble的布局由ListView
的异步布局机制驱动。上图显示了最重要的布局方法的调用流程。在我使用iOS 13.5的iPhone 6s进行测试期间,FPS能够保持在58以上,这比具有较长且复杂的列表UI的其他应用更好。足以证明AsyncDisplayKit
是Telegram方案的不错选择。
需要注意的一件事是ListView
不会缓存布局结果。如果您的设备确实很慢,在滚动过程中会看到空白的单元格。
总结
这篇文章简要说明了Telegram-iOS中消息气泡的数据模型和UI结构。数据结构对于复杂的消息是灵活的,这对于检查是否开始设计自己的Messenger是一个很好的参考。我鼓励您在看完本篇介绍之后继续学习代码,因为此处不涉及更多详细信息。