最近碰到一個需求,需要畫一個儀表盤的頁面碌嘀。圖上所示涣旨。
計算角度
圓弧部分還好,用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的時候浆洗,按逆時針方向畫弧。
這里有一個要注意的地方是
- 順時針的360°表示的值集峦,必須是0~2*M_PI伏社,
- 逆時針的360°表示的值必須是0~-2*M_PI。
- 同一個角度表示的值少梁,在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