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

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

效果图:

iOS开发之高度自定义一个直方图_第1张图片
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开发之高度自定义一个直方图_第2张图片
sectionHeader(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等分)


iOS开发之高度自定义一个直方图_第3张图片
单个直方图的背景

然后添加显示直方图高度的两个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

你可能感兴趣的:(iOS开发之高度自定义一个直方图)