美文网首页加载动画
iOS绘制仪表盘,游标沿圆形轨迹移动动画

iOS绘制仪表盘,游标沿圆形轨迹移动动画

作者: 在ios写bug的杰克 | 来源:发表于2020-12-11 12:34 被阅读0次
    image

    最近碰到一个需求,需要画一个仪表盘的页面。图上所示。

    计算角度

    圆弧部分还好,用CAShapeLayer+UIBezierPath曲线,只要确定好圆心部分和左右两边的角度就行。这里正好说明一下

    - (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise API_AVAILABLE(ios(4.0));
    

    这个接口startAngle、endAngle和clockwise的关系,之前一直记不太清。

    我们需要看一下下面这张图

    在这里插入图片描述
    clockwise为true的时候,就会从startAngle按顺时针方向画弧。为false的时候,按逆时针方向画弧。
    这里有一个要注意的地方是
    1. 顺时针的360°表示的值,必须是0~2*M_PI,
    2. 逆时针的360°表示的值必须是0~-2*M_PI。
    3. 同一个角度表示的值,在clockwise取true或者false的情况下,是需要转换的。

    了解了上面的注意点之后,三条圆弧还是能很方便的画出来的。
    接下来就是动态的显示进度。即白色圆弧每次数值变化,弧线动态增长或减少。

    StrokeEnd

    一开始想的是每次都重新绘制贝塞尔曲线,但是发现从动画效果上来看,每次重新绘制,都会从起点位置绘制到终点位置。不是想要的效果。
    然后又想着设置layer.masksToBounds=true然后,画一条半圆,通过旋转来达到左右移动的效果,这样超出layer的部分就不会显示了,但是又发现背景的圆弧不是一块半圆,会存在覆盖不全的情况。
    后来查看CAShaperLayer的说明,发现这样一个属性

    /* These values define the subregion of the path used to draw the
     * stroked outline. The values must be in the range [0,1] with zero
     * representing the start of the path and one the end. Values in
     * between zero and one are interpolated linearly along the path
     * length. strokeStart defaults to zero and strokeEnd to one. Both are
     * animatable. */
    
    @property CGFloat strokeStart;
    @property CGFloat strokeEnd;
    

    这两个值默认是0和1,对应的就是起始点和终点的比例,当storkeEnd=0.5的时候,原来圆弧终点的值就会减少为原来的一半
    那么我就可以这样了,我先画一套完整的覆盖背景圆弧的实线圆弧,设置strokeEnd=0,这样圆弧长度就为0了。当值变化的时候,在调整strokeEnd的值。就可以动态的变化圆弧长度了。
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    animation.duration = 1;
    animation.fromValue = @(oldValue/100.0);
    animation.toValue = @(value/100.0);
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeForwards;
    [self.valueLayer addAnimation:animation forKey:@"valueProgress"];

    绘制移动路径

    搞定了圆弧的变化之后,还有一部分是小圆点的移动,它是按照圆弧的轨迹移动的,那么在做动画效果的时候,就要让小圆点按照圆弧的轨迹移动位置。

    UIBezierPath *bezierPath = [UIBezierPath bezierPath];
    CGFloat outerWidth = 226;
    if (oldValue > value) { // <-
        CGFloat valueAngle = value/100.0 * (2*M_PI - (self.startAngle - self.endAngle)) + self.startAngle;
        CGFloat oldAngle = oldValue/100.0 * (2*M_PI - (self.startAngle - self.endAngle)) + self.startAngle;
        
        valueAngle = valueAngle - 2*M_PI;
        oldAngle = oldAngle - 2*M_PI;
        
        [bezierPath addArcWithCenter:CGPointMake(outerWidth/2, outerWidth/2) radius:outerWidth/2 startAngle:oldAngle endAngle:valueAngle clockwise:NO];
    } else if (oldValue < value) { // ->
        CGFloat valueAngle = value/100.0 * (2*M_PI - (self.startAngle - self.endAngle)) + self.startAngle;
        CGFloat oldAngle = oldValue/100.0 * (2*M_PI - (self.startAngle - self.endAngle)) + self.startAngle;
        [bezierPath addArcWithCenter:CGPointMake(outerWidth/2, outerWidth/2) radius:outerWidth/2 startAngle:oldAngle endAngle:valueAngle clockwise:YES];
    } else {
        return;
    }
    
    CAKeyframeAnimation *positionKF = [CAKeyframeAnimation animationWithKeyPath:`@"position"`];
    positionKF.duration = 1;
    positionKF.path = bezierPath.CGPath;
    positionKF.calculationMode = kCAAnimationPaced;
    positionKF.removedOnCompletion = NO;
    positionKF.fillMode = kCAFillModeForwards;
    
    [self.cursorLayer addAnimation:positionKF forKey:`@"rotateCursorAnimated"`];
    

    我们根据起点和终点绘制一段小圆点移动的路径,设置CAKeyframAnimation即可。这里比较绕的时候,当小圆点从左往右移动和从右往左移动,一个是顺时针clock=YES一个是逆时针clock=NO,这里就要注意我们前面说的了,相同角度下,顺时针和逆时针需要换算一下。参考上面的代码。

    作为一个ios开发者,遇到问题的时候,有一个学习的氛围跟一个交流圈子特别重要对自身有很大帮助,众人拾柴火焰高 这是一个我的iOS交流群:711315161,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!希望帮助开发者少走弯路。

    代码部分

    完整代码如下,仅供参考

    #import "PointView.h"
    
    @interface PointView ()
    
    @property (nonatomic, strong) CAShapeLayer *valueLayer;
    @property (nonatomic, strong) CALayer *cursorLayer;
    @property (nonatomic, strong) UIBezierPath *valuePath;
    @property (nonatomic, assign) CGFloat startAngle;
    @property (nonatomic, assign) CGFloat endAngle;
    @property (nonatomic, assign) CGFloat currenAngle;
    @property (nonatomic, strong) CADisplayLink *link;
    @property (nonatomic, strong) UILabel *numberLabel;
    @property (nonatomic, assign) NSInteger oldValue;
    
    @end
    
    @implementation PointView
    
    - (instancetype)initWithFrame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if (self) {
            _startAngle = M_PI*15.2/18;
            _endAngle = M_PI*2.8/18;
            
            [self setupViews];
        }
        return self;
    }
    
    - (void)dealloc {
        [self.link invalidate];
        self.link = nil;
    }
    
    /**
     p1********************************************p6
     *                                             *
     *                                             *
     *                                             *
     *                                             *
     p2******************p3   p4******************p5
                       p3.1 *  p4.1
     */
    
    - (void)setupViews {
        CGFloat x = 0;
        CGFloat y = 0;
        CGFloat width = self.frame.size.width;
        CGFloat height = self.frame.size.height;
        CGFloat radius = 15;
        
        CGPoint p1 = CGPointMake(x, y);
        CGPoint p2 = CGPointMake(x, height-radius);
        CGPoint p3 = CGPointMake(width/2-radius, height-radius);
        CGPoint p3_1 = CGPointMake(width/2-radius, height);
        CGPoint p4 = CGPointMake(width/2+radius, height-radius);
        CGPoint p4_1 = CGPointMake(width/2+radius, height);
        CGPoint p5 = CGPointMake(width, height-radius);
        CGPoint p6 = CGPointMake(width, y);
        
        CAGradientLayer *gradientLayer = [[CAGradientLayer alloc] init];
        
        UIColor *startColor = [UIColor colorWithRed:225/255.0 green:187/255.0 blue:118/255.0 alpha:1];
        UIColor *endColor = [UIColor colorWithRed:209/255.0 green:162/255.0 blue:92/255.0 alpha:1];
        
        gradientLayer.colors = @[(__bridge id)startColor.CGColor, (__bridge id)endColor.CGColor];
        gradientLayer.startPoint = CGPointMake(0.5, 0);
        gradientLayer.endPoint = CGPointMake(0.5, 1);
        gradientLayer.frame = self.bounds;
        [self.layer addSublayer:gradientLayer];
        
        CAShapeLayer *shapeLayer = [CAShapeLayer layer];
        UIBezierPath *bezierPath = [UIBezierPath bezierPath];
        [bezierPath moveToPoint:p1];
        [bezierPath addLineToPoint:p2];
        [bezierPath addLineToPoint:p3];
        [bezierPath addArcWithCenter:p3_1 radius:radius startAngle:1.5*M_PI endAngle:0 clockwise:YES];
        [bezierPath addArcWithCenter:p4_1 radius:radius startAngle:-1*M_PI endAngle:-0.5*M_PI clockwise:YES];
        [bezierPath moveToPoint:p4];
        [bezierPath addLineToPoint:p5];
        [bezierPath addLineToPoint:p6];
        [bezierPath addLineToPoint:p1];
        
        shapeLayer.path = bezierPath.CGPath;
        self.layer.mask = shapeLayer;
        
        // inner circle
        CAShapeLayer *innerLayer = [CAShapeLayer layer];
        CGFloat innerWidth = 170;
        CGFloat innerHeight = 135;
        innerLayer.frame = CGRectMake(width/2-innerWidth/2, 54, innerWidth, innerHeight);
        
        UIBezierPath *innerPath = [UIBezierPath bezierPath];
        [innerPath addArcWithCenter:CGPointMake(innerWidth/2, innerWidth/2) radius:innerWidth/2 startAngle:M_PI*14.4/18 endAngle:M_PI*3.6/18 clockwise:YES];
    
        innerLayer.path = innerPath.CGPath;
        innerLayer.strokeColor = [[UIColor whiteColor] colorWithAlphaComponent:0.4].CGColor;
        innerLayer.fillColor = [UIColor clearColor].CGColor;
        innerLayer.lineDashPattern = @[@4, @3];
        [self.layer addSublayer:innerLayer];
        
        // middle circle
        CAShapeLayer *middleLayer = [CAShapeLayer layer];
        CGFloat middleWidth = 199;
        CGFloat middleHeight = 158;
        middleLayer.frame = CGRectMake(width/2-middleWidth/2, 37, middleWidth, middleHeight);
        
        UIBezierPath *middlePath = [UIBezierPath bezierPath];
        [middlePath addArcWithCenter:CGPointMake(middleWidth/2, middleWidth/2) radius:middleWidth/2 startAngle:M_PI*15/18 endAngle:M_PI*3/18 clockwise:YES];
        
        middleLayer.path = middlePath.CGPath;
        middleLayer.strokeColor = [[UIColor whiteColor] colorWithAlphaComponent:0.4].CGColor;
        middleLayer.fillColor = [UIColor clearColor].CGColor;
        middleLayer.lineWidth = 10;
        middleLayer.lineCap = kCALineCapRound;
        [self.layer addSublayer:middleLayer];
        
        // outer circle
        CAShapeLayer *outerLayer = [CAShapeLayer layer];
        CGFloat outerWidth = 226;
        CGFloat outerHeight = 165;
        outerLayer.frame = CGRectMake(width/2-outerWidth/2, 24, outerWidth, outerHeight);
        
        UIBezierPath *outerPath = [UIBezierPath bezierPath];
        [outerPath addArcWithCenter:CGPointMake(outerWidth/2, outerWidth/2) radius:outerWidth/2 startAngle:M_PI*15.2/18 endAngle:M_PI*2.8/18 clockwise:YES];
        
        outerLayer.path = outerPath.CGPath;
        outerLayer.strokeColor = [[UIColor whiteColor] colorWithAlphaComponent:0.4].CGColor;
        outerLayer.fillColor = [UIColor clearColor].CGColor;
        outerLayer.lineWidth = 3;
        outerLayer.lineCap = kCALineCapRound;
        [self.layer addSublayer:outerLayer];
        
        // value circle
        CAShapeLayer *valueLayer = [CAShapeLayer layer];
        valueLayer.frame = CGRectMake(width/2-outerWidth/2, 24, outerWidth, outerHeight);
        
        self.valuePath = [UIBezierPath bezierPath];
        [self.valuePath addArcWithCenter:CGPointMake(outerWidth/2, outerWidth/2) radius:outerWidth/2 startAngle:self.startAngle endAngle:self.endAngle clockwise:YES];
        
        valueLayer.path = self.valuePath.CGPath;
        valueLayer.strokeColor = [UIColor whiteColor].CGColor;
        valueLayer.fillColor = [UIColor clearColor].CGColor;
        valueLayer.lineWidth = 3;
        valueLayer.lineCap = kCALineCapRound;
        
        self.cursorLayer = [CALayer layer];
        self.cursorLayer.backgroundColor = [UIColor whiteColor].CGColor;
        self.cursorLayer.cornerRadius = 4;
        self.cursorLayer.masksToBounds = YES;
        CGPoint startPoint = [[self pointsFromBezierPath:self.valuePath].firstObject CGPointValue];
        self.cursorLayer.frame = CGRectMake(startPoint.x, startPoint.y, 8, 8);
        [valueLayer addSublayer:self.cursorLayer];
        
        
        [self.layer addSublayer:valueLayer];
        self.valueLayer = valueLayer;
        self.valueLayer.strokeEnd = 0;
        self.value = 0;
        
        self.numberLabel = [[UILabel alloc] initWithFrame:CGRectMake(width/2-100/2, 102, 100, 63)];
        self.numberLabel.textColor = [UIColor whiteColor];
        self.numberLabel.font = [UIFont systemFontOfSize:45];
        self.numberLabel.textAlignment = NSTextAlignmentCenter;
        self.numberLabel.text = @"0";
        [self addSubview:self.numberLabel];
        
        self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayNumber)];
        [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        self.link.paused = YES;
    }
    
    - (void)displayNumber {
        NSLog(@"display link变化");
        NSInteger currentNumber = [self.numberLabel.text integerValue];
        if (self.value < currentNumber) {
            currentNumber -= 1;
        } else if (self.value > currentNumber) {
            currentNumber += 1;
        }
        if (currentNumber == self.value) {
            self.link.paused = YES;
        }
        self.numberLabel.text = [NSString stringWithFormat:@"%ld", currentNumber];
    }
    
    - (void)setValue:(NSInteger)value {
        NSInteger oldValue = _value;
        _oldValue = oldValue;
        _value = value;
        
        NSLog(@"旧值:%f | 新值:%f", oldValue/100.0, value/100.0);
        
        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
        animation.duration = 1;
        animation.fromValue = @(oldValue/100.0);
        animation.toValue = @(value/100.0);
        animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
        animation.removedOnCompletion = NO;
        animation.fillMode = kCAFillModeForwards;
        [self.valueLayer addAnimation:animation forKey:@"valueProgress"];
    //    self.valueLayer.strokeEnd = value/100.0;
        self.link.paused = NO;
        
    //    [CATransaction begin];
        UIBezierPath *bezierPath = [UIBezierPath bezierPath];
        CGFloat outerWidth = 226;
        if (oldValue > value) { // <-
            CGFloat valueAngle = value/100.0 * (2*M_PI - (self.startAngle - self.endAngle)) + self.startAngle;
            CGFloat oldAngle = oldValue/100.0 * (2*M_PI - (self.startAngle - self.endAngle)) + self.startAngle;
            
            valueAngle = valueAngle - 2*M_PI;
            oldAngle = oldAngle - 2*M_PI;
            
            [bezierPath addArcWithCenter:CGPointMake(outerWidth/2, outerWidth/2) radius:outerWidth/2 startAngle:oldAngle endAngle:valueAngle clockwise:NO];
        } else if (oldValue < value) { // ->
            CGFloat valueAngle = value/100.0 * (2*M_PI - (self.startAngle - self.endAngle)) + self.startAngle;
            CGFloat oldAngle = oldValue/100.0 * (2*M_PI - (self.startAngle - self.endAngle)) + self.startAngle;
            [bezierPath addArcWithCenter:CGPointMake(outerWidth/2, outerWidth/2) radius:outerWidth/2 startAngle:oldAngle endAngle:valueAngle clockwise:YES];
        } else {
            return;
        }
        
        CAKeyframeAnimation *positionKF = [CAKeyframeAnimation animationWithKeyPath:@"position"];
        positionKF.duration = 1;
        positionKF.path = bezierPath.CGPath;
        positionKF.calculationMode = kCAAnimationPaced;
        positionKF.removedOnCompletion = NO;
        positionKF.fillMode = kCAFillModeForwards;
    
        [self.cursorLayer addAnimation:positionKF forKey:@"rotateCursorAnimated"];
    }
    
    void getPointsFromBezier(void *info, const CGPathElement *element) {
        NSMutableArray *bezierPoints = (__bridge NSMutableArray *)info;
        CGPathElementType type = element->type;
        CGPoint *points = element->points;
        
        if (type != kCGPathElementCloseSubpath) {
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[0]]];
            if (type != kCGPathElementAddLineToPoint && type != kCGPathElementMoveToPoint) {
                [bezierPoints addObject:[NSValue valueWithCGPoint:points[1]]];
            }
        }
        
        if (type == kCGPathElementAddCurveToPoint) {
            [bezierPoints addObject:[NSValue valueWithCGPoint:points[2]]];
        }
    }
    
    - (NSArray *)pointsFromBezierPath:(UIBezierPath *)path {
        NSMutableArray *points = [NSMutableArray array];
        CGPathApply(path.CGPath, (__bridge void *)points, getPointsFromBezier);
        return points;
    }
    
    @end
    

    作者:神奇奶盖
    链接:https://juejin.cn/post/6896859284295188488

    相关文章

      网友评论

        本文标题:iOS绘制仪表盘,游标沿圆形轨迹移动动画

        本文链接:https://www.haomeiwen.com/subject/rzrjgktx.html