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

最近的一个项目要实现一个类似iOS日历的效果,首页是一个年历,要显示每个月份的日期,如果有提醒任务的话需要加一个红圈提醒。

效果图

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。

图二

所以使用等宽字体是不行的,后来经过测试发现:一个空格的宽度恰好是数字宽度的一半。这样我们就可以通过空格控制文字的纵向居中显示了。

因此,文字的格式就变成了如下格式(-表示空格):

-------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初探》,希望我的这篇总结能够对你有所帮助

参考

iOS中CoreText的学习记录(1)
iOS文字排版(CoreText)那些事儿