基于Core Text实现的TXT电子书阅读器

本篇文章的项目地址基于Core Text实现的TXT电子书阅读器
最近花了一点时间学习了iOS的底层文字处理的框架Core Text。在网上也参考很多资料,具体的资料在文章最后列了出来,有兴趣的可参考一下。
本篇主要介绍实现TXT电子书阅读器设计用到的Core Text相关的用法与实现。

关于Core Text

Core TextiOS底层的文字处理框架,只提供一套C函数接口,使用Core Text对象时要注意手动管理内存以避免发生内存泄漏。之前写了一篇iOS富文本(二)初识Text Kit是介绍iOS的另一个文字处理框架Text KitText Kit是封装Core Text函数提供一套Objective-C的接口,使用起来也比较友好。高度的封装意味着可定制性差,灵活性低。所以如果需要实现更多的功能最好还是用Core Text

关于文字的相关知识参考文章最后列出的资料,因为涉及到字体大小的计算。例如在算字体高度时应该是baseline+ascent+descent的总和,了解这些知识对理解Core Text相关函数很有帮助

Core Text运行时的层次

介绍一下这个层级。framesetter对象(CTFramesetterRef)最为顶层接收一个属性化字符串(attributedString)作为输入,一个framesetter对象生成一个或多个文本中的帧(CTFrameRef)每一个CTFrame都代表一个段落。
要生成帧(CTFrameRef)时,framesetter调用一个typesetter对象(CTTypesetterRef),它放置文本在frame中,framesetter设置段落样式给typesetter对象,包括属性对齐方式,制表位,行间距,缩进和换行模式,typesetter对象用这些属性转换每个字符成字形,然后在每行中填充这些字型,再用这些行填满整个绘制区间。
每个CTFrame对象包含段落线(CTLine)对象。每个(CTLine)对象代表段落中的每一行,一个CTFrame可以包含一个或者多个CTLine对象。CTLine由typesetter对象操作期间被创建。
每个CTLine是包含字形管理(CTRun)对象的数组,一个CTRun对象是一组共享相同属性,方向的连续字形。

基于Core Text实现的电子书阅读器

根据配置文件得到文字显示的属性。

+(NSDictionary *)parserAttribute:(LSYReadConfig *)config
{
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    dict[NSForegroundColorAttributeName] = config.fontColor;
    dict[NSFontAttributeName] = [UIFont systemFontOfSize:config.fontSize];
    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
    paragraphStyle.lineSpacing = config.lineSpace;
    paragraphStyle.alignment = NSTextAlignmentJustified;
    dict[NSParagraphStyleAttributeName] = paragraphStyle;
    return [dict copy];
}

根据属性生成属性化字符串,然后属性化字符串作为输入得到CTFrame对象

+(CTFrameRef)parserContent:(NSString *)content config:(LSYReadConfig *)parser bouds:(CGRect)bounds
{
    NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:content];
    NSDictionary *attribute = [self parserAttribute:parser];
    [attributedString setAttributes:attribute range:NSMakeRange(0, content.length)];
    CTFramesetterRef setterRef = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attributedString);
    CGPathRef pathRef = CGPathCreateWithRect(bounds, NULL);
    CTFrameRef frameRef = CTFramesetterCreateFrame(setterRef, CFRangeMake(0, 0), pathRef, NULL);
    CFRelease(setterRef);
    CFRelease(pathRef);
    return frameRef;

}

生成的CTFrame在要View的drawRect方法中调用CTFrameDraw就可以进行绘制。

因为我们不仅要绘制出文字还要和文字进行交互所以仅仅这两个函数是不够的。
还需要以下函数

//根据触摸点获取当前文字的索引
+(CFIndex)parserIndexWithPoint:(CGPoint)point frameRef:(CTFrameRef)frameRef
{
    CFIndex index = -1;
    CGPathRef pathRef = CTFrameGetPath(frameRef);   //获取绘制的路径
    CGRect bounds = CGPathGetBoundingBox(pathRef);  //获取绘制的区间
    NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frameRef); //获取绘制区间内所有的行数
    if (!lines) {
        return index;
    }
    NSInteger lineCount = [lines count];
    CGPoint *origins = malloc(lineCount * sizeof(CGPoint)); //给每行的起始点开辟内存
    if (lineCount) {
        CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), origins);  //获取每行的坐标
        for (int i = 0; i<lineCount; i++) {
            CGPoint baselineOrigin = origins[i];
            CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:i];
            CGFloat ascent,descent,linegap; //声明字体的上行高度和下行高度和行距
            CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &linegap);  //获取每行的宽度
            CGRect lineFrame = CGRectMake(baselineOrigin.x, CGRectGetHeight(bounds)-baselineOrigin.y-ascent, lineWidth, ascent+descent+linegap+[LSYReadConfig shareInstance].lineSpace);    //没有转换坐标系左下角为坐标原点 字体高度为上行高度加下行高度
            if (CGRectContainsPoint(lineFrame,point)){
                index = CTLineGetStringIndexForPosition(line, point); //得到当前文字的索引
                break;
            }
        }
    }
    free(origins);  释放内存
    return index;

}

长按文字会默认选中两个文字这样就要计算选中的区间

+(CGRect)parserRectWithPoint:(CGPoint)point frameRef:(CTFrameRef)frameRef
{
    CFIndex index = -1;
    CGPathRef pathRef = CTFrameGetPath(frameRef);
    CGRect bounds = CGPathGetBoundingBox(pathRef);
    CGRect rect = CGRectZero;
    NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frameRef);
    if (!lines) {
        return rect;
    }
    NSInteger lineCount = [lines count];
    CGPoint *origins = malloc(lineCount * sizeof(CGPoint)); //给每行的起始点开辟内存
    if (lineCount) {
        CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), origins);
        for (int i = 0; i<lineCount; i++) {
            CGPoint baselineOrigin = origins[i];
            CTLineRef line = (__bridge CTLineRef)[lines objectAtIndex:i];
            CGFloat ascent,descent,linegap; //声明字体的上行高度和下行高度和行距
            CGFloat lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &linegap);
            CGRect lineFrame = CGRectMake(baselineOrigin.x, CGRectGetHeight(bounds)-baselineOrigin.y-ascent, lineWidth, ascent+descent+linegap+[LSYReadConfig shareInstance].lineSpace);    //没有转换坐标系左下角为坐标原点 字体高度为上行高度加下行高度加行间距 注:[LSYReadConfig shareInstance].lineSpace为配置文件中设置的行间距
            if (CGRectContainsPoint(lineFrame,point)){
                CFRange stringRange = CTLineGetStringRange(line);
                index = CTLineGetStringIndexForPosition(line, point);
                CGFloat xStart = CTLineGetOffsetForStringIndex(line, index, NULL);  //获取当前索引在当前行的偏移量
                CGFloat xEnd;
                //默认选中两个单位
                if (index > stringRange.location+stringRange.length-2) {
                    xEnd = xStart;
                    xStart = CTLineGetOffsetForStringIndex(line,index-2,NULL);
                }
                else{
                    xEnd = CTLineGetOffsetForStringIndex(line,index+2,NULL);
                }
                rect = CGRectMake(origins[i].x+xStart,baselineOrigin.y-descent,fabs(xStart-xEnd), ascent+descent);
                break;
            }
        }
    }
    free(origins);
    return rect;
}

上面就是实现这个项目使用的大部分关于Core Text代码,实际项目实现起来远比这要复杂的多。具体实现请参考这个项目基于Core Text实现的TXT电子书阅读器

参考资料

Core Text 入门
基于 CoreText 的排版引擎:基础
Core Text Tutorial for iOS: Making a Magazine App
NIAttributedLabel.m
WFCoretext

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
package cn.huang.my_txtreader; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.text.DecimalFormat; import java.util.Vector; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Align; public class BookPageFactory { //把字体绘制到界面上 private File book_file = null; private MappedByteBuffer m_mbBuf = null;//MappedByteBuffer 将文件直接映射到内存 private int m_mbBufLen = 0; private int m_mbBufBegin = 0; private int m_mbBufEnd = 0; private String m_strCharsetName = "gbk";//文本格式 private Bitmap m_book_bg = null;//文本图像 private int mWidth; private int mHeight; private Vector<String> m_lines = new Vector<String>(); //用于一行一行显示 private int m_fontSize = 24; private int m_textColor = Color.BLACK;//字体颜色 private int m_backColor = 0xffff9e85; // 背景颜色 private int marginWidth = 15; // 左右与边缘的距离 private int marginHeight = 20; // 上下与边缘的距离 private int mLineCount; // 每页可以显示的行数 private float mVisibleHeight; // 绘制内容的宽 private float mVisibleWidth; // 绘制内容的宽 private boolean m_isfirstPage,m_islastPage; // private int m_nLineSpaceing = 5; private Paint mPaint; //设置阅读界面,包括字体,显示多少行 public BookPageFactory(int w, int h) { // TODO Auto-generated constructor stub mWidth = w; mHeight = h;//获得宽和高 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setTextAlign(Align.LEFT);//设置文本对齐方式 mPaint.setTextSize(m_fontSize);//设置字体大小 mPaint.setColor(m_textColor);//设置颜色 mVisibleWidth = mWidth - marginWidth * 2; mVisibleHeight = mHeight - marginHeight * 2;//绘制的内容宽和高 mLineCount = (int) (mVisibleHeight / m_fontSize); // 可显示的行数 } //获得文件,并映射到内存 public void openbook(String strFilePath) throws IOException { book_file = new File(strFilePath); long lLen = book_file.length();//文本长度 m_mbBufLen = (int) lLen;//缓存的长度 m_mbBuf = new RandomAccessFile(book_file, "r").getChannel().map( FileChannel.MapMode.READ_ONLY, 0, lLen); //RandomAccessFile是用来访问那些保存数据记录的文件的 } //读一段 protected byte[] readParagraphBack(int nFromPos) { int nEnd = nFromPos;//字符缓存开始的位置 int i; byte b0, b1; if (m_strCharsetName.equals("UTF-16LE")) { i = nEnd - 2;//? while (i > 0) { b0 = m_mbBuf.get(i); b1 = m_mbBuf.get(i + 1); if (b0 == 0x0a && b1 == 0x00 && i != nEnd - 2) { i += 2; break; } i--; } } else if (m_strCharsetName.equals("UTF-16BE")) { i = nEnd - 2; while (i > 0) { b0 = m_mbBuf.get(i);//返回指定索引 b1 = m_mbBuf.get(i + 1); if (b0 == 0x00 && b1 == 0x0a && i != nEnd - 2) { i += 2; break; }//过段时 i--; } } else { i = nEnd - 1;//? while (i > 0) { b0 = m_mbBuf.get(i); if (b0 == 0x0a && i != nEnd - 1) { i++; break; }//过段时 i--; } } if (i < 0) i = 0; //i是过段的索引位置 int nParaSize = nEnd - i; int j; byte[] buf = new byte[nParaSize]; for (j = 0; j < nParaSize; j++) { buf[j] = m_mbBuf.get(i + j); } return buf; } // 读取上一段落 protected byte[] readParagraphForward(int nFromPos) { int nStart = nFromPos;//字符缓存开始的位置 int i = nStart; byte b0, b1; // 根据编码格式判断换行 if (m_strCharsetName.equals("UTF-16LE")) { while (i < m_mbBufLen - 1) { b0 = m_mbBuf.get(i++); b1 = m_mbBuf.get(i++); if (b0 == 0x0a && b1 == 0x00) { break; } } } else if (m_strCharsetName.equals("UTF-16BE")) { while (i < m_mbBufLen - 1) { b0 = m_mbBuf.get(i++); b1 = m_mbBuf.get(i++); if (b0 == 0x00 && b1 == 0x0a) { break; } } } else { while (i < m_mbBufLen) { b0 = m_mbBuf.get(i++); if (b0 == 0x0a) { break; } } } //这使i在换行的索引位置 int nParaSize = i - nStart; byte[] buf = new byte[nParaSize]; for (i = 0; i < nParaSize; i++) { buf[i] = m_mbBuf.get(nFromPos + i); }//把读到的段输入字节流中 return buf; } protected Vector<String> pageDown() { String strParagraph = ""; Vector<String> lines = new Vector<String>(); while (lines.size() < mLineCount && m_mbBufEnd < m_mbBufLen) { //不能大于给定的最多行数 byte[] paraBuf = readParagraphForward(m_mbBufEnd); // 读取一个段落 m_mbBufEnd += paraBuf.length; //减去读到的长度,作为下一个结束的地步 try { strParagraph = new String(paraBuf, m_strCharsetName); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } String strReturn = ""; if (strParagraph.indexOf("\r\n") != -1) { //"\r\n"在字符串中则,下同 strReturn = "\r\n"; strParagraph = strParagraph.replaceAll("\r\n", ""); } else if (strParagraph.indexOf("\n") != -1) { strReturn = "\n"; strParagraph = strParagraph.replaceAll("\n", ""); } if (strParagraph.length() == 0) { lines.add(strParagraph); } while (strParagraph.length() > 0) { int nSize = mPaint.breakText(strParagraph, true, mVisibleWidth, null); //返回刚好要超过规定长度mVisibleWidth的值 lines.add(strParagraph.substring(0, nSize)); strParagraph = strParagraph.substring(nSize); if (lines.size() >= mLineCount) {//超过规定的行数 break; } } if (strParagraph.length() != 0) { try { m_mbBufEnd -= (strParagraph + strReturn) .getBytes(m_strCharsetName).length; //即返回字符串在GBK、UTF-8和ISO8859-1编码下的byte数组表示 //目的在于把m_mbBufEnd改成指向下一行 } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } return lines; } //设置文本显示 //预防文本超过界面规定的范围 protected void pageUp() { if (m_mbBufBegin < 0) m_mbBufBegin = 0; Vector<String> lines = new Vector<String>(); String strParagraph = ""; while (lines.size() < mLineCount && m_mbBufBegin > 0) { //不能大于给定的最多行数 Vector<String> paraLines = new Vector<String>(); byte[] paraBuf = readParagraphBack(m_mbBufBegin); //读到一段 //从头开始读 m_mbBufBegin -= paraBuf.length; //减去读到的长度,作为下一个开始要读的标志 try { strParagraph = new String(paraBuf, m_strCharsetName); //第一个参数读到的字符串,第二个参数是文本格式 } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } strParagraph = strParagraph.replaceAll("\r\n", ""); //用第二个参数的字符串替换第一个 strParagraph = strParagraph.replaceAll("\n", ""); //替换掉任何过行的记录 if (strParagraph.length() == 0) {//为空,没有字符串时 paraLines.add(strParagraph); } while (strParagraph.length() > 0) { int nSize = mPaint.breakText(strParagraph, true, mVisibleWidth, null); //测量第一个字符串,与第三个参数即宽度相比 paraLines.add(strParagraph.substring(0, nSize)); //获得0到nSize的字符串 strParagraph = strParagraph.substring(nSize); //字符串变成从nSize开始 }//在于把字符串变成不会超过规定长度mVisibleWidth的字符串 lines.addAll(0, paraLines);//加入所有Vector字符串列 } while (lines.size() > mLineCount) {//超过规定行数时 try { m_mbBufBegin += lines.get(0).getBytes(m_strCharsetName).length; //.get()表示返回指定位置的元素 //String的getBytes()方法是得到一个操作系统默认的编码格式的字节数组 //即返回字符串在GBK、UTF-8和ISO8859-1编码下的byte数组表示 //目的在于把m_mbBufBegin改成指向下一行 lines.remove(0); //删除指定位置的元素 } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } } m_mbBufEnd = m_mbBufBegin; return; } protected void prePage() throws IOException { if (m_mbBufBegin <= 0) { m_mbBufBegin = 0; m_isfirstPage=true; return; }else m_isfirstPage=false; m_lines.clear(); //删除所有元素 pageUp(); m_lines = pageDown(); //把收集到的文本放到m_lines中 } public void nextPage() throws IOException { if (m_mbBufEnd >= m_mbBufLen) { m_islastPage=true; return; }else m_islastPage=false; m_lines.clear(); m_mbBufBegin = m_mbBufEnd; m_lines = pageDown(); } public void onDraw(Canvas c) { if (m_lines.size() == 0) m_lines = pageDown(); //现在m_lines的格式是按照 //界面阅读规定的,即有多宽的,有多行 if (m_lines.size() > 0) { if (m_book_bg == null) c.drawColor(m_backColor);//设置背景颜色 else c.drawBitmap(m_book_bg, 0, 0, null); int y = marginHeight; for (String strLine : m_lines) { y += m_fontSize; c.drawText(strLine, marginWidth, y, mPaint); //给界面的每一行绘制 } } float fPercent = (float) (m_mbBufBegin * 1.0 / m_mbBufLen); DecimalFormat df = new DecimalFormat("#0.0");//用于格式化十进制数字 //即按照参数的格式输出 String strPercent = df.format(fPercent * 100) + "%"; int nPercentWidth = (int) mPaint.measureText("999.9%") + 1; //返回字符串的宽度 c.drawText(strPercent, mWidth - nPercentWidth, mHeight - 5, mPaint); } //绘制图像 public void setBgBitmap(Bitmap BG) { m_book_bg = BG; } //返回第一页 public boolean isfirstPage() { return m_isfirstPage; } //返回最后一页 public boolean islastPage() { return m_islastPage; } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值