前言

本人今年主要在负责猿题库iOS客户端的开发,本文旨在通过分享猿题库iOS客户端开发过程中的技术细节,达到总结和交流的目的。

这是本技术分享系列文章的第三篇。本文涉及的技术细节是:基于CoreText的排版引擎。

CoreText概述

因为猿题库的做题和解析界面需要复杂的排版,所以我们基于CoreText实现了自己的富文本排版引擎。我们的排版引擎对公式、图片和链接有着良好支持,并且支持各种字体效果混排。对于内容中的图片,支持点击查看大图功能,对于内容中的链接,支持点击操作。

下图是我们应用的一个截图,可以看到公式,图片与文字混排良好。

对于富文本排版,除了可以用CoreText实现外,还可以用UIWebView实现。我以前写过一篇介绍如何用UIWebView进行复杂内容显示和交互的文章《关于UIWebView和PhoneGap的总结》,里面介绍了使用UIWebView如何处理参数传递,同步与异步等问题,感兴趣的同学也可以翻看。

基于CoreText来实现和基于UIWebView来实现相比,前者有以下好处:

  1. CoreText占用的内存更少,UIWebView占用的内存更多。
  2. CoreText在渲染界面前就可以精确地获得显示内容的高度(只要有了CTFrame即可),而UIWebView只有渲染出内容后,才能获得内容的高度(而且还需要用javascript代码来获取)
  3. CoreText的CTFrame可以在后台线程渲染,UIWebView的内容只能在主线程(UI线程)渲染。
  4. 基于CoreText可以做更好的原生交互效果,交互效果可以更细腻。而UIWebView的交互效果都是用javascript来实现的,在交互效果上会有一些卡顿存在。例如,在UIWebView下,一个简单的按钮按下效果,都无法做到原生按钮的即时和细腻的按下效果。

当然基于CoreText的方案也有一些劣势:

  1. CoreText渲染出来的内容不能像UIWebView那样方便地支持内容的复制。
  2. 基于CoreText来排版,需要自己处理图片排版相关的逻辑,也需要自己处理链接点击操作的支持。

我们最初的猿题库行测第一版采用了基于UIWebView来实现,但是做出来发现一些小的交互细节无法做到精致。所以后来的第二版我们就全部转成用CoreText实现,虽然实现成本上增加了不少,但是应用的交互效果好多了。

使用CoreText也为我们后来的iPad版提供了技术积累,因为iPad版的页面排版更加复杂,用UIWebView是完全无法完成相应的交互和排版需求的。

关于如何基于CoreText来做一个排版引擎,我主要参考的是这篇教程:《Core Text Tutorial for iOS: Making a Magazine App》 以及Nimbus 中的NIAttributeLabel.m 的实现,在这里我就不重复教程中的内容了,我主要讲一些实现细节。

实现细节

服务端接口

我们在后台实现了一个基于UBB 的富文本编译器。使用UBB的原因是:

  1. UBB相对于HTML来说,虽然功能较简单,但是能完全满足我们对于富文本排版的需求。
  2. 做一个UBB的语法解析器比较简单,便于我们将UBB渲染到各个平台上。

为了简化iOS端的实现,我们将UBB的语法解析在服务器端完成。服务器端提供了接口,可以直接获得将UBB解析成类似HTML的文件对象模型(DOM) 的树型数据结构。有了这个树型数据结构,iOS端渲染就简单多了,无非就是递归遍历树型节点,将相关的内容转换成 NSAttributeString即可,之后将NSAttrubiteString转成CoreText的CTFrame即可用于界面的绘制。

支持图文混排

支持图文混排在教程:《Core Text Tutorial for iOS: Making a Magazine App》 中有介绍,我们在解析DOM树遇到图片节点时,则将该内容转成一个空格,随后设置该空格在绘制时,需要我们自己指定宽高相关信息,而宽高信息在图片节点中都有提供。这样,CoreText引擎在绘制时,就会把相关的图片位置留空,之后我们将图片异步下来下来后,使用CoreGraph相关的API将图片再画在界面上,就实现了图文混排功能。

