close

饼状图的绘制

from http://www.winddisk.com/2012/06/17/piechart/

学习开源代码BNPieChart中饼状图的绘制流程,目的是绘制如下图形。

一.获取饼状图大小及各个扇区尺寸

根据给定的Frame计算出饼状图的圆心(centerX,centerY)以及半径(radius),设置需要绘制的扇区数组,此处取7个扇区,其所占比例分别为0.1,0.2,0.1,0.1,0.3,0.1,0.1,总和为1。

二.绘制背景圆形

以(centerX,centerY)为圆心,radius为半径,绘制白色背景圆。

	// Draw a white background for the pie chart.
	// We need to do this since many of our color components have alpha < 1.
	CGContextRef context = UIGraphicsGetCurrentContext();
	CGContextBeginPath(context);
	CGContextAddArc(context, centerX, centerY, radius, 0, 2*M_PI, 1);
	CGContextSetRGBFillColor(context, 1.0, 1.0, 1.0, 1.0);
	CGContextFillPath(context);

如图所示(因背景圆是白色,将底色设为黑色以突出显示,其它图中底色也为白色)。

三.逐个绘制各扇区部分

    CGContextSaveGState(context);
    float shadowSize = radius / 15.0;
    CGContextSetShadow(context, CGSizeMake(shadowSize, shadowSize), shadowSize);
    CGContextBeginTransparencyLayer(context, NULL);
    for (int i = 0; i < [slicePortions count]; ++i) {
      [self drawSlice:i inContext:context];
    }
    CGContextEndTransparencyLayer(context);
    CGContextRestoreGState(context);
  • 绘制时启用阴影(Shadow)效果以增加立体感,CGContextSetShadow设置阴影的区域偏移为(shadowSize, shadowSize),即阴影区域在UIView坐标系的右下方,边界柔和度为radius / 15.0。使用CGContextSetShadow绘制的阴影区域为1/3 alpha的黑色,即{0, 0, 0, 1.0/3.0}。若想定制阴影部分的颜色,可使用CGContextSetShadowWithColor
  • 此处使用TransparencyLayer将所有扇区对象组合成单个对象再启用阴影效果。Transparency layer用于对一组对象设置某种效果,如以上的阴影效果。如果对每个扇区单独实施阴影效果,则每个扇区的阴影会叠加在一起,而我们想要的是一个整体的阴影效果。两种效果对比如下:

