iOS 如何繪制餅狀圖

先來見識一下史上最無力的背打



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)鍵就在排列組合上面聂宾,下面提供兩種思路:

  1. 所有CAShapeLayer共用一個圓形的UIBezierPath果善,然后通過設(shè)置每一個CAShapeLayer的strokeStartstrokeEnd屬性來控制該layer在這個Path中所在的位置,這樣就能輕松畫出一個餅狀圖了系谐。接下來我們就需要計算每一個CAShapeLayer的strokeStartstrokeEnd巾陕,當我們拿到數(shù)據(jù)源的時候,先對數(shù)據(jù)進行排序處理纪他,然后遍歷數(shù)組鄙煤,將strokeStartstrokeEnd收尾相接即可,廢話有點多茶袒,直接上代碼:

    - (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;
    }
    
  2. 每個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在此漱病,看到了就順手點個贊唄!??
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末把曼,一起剝皮案震驚了整個濱河市杨帽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嗤军,老刑警劉巖注盈,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異叙赚,居然都是意外死亡老客,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門纠俭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沿量,“玉大人,你說我怎么就攤上這事冤荆∑釉颍” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵钓简,是天一觀的道長乌妒。 經(jīng)常有香客問我,道長外邓,這世上最難降的妖魔是什么撤蚊? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮损话,結(jié)果婚禮上侦啸,老公的妹妹穿的比我還像新娘。我一直安慰自己丧枪,他們只是感情好光涂,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拧烦,像睡著了一般忘闻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上恋博,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天齐佳,我揣著相機與錄音私恬,去河邊找鬼。 笑死炼吴,一個胖子當著我的面吹牛本鸣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播缺厉,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼永高,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了提针?” 一聲冷哼從身側(cè)響起命爬,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎辐脖,沒想到半個月后饲宛,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡嗜价,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年艇抠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片久锥。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡家淤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瑟由,到底是詐尸還是另有隱情絮重,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布歹苦,位于F島的核電站青伤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏殴瘦。R本人自食惡果不足惜狠角,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蚪腋。 院中可真熱鬧丰歌,春花似錦、人聲如沸屉凯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽神得。三九已至,卻和暖如春偷仿,著一層夾襖步出監(jiān)牢的瞬間哩簿,已是汗流浹背宵蕉。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留节榜,地道東北人羡玛。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像宗苍,于是被迫代替她去往敵國和親稼稿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

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