我知道的有两种方法可以实现图文混排,一种是UIWebView实现,一种是用coreText实现
第一、使用UIWebView实现图文混排
1、创建两个视图控制器vc1和vc2,并且分别添加一个UIWebView到self.view上。
(1)关闭webView的回弹效果
- (void)clearWebViewBackground:(UIWebView*)webView
{
UIWebView * web = webView;
for (id obj in web.subviews)
{
if ([obj isKindOfClass:[UIScrollView class]])
{
[obj setBounces:NO];
}
}
}
(2)设置
NSString *linkStr=[NSString stringWithFormat:@"<a href='%@'>我的博客</a> <a href='%@'>原文</a>",@"http://blog.youkuaiyun.com/wildcatlele",@"http://jincuodao.baijia.baidu.com/article/26059"];
];NSString *p1=@"韩寒《后会无期》的吸金能力很让我惊讶!8月12日影片票房已成功冲破6亿大关。而且排片量仍保持10 以上,以日收千万的速度稳步向七亿进军。"; NSString *p2=@"要知道,《后会无期》不是主流类型片,是一个文艺片。不像《小时代》,是一个商业主流的偶像电影。"; NSString *image1=[NSString stringWithFormat:@"<img src='%@' height='280' width='300' />",@"http://nvren.so/uploads/allimg/c140801/140DR4554L40-YB9.jpg"]; NSString *image2=[NSString stringWithFormat:@"<img src='%@' height='280' width='300' />",@"http://f.hiphotos.baidu.com/news/w%3D638/sign=78315beeb1fb43161a1f797918a44642/2934349b033b5bb58cb61bdb35d3d539b600bcb5.jpg"]; NSString *p3=@"太奇葩了!有人说,这是中国电影市场的红利,是粉丝电影的成功。但是,有一部投资3000万的粉丝电影《我就是我》,有明星,制作也不错,基本上是惨败。"; NSString *p4=@"《后会无期》卖的不是好故事,是优越感。特别是针对80、90后的人群,你有没有发现,看《后会无期》比看《小时代3》有明显的优越感。故事虽然一般,但是很多人看完后,会在微博、微信上晒照片。所以说,对一个族群靠的不是广度,而是深度。<br>\ \ 很凶残,值得大家借鉴。韩寒《后会无期》还有什么秘密武器,欢迎《后会无期》团队或相关方爆料,直接留言即可,有料的可以送黎万强亲笔签名的《参与感》一书。"; //初始化和html字符串 NSString *htmlURlStr=[NSString stringWithFormat:@"<body style='background-color:#EBEBF3'><h2>%@</h2><p>%@</p> <p>%@ </p>%@ <br><p> %@</p> <p>%@</p>%@<p>%@</p></body>",title,linkStr,p1,image1,p2,p3,image2,p4]; self.myWebView.delegate = self; [self.myWebView loadHTMLString:htmlURlStr baseURL:nil]; }
(3)实现代理方法,处理点击事件
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
NSString * urlStr = request.URL.absoluteString;
NSLog(@"%@",urlStr);
//为空,第一次加载本页面
if([urlStr isEqualToString:@"about.blank"])
{
return YES;
}
//设置点击后的视图控制器
viewController2 * vc2 = [[viewController2 alloc] init];
vc2.originUrl = urlStr;//设置请求连接
//跳转到点击后的控制器并加载WebView
[self.navigationController pushViewController:vc2 animated:YES];
return NO;
}
(4)实现vc2
#important "viewController2.h"
@interface viewController2()
{
UIWebView * _webView;
}
@end;
@implementation viewController2
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibName bundle:nibBundleOrNil];
if (self){
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = @"原文";
_webView = [[UIWebView alloc]init];
_webView.frame =[ [UIScreen mainScreen] bounds];
[self.view addSubView:_webView];
[_webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:self.originUrl]]];
}
第二、CoreText(原博文:http://www.brighttj.com/ios/use-coretext-make-typesetting-picture-and-text.html)
(1)CoreText与UIWebView在排版方面的优劣比较
·CoreText占用的内容更少,渲染速度更快。UIWebView占用的内存多,渲染速度慢。
·CoreText在渲染界面以前就可以精确的获得显示内容的高度(只要有了CTFrame即可),而WebView只有渲染出内容后,才能获得内容的高度(而且还需要javaScript代码来获取)。
·CoreText的CTFrame可以在后台线程渲染,UIWebView的内容只能在主线程(UI线程)渲染。
·基于CoreText可以做更好的原生交互效果,交互效果可以更加细腻,而UIWebView得交互效果都是用JavaScript来实现的,在交互效果上会有一些卡顿的情况存在。
CoreText排版劣势:
CoreText渲染出来的内容不能像UIWebView那样方便的支持内容复制。
基于CoreText来排版需要自己处理很多复制的逻辑,例如需要自己处理图片与文字混排相关的逻辑,也需要自己实现连接点击操作的支持。
在业界有很多应用都采用CoreText技术进行排版,例如新浪微博客户端,多看阅读客户端等。
(2)代码
我们创建一个继承于UIView的类,重写他的drawRect方法,来绘制纯文本。
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
//步骤1:得到当前用于绘制画布的上下文,用于后续将内容绘制在画布上
//因为CoreText要配合Core Graphic配合使用,如Core Graphics一样,绘图的时候需要获得当前的上下文进行绘制。
CGContextRef context = UIGraphicsGetCurrentContext();
//步骤2:翻转当前的坐标系(因为对于底层绘制引擎来说,屏幕左下角为(0,0))
CGContextSetTextMatrix(context,CGAffineTransformIdentity);
CGContextTranslateCTM(context,0,self.bounds.size.height);
CGContextScaleCTM(context,1.0,-1.0);
//步骤3:创建绘制区域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddEllipseInRect(path,NULL,self.bounds);
//步骤4:创建需要绘制的文字与计算需要绘制的区域
NSMutableAttributeString * attriString = [[NSMutableAttributeString alloc]initWithString:@"iOS程序在启动时会创建一个主线程,而在一个线程只能执行一件事情,如果在主线程执行某些耗时操作,例如加载网络图片,下载资源文件等会阻塞主线程(导致界面卡死,无法交互),所以就需要使用多线程技术避免这类情况。iOS中有三种多线程技术 NSThread,NSOperation,GCD,这三种技术是随着iOS发展引入的。"];
//步骤5:根据AttributedString生成CTFramessetterRef
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributeStringRef)attrString);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter,CFRangeMake(0,[attrString length]),path,NULL);
//步骤6:进行绘制
CTFrameDraw(frame,context);
//步骤7:内存管理
CFRelease(frame);
CFRelease(path);
CFRelease(frameSetter);
}
(3)关于坐标系
上述的步骤2对绘图的坐标系进行了处理,因为在iOS UIKit中,UIView是以左上角为原点,而CoreText一开始的定位是使用与桌面应用的排版系统,桌面应用的坐标系是以左下角为原点,也就是说CoreText在绘制的时候也是参照左下角为原点进行绘制的,所以需要对当前的坐标系进行处理。
实际上,Core Graphics中的context也是以左下角为原点的,但是为什么我们用CoreGraphics绘制一些简单的图形的时候不需要对坐标系进行处理,是因为通过这个方法UIGraphicsGetCurrentContext()来获得的当前context是已经被处理过的,用下面方法可以查看指定的上下文的当前图形状态变换矩阵。
NSLog(@"当前context的变换矩阵%@",NSStringFromCGAffineTransform(CGContextGetCTM(context)));
函数CGContextTranslateCTM的作用变换坐标系的原点,函数CGContextScaleCTM的作用是改变用户坐标系统的规模比例。
(4)自定义文本的颜色,字体与行间距
可以看到我们使用了NSMutableAttributedString这个类来描述需要绘制的文字,而一个NSMutableAttributedString对象可以包含很多属性,每一个属性都有起对应的字符区域,我们可以用这些属性来描述文本中特殊的颜色和字体。
- (void)drawRect:(CGRect)rect
{
//在上一部分的步骤1-4
[super drawRect:rect];
//步骤1:得到当前用于绘制画布的上下文,用于后续将内容绘制在画布上
//因为CoreText要配合Core Graphic配合使用,如Core Graphics一样,绘图的时候需要获得当前的上下文进行绘制。
CGContextRef context = UIGraphicsGetCurrentContext();
//步骤2:翻转当前的坐标系(因为对于底层绘制引擎来说,屏幕左下角为(0,0))
CGContextSetTextMatrix(context,CGAffineTransformIdentity);
CGContextTranslateCTM(context,0,self.bounds.size.height);
CGContextScaleCTM(context,1.0,-1.0);
//步骤3:创建绘制区域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddEllipseInRect(path,NULL,self.bounds);
//步骤4:创建需要绘制的文字与计算需要绘制的区域
NSMutableAttributeString * attriString = [[NSMutableAttributeString alloc]initWithString:@"iOS程序在启动时会创建一个主线程,而在一个线程只能执行一件事情,如果在主线程执行某些耗时操作,例如加载网络图片,下载资源文件等会阻塞主线程(导致界面卡死,无法交互)");
//步骤8:设置部分文字颜色
[attrString addAttribute:(id)kCTForegroundColorAttributeName value:[UIColor greenColor] range:NSMakeRange(10,10)];
//设置部分文字
CGFloat fontSize = 20;
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT",fontSize,NULL);
[attrString addAttribute:(id)kCTFontAttributeName value:(__bridge id)fontRef range:NSMakeRange(15,10)];
CFRelease(fontRef);
//设置行间距
CGFloat lineSpacing = 10;
const CFIndex kNumberOfSettings = 3;
CTParagraphStyleSetting theSettings[kNumberOfSettings]={
{kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpacing},
{kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpacing},
{kCTParagraphStyleSpecifierMinmumLineSpacing,sizeof(CGFloat),&lineSpacing}
};
CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSetting,kNumberOfSettings);[attrString addAttribute:(id)kCTParagraphStyleAttributeName value:(__bridge id)theParagraphRef range:NSMakeRange(0,attrString.length)];
CFRelease(theParagraphRef);
//省略之后的5-7;
}
注:(__bridge id):oc方法oc对象与C语言的类型相互转换和操作。
(5)开始真正的图文混排
CFAttributeStringRef:属性字符串,用于存储需要绘制的文字字符和字符属性
CTFramesetterRef:通过CFAttributedStringRef进行初始化,作为CTFrame对象的生产工厂,负责很据path创建对应的CTFrame。
CTFrame:用于绘制文字的类,可以通过CTFrameDraw函数,直接将文字绘制到context上
CTLine:在CTFrame内部是由多个CTLine来组成的,每个CTLine代表一行
CTRun:每个CTLine又是由多个CTRun组成,每个CTRun代表一组显示风格一致的文本。
实际上CoreText是不直接支持绘制图片的,但是我们可以现在需要显示图片的地方用一个特殊的空白占位符代替,同时设置该字体的CTRunDelegate信息为要显示的图片的宽度和高度,这样绘制文字的时候就会先把图片的位置留出来,再在drawRect方法里面用CGContextDrawImage绘制图片。
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
// 省略步骤1-4 ,步骤8
// 步骤9:图文混排部分
// CTRunDelegateCallbacks:一个用于保存指针的结构体,由CTRun delegate进行回调
CTRunDelegateCallbacks callbacks;
memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
callbacks.version = kCTRunDelegateVersion1;
callbacks.getAscent = ascentCallback;
callbacks.getDescent = descentCallback;
callbacks.getWidth = widthCallback;
// 图片信息字典
NSDictionary *imgInfoDic = @{@"width":@100,@"height":@30};
// 设置CTRun的代理
CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)imgInfoDic);
// 使用0xFFFC作为空白的占位符
unichar objectReplacementChar = 0xFFFC;
NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1];
NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
CFRelease(delegate);
// 将创建的空白AttributedString插入进当前的attrString中,位置可以随便指定,不能越界
[attrString insertAttributedString:space atIndex:50];
// 省略步骤5-6
// 步骤10:绘制图片
UIImage *image = [UIImage imageNamed:@"coretext-img-1.png"];
CGContextDrawImage(context, [self calculateImagePositionInCTFrame:frame], image.CGImage);
// 省略步骤7
}
#pragma mark - CTRun delegate 回调方法
static CGFloat ascentCallback(void *ref) {
return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"height"] floatValue];
}
static CGFloat descentCallback(void *ref) {
return 0;
}
static CGFloat widthCallback(void *ref) {
return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width"] floatValue];
}
/**
* 根据CTFrameRef获得绘制图片的区域
*
* @param ctFrame CTFrameRef对象
*
* @return绘制图片的区域
*/
- (CGRect)calculateImagePositionInCTFrame:(CTFrameRef)ctFrame {
// 获得CTLine数组
NSArray *lines = (NSArray *)CTFrameGetLines(ctFrame);
NSInteger lineCount = [lines count];
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), lineOrigins);
// 遍历每个CTLine
for (NSInteger i = 0 ; i < lineCount; i++) {
CTLineRef line = (__bridge CTLineRef)lines[i];
NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
// 遍历每个CTLine中的CTRun
for (id runObj in runObjArray) {
CTRunRef run = (__bridge CTRunRef)runObj;
NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
if (delegate == nil) {
continue;
}
NSDictionary *metaDic = CTRunDelegateGetRefCon(delegate);
if (![metaDic isKindOfClass:[NSDictionary class]]) {
continue;
}
CGRect runBounds;
CGFloat ascent;
CGFloat descent;
runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
runBounds.size.height = ascent + descent;
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
runBounds.origin.x = lineOrigins[i].x + xOffset;
runBounds.origin.y = lineOrigins[i].y;
runBounds.origin.y -= descent;
CGPathRef pathRef = CTFrameGetPath(ctFrame);
CGRect colRect = CGPathGetBoundingBox(pathRef);
CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
return delegateBounds;
}
}
return CGRectZero;
}
参考资料:
http://geeklu.com/2013/03/core-text/
http://blog.devtang.com/blog/2013/10/21/the-tech-detail-of-ape-client-3/
http://www.yifeiyang.net/development-of-the-iphone-simply-6/