这一话来讲解一下视图的绘制,首先介绍一下相关的结构体
视图中的所有coordinate(坐标)的类型都是CGFloat,CGFloat在Swift中是结构体,在处理视图绘制和手势识别的时候我们使用的都是CGFloat,不要用我们常规的Double和Float。你可以通过使用CGFloat的初始化方法把Double或者Float类型的数据转换成CGFloat。
另外一个结构体是CGPoint,它由x和y两个CGFloat类型组成,用来定义坐标中的一个点。
还有一个结构体是CGSize,用来表示长和宽。
CGSize和CGPoint通常不会用到什么方法。
把一个CGPoint和一个CGSize组合起来就是一个CGRect。origin是CGPoint类型的表示原点,在区域的左上角。size是CGSize的表示宽和高。CGRect有很多属性和方法。比如minX返回左边界,midY返回垂直方向的中点。intersects传递另一个矩阵进去判断这两个矩阵有没有重叠。
intersect创建一个更小的矩阵,由两个矩阵重叠的部分组成。
contains传入一个point,判断这个点是否在矩阵中。
CGRect有很多有意思的方法,大家可以自己去尝试一下。
现在来讲解一下视图的坐标系统,首先坐标系的原点是在左上方的。比如这里有个点(500,35),那么它距离左边500,距离上边35.
我们在这用的单位是points(点)而不是pixels(像素)高分辨率的屏幕中一个点可能包含多个像素,这样可以用来绘制平滑的曲线和文本,并且具有抗锯齿能力。通常你不用在意点与像素的关联,因为系统会自己考虑一个点使用多少像素才能使绘制看起来优美。但有时候你需要注意,特别是你需要处理非常精细的边界的时候,你可以访问系统中的一个属性contentScaleFactor,它会告诉你每个点返回多少像素。在视网膜屏幕(Retina)中一个点占用两个像素,非视网膜屏幕只占用一个。
另外我们绘制图形的边界在哪里,你已经有一个宽和高了,这就涉及到属性bounds,它是CGRect类型的,它定义了坐标系中的绘制区域,需要注意的是绘制是相对于自身的,因为视图可以被拉伸、旋转或者变换,你使用bounds的时候你是在自身坐标系绘制,你知道这个矩形的确切位置。bounds的原点通常是(0,0),但是也有例外,比如scroll view中会更改它的原点到目前滚动的位置。大部分情况下你的原点就是(0,0),你需要关心的是它的长和宽,也就是我们绘制的区域面积。
这里有两个容易和bounds搞混的属性center和frame。center是你自身视图坐标的中点,但是它是相对于父视图的而不是视图本身的坐标。如果你想在视图中心绘制点东西使用了center,那么这样做是错误的,因为center基于父视图和绘制没有关系,它仅仅用来在父视图中定位。
frame同样是父视图中用来包含子视图的一个矩形,它相对于父视图,与绘制无关。绘制的时候我们使用bounds。
为了方便理解你可以认为frame的size和bounds的size总是相同的,代表一个矩形要完全包含另一个矩形,但是也有例外,因为视图是可以旋转的。
你可以看到如果发生旋转,那么frame的尺寸要比bounds大得多,所以一定要注意frame和center用来定位视图,而bounds才是用来绘制视图的。那么如何创建一个视图呢?
大部分视图是直接通过storyboard拖拽创建的,但是官方视图可能不能满足你的要求,这时候你就需要先拖拽一个通用的视图UIView。一旦你拖出这个视图,你就需要在你的Identity Inspector里把对应的类修改为你自己实现的类。只有在很少的情况下才通过纯代码来创建视图。你可以使用一个无参数的构造器,这样返回的是一个原点和尺寸都是0的空视图,你可以在稍后再给它赋值。
UILabel是UIView的一个子类,所以我们可以使用带参数frame的构造器,创建好之后把它加到父视图中,这里的父视图是我们之前讲的viewController中最顶层的view。可以通过纯代码这样做但是在IOS开发中我们几乎不这么做,我们使用storyboard。
那么我们什么时候需要自己创建一个视图呢,如果你只是需要使用一些常规的组件那么直接使用系统自带的就可以,如果你想要绘制一些特殊的图形,或者相对不同的触摸手势做出不同的反应的话,那么你需要自己定义一个视图。
要绘制一个视图,你需要重写drawRect方法,它只接受一个参数,在系统中它叫rect,但是我们已经把它重新命名为regionThatNeedsToBeDrawn,这个参数纯粹是起一个优化的效果,绘图时会重点关注参数中的区域。
需要注意:永远都不要调用drawRect方法!drawRect是被系统调用的,当你的视图需要重新绘制时,它会自动被系统调用,你可能会问系统是如何知道你想要重绘。在你的视图中有一个方法叫做setNeddDisplay方法,如果你需要重绘视图,那么调用它,系统会在适合的时机调用drawRect,但是首先它会设置一些东西,你周围的其他视图,它们必须先被组织起来。
那么我们如何实现drawRect方法呢你可以使用类似于C语言基于函数的API叫做Core Graphics,它是所有图像绘制的基础。或者使用一种面向对象的方法UIBezierPath,这是我们接下来要用到的方法,遇到底层一些的东西会降到Core Graphics的概念,它是构建UIBezierPath的基础和核心。
在Core Graphics中第一件很重要的事情是你需要拥有一个用来绘制的上下文(context),在drawRect中,当你准备绘制时,系统会调用drawRect并且为你建立一个绘制的context,它是自动建立的,并且被称之为当前的context,通过UIGraphicsGetCurrentContext方法,它将会返回这个cookie作为你的上下文,所有的类C API都是用上下文作为参数。
然后你创建路径,通过线条和弧线诸如此类的来创建路径。
接着你设置绘制的属性,你希望绘制的线条颜色、宽度等等。
最后你通过描边并且填充这些路径。
这就是你绘制的基础,类似于绘制文字之类的事情只需要知道字体并且如何获得一个完美的路径来挥之漂亮的字母,你的描边也将绘制字母的边界最后填充字母内部。
现在来介绍UIBezierPath这个面向对象的方法在最上面,通常在当前的context上绘制,它是将所有的Core Graphics放到一个类中。
下面我们来看看如何定义一个路径并且绘制一个三角形。
首先你要初始化一个路径UIBezierPath,然后你可以在周围移动,移动到点(80,50),然后添加一条线到(140,150)
然后再添加一条线到(10,150)
最后闭合这个路径,当然虽然示例中你看到了一个三角形,但实际这时候你在屏幕上是什么都看不到的。你需要继续设置它的属性:
比如我把填充色设置为绿色,把描边的颜色设置为红色,注意设置颜色用的是UIColor的方法而不是使用BezierPath的方法。但是线宽就需要和path来商议了,所以发消息给path告诉它线宽为3.0。全部完成后就可以开始填充了,使用方法fill()。
现在屏幕上出现了一个绿色三角形,然后再给它描边,就会出现红色边框:
我们会在接下来的几话中绘制更多更复杂的图形。