保存到桌面加入收藏设为首页
IOS开发
当前位置:首页 > IOS开发

iOS阅读器实践系列(三)图文混排-安度博客

时间:2019-02-08 15:56:28   作者:   来源:   阅读:139   评论:0
内容摘要:本篇介绍coretext中的图文混排,这里暂用静态的内容,即在文本中某一固定位置插入图片,而不是插入位置是根据文本内容动态插入的(要实现这一效果需要写一个文本解析器,将原信息内容解析为某些特定格式的结构来标示出特定的类型(比如文字、图片、链接等),然后按照其结构中的属性配置,生成......
  • 本篇介绍coretext中的图文混排,这里暂用静态的内容,即在文本中某一固定位置插入图片,而不是插入位置是根据文本内容动态插入的(要实现这一效果需要写一个文本解析器,将原信息内容解析为某些特定格式的结构来标示出特定的类型(比如文字、图片、链接等),然后按照其结构中的属性配置,生成属性字符串,之后渲染到视图中)。

    这部分的思路参考唐巧大神的blog。

    在第一篇介绍过coretext是离屏渲染的,即在将内容渲染到屏幕上之前,coretext已完成排版工作。coretext排版的第一步是组织数据,即由原始字符串通过特定的配置来得到属性字符串。实现在文字中插入图片的大部分工作都是在这一步中完成的。

    大体思路是:首先将原始纯文本数据通过预定义的配置生成相应的属性字符串A,然后生成一个字符作为占位符(绘制时在这个占位符中填充图片),根据特定的配置生成属性字符串B,然后将B插入到A相应位置,最后将合并后的A和图片渲染到视图中。

    具体步骤如下:

    一、创建存储A的数据结构CoreTextData与存储B的数据结构CTImgData

    二、生成属性字符串A

    三、生成属性字符串B

    四、检测图片位置,以便后续对图片操作进行处理

    1、创建存储A的数据结构CoreTextData与存储B的数据结构CTImgData

    在实际开发中我们需要一个结构来存储排版和业务的一些数据,首先介绍CoreTextData:

    @interface CoreTextData : NSobject@property (assign nonatomic) CTframeRef ctframe;@property (assign nonatomic) CGFloat height;@property (strong nonatomic) NSMutableAttributedString *content;@property (nonatomic strong) NSMutableArray *imgDataArray;@property (nonatomic assign) NSInteger characterNum;@end

    代码中列出了,排版和业务上可能用的一些数据(当然你完全可以根据自己的需求来定义结构,这里只是举了一些我觉得可能用到的字段),其中CTframe用于文本渲染,height记录的属性字符串排版时所需的高度,content保存了文本内容,imgDataArray是保存CTImgData的数组,characterNum记录的content的字数。

    CoreTextImgData结构:

    @interface CoreTextImgData : NSobject@property (strong nonatomic) NSString *name;@property (nonatomic) NSUInteger position;@property (nonatomic assign) CGFloat leftMargin;@property (nonatomic assign) CGFloat topMargin;//坐标系为coreText坐标系,而不是UIKit坐标系@property (nonatomic) CGRect imgPosition;@property (nonatomic assign) BOOL isResponseTap;- (void)handleImgTapped:(NSInteger)chapterId chapterTitle:(NSString *)title;@end

    name表示所用图片的名字,position表示图片在文本中的字符索引,leftMargin和topMargin用于排版时图片位置的调整(距左边与上边的间隔),图片在视图中坐标(coretext坐标系),isResponseTap表示图片是否响应点击,下面的方法是处理图片的事件的,后面介绍。

    生成文本与图片的属性字符串

    contentString = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes];//bottom line CGFloat bottomLineW = viewWidth - 20; NSDictionary *bottomImgDict = [NSDictionary dictionaryWithobjectsAndKeys:[NSNumber numberWithFloat:bottomLineW] @'width' [NSNumber numberWithFloat:1] @'height' @'Line' @'imgName' nil]; CoreTextImgData *bottomImgData = [[CoreTextImgData alloc] init]; bottomImgData.position = contentString.length; bottomImgData.name = bottomImgDict[@'imgName']; [imgDataArray addobject:bottomImgData]; NSDictionary *bottomImgAttributes = [manager getAditionLineAttribute]; NSAttributedString *bottomLineContent = [self getImageAttributeContentWithDictionary:bottomImgDict attribute:bottomImgAttributes isLineFeed:YES imgName:bottomImgData.name imgData:bottomImgData leftMargin:10 topMargin:0]; [contentString appendAttributedString:bottomLineContent];
    + (NSAttributedString *)getImageAttributeContentWithDictionary:(NSDictionary *)dict attribute:(NSDictionary *)attribute isLineFeed:(BOOL)isLineFeed imgName:(NSString *)imgName imgData:(CoreTextImgData *)imgData leftMargin:(CGFloat)leftMargin topMargin:(CGFloat)topMargin{ CTRunDelegateCallbacks callbacks; memset(&callbacks 0 sizeof(CTRunDelegateCallbacks)); callbacks.version = kCTRunDelegateVersion1; callbacks.getAscent = ascentCallback; callbacks.getDescent = descentCallback; callbacks.getWidth = widthCallback; CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks (__bridge void * _Nullable)(dict)); unichar objectReplacementChar = 0xFFFC; NSString *imgContent = [NSString stringWithCharacters:&objectReplacementChar length:1]; NSMutableAttributedString *space = nil; if (isLineFeed) { imgContent = [NSString stringWithFormat:@'%@' imgContent]; space = [[NSMutableAttributedString alloc] initWithString:imgContent attributes:attribute]; CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space CFRangeMake(1 space.length - 1) kCTRunDelegateAttributeName delegate); [space addAttribute:@'imgName' value:imgName range:NSMakeRange(1 space.length - 1)]; imgData.position += 1; } else { space = [[NSMutableAttributedString alloc] initWithString:imgContent attributes:attribute]; CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space CFRangeMake(0 space.length) kCTRunDelegateAttributeName delegate); [space addAttribute:@'imgName' value:imgName range:NSMakeRange(0 space.length)]; } imgData.leftMargin = leftMargin; imgData.topMargin = topMargin; CFRelease(delegate); return space;}
    static CGFloat ascentCallback(void *ref) { return [[(__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];}

    这里是在contentString后插入一条直线。bottomImgDict定义了图片的一些属性然后将某些属性存入CoreTextImgData中,然后将这个CoretextImgData存入imgDataArray中用于往视图中依次渲染。

    下面方法用于生成图片的属性字符串,首先是根据传入的属性字典得到图片的宽高信息,然后定义占位字符0xFFFC,isLineFeed表示是否需要换行,利用占位字符生成属性字符串,配置相应属性,在换行条件中因为在拼接字符串时在占位符前加了一个换行符,故占位符的索引需要加一,对应 imgData.position += 1,返回占位符生成的属性字符串,最后将其拼接到原有属性字符串的后面。

    检测图片位置,以便后续对图片操作进行处理:

    + (void)fillImagePositionWithCTframe:(CTframeRef)ctframe coreTextData:(CoreTextData *)coreTextData imageDataArray:(NSMutableArray *)arrImgData{ if (arrImgData == nil || arrImgData.count == 0) { return; } int imgIndex = 0; CoreTextImgData *imageData = arrImgData[0]; CFRange frameRange = CTframeGetVisibleStringRange(ctframe); //在多页显示的情况下,while循环确保当前的imgData是包含在当前CTframe中的,比如第一页有两个图片,第二页有一个图片,如果当前的CTframe是第二页的,那么while循环完成时imgIndex = 2 while ( imageData.position < frameRange.location ) { imgIndex++; if (imgIndex>=[arrImgData count]) return; //quit if no images for this column imageData = [arrImgData objectAtIndex:imgIndex]; } NSArray *lines = (NSArray *)CTframeGetLines(ctframe); NSUInteger lineCount = lines.count; CGPoint lineOrigins[lineCount]; CTframeGetLineOrigins(ctframe CFRangeMake(0 0) lineOrigins); for (int i = 0; i < lineCount; ++i) { if (imageData == nil) { break; } CTLineRef line = (__bridge CTLineRef)(lines[i]); NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns(line); for (id runObj in runObjArray) { CTRunRef run = (__bridge CTRunRef)runObj; CFRange runRange = CTRunGetStringRange(run);// NSDictionary *runAttributesDict = (NSDictionary *)CTRunGetAttributes(run);// NSString *imgName = [runAttributesDict objectForKey:@&#39;imgName&#39;]; //确保图片的字符索引在当前runRange范围内 if (runRange.location <= imageData.position && runRange.location + runRange.length > imageData.position) { NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run); CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName]; if (delegate == nil) { 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 + imageData.leftMargin; runBounds.origin.y = lineOrigins[i].y; runBounds.origin.y -= descent + imageData.topMargin; CGPathRef pathRef = CTframeGetPath(ctframe); CGRect colRect = CGPathGetBoundingBox(pathRef); CGRect delegateBounds = CGRectOffset(runBounds colRect.origin.x colRect.origin.y); //得到图片在其所在CTframe包含区域中的位置区域 imageData.imgPosition = delegateBounds; [coreTextData.imgDataArray addobject:imageData]; imgIndex++; if (imgIndex == arrImgData.count) { imageData = nil; break; } else { imageData = arrImgData[imgIndex]; } } } }}

    上面方法主要目的是获取图片的位置区域,用于图片点击。即代码中的注释部分,前面的的代码都是为获取这个区域所做的准备。

    我是在排版内容时调用上述方法,内容生成多少个CTframe该方法就会调用多少次:

     CTframesetterRef framesetter = CTframesetterCreateWithAttributedString((CFAttributedStringRef)mabStr); NSMutableArray *coreTextDatas = [[NSMutableArray alloc] init]; int textPos = 0; while (textPos < mabStr.length) { //得到每页的尺寸 CGFloat aOriginY; CGFloat frameHeight; if (textPos == 0) { aOriginY = firstOriginY; } else { aOriginY = originY; } frameHeight = [self getframeHeight:framesetter viewWidth:viewWidth viewHeight:viewHeight flipDirection:manager.flipOverDirection] - aOriginY - bottomMargin; // 生成 CTframeRef 实例 CGFloat finalOriginY = bottomMargin; //将UIKit坐标系下y轴的偏移aOriginY转化为coretext坐标系下的偏移 CTframeRef frame = [self createframeWithframesetter:framesetter frameWidth:viewWidth stringRange:CFRangeMake(textPos 0) orginY:finalOriginY height:frameHeight]; CFRange frameRange = CTframeGetVisibleStringRange(frame); // 将生成好的 CTframeRef 实例和计算好的绘制高度保存到 CoreTextData 实例中,最后返回 CoreTextData 实例 CoreTextData *data = [[CoreTextData alloc] init]; data.ctframe = frame; data.height = frameHeight; [self fillImagePositionWithCTframe:data.ctframe coreTextData:data imageDataArray:imgDataArray]; NSAttributedString *aStr = [mabStr attributedSubstringFromRange:NSMakeRange(textPos frameRange.length)]; data.content = aStr; [coreTextDatas addobject:data]; textPos += frameRange.length; // 释放内存 CFRelease(frame); } // 释放内存 CFRelease(framesetter); return coreTextDatas;

    当在视图中点击图片时:可在视图类中定义如下方法:

    - (void)setupEvents{ UIGestureRecognizer * tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(userTapGestureDetected:)]; tapRecognizer.delegate = self; [self addGestureRecognizer:tapRecognizer]; self.userInteractionEnabled = YES;}//处理点击事件- (void)userTapGestureDetected:(UIGestureRecognizer *)recognizer{ CGPoint point = [recognizer locationInView:self]; CoreTextImgData *imgData = [self getTheResponsedImgData:point]; if (imgData != nil) { [imgData handleImgTapped:_chapterId chapterTitle:_chapterTitle]; }}-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{ CGPoint point = [gestureRecognizer locationInView:self]; BOOL isDispatch = [self isPointInResponsedImgRect:point]; if (isDispatch) { return YES; } return NO;}//翻转坐标系- (CGRect)transformCTM:(CGRect)rect{ CGPoint originPoint = rect.origin; originPoint.y = self.bounds.size.height - rect.origin.y - rect.size.height; CGRect aRect = CGRectMake(originPoint.x originPoint.y rect.size.width rect.size.height); return aRect;}- (BOOL)isPointInResponsedImgRect:(CGPoint)point{ for (CoreTextImgData *imgData in self.data.imgDataArray) { CGRect rect = [self transformCTM:imgData.imgPosition]; if (CGRectContainsPoint(rect point)) { if (imgData.isResponseTap) { return NO; } } } return YES;}- (CoreTextImgData *)getTheResponsedImgData:(CGPoint)point{ for (CoreTextImgData *imgData in self.data.imgDataArray) { CGRect rect = [self transformCTM:imgData.imgPosition]; if (CGRectContainsPoint(rect point)) { if (imgData.isResponseTap) { return imgData; } } } return nil;}

    然后在ImgData中处理具体的图片点击事件:

    - (void)handleImgTapped:(NSInteger)chapterId chapterTitle:(NSString *)title{ if ([self.name isEqualToString:@&#39;xxx&#39;]) { NSDictionary *userInfo = [NSDictionary dictionaryWithobjectsAndKeys:[NSNumber numberWithInteger:chapterId] @&#39;chapterId&#39; title @&#39;chapterTitle&#39; nil]; NSNotification *notification =[NSNotification notificationWithName:@&#39;chpaterReply&#39; object:nil userInfo:userInfo]; [[NSNotificationCenter defaultCenter] postNotification:notification]; }}

    这里通过imgData中name属性区分不同图片,进行不同处理。

    PS:写的有点仓促,有些系统函数的作用没有介绍,如有不太清楚或错误的地方,欢迎交流。


本站所有站内信息仅供娱乐参考,不作任何商业用途,不以营利为目的,专注分享快乐,欢迎收藏本站!
所有信息均来自:百度一下 (威尼斯人官网)