前言:
百度ECharts上有很多漂亮的图表,但是对于native应用并不太友好,今天记录一下用OC自定义一个漏斗图,实现数据动态加载,图表实时更新。
思路:
1.将漏斗图横向切割,假如有四组数据,就横向切割为四等分,每一个部分将由一个UIButton来实现,实际上就是将四个UIButton叠起来(本文记录的是四组数据,所以就以四组数据为例);
2.通过给定数据,计算每一个UIButton对应的四个点,重写UIButton的drawRect方法,利用UIBezierPath+CAShapeLayer绘制每个UIButton的实际大小跟形状;
3.利用CABasicAnimation添加一个绘制动画;
4.添加一个点击响应来显示每一组数据;
整体预览
如下图所示,每一个四边形(最后一个是三角形)都是根据给定数据动态绘制出来的:
实现步骤
1.首先重写UIButton的drawRect方法,实现一个根据给定四个点动态创建一个UIButton:
.h文件添加一个pointArray
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface FHXCustomButton : UIButton
@property (nonatomic, strong) NSMutableArray *pointArray;
@end
NS_ASSUME_NONNULL_END
.m文件实现重写drawRect
#import "FHXCustomButton.h"
@interface FHXCustomButton()
@property (nonatomic, strong) UIBezierPath * path;
@end
@implementation FHXCustomButton
// pointArray
- (void)setPointArray:(NSMutableArray *)pointArray
{
_pointArray = pointArray;
}
// 绘制图形时添加path遮罩
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CAShapeLayer *shapLayer = [CAShapeLayer layer];
shapLayer.fillColor = [UIColor redColor].CGColor;
self.layer.mask = shapLayer;
//获取四个点
CGPoint point0 = CGPointFromString(_pointArray[0]);
CGPoint point1 = CGPointFromString(_pointArray[1]);
CGPoint point2 = CGPointFromString(_pointArray[2]);
CGPoint point3 = CGPointFromString(_pointArray[3]);
//构造fromPath
UIBezierPath * fromPath = [UIBezierPath bezierPath];
[fromPath moveToPoint:point0];
[fromPath addLineToPoint:point0];//起始的path也必须是一个矩形,不然动画效果会有区别
[fromPath addLineToPoint:point1];
[fromPath addLineToPoint:point1];//同上
shapLayer.path = fromPath.CGPath;
//构造toPath(最终图形样式)
UIBezierPath * toPath = [UIBezierPath bezierPath];
[toPath moveToPoint:point0];
[toPath addLineToPoint:point3];
[toPath addLineToPoint:point2];
[toPath addLineToPoint:point1];
[toPath closePath];
// 构造动画
CABasicAnimation * animation = [CABasicAnimation animation];
animation.keyPath = @"path";
animation.duration = 1;
animation.fromValue = (id)fromPath.CGPath;
shapLayer.path = (__bridge CGPathRef _Nullable)((id)toPath.CGPath);
[shapLayer addAnimation:animation forKey:nil];
}
// 点击的覆盖方法,点击时判断点是否在path内,YES则响应,NO则不响应
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL res = [super pointInside:point withEvent:event];
if (res)
{
if ([self.path containsPoint:point])
{
return YES;
}
return NO;
}
return NO;
}
@end
动态绘制UIButton效果如下图:
本文是以四组数据为例,所以只需要创建四个上述button,即可实现一个数据漏斗图。
2.利用上述customButton,实现一个数据漏斗图,新建一个view继承UIView:
.文件
#import <UIKit/UIKit.h>
#import "FHXCustomButton.h"
#import "FHXFunnelModel.h"
#import "FHXChartNoDataView.h"
#define SCREEN_WIDTH ([UIScreen mainScreen].bounds.size.width)
#define kViewWidth(View) CGRectGetWidth(View.frame)
#define kViewHeight(View) CGRectGetHeight(View.frame)
#define kMidHeight 3.0f
NS_ASSUME_NONNULL_BEGIN
@interface FHXFunnelView : UIView
/*标题*/
@property(nonatomic,strong)NSString * title;
/*小标题*/
@property(nonatomic,strong)NSMutableArray * desArray;
/*数据*/
@property(nonatomic,strong)FHXFunnelModel * funnelModel;
/*点击展示文案*/
@property(nonatomic,strong)UIButton * showButton;
/*无数据*/
@property(nonatomic,strong)FHXChartNoDataView * noDataView;
@end
NS_ASSUME_NONNULL_END
.m文件里创建四个FHXCustomButton,从上到下一次排开,并在y轴(数值方向)对齐,通过传入的四组数据计算每一个button的位置,这里是将四个button的高度固定为60,第一个button的上边宽固定,下边宽由前两个数据的比例来计算(类似于一个等腰梯形),依次类推,最后一Button将会被绘制成一个等腰三角形,具体实现代码如下
#import "FHXFunnelView.h"
#import "Masonry.h"
#import "UIColor+Extensions.h"
#import "UIButton+HXEnlargeTouchArea.h"
@implementation FHXFunnelView{
FHXCustomButton * firstBtn;
FHXCustomButton * secondBtn;
FHXCustomButton * thirdBtn;
FHXCustomButton * fourthBtn;
UILabel * titleLabel;
CGFloat registerCount;
CGFloat openCount;
CGFloat lendCount;
CGFloat repeatCount;
NSMutableArray * labelArray;
NSMutableArray * imageViewArray;
}
//添加漏斗数据
-(void)setFunnelModel:(FHXFunnelModel *)funnelModel{
//刷新数据隐藏显示button
self.showButton.hidden = YES;
if (funnelModel == nil) {
//暂无数据
self.noDataView.hidden = NO;
return;
}else{
self.noDataView.hidden = YES;
}
_funnelModel = funnelModel;
//数据整化
registerCount = _funnelModel.firstItemNum.floatValue;
openCount = _funnelModel.secondItemNum.floatValue;
lendCount = _funnelModel.thirdItemNum.floatValue;
repeatCount = _funnelModel.fourthItemNum.floatValue;
//处理数据为0的情况
if (registerCount == 0) {
//暂无数据
self.noDataView.hidden = NO;
return;
}else{
self.noDataView.hidden = YES;
}
if (openCount == 0) {
openCount = 1;
lendCount = 1;
repeatCount = 1;
}
if (lendCount == 0) {
lendCount = 1;
repeatCount = 1;
}
if (repeatCount == 0) {
repeatCount = 1;
}
//注册
firstBtn.frame = CGRectMake(32, 62, SCREEN_WIDTH - 32*2, 60);
CGFloat baseProportion1 = openCount/registerCount;
CGFloat leftProportion1 = (1 - baseProportion1)/2.0f;
CGFloat rightProportion1 = 1 - leftProportion1;
// 添加路径关键点array
NSMutableArray *pointArray = [NSMutableArray array];
[pointArray addObject:NSStringFromCGPoint(CGPointMake(0.f, 0.f))];
[pointArray addObject:NSStringFromCGPoint(CGPointMake(kViewWidth(firstBtn), 0.f))];
[pointArray addObject:NSStringFromCGPoint(CGPointMake(kViewWidth(firstBtn) *rightProportion1, firstBtn.frame.size.height))];
[pointArray addObject:NSStringFromCGPoint(CGPointMake(kViewWidth(firstBtn) *leftProportion1, kViewHeight(firstBtn)))];
firstBtn.pointArray = [pointArray mutableCopy];
firstBtn.backgroundColor = [UIColor colorWithHex:0x4162ff];
[firstBtn setNeedsDisplay];
//开户
secondBtn.frame = CGRectMake(32, 62 + 60, kViewWidth(firstBtn) *baseProportion1, 60);
[secondBtn mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(firstBtn.mas_bottom).with.offset(kMidHeight);
make.centerX.equalTo(firstBtn);
make.width.equalTo(@(kViewWidth(firstBtn) * baseProportion1));
make.height.equalTo(@60.f);
}];
CGFloat baseProportion2 = lendCount/openCount;
CGFloat leftProportion2 = (1 - baseProportion2)/2.0f;
CGFloat rightProportion2 = 1 - leftProportion2;
// 添加路径关键点array
NSMutableArray *pointArray1 = [NSMutableArray array];
[pointArray1 addObject:NSStringFromCGPoint(CGPointMake(0.f, 0.f))];
[pointArray1 addObject:NSStringFromCGPoint(CGPointMake(kViewWidth(secondBtn), 0.f))];
[pointArray1 addObject:NSStringFromCGPoint(CGPointMake(kViewWidth(secondBtn) *rightProportion2, secondBtn.frame.size.height))];
[pointArray1 addObject:NSStringFromCGPoint(CGPointMake(kViewWidth(secondBtn) *leftProportion2, kViewHeight(secondBtn)))];
secondBtn.pointArray = [pointArray1 mutableCopy];
secondBtn.backgroundColor = [UIColor colorWithHex:0xffce66];
[secondBtn setNeedsDisplay];
//绑卡
thirdBtn.frame = CGRectMake(32, 62 + 60, kViewWidth(secondBtn) * baseProportion2, 60);
[thirdBtn mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(secondBtn.mas_bottom).with.offset(kMidHeight);
make.centerX.equalTo(secondBtn);
make.width.equalTo(@(kViewWidth(secondBtn) * baseProportion2));
make.height.equalTo(@60.f);
}];
CGFloat baseProportion3 = repeatCount/lendCount;
CGFloat leftProportion3 = (1 - baseProportion3)/2.0f;
CGFloat rightProportion3 = 1 - leftProportion3;
// 添加路径关键点array
NSMutableArray *pointArray2 = [NSMutableArray array];
[pointArray2 addObject:NSStringFromCGPoint(CGPointMake(0.f, 0.f))];
[pointArray2 addObject:NSStringFromCGPoint(CGPointMake(kViewWidth(thirdBtn), 0.f))];
[pointArray2 addObject:NSStringFromCGPoint(CGPointMake(kViewWidth(thirdBtn) *rightProportion3, thirdBtn.frame.size.height))];
[pointArray2 addObject:NSStringFromCGPoint(CGPointMake(kViewWidth(thirdBtn) *leftProportion3, kViewHeight(thirdBtn)))];
thirdBtn.pointArray = [pointArray2 mutableCopy];
thirdBtn.backgroundColor = [UIColor colorWithHex:0xff6f6f];
[thirdBtn setNeedsDisplay];
//下单
fourthBtn.frame = CGRectMake(32, 62 + 60, kViewWidth(thirdBtn) * baseProportion3, 60);
[fourthBtn mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(thirdBtn.mas_bottom).with.offset(kMidHeight);
make.centerX.equalTo(thirdBtn);
make.width.equalTo(@(kViewWidth(thirdBtn) * baseProportion3));
make.height.equalTo(@60.f);
}];
// 添加路径关键点array
NSMutableArray *pointArray3 = [NSMutableArray array];
[pointArray3 addObject:NSStringFromCGPoint(CGPointMake(0.f, 0.f))];
[pointArray3 addObject:NSStringFromCGPoint(CGPointMake(kViewWidth(fourthBtn), 0.f))];
[pointArray3 addObject:NSStringFromCGPoint(CGPointMake(kViewWidth(fourthBtn) *1/2, fourthBtn.frame.size.height))];
[pointArray3 addObject:NSStringFromCGPoint(CGPointMake(kViewWidth(fourthBtn) *1/2, kViewHeight(fourthBtn)))];
fourthBtn.pointArray = [pointArray3 mutableCopy];
fourthBtn.backgroundColor = [UIColor colorWithHex:0x8ea3ff];
[fourthBtn setNeedsDisplay];
//扩大button点击区域
[firstBtn setTouchExpandEdgeWithTop:0 right:40 bottom:0 left:40];
[secondBtn setTouchExpandEdgeWithTop:0 right:40 bottom:0 left:40];
[thirdBtn setTouchExpandEdgeWithTop:0 right:40 bottom:0 left:40];
[fourthBtn setTouchExpandEdgeWithTop:0 right:40 bottom:0 left:40];
}
//大标题
-(void)setTitle:(NSString *)title{
_title = title;
titleLabel.text = _title;
}
//小标题
-(void)setDesArray:(NSMutableArray *)desArray{
_desArray = desArray;
for (int i = 0; i < _desArray.count; i++) {
//文字
UILabel * label = labelArray[i];
label.text = _desArray[i];
//标识
UIImageView * imageView = imageViewArray[i];
if (i == 0) {
[label mas_remakeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(self).offset(32);
make.bottom.mas_equalTo(self).offset(-15);
make.width.equalTo(@25.0f);
make.height.equalTo(@15.0f);
}];
[imageView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(label);
make.top.mas_equalTo(label.mas_bottom).offset(5);
make.width.equalTo(@25.0f);
make.height.equalTo(@5.0f);
}];
}else{
UILabel * preLabel = labelArray[i - 1];
[label mas_remakeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(preLabel.mas_right).offset(14);
make.centerY.equalTo(preLabel);
make.width.equalTo(@25.0f);
make.height.equalTo(@15.0f);
}];
[imageView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(label);
make.top.mas_equalTo(label.mas_bottom).offset(5);
make.width.equalTo(@25.0f);
make.height.equalTo(@5.0f);
}];
}
}
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
//标题
titleLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 10, frame.size.width, 40)];
titleLabel.textAlignment = NSTextAlignmentCenter;
titleLabel.font = [UIFont boldSystemFontOfSize:18];
[self addSubview:titleLabel];
//注册
firstBtn = [FHXCustomButton buttonWithType:UIButtonTypeCustom];
firstBtn.frame = CGRectMake(32, 62, SCREEN_WIDTH - 32*2, 60);
firstBtn.layer.masksToBounds = YES;
firstBtn.layer.cornerRadius = 5.0f;
firstBtn.tag = 5001;
[firstBtn addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:firstBtn];
//开户
secondBtn = [FHXCustomButton buttonWithType:UIButtonTypeCustom];
[self addSubview:secondBtn];
secondBtn.tag = 5002;
[secondBtn addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
//绑卡
thirdBtn = [FHXCustomButton buttonWithType:UIButtonTypeCustom];
thirdBtn.frame = fourthBtn.frame;
[self addSubview:thirdBtn];
thirdBtn.tag = 5003;
[thirdBtn addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
//下单
fourthBtn = [FHXCustomButton buttonWithType:UIButtonTypeCustom];
[self addSubview:fourthBtn];
fourthBtn.tag = 5004;
[fourthBtn addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
//创建四个标识标签
labelArray = [[NSMutableArray alloc]initWithCapacity:0];
imageViewArray = [[NSMutableArray alloc]initWithCapacity:0];
for (int i = 0; i < 4; i++) {
UILabel * testLabel = [[UILabel alloc]init];
testLabel.textColor = [UIColor colorWithHex:0x333333];
testLabel.font = [UIFont systemFontOfSize:12];
testLabel.textAlignment = NSTextAlignmentCenter;
[labelArray addObject:testLabel];
[self addSubview:testLabel];
UIImageView * imageVIew = [[UIImageView alloc]init];
imageVIew.layer.masksToBounds = YES;
imageVIew.layer.cornerRadius = 2.5f;
[imageViewArray addObject:imageVIew];
[self addSubview:imageVIew];
switch (i) {
case 0:
imageVIew.backgroundColor = [UIColor colorWithHex:0x4262ff];
break;
case 1:
imageVIew.backgroundColor = [UIColor colorWithHex:0xFFCE66];
break;
case 2:
imageVIew.backgroundColor = [UIColor colorWithHex:0xFF6F6F];
break;
case 3:
imageVIew.backgroundColor = [UIColor colorWithHex:0x8EA3FF];
break;
default:
break;
}
}
}
return self;
}
//点击效果
-(void)btnAction:(id)sender{
self.showButton.hidden = NO;
UIButton * btn = (UIButton *)sender;
if (btn.tag == 5001) {
[self.showButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(100 - firstBtn.center.x);
make.centerY.equalTo(firstBtn);
make.width.equalTo(@117.0f);
make.height.equalTo(@43.0f);
}];
[self.showButton setBackgroundImage:[UIImage imageNamed:@"icon_message_register"] forState:UIControlStateNormal];
[self.showButton setTitle:[NSString stringWithFormat:@"人数: %.0f",registerCount] forState:UIControlStateNormal];
}
if (btn.tag == 5002) {
[self.showButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(100 - secondBtn.center.x);
make.centerY.equalTo(secondBtn);
make.width.equalTo(@115.0f);
make.height.equalTo(@57.0f);
}];
[self.showButton setBackgroundImage:[UIImage imageNamed:@"icon_message_other"] forState:UIControlStateNormal];
//转化率
NSString * transRateStr = [[NSString stringWithFormat:@"%.2f",openCount/registerCount*100] stringByAppendingString:@"%"];
[self.showButton setTitle:[NSString stringWithFormat:@"人数: %.0f\n转化率: %@",openCount,transRateStr] forState:UIControlStateNormal];
}
if (btn.tag == 5003) {
[self.showButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(100 - thirdBtn.center.x);
make.centerY.equalTo(thirdBtn);
make.width.equalTo(@115.0f);
make.height.equalTo(@57.0f);
}];
[self.showButton setBackgroundImage:[UIImage imageNamed:@"icon_message_other"] forState:UIControlStateNormal];
//转化率
NSString * transRateStr = [[NSString stringWithFormat:@"%.2f",lendCount/openCount*100] stringByAppendingString:@"%"];
[self.showButton setTitle:[NSString stringWithFormat:@"人数: %.0f\n转化率: %@",lendCount,transRateStr] forState:UIControlStateNormal];
}
if (btn.tag == 5004) {
[self.showButton mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(100 - fourthBtn.center.x);
make.centerY.equalTo(fourthBtn);
make.width.equalTo(@115.0f);
make.height.equalTo(@57.0f);
}];
[self.showButton setBackgroundImage:[UIImage imageNamed:@"icon_message_other"] forState:UIControlStateNormal];
//转化率
NSString * transRateStr = [[NSString stringWithFormat:@"%.2f",repeatCount/lendCount*100] stringByAppendingString:@"%"];
[self.showButton setTitle:[NSString stringWithFormat:@"人数: %.0f\n转化率: %@",repeatCount,transRateStr] forState:UIControlStateNormal];
}
}
- (UIButton *)showButton{
if (!_showButton) {
_showButton = [UIButton buttonWithType:UIButtonTypeCustom];
_showButton.frame = CGRectMake(0, 0, 115, 57);
[_showButton setTitleColor:[UIColor colorWithHex:666666] forState:UIControlStateNormal];
_showButton.titleLabel.font = [UIFont systemFontOfSize:12];
_showButton.titleEdgeInsets = UIEdgeInsetsMake(0, -10, 0, 0);
_showButton.titleLabel.lineBreakMode = 0;
[self addSubview:_showButton];
}
return _showButton;
}
-(FHXChartNoDataView *)noDataView{
if (!_noDataView) {
_noDataView = [[NSBundle mainBundle]loadNibNamed:@"FHXChartNoDataView" owner:self options:nil][0];
_noDataView.frame = self.bounds;
[self addSubview:_noDataView];
_noDataView.hidden = YES;
[_noDataView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self);
make.centerY.equalTo(self);
make.width.equalTo(@128.0f);
make.height.equalTo(@128.0f);
}];
}
return _noDataView;
}
@end
因为是纯代码创建的图,所以借用了优秀的Masonry库来添加代码约束。因为这种图在视觉上就是直观的看一个数据转化率,所以可以在计算每个点的位置的时候,将数据整数化,使用整数来计算更方便,因为实际数据是要通过点击事件显示出来的,所以取整数计算点并绘图,不影响结果。
使用
1.创建funnlView(漏斗图)
_funnelView = [[FHXFunnelView alloc]initWithFrame:CGRectMake(0, 64, SCREEN_WIDTH, 360)];
[self.view addSubview:self.funnelView];
[self initTestData];
2.添加数据,使用随机数模拟真实数据
//动态添加数据
FHXFunnelModel * funnelModel = [[FHXFunnelModel alloc]init];
funnelModel.firstItemNum = [NSString stringWithFormat:@"%d",(arc4random() % 100) + 300];
funnelModel.secondItemNum = [NSString stringWithFormat:@"%d",(arc4random() % 100) + 200];
funnelModel.thirdItemNum = [NSString stringWithFormat:@"%d",(arc4random() % 100) + 100];
funnelModel.fourthItemNum = [NSString stringWithFormat:@"%d",(arc4random() % 100) + 0];
self.funnelView.funnelModel = funnelModel;
//大标题
self.funnelView.title = @"用户数据漏斗图";
//小标题
NSArray * array = @[@"注册",@"开户",@"绑卡",@"下单"];
NSMutableArray * titleArray = [NSMutableArray arrayWithArray:array];
self.funnelView.desArray = titleArray;
PS:实际应用中,很多数据都是呈现漏斗图的趋势,类似“注册用户-->实名用户-->下单用户-->二次下单用户”这种数据,可根据实际需求,定制各类数据漏斗图。下一步,将会对绘制动画做一下优化。