下面的相关的示例代码:

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  1. /* Callbacks */
  2. static void deallocCallback( void* ref ){
  3. [(id)ref release];
  4. }
  5. static CGFloat ascentCallback( void *ref ){
  6. CGFloat height = [(NSString*)[(NSDictionary*)ref objectForKey:@"height"] floatValue];
  7. return height/2 + [FrameParserConfig sharedInstance].baselineFromMid;
  8. }
  9. static CGFloat descentCallback( void *ref ){
  10. CGFloat height = [(NSString*)[(NSDictionary*)ref objectForKey:@"height"] floatValue];
  11. return height/2 - [FrameParserConfig sharedInstance].baselineFromMid;
  12. }
  13. static CGFloat widthCallback( void* ref ){
  14. return [(NSString*)[(NSDictionary*)ref objectForKey:@"width"] floatValue];
  15. }
  16. + (void)appendDelegateData:(NSDictionary *)delegateData ToString:(NSMutableAttributedString*)contentString {
  17. //render empty space for drawing the image in the text //1
  18. CTRunDelegateCallbacks callbacks;
  19. callbacks.version = kCTRunDelegateCurrentVersion;
  20. callbacks.getAscent = ascentCallback;
  21. callbacks.getDescent = descentCallback;
  22. callbacks.getWidth = widthCallback;
  23. callbacks.dealloc = deallocCallback;
  24. CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, delegateData);
  25. [delegateData retain];
  26. // Character to use as recommended by kCTRunDelegateAttributeName documentation.
  27. // use " " will lead to wrong width in CTFramesetterSuggestFrameSizeWithConstraints
  28. unichar objectReplacementChar = 0xFFFC;
  29. NSString * objectReplacementString = [NSString stringWithCharacters:&objectReplacementChar length:1];
  30. NSDictionary * attributes = [self getAttributesWithStyleArray:nil];
  31. //try to apply linespacing attributes to this placeholder
  32. NSMutableAttributedString * space = [[NSMutableAttributedString alloc] initWithString:objectReplacementString attributes:attributes];
  33. CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
  34. CFRelease(delegate);
  35. [contentString appendAttributedString:space];
  36. [space release];
  37. }

这里需要注意的是,用来代替图片的占位符使用空格会带来排版上的异常,具体原因未知,我们猜测是CoreText的bug,参考Nimbus 的实现后,我们使用 0xFFFC作为占位符,就没有遇到问题了。

支持链接

支持链接点击的主要实现的方式是:

  1. 在解析DOM树的时候,记录下链接串在整个富文本中的位置信息(包括offset和length)。
  2. 在CoreText渲染到的view上,监听用户操作事件,使用 CTLineGetStringIndexForPosition函数来获得用户点击的位置对应 NSAttributedString 字符串上的位置信息(index) 3.判断第2步得到的index是否在第一步记录的各个链接的区间范围内,如果在范围内,则表示用户点击了某一个链接。

这段逻辑的关键代码如下:

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  1. // test touch point is on link or not
  2. + (LinkData *)touchLinkInView:(UIView *)view atPoint:(CGPoint)point data:(CTTableViewCellData *)data {
  3. CTFrameRef textFrame = data.ctFrame;
  4. CFArrayRef lines = CTFrameGetLines(textFrame);
  5. if (!lines) return nil;
  6. CFIndex count = CFArrayGetCount(lines);
  7. LinkData *foundLink = nil;
  8. CGPoint origins[count];
  9. CTFrameGetLineOrigins(textFrame, CFRangeMake(0,0), origins);
  10. // CoreText context coordinates are the opposite to UIKit so we flip the bounds
  11. CGAffineTransform transform = CGAffineTransformScale(CGAffineTransformMakeTranslation(0, view.bounds.size.height), 1.f, -1.f);
  12. for (int i = 0; i < count; i++) {
  13. CGPoint linePoint = origins[i];
  14. CTLineRef line = CFArrayGetValueAtIndex(lines, i);
  15. CGRect flippedRect = [self getLineBounds:line point:linePoint];
  16. CGRect rect = CGRectApplyAffineTransform(flippedRect, transform);
  17. if (CGRectContainsPoint(rect, point)) {
  18. CGPoint relativePoint = CGPointMake(point.x-CGRectGetMinX(rect),
  19. point.y-CGRectGetMinY(rect));
  20. CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint);
  21. foundLink = [self linkAtIndex:idx linkArray:data.linkArray];
  22. return foundLink;
  23. }
  24. }
  25. return nil;
  26. }

基于CoreText的内容省略

