iOS开发之高度自定义一个直方图

前言:在日常开发过程中,难免会碰到一些图表之类的需求,网上有很多优秀的图表库,使用起来也挺方便的,但是使用第三方库,难免会碰到UI难以满足需求,还会有些代码兼容性问题,因此本文记录了一个高度自定义直方图的开发之路,实现UI完全自定义,数据随意刷新,不用再受第三方库的约束(不用再跟UI干架啦)。
效果图:

iOS开发之高度自定义一个直方图
文章图片
histogam.gif
思路:
1.使用一个横向的UICollectionView,用SectionHeader实现直方图的纵坐标(也就是y轴),纵坐标根据需求选择n等分,取所有数据的最大值然后平均分;
2.自定义一个UICollectionViewCell,每一个cell就是一个单一的直方图,通过纵坐标的最值和每一组数据的比例计算直方图的高度;
3.添加一个点击显示数据的view,通过Masonry添加显示label的约束;
4.每一个cell上添加两个button(单一的直方图就添加一个),通过设置button的frame(横向宽度固定,高度代表数值大小),配合动态数据,实现每个直方图的高度;
PS:由于此直方图原先是放在tableView的cell上,
所以这里依旧是放在一个tableView上进行展示的。
实现步骤:
1.在tableView的cell上添加一个UICollectionView,并按UI需求配置好collectionView:

//初始化UICollectionView,并设置好cell的大小,已经collectionView的sectionHeader UICollectionViewFlowLayout * layout = [[UICollectionViewFlowLayout alloc]init]; [layout setScrollDirection:UICollectionViewScrollDirectionHorizontal]; layout.minimumLineSpacing = 0; layout.minimumInteritemSpacing = 0; layout.itemSize = CGSizeMake(55, 250); layout.sectionHeadersPinToVisibleBounds = YES; layout.headerReferenceSize = CGSizeMake(40, 250); layout.footerReferenceSize = CGSizeMake(40, 250); hxCollectionView = [[UICollectionView alloc]initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:layout]; hxCollectionView.backgroundColor = [UIColor whiteColor]; hxCollectionView.showsHorizontalScrollIndicator = NO; hxCollectionView.delegate = self; hxCollectionView.dataSource = self; [hxCollectionView registerNib:[UINib nibWithNibName:@"FHXCollectionReusableView" bundle:nil] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"FHXCollectionReusableView"]; [hxCollectionView registerNib:[UINib nibWithNibName:@"HXCollectionReusableView" bundle:nil] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"HXCollectionReusableView"]; hxCollectionView.bounces = NO; //hxCollectionView.contentOffset = CGPointMake(SCREEN_WIDTH/2.0f, 0); [self.bgView addSubview:hxCollectionView]; [hxCollectionView mas_makeConstraints:^(MASConstraintMaker *make) {make.left.equalTo(self.bgView.mas_left).with.offset(0.0); make.right.equalTo(self.bgView.mas_right).with.offset(0.0); make.top.equalTo(self.bgView.mas_top).with.offset(50.0f); make.height.equalTo(@250.0f); }];

2.实现collectionView的sectionHeader,这里选择在sectionHeader上放8个label,将纵坐标(y轴)的高度固定,然后根据服务端返回数据的最值,决定(纵坐标)的间隔;

