Telegram-iOS 源码分析:第六部分(Bubbles)

版权声明
本文内容均为搬运,目的只为更方便的学习Telegram编码思维。

如需查阅原作者文章,附赠原文章机票

Bubbles是一类UI展示方式,几乎是我们日常生活中不可或缺的一部分。如果消息只是一段纯文本或一个图像文件,事情将会很简单。但是Telegram中的情形很复杂,因为有许多消息样式,例如文本,带样式的文本,markdown文本,图片,相册,视频,文件,网页,位置等。一条消息几乎可以包含任意类型的多个样式,因此很具挑战性。本文将阐述Telegram-iOS如何在其异步UI框架上构建消息bubbles。

预览使用到的类

part6-class.png

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模式:bubbleslistbubbles模式用于普通聊天,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<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void
    )
}

ChatMessageItem表示一条聊天消息或一组聊天消息。ChatMessageItemView的四个子类是不同类型bubble的容器节点。ChatMessageBubbleItemNode实现了一种机制,用于呈现具有多个内容元素的消息气泡,这些内容元素是ChatMessageBubbleContentNode的子类。

列表翻转

聊天消息列表将最新消息放在底部,垂直滚动指示器也从底部开始。实际上,这是iOS上通用列表UI的一种翻转。Telegram-iOS使用类似在AsyncDisplayKitASTableNode的UI变换伎俩。ChatHistoryListNode利用ASDisplayNodetransform属性旋转了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)
    }
}
...

下面的屏幕截图演示了应用逐步转换后的样子:


part6-list.png

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是一个维护了ChatControllerImpl77个操作回调数据类。它通过项传递,以使它们能够在不引用控制器的情况下触发回调。

ChatMessageItemContent的结构很有趣。它是一个枚举,可以是一个消息或一组消息。在我看来,它可以被简化成只是.group作为.message可以由一组与一个元素来表达。

Message通过两个协议MessageAttributeMedia描述消息中的内容元素。

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。如果attributesentry为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<Int>
    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及其类的实现描述了一组丰富的媒体类型,如TelegramMediaImageTelegramMediaFileTelegramMediaMap等。

总而言之,Message基本上是带有几个媒体附件的属性字符串,而ChatMessageItem实际上是一组Message实例。这种设计非常灵活,可以表示复杂的消息内容,并可以轻松保持向后兼容性。例如,将组图表示为具有多个消息的item,而每个消息的媒体为TelegramMediaImage

Bubble Nodes

ChatMessageItem实现nodeConfiguredForParams以匹配数据设置bubble nodes。如果我们看一下代码,会发现它对item结构有一些规则。

  • 如果第一条消息的animated sticker媒体文件小于128 KB,ChatMessageAnimatedStickerItemNode则选择使用贴纸渲染气泡。该item中的其他消息和媒体数据将被忽略。
  • 默认情况下,large emoji支持的设置在应用程序中处于打开状态。如果一条消息只有一个emoji字符或所有字符都是emojis,ChatMessageAnimatedStickerItemNode或者ChatMessageStickerItemNode用于实现较大的渲染效果而不是纯文本。
part6-send.png

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

part6-layout.png

bubble的布局由ListView的异步布局机制驱动。上图显示了最重要的布局方法的调用流程。在我使用iOS 13.5的iPhone 6s进行测试期间,FPS能够保持在58以上,这比具有较长且复杂的列表UI的其他应用更好。足以证明AsyncDisplayKit是Telegram方案的不错选择。

需要注意的一件事是ListView不会缓存布局结果。如果您的设备确实很慢,在滚动过程中会看到空白的单元格。

总结

这篇文章简要说明了Telegram-iOS中消息气泡的数据模型和UI结构。数据结构对于复杂的消息是灵活的,这对于检查是否开始设计自己的Messenger是一个很好的参考。我鼓励您在看完本篇介绍之后继续学习代码,因为此处不涉及更多详细信息。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容