版权声明
本文内容均为搬运,目的只为更方便的学习Telegram编码思维。
如需查阅原作者文章,附赠原文章机票
Telegram-iOS
使用AsyncDisplayKit
构建大多数UI。AsyncDisplayKit
是项目的一个子模块,其中很多功能被移除,一些功能已在Swift中重新实现。本篇文章探讨项目中的组件结构和UI编程模式。
1.概述
AsyncDisplayKit
是一个异步UI框架,最初是从Facebook诞生的。它已被Pinterest所采用,并于2017年重命名为Texture。其核心概念是node作为的抽象UIView
使用,与React Virtual DOM的想法有点相似。节点是线程安全的,这有助于将复杂的UI操作从主线程中移出,例如图像解码,文本大小调整等。Nodes
是轻量级的,这允许你在tables和collections里面不重复使用Cell
如图所示,Telegram-iOS保留约35%的
AsyncDisplayKit
代码(蓝色框表示),并删除其他代码。
- 合并的最新一次提交是 ae2b3af9
- 基础Nodes比如ASDisplayNode,ASControlNode,ASEditableTextNode,和ASScrollNode大多保持原状。
- ASImageNode以及其子类被移除。Telegram使用MTProto而非HTTPS从数据中心下载文件。官方版本对网络图片的支持没有用,因此依赖库PINRemoteImage也被删除。
- 所有官方的node containers被移除,比如ASCollectionNode,ASTableNode,ASViewController等。
Tables
和view controllers
使用Swift重新实现并且移除了对IGListKit库的依赖。 - Telegram-iOS项目更喜欢手动布局。所以CSS Flexbox的官方布局API被删除,yoga engine同样被删除。
- 内部日志记录和调试功能支持已删除。
基本上来说,Telegram-iOS保留了最少的核心Node类,然后重写了几百个Node的子类。这些代码散布在子模块(如Display,TelegramUI,ItemListUI)以及支持主要Telegram UI功能的其他子模块中。
2.核心Node
有一些节点类是构建应用程序用户界面的基本模块。让我们了解一下它们,如图所示。
带箭头的边缘表示右边的Node是左边Node的子类。没有边缘的相同级别的Nodes意味着它们与最左边的Node是相同的父类。
文本
TextNode,ImmediateTextNode和ASTextNode负责文本渲染。
public class TextNode: ASDisplayNode {
public internal(set) var cachedLayout: TextNodeLayout?
public static func asyncLayout(_ maybeNode: TextNode?) -> (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)
}
public class ImmediateTextNode: TextNode {
public var attributedText: NSAttributedString?
public var textAlignment: NSTextAlignment = .natural
public var truncationType: CTLineTruncationType = .end
public var maximumNumberOfLines: Int = 1
public var lineSpacing: CGFloat = 0.0
public var insets: UIEdgeInsets = UIEdgeInsets()
public var textShadowColor: UIColor?
public var textStroke: (UIColor, CGFloat)?
public var cutout: TextNodeCutout?
public var truncationMode: NSLineBreakMode
public var linkHighlightColor: UIColor?
public var trailingLineWidth: CGFloat?
public var highlightAttributeAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)?
public var tapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public var longTapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
...
}
public class ASTextNode: ImmediateTextNode {
override public var attributedText: NSAttributedString? {
didSet {
self.setNeedsLayout()
}
}
...
}
TextNode
利用CoreText
渲染NSAttributedString
。它有一个计算基于行的文本布局的方法calculateLayout,并重写了父类方法draw以渲染文本。公开的方法asyncLayout用来异步调用布局计算并缓存结果。asyncLayout
方法是被设计来供上一级调用的。否则,它将不会呈现任何内容,因为缓存的布局为nil。实现支持RTL和Accessibility是值得称赞的。
ImmediateTextNode
通过添加更多属性来控制文本布局样式使得TextNode
更加丰富。它还支持高亮显示和点击时间。
ASTextNode
只需在设置attributedText
属性时就是更新布局。这和官方AsyncDisplayKit里的不是同一个,尽管名字一样。
EditableTextNode扩展ASEditableTextNode
以支持RTL输入检测。
Image
open class ASImageNode: ASDisplayNode {
public var image: UIImage?
}
public class ImageNode: ASDisplayNode {
public func setSignal(_ signal: Signal)
}
open class TransformImageNode: ASDisplayNode {
public var imageUpdated: ((UIImage?) -> Void)?
public var contentAnimations: TransformImageNodeContentAnimations = []
public func setSignal(_ signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>, attemptSynchronously: Bool = false, dispatchOnDisplayLink: Bool = true)
public func setOverlayColor(_ color: UIColor?, animated: Bool)
}
ASImageNode渲染UIImage
并使用图像的Size作为Node的Size。同样,它和官方AsyncDisplayKit里的不是同一个Node。
ImageNode接受一个Signal来异步设置图像内容。仅由AvatarNode使用,尽管它的名称看起来很普通。
TransformImageNode是异步图片使用最广泛的类。它支持在更改图片时使用alpha动画,并支持颜色叠加。
Button
open class ASButtonNode: ASControlNode {
public let titleNode: ImmediateTextNode
public let highlightedTitleNode: ImmediateTextNode
public let disabledTitleNode: ImmediateTextNode
public let imageNode: ASImageNode
public let disabledImageNode: ASImageNode
public let backgroundImageNode: ASImageNode
public let highlightedBackgroundImageNode: ASImageNode
}
open class HighlightTrackingButtonNode: ASButtonNode {
public var highligthedChanged: (Bool) -> Void = { _ in }
}
open class HighlightableButtonNode: HighlightTrackingButtonNode {
...
}
ASButtonNode 为具有图片和标题的button,具有三个状态:normal, highlighted, disabled。
HighlightableButtonNode 在点击按钮时添加高亮动画。
Status
ActivityIndicator 模仿UIActivityIndicatorView
样式并提供灵活的选项以自定义细节,例如颜色,直径和线宽。
Media
Telegram-iOS实现了一组丰富的组件来支持不同的媒体类型。本文只是大致了解,它值得在本系列中撰写一篇专门的文章,包括FFMpeg集成,第三方视频网站的应用内视频播放,贴纸动画等。
MediaPlayNode是MediaPlayer的子类,用于在AVSampleBufferDisplayLayer
上渲染视频帧。
WebEmbedPlayerNode通过嵌入播放网页内的视频WKWebView
。它支持来自Youtube,Vimeo,Twitch等的视频。
AnimatedStickerNode播放用于播放来自AnimatedStickerNodeSource
的动画。
Bar
SearchBarNode,NavigationBar,TabBarNode,和ToolbarNode模仿了在UIKit中对应的功能。它还消除了各系统版本之间行为不一致的影响,UIKit内部对于开发人员是不可见的,这始终是个不太好的问题。
StatusBar 在系统状态栏区域显示呼叫中的文本通知。
List
ListView是为可滑动列表设计的最复杂的node类之一。正如我们从WWDC 2014中学到的那样,它利用隐藏的UIScrollView
并借用其pan手势来获得滚动行为。除了管理列表中元素(无论大小)的可见性之外,它还提供其他简洁的功能,例如方便的项目标题,可自定义的滚动指示器,记录项目,滚动项目,捕捉边界等。
GridNode是另一个用于网格布局的滚动UI组件。它在项目中使用到的场景例如贴纸选择,墙纸设置等功能。
3. Controllers
ViewController使UIViewController像nodes层次结构的容器一样工作。与官方node控制器类ASViewController不同,它没有可见深度和智能预载之类的功能。
| @objc open class ViewController: UIViewController, ContainableController { |
| | // the root content node |
| | private var _displayNode: ASDisplayNode? |
| | public final var displayNode: ASDisplayNode { |
| | get { |
| | if let value = self._displayNode { |
| | return value |
| | } |
| | else { |
| | self.loadDisplayNode() |
| | ... |
| | return self._displayNode! |
| | } |
| | } |
| | ... |
| | } |
| | open func loadDisplayNode() |
| | open func displayNodeDidLoad() |
| | |
| | // shared components |
| | public let statusBar: StatusBar |
| | public let navigationBar: NavigationBar? |
| | private(set) var toolbar: Toolbar? |
| | private var scrollToTopView: ScrollToTopView? |
| | // customizations of navigationBar |
| | public var navigationOffset: CGFloat |
| | open var navigationHeight: CGFloat |
| | open var navigationInsetHeight: CGFloat |
| | open var cleanNavigationHeight: CGFloat |
| | open var visualNavigationInsetHeight: CGFloat |
| | public var additionalNavigationBarHeight: CGFloat |
| | } |
[view raw](https://gist.github.com/openaphid/23530e81ca4d4fa8336327e092a6ebf0/raw/950b1f1af5d90f02dedec1523f271d145d1b9301/ViewController.swift) [](https://gist.github.com/openaphid/23530e81ca4d4fa8336327e092a6ebf0#file-viewcontroller-swift)
每个ViewController
通过一个root node
来管理node层次结构,该root node
存储在displayNode
该类的属性中。loadDisplayNode
和displayNodeDidLoad
方法实现我们在UIViewController中熟悉的懒加载行为。
作为基类,它为子类准备了几个共享的node组件:状态栏,导航栏,工具栏和返回到顶部功能。还有一些方便的属性可以自定义其导航栏,这对于普通的UIViewController而言仍然是一个麻烦的问题。
ViewController很少单独使用,项目中有100多个控制器子类用于不同的用户界面。两种UIKit中最常用的容器控制器UINavigationController
和UITabBarController
,分别用NavigationController
和TabBarController
重新实现。
open class NavigationController: UINavigationController, ContainableController, UIGestureRecognizerDelegate {
private var _viewControllers: [ViewController] = []
// NavigationControllerNode
private var _displayNode: ASDisplayNode?
private var theme: NavigationControllerTheme
// manage layout and transition animation
private func updateContainers(layout rawLayout: ContainerViewLayout, transition: ContainedViewLayoutTransition)
// push with a completion handler
public func pushViewController(_ controller: ViewController, animated: Bool = true, completion: @escaping () -> Void)
}
// NavigationLayout.swift
enum RootNavigationLayout {
case split([ViewController], [ViewController])
case flat([ViewController])
}
// NavigationContainer.swift
final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate
override func didLoad() {
// the interactive pop gesture
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: ...)
}
}
NavigationController扩展UINavigationController
以借用其公共API,使得它可以像普通的UIViewController一样被使用。它在内部重写了以下所有内容:
- 直接管理子控制器。由于它只是一个简单的数组,因此可以自由调整以进行堆栈操作。
- 过渡动画。您可以在ContainedViewLayoutTransition中找到所有动画详细信息。
- 交互式pop手势。InteractiveTransitionGestureRecognizer可以在整个屏幕范围响应pop手势。
- 像iPad这样的大屏设备分屏布局。它支持两种类型的布局:
flat
和split
。最好有一个容器控制器同时支持iPhone和iPad,而不需要花很多精力在容器控制器上UISplitViewController
。 - 主题。通过
theme
属性可以很容易地自定义外观。
TabBarController只能在根屏幕中使用,它是ViewController
的子类,而不是UITabBarController
,因此不需要保留API。该规则同样适用于ActionSheetController,AlertController和ContextMenuController。这些实现完美地覆盖了系统视图控制器内部细节,在我看来,用户体验几乎相同。
ItemListController相当于UITableViewController
管理一个ListView
。它还支持自定义overlay node,search view和items排序。
4.布局
AsyncDisplayKit中的Flexbox布局系统已由混合布局机制取代:
// NavigationBar.swift
// layout in the main thread
open class NavigationBar: ASDisplayNode {
override open func layout() {
super.layout()
if let validLayout = self.validLayout, self.requestedLayout {
self.requestedLayout = false
self.updateLayout(size: validLayout.0, defaultHeight: validLayout.1, additionalHeight: validLayout.2, leftInset: validLayout.3, rightInset: validLayout.4, appearsHidden: validLayout.5, transition: .immediate)
}
}
func updateLayout(size: CGSize, defaultHeight: CGFloat, additionalHeight: CGFloat, leftInset: CGFloat, rightInset: CGFloat, appearsHidden: Bool, transition: ContainedViewLayoutTransition)
}
// TabBarController.swift
// layout in the main thread
open class TabBarController: ViewController {
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.tabBarControllerNode.containerLayoutUpdated(layout, toolbar: self.currentController?.toolbar, transition: transition)
...
}
}
// ListView.swift
// asynchronously load visible items by the scrolling event
open class ListView: ASDisplayNode, ... {
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateScrollViewDidScroll(scrollView, synchronous: false)
}
private func updateScrollViewDidScroll(_ scrollView: UIScrollView, synchronous: Bool) {
...
self.enqueueUpdateVisibleItems(synchronous: synchronous)
}
private func enqueueUpdateVisibleItems(synchronous: Bool) {
...
strongSelf.updateVisibleItemsTransaction(synchronous: synchronous, completion:...)
}
private func updateVisibleItemsTransaction(synchronous: Bool, completion: @escaping () -> Void)
}
- 所有布局都是手动完成的。显然,开发者不喜欢自动布局的概念。
- 布局计算在简单UI的主线程中运行。布局代码可以放在node的layout方法中,也可以放在视图控制器的containerLayoutUpdated方法中。
- ListView 为其item nodes构建灵活的布局机制,该机制支持同步和异步计算。
5.结论
Telegram集成AsyncDisplayKit的方式令人印象深刻。它在node之上重建整个UIKit组件生态,以提高效率和自由控制。尽管聊天气泡用户界面很复杂,但是聊天消息列表在旧设备上感觉很流畅。处理系统升级适配的代码很少,每年WWDC之后,大多数开发者总是要花一些“快乐的时光”。