我们在使用CoreText时,还遇到一个具体排版上的问题。正常情况下,在生成CTFrame之后,只需要调用:CTFrameDraw(self.data.ctFrame, context);即可完成界面的绘制。但是产品提出了一个需求,对于某些界面,当显示不下的时候,需要将多余内容用...来表示。这让我们的绘制逻辑需要特别处理,以下是具体的实现:

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  1. static NSString* const kEllipsesCharacter = @"\u2026";
  2. CGPathRef path = CTFrameGetPath(_data.ctFrame);
  3. CGRect rect = CGPathGetBoundingBox(path);
  4. CFArrayRef lines = CTFrameGetLines(_data.ctFrame);
  5. CFIndex lineCount = CFArrayGetCount(lines);
  6. NSInteger numberOfLines = MIN(_numberOfLines, lineCount);
  7. CGPoint lineOrigins[numberOfLines];
  8. CTFrameGetLineOrigins(_data.ctFrame, CFRangeMake(0, numberOfLines), lineOrigins);
  9. NSAttributedString *attributedString = _data.attributedString;
  10. for (CFIndex lineIndex = 0; lineIndex < numberOfLines; lineIndex++) {
  11. CGPoint lineOrigin = lineOrigins[lineIndex];
  12. lineOrigin.y = self.frame.size.height + (lineOrigin.y - rect.size.height);
  13. CGContextSetTextPosition(context, lineOrigin.x, lineOrigin.y);
  14. CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
  15. BOOL shouldDrawLine = YES;
  16. if (lineIndex == numberOfLines - 1) {
  17. CFRange lastLineRange = CTLineGetStringRange(line);
  18. if (lastLineRange.location + lastLineRange.length < (CFIndex)attributedString.length) {
  19. CTLineTruncationType truncationType = kCTLineTruncationEnd;
  20. NSUInteger truncationAttributePosition = lastLineRange.location + lastLineRange.length - 1;
  21. NSDictionary *tokenAttributes = [attributedString attributesAtIndex:truncationAttributePosition
  22. effectiveRange:NULL];
  23. NSAttributedString *tokenString = [[NSAttributedString alloc] initWithString:kEllipsesCharacter
  24. attributes:tokenAttributes];
  25. CTLineRef truncationToken = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)tokenString);
  26. NSMutableAttributedString *truncationString = [[attributedString attributedSubstringFromRange:NSMakeRange(lastLineRange.location, lastLineRange.length)] mutableCopy];
  27. if (lastLineRange.length > 0) {
  28. // Remove any whitespace at the end of the line.
  29. unichar lastCharacter = [[truncationString string] characterAtIndex:lastLineRange.length - 1];
  30. if ([[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:lastCharacter]) {
  31. [truncationString deleteCharactersInRange:NSMakeRange(lastLineRange.length - 1, 1)];
  32. }
  33. }
  34. [truncationString appendAttributedString:tokenString];
  35. CTLineRef truncationLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)truncationString);
  36. CTLineRef truncatedLine = CTLineCreateTruncatedLine(truncationLine, self.size.width, truncationType, truncationToken);
  37. if (!truncatedLine) {
  38. // If the line is not as wide as the truncationToken, truncatedLine is NULL
  39. truncatedLine = CFRetain(truncationToken);
  40. }
  41. CFRelease(truncationLine);
  42. CFRelease(truncationToken);
  43. CTLineDraw(truncatedLine, context);
  44. CFRelease(truncatedLine);
  45. shouldDrawLine = NO;
  46. }
  47. }
  48. if (shouldDrawLine) {
  49. CTLineDraw(line, context);
  50. }
  51. }

后记

以上源码很多都参考了Nimbus的实现,在此再一次表达一下对开源社区的感谢。

在大约2年前,CoreText还是一个新玩意。那时候微博的界面都还是用控件组合得到的。慢慢的,大家都开始接受CoreText,很多应用都广泛地将CoreText应用于自己的界面中,做出来了更加复杂的排版、交互效果。在iOS7之后,苹果推出了更加易于使用的TextKit,使得富文本排版更加容易,相信以后的iOS应用界面会更加美观,交互更加绚丽。

转自:http://blog.devtang.com/blog/2013/10/21/the-tech-detail-of-ape-client-3/

Posted by 唐巧 Oct 21st, 2013  iOS

原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0

