Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Draw dynamic shapes iOS at very high rate

I'm trying to create a sort of "radar" that would look like this :

first radar

The idea is that the blue part will act like a compass to indicate proximity of stuff to the user. The shape would be narrow, long and blue when the user is far from the objective, and become shorter, thicker and red when he gets closer, as shown below:

second radar

To achieve this, I drew a few circles (actually I tried IBDesignable and IBInspectable but I keep getting "Build failed" but that is another subject)

My question is : Is this possible to draw the "cone" part that is supposed to change its dimensions at a high rate without risking lag ? Can I use UIBezierPath / AddArc or Addline methods to achieve this or I am heading to a dead end ?

like image 794
H4Hugo Avatar asked Dec 19 '22 16:12

H4Hugo


1 Answers

There are two approaches:

  1. Define complex bezier shapes and animate use CABasicAnimation:

    - (UIBezierPath *)pathWithCenter:(CGPoint)center angle:(CGFloat)angle radius:(CGFloat)radius {
        UIBezierPath *path = [UIBezierPath bezierPath];
    
        CGPoint point1 = CGPointMake(center.x - radius * sinf(angle), center.y - radius * cosf(angle));
    
        [path moveToPoint:center];
        [path addLineToPoint:point1];
    
        NSInteger curves = 8;
        CGFloat currentAngle = M_PI_2 * 3.0 - angle;
    
        for (NSInteger i = 0; i < curves; i++) {
            CGFloat nextAngle = currentAngle + angle * 2.0 / curves;
            CGPoint point2 = CGPointMake(center.x + radius * cosf(nextAngle), center.y + radius * sinf(nextAngle));
            CGFloat controlPointRadius = radius / cosf(angle / curves);
            CGPoint controlPoint = CGPointMake(center.x + controlPointRadius * cosf(currentAngle + angle / curves), center.y + controlPointRadius * sinf(currentAngle + angle / curves));
            [path addQuadCurveToPoint:point2 controlPoint:controlPoint];
            currentAngle = nextAngle;
            point1 = point2;
        }
        [path closePath];
        return path;
    }
    

    I just split the arc into a series of quad curves. I find cubic curves can get closer, but with just a few quad curves, we get a very good approximation.

    And then animate it with CABasicAnimation:

    - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
    
        CAShapeLayer *layer = [CAShapeLayer layer];
        layer.fillColor = self.startColor.CGColor;
        layer.path = self.startPath.CGPath;
        [self.view.layer addSublayer:layer];
        self.radarLayer = layer;
    }
    
    - (IBAction)didTapButton:(id)sender {
        self.radarLayer.path = self.finalPath.CGPath;
        CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
        pathAnimation.fromValue = (id)self.startPath.CGPath;
        pathAnimation.toValue = (id)self.finalPath.CGPath;
        pathAnimation.removedOnCompletion = false;
    
        self.radarLayer.fillColor = self.finalColor.CGColor;
        CABasicAnimation *fillAnimation = [CABasicAnimation animationWithKeyPath:@"fillColor"];
        fillAnimation.fromValue = (id)self.startColor.CGColor;
        fillAnimation.toValue = (id)self.finalColor.CGColor;
        fillAnimation.removedOnCompletion = false;
    
        CAAnimationGroup *group = [CAAnimationGroup animation];
        group.animations = @[pathAnimation, fillAnimation];
        group.duration = 2.0;
        [self.radarLayer addAnimation:group forKey:@"bothOfMyAnimations"];
    }
    

    That yields: enter image description here

  2. The other approach is to use simple bezier, but animate with display link:

    If you wanted to use a CAShapeLayer and a CADisplayLink (which is like a timer that's optimized for screen updates), you could do something like:

    @interface ViewController ()
    
    @property (nonatomic, strong) CADisplayLink *displayLink;
    @property (nonatomic) CFAbsoluteTime startTime;
    @property (nonatomic) CFAbsoluteTime duration;
    @property (nonatomic, weak) CAShapeLayer *radarLayer;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
    
        CAShapeLayer *layer = [CAShapeLayer layer];
        [self.view.layer addSublayer:layer];
        self.radarLayer = layer;
        [self updateRadarLayer:layer percent:0.0];
    }
    
    - (IBAction)didTapButton:(id)sender {
        [self startDisplayLink];
    }
    
    - (void)updateRadarLayer:(CAShapeLayer *)radarLayer percent:(CGFloat)percent {
        CGFloat angle = M_PI_4 * (1.0 - percent / 2.0);
        CGFloat distance = 200 * (0.5 + percent / 2.0);
    
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path moveToPoint:self.view.center];
        [path addArcWithCenter:self.view.center radius:distance startAngle:M_PI_2 * 3.0 - angle endAngle:M_PI_2 * 3.0 + angle clockwise:true];
        [path closePath];
    
        radarLayer.path = path.CGPath;
        radarLayer.fillColor = [self colorForPercent:percent].CGColor;
    }
    
    - (UIColor *)colorForPercent:(CGFloat)percent {
        return [UIColor colorWithRed:percent green:0 blue:1.0 - percent alpha:1];
    }
    
    - (void)startDisplayLink {
        self.startTime = CFAbsoluteTimeGetCurrent();
        self.duration = 2.0;
        self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
        [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    }
    
    - (void)handleDisplayLink:(CADisplayLink *)link {
        CGFloat percent = (CFAbsoluteTimeGetCurrent() - self.startTime) / self.duration;
        if (percent < 1.0) {
            [self updateRadarLayer:self.radarLayer percent:percent];
        } else {
            [self stopDisplayLink];
            [self updateRadarLayer:self.radarLayer percent:1.0];
        }
    }
    
    - (void)stopDisplayLink {
        [self.displayLink invalidate];
        self.displayLink = nil;
    }
    

    That yields: enter image description here

like image 74
Rob Avatar answered Jan 02 '23 18:01

Rob