先來見識一下史上最無力的背打
OK变秦!進入正題赎婚,哈哈??
前言
對于圖形的繪制,我們可以用
CoreGraphics
中CGContext或者UIKit
中UIBezierPath撩嚼,前者是一個C語言類,功能強大猖任,后者是基于前者封裝的OC類,使用起來更方便,兩者都可以在View的drawRect
中直接繪制出想要的圖形。但是我們這里為了后續(xù)操作方便谁尸,每個path都使用一個承載對象悍赢,CALayer跟CAShapeLayer都可以,顯然后者更專業(yè)左权,專注形狀繪制屡贺,那么就它了??
主要對象及屬性
UIBezierPath,之前專門寫了一篇對 UIBezierPath的理解與運用谤职,這里我就不再說了亿鲜。
-
CAShapeLayer,專注繪制形狀的圖層,繼承自CALayer怠李,擁有CALayer所有特性构挤,且增加了一些特有的屬性唐础。
//路徑,根據(jù)這個路徑來繪制形狀 @property(nullable) CGPathRef path; //路徑所包含區(qū)域的填充色 @property(nullable) CGColorRef fillColor; //繪制路徑的顏色 @property(nullable) CGColorRef strokeColor; //路徑繪制的開始值,默認為0输钩,可以更改這個值來控制路徑繪制的起始位置 @property CGFloat strokeStart; //路徑繪制的結(jié)束值,默認為1,可以更改這個值來控制路徑繪制的結(jié)束位置 @property CGFloat strokeEnd; //線寬 @property CGFloat lineWidth;
怎么做?
要繪制一個餅狀圖,首先我們要了解餅狀圖的結(jié)構(gòu),我們前面提到過轧铁,可以直接在drawRect通過UIBezierPath拼接來畫出一個餅狀圖每聪,但是每個path沒有單獨的承載對象,不方便交互齿风,所以才用CAShapeLayer药薯,那么一個餅狀圖就是一個一個的CAShapeLayer按照順序排列組合起來的,所以關(guān)鍵就在排列組合上面聂宾,下面提供兩種思路:
-
所有CAShapeLayer共用一個圓形的UIBezierPath果善,然后通過設(shè)置每一個CAShapeLayer的
strokeStart
跟strokeEnd
屬性來控制該layer在這個Path中所在的位置,這樣就能輕松畫出一個餅狀圖了系谐。接下來我們就需要計算每一個CAShapeLayer的strokeStart
跟strokeEnd
巾陕,當我們拿到數(shù)據(jù)源的時候,先對數(shù)據(jù)進行排序處理纪他,然后遍歷數(shù)組鄙煤,將strokeStart
跟strokeEnd
收尾相接即可,廢話有點多茶袒,直接上代碼:- (void)setDatas:(NSArray <NSNumber *>*)datas colors:(NSArray <UIColor *>*)colors{ NSArray *newDatas = [self getPersentArraysWithDataArray:datas]; CGFloat start = 0.f; CGFloat end = 0.f; UIBezierPath *piePath = [UIBezierPath bezierPathWithArcCenter:_center radius:_radius + Hollow_Circle_Radius startAngle:-M_PI_2 endAngle:M_PI_2*3 clockwise:YES]; for (int i = 0; i < newDatas.count; i ++) { NSNumber *number = newDatas[i]; end = start + number.floatValue; CAShapeLayer *pieLayer = [CAShapeLayer layer]; pieLayer.strokeStart = start; pieLayer.strokeEnd = end; pieLayer.lineWidth = _radius*2 - Hollow_Circle_Radius; pieLayer.strokeColor = [colors.count > i?colors[i]:kPieRandColor CGColor]; pieLayer.fillColor = [UIColor clearColor].CGColor; pieLayer.path = piePath.CGPath; [self.layer addSublayer:pieLayer]; start = end; } }
數(shù)據(jù)處理:
/** 將數(shù)據(jù)按降序排列梯刚,再計算出所占比例返回 @param datas 原始數(shù)據(jù) @return 數(shù)據(jù)占比數(shù)組 */ - (NSArray *)getPersentArraysWithDataArray:(NSArray *)datas{ NSArray *newDatas = [datas sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { if ([obj1 floatValue] < [obj2 floatValue]) { return NSOrderedDescending; }else if ([obj1 floatValue] > [obj2 floatValue]){ return NSOrderedAscending; }else{ return NSOrderedSame; } }]; NSMutableArray *persentArray = [NSMutableArray array]; NSNumber *sum = [newDatas valueForKeyPath:@"@sum.floatValue"]; for (NSNumber *number in newDatas) { [persentArray addObject:@(number.floatValue/sum.floatValue)]; } return persentArray; }
-
每個CAShapeLayer對應一個單獨的UIBezierPath,我比較推薦使用這一種方式薪寓,因為在處理點擊事件的時候亡资,這種方式就可以判斷point是否是在path內(nèi)部,從而找到對應的layer向叉,而第一種方式因為所有l(wèi)ayer共用的一個UIBezierPath锥腻,以致于無法識別點擊的point是屬于哪一個layer,下面再看代碼:
- (void)setDatas:(NSArray <NSNumber *>*)datas colors:(NSArray <UIColor *>*)colors{ NSArray *newDatas = [self getPersentArraysWithDataArray:datas]; CGFloat start = -M_PI_2; CGFloat end = start; //之所以加上這個循環(huán)母谎,也是考慮到如果多次調(diào)用瘦黑,也不會重復創(chuàng)建layer while (newDatas.count > self.layer.sublayers.count) { XZMLayer *pieLayer = [XZMLayer layer]; pieLayer.strokeColor = NULL; [self.layer addSublayer:pieLayer]; } for (int i = 0; i < self.layer.sublayers.count; i ++) { XZMLayer *pieLayer = (XZMLayer *)self.layer.sublayers[i]; if (i < newDatas.count) { pieLayer.hidden = NO; end = start + M_PI*2*[newDatas[i] floatValue]; UIBezierPath *piePath = [UIBezierPath bezierPath]; [piePath moveToPoint:_center]; [piePath addArcWithCenter:_center radius:_radius*2 startAngle:start endAngle:end clockwise:YES]; pieLayer.fillColor = [colors.count > i?colors[i]:kPieRandColor CGColor]; pieLayer.startAngle = start; pieLayer.endAngle = end; pieLayer.path = piePath.CGPath; start = end; }else{ pieLayer.hidden = YES; } } }
定義一個
XZMLayer
出來,也是為了方便我們添加屬性@interface XZMLayer : CAShapeLayer @property (nonatomic,assign)CGFloat startAngle; //開始角度 @property (nonatomic,assign)CGFloat endAngle; //結(jié)束角度 @property (nonatomic,assign)BOOL isSelected; //是否已經(jīng)選中 @end
至此奇唤,我們的餅狀圖就基本已經(jīng)畫出來了幸斥,下面貼一張Demo圖片
這個圖太死板了,我們給它來點活力咬扇,添加動畫甲葬!
添加動畫
乍一看感覺不知從何下手,其實很簡單冗栗!就是通過一個mask屬性來控制顯示區(qū)域演顾,mask可以理解成一個背景層供搀,當我們自定義一個mask的layer,layer默認是透明的钠至,即背景是透明的葛虐,所以什么都看不到,當給mask賦值一個非透明任意背景顏色棉钧,則表現(xiàn)層的東西才能展示出來屿脐,所以我們只要通過給mask加一個繪制動畫就可以了。
創(chuàng)建一個layer作為整個view.layer.mask
//通過mask來控制顯示區(qū)域
_maskLayer = [CAShapeLayer layer];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithArcCenter:_center radius:self.bounds.size.width/4.f startAngle:-M_PI_2 endAngle:M_PI_2*3 clockwise:YES];
//設(shè)置邊框顏色為不透明宪卿,則可以通過邊框的繪制來顯示整個view
_maskLayer.strokeColor = [UIColor greenColor].CGColor;
_maskLayer.lineWidth = self.bounds.size.width/2.f;
//設(shè)置填充顏色為透明的诵,可以通過設(shè)置半徑來設(shè)置中心透明范圍
_maskLayer.fillColor = [UIColor clearColor].CGColor;
_maskLayer.path = maskPath.CGPath;
_maskLayer.strokeEnd = 0;
self.layer.mask = _maskLayer;
給這個maskLayer添加一個基礎(chǔ)動畫
- (void)stroke{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
animation.duration = 1.f;
animation.fromValue = [NSNumber numberWithFloat:0.f];
animation.toValue = [NSNumber numberWithFloat:1.f];
//禁止還原
animation.autoreverses = NO;
//禁止完成即移除
animation.removedOnCompletion = NO;
//讓動畫保持在最后狀態(tài)
animation.fillMode = kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[_maskLayer addAnimation:animation forKey:@"strokeEnd"];
}
來看看效果
交互
交互主要體現(xiàn)在用戶點擊上面,表現(xiàn)形式有很多種佑钾,具體根據(jù)需求來定西疤,這里我來講一下用戶點擊進行單元拆分的這么一個功能。首先我們要確定觸摸的point是在哪一個模塊休溶,拿到這個模塊之后再來拆分代赁,也就是更改layer的position(中心點)屬性值,默認是(0,0)兽掰,而拆分的方向是沿著一條過圓心以及l(fā)ayer的position的直線往外的方向芭碍,在設(shè)定偏移量之后,根據(jù)三角函數(shù)可以輕松求出新的position(x,y)孽尽,說起來有點繞口窖壕,來畫個圖就清楚了??
這圖畫的我自己都怕??,圖上綠色的就是設(shè)定的偏移值杉女,我們只要求出(x,y)就可以了瞻讽,很簡單吧,一個直角三角形熏挎,已知斜邊跟角度卸夕,求另外兩邊長,三角函數(shù)啊婆瓜,初中就學過的吧,哈哈贡羔!不廢話了廉白,上代碼
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CGPoint point = [touches.anyObject locationInView:self];
[self upDateLayersWithPoint:point];
}
- (void)upDateLayersWithPoint:(CGPoint)point{
//遍歷查找點擊的是哪一個layer
for (XZMLayer *layer in self.layer.sublayers) {
if (CGPathContainsPoint(layer.path, &CGAffineTransformIdentity, point, 0) && !layer.isSelected) {
layer.isSelected = YES;
//原始中心點為(0,0)乖寒,扇形所在圓心猴蹂、原始中心點、偏移點三者是在一條直線楣嘁,通過三角函數(shù)即可得到偏移點的對應x磅轻,y珍逸。
CGPoint currPos = layer.position;
double middleAngle = (layer.startAngle + layer.endAngle)/2.0;
CGPoint newPos = CGPointMake(currPos.x + KOffsetRadius*cos(middleAngle), currPos.y + KOffsetRadius*sin(middleAngle));
layer.position = newPos;
}else{
layer.position = CGPointMake(0, 0);
layer.isSelected = NO;
}
}
}
再來看效果
注意
- CAShapeLayer的
lineWidth
會影響整個path的大小,因為lineWidth
是從邊界往內(nèi)外兩邊延伸的聋溜,比如lineWidth
設(shè)置為10谆膳,那么就會往外擴大5,所以在設(shè)置半徑的時候需要把線寬考慮進來撮躁。 - Demo在此漱病,看到了就順手點個贊唄!??