前言:在日常开发过程中,难免会碰到一些图表之类的需求,网上有很多优秀的图表库,使用起来也挺方便的,但是使用第三方库,难免会碰到UI难以满足需求,还会有些代码兼容性问题,因此本文记录了一个高度自定义直方图的开发之路,实现UI完全自定义,数据随意刷新,不用再受第三方库的约束(不用再跟UI干架啦)。
效果图:
思路:
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轴)的高度固定,然后根据服务端返回数据的最值,决定(纵坐标)的间隔;
通过图表数据的最值,动态实现纵坐标赋值:
-(void)setMaxData:(NSInteger)maxData{
_maxData = 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 = 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等分)
然后添加显示直方图高度的两个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轴)
@end
NS_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 = (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 = 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 = 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实现,所以数据可以随意刷新,不存在卡顿,或者是线程阻塞等问题。
END:具体实现详见面Demo