iOS普通仪表盘的绘制

一、前言与背景

2017.9.10介绍了一种工作中需要实现的iOS仪表盘的绘制,不够普通,本文介绍另一种适用性更大的仪表盘的实现。备注:本文无其他依赖代码,移植和学习都很方便。
先看下运行后的效果图:

普通仪表盘2.png

二、需求分析

思路:可分成三步:
1.绘制三个彩色圆环带;
2.绘制文本;
3.绘制指针。
其中第3步,指针最为复杂,是本文的重点。

三、实现

首先创建一个继承自UIView的子类XRInstrumentBoard,所有的绘制都是在其内部实现,并开放一些接口供外部对象使用。
类声明和外部接口:

@interface XRInstrumentBoard : UIView

@property (nonatomic, assign) CGFloat value;

- (void)strokePath;

@end

类内部属性:

@interface XRInstrumentBoard ()

@property (nonatomic, assign) CGPoint dotCenter;
@property (nonatomic, assign) CGFloat radius;
@property (nonatomic, assign) CGFloat pointLenth;
@property (nonatomic, strong) NSArray *stateArray;
@property (nonatomic, strong) CALayer *pointLayer;

@end

关键词解释:仪表分值、指针圆点、半径、指针长度、角度数组、指针layer。

1.绘制三个彩色圆环带

首先抽象出一个方法:返回一个给定起始角度和填充颜色的圆环。跟之前一样,使用 CAShapeLaye配合UIBezierPath即可在layer层完成绘制。

- (void)drawPieWithStartAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle color:(UIColor *)color {
    UIBezierPath *piePath = [UIBezierPath bezierPath];
    [piePath addArcWithCenter:self.dotCenter radius:self.radius startAngle:toRad(startAngle) endAngle:toRad(endAngle) clockwise:YES];
    
    CAShapeLayer *pieShapeLayer = [[CAShapeLayer alloc] init];
    pieShapeLayer.lineWidth = 10;
    pieShapeLayer.fillColor = nil;
    pieShapeLayer.strokeColor = color.CGColor;
    pieShapeLayer.path = [piePath CGPath];;
    [self.layer addSublayer:pieShapeLayer];
}

然后手动计算三条圆环的起始角度并给定好填充颜色,依次绘制。XRColorRGB:一个颜色的宏定义,默认的绿色太辣眼睛,请无视,自行替换自己需要的颜色。

    [self drawPieWithStartAngle:-180 endAngle:-120 color:[UIColor redColor]];
    [self drawPieWithStartAngle:-120 endAngle:-60 color:[UIColor orangeColor]];
    [self drawPieWithStartAngle:-60 endAngle:0 color:XRColorRGB(142, 195, 92)];

2.绘制文本

使用for循环创建指定数量的label即可,会用到一些高中的二维坐标函数公式。本文使用较浅,有很多复杂的绘图和动画会大量使用这些公式,不熟悉的建议先补充,知识点:三角函数、圆、弧度、坐标系。

- (void)drawText {
    for (NSInteger i=0; i<3; i++) {
        CGFloat startAngle = -150 + 60*i;
        CGPoint labelCenter = [self pointWithAngle:toRad(startAngle) radius:self.radius + 30];
        
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 40, 40)];
        label.center = labelCenter;
        label.font = XRFont(14);
        label.backgroundColor = XRTextBlueColor;
        label.textColor = [UIColor whiteColor];
        label.textAlignment = NSTextAlignmentCenter;
        label.text = self.stateArray[i];
        label.layer.cornerRadius = 20;
        label.layer.masksToBounds = YES;
        [self addSubview:label];
    }
}

备注:角度和弧度转换公式:

#define toRad(angle) ((angle) * M_PI / 180)

给定角度和半径求点的位置:

- (CGPoint)pointWithAngle:(CGFloat)angle radius:(CGFloat)radius {
    CGFloat x = self.dotCenter.x + cosf(angle) * radius;
    CGFloat y = self.dotCenter.y + sinf(angle) * radius;
    return CGPointMake(x, y);
}

3.绘制指针

下面开始本文的重点部分,这一步分三部分。1)绘制指针锚点;2)绘制指针;3)添加指针动画。

1)绘制指针锚点

一个封闭单色填充的圆,圆点初始化时会给予赋值,后面的代码会提供。

