原文:http://www.raywenderlich.com/4147/how-to-create-a-simple-magazine-app-with-core-text
本文作者Marin Todorov,拥有12年以上的软件开发经历,是一位独立的iOS开发者,同时是TouchCode 杂志的创建者。
Core Text是iOS 3.2和OSX10.5之后的文本引擎,能够精确控制文本的布局及样式。
它位于UIKit和CoreGraphics/Quartz之间:
- 使用UIKit,你可以通过简单拖拽将文本显示在UILabel上,但你无法单独控制文本中每个字的颜色。
- 使用Core Graphics/Quartz,你可以做到系统能够做到的一切,但你需要计算文本每个字符绘制在屏幕上的坐标。
- Core Text 恰巧位于二者之间。你能完全控制文字的位置、布局以及颜色大小等属性,但Core Text也为你省去了一些工作——例如从文本换行到字体的渲染。
在创建一个杂志或书刊类的应用时,Core Text尤其便利——而且在iPad上也工作得很好。
本文通过创建一本非常简单的杂志应用教你如何使用CoreText。你将学到:
- 在屏幕上显示格式文本
- 精确控制文本的外观
- 在文本内容中插入图片
- 最终创建一个杂志应用,并通过标记来控制文本的格式
在阅读本文之前,你需要有一点iOS开发基础。否则,你可以先阅读这个站点的其他教程。
让我们立即开始iPad杂志之旅。
创建 Core Text 项目打开Xcode,点击 File\New\New Project, 选择 iOS\Application\View-basedApplication, 点击 Next。项目名称命名为 CoreTextMagazine, Device family选择iPad, 点击 Next, 选择项目存放路径,然后点击 Create。
接下来将Core Text框架加到项目中。
Next thing you have to do is add the Core Text framework to theproject:
接下来编写一些代码。
添加一个 Core Text 视图为了便于使用Core Text,我们应当创建一个定制的UIView,然后在它的drawRect:方法中使用Core Text。
打开File\New\New File菜单,选择 iOS\Cocoa Touch\Objective-C class, 点击 Next。选择UIView的子类, 点击 Next, 类名命名为 CTView, 然后点击 Save。
在CTView.h 的 @interface 加入一下代码已包含 Core Text 框架:
#import <CoreText/CoreText.h>
接下来,设置新的定制的UIView作为应用程序的主视图。
在项目导航窗口中选择 “CoreTextMagazineViewController.xib”文件,转到Xcode的 Utilities 工具条,选择第3个按钮即 Identity 栏.
现在界面编辑器中选择view对象,在Utilities窗口的Class栏,可以看到UIView字样,将它改为 “CTView” 然后回车。
现在,应用程序启动时将显示定制的Core Text视图了。接下来,我们编写一点代码作为测试。
打开CTView.m 删除所有方法。输入以下代码,以便在视图中绘制一个内容为 “Hello world” 的文本:
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable(); //1
CGPathAddRect(path, NULL, self.bounds );
NSAttributedString* attString = [[[NSAttributedString alloc] initWithString:@"Hello core text world!"] autorelease]; //2
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString); //3
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attString length]), path, NULL);
CTFrameDraw(frame, context); //4
CFRelease(frame); //5
CFRelease(path);
CFRelease(framesetter);
}
让我们逐句讨论以上代码(序号和代码后面注释的行号对应):
注意,在使用Core text时,请使用一系列函数如 CTFramesetterCreateWithAttributedString 和CTFramesetterCreateFrame 而尽量避免直接使用 Objective-C 对象。
你可能会奇怪“为什么又要用C,为什么不用Objective-C?”
这是因为在iOS中的许多低级类库,为了高效和简洁起见,都是用C写的。不必担心,你会发现CoreText函数其实蛮简单的。
此外,还有一件值得注意的事情:在用“Create”函数创建对象之后,不要忘记同时要用CFRelease函数释放它们。
无论如何,用Core Text绘制一段简单文本就是这么多了。点击Run,查看运行结果。
出现了什么问题?就好比许多低级APIs,CoreText使用的是反Y轴坐标系。因此文本内容被渲染成倒立的。我们需要记住一点,如果我们把UIKit和Core Text的绘制方向搞混了,你会得到奇怪的结果。让我们来解决这个问题。在
“CGContextRef context =UIGraphicsGetCurrentContext();”一行后加入一下代码:
// Flip the coordinate system
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
代码很简单,仅仅是在当前视图的上下文中应用了一个转换(上下颠倒坐标系)。在每次绘制Core Text的时候复制粘贴这段代码。
点击Run——庆祝你的第一个CoreText程序吧!如果你搞不清什么是 CTFramesetter 和 CTFrame – 那么,我会在这里简单地解释一下Core Text是如何渲染文本内容的。
Core Text的对象模型如下图所示:首先用一个NSAttributedString做为参数创建一个CTFramesetter引用。这将自动创建一个CTTypesetter实例,它管理着你的字体。接下来使用这个CTFramesetter创建一个(或多个)文本块(CTFrame)。CTFrame用于渲染文本。
在创建文本块时,需要告诉它一个范围(NSRange),指定文本将在某个矩形内进行渲染。Core Text为每行文字自动创建一个CTLine,同时(注意这里)为每个CTLine中的每一段格式相同的文字创建一个CTRun。
例如,如果在一行中,有几个字是红色,另外几个字是黑色,剩余的字则为粗体,则Core Text将总共为它们创建3个CTRun。特别注意:不需要你来创建CTRun属性,Core Text会基于你提供的NSAttributedString来创建它们。
每个CTRun对象都采用各自的属性,因此你可以精确地控制诸如字距、连体、字宽、字重等属性。
创建杂志应用程序创建杂志应用程序,我们需要标记出每段文字的属性。我们可以用NSAttributedString的setAttributes:range方法,但未免不够灵活(除非你愿意老老实实地写上成打的代码)。
更简单地方法,是创建一种简单标记语法的解析器,通过它对杂志内容进行格式化。
点击File\New\New File, 选择 iOS\Cocoa Touch\Objective-C class, 然后点击Next。选择 NSObject for Subclass of, 点击 Next,为类命名为MarkupParser.m, 然后点击 Save。
在MarkupParser.h 中删除所有代码,编辑代码,声明属性和方法如下:
#import <Foundation/Foundation.h>
#import <CoreText/CoreText.h>
@interface MarkupParser : NSObject {
NSString* font;
UIColor* color;
UIColor* strokeColor;
float strokeWidth;
NSMutableArray* images;
}
@property (retain, nonatomic) NSString* font;
@property (retain, nonatomic) UIColor* color;
@property (retain, nonatomic) UIColor* strokeColor;
@property (assign, readwrite) float strokeWidth;
@property (retain, nonatomic) NSMutableArray* images;
-(NSAttributedString*)attrStringFromMarkup:(NSString*)html;
@end
打开MarkupParser.m 将其中内容替换为以下代码:
#import "MarkupParser.h"
@implementation MarkupParser
@synthesize font, color, strokeColor, strokeWidth;
@synthesize images;
-(id)init {
self = [super init];
if (self) {
self.font = @"Arial";
self.color = [UIColor blackColor];
self.strokeColor = [UIColor whiteColor];
self.strokeWidth = 0.0;
self.images = [NSMutableArray array];
}
return self;
}
-(NSAttributedString*)attrStringFromMarkup:(NSString*)markup { }
-(void)dealloc {
self.font = nil;
self.color = nil;
self.strokeColor = nil;
self.images = nil;
[super dealloc];
}
@end
如你所见,这个解析器的代码很简单——它包含的属性只有字体、字体颜色、删除线粗细、删除线颜色。后面我们将在文本中使用图片,因此用一个数组来存储这些图片。
通常写一个解析器不是件轻松的活儿,我将演示的是教你编写一个非常非常简单的解析器(使用正则式)。本教程的解析器非常简单,仅支持开放标签——例如,从一个标签后开始设置文本的样式,一直应用这个样式直到遇到新的标签。就像这段标记语言:
These are <font color="red">red<font color="black"> and <font color="blue">blue <font color="black">words.
将产生如下输出:
These are red and blue words.
对于本文而言,这就足够了。在你自己的项目中,你可以扩展它。
让我们开始吧!
在attrStringFromMarkup: 方法中,加入:
NSMutableAttributedString* aString =
[[NSMutableAttributedString alloc] initWithString:@""]; //1
NSRegularExpression* regex = [[NSRegularExpression alloc]
initWithPattern:@"(.*?)(<[^>]+>|\\Z)"
options:NSRegularExpressionCaseInsensitive|
NSRegularExpressionDotMatchesLineSeparators
error:nil]; //2
NSArray* chunks = [regex matchesInString:markup
options:0
range:NSMakeRange(0, [markup length])];
[regex release];
以上代码分为两步:
为什么要创建这个正则表达式?我们用它来搜索字符串中每个匹配的位置,然后:
1)渲染所找到的文本部分;
2)根据所找到标签改变当前样式。
整个过程重复进行,直至文本结束。
确实很简单,不是吗?
使用12版本时,导入eclipse的代码会出现如上问题,想了一下应该是编码问题。不罗嗦之间给出解决流程:
Setting->File Encodings->IDE Encoding选择utf-8,不要选择“system default”
同时,Default encoding for properties files 也选择 UTF-8
import java.util.*; /* 对给定数组进行排序。 {5,1,6,4,2,8,9} */ class ArrayTest2 { /* 选择排序。 内循环结束一次,最值出现头角标位置上。 */ public static void selectSort(int[] arr) { for (int x=0; x<arr.length-1 ; x++) { for(int y=x+1; y<arr.length; y++) { if(arr[x]>arr[y]) { /* int temp = arr[x]; arr[x] = arr[y]; arr[y]= temp; */ swap(arr,x,y); } } } } /* 冒泡排序 */ public static void bubbleSort(int[] arr) { for(int x=0; x<arr.length-1; x++) { for(int y=0; y<arr.length-x-1; y++)//-x:让每一次比较的元素减少,-1:避免角标越界。 { if(arr[y]<arr[y+1]) { /* int temp = arr[y]; arr[y] = arr[y+1]; arr[y+1] = temp; */ swap(arr,y,y+1); } } } } /* 发现无论什么排序。都需要对满足条件的元素进行位置置换。 所以可以把这部分相同的代码提取出来,单独封装成一个函数。 */ public static void swap(int[] arr,int a,int b) { int temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; } public static void main(String[] args) { int[] arr = {5,1,6,4,2,8,9}; //排序前; printArray(arr); //排序 //selectSort(arr); //bubbleSort(arr); //Arrays.sort(arr);//java中已经定义好的一种排序方式。开发中,对数组排序。要使用该句代码。 //排序后: printArray(arr); } public static void printArray(int[] arr) { System.out.print("["); for(int x=0; x<arr.length; x++) { if(x!=arr.length-1) System.out.print(arr[x]+", "); else System.out.println(arr[x]+"]"); } } }