Core Graphics 101: 弧线和轨迹

来源:互联网 发布:华安股票交易软件 编辑:程序博客网 时间:2024/05/01 20:54

这是本教程系列的第三部分,讲解如何通过实例使用Core Graphics API!

在第一部分教程系列中,我们通过制作一个美观的tablie view cell 背景,讲解了如何绘制线,矩形和颜色渐变。

在第二部分教程系列中,我们通过制作一个美观的table view cell header,讲解了如何绘制阴影和光滑效果。

在本篇教程中,我们将通过为table view添加footer和结束触控事件来结束table view的制作。在教程中,我们也将学习绘制弧线,裁剪和轨迹的相关内容!

如果你还没有本教程的例子代码, 可以在这里下载上篇教程的代码。

开始

我们首先要做的,是用15像素点高的红色view去替换掉footer – 就像前面的教程那样做 – 以确保一切运作正常。

到现在,你应该相当熟悉如何操作了… 那为何不自己做一遍呢?如果遇到什么问题,你都可以随时回到这里。

…等待…

…等待…

…等待…

 

Tomato-San is angry!

Tomato-San 生气了!

 

 

什么?!你现在还在这里发呆?!你可以做到的 – 继续尝试下吧!:]

解决方案

假如你遇到任何麻烦,以下是解决方案。

确保你的 ”Groups & Files”下面的“Classes”分组被选中,前往菜单项的 ”FileNew File…”,选择iOSCocoa Touch Class,Objective-C class,确保”Subclass of UIView”被选中,然后点击下一步。命名文件为”CustomFooter.m”,确保”Also create CustomFooter.h”被选中,然后点击”Finish”。

切换到CustomFooter.m文件,取消drawRect方法的注释,然后使用以下代码替换掉原来的内容:

CGContextRef context = UIGraphicsGetCurrentContext();CGColorRef redColor = [UIColor colorWithRed:1.0 green:0.0     blue:0.0 alpha:1.0].CGColor; CGContextSetFillColorWithColor(context, redColor);CGContextFillRect(context, self.bounds);

然后切换到RootViewController.m文件,根据以下代码做修改:

// In import section

#import "CustomFooter.h"// Add new methods-(CGFloat) tableView:(UITableView *)tableView     heightForFooterInSection:(NSInteger)section {    return 15;}- (UIView *) tableView:(UITableView *)tableView     viewForFooterInSection:(NSInteger)section {    return [[[CustomFooter alloc] init] autorelease];}

 

编译并运行工程,如果一切运行正常,你将看到以下画面:

Table View Footer Placeholder

回到正题

 

既然现在我们在当前区域有一个预留的view,我们对它美化下吧。首先,先为接下来的操作刷新内存。

Zoomed image of Table View Footer

注意以下对上图效果的描述:

  • 页面底部有一条匀整的弧线。
  • 页面有从浅灰色到深灰色的渐变效果。
  • 在页面的左右边缘还有种白色的高亮显示效果。
  • 页面的弧线下部有一块合适的阴影效果。

显然这里的新效果就是弧线。让我们对它做一点讨论。

创建弧线 – 数学问题

 

从示意图可以看出,我们想要的弧线只是一块有很大半径,从特定的开始角度到特定的结束角度的大圆顶部。

Diagram of the circle that our arc is part of

那现在我们如何用Core Graphics去描述这段弧线呢?好的,我们将要使用的API是CGContextAddArc API,作为这个函数的输入参数,我们需要告诉Core Graphics以下三项内容:

  1. 圆的中心点
  2. 圆的半径
  3. 绘制弧线的开始点和结束点

但遗憾的是我们并不知道这些内容,那我们能做什么呢?!

这里就需要一些简单的数学原理了。我们可以根据已经知道的计算出所需要的参数!

首先我们知道想要绘制弧线的方框的尺寸大小:
Diagram of the rectangle in which our arc will be drawn

其次我们知道一个有趣的数学定理,叫做相交弦定理, 该定理说如果你想要在一个圆中,绘制任意两条连接圆上两个点的线(这些线称为弦),两条弦的分段线段长度乘积相等。