四.每个扇区的绘制流程

  • 首先创建表示扇区的path
    	CGFloat startAngle = 2 * M_PI * [self pointAtIndex:index];
    	CGFloat endAngle = 2 * M_PI * [self pointAtIndex:(index + 1)];
    
    	CGMutablePathRef path = CGPathCreateMutable();
    	CGPathAddArc(path, NULL, centerX, centerY, radius, startAngle, endAngle, 0);
    	CGPathAddLineToPoint(path, NULL, centerX, centerY);
    	CGPathCloseSubpath(path);

    每个扇区归一化的起始偏移与终止偏移已事先存储在数组中,通过pointAtIndex方法获取到,然后乘以2*M_PI即起始弧度与终止弧度。通过一系列path函数建立以(centerX, centerY)为圆心,以radius为半径,弧度范围(startAngle,endAngle)的扇形路径。即这样一个区域

  • 填充扇区底色
    	CGContextSaveGState(context);
    	CGContextAddPath(context, path);
    	CGFloat red, green, blue;
    	[self getRGBForIndex:index red:&red green:&green blue:&blue];
    	CGContextSetRGBFillColor(context, red, green, blue, 0.35);
    	CGContextFillPath(context);
    	CGContextRestoreGState(context);
    

    通过getRGBForIndex可获取针对当前扇区的颜色值,可根据需要调整。获取到当前扇区的颜色后将之填充到当前扇区,颜色透明度设为0.35。效果如下

  • 覆盖渐变色
    	// Draw the left-right gradient.
    	CGContextSaveGState(context);
    	CGContextAddPath(context, path);
    	CGContextClip(context);
    	CGGradientRef gradient = [self newGradientForIndex:index];
    	CGContextDrawLinearGradient(context, gradient,
                                  CGPointMake(centerX + radius, centerY),
                                  CGPointMake(centerX - radius, centerY), 0);
    	CGGradientRelease(gradient);
    	CGContextRestoreGState(context);
    

    Gradient作用的区域是整个Context,但是我们只需要对扇区部分进行着色,所以需先Clip到扇区部分,再使用CGContextDrawLinearGradient绘制线性渐变色。
    渐变色在扇区底色基础上变换,从0.0位置的深不透明色(red, green, blue, 0.9)渐变到1.0位置的(sqrt(red), sqrt(green), sqrt(blue), 0.15)浅透明色,获取方法如下

    - (CGGradientRef)newGradientForIndex:(int)index {
    	size_t num_locations = 2;
    	CGFloat locations[2] = {0.0, 1.0};
    	CGFloat red, green, blue;
    	[self getRGBForIndex:index red:&red green:&green blue:&blue];
    	CGFloat components[8] = {red, green, blue, 0.9,
        sqrt(red), sqrt(green), sqrt(blue), 0.15};
    	return CGGradientCreateWithColorComponents(colorspace, components,
                                                 locations, num_locations);
    }
    

    填充渐变色后的效果如下:

    关于Gradient有两种方式的渐进形式Axial和Radial,分别为线性渐变和射线渐变;创建的Gradient对象也有两种CGShadingRefCGGradientRef,CGShadingRef使用起来比较负责,需要自己传递函数来计算每个点的颜色值,而CGGradientRef则由系统自动计算。关于两种Gradient对象的创建和使用,可参考Quartz 2D Programming Guide的Gradients部分和CoreGraphics之gloss gradients中CGShadingRef的使用。

  • 以半透明黑色绘制扇区边线
    	// Draw the slice outline.
    	CGContextSaveGState(context);
    	CGContextAddPath(context, path);
    	CGContextClip(context);
    	CGContextAddPath(context, path);
    	CGContextSetLineWidth(context, 0.5);
    	UIColor* darken = [UIColor colorWithWhite:0.0 alpha:0.2];
    	CGContextSetStrokeColorWithColor(context, darken.CGColor);
    	CGContextStrokePath(context);
    	CGContextRestoreGState(context);
    

    绘线的函数很简单,这里做了一个处理,即先clip到扇区区域后再画边线,这样做的作用时将线限制在扇区区域内。若不Clip,则画的线会有一半在当前扇形区域外,相邻扇形之间相互重叠,会使边线颜色过深。将线的颜色改为不透明蓝色,比较下两者的区别

    • 不使用Clip时的整体效果与Photoshop放大后的像素效果为:
    • 使用Clip后的整体效果与Photoshop放大后的效果为:

    可以明显看出不使用Clip的线宽2个像素,而使用Clip线宽1个像素。

  • 全部扇区绘制完成后的效果如下:

五.左上方添加眩光效果

	// Draw the glare.
	CGContextBeginPath(context);
	CGContextAddArc(context, centerX, centerY, radius, 0, 2*M_PI, 1);
	CGContextClip(context);
	CGContextBeginPath(context);
	CGContextAddArc(context, centerX - radius * 0.5, centerY - radius * 0.5,
					radius * 1.1, 0, 2*M_PI, 1);
	CGContextClip(context);

	// Set up the gradient for the glare.
	size_t num_locations = 2;
	CGFloat locations[2] = {0.0, 1.0};
	CGFloat components[8] = {1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.45};
	CGGradientRef gradient = CGGradientCreateWithColorComponents(colorspace, components,
																 locations, num_locations);
	CGContextDrawLinearGradient(context, gradient,
								CGPointMake(centerX + radius * 0.6, centerY + radius * 0.6),
								CGPointMake(centerX - radius, centerY - radius), 0);
	CGGradientRelease(gradient);

取饼状图圆形区域1(圆心(centerX, centerY), 半径radius)和左上稍大圆形区域(圆心(centerX - radius * 0.5),centerY - radius * 0.5),半径radius * 1.1)。
修改Clip区域为两圆相交区域。在相交区域内,从右下角到左上角绘制线性色,起始色为全透明白色(1.0, 1.0, 1.0, 0.0),终止色为半透明白色(1.0, 1.0, 1.0, 0.45)。
最终效果如下:

六.后续学习

BNPieChart代码中还可以对扇区添加标签,其中计算标签的位置以及根据标签长度调整扇区半径的代码也值得研究。

源码:
BNPieChart.h
BNPieChart.m
https://github.com/tylerneylon/moriarty

参考:
BNPieChart
Quartz 2D Programming Guide
CGContext Reference
CGGradient Reference

arrow
arrow
    全站熱搜

    zer931 發表在 痞客邦 留言(0) 人氣()