PaintCode 是一个能够让你像Photoshop一样去设计你的用户界面的软件 – 但是它并不是仅仅保存一张图片当作资源让你来使用,它能够为你生成 CoreGraphic 源码直接使用到View的绘制中.
在这个系列的第一部分教程中, 我们已经告诉大家如何去用PaintCode去创造可以随时改变大小,随时改变颜色的自定义按钮.
这次的教程将会教大家如何使用PaintCode去创造设计一个属于自己的进度条,并且将它集成到代码里.
对于看了第一部分教程读者的来说这篇教程会相对更容易一些。当然,如果没有看过,并不要紧,因为这边教程仍然能够详细的带大家一步一步熟悉PaintCode。第一步一定是安装PaintCode,然后下载这个我已经为大家准备好了的初始项目.
话不多说,现在就打开PaintCode开始学习如何创建新的UI控件吧!
在PaintCode中创建一个新的文件, 点击 FileRename…,将它命名为 DynamicProgressIndicator, 然后确定.
在这个教程中,我们做得控件并不是retina显示的。 任何时候如果你需要生成一个retina显示的控件, 在PaintCode窗口的右下角,先点击Retina 按钮, 然后点击 Canvas 按钮就可以了。
如果你打开实时的代码输出panel, 这些按钮将会在窗口最下面这个panel的顶端, 如下图所示:
接下来,为了让canvas更加适合我们的进度条,我们将canvas稍微的调小一点, 然后把它的背景色换成一个暗一点的颜色,这样可以让看你空间看起来更舒适.
具体步骤是: 点击 Canvas 按钮, 把 canvas 从 320 pixels调整到 70 pixels.然后把Underlay color 调整成如下图一样的灰色. 如果你想要设置成特定的颜色值, 使用 R=45, G=45, B=45, A=255.
在canvas 一切就绪之后, 我们可以开始着手为进度条创建基本的 shapes, gradients, 和 colors.
在工具栏选择Round Rect 工具,然后画一个圆角的长方形在canvas上做为我们控件的外边框. 完成之后选中画好的外边框, 设置这个 shape’s 的属性为如下:
你的 shape 属性栏应该和下图一样:
保持外边框继续选中着, 在左边栏里找到 Stroke 设置, 然后把它设置为 No Stroke. 同样的, 找到并点击 Fill 设置, 选择 Add New Gradient…, 然后点击渐变编辑框左下角的那个颜色选取点. 点击右下角的颜色选取按钮,把 RGB 值设置为 to 82 82 82.然后在点击渐变编辑框右下角那个颜色选取点,并且把他的 RGB 值设置为 106 106 106. 最后将这个 gradient 命名为 Outer Rect Gradient.
注意: 如果以后需要修改PaintCode里的 gradients, colors, 或者 shadows, 大家可以再左边栏下面的 panel 里点击对应的名字的 tab 去修改已命名的属性. 双击需要修改的属性的名字,然后在弹出的dialog里做修改就好了.
现在我们先关掉dialog, 在外边框的属性栏里找到 Outer Shadow section. 然后添加一个RGB 值是 51 51 51 的 Outer Shadow. 然后把它命名为 DarkShadow, 如下图所示:
接着, 我们继续加一个浅色的RGB 值为 171 171 171的 Inner Shadow. 命名 LightShadow,如下图所示:
最后我们把这个Rounded Rectangle 的名字改为 Border.每当完成编辑一个元素立刻给他一个描述性的名字永远都是一个好的习惯,因为这可以让你更好的在你自己的应用里找到他们.
到此为止,进度条的基本外边已经完成, 我们接下来要开始做的是进度槽.
再找到 Round Rect 工具,然后另外拖一个矩形在你的canvas上。 把它属性设置为如下:
然后给这个shape的Fill 设置一个新的gradient,左边的颜色选取点的RGB设置为 48 48 48,右边的设置为 63 63 63, 如下图所示:
设置 Outer Shadow 为 LightShadow, Inner Shadow 为 DarkShadow, 然后把这个shape命名为ProgressTrack.
教程到这里, 你的空间应该看起来像下图一样了:
现在我们来加最后一个可见的元素.
选择Round Rect工具,再拖一个矩形并且把它的属性设置为如下:
最后的这个shape将会在 ProgressTrack 左边缘靠右2像素的位置并且中心垂直对称.
注意:你会发现如果不是去直接修改数值的话,自己手动去修改这个shape的大小和位置会很难,因为进度槽和它的背景太相似了.
当然, 除了直接修改数值,你可以通过下面2种方法达到同样的效果: 1) 选中进度槽背景,复制粘贴,然后把它稍微的修改小一点, 或者 2) 如果你有一个trackpad,用手势把当前的shape放大,这样任然可以使制作更加容易。
为了让进度条看起来更加的显眼一点,我们把进度条改成绿色的— RGB 0 226 0。 大家也可以根据自己爱好去修改颜色。
最后, 把这个shape的 Shadow 和 Inner Shadow 都改成 No Shadow, 取消 Stroke 然后把它改名为 ProgressTrackActive. 现在你的控件应该和下面的差不多了:
看起来很棒吧 — 但是如果想要它动起来,我们需要加一些组件来让它动态的响应事件.
有一点需要大家注意的 – 如果大家想在PaintCode里随时的去调整一个元素的大小,那就应该把它放在一个frame里面,,并且去设置他的属性,这样PaintCode才知道如何去调整它的大小当它的parent frame改变的时候。
下面是我们需要做的:
在上方工具栏选择 Frame tool。在绿色的进度条周围拖出一个fame,如下图所示:
然后把这个fame命名ActiveProgressFrame 然后把它和ProgressTrackActive方框group在一起 (同时选中2个然后点击工具栏里的Group)。然后把这个把这个group命名为 ProgressActiveGroup。
再次选中ActiveProgressFrame 然后勾选Apply only to entirely enclosed shapes。 这样能使这个frame只能影响ProgressTrackActive bar,因为ProgressTrackActive 是这个group里唯一放在在ActiveProgressFrame范围内的元素.
为了让进度条水平居中,进度条需要设置为固定高度以及可变的宽度。
首先,我们需要选中 ProgressTrackActive 然后点击右边和下边的 springs 将他们设置为不可变的。然后点击中间的2个横条将他们变成spring, 如下图所示:
为了测试刚才做得改动,你可以尝试向右拖动ActiveProgressFrame 的拖动点 。 绿色的横条应该会随着你的拖动拉伸。 如果大家尝试将frame上下拖动,会发现绿色的方框只会水平的拉伸,竖直放假仅仅会跟着frame的边框移动。
为了能让整个进度条随着frame的改动而调整,我们需要给整个进度条加一个frame。 首先,把所有元素都放在一个group里面 (包含已经创建了的group),然后点击工具栏里的 Group按钮。最后,命名把这个group为Progress Bar。 新的group应该和下图一样:
接着,选择 Frame 工具,创建一个frame把整个进度条都包含进去。 然后把这个frame加入Progress Bargroup,最后把它命名 ProgressIndicatorFrame.。任然勾选 Apply only to entirely closed shapes,如果你忘记了为什么要这样做,可以在看看教程的前几部。
接下来选中 ProgressActiveGroup。 为了让所有的元素都保持自己的位置,向前几步一样,我们需要设置这个frame只能水平改变大小。
点击 springs/struts 里的水平横条将它变成一个spring。为了让进度槽的位置相对于上左右边框都不会改变, 所以确保只有下面的竖条是一个spring。 下面的图即是frame的具体设置:
将ProgressTrack 和 Border 修改为同样的设置。
现在让我们开始试试刚才的修改吧!选中ProgressIndicatorFrame然后随便的拖拽改变他的大小 — 进度条应该只会水平的改变大小,竖直方向保持不变。
选中ProgressTrackActive shape 然后点击 variable width 按钮, 这个按钮就在 Width 属性的右边,2个中括号包住一个小圆点的图标。
记住!!!保存你所有的改动。 接下来我们要开始把进度条和代码联系起来了。
在 Xcode中,打开刚才下载的初始项目,或者继续你在第一部分教程创建的项目。打开项目之后,展开group Classes > Views。
现在我们需要做的就是像教程1一样,创建一个进度条的subclass。这次我们继承的是 UIProgressView
,这样的话,大家用 PaintCode 创建的进度条就拥有和iOS进度条一样的表现了。
右键点击 Views group然后选择 NewFile…. 接着选择iOSCocoa TouchObjective-C class 模板。 把类命名为 ProgressView,然后设置它继承 UIProgressView。
打开 ProgressView.m文件,删除方法 initWithFrame:
然后去掉 drawRect:方法的注释。
下面的这个文件这时候的样子:
#import "ProgressView.h" @implementation ProgressView - (void)drawRect:(CGRect)rect { // Drawing code } @end
现在我们可以开始粘贴PaintCode给你的代码了。
回到 PaintCode 然后确定代码窗口在下面打开着; 如果没有,在菜单里选择 View > Code 。 设置平台为 iOS > Objective-C,系统版本是 iOS 5+,orgin设置为Default Origin,内存管理设置为 ARC, 如下图所示:
现在把所有代码复制粘贴到 drawRect:方法里。
这段代码可能很长,但是它是PaintCode早就为你准备好的了。
//// General Declarations CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = UIGraphicsGetCurrentContext(); //// Color Declarations UIColor* fillColor = [UIColor colorWithRed: 0.416 green: 0.416 blue: 0.416 alpha: 1]; UIColor* strokeColor = [UIColor colorWithRed: 0.322 green: 0.322 blue: 0.322 alpha: 1]; UIColor* shadowColor2 = [UIColor colorWithRed: 0.2 green: 0.2 blue: 0.2 alpha: 1]; UIColor* shadowColor3 = [UIColor colorWithRed: 0.671 green: 0.671 blue: 0.671 alpha: 1]; UIColor* fillColor2 = [UIColor colorWithRed: 0.247 green: 0.247 blue: 0.247 alpha: 1]; UIColor* strokeColor2 = [UIColor colorWithRed: 0.188 green: 0.188 blue: 0.188 alpha: 1]; UIColor* color = [UIColor colorWithRed: 0 green: 0.886 blue: 0 alpha: 1]; //// Gradient Declarations NSArray* outerRectGradientColors = [NSArray arrayWithObjects: (id)strokeColor.CGColor, (id)fillColor.CGColor, nil]; CGFloat outerRectGradientLocations[] = {0, 1}; CGGradientRef outerRectGradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)outerRectGradientColors, outerRectGradientLocations); NSArray* gradientColors = [NSArray arrayWithObjects: (id)strokeColor2.CGColor, (id)fillColor2.CGColor, nil]; CGFloat gradientLocations[] = {0, 1}; CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)gradientColors, gradientLocations); //// Shadow Declarations UIColor* darkShadow = shadowColor2; CGSize darkShadowOffset = CGSizeMake(3.1, 3.1); CGFloat darkShadowBlurRadius = 5; UIColor* lightShadow = shadowColor3; CGSize lightShadowOffset = CGSizeMake(3.1, 3.1); CGFloat lightShadowBlurRadius = 5; //// Frames CGRect progressIndicatorFrame = CGRectMake(-1, 0, 321, 47); //// Subframes CGRect group = CGRectMake(CGRectGetMinX(progressIndicatorFrame) + 10, CGRectGetMinY(progressIndicatorFrame) + 9, CGRectGetWidth(progressIndicatorFrame) - 25, 20); CGRect activeProgressFrame = CGRectMake(CGRectGetMinX(group) + floor(CGRectGetWidth(group) * 0.00000 + 0.5), CGRectGetMinY(group) + floor(CGRectGetHeight(group) * 0.00000 + 0.5), floor(CGRectGetWidth(group) * 1.00000 + 0.5) - floor(CGRectGetWidth(group) * 0.00000 + 0.5), floor(CGRectGetHeight(group) * 1.00000 + 0.5) - floor(CGRectGetHeight(group) * 0.00000 + 0.5)); //// Abstracted Attributes CGRect progressTrackActiveRect = CGRectMake(CGRectGetMinX(activeProgressFrame) + 4, CGRectGetMinY(activeProgressFrame) + 5, CGRectGetWidth(activeProgressFrame) - 8, 10); //// Progress Bar { //// Border Drawing CGRect borderRect = CGRectMake(CGRectGetMinX(progressIndicatorFrame) + 2, CGRectGetMinY(progressIndicatorFrame) + 3, CGRectGetWidth(progressIndicatorFrame) - 5, 34); UIBezierPath* borderPath = [UIBezierPath bezierPathWithRoundedRect: borderRect cornerRadius: 4]; CGContextSaveGState(context); CGContextSetShadowWithColor(context, darkShadowOffset, darkShadowBlurRadius, darkShadow.CGColor); CGContextBeginTransparencyLayer(context, NULL); [borderPath addClip]; CGContextDrawLinearGradient(context, outerRectGradient, CGPointMake(CGRectGetMidX(borderRect), CGRectGetMinY(borderRect)), CGPointMake(CGRectGetMidX(borderRect), CGRectGetMaxY(borderRect)), 0); CGContextEndTransparencyLayer(context); ////// Border Inner Shadow CGRect borderBorderRect = CGRectInset([borderPath bounds], -lightShadowBlurRadius, -lightShadowBlurRadius); borderBorderRect = CGRectOffset(borderBorderRect, -lightShadowOffset.width, -lightShadowOffset.height); borderBorderRect = CGRectInset(CGRectUnion(borderBorderRect, [borderPath bounds]), -1, -1); UIBezierPath* borderNegativePath = [UIBezierPath bezierPathWithRect: borderBorderRect]; [borderNegativePath appendPath: borderPath]; borderNegativePath.usesEvenOddFillRule = YES; CGContextSaveGState(context); { CGFloat xOffset = lightShadowOffset.width + round(borderBorderRect.size.width); CGFloat yOffset = lightShadowOffset.height; CGContextSetShadowWithColor(context, CGSizeMake(xOffset + copysign(0.1, xOffset), yOffset + copysign(0.1, yOffset)), lightShadowBlurRadius, lightShadow.CGColor); [borderPath addClip]; CGAffineTransform transform = CGAffineTransformMakeTranslation(-round(borderBorderRect.size.width), 0); [borderNegativePath applyTransform: transform]; [[UIColor grayColor] setFill]; [borderNegativePath fill]; } CGContextRestoreGState(context); CGContextRestoreGState(context); //// ProgressTrack Drawing CGRect progressTrackRect = CGRectMake(CGRectGetMinX(progressIndicatorFrame) + 12, CGRectGetMinY(progressIndicatorFrame) + 12, CGRectGetWidth(progressIndicatorFrame) - 29, 14); UIBezierPath* progressTrackPath = [UIBezierPath bezierPathWithRoundedRect: progressTrackRect cornerRadius: 7]; CGContextSaveGState(context); CGContextSetShadowWithColor(context, lightShadowOffset, lightShadowBlurRadius, lightShadow.CGColor); CGContextBeginTransparencyLayer(context, NULL); [progressTrackPath addClip]; CGContextDrawLinearGradient(context, gradient, CGPointMake(CGRectGetMidX(progressTrackRect), CGRectGetMinY(progressTrackRect)), CGPointMake(CGRectGetMidX(progressTrackRect), CGRectGetMaxY(progressTrackRect)), 0); CGContextEndTransparencyLayer(context); ////// ProgressTrack Inner Shadow CGRect progressTrackBorderRect = CGRectInset([progressTrackPath bounds], -darkShadowBlurRadius, -darkShadowBlurRadius); progressTrackBorderRect = CGRectOffset(progressTrackBorderRect, -darkShadowOffset.width, -darkShadowOffset.height); progressTrackBorderRect = CGRectInset(CGRectUnion(progressTrackBorderRect, [progressTrackPath bounds]), -1, -1); UIBezierPath* progressTrackNegativePath = [UIBezierPath bezierPathWithRect: progressTrackBorderRect]; [progressTrackNegativePath appendPath: progressTrackPath]; progressTrackNegativePath.usesEvenOddFillRule = YES; CGContextSaveGState(context); { CGFloat xOffset = darkShadowOffset.width + round(progressTrackBorderRect.size.width); CGFloat yOffset = darkShadowOffset.height; CGContextSetShadowWithColor(context, CGSizeMake(xOffset + copysign(0.1, xOffset), yOffset + copysign(0.1, yOffset)), darkShadowBlurRadius, darkShadow.CGColor); [progressTrackPath addClip]; CGAffineTransform transform = CGAffineTransformMakeTranslation(-round(progressTrackBorderRect.size.width), 0); [progressTrackNegativePath applyTransform: transform]; [[UIColor grayColor] setFill]; [progressTrackNegativePath fill]; } CGContextRestoreGState(context); CGContextRestoreGState(context); //// Group { //// ProgressTrackActive Drawing UIBezierPath* progressTrackActivePath = [UIBezierPath bezierPathWithRoundedRect: progressTrackActiveRect cornerRadius: 5]; [color setFill]; [progressTrackActivePath fill]; } } //// Cleanup CGGradientRelease(outerRectGradient); CGGradientRelease(gradient); CGColorSpaceRelease(colorSpace);
注意: 你的代码可能看起来和上面的有一些不一样,这取决于每个人在画图的时候的细节,这没啥关系。
这里我们可以明显看到PaintCode给我们做了多少工作。大家可以想象如果一行一行的去写会用多少时间!但是如果这是你的爱好,当然没有关系— 但是大部分应该会更想花时间在编写一些更有趣的代码上吧! :]
代码前部分是 colors, gradients, 和 shadows的申明。 接着是frame的申明。 这里会有一个 progressTrackActiveRect
在//// Abstracted Attributes
代码段里, 他是用来控制进度条绿色部分的进度的, 工作原理和 UIProgressView
一样。
接下来 Progress Bar
代码段是用来绘制整个进度条的边框,进度槽,以及其他的部分,然后另一个代码段是管理 ProgressActiveGroup的。
最后就是一些收尾代码。
注意: 这篇教程并不会去深入的讲解 Core Graphics绘制。 我们使用PaintCode的原因就是让大家能够在不了解CoreGraphic情况下去创建更多的炫酷的控件。这让大家能够花更多时间在开发和调试上,而不是在这里慢慢的画画。
但是,了解一点点Core Graphics 知识能让大家更好的去认识PaintCode生成的代码,也同样可以让大家去修改这些代码。 要想了解更多 Core Graphics, 请查看我们的些列教程 Core Graphics tutorial series!
现在我们的进度条已经准备好了,接下来的第一个测试就是看它是否能显示出来。
在xcode里找到 ResourcesStoryboardsMainStoryboard.storyboard ,然后打开它。 选择 Progress View Controller scene 然后从Object Library添加一个 View 进去。在Identity Inspector 里改变这个view的class为ProgressView, 然后在 Size Inspector里面把它的属性改为如下:
最后把这个项目运行一次。我们的进度条应该和下面一样:
虽然进度条看起来已经不错了 — 但是他并没有按照storyboard里view的大小绘制出来,要解决这个问题,我们继续回到 ProgressView.m里的 drawRect:
找到下面这行:
… //// Frames CGRect progressIndicatorFrame = CGRectMake(2, 1, 318, 34); ...
把它改成下面的这句:
- (void)drawRect:(CGRect)rect { ... // Frames CGRect progressIndicatorFrame = rect; ... }
这句是让进度条的大小和我们在storyboard里设置的view的大小一样。
再编译运行一次,进度条就应该和storyboard里的veiw一样大小了:
现在进度条已经能正确的显示出来了,接下来我们要做的就是让他动起来。
打开 ProgressViewController.m 在 @implementation
之前加入下面的代码:
// 1 #import "ProgressView.h" #import "ProgressViewController.h" // 2 #define kSecondsForCompleteUpdate 3.0 #define kUpdateInterval 0.02 // 3 @interface ProgressViewController () @property (weak, nonatomic) IBOutlet ProgressView *progressView; @property (weak, nonatomic) IBOutlet UIButton *startProgressButton; @property (strong, nonatomic) NSTimer *timer; @end
我们现在来看看上面的代码做了什么:
kSecondsForCompleteUpdate
和 kUpdateInterval
并给他们赋值。 用它们来控制着进度条的行为。UIButton
和 UIProgressView申明了带xib连接口的属性,
以及一个定时器用来更新进度条的 progress属性
.接着添加下面的代码 (任然在 ProgressViewController.m里):
-(IBAction)startProgressTapped { self.progressView.progress = 0.0; self.startProgressButton.enabled = NO; self.timer = [NSTimer timerWithTimeInterval:kUpdateInterval target:self selector:@selector(updateProgressView) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode]; [self.timer fire]; }
当屏幕上的按钮被点击时,这段代码将被调用。 首先先把进度条的 progress
属性设置为 0。然后将按钮禁用掉,这样在进度条动画进行的时候我们将不能再次点击按钮。 然后,创建一个定时器,用来控制动画的进行时长。
最后, 把定时器加进run loop来启动定时器-模拟事件的开始。
让我们来看看self.timer
这行代码。 大家会发现定事情调用了 updateProgressView这个方法,
这是用来更新进度条的进度的。 我们马上加加入这段代码,
把下面这段加入 ProgressViewController.m中:
-(void)updateProgressView; { if (self.progressView.progress < 1.0) { self.progressView.progress += (kUpdateInterval / kSecondsForCompleteUpdate); } else { [self.timer invalidate]; self.startProgressButton.enabled = YES; } }
这段代码会检查progress属性是否还小于1, 也就是还没有完成。 如果还没完成,逐步的增加进度, 用前面定义2个常量来控制进度条增加的速率。
如果进度条的progress大于1了,也就是他100%完成了, 我们会把定时器禁用掉,然后恢复按钮来让我们能够再次模拟进度条增加。
现在我们剩下的就是添加一个按钮来触发 startProgressTapped方法,让我们能开始模拟整个测试了。
回到storyboard然后创建一个 Round Rect Button 放在 Progress View Controller 里面。 在 Attributes Inspector 里面把它的 title 改成 “Start Progress” 选择 Touch Up Inside 把他和代码里的 startProgressTapped连接
起来。
然后修改按钮属性,在 Size Inspector里,应该和下图一样:
接下来,链接代码里的按钮和进度条。 你的链接应该和下面一样:
编译运行一次项目,然后点击 Start Progress 按钮。我们的进度条应该就和下图一样了:
Umm…进度条居然没有动! 点了按钮以后啥都没发生!!怎么破!
问题出在,在 ProgressView
里面没有代码去更新进度槽。这玩意儿仍然是静态的被显示着,就像在PaintCode里一样。 我们需要增加一些代码,来让进度条可以基于自己的 progress属性来绘制。
换到 ProgressView.m文件里.。找到下面这段:
… //// Abstracted Attributes CGRect progressTrackActiveRect = CGRectMake(CGRectGetMinX(activeProgressFrame) + 2, CGRectGetMinY(activeProgressFrame) + 2, CGRectGetWidth(activeProgressFrame) - 4, 10); …
用下面的代码替换掉整个 CGRect progressTrackActiveRect
这行:
CGRect progressTrackActiveRect = CGRectMake(CGRectGetMinX(activeProgressFrame) + 3, CGRectGetMinY(activeProgressFrame) + 2, (CGRectGetWidth(activeProgressFrame) - 4) * self.progress, 10);
新的代码会基于进度条的 progress
属性去改变它的rect的宽。
最后一件事 —设置进度条的progress
为0。 这样在程序启动的时候,进度条就能和我们希望的一样处于0的位置。
回到 ProgressViewController.m 文件加入下面的代码:
-(void)viewDidLoad { [super viewDidLoad]; self.progressView.progress = 0.0; }
在跑一次程序,点击按钮。 我们的的进度条应该开始逐步的填满整个进度槽了。
现在我们完成了可以动态更新的进度条了。 他可以用来模拟3秒钟的后台任务或者下载任务。
你可以在这里下载完整的PaintCode和XCode项目。
恭喜大家 – 在这个过程中使用PaintCode制作了一个自定义的进度条,并且把它集成进了自己的项目中,相信大家一定学到了不少。
有了这个新技术,大家可以想怎么做怎么做,随意的去自定义自己的进度条 – 甚至是iOS7里的扁平进度条 :]
我们还有好消息 – 更多的教程会陆续而!在第三部分教程里,我们将会教你如何去用bezier路径画出一个箭头,并把它使用在一个简单的游戏里。
http://www.raywenderlich.com/35720/paintcode-tutorial-custom-progress-bar