- (void)drawDot {
    UIBezierPath *piePath = [UIBezierPath bezierPath];
    [piePath addArcWithCenter:self.dotCenter radius:10 startAngle:0 endAngle:2*M_PI clockwise:YES];
    
    CAShapeLayer *pieShapeLayer = [[CAShapeLayer alloc] init];
    pieShapeLayer.strokeColor = nil;
    pieShapeLayer.fillColor = XRTextBlueColor.CGColor;
    pieShapeLayer.path = [piePath CGPath];;
    [self.layer addSublayer:pieShapeLayer];
}

2)绘制指针

由于后面需要添加摆动动画,所以将指针layer定义成属性。

- (void)drawPoint {
    self.pointLayer = [CALayer layer];
    self.pointLayer.backgroundColor = XRTextBlueColor.CGColor;
    self.pointLayer.frame = CGRectMake(0, 0, 2, self.pointLenth);
    self.pointLayer.position = CGPointMake(self.dotCenter.x, self.dotCenter.y);
    self.pointLayer.anchorPoint = CGPointMake(0.8, 0.8);
    [self.layer addSublayer:self.pointLayer];
}

3)添加指针动画

这里使用CAAnimationGroup将给定的CABasicAnimation对象添加为一组动画。原理与电影的制作相同。这里的动画组分2个场景:1.从最终位置移动到最右端;2.从最右端移动到最左端。执行次数均为1次,所有动画执行完成后,会回到最初也就是最终的位置上。

- (void)strokePath {
    CGFloat diff = (self.value - 50)/100*M_PI;
    self.pointLayer.transform = CATransform3DMakeRotation(diff, 0, 0, 1);

    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    animation.duration = 1.0f;
    animation.fromValue = @(diff);
    animation.toValue = @(M_PI_2);
    animation.fillMode = kCAFillModeForwards;
    animation.removedOnCompletion = NO;
    animation.repeatCount = 1;
    
    CABasicAnimation *animation2 = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    animation2.duration = 1.0f;
    animation2.fromValue = @(M_PI_2);
    animation2.toValue = @(-M_PI_2);
    animation2.fillMode = kCAFillModeForwards;
    animation2.removedOnCompletion = NO;
    animation2.repeatCount = 1;
    
    CAAnimationGroup *groupAnnimation = [CAAnimationGroup animation];
    groupAnnimation.duration = 1.0f;
    groupAnnimation.autoreverses = YES;
    groupAnnimation.animations = @[animation, animation2];
    groupAnnimation.repeatCount = 1;
    [self.pointLayer addAnimation:groupAnnimation forKey:@"groupAnnimation"];
}

4.内部调用逻辑

重写、初始化相关变量默认值。

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        self.dotCenter = CGPointMake(frame.size.width/2.0, frame.size.height-20);
        self.radius = frame.size.height - 80;
        self.pointLenth = self.radius;
        self.stateArray = @[@"危险", @"普通", @"优秀"];
        self.value = 50;
        [self loadSubViews];
    }
    return self;
}

- (void)loadSubViews {
    [self drawPieWithStartAngle:-180 endAngle:-120 color:[UIColor redColor]];
    [self drawPieWithStartAngle:-120 endAngle:-60 color:[UIColor orangeColor]];
    [self drawPieWithStartAngle:-60 endAngle:0 color:XRColorRGB(142, 195, 92)];
    
    [self drawText];
    [self drawDot];
    [self drawPoint];
}

5.外部使用

创建一个视图控制器,导入头文件,添加视图属性变量,简单的赋值即可。

#import "XRInstumentBoardViewController.h"
#import "XRInstrumentBoard.h"

@interface XRInstumentBoardViewController ()

@property (nonatomic, strong) XRInstrumentBoard *instrumentBoard;

@end

@implementation XRInstumentBoardViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    // Do any additional setup after loading the view.
    self.instrumentBoard = [[XRInstrumentBoard alloc] initWithFrame:CGRectMake(0, 0, 300, 200)];
    self.instrumentBoard.center = self.view.center;
    [self.view addSubview:self.instrumentBoard];
    
    self.instrumentBoard.value = 75;
    [self.instrumentBoard strokePath];   
}

四、运行动态效果图

仪表盘动画.gif

五、GitHub下载地址欢迎点赞。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335