Diagram of the Intersecting Chord Theorem

如果你想要理解这个原理,可以进入上面的网络链接,该链接里面有一个很酷的小javascript demo提供给你玩下。

有了以上两项已知条件,看看如果我们像下面那样绘制两条弦会发生什么:

Diagram of the lines we'll draw to figure out the radius...

这里我们绘制了一条线,连接我们的弧线方框底部的两个点,另外还绘制了一条连接弧线方框顶部和圆底部的线。

如果我们这样做,已知a,b和c,就可以计算出d了。

所以d就是 (a * b) / c。用代码表示出来就是:

// Just substituting...CGFloat d = ((arcRectWidth/2) * (arcRectWidth/2)) / (arcRectHeight);// Or more simply...CGFloat d = pow(arcRectWidth, 2)/(4*arcRectHeight);

现在我们知道了c和d,我们知道半径就是他们的和的一半(c + d) / 2!用代码表述如下:

// Just substituting...CGFloat radius = (arcRectHeight + (pow(arcRectWidth, 2)/(4*arcRectHeight)))/2;// Or more simply...CGFloat radius = (arcRectHeight/2) + (pow(arcRectWidth, 2)/(8*arcRectHeight));

很好!现在我们知道了半径,很容易就可以知道圆心了 – 用阴影矩形的中心点的Y坐标值减去半径。

CGPoint arcCenter = CGPointMake(arcRectTopMiddleX, arcRectTopMiddleY - radius);

一旦我们知道了中心点,半径和弧线方框,很容易就可以计算出弧线的开始和结束角度了:

Diagram of how to figure out the start and end angle for an arc from the radius...

现在我们可以从计算图示的角度值开始。还记得三角函数吗?角度的cos值等于三角形中与角相邻的直角边长度除以斜边长度。

用另一种方式表述,就是 cosine(angle) = (arcRectWidth/2) / radius。所以要获得角度,我们可以使用arccos函数:

CGFloat angle = acos(arcRectWidth/(2*radius));

既然我们知道了角度,很容易就获得开始和结束的角度值了:

Diagram of how to figure out the start and end angles...

很好!既然我们知道了所有的原理,让我们写在函数里面吧。

另外,值得一提的是 – 有一种更容易的,像这样使用CGContextAddArcToPoint函数去绘制弧线的方法。我们将会在下一篇教程系列中讨论它,但是我觉得很有必要讲解下上面所述方法的相关数学原理。

绘制弧线和创建轨迹

打开Common.h文件,添加以下代码到文件的底部:

static inline double radians (double degrees) { return degrees * M_PI/180; }CGMutablePathRef createArcPathFromBottomOfRect(CGRect rect, CGFloat arcHeight);

我们首先要添加的是一个转换度数值为弧度值的辅助函数。

然后…等会…什么叫 CGMutablePathRef?

还记得使用Core Graphics绘制图形需要两个步骤。首先你要定义轨迹,然后画轨迹或者给轨迹填充颜色。

到现在为止,我们只是简单的使用CGContextMoveToPoint, CGContextAddLineToPoint 和 CGContextAddRect等函数直接在context上绘制想要的轨迹 ,然后使用CGContextStrokePath 或者 CGContextFillPath函数去给轨迹绘制或者填充颜色。

有时候,我们也会用简便的方法,在context上添加一段轨迹并且调用一个函数,比如CGContextFillRect,给它填充颜色。

但是现在,我们将保存一段轨迹到一个特定的path变量中,而不是直接把轨迹添加到context上面。这样可以很容易的重用定义好的轨迹path,而不需要反复的去调用同一个函数。

绘制可重用的轨迹相当的容易 – 你只需要调用CGPathXXX 类型函数,而不是 CGContextXXX类型函数。

让我们看下它的工作原理。添加以下代码到Common.m文件的底部:

