Swift Core Graphics教程之Gradients 与 Context
欢迎回到我们的Swift核心绘图教程系列! 在第一部分中,你学习到了使用storyboard绘制线条和矩形. 在第二部部分中,你将深入核心绘图,使用CGContext实现渐变效果 更新时间 04/15/2015为Xcode 6.3 和 Swift1.2更新 核心绘图你现在已经从简单的UIKit深入到核心绘图。 下图是各个框架的关系图: UIKit是在最顶层,使用最友好的框架。你使用过UIBezierPath的就是在UIKit层中对Core Graphics层中CGPath的封装。 你可以看到 Core Graphics 的对象和方法都是CG开头的,非常容易辨认。 另外CG方法都是C方法,在调用的时候不需要明确的指定参数名,和一般Swift调用方法不一样。 从Graph View开始本次的目标就是使用历史数据创建一个图表。 在绘图之前,你需要创建一个storyboard,写一些显示图表的代码。 完成后的视图结构如下图: 如果你还没有完成,从这里下载开始的项目。 打开 File/New/File…,选择 iOS/Source/Cocoa Touch Class 打开 Main.storyboard 拖一个 UIView 到控制器的视图。 视图将包含一个Graph和Counter Views,确定他们是视图控制器的主视图的子视图,并且Graph在Counter Views之上。 此时目录结构应该是这样的: 在 Size Inspector,设置 X=150,Y=50,Width=300, 创建自动布局约束非常简单,在第一部分中有讲到。
当创建了视图,通常我们我们会设置一个临时的背景颜色,这样做非常有用,可以让我们清楚的看到我们做了什么。 在 Attributes Inspector,将背景颜色设置为黄色. 再拖一个 UIView 到黄色的视图作为它的子视图. 在 Identity Inspector,将新创建的视图的class设置为GraphView. 在 Size Inspector,设置 X=0,Y=25, 在Document Outline,将 Counter View拖到黄色的视图中作为它的子视图,这里需要注意需要放置在Graph View的后方。 在移动 Counter View之后,自动布局约束会变红. 选择 Counter View,在storyboard 右下方找到并点击 Resolve Auto Layout Issues,选择 你可以重置为默认的约束,因为现在Counter View充满了整个Container View 在 Document Outline 双击黄色视图来重命名为 Container View. 现在 Document Outline 是这样子的: 你需要一个Container View是因为在 Counter View 和 Graph View的渐变动画中需要用到。 打开 ViewController.swift,为Container 和 Graph Views 添加outloets: @IBOutlet weak var containerView: UIView! @IBOutlet weak var graphView: GraphView! 这一步为container view 和 graph view创建outlet,现在我们将他们和storyboard的视图关联起来。 打开 Main.storyboard, 将 Graph View Container View 和 outlets关联起来: 设置渐变动画仍然在 Main.storyboard中,从Object Library 拖一个UITapGestureRecognizer 到 Container View: 打开 ViewController.swift 在开头添加属性: var isGraphViewShowing = false 这个将简单的标记用于判断当前graph view是否正在显示; 为点击事件添加过渡效果的方法: @IBAction func counterViewTap(gesture:UITapGestureRecognizer?) { if (isGraphViewShowing) { //hide Graph UIView.transitionFromView(graphView,toView: counterView,duration: 1.0,options: UIViewAnimationOptions.TransitionFlipFromLeft UIViewAnimationOptions.ShowHideTransitionViews,completion:nil) } else { //show Graph UIView.transitionFromView(counterView,toView: graphView,options: UIViewAnimationOptions.TransitionFlipFromRight UIViewAnimationOptions.ShowHideTransitionViews,completion: nil) } isGraphViewShowing = !isGraphViewShowing } UIView.transitionFromView(_:toView:duration:options:completion:) 在btnPushButton(_:)的最后添加以下代码: if isGraphViewShowing { counterViewTap(nil) } 这是为了防止在图标正在显示的过程中点击,将闪回计数器视图。 最后,为了让这个效果生效,打开 Main.storyboard,将点击手势与counterViewTap(gesture:)关联起来。 构建并运行程序,你可以看到graph view在你打开程序的时候。点击它,graph view会消失,counter view现实,中间有过渡动画。 分析Graph View还记得第一部分的绘图模型嘛?它描述了用Core Graphics绘图从里到外的过程,你需要明白这里的顺序在你写代码之前:
绘制渐变现在我们在Graph View 的 context中绘制渐变效果。 打开 GraphView.swift 将里面带代码替换为以下: import UIKit @IBDesignable class GraphView: UIView { //1 - the properties for the gradient @IBInspectable var startColor: UIColor = UIColor.redColor() @IBInspectable var endColor: UIColor = UIColor.greenColor() override func drawRect(rect: CGRect) { //2 - get the current context let context = UIGraphicsGetCurrentContext() let colors = [startColor.CGColor,endColor.CGColor] //3 - set up the color space let colorSpace = CGColorSpaceCreateDeviceRGB() //4 - set up the color stops let colorLocations:[CGFloat] = [0.0,1.0] //5 - create the gradient let gradient = CGGradientCreateWithColors(colorSpace,colors,colorLocations) //6 - draw the gradient var startPoint = CGPoint.zeroPoint var endPoint = CGPoint(x:0,y:self.bounds.height) CGContextDrawLinearGradient(context,gradient,startPoint,endPoint,0) } } 这里有以下步骤
通过drawRect(_:)渐变效果将充满整个rect。 在Xcode你通过设置代码或者在storyboard的Assistant Editor调节,可以直接在Graph View看到效果。 在storyboard中,选择 Graph View. 在 *Attributes 在 Main.storyboard,一次选择所有的视图,除了控制器自带的,将他们的BackgroundColor 设置为clear color. 现在已经不需要黄色背景了,同样将按钮的背景颜色设置为透明. 运行程序,你会发现现在变得好看很多。 裁剪区域你刚才用渐变填充了整个上下文的区域。现在你可以创建路径来裁剪当前的绘图区域。 打开GraphView.swift,在drawRect(_:)的顶部加入以下代码: let width = rect.width let height = rect.height //set up background clipping area var path = UIBezierPath(roundedRect: rect,byRoundingCorners: UIRectCorner.AllCorners,cornerRadii: CGSize(width: 8.0,height: 8.0)) path.addClip() 这个将创建一个裁剪区域来限制渐变效果,你可以用相同的方式绘制渐变效果在在折线上。 构建运行程序,你会发现有漂亮的圆角效果: 注意: 绘制静态的图像使用Core Graphics是绰绰有余的,但是当你的图像需要频繁的重绘时,你应当使用Core Animation layers,它充分利用了GPU而不是CPU。CPU是通过调用 drawRect(_:)绘制的。 除了使用裁剪路径,你还可以通过使用CALayer的cornerRadius属性来创建圆角,但是你需要优化。为了更好的理解,可以参考Mikael Konutgan and Sam Davies写的Custom Control Tutorial for iOS and Swift: A Reusable 计算坐标点现在可以短暂了停一下画图,我们需要绘制7个点,横坐标为一周的七天,纵坐标为 首先,设置几个简单的值。 在 GraphView.swift,在顶部添加属性: //Weekly sample data var graphPoints:[Int] = [4,2,6,4,5,8,3] 这里简单的数据代表了七天的数据。 接着,在drawRect(_:)的最后添加代码: //calculate the x point let margin:CGFloat = 20.0 var columnXPoint = { (column:Int) -> CGFloat in //Calculate gap between points let spacer = (width - margin*2 - 4) / CGFloat((self.graphPoints.count - 1)) var x:CGFloat = CGFloat(column) * spacer x += margin + 2 return x } 横坐标点包括了7个等距离的点。上述代码在一个闭包表达式中,应该是要独立的封装成一个方法,但是对于这个小的运算量,是可以内嵌在代码之中。 columnXPoint 接受 column作为参数,返回点所在的横坐标的值. 接着,在drawRect(_:)的最后添加代码来计算纵坐标的点: // calculate the y point let topBorder:CGFloat = 60 let bottomBorder:CGFloat = 50 let graphHeight = height - topBorder - bottomBorder let maxValue = maxElement(graphPoints) var columnYPoint = { (graphPoint:Int) -> CGFloat in var y:CGFloat = CGFloat(graphPoint) / CGFloat(maxValue) * graphHeight y = graphHeight + topBorder - y // Flip the graph return y } columnYPoint 同样的也在一个闭包表达式中,将数组中得每一天作为参数.返回纵坐标的位置,在0和最大饮水量之间. 因为起始点为左上角,但是你是从左下角开始绘制, columnYPoint会调整它的返回值来朝向你期望的地方。 继续在drawRect(_:)的底部添加以下代码: // draw the line graph UIColor.whiteColor().setFill() UIColor.whiteColor().setStroke() //set up the points line var graphPath = UIBezierPath() //go to start of line graphPath.moveToPoint(CGPoint(x:columnXPoint(0),y:columnYPoint(graphPoints[0]))) //add points for each item in the graphPoints array //at the correct (x,y) for the point for i in 1..<graphPoints.count { let nextPoint = CGPoint(x:columnXPoint(i),y:columnYPoint(graphPoints[i])) graphPath.addLineToPoint(nextPoint) } graphPath.stroke() 在这个代码快中,你创建了图标的路径, UIBezierPath用来连接graphPoints种的每一个点。 现在,storyboard中的图标看起来是这样的: 现在你验证了线已经正确绘制,从drawRect(_:)移除最后一行代码 graphPath.stroke() 这行代码让你检查线在storyboard是否被正确的绘制,点是否计算正确. 图表渐变现在在这个折线的路径之下来绘制渐变效果. 首先设置裁剪路径在drawRect(_:)的最下方: //Create the clipping path for the graph gradient //1 - save the state of the context (commented out for now) //CGContextSaveGState(context) //2 - make a copy of the path var clippingPath = graphPath.copy() as! UIBezierPath //3 - add lines to the copied path to complete the clip area clippingPath.addLineToPoint(CGPoint( x: columnXPoint(graphPoints.count - 1),y:height)) clippingPath.addLineToPoint(CGPoint( x:columnXPoint(0),y:height)) clippingPath.closePath() //4 - add the clipping path to the context clippingPath.addClip() //5 - check clipping path - temporary code UIColor.greenColor().setFill() let rectPath = UIBezierPath(rect: self.bounds) rectPath.fill() //end temporary code 通过上述代码让块于块之间分割:
现在storyboard中的图标看起来应该是这样的: 下一步,你会将可爱的绿色换成你为背景创建的渐变的效果 移除drawRect(_:)底部填充绿色的临时代码,换成以下代码: let highestYPoint = columnYPoint(maxValue) startPoint = CGPoint(x:margin,y: highestYPoint) endPoint = CGPoint(x:margin,y:self.bounds.height) CGContextDrawLinearGradient(context,0) //CGContextRestoreGState(context) 在这里,你将找到最大的数字作为渐变的起点。 You can’t fill the whole rect the same way you did with the green 你不能填充这个rect就像填充绿色一样,渐变会从上下问得顶部而不是图标的顶部开始进行渐变。 注意CGContextRestoreState的注释,你在绘制之后需要取消注释。 在 drawRect(_:)之后添加代码: //draw the line on top of the clipped gradient graphPath.lineWidth = 2.0 graphPath.stroke() 这里绘制了原始的路径 现在图标看起来是这样的: 在drawRect(_:)的底部添加: //Draw the circles on top of graph stroke for i in 0..<graphPoints.count { var point = CGPoint(x:columnXPoint(i),y:columnYPoint(graphPoints[i])) point.x -= 5.0/2 point.y -= 5.0/2 let circle = UIBezierPath(ovalInRect: CGRect(origin: point,size: CGSize(width: 5.0,height: 5.0))) circle.fill() } 绘制小圆点的代码并不是新的。这里将为每一个数组中的元素的位置绘制一个圆点。 现在为什么不好看呢,对了,小圆点。没事,我们一会加上去。 上下文状态图形上下文可以保存状态。当你设置了许多上下文属性,如填充颜色,色域,变换矩阵,或裁剪区域,你实际上设置为当前图形状态。 你可以使用CGContextSaveGState()保存状态,这会就将当前状态的拷贝压进状态栈。你可以改变上下文的属性,但是当你调用CGContextRestoreGState()原来的状态将会出栈,还原当前的状态。 在 GraphView.swift的drawRect(_:)方法中,注意CGContextSaveGState() 发生在创建裁剪路径之前, CGContextRestoreGState()发生在裁剪路径之后。 现在的图标和圆点看起来更加的清晰: 在 drawRect(_:)后添加代码来添加三条横线: //Draw horizontal graph lines on the top of everything var linePath = UIBezierPath() //top line linePath.moveToPoint(CGPoint(x:margin,y: topBorder)) linePath.addLineToPoint(CGPoint(x: width - margin,y:topBorder)) //center line linePath.moveToPoint(CGPoint(x:margin,y: graphHeight/2 + topBorder)) linePath.addLineToPoint(CGPoint(x:width - margin,y:graphHeight/2 + topBorder)) //bottom line linePath.moveToPoint(CGPoint(x:margin,y:height - bottomBorder)) linePath.addLineToPoint(CGPoint(x:width - margin,y:height - bottomBorder)) let color = UIColor(white: 1.0,alpha: 0.3) color.setStroke() linePath.lineWidth = 1.0 linePath.stroke() 没有新的代码,你需要做的只是移动到一个点然后水平画线。 添加图标的标签现在你可以添加标签来让图标的用户体验更好。 //Label outlets @IBOutlet weak var averageWaterDrunk: UILabel! @IBOutlet weak var maxLabel: UILabel! 这两个标签将会动态的改变内容(饮水量的平均值和最大值)。 在 Main.storyboard 添加以下的 UILabels 作为 Graph View的子视图:
通过Shift全选所有标签,改变字体为Avenir Next Condensed,Medium style. 在Main.storyboard关联averageWaterDrunk 和 maxLabel .按住Control从View Controller拖分别拖到相应的标签。 对于每天得标签来说,在Attributes Inspector 改变 View’s Tag 属性,分别食从1到7 现在已经完成了图标的设置,在Main.storyboard 选择 Graph View并选择Hidden,让程序第一次出现的时候不显示图标。 在 ViewController.swift 添加以下方法来设置标签: func setupGraphDisplay() { //Use 7 days for graph - can use any number,//but labels and sample data are set up for 7 days let noOfDays:Int = 7 //1 - replace last day with today's actual data graphView.graphPoints[graphView.graphPoints.count-1] = counterView.cou nter //2 - indicate that the graph needs to be redrawn graphView.setNeedsDisplay() maxLabel.text = "/(maxElement(graphView.graphPoints))" //3 - calculate average from graphPoints let average = graphView.graphPoints.reduce(0,combine: +) / graphView.graphPoints.count averageWaterDrunk.text = "/(average)" //set up labels //day of week labels are set up in storyboard with tags //today is last day of the array need to go backwards //4 - get today's day number let dateFormatter = NSDateFormatter() let calendar = NSCalendar.currentCalendar() let componentOptions:NSCalendarUnit = .CalendarUnitWeekday let components = calendar.components(componentOptions,fromDate: NSDate()) var weekday = components.weekday let days = ["S","S","M","T","W","F"] //5 - set up the day name labels with correct day for i in reverse(1...days.count) { if let labelView = graphView.viewWithTag(i) as? UILabel { if weekday == 7 { weekday = 0 } labelView.text = days[weekday--] if weekday < 0 { weekday = days.count - 1 } } } } This looks a little burly,but it’s required to set up the calendar and retrieve the current day of the week. Take it in sections: 这看起来有点粗鲁的,但它需要设置日历和知道今天是星期几:
还是在ViewController.swift,在counterViewTap(_:)方法的else分支中调用新方法来显示图表: “` “` 运行程序,点击计数器,万岁,图表将显示所有的数据。 掌握矩阵现在程序看起来有点突兀,我们子啊第一部分创建的计数器还需要提升一下,在饮水量上添加指示。 现在已经有CG方法绘图的一些经验了,你将会使用旋转和平移。 和在上下文绘图一样,你可以通过旋转,缩放,和平移来进行矩阵变幻。 一开始这可能是令你费解的,但是在经过一些练习之后,将会变得更有意义,变换的顺序非常重要,我先说画出你需要做的示意图。 黑色的矩形食旋转前的上下文。然后是绿的,再是红得。有两件事需要注意。
当你绘制计数器的标志的时候,你需要先旋转上下文,然后再绘制它。 在这个示意图中,矩形的标记在左上方,蓝色轮廓就是变换的上下文,然后上下文旋转(红色虚线),然后再次旋转。 最后当红色的标识被绘制完之后,它的角度如图所示: 在上下文旋转和平移得到红色标记之后,它需要回到中心来进行下一次的变换得到绿的标志。 就像你在裁剪的时候保存上下文一样,你需要保存和恢复上下文状态在每次矩阵变换的时候。 打开CounterView.swift 在drawRect(_:)的最后添加代码来增加标记: //Counter View markers let context = UIGraphicsGetCurrentContext() //1 - save original state CGContextSaveGState(context) outlineColor.setFill() let markerWidth:CGFloat = 5.0 let markerSize:CGFloat = 10.0 //2 - the marker rectangle positioned at the top left var markerPath = UIBezierPath(rect: CGRect(x: -markerWidth/2,y: 0,width: markerWidth,height: markerSize)) //3 - move top left of context to the previous center position CGContextTranslateCTM(context,rect.width/2,rect.height/2) for i in 1...NoOfGlasses { //4 - save the centred context CGContextSaveGState(context) //5 - calculate the rotation angle var angle = arcLengthPerGlass * CGFloat(i) + startAngle - π/2 //rotate and translate CGContextRotateCTM(context,angle) CGContextTranslateCTM(context,rect.height/2 - markerSize) //6 - fill the marker rectangle markerPath.fill() //7 - restore the centred context for the next rotate CGContextRestoreGState(context) } //8 - restore the original state in case of more painting CGContextRestoreGState(context) 这里你做了下面几点:
喔,干的不错,现在来运行程序,欣赏这个漂亮的界面。 接下来该做什么?这里,是到现在为止为止所有的代码. 现在你已经学会了绘制路径,渐变效果和通过上下文的矩阵变化 在第三部分的核心绘图教程中,你将创建图案的背景和绘制矢量图。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- Ajax基础知识整理---创建一个XMLHttpRequest对象
- 基于Json序列化和反序列化通用的封装完整代码
- Swift优秀开源项目 Swift Weather介绍
- ruby-on-rails – 大十进制存储为60.00,返回0.0?
- 在Swift中实现CLLocationManagerDelegate方法
- 超时 – 使用AJAX时出现Cloudflare 524错误
- 通过JAXB看XML外部实体注入(XML External Entity)
- ruby-on-rails – Ruby on rails:Heroku生产中不允许使用基
- c – 在功能之外吞下分号的宏
- c# – 如何使用Microsoft.Scripting.Hosting?