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模式: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利用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通过两个协议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。如果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
    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用于实现较大的渲染效果而不是纯文本。
part6-send.png
  • 如果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

part6-layout.png

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

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

总结

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

你可能感兴趣的:(Telegram-iOS 源码分析:第六部分(Bubbles))