iBrad Apps iBrad Apps - 5 months ago 23
iOS Question

UIBezierPath Draw On Top of Previous Path?

I am trying to customize EChart, a library for bar graphs on Github for my own purposes. In this file: https://github.com/zhuhuihuihui/EChart/blob/master/EChart/EColumn.m

I am attempting to modify the

setGrade:
method so that instead of redrawing the entire bar from the bottom of the graph up, it instead will just draw upon the top of the bar to the next desired Y position in the graph. I included a GIF below to show the problem I am having.

This is the code that is drawing this bar (along with a CABasicAnimation below it):

-(void)setGrade:(float)grade {
_grade = grade;
UIBezierPath *progressline = [UIBezierPath bezierPath];

[progressline moveToPoint:CGPointMake(self.frame.size.width/2.0, self.frame.size.height)];
[progressline addLineToPoint:CGPointMake(self.frame.size.width/2.0, (1 - grade) * self.frame.size.height)];
_chartLine.path = progressline.CGPath;
_chartLine.strokeEnd = 1.0;

CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
pathAnimation.duration = 1.0;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[_chartLine addAnimation:pathAnimation forKey:@"strokeEndAnimation"];
}


This code is called from EColumnChart.m (https://github.com/zhuhuihuihui/EChart/blob/master/EChart/EColumnChart.m)
This is the relevant part, the first if just initializes a bar the first time, and the second I customized to just update the value, calling the
setGrade:
method

if (nil == eColumn) {
eColumn = [[EColumn alloc] initWithFrame:CGRectMake(widthOfTheColumnShouldBe * 0.5 + (i * widthOfTheColumnShouldBe * 1.5), 0, widthOfTheColumnShouldBe, self.frame.size.height)];
eColumn.shouldAnimate = NO;
eColumn.backgroundColor = [UIColor clearColor];
eColumn.grade = eColumnDataModel.value / _fullValueOfTheGraph;
eColumn.eColumnDataModel = eColumnDataModel;
[eColumn setDelegate:self];
[self addSubview:eColumn];
} else {
eColumn.shouldAnimate = YES;
eColumn.grade = eColumnDataModel.value / _fullValueOfTheGraph;
}
[_eColumns setObject:eColumn forKey:[NSNumber numberWithInteger:currentIndex ]];


_fullValueOfTheGraph has its own algorithm to determine how far up the graph the bar should be drawn. The eColumnDataModel.value is just an integer I give it which is the Y value of the bar column.

I am extremely unfamiliar with bezier paths. Is the trick to make the bezier path an ivar and somehow keep track of the top location?

enter image description here

Update:
I have tried to use this code in the beginning part of the setGrade: method however it just creates the little sliver difference between the last update and the current one. I added a prevGrade ivar which is the last grade that was used to update the bar. It is close but it is as if I need to combine the bezier paths in order to get the full bar shape. Any ideas?

UIBezierPath *_progressline = [UIBezierPath bezierPath];
if (_prevGrade != 0.0f) {
if (grade < _prevGrade) { //Bar graph going down
[_progressline moveToPoint:CGPointMake(self.frame.size.width/2.0, (1 + _prevGrade) * self.frame.size.height)];
} else { //Bar graph going up
[_progressline moveToPoint:CGPointMake(self.frame.size.width/2.0, (1 - _prevGrade) * self.frame.size.height)];
}
} else {
[_progressline moveToPoint:CGPointMake(self.frame.size.width/2.0, self.frame.size.height)];
}
[_progressline addLineToPoint:CGPointMake(self.frame.size.width/2.0, (1 - grade) * self.frame.size.height)];

Answer

I think that what you need is animate path for CAShapeLayer instead of strokeEnd. I had been testing the code from the repo that you share and I make some changes in order to animate _chartLine.

this is the code of setGrade: method

-(void)setGrade:(float)grade
{
    _grade = grade;

    if(_chartLine.path == nil)
    {
        UIBezierPath *_progressline = [UIBezierPath bezierPath];
        if (_prevGrade != 0.0f) {
            if (grade < _prevGrade) { //Bar graph going down
                [_progressline moveToPoint:CGPointMake(self.frame.size.width/2.0, (1 + _prevGrade) * self.frame.size.height)];
            } else { //Bar graph going up
                [_progressline moveToPoint:CGPointMake(self.frame.size.width/2.0, (1 - _prevGrade) * self.frame.size.height)];
            }
        } else {
            [_progressline moveToPoint:CGPointMake(self.frame.size.width/2.0, self.frame.size.height)];
        }
        [_progressline addLineToPoint:CGPointMake(self.frame.size.width/2.0, (1 - grade) * self.frame.size.height)];

        _chartLine.path = _progressline.CGPath;
        _chartLine.strokeEnd = 1.0;
    }else
    {

        CAKeyframeAnimation *morph = [CAKeyframeAnimation animationWithKeyPath:@"path"];
        morph.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
        morph.values = [self getPathValuesForAnimation:_chartLine startGrade:_prevGrade neededGrade:_grade];
        morph.duration = 1;
        morph.removedOnCompletion = YES;
        morph.fillMode = kCAFillModeForwards;
        morph.delegate = self;
        [_chartLine addAnimation:morph forKey:@"pathAnimation"];
        _chartLine.path = (__bridge CGPathRef _Nullable)((id)[morph.values lastObject]);
    }

    _prevGrade = grade;
}


//this method return values for path animation

-(NSArray*)getPathValuesForAnimation:(CAShapeLayer*)layer startGrade:(float)currentGrade neededGrade:(float)neededGrade
{

    NSMutableArray * values = [NSMutableArray array];
    if(_prevGrade != 0.0f)
        [values addObject:(id)layer.path];

    UIBezierPath * finalPath = [UIBezierPath bezierPath];

    [finalPath moveToPoint:CGPointMake(self.frame.size.width/2.0, self.frame.size.height)];
    [finalPath addLineToPoint:CGPointMake(self.frame.size.width/2.0,(1 - neededGrade)*self.frame.size.height)];

    [values addObject:(id)[finalPath CGPath]];

    return values;
}

I hope this helps you, for me works great, regards

This is how it works for me, (My gif exporter is not good, so all glitch that you see is problem of my gif exporter)

This is with _chartLine.lineCap = kCALineCapButt;

enter image description here

And this is with _chartLine.lineCap = kCALineCapRound;

enter image description here