基于CoreText的排版引擎的更多相关文章

  1. iOS:基于CoreText的排版引擎

    一.CoreText的简介 CoreText是用于处理文字和字体的底层技术.它直接和Core Graphics(又被称为Quartz)打交道.Quartz是一个2D图形渲染引擎,能够处理OSX和iOS ...

  2. 浏览器内核、排版引擎、js引擎

    [定义] 浏览器最重要或者说核心的部分是“Rendering Engine”,可大概译为“渲染引擎”,不过我们一般习惯将之称为“浏览器内核”.负责对网页语法的解释(如标准通用标记语 言下的一个应用HT ...

  3. Atitit 基于dom的游戏引擎

    Atitit 基于dom的游戏引擎 1. 添加sprite控件(cocos,createjs,dom)1 1.1.1. Cocos1 1.1.2. createjs1 1.1.3. Dom模式2 1. ...

  4. 第四十二课:基于CSS的动画引擎

    由于低版本浏览器不支持css3 animation,因此我们需要根据浏览器来选择不同的动画引擎.如果浏览器支持css3 animation,那么就使用此动画引擎,如果不支持,就使用javascript ...

  5. 【转】Spark是基于内存的分布式计算引擎

    Spark是基于内存的分布式计算引擎,以处理的高效和稳定著称.然而在实际的应用开发过程中,开发者还是会遇到种种问题,其中一大类就是和性能相关.在本文中,笔者将结合自身实践,谈谈如何尽可能地提高应用程序 ...

  6. Lucene:基于Java的全文检索引擎简介

    Lucene:基于Java的全文检索引擎简介 Lucene是一个基于Java的全文索引工具包. 基于Java的全文索引/检索引擎--Lucene Lucene不是一个完整的全文索引应用,而是是一个用J ...

  7. 基于MySQL的Activiti6引擎创建

    整个activiti6的搭建都是在spring boot2之上的,首先贴一下pom: <dependencies> <!-- 这是activiti需要的最基本的核心引擎 --> ...

  8. 随着应用对事务完整性和并发性要求的不断提高,MySQL才开始开发基于事务的存储引擎

    MYSQL 解锁与锁表 - 专注it - 博客园 https://www.cnblogs.com/wanghuaijun/p/5949934.html 2016-10-11 16:50 MYSQL 解 ...

  9. Rendering Engine 主流的浏览器内核(排版引擎、渲染引擎、解释引擎)有哪几种,分别的特点

    一.A web browser engine A rendering engine is software that draws text and images on the screen. The ...

随机推荐

  1. hdu1877进制转换

    #include <stdio.h> int m; void Ck(int n) { if(n>=m) Ck(n/m); printf("%d",n%m); } ...

  2. P3200 [HNOI2009]有趣的数列

    题目描述 我们称一个长度为2n的数列是有趣的,当且仅当该数列满足以下三个条件: (1)它是从1到2n共2n个整数的一个排列{ai}: (2)所有的奇数项满足a1<a3<...<a2n ...

  3. Codeforces 585D Lizard Era: Beginning | 折半搜索

    参考这个博客 #include<cstdio> #include<algorithm> #include<cstring> #include<map> ...

  4. 仿今日头条按钮loading效果

    效果 代码: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UT ...

  5. Tomcat给我的java.lang.OutOfMemoryError: PermGen

    今天,Tomcat给了我这么一个异常:java.lang.OutOfMemoryError: PermGen space.自己是第一次遇到,抱着好奇的心情google了一下,居然是个很常见的异常!故记 ...

  6. C中堆和栈的区别

    C++中堆和栈的完全解析 内存分配方面: 堆: 操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删 除,并 ...

  7. 一个C优先级队列实现

    刚下班没事干,实现了一个简单的优先级队列 #include <stdlib.h>#include <stdio.h> typedef void (*pqueue_setinde ...

  8. usb驱动的基本结构和函数简介【转】

    转自:http://blog.csdn.net/jeffade/article/details/7698404 几个重要的结构 struct--接口 struct usb_interface { /* ...

  9. (八)ubuntu安装软件提示:Could not get lock /var/lib/dpkg/lock - open (11: Resource temporarily unavailable)

    question: ubuntu@ubuntu:/usr/src/Linux-headers-3.5.0-23-generic$ sudo apt-get install cheese [sudo] ...

  10. myeclipse10.7配置resin4.0.36

    Resin-4.0.35 (built Tue, 12 Feb 2013 10:05:50 PST) Copyright(c) 1998-2012 Caucho Technology.  All ri ...