写在前面
Quartz2D是苹果提供的一套高效绘图的引擎,不是框架。。里面可以自定义许多的控件,一般的控件都是继承自UI~,而使用Quartz2D可以做出我们自己想要的类型的控件出来。这里就试着用Quartz2D来画一个时钟走表的控件。
关于Quartz2D的绘制的步骤
绘制Quartz2D一般分为以下几个步骤:
- 1.上下文的获取 (获取或者创建都是以UIGraphics开头)
上下文已经是创建好了的,只要获取就可以了。而且只有当系统调用-drawRect:
这个方法的时候,系统才会去创建上下文,如果就单纯的手动调用-drawRect:
这个方法,是不会去创建上下文,这一点很重要。 - 2.绘制路径
这个路径也就是相当于我们用笔在一张白纸上画的内容,包括了内容的颜色,是直线还是曲线等状态。 - 3.把绘制的内容保存到上下文中
我们用笔在纸张上画图,直接就显示出来我们所画的内容了,而计算机没那么智能,所以我们要手动的保存到上下文中。 - 4.把上下文内容显示渲染到view的layer上去(stroke/fill)
这一步就是把画的内容显示到需要展示的view中,这里展示的是view的layer层上。其实还可以展示到PDF中,printer(打印机),bitmap(一个绘图工具)等。
0.界面相关
既然是自定义的view,那么久要创建一个view继承自UIView,然后再view中去重写- drawRect: 这个方法。然后回到控制器去创建view,并且展示出来。
1.绘制表盘
1.1绘制圆,
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGPoint center = CGPointMake(rect.size.width * 0.5, rect.size.height * 0.5);
CGFloat radius = rect.size.width * 0.5 - 10;
// 圆
UIBezierPath *circle = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(center.x - radius, center.y - radius, radius * 2, radius * 2)];
[circle setLineWidth:3.0f];
[[UIColor lightGrayColor] set];
CGContextAddPath(ctx, circle.CGPath);
CGContextStrokePath(ctx);
这就把圆绘制好了。
1.2绘制刻度
表盘上的刻度是每6°绘制一次的。我们这里需要把刻度那根直线的一个顶点定位到圆环上,然后另一个顶点根据是不是5的倍数来定。
首先就要确定刻度线在圆上的顶点:
都知道圆的标准公式为:
(x-a)^2+ (y-b)^2 = r^2;
我们可以得知x与y的值:
// 这是按照笛卡尔的坐标系的计算的结果,并不是以iOS的坐标系的结果。
// angle代表的是角度
x = radius * cos(angle) ;
y = radius * sin(angle);
转换成iOS的坐标系:
CGFloat x = center.x + radius * cos(angle) ;
CGFloat y = center.y + radius * sin(angle);
这个是在圆上的点,那么同理我们可以计算另一个点,只是修改下半径就可以得到。
// 一般的短刻度线
CGFloat x0 = center.x + (radius - 10) * cos(angle);
CGFloat y0 = center.y + (radius - 10) * sin(angle);
// 整点时的长刻度线
CGFloat x1 = center.x + (radius - 20) * cos(angle);
CGFloat y1 = center.y + (radius - 20) * sin(angle);
这里的angle传入的是以-90°开始的角度,也就是12点的位置开始的角度。
CGFloat angle = -M_PI_2 + i / 60.0f * M_PI * 2;
依次循环添加到表盘上。
最后展示的效果如下:
2.绘制指针
秒针的绘制基本思路还是画一条直线,然后根据时间的来进行修改。
2.1定时器
这里就需要用到定时器了,需要重复执行绘制图形的操作,所以首先在控制器中添加定时器。
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
[self catchTime];
}];
[self.timer fire];
一定要注意的是在页面销毁的时候,也需要把定时器进行销毁。
2.2秒针、分针、时针
指针的起点在圆心出,终点可以在圆上或不在圆上,我在这里设置的不在圆上,因为有刻度线,所以留有一定的距离。
// 起点
CGPoint center = CGPointMake(rect.size.width * 0.5, rect.size.height * 0.5);
[path moveToPoint:center];
需要注意的是这里的point需要指明的是这个view所在的中心点,如果直接写self.view.center,这样会找到这个自定义view的父控件上去,到时候绘制出来的效果可能有偏差。
// 终点
// 每一次的角度。-M_PI_2 起始角度,也就是12点的位置。
CGFloat angle = -M_PI_2 + self.seconds / 60.0 * M_PI * 2; // 外部传入的时间。。秒
// 圆上的点
CGFloat radius = rect.size.width * 0.5 - 15; // 秒针的长度
CGFloat x = center.x + radius * cos(angle) ;
CGFloat y = center.y + radius * sin(angle);
[path addLineToPoint:CGPointMake(x, y)];
分针和时针的做法同上。
这样就把三条指针所在的线绘制出来了。
3.走时
3.1定时器的选择
关于定时器,可以采用NSTimer方式或者是CADisplayLink,然后加入到NSRunloop中去。
// 方式一:
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
// [self catchTime];
[self catchTimeDict];
}];
[self.timer fire];
// 方式二:
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(catchTime)];
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
方式二的刷新频率(1/60)过快,所以演示起来有点辣眼睛。
3.2刷新并走时
最后让这个指针动起来,所以就应该在控制器中去调用这个自定义的view。指示当前的时间,需要外部传入,也就是说自定义的view需要定义三个属性,让外部传入时分秒。
/* sencond*/
@property (nonatomic, assign) int seconds;
/* minute*/
@property (nonatomic, assign) int minute;
/* hour*/
@property (nonatomic, assign) int hour;
计算时分秒,进行调用:
// 方法一:
NSString *timeStamp = [NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]];
self.wjClockView.seconds = [WJTime wjTimeSecondsWithTimeStamp:timeStamp];
self.wjClockView.minute = [WJTime wjTimeMinuteWithTimeStamp:timeStamp];
self.wjClockView.hour = [WJTime wjTimeHourWithTimeStamp:timeStamp] - 12; // 这里是标注的是12小时制,所以要减去12.
self.wjTimeLable.text = [NSString stringWithFormat:@"%02d:%02d:%02d", self.wjClockView.hour + 12, self.wjClockView.minute, self.wjClockView.seconds]; // 这里是在标签中显示24小时制,所以加12
// 方法二
NSString *timeStamp = [NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]];
NSDictionary *timeDict = [WJTime wjTimeGetTimeDictWithTimeStamp:timeStamp];
NSNumber *seconds = timeDict[@"seconds"];
NSNumber *minute = timeDict[@"minute"];
NSNumber *hour = timeDict[@"hour"];
self.wjClockView.seconds = seconds.intValue;
self.wjClockView.minute = minute.intValue;
self.wjClockView.hour = hour.intValue - 12;
self.wjTimeLable.text = [NSString stringWithFormat:@"%02d:%02d:%02d", self.wjClockView.hour + 12, self.wjClockView.minute, self.wjClockView.seconds];
- wjTimeSecondsWithTimeStamp :
这些方法是我自己写的一些类,专门返回现在时刻的时分秒的,只要传入当前的时间戳就可以了。
这样就可以显示当前时间的效果,还有一点问题就是,这个虽然调用了这个方法,但是不走时。
解决这个问题,我们可以在自定义view中进行重写seconds属性的setter方法,然后看看手动调用-drawRect:
方法,看这个方法能否实现。
// 重写setter方法
- (void)setSeconds:(int)seconds {
_seconds = seconds;
[self drawRect:self.frame]; // 不可行方式
}
但是很遗憾,并不能实现。
因为在开篇的时候就讲了,调用-drawRect :
方法时,如果是系统调用的话,那么会自动创建一个上下文,只要获取就可以了,但是如果是手动调用的话,就不会创建这个上下文,也就是说没法进行重绘。所以这种手动调用的方法失效。
其实调用系统提供的另一个方法可以实现:- setNeedsDisplay
,这个方法,系统会自动的去调用-drawRect :
这个方法,也就是说系统也会自动的去创建上下文,所以可以进行重绘制图形,而-setNeedsDisplay
在调用-drawRect :
方法的时候,也不是马上去调用修改的,而是在建立一个flag标识,当屏幕刷新的时候进行调用-drawRect :
这个方法,而屏幕刷新是在什么时候去刷新,屏幕刷新会在1s中刷新60次。
// 重写setter方法
- (void)setSeconds:(int)seconds {
_seconds = seconds;
[self setNeedsDisplay]; // 可行方式
}
以上就可以发现走时功能实现了。
但是以上的走时,总感觉是一顿一顿的。分针和时针的走时不是那么顺滑,所以这里也需要对minute和hour属性的setter重写。
而且对于分针和时针转动的角度,也是需要修改,为的是在走时的时候,分针和时针比较顺滑的走动,而不是走到秒针走了一圈,分针才开始跳一格。
// 分针的角度
CGFloat angle = -M_PI_2 + (self.minute / 60.0) * M_PI * 2 + self.seconds / 3600.f * M_PI * 2;
// 时针的角度
CGFloat angle = -M_PI_2 + (self.hour / 12.0) * M_PI * 2 + self.minute / 720.f * M_PI * 2 + self.seconds / (12 * 3600.f) * M_PI * 2;
到这一步就是基本上完成了钟表走时的功能了。
添加logo .
demo传送门