欢迎回到Core Graphics 101教程系列的另一篇教程!在这篇教程中,我们将会结合实例开始讲解Core Graphics 101!
在教程1,2和3,我们讲解了如何个性化定制一个Table view的全过程 – 仅仅使用了Core Graphics!
在这篇教程中,我们将着手另一个实例 – 如何个性化定制一个UIButton。
在教程中,我们将会学到如何绘制圆角矩形,如何对使用Core Graphics绘制的图形进行简单的染色处理,并且巩固我们之前学过的一些概念。
正如来自Under The Bridge的Alex Curylo多次提到过的,已经有很多关于如何个性化定制UIButton的方法了。
我个人推荐的快速简易制作按钮的方法是Dermot Daly写的Button Maker。但是我认为在这种讨论中缺少的是如何使用Core Graphics个性化定制你的按钮的详细教程。
教程相当的简单,用这种教学方式,你可以直观的了解你将要开发的app内容。现在让我们开始制作按钮吧!开始
在Xcode中,使用View-based Application 模版创建一个新的项目,命名为“CoolButton”。
然后确保你选中了 ”Groups & Files”中的”Classes” 组,进入菜单的”FileNew File…”,选择 iOSCocoa Touch Class,Objective-C 类,确保”Subclass of UIView”被选中,然后点击下一步。命名文件为 ”CoolButton.m”,然后注意选上”Also create CoolButton.h”,然后点击”完成“。
接着使用以下的代码,替换掉CoolButton.h文件的内容:
#import <UIKit/UIKit.h> @interface CoolButton : UIButton { CGFloat _hue; CGFloat _saturation; CGFloat _brightness; } @property CGFloat hue; @property CGFloat saturation; @property CGFloat brightness; @end |
注意到我们没有继承UIVIew,而是继承UIButton。我们还定义了一些属性变量,去设置view的色彩,饱和度还有亮度。
接下来,我们使用以下内容对CoolButton.m文件做改动:
// Under @implementation @synthesize hue = _hue; @synthesize saturation = _saturation; @synthesize brightness = _brightness; // Delete initWithFrame and add the following: -(id) initWithCoder:(NSCoder *)aDecoder { if ((self = [super initWithCoder:aDecoder])) { self.opaque = NO; self.backgroundColor = [UIColor clearColor]; _hue = 1.0; _saturation = 1.0; _brightness = 1.0; } return self; } // Uncomment drawRect and replace the contents with: CGContextRef context = UIGraphicsGetCurrentContext(); CGColorRef color = [UIColor colorWithHue:_hue saturation:_saturation brightness:_brightness alpha:1.0].CGColor; CGContextSetFillColorWithColor(context, color); CGContextFillRect(context, self.bounds); // Add to the bottom of the file - (void)setHue:(CGFloat)hue { _hue = hue; [self setNeedsDisplay]; } - (void)setSaturation:(CGFloat)saturation { _saturation = saturation; [self setNeedsDisplay]; } - (void)setBrightness:(CGFloat)brightness { _brightness = brightness; [self setNeedsDisplay]; } |
这里我们只是初始化了一些变量,使用设定的颜色去填充整个按钮,确保一切正常开始。
注意到我们使用了不同的构造函数去设置颜色 – 而不是使用 colorWithRed:Green:Blue方法,我们使用了colorWithHue:saturation:brightness方法。这将让接下来的操作更简单。
我们最后要做的事情,是重写hue(颜色),saturation(饱和度)和brightness(亮度)属性变量的setter方法,然后当setter方法被调用的时候,我们调用setNeedsDisplay方法。这会在用户改变按钮的颜色的时候,强制我们的view去重绘一遍。
现在,我们回到CoolButtonViewController.h文件,使用以下代码对它进行修改:
// Before @interface @class CoolButton; // Inside @interface CoolButton *_button; // After @interface @property (retain) IBOutlet CoolButton *button; - (IBAction)hueValueChanged:(id)sender; - (IBAction)saturationValueChanged:(id)sender; - (IBAction)brightnessValueChanged:(id)sender; |
这里我们申明了一个对button的outlet引用(我们将在Interface Builder中添加)和一些设定slider控件属性值时的Action回调函数(我们也将会在Interface Builder中添加)。
现在让我们继续吧。打开CoolButtonViewController.m文件,然后拖动一个UIButton,3个UILabel和3个UISlider控件到view中,像下图一样:
接下来,转到Identity Inspector,修改Class的下拉选项为CoolButton,从而改变UIButton的class属性为CoolButton。
另外,确保Attributes Inspector被选中,然后切换Button的drawing type为Custom,用以取消按钮圆角的默认设定。
然后,control键+鼠标拖动“File’s Owner”到CoolButton,然后连接到按钮的outlet上。相似的,control键+鼠标拖动slider的“File’s Owner”,连接到恰当的数值变化回调函数。在我们运行代码之前,使用以下代码修改CustomViewController.m文件:
// In the import section #import "CoolButton.h" // Under @implementation @synthesize button = _button; // In viewDidUnload self.button = nil; // In dealloc [_button release]; _button = nil; // Add to the bottom of the file - (IBAction)hueValueChanged:(id)sender { UISlider *slider = (UISlider *)sender; _button.hue = slider.value; } - (IBAction)saturationValueChanged:(id)sender { UISlider *slider = (UISlider *)sender; _button.saturation = slider.value; } - (IBAction)brightnessValueChanged:(id)sender { UISlider *slider = (UISlider *)sender; _button.brightness = slider.value; } |
注意到UISlider的默认值范围设定为0.0到1.0, 和我们变化范围在0.0到1.0的颜色值,饱和度和亮点值一致,所以我们可以直接对它们进行设定了。编译运行工程,然后如果一切都运行正常,你应该能使用slider去调整”CoolButton”的颜色。
绘制圆角矩形按钮
你其实也可以使用直角按钮,但是目前大部分的软件都是用圆角按钮的。
在最后的教程中,我们讲解了如何使用CGContextAddArc API去绘制弧线。基于经验我们当然可以使用它在每个边角处绘制弧线了,然后绘制线条去连接他们。
但是有个更简单的方法,让我们不需要做这么多数学运算,它很适合绘制圆角矩形。这种方法就是CGContextAddArcToPoint API。
CGContextAddArcToPoint API可以让你在绘制弧线的时候,指定两条切线和半径的大小。下面来自Quartz2D Programming Guide的示意图很好的解释了相关含义。
所以对于一个矩形的情况,我们很明显知道每条想要绘制的弧线的切线 – 他们就是矩形的边!并且我们可以根据想要的矩形角弧度去指定半径的大小 – 更大,更圆。
另外还需要对这个函数补充的是如果当前在轨迹上的点没有设定到你开始绘制弧线的位置,程序就会从当前点到轨迹的初始位置绘制一条线。所以你可以使用这种简便方法,仅调用几个函数去绘制一个圆角矩形。
因为我们将要在这篇教程中去创建多个圆角矩形,让我们在Common.h/m文件中添加一个辅助函数去创建圆角矩形的轨迹方法,获得一个矩形。
如果你现在还没有Common.h/m文件,请到这里下载Common.h/m,并且添加到你的工程中。然后将以下内容添加到Common.h文件中:
CGMutablePathRef createRoundedRectForRect(CGRect rect, CGFloat radius); |
添加以下代码到Common.m文件的底部:
CGMutablePathRef createRoundedRectForRect(CGRect rect, CGFloat radius) { CGMutablePathRef path = CGPathCreateMutable(); CGPathMoveToPoint(path, NULL, CGRectGetMidX(rect), CGRectGetMinY(rect)); CGPathAddArcToPoint(path, NULL, CGRectGetMaxX(rect), CGRectGetMinY(rect), CGRectGetMaxX(rect), CGRectGetMaxY(rect), radius); CGPathAddArcToPoint(path, NULL, CGRectGetMaxX(rect), CGRectGetMaxY(rect), CGRectGetMinX(rect), CGRectGetMaxY(rect), radius); CGPathAddArcToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMaxY(rect), CGRectGetMinX(rect), CGRectGetMinY(rect), radius); CGPathAddArcToPoint(path, NULL, CGRectGetMinX(rect), CGRectGetMinY(rect), CGRectGetMaxX(rect), CGRectGetMinY(rect), radius); CGPathCloseSubpath(path); return path; } |
上面的代码按照以下图示顺序绘制了圆角矩形:
另外注意到上面,我们使用了一些来自CGGeometry.h文件的辅助函数,在我们提供的矩形中,来获得多个位置点信息。好的,让我们现在来看下它的原理!打开 CoolButton.m文件,根据以下代码做修改:
// At top of file #import "Common.h" // Replace the contents of drawRect with the following: CGContextRef context = UIGraphicsGetCurrentContext(); CGColorRef outerTop = [UIColor colorWithHue:_hue saturation:_saturation brightness:_brightness alpha:1.0].CGColor; CGColorRef shadowColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.5].CGColor; CGFloat outerMargin = 5.0f; CGRect outerRect = CGRectInset(self.bounds, outerMargin, outerMargin); CGMutablePathRef outerPath = createRoundedRectForRect(outerRect, 6.0); if (self.state != UIControlStateHighlighted) { CGContextSaveGState(context); CGContextSetFillColorWithColor(context, outerTop); CGContextSetShadowWithColor(context, CGSizeMake(0, 2), 3.0, shadowColor); CGContextAddPath(context, outerPath); CGContextFillPath(context); CGContextRestoreGState(context); } |
这里我们定义了自己的两种颜色,然后使用了CGRectInset函数去获取一个更小的矩形(每条边有5个像素点),我们将会在这个矩形上绘制圆角矩形。并且我们把它做的尽量小以便留有空间在它的外围绘制阴影。
接下来,我们调用刚才写好的函数为我们的圆角矩形创建轨迹,然后设置填充颜色和阴影,添加轨迹到我们的context上面,并且调用FillPath函数,使用当前颜色填充整个矩形。
注意我们只希望在按钮没有被点击的时候,运行代码。编译并运行app,如果一些运行正常,你将看到以下画面:
美化我们的按钮
我们现在有个像按钮模样的东西了!但是还不够美观:[
让我们现在着手处理下吧!增加一些重要的改进步骤。打开CoolButton.m文件,并根据以下代码做修改:
// Replace the colors section with the following CGColorRef blackColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:1.0].CGColor; CGColorRef highlightStart = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.4].CGColor; CGColorRef highlightStop = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.1].CGColor; CGColorRef shadowColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:0.5].CGColor; CGColorRef outerTop = [UIColor colorWithHue:_hue saturation:_saturation brightness:1.0*_brightness alpha:1.0].CGColor; CGColorRef outerBottom = [UIColor colorWithHue:_hue saturation:_saturation brightness:0.80*_brightness alpha:1.0].CGColor; CGColorRef innerStroke = [UIColor colorWithHue:_hue saturation:_saturation brightness:0.80*_brightness alpha:1.0].CGColor; CGColorRef innerTop = [UIColor colorWithHue:_hue saturation:_saturation brightness:0.90*_brightness alpha:1.0].CGColor; CGColorRef innerBottom = [UIColor colorWithHue:_hue saturation:_saturation brightness:0.70*_brightness alpha:1.0].CGColor; // Add the following to the bottom CGContextSaveGState(context); CGContextAddPath(context, outerPath); CGContextClip(context); drawGlossAndGradient(context, outerRect, outerTop, outerBottom); CGContextRestoreGState(context); |
首先我们设置了多种颜色,以便后面使用。有一些普通的颜色,还有一些基于以前的参数设置的颜色。基本颜色是我们构建的,还构建了其他一些颜色,让它们比当前颜色在不同程度上更暗些。
如果你这样去定义颜色,可以让你更容易去改变view(视图)的颜色,提高代码的可重用性!
接下来我们要做的就是裁切我们的圆角矩形,然后用渐变的颜色去填充它(而不是单一的颜色)。编译并运行,我们的按钮现在变得更美观了:
现在让我们在按钮内部轨迹添加一种跟外部不同的渐变颜色,创建一种斜面视觉效果。使用以下代码对CoolButton.m文件进行修改:
// Add after the creation of the outerPath CGFloat innerMargin = 3.0f; CGRect innerRect = CGRectInset(outerRect, innerMargin, innerMargin); CGMutablePathRef innerPath = createRoundedRectForRect(innerRect, 6.0); // At the bottom CGContextSaveGState(context); CGContextAddPath(context, innerPath); CGContextClip(context); drawGlossAndGradient(context, innerRect, innerTop, innerBottom); CGContextRestoreGState(context); |
这里我们使用CGRectInset函数进一步缩小矩形,然后利用它获得一个圆角矩形,并且填充上渐变颜色。编译运行,你会看到一些微妙的改进:
当按钮被点击的时候,我们在它的顶部添加一点高亮效果。使用以下代码对CoolButton.m文件做进一步的修改:
// After the creation of the innerPath CGFloat highlightMargin = 2.0f; CGRect highlightRect = CGRectInset(outerRect, highlightMargin, highlightMargin); CGMutablePathRef highlightPath = createRoundedRectForRect(highlightRect, 6.0); // At the bottom if (self.state != UIControlStateHighlighted) { CGContextSaveGState(context); CGContextSetLineWidth(context, 4.0); CGContextAddPath(context, outerPath); CGContextAddPath(context, highlightPath); CGContextEOClip(context); drawLinearGradient(context, outerRect, highlightStart, highlightStop); CGContextRestoreGState(context); } |
我们在创建另一个比外部矩形稍微小的圆角矩形,并且在两个矩形之间使用一种alpha高亮渐变颜色去填充,使用我们上篇教程提到的Even-Odd Clip技巧。编译运行程序,你会发现这是很微妙的变化:
让我们结束所有的步骤吧。添加以下代码到drawRect:方法的底部:
CGContextSaveGState(context); CGContextSetLineWidth(context, 2.0); CGContextSetStrokeColorWithColor(context, blackColor); CGContextAddPath(context, outerPath); CGContextStrokePath(context); CGContextRestoreGState(context); CGContextSaveGState(context); CGContextSetLineWidth(context, 2.0); CGContextSetStrokeColorWithColor(context, innerStroke); CGContextAddPath(context, innerPath); CGContextClip(context); CGContextAddPath(context, innerPath); CGContextStrokePath(context); CGContextRestoreGState(context); CFRelease(outerPath); CFRelease(innerPath); CFRelease(highlightPath); |
我们这里所做的是对外部轨迹使用黑色上色(使用两个像素点以避免1px的问题),然后使用1像素点对内部轨迹上色。
你可能会感慨,”我的天“,“那是两个像素点,而不是一个!”好的,我们这里使用了不同的技术解决ipx的问题 – 之前提到过的”clipping mask”技巧。基本上我们设定笔画为2个像素点,然后裁剪掉外部的区域,最后我们释放掉创建的轨迹线。
几笔画能做到的这样子确实挺赞的!
突出显示按钮
虽然 我们的按钮看起来相当酷,但是它并不像一个按钮那样可以响应点击操作。没有任何迹象可以看出按钮是否被按下。
幸运的是,Jeff LaMarche在他的文章 posts on the subject 中展示了解决方案。
基本的思路是我们需要重写touch events的方法,去让按钮自己重新绘制显示,因为它需要根据点击状态去更新外形。使用下面的代码对CoolButton.m文件进行修改:
// Add the following methods to the bottom - (void)hesitateUpdate { [self setNeedsDisplay]; } -(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; [self setNeedsDisplay]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesMoved:touches withEvent:event]; [self setNeedsDisplay]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesCancelled:touches withEvent:event]; [self setNeedsDisplay]; [self performSelector:@selector(hesitateUpdate) withObject:nil afterDelay:0.1]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; [self setNeedsDisplay]; [self performSelector:@selector(hesitateUpdate) withObject:nil afterDelay:0.1]; } |
编译运行工程,现在当你点击按钮的时候,你将看到不同的变化 - 突出显示和点击消失了。现在让我们修改一下代码做得更好一些: |
// At beginning of function CGFloat actualBrightness = _brightness; if (self.state == UIControlStateHighlighted) { actualBrightness -= 0.10; } // Then replace all further cases in the function that use // "_brightness" to define colors with "actualBrightness". |
重新编译运行,现在当你点击按钮的时候,按钮看起来更美观了!
接下来我们可以做什么?
本教程的例子源代码工程,可以到这里下载。
既然我们从零开始了解了如果创建个性化按钮的全过程,你将对如何个性化定制按钮,以便使用到自己的工程中觉得相当熟悉了!
接下来在Core Graphics 101 教程系列的最后一篇教程,我们将讨论如何使用模式去制作很酷的效果!