CGMutablePathRef createArcPathFromBottomOfRect(CGRect rect, CGFloat arcHeight) {     CGRect arcRect = CGRectMake(rect.origin.x,         rect.origin.y + rect.size.height - arcHeight,         rect.size.width, arcHeight);     CGFloat arcRadius = (arcRect.size.height/2) +         (pow(arcRect.size.width, 2) / (8*arcRect.size.height));    CGPoint arcCenter = CGPointMake(arcRect.origin.x + arcRect.size.width/2,         arcRect.origin.y + arcRadius);     CGFloat angle = acos(arcRect.size.width / (2*arcRadius));    CGFloat startAngle = radians(180) + angle;    CGFloat endAngle = radians(360) - angle;     CGMutablePathRef path = CGPathCreateMutable();    CGPathAddArc(path, NULL, arcCenter.x, arcCenter.y, arcRadius,         startAngle, endAngle, 0);    CGPathAddLineToPoint(path, NULL, CGRectGetMaxX(rect), CGRectGetMinY(rect));    CGPathAddLineToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMinY(rect));    CGPathAddLineToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMaxY(rect));    return path;     }

好的,这个函数需要输入整个区域的矩形变量和一个定义弧线有多大的浮点数值(这段弧线位于矩形的底部)。然后根据这两个参数去计算弧线矩形。

然后我们利用上面提到的数学方法,计算出半径,圆心,开始和结束的角度值。

接下来,我们创建轨迹。轨迹将包括弧线,还有包围弧线上面的矩形区域的边界线。

首先我们使用CGPathCreateMutable函数创建可重用的轨迹变量。然后我们使用CGPathXXX类型函数,而不是CGContextXXX类型函数。

我们使用CGPathAddArc函数去添加我们的弧线,传入已经计算出来的所有数值参数,然后从每个端点绘制线段以完成轨迹。

使用以下代码,对CustomFooter.m文件进行修改:

// At top of file

#import "Common.h"// Inside initWithFrameself.opaque = YES;self.backgroundColor = [UIColor clearColor];// Replace the contents of drawRect with the following:CGContextRef context = UIGraphicsGetCurrentContext();CGColorRef whiteColor = [UIColor colorWithRed:1.0 green:1.0     blue:1.0 alpha:1.0].CGColor; CGColorRef lightGrayColor = [UIColor colorWithRed:230.0/255.0 green:230.0/255.0     blue:230.0/255.0 alpha:1.0].CGColor;CGColorRef darkGrayColor = [UIColor colorWithRed:187.0/255.0 green:187.0/255.0     blue:187.0/255.0 alpha:1.0].CGColor;CGColorRef shadowColor = [UIColor colorWithRed:0.2 green:0.2     blue:0.2 alpha:0.5].CGColor;CGFloat paperMargin = 9.0;CGRect paperRect = CGRectMake(self.bounds.origin.x+paperMargin,                         self.bounds.origin.y,                        self.bounds.size.width-paperMargin*2,                         self.bounds.size.height);CGRect arcRect = paperRect;arcRect.size.height = 8;CGContextSaveGState(context);CGMutablePathRef arcPath = createArcPathFromBottomOfRect(arcRect, 4.0);CGContextAddPath(context, arcPath);CGContextClip(context);            drawLinearGradient(context, paperRect, lightGrayColor, darkGrayColor);CGContextRestoreGState(context);CFRelease(arcPath);

好的,我们这里首先设置view为opaque(非透明),并且使用默认的clearColor作为背景颜色。

注意到我们实际上可以在initWithFrame方法中进行这些设置,然后即使当我们在RootViewController中调用“init”方法时,这些代码也会被调用。

更新:来自评论部分的edj解释了相关原因:

Cocoa convention表示,有一个指定的初始化函数(通常是最完整的一个),会被类里面的其他任何一个init方法调用。所以 – init方法替你调用了 – initWithFrame方法。

接下来我们进行Core Graphics的基本设置(context,颜色)以及为整个页面区域创建一个矩形方框(记住,我们需要做些缩进调整)还有我们希望弧形所在的区域(矩形的顶部,在弧形下面有一些阴影或者黑色间隔)。

然后,我们通过调用刚才定义的createArcPathFromBottomOfRect方法,获得页面的轨迹(还有底部的弧线)。然后可以添加轨迹到context上,并且裁剪到那个轨迹上。

接下来所有的绘制都会限定在这个轨迹上面。我们可以使用之前定义好的drawLinearGradient方法!

最后,当我们完成绘制工作的时候,使用CFRelease函数释放掉path变量。

编译运行工程,如果一切运行正常,你将看到以下画面:

Table Footer, First Draft

看起来不错,但是我们需要进一步修饰下它。

修剪,轨迹,还有Even-Odd规则

接下来添加以下代码到drawRect函数的底部(但是要在CFRelease方法调用之前):

CGContextSaveGState(context);CGPoint pointA = CGPointMake(arcRect.origin.x,     arcRect.origin.y + arcRect.size.height - 1);CGPoint pointB = CGPointMake(arcRect.origin.x, arcRect.origin.y);CGPoint pointC = CGPointMake(arcRect.origin.x + arcRect.size.width - 1,     arcRect.origin.y);CGPoint pointD = CGPointMake(arcRect.origin.x + arcRect.size.width - 1,     arcRect.origin.y + arcRect.size.height - 1);draw1PxStroke(context, pointA, pointB, whiteColor);draw1PxStroke(context, pointC, pointD, whiteColor);    CGContextRestoreGState(context);

这里我们在footer的每条边界上绘制了一条粗白线,让它突出来一点。基于之前的教程,你应该相当的熟悉了。

你可以编译运行看下是不是你想要的效果。当你准备好的时候,将以下代码添加到你刚才在弧线下部绘制阴影的代码下面:

CGContextAddRect(context, paperRect);CGContextAddPath(context, arcPath);CGContextEOClip(context);CGContextAddPath(context, arcPath);CGContextSetShadowWithColor(context, CGSizeMake(0, 2), 3.0, shadowColor);CGContextFillPath(context);

这里出现了一个新的关键概念,我解释下。

记得在上篇教程中绘制阴影,你进行了阴影绘制,然后绘制了轨迹。Core Graphics会自动绘制上轨迹,然后在轨迹下面画上恰当的阴影。

但是这里,我们已经用颜色渐变绘制了轨迹 – 所以我们不希望再用颜色去覆盖那个区域。

这似乎像是裁剪的工作!我们可以设置好clipping以便Core Graphics只在页面区域的外围部分进行绘制。然后我们可以让它去填充页面区域并且绘制阴影了,页面的填充就会被忽略(因为它是裁剪),但是阴影还会显现出来。

但是我们还没有轨迹 – 目前我们有的轨迹只是给页面区域的,外围部分的就没有…

幸运的是,我们可以简单的通过内部区域获取外部区域的轨迹,通过Core Graphics的简单方法。你需要做的就是添加不止一个轨迹到context上,然后调用“CGContextEOClip”方法。

当你添加不止一条轨迹到context上时,Core Graphics 需要一些方法去决定哪个点应该被填充,哪个又不用。例如,你可以甜甜圈形状,它的外部被填充了,但是内部却是空的,或者一个甜甜圈洞形状,它的内部被填充了,但是外部却没有。

你可以制定不同的算法,让Core Data知道如何去处理这些情况。其中一种算法是“EO”,或者even-odd。它表示对任何一个点,Core graphics将会绘制一条连接那个点到绘制区域外部的线。如果这条线穿过偶数值点,它就会被填充,反之不会。以下来自Quartz2D Programming Guide的图示展示了这个原理:

Even Odd Rule Diagram

当你使用“EO”规则时候,我们告诉Core Graphics,即使添加了两条轨迹到context上,根据EO规则,仍应该只把它当做一条轨迹。所以外围部分(整个paperRect)会被填充,内部(arcPath)就不会被填充了。

然后我们让Core Graphics去裁剪那段轨迹,仅仅在外部区域绘制。

一旦我们裁剪好后,我们会为弧线添加轨迹,设置好阴影,并且填充弧线。当然,没有东西会被填充(因为它被裁剪掉了),但是阴影仍然会被绘制在外部区域!

编译运行工程,如果一切运行正常,你将看到footer下面的阴影了:
Table Footer With Shadows

收尾工作

我们的table view现在看起来相当不错了,但是让我们再花点时间做些收尾工作吧。

首先,修复最坏的部分:最后的table view cell 真不该绘制分隔线,让人觉得奇怪。另外,table view cell选取和被选取时没有什么区别,文本的颜色也变成了难看的白色。所以根据以下代码,对CustomCellBackground.h文件做出修改:

// Inside @interfaceBOOL _lastCell;BOOL _selected;// After @interface@property  BOOL lastCell;@property  BOOL selected;

同理根据以下代码,修改CustomCellBackground.m文件:

// After @implementation@synthesize lastCell = _lastCell;@synthesize selected = _selected;// Replace first call to drawLinearGradient with the following:if (_selected) {    drawLinearGradient(context, paperRect, lightGrayColor, separatorColor);} else {    drawLinearGradient(context, paperRect, whiteColor, lightGrayColor);}// Wrap the rest of the code with the followingif (!_lastCell) {    // Code drawing 1 px stroke and separator}// Then add the followingelse {     CGContextSetStrokeColorWithColor(context, whiteColor);    CGContextSetLineWidth(context, 1.0);     CGPoint pointA = CGPointMake(paperRect.origin.x,         paperRect.origin.y + paperRect.size.height - 1);    CGPoint pointB = CGPointMake(paperRect.origin.x, paperRect.origin.y);    CGPoint pointC = CGPointMake(paperRect.origin.x + paperRect.size.width - 1,         paperRect.origin.y);    CGPoint pointD = CGPointMake(paperRect.origin.x + paperRect.size.width - 1,         paperRect.origin.y + paperRect.size.height - 1);     draw1PxStroke(context, pointA, pointB, whiteColor);    draw1PxStroke(context, pointB, pointC, whiteColor);    draw1PxStroke(context, pointC, pointD, whiteColor)}
对上面的代码,你现在应该相当熟悉了。最后根据以下代码对RootViewController.m文件进行修改:
// Inside tableView:cellForRowAtIndexPath, inside cell == nil case:((CustomCellBackground *)cell.selectedBackgroundView).selected = YES;// After creating cell, inside indexPath.section == 0 case:((CustomCellBackground *)cell.backgroundView).lastCell =         indexPath.row == _thingsToLearn.count - 1;((CustomCellBackground *)cell.selectedBackgroundView).lastCell =         indexPath.row == _thingsToLearn.count - 1;// After creating cell, else case:((CustomCellBackground *)cell.backgroundView).lastCell =         indexPath.row == _thingsLearned.count - 1;((CustomCellBackground *)cell.selectedBackgroundView).lastCell =         indexPath.row == _thingsLearned.count - 1;// At the end, before return cell:cell.textLabel.highlightedTextColor = [UIColor blackColor];
编译运行工程,你将看到table view变得更美观了:

Table View Finishing Touches

然后,让我们设置下背景和navigation bar,让它们看起来更美观。打开MainWindow.xib文件,展开Navigation Controller,然后点击 Navigation Bar。在Attributes Inspector中,改变Tint颜色为一种更好看的深色,如下所示:

Setting the tint color in a navigation bar

接下来,双击Window,拖动UIImageView控件到window上面,让它填充整个空间。使用你喜欢的图片对UIImageView进行image设置,或者你可以下载来自sxc.hu的jaylopez 制作的图片。

最后我们需要做的是取消table view的默认背景,这样我们的图片就可以看到了。打开 RootViewController.xib文件,选取Table View,修改View section下面Background选项为clear background color:

Removing the striped grouped table view background color

保存XIB文件,编译运行工程,如果一切运行正常,你将看到以下画面:

Finished custom drawn table view

就是这样!一个完整的使用Core Graphics绘制的个性化table view!

现在还可以做什么?

这是本教程开发的例子代码,你可以在这里下载!

现在你应该可以使用Core Graphics去做一些很酷的东西了!但是如果你感兴趣,还有更多的东西提供给你学习 – 看下这篇使用Core Graphics创建光滑按钮的教程,或者这篇使用Core Graphics创建图案的教程!