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:@"\n%@", 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:写的有点仓促,有些系统函数的作用没有介绍,如有不太清楚或错误的地方,欢迎交流。

时间: 2024-11-06 09:45:59

iOS阅读器实践系列(三)图文混排的相关文章

iOS阅读器实践系列(一)coretext纯文本排版基础

前言:之前做了公司阅读类的App,最近有时间来写一下阅读部分的实现过程,供梳理逻辑,计划会写一个系列希望能涉及到尽量多的方面与细节,欢迎大家交流.吐槽.拍砖,共同进步. 阅读的排版用的是coretext,这篇介绍用coretext实现基本的排版功能. 关于coretext的实现原理,可以查看文档或其他资料,这里就不介绍了,只介绍如何应用coretext来实现一个简单的文本排版功能. 因为coretext是离屏排版的,即在将内容渲染到屏幕之前,内容的排版工作的已经完成了. 排版过程大致过程分为 步

iOS阅读器实践系列(四)阅读视图方案

阅读过程中文章内容可能需要很多屏才能展示完,那么现在的问题是创建多少个视图来渲染内容. 经过考虑,决定用两个视图,利用手势来控制不断来回切换来实现. 首先定义两个私有的实体的view对象: @property (nonatomic, strong) DisplyManagerView *buffer1; @property (nonatomic, strong) DisplyManagerView *buffer2; buffer1与buffer2就是用于渲染内容的两个视图. 其次还要定义一个用

IOS开发之使用UIWebView实现图文混排

IOS开发之使用UIWebView实现图文混排时间 2014-08-15 13:45:35  CSDN博客原文  http://blog.csdn.net/wildcatlele/article/details/38583731主题 UIWebView一开始做第一个新闻资讯项目的时候,不知道可以使用UIWebView实现图文并茂的效果,于是就用了最笨的方法,使用TableView解决的新闻浏览.当有点项目经验后知道可以使用UIWebView或者CoreText实现,一直也没有尝试.当上次面试被问

IOS总结_实现UIButton的图文混排,同时显示文字和图片

我们常常需要用到按钮需要文字和图片同时显示,例如: 我们需要定义一个UIButton的子类 实现文件 - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { //可根据自己的需要随意调整 self.titleLabel.textAlignment=NSTextAlignmentRight; self.titleLabel.font=[UIFont systemFontOfSize:14

IOS开发UI篇--一个支持图文混排的ActionSheet

一.简单介绍 UIActionSheet是IOS提供给我们开发人员的底部弹出菜单控件.一般用于菜单选择.操作确认.删除确认等功能.IOS官方提供的下面方式对UIActionView进行实例化: - (instancetype)initWithTitle:(NSString *)title delegate:(id<UIActionSheetDelegate>)delegate cancelButtonTitle:(NSString *)cancelButtonTitle destructive

IOS总结_实现UIButton的图文混排(二)

很久没有写博客了,之前写过一篇关于UIButton图文混排的,但是有点复杂,今天来一个比较简单地,相信大家回用得着 UIButton *button=[[UIButton alloc] initWithFrame:CGRectMake(60, 90, 100, 40)]; //加文字 [button setTitle:@"描述文字" forState:UIControlStateNormal]; [button setTitleColor:[UIColor redColor] forS

iOS-Swift3富文本(UILable文本图文混排)

转载注明出处:http://blog.csdn.net/qxuewei/article/details/53213636 介绍下iOS开发中常用的符文布图文混排 需求: 邱学伟是大帅哥(加个笑脸图片) 邱学伟:红色背景绿色字体加粗显示 是:蓝色字体 10号小字体 大帅哥:灰色42号字体 UILabel中显示结果: 原谅我跟哥们开玩笑起的low爆了的项目名 核心代码: //需求 邱学伟是大帅哥(加个笑脸图片) 邱学伟:红色背景绿色字体加粗显示 是:蓝色字体 10号小字体 大帅哥:灰色42号字体 f

iOS系列教程之TextKit实现图文混排读后记

iOS系列教程之TextKit实现图文混排读后记 前两天看搜狐家明哥写的<TextKit实现图文混排> 今晚回家看了下API发现了一个更加取巧的实现方式.可以直接将后台返回的html富文本用textView显示出来. 记得两年前当时做这个的时候还是借助了笨重的webview. > Textkit是iOS7新推出的类库,其实是在之前推出的CoreText上的封装,有了这个TextKit,以后不用再拿着CoreText来做累活了, 下面是我分别用UITextView 和UIWebView 显

iOS火焰动画效果、图文混排框架、StackView效果、偏好设置、底部手势等源码

iOS精选源码 高性能图文混排框架,构架顺滑的iOS应用. 使用OpenGLE覆盖阿尔法通道视频动画播放器视图. 可选最大日期截至当日日期的日期轮选器ChooseDatePicker 简单轻量的图片浏览器YCPhotoBrower 使用偏好设置.属性列表.归档解档保存数据.恢复数据 页面底部手势交互滚动UITableView 使用CoreAnimation来模拟iOS中的StackView. 盒子可以更具长宽高变化的动画 iOS优质博客 iOS导航栏使用总结 目录:一.设置导航栏样式二.自定义导