iOS开发之高度自定义一个直方图
文章图片
sectionHeader(y轴)初始样式
通过图表数据的最值,动态实现纵坐标赋值:
-(void)setMaxData:(NSInteger)maxData{ _maxData = https://www.it610.com/article/maxData; currentMax = _maxData; //纵坐标间隔 NSInteger interDiscount = currentMax/7.0f; for (int i = 0; i

这样实现,每次y周的刻度值都是不一样的,是根据图表数据的最值计算并动态赋值的。
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {FHXCollectionReusableView *view = [hxCollectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"FHXCollectionReusableView" forIndexPath:indexPath]; view.backgroundColor = [UIColor whiteColor]; //给y轴的最大值赋值 if (resultArray.count > 0 && maxValue - 1 > 0) {view.maxData = https://www.it610.com/article/maxValue; } return view; }else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { UICollectionReusableView* view = [hxCollectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"HXCollectionReusableView" forIndexPath:indexPath]; return view; }else{ return nil; } }

这里同时设置一个SectionFooter,是为了更好的调整UI,可以根据实际情况调整。
3.自定义一个UICollectionViewCell,首先添加8条横线(因为这里的纵坐标的8等分)

iOS开发之高度自定义一个直方图
文章图片
单个直方图的背景 然后添加显示直方图高度的两个button(如果是单一的直方图,一个button就可以)和一个点击显示数据的自定义view,后面会通过每一组数据的大小来计算messageView(显示文案的view)显示的位置,因为直方图有高有低,所以需要动态计算每个messageView的frame
//老用户 oldUserBtn = [UIButton buttonWithType:UIButtonTypeCustom]; [oldUserBtn setBackgroundColor:HXRGB(65, 109, 251)]; oldUserBtn.userInteractionEnabled = NO; [self.contentView addSubview:oldUserBtn]; [oldUserBtn addTarget:self action:@selector(clickOldBtnAction:) forControlEvents:UIControlEventTouchUpInside]; //新用户 newUserBtn = [UIButton buttonWithType:UIButtonTypeCustom]; newUserBtn.backgroundColor = HXRGB(255, 206, 102); newUserBtn.userInteractionEnabled = NO; [self.contentView addSubview:newUserBtn]; [newUserBtn addTarget:self action:@selector(clickNewBtnAction:) forControlEvents:UIControlEventTouchUpInside]; //点击展示数据,默认隐藏 self.messageView =[[NSBundle mainBundle]loadNibNamed:@"FHXSmallMessageView" owner:self options:nil][0]; self.messageView.backgroundColor = [UIColor clearColor]; self.messageView.frame = CGRectMake(0, 0, 100, 60); self.messageView.hidden = YES; [self.contentView addSubview:self.messageView];

4.每个直方图是通过UIButton的纵向高度展示出来,根据每一组数据来计算每个button的高度,后面添加数据会展示,到这里,一个直方图的背景图算是基本完成了。
5,准备一组数据,这里是随机生成的一个数据,实际应用中数据将有服务端下发,数据结果如下:
#import NS_ASSUME_NONNULL_BEGIN@interface FHXTrendModel : NSObject @property(nonatomic,strong)NSString * x; //横坐标数值(x轴) @property(nonatomic,strong)NSString * y0; //纵坐标数值(y轴) @property(nonatomic,strong)NSString * y1; //纵坐标数值(y轴) @property(nonatomic,strong)NSString * y2; //纵坐标数值(y轴) @endNS_ASSUME_NONNULL_END

因为本实例中是要展示同一日期的两组数据,所以用到的是y0和y1.
准备数据:用一个数组存放数据,方便后续赋值
#pragma mark -- 创建数据 -(void)creatData{//模拟20条数据 for (int i = 0; i < 20; i++) {FHXTrendModel * model = [[FHXTrendModel alloc]init]; if (i < 9) { model.x = [NSString stringWithFormat:@"2020010%d",i + 1]; }else{ model.x = [NSString stringWithFormat:@"202001%d",i + 1]; }model.y0 = [NSString stringWithFormat:@"%d",arc4random()%200]; model.y1 = [NSString stringWithFormat:@"%d",arc4random()%100]; [self.orderArray addObject:model]; } [self.tableView reloadData]; }

6.将数据添加到图表,由于这里是用的是一个tableView来展示数据,所以这个直方图(UICollectionView)放在了tableView的cell上
#pragma mark -- UITableViewDelegate,UITableViewDataSource -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{FHXOrderChartCell * cell = [tableView dequeueReusableCellWithIdentifier:@"FHXOrderChartCell"]; if (cell == nil) { cell = [[[NSBundle mainBundle]loadNibNamed:@"FHXOrderChartCell" owner:self options:nil] lastObject]; } cell.delegate = self; cell.columnarDataArray = self.orderArray; cell.unitLabel.text = @"单"; [cell.titleButton setTitle:@"订单数" forState:UIControlStateNormal]; cell.selectionStyle = UITableViewCellSelectionStyleNone; return cell; }

7.提取数据,并做一些换算,需要将数据的x轴数据,y轴数据,y0+y1的最值全部找出来
NSMutableArray * arrayX; //横坐标 NSMutableArray * arrayY0; //纵坐标 NSMutableArray * arrayY1; //纵坐标 NSMutableArray * resultArray; //y0+y1 NSInteger maxValue; //y0+y1最大值 NSIndexPath * selIndex; //记录当前选中cell

-(void)setColumnarDataArray:(NSMutableArray *)columnarDataArray{//暂无数据处理 if (columnarDataArray.count == 0) { self.noDataView.hidden = NO; return; }else{ self.noDataView.hidden = YES; }//分离数据 _columnarDataArray = columnarDataArray; for (FHXTrendModel * model in _columnarDataArray) {[arrayX addObject:model.x]; [arrayY0 addObject:model.y0]; [arrayY1 addObject:model.y1]; CGFloat result = ([model.y0 floatValue] + [model.y1 floatValue]); [resultArray addObject:[NSString stringWithFormat:@"%.2f",result]]; } //取出y0+y1的最大值 CGFloat maxMun = [[resultArray valueForKeyPath:@"@max.floatValue"] floatValue]; if (maxMun == 0) { self.noDataView.hidden = NO; return; }else{ self.noDataView.hidden = YES; } maxValue = https://www.it610.com/article/(NSInteger)(maxMun) + 1; //对7取余数 int remainder = maxValue%7; //确保maxValue能被7整除 maxValue = maxValue + (7 - remainder); [hxCollectionView reloadData]; }

8.将处理好的数据赋给collectionView,然后在collectionView的计算直方图的高度,以及点击显示view的frame
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{UINib *nib = [UINib nibWithNibName:@"FHXOrderCollectionCell" bundle: [NSBundle mainBundle]]; [collectionView registerNib:nib forCellWithReuseIdentifier:@"FHXOrderCollectionCell"]; FHXOrderCollectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"FHXOrderCollectionCell" forIndexPath:indexPath]; if (arrayX.count > 0 && maxValue - 1 > 0) {cell.dateLabel.text = arrayX[indexPath.row]; } if (_columnarDataArray.count > 0 && maxValue - 1 > 0) {cell.maxValue = https://www.it610.com/article/maxValue; cell.trendModel = _columnarDataArray[indexPath.row]; } if (selIndex == indexPath) { cell.messageView.hidden = NO; }else{ cell.messageView.hidden = YES; } cell.backgroundColor = [UIColor whiteColor]; return cell; }

计算直方图的位置和高度(其实是通过UIButton实现的):
-(void)setMaxValue:(NSInteger)maxValue{_maxValue = https://www.it610.com/article/maxValue; }-(void)setTrendModel:(FHXTrendModel *)trendModel{_trendModel = trendModel; //计算老用户占比 CGFloat originY0 = [_trendModel.y0 floatValue]; CGFloat maxMun = (CGFloat)(_maxValue); CGFloat positionY0 = kheight*(1 - originY0/maxMun); oldUserBtn.frame = CGRectMake((55-kwidth)/2.0, positionY0 + 6, kwidth, kheight*originY0/maxMun); //计算新用户占比 CGFloat originY1 = [_trendModel.y1 floatValue]; CGFloat positionY1 = positionY0 - (originY1/maxMun)*kheight; newUserBtn.frame = CGRectMake((55-kwidth)/2.0, positionY1 + 6, kwidth, kheight*originY1/maxMun); //button上半部分圆角 UIBezierPath * maskPath = [UIBezierPath bezierPathWithRoundedRect:newUserBtn.bounds byRoundingCorners:UIRectCornerTopRight | UIRectCornerTopLeft cornerRadii:CGSizeMake(5, 5)]; CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init]; maskLayer.frame = newUserBtn.bounds; maskLayer.path = maskPath.CGPath; newUserBtn.layer.mask = maskLayer; //显示view的位置 self.messageView.firstLabel.text = [Helper notRounding:_trendModel.y1 afterPoint:2]; self.messageView.secondLabel.text = [Helper notRounding:_trendModel.y0 afterPoint:2]; //根据y0+y1的值判断显示view位置 CGFloat currentY = _trendModel.y0.floatValue + _trendModel.y1.floatValue; if (currentY> _maxValue*(5/7.0)) { self.messageView.type = 0; [self.messageView mas_remakeConstraints:^(MASConstraintMaker *make) { make.bottom.equalTo(newUserBtn.mas_top).offset(65); make.centerX.equalTo(newUserBtn); make.width.equalTo(@60.0f); make.height.equalTo(@60.0f); }]; }else{ self.messageView.type = 1; [self.messageView mas_remakeConstraints:^(MASConstraintMaker *make) { make.bottom.equalTo(newUserBtn.mas_top).offset(-5); make.centerX.equalTo(newUserBtn); make.width.equalTo(@60.0f); make.height.equalTo(@60.0f); }]; }self.firstLineView.backgroundColor = HXRGB(226, 235, 242); self.secondLineView.backgroundColor = HXRGB(226, 235, 242); self.thirdLineView.backgroundColor = HXRGB(226, 235, 242); self.fourthLineView.backgroundColor = HXRGB(226, 235, 242); self.fifthLineView.backgroundColor = HXRGB(226, 235, 242); self.sixLineView.backgroundColor = HXRGB(226, 235, 242); self.sevenLineView.backgroundColor = HXRGB(226, 235, 242); self.eightLineView.backgroundColor = HXRGB(226, 235, 242); }

9.到此为止,完成这个直方图的主要工作基本完成了,具体实现请参考Demo,大部分这种图包括折线图,曲线图都是为了看数据变化趋势,和做数据对比,所以,做出来的图表能达到100%的UI还原,还是比较舒心的。
PS: 之前在项目开发过程还涉及到数据类型的筛选,这里省略掉了,实现筛选其实就是数据重载,这里因为用的的UICollectionView实现,所以数据可以随意刷新,不存在卡顿,或者是线程阻塞等问题。
【iOS开发之高度自定义一个直方图】END:具体实现详见面Demo

    推荐阅读