zoukankan      html  css  js  c++  java
  • iOS阅读器实践系列(三)图文混排

    本篇介绍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:@"imgName"];
    //确保图片的字符索引在当前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:@"xxx"])
        {
            NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInteger:chapterId], @"chapterId", title, @"chapterTitle", nil];
            NSNotification *notification =[NSNotification notificationWithName:@"chpaterReply" object:nil userInfo:userInfo];
            [[NSNotificationCenter defaultCenter] postNotification:notification];
        }
    }

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

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

  • 相关阅读:
    CentOS 6.2安装Darwin Streaming Server
    流媒体技术笔记(协议相关)
    流媒体技术笔记(视频编码相关)
    CentOS6.2下编译mpeg4ip
    用popen函数操作其它程序的输入和输出
    给centos6.2安装yum源
    启动新进程(fork和exec系列函数实现)
    扩展Asterisk1.8.7的Dialplan Applications
    源码安装ffmpeg(带libx264)
    扩展Asterisk1.8.7的AMI接口
  • 原文地址:https://www.cnblogs.com/summer-blog/p/6044118.html
Copyright © 2011-2022 走看看