iOS繪制儀表盤箩溃,游標(biāo)沿圓形軌跡移動動畫

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的關(guān)系止状,之前一直記不太清烹棉。

我們需要看一下下面這張圖

在這里插入圖片描述

clockwise為true的時候,就會從startAngle按順時針方向畫弧怯疤。為false的時候浆洗,按逆時針方向畫弧。
這里有一個要注意的地方是

  1. 順時針的360°表示的值集峦,必須是0~2*M_PI伏社,
  2. 逆時針的360°表示的值必須是0~-2*M_PI。
  3. 同一個角度表示的值少梁,在clockwise取true或者false的情況下洛口,是需要轉(zhuǎn)換的。

了解了上面的注意點(diǎn)之后凯沪,三條圓弧還是能很方便的畫出來的第焰。
接下來就是動態(tài)的顯示進(jìn)度。即白色圓弧每次數(shù)值變化妨马,弧線動態(tài)增長或減少挺举。

StrokeEnd

一開始想的是每次都重新繪制貝塞爾曲線,但是發(fā)現(xiàn)從動畫效果上來看烘跺,每次重新繪制湘纵,都會從起點(diǎn)位置繪制到終點(diǎn)位置。不是想要的效果滤淳。
然后又想著設(shè)置layer.masksToBounds=true然后梧喷,畫一條半圓,通過旋轉(zhuǎn)來達(dá)到左右移動的效果,這樣超出layer的部分就不會顯示了铺敌,但是又發(fā)現(xiàn)背景的圓弧不是一塊半圓汇歹,會存在覆蓋不全的情況。
后來查看CAShaperLayer的說明偿凭,發(fā)現(xiàn)這樣一個屬性

/* 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;

這兩個值默認(rèn)是0和1产弹,對應(yīng)的就是起始點(diǎn)和終點(diǎn)的比例,當(dāng)storkeEnd=0.5的時候弯囊,原來圓弧終點(diǎn)的值就會減少為原來的一半
那么我就可以這樣了痰哨,我先畫一套完整的覆蓋背景圓弧的實(shí)線圓弧,設(shè)置strokeEnd=0匾嘱,這樣圓弧長度就為0了斤斧。當(dāng)值變化的時候,在調(diào)整strokeEnd的值奄毡。就可以動態(tài)的變化圓弧長度了折欠。
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"];

繪制移動路徑

搞定了圓弧的變化之后,還有一部分是小圓點(diǎn)的移動吼过,它是按照圓弧的軌跡移動的锐秦,那么在做動畫效果的時候,就要讓小圓點(diǎn)按照圓弧的軌跡移動位置盗忱。

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"`];

我們根據(jù)起點(diǎn)和終點(diǎn)繪制一段小圓點(diǎn)移動的路徑酱床,設(shè)置CAKeyframAnimation即可。這里比較繞的時候趟佃,當(dāng)小圓點(diǎn)從左往右移動和從右往左移動扇谣,一個是順時針clock=YES一個是逆時針clock=NO,這里就要注意我們前面說的了闲昭,相同角度下罐寨,順時針和逆時針需要換算一下。參考上面的代碼序矩。

作為一個ios開發(fā)者鸯绿,遇到問題的時候,有一個學(xué)習(xí)的氛圍跟一個交流圈子特別重要對自身有很大幫助簸淀,眾人拾柴火焰高 這是一個我的iOS交流群:711315161瓶蝴,分享BAT,阿里面試題、面試經(jīng)驗(yàn)租幕,討論技術(shù)舷手, 大家一起交流學(xué)習(xí)成長!希望幫助開發(fā)者少走彎路劲绪。

代碼部分

完整代碼如下男窟,僅供參考

#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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末盆赤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蝎宇,更是在濱河造成了極大的恐慌弟劲,老刑警劉巖祷安,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件姥芥,死亡現(xiàn)場離奇詭異,居然都是意外死亡汇鞭,警方通過查閱死者的電腦和手機(jī)凉唐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來霍骄,“玉大人台囱,你說我怎么就攤上這事《琳” “怎么了簿训?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長米间。 經(jīng)常有香客問我强品,道長,這世上最難降的妖魔是什么屈糊? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任的榛,我火速辦了婚禮,結(jié)果婚禮上逻锐,老公的妹妹穿的比我還像新娘夫晌。我一直安慰自己,他們只是感情好昧诱,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布晓淀。 她就那樣靜靜地躺著,像睡著了一般盏档。 火紅的嫁衣襯著肌膚如雪凶掰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天妆丘,我揣著相機(jī)與錄音锄俄,去河邊找鬼。 笑死勺拣,一個胖子當(dāng)著我的面吹牛奶赠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播药有,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼毅戈,長吁一口氣:“原來是場噩夢啊……” “哼苹丸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起苇经,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤赘理,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扇单,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體商模,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年蜘澜,在試婚紗的時候發(fā)現(xiàn)自己被綠了施流。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡鄙信,死狀恐怖瞪醋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情装诡,我是刑警寧澤银受,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站鸦采,受9級特大地震影響宾巍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赖淤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一蜀漆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧咱旱,春花似錦确丢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至诸典,卻和暖如春描函,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狐粱。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工舀寓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肌蜻。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓互墓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蒋搜。 傳聞我的和親對象是個殘疾皇子篡撵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評論 2 361

推薦閱讀更多精彩內(nèi)容