CoreText初探——iOS日历的年历视图

独奏

技术分享|2014-11-3|最后更新: 2023-2-23|
type
Post
status
Published
date
Nov 3, 2014
slug
summary
tags
iOS
category
技术分享
icon
password
最近的一个项目要实现一个类似iOS日历的效果,首页是一个年历,要显示每个月份的日期,如果有提醒任务的话需要加一个红圈提醒。
notion image
效果图
iOS的年历中每年都是一个3*4的格子,可以无限滑动的。首先想到的这里就是使用UICollectionView来实现,每年作为一个section,每个section有12个cell。
但是每个cell内要显示该月的日期,这里的布局就成了问题了。如果每个cell内使用lable来显示日期的话,一屏有12个cell就意味着要有365个lable,这么多view必然会对流畅性产生影响。所以最终决定只能使用CoreText来进行月cell内的日期布局了。下面是遇到的几个问题,一一说一下思路

文字纵向对齐

首先想到的问题就是文字对齐的问题,比如日期1、8、15、22、29要在一列上面对齐,最初的想法是使用等宽字体来显示,这样显示出来的效果就是1、8,15的1、22的第一个2、29的2对齐。但是观察iOS的日历发现其对齐是居中对齐的,也就是1、8是和15、22、29的中间对齐的,如图2。
notion image
图二
所以使用等宽字体是不行的,后来经过测试发现:一个空格的宽度恰好是数字宽度的一半。这样我们就可以通过空格控制文字的纵向居中显示了。
因此,文字的格式就变成了如下格式(-表示空格):
-------1----2----3----4----5----6---\n -7----8----9---10--11--12--13--\n 14--15--16--17--18--19--20--\n 21--22--23--24--25--26--27--\n 28--29--30--31\n
说明:
  • 四个空格表示两个数字字符
  • 7-就可以这种写法就可以让7在14的中间的上方
  • 每个日期之间间隔为两个空格——即一个数字的宽度

CoreText排版问题

文本格式问题解决了,接下来就是使用CoreText进行排版了。CoreText排版实际就是在- (void)drawRect:(CGRect)rect方法里面通过设置NSAttributedString的attribute属性进行控制文字格式的。关于这个的资料比较多,我就不详细说明了。下面只列举一下我用到的一些属性
//创建字体以及字体大小 CTFontRef helvetica = CTFontCreateWithName(CFSTR("Helvetica"), 8, NULL); [mdayStr addAttribute:(id)kCTFontAttributeName value:(__bridge id)helvetica range:NSMakeRange(0, [mdayStr length])]; //------------------设置行间距 start-------------------------------- //段落 CTParagraphStyleSetting lineBreakMode; CTLineBreakMode lineBreak = kCTLineBreakByCharWrapping; //换行模式 lineBreakMode.spec = kCTParagraphStyleSpecifierLineBreakMode; lineBreakMode.value = &lineBreak; lineBreakMode.valueSize = sizeof(CTLineBreakMode); //行间距 CTParagraphStyleSetting LineSpacing; CGFloat spacing = 5; //指定间距 LineSpacing.spec = kCTParagraphStyleSpecifierLineSpacingAdjustment; LineSpacing.value = &spacing; LineSpacing.valueSize = sizeof(CGFloat); CTParagraphStyleSetting settings[] = {lineBreakMode,LineSpacing}; CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, 2); //第二个参数为settings的长度 [mdayStr addAttribute:(NSString *)kCTParagraphStyleAttributeName value:(__bridge id)paragraphStyle range:NSMakeRange(0, mdayStr.length)]; //------------------设置行间距 end--------------------------------
我只是设置一下字体和行间距,至于NSAttributeString有哪些属性,可以再网上找到很全面的文档,这里就不列举了。

特殊日期标注

如[图一]所示,如果当前日期有事件,需要用红圈标注。这个显然是要获取到这个日期的文字所在位置的。先看代码
代码一
// 有底色的内容预留一个属性,区别于其他属性,value为这个日期的数字位数,一位数字为1,两位数组为2 [mdayStr addAttribute:@"tagCharacter" value:@1 range:NSMakeRange(range.location, 1)];
代码二
// --------------逐行逐字的扫描设置文字背景 start-------------------- //获取画出来的内容的行数 CFArrayRef lines = CTFrameGetLines(_frame); //获取每行的原点坐标 CGPoint lineOrigins[CFArrayGetCount(lines)]; CTFrameGetLineOrigins(_frame, CFRangeMake(0, 0), lineOrigins); for (int i = 0; i < CFArrayGetCount(lines); i++) { CTLineRef line = CFArrayGetValueAtIndex(lines, i); CGFloat lineAscent; CGFloat lineDescent; CGFloat lineLeading; //获取每行的宽度和高度 CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading); //获取每个CTRun CFArrayRef runs = CTLineGetGlyphRuns(line); for (int j = 0; j < CFArrayGetCount(runs); j++) { CGFloat runAscent; CGFloat runDescent; CGPoint lineOrigin = lineOrigins[i]; //获取每个CTRun CTRunRef run = CFArrayGetValueAtIndex(runs, j); NSDictionary* attributes = (NSDictionary*)CTRunGetAttributes(run); NSNumber *charNum = [attributes objectForKey:@"tagCharacter"]; //图片渲染逻辑,把需要被图片替换的字符位置画上图片 if (charNum) { CGRect runRect; //调整CTRun的rect runRect=CGRectMake(lineOrigin.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL), lineOrigin.y - runDescent, runRect.size.width, runAscent + runDescent); runRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0,0), &runAscent, &runDescent, NULL); if (charNum.intValue == 1) { // 获取这个字符的宽度 const CGSize *bounds = CTRunGetAdvancesPtr(run); CGContextRef con = UIGraphicsGetCurrentContext(); CGContextAddEllipseInRect(con, CGRectMake(runRect.origin.x+lineOrigin.x+(*bounds).width/2.0f-1,lineOrigin.y-(*bounds).height/2.0f-3,dayFontSize+4,dayFontSize+4)); CGContextSetFillColorWithColor(con, [UIColor colorWithRed:0.93 green:0.22 blue:0.18 alpha:1].CGColor); CGContextFillPath(con); }else if (charNum.intValue == 2){ const CGSize *bounds = CTRunGetAdvancesPtr(run); CGContextRef con = UIGraphicsGetCurrentContext(); CGContextAddEllipseInRect(con, CGRectMake(runRect.origin.x + lineOrigin.x+(*bounds).width-1,lineOrigin.y-(*bounds).height/2.0f-3,dayFontSize+4,dayFontSize+4)); CGContextSetFillColorWithColor(con, [UIColor colorWithRed:0.93 green:0.22 blue:0.18 alpha:1].CGColor); CGContextFillPath(con); } } } } // --------------逐行逐字的扫描设置文字背景 end---------------------
这里我把需要标注的日期统一加一个tagCharacter属性,来标注出来,然后再绘图的时候,逐字检查,如果发现这个属性,就计算这个文字的位置,在该位置画一个圆。这里要注意的就是由于日期的数字位数不同,所以根据位数计算圆的位置。因此我在添加属性的时候,属性值我给的就是数字位数

总结

以上问题就是我在做这个日历的年历视图时遇到的问题,实际上主要还是对CoreText的运用,由于这是本人第一次真正使用CoreText,所以题目就叫做《CoreText初探》,希望我的这篇总结能够对你有所帮助

参考