1、首先说一下canvas类:Class Overview
The Canvas class holds the "draw" calls. To draw something, you need 4 basic components: A Bitmap to hold the pixels, a Canvas to host the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect, Path, text, Bitmap), and a paint (to describe the colors and styles for the drawing).
这个类相当于一个画布,你可以在里面画很多东西;
我们可以把这个Canvas理解成系统提供给我们的一块内存区域(但实际上它只是一套画图的API,真正的内存是下面的Bitmap),而且它还提供了一整套对这个内存区域进行操作的方法,所有的这些操作都是画图API。也就是说在这种方式下我们已经能一笔一划或者使用Graphic来画我们所需要的东西了,要画什么要显示什么都由我们自己控制。
这种方式根据环境还分为两种:一种就是使用普通View的canvas画图,还有一种就是使用专门的SurfaceView的canvas来画图。两种的主要是区别就是可以在SurfaceView中定义一个专门的线程来完成画图工作,应用程序不需要等待View的刷图,提高性能。前面一种适合处理量比较小,帧率比较小的动画,比如说象棋游戏之类的;而后一种主要用在游戏,高品质动画方面的画图。
下面是Canvas类常用的方法:
drawRect(RectF rect, Paint paint) //绘制区域,参数一为RectF一个区域
drawPath(Path path, Paint paint) //绘制一个路径,参数一为Path路径对象
drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) //贴图,参数一就是我们常规的Bitmap对象,参数二是源区域(这里是bitmap),参数三是目标区域(应该在canvas的位置和大小),参数四是Paint画刷对象,因为用到了缩放和拉伸的可能,当原始Rect不等于目标Rect时性能将会有大幅损失。
drawLine(float startX, float startY, float stopX, float stopY, Paintpaint) //画线,参数一起始点的x轴位置,参数二起始点的y轴位置,参数三终点的x轴水平位置,参数四y轴垂直位置,最后一个参数为Paint 画刷对象。
drawPoint(float x, float y, Paint paint) //画点,参数一水平x轴,参数二垂直y轴,第三个参数为Paint对象。
drawText(String text, float x, floaty, Paint paint) //渲染文本,Canvas类除了上面的还可以描绘文字,参数一是String类型的文本,参数二x轴,参数三y轴,参数四是Paint对象。
drawOval(RectF oval, Paint paint)//画椭圆,参数一是扫描区域,参数二为paint对象;
drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)//画弧,
参数一是RectF对象,一个矩形区域椭圆形的界限用于定义在形状、大小、电弧,参数二是起始角(度)在电弧的开始,
参数三扫描角(度)开始顺时针测量的,参数四是如果这是真的话,包括椭圆中心的电弧,并关闭它,如果它是假这将是一个弧线,参数五是Paint对象;
还要理解一个paint类:
Class Overview
The Paint class holds the style and color information about how to draw geometries, text and bitmaps.
Paint 代表了Canvas上的画笔、画刷、颜料等等;
Paint类常用方法:
setARGB(int a, int r, int g, int b) // 设置 Paint对象颜色,参数一为alpha透明值
setAlpha(int a) // 设置alpha不透明度,范围为0~255
setAntiAlias(boolean aa) // 是否抗锯齿
setColor(int color) // 设置颜色,这里Android内部定义的有Color类包含了一些常见颜色定义
setTextScaleX(float scaleX) // 设置文本缩放倍数,1.0f为原始
setTextSize(float textSize) // 设置字体大小
setUnderlineText(booleanunderlineText) // 设置下划线
2、直接看案例看一下效果图:
在此案例中我们用到的是自定义view类;
CustomActivity.java
public class CustomActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
init();
}
private void init() {
LinearLayout layout=(LinearLayout) findViewById(R.id.root);
final DrawView view=new DrawView(this);
view.setMinimumHeight(500);
view.setMinimumWidth(300);
//通知view组件重绘
view.invalidate();
layout.addView(view);
}
}
今天要给大家介绍的是在android中画饼图:
画扇形:
RectF oval2 = new RectF(60, 100, 200, 240);
// 设置个新的长方形,60为左上点的x坐标,100为左上点的y坐标;200为右下点的 x坐标,240为右下点的y坐标。
// 画弧,第一个参数是RectF:该类是第二个参数是角度的开始,第三个参数是多少度,第四个参数是真的时候画扇形,是假的时候画弧 线
canvas.drawArc(oval2, 200, 130, true, p);
一、 无网情况下:
由于Android 画图API为提供直接画饼图的方法,采用了比较原始的方法,画扇形,然后拼接在一起,由于为了显示立体效果,程序画了20次,每次改变上下的位置,结果看起来就会有立体感(如果谁有更好的方式,非常愿意学习)
canvas.drawArc(new RectF(0, 0, 300, 100), 0,60, true, paint);
会画一个宽300,高100,水平 顺时针方向的60度的扇形,程序中就是用这种方式拼接成整个饼图的.
二、有网情况下:
使用Google API实现:
WebView webView = new WebView(this); // 使用红色,并让它50%透明
int opacity = 127;
int intColor = Color.argb(opacity, 255, 0, 0);
int parsedColor = Color.parseColor("#7FFF0000");
// 让颜色50%透明
int opacity = 127;
myPaint.setAlpha(opacity);
int colorFrom = Color.BLACK;
int colorTo = Color.WHITE;
LinearGradient linearGradientShader = new LinearGradient(x1, y1, x2, y2, colorFrom, colorTo, TileMode.CLAMP);
int[] gradientColors = new int[3];
gradientColors[0] = Color.GREEN;
gradientColors[1] = Color.YELLOW;
gradientColors[2] = Color.RED;
float[] gradientPositions = new float[3];
gradientPositions[0] = 0.0f;
gradientPositions[1] = 0.5f;
gradientPositions[2] = 1.0f;
RadialGradient radialGradientShader=new RadialGradient(centerX,centerY, radius, gradientColors, gradientPositions, TileMode.CLAMP);
// 设置光源的方向
float[] direction = new float[]{ 1, 1, 1 };
//设置环境光亮度
float light = 0.4f;
// 选择要应用的反射等级
float specular = 6;
// 向mask应用一定级别的模糊
float blur = 3.5f;
EmbossMaskFilter emboss=new EmbossMaskFilter(direction,light,specular,blur);
// 应用mask myPaint.setMaskFilter(emboss);
SDK中包含的FingerPaint API demo是说明如何使用MaskFilter的一个非常好的例子。它展示了这两种filter的效果。
使用ColorFilter
MaskFilter是对一个Paint的alpha通道的转换,而ColorFilter则是对每一个RGB通道应用转换。所有由ColorFilter所派生的类在执行它们的转换时,都会忽略alpha通道。
Android包含三个ColorFilter:
ColorMatrixColorFilter 可以指定一个4×5的ColorMatrix并将其应用到一个Paint中。ColorMatrixes通常在程序中用于对图像进行处理,而且由于它们支持使用矩阵相乘的方法来执行链接转换,所以它们很有用。
LightingColorFilter 乘以第一个颜色的RGB通道,然后加上第二个颜色。每一次转换的结果都限制在0到255之间。
PorterDuffColorFilter 可以使用数字图像合成的16条Porter-Duff 规则中的任意一条来向Paint应用一个指定的颜色。
使用setColorFilter方法应用ColorFilter,如下所示:
myPaint.setColorFilter(new LightingColorFilter(Color.BLUE, Color.RED));
API中的ColorMatrixSample是说明如何使用ColorFilter和Color Matrix的非常好的例子。
使用PathEffect
到目前为止,所有的效应都会影响到Paint填充图像的方式;PathEffect是用来控制绘制轮廓(线条)的方式。
PathEffect对于绘制Path基本图形特别有用,但是它们也可以应用到任何Paint中从而影响线条绘制的方式。
使用PathEffect,可以改变一个形状的边角的外观并且控制轮廓的外表。Android包含了多个PathEffect,包括:
CornerPathEffect 可以使用圆角来代替尖锐的角从而对基本图形的形状尖锐的边角进行平滑。
DashPathEffect 可以使用DashPathEffect来创建一个虚线的轮廓(短横线/小圆点),而不是使用实线。你还可以指定任意的虚/实线段的重复模式。
DiscretePathEffect 与DashPathEffect相似,但是添加了随机性。当绘制它的时候,需要指定每一段的长度和与原始路径的偏离度。
PathDashPathEffect 这种效果可以定义一个新的形状(路径)并将其用作原始路径的轮廓标记。
下面的效果可以在一个Paint中组合使用多个Path Effect。
SumPathEffect 顺序地在一条路径中添加两种效果,这样每一种效果都可以应用到原始路径中,而且两种结果可以结合起来。
ComposePathEffect 将两种效果组合起来应用,先使用第一种效果,然后在这种效果的基础上应用第二种效果。
对象形状的PathEffect的改变会影响到形状的区域。这就能够保证应用到相同形状的填充效果将会绘制到新的边界中。
使用setPathEffect方法可以把PathEffect应用到Paint对象中,如下所示:
java代码:
borderPaint.setPathEffect(new CornerPathEffect(5));
PathEffect API示例给出了如何应用每一种效果的指导说明。
修改Xfermode
可以通过修改Paint的Xfermode来影响在Canvas已有的图像上面绘制新的颜色的方式。
在正常的情况下,在已有的图像上绘图将会在其上面添加一层新的形状。如果新的Paint是完全不透明的,那么它将完全遮挡住下面的Paint;如果它是部分透明的,那么它将会被染上下面的颜色。
下面的Xfermode子类可以改变这种行为:
AvoidXfermode 指定了一个颜色和容差,强制Paint避免在它上面绘图(或者只在它上面绘图)。
PixelXorXfermode 当覆盖已有的颜色时,应用一个简单的像素XOR操作。
PorterDuffXfermode 这是一个非常强大的转换模式,使用它,可以使用图像合成的16条Porter-Duff规则的任意一条来控制Paint如何与已有的Canvas图像进行交互。
要应用转换模式,可以使用setXferMode方法,如下所示:
java代码:
AvoidXfermode avoid = new AvoidXfermode(Color.BLUE, 10, AvoidXfermode.Mode. AVOID); borderPen.setXfermode(avoid);
3. 使用抗锯齿效果提高Paint质量
在绘制一个新的Paint对象时,可以通过传递给它一些标记来影响它被渲染的方式。ANTI_ALIAS_FLAG是其中一种很有趣的标记,它可以保证在绘制斜线的时候使用抗锯齿效果来平滑该斜线的外观。
在绘制文本的时候,抗锯齿效果尤为重要,因为经过抗锯齿效果处理之后的文本非常容易阅读。要创建更加平滑的文本效果,可以应用SUBPIXEL_TEXT_FLAG,它将会应用子像素抗锯齿效果。
也可以手工地使用setSubpixelText和setAntiAlias方法来设置这些标记,如下所示:
java代码:
myPaint.setSubpixelText(true);
myPaint.setAntiAlias(true);
2D图形的硬件加速
在当前这个到处都是2D图形爱好者的时代,Android允许你使用硬件加速来渲染你的应用程序。
如果设备可以使用硬件加速,那么通过设置这个标记可以让活动中的每一个View都能使用硬件渲染。尽管减少了系统处理程序的负载,但在极大地提高了图像处理速度的同时,硬件加速也带来了相应的负面效果。
使用requestWindowFeature方法,可以在你的活动中应用Window.FEATURE_OPENGL标记来打开硬件加速,如下所示:
java代码:
myActivity.requestWindowFeature(Window.FEATURE_OPENGL);
并不是Android中所有的2D绘图基本图形都被硬件支持(特别是前面描述的大部分PathEffect)。
与此同时,由于整个活动实际上是作为一个Canvas进行渲染的,所以对任何View的无效请求都将会导致整个活动被重新绘制。
Canvas绘图最佳实践经验
2D自绘操作是非常耗费处理程序资源的;低效的绘图方法会阻塞GUI线程,并且会对应用程序的响应造成不利的影响。对于那些只有一个处理程序的资源受限的环境来说,这一点就更加现实了。
这里需要注意onDraw方法的资源消耗以及CPU周期的耗费,这样才能保证不会把一个看起来很吸引人的应用程序变得完全没有响应。
目前有很多技术可以帮助将与自绘控件相关的资源消耗最小化。我们关心的不是一般的原则,而是某些Android特定的注意事项,从而保证你可以创建外观时尚、而且能够保持交互的活动(注意,以下这个列表并不完整):
考虑硬件加速 OpenGL硬件加速对2D图形的支持是非常好的,所以你总是应该考虑它是否适合你的活动。另一种比较优秀的方法是只用一个单独的View和迅速的、耗时的更新来组成活动。一定要保证你使用的基本图形能够被硬件支持。
考虑大小和方向 当在设计View和布局的时候,一定要保证考虑(和测试)它们在不同的分辨率和大小下的外观。
只创建一次静态对象 在Android中对象的创建是相当昂贵的。因此,在可能的地方,应用只创建一次像Paint对象、Path和Shader这样的绘图对象,而不是在View每次无效的时候都重新创建它们。
记住onDraw是很消耗资源的 执行onDraw方法是很消耗资源的处理,它会强制Android执行多个图片组合和位图构建操作。下面有几点建议可以让你修改Canvas的外观,而不用重新绘制它:
使用Canvas转换 可以使用像rotate和translate这样的转换,来简化Canvas中元素复杂的相关位置。例如,相比放置和旋转一个表盘周围的每一个文本元素,你可以简单地将canvas旋转22.5?,然后在相同的位置绘制文本。
使用动画 可以考虑使用动画来执行View的预设置的转换,而不是手动地重新绘制它。在活动的View中可以执行缩放、旋转和转换动画,并可以提供一种能够有效利用资源的方式来提供缩放、旋转或者抖动效果。
考虑使用位图和9 Patch 如果View使用了静态背景,那么你应该考虑使用一个图片,如位图或者9 patch,而不是手动地重新绘制。
高级指南针表盘的例子
已经创建了一个简单的指南针。而在上一章,你又回到了这个例子,对它进行了扩展从而使它够使用加速计硬件来显示横向和纵向方向。
那些例子中的UI都很简单,从而保证了那些章节中的代码都尽可能地清晰。
在下面的例子中,将对CompassView的onDraw方法做一些重要的改动,从而把它从一个简单的、平面的指南针,变成一个动态的航空地平仪(artificial horizon ),如图所示。
由于上面的图片是黑白的,所以需要实际动手创建这个控件来看到完全的效果。
(1) 首先,通过修改colors.xml资源文件来包含边界、表盘阴影以及天空和地面的颜色值。同时还要更新边界和盘面标记所使用的颜色。
java代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="text_color">#FFFF</color>
<color name="background_color">#F000</color>
<color name="marker_color">#FFFF</color>
<color name="shadow_color">#7AAA</color>
<color name="outer_border">#FF444444</color>
<color name="inner_border_one">#FF323232</color>
<color name="inner_border_two">#FF414141</color>
<color name="inner_border">#FFFFFFFF</color>
<color name="horizon_sky_from">#FFA52A2A</color>
<color name="horizon_sky_to">#FFFFC125</color>
<color name="horizon_ground_from">#FF5F9EA0</color>
<color name="horizon_ground_to">#FF00008B</color>
</resources>
(2) 用作航空地平仪的天空和地面的Paint和Shader对象是根据当前View的大小创建的,所以它们不能像你在创建的Paint对象那样,是静态的。因此,不再创建Paint对象,取而代之的是构造它们所使用的渐变数组和颜色。
java代码:
int[] borderGradientColors;
float[] borderGradientPositions;
int[] glassGradientColors;
float[] glassGradientPositions;
int skyHorizonColorFrom;
int skyHorizonColorTo;
int groundHorizonColorFrom;
int groundHorizonColorTo;
(3) 更新CompassView的initCompassView方法,来使用第(1)步中所创建的资源来初始化第(2)步中所创建的变量。现存的方法代码大部分可以保留,而只需要对textPaint、circlePaint和markerPaint变量做些许改动,如下所示:
java代码:
protected void initCompassView() {
setFocusable(true);
// 获得外部资源
Resources r = this.getResources();
circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
circlePaint.setColor(R.color.background_color);
circlePaint.setStrokeWidth(1);
circlePaint.setStyle(Paint.Style.STROKE);
northString = r.getString(R.string.cardinal_north);
eastString = r.getString(R.string.cardinal_east);
southString = r.getString(R.string.cardinal_south);
westString = r.getString(R.string.cardinal_west);
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(r.getColor(R.color.text_color));
textPaint.setFakeBoldText(true);
textPaint.setSubpixelText(true);
textPaint.setTextAlign(Align.LEFT);
textHeight = (int)textPaint.measureText("yY");
markerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
markerPaint.setColor(r.getColor(R.color.marker_color));
markerPaint.setAlpha(200);
markerPaint.setStrokeWidth(1);
markerPaint.setStyle(Paint.Style.STROKE);
markerPaint.setShadowLayer(2, 1, 1, r.getColor(R.color.shadow_color));
a. 创建径向Shader用来绘制外边界所使用的颜色和位置数组。
java代码:
borderGradientColors = new int[4];
borderGradientPositions = new float[4];
borderGradientColors[3] = r.getColor(R.color.outer_border);
borderGradientColors[2] = r.getColor(R.color.inner_border_one);
borderGradientColors[1] = r.getColor(R.color.inner_border_two);
borderGradientColors[0] = r.getColor(R.color.inner_border);
borderGradientPositions[3] = 0.0f;
borderGradientPositions[2] = 1-0.03f;
borderGradientPositions[1] = 1-0.06f;
b. 现在创建径向渐变的颜色和位置数组,它们将用来创建半透明的"glass dome"(玻璃圆顶),它放置在View的上面,从而使人产生深度的幻觉。
java代码:
glassGradientColors = new int[5];
glassGradientPositions = new float[5];
int glassColor = 245;
glassGradientColors[4]=Color.argb(65,glassColor,glassColor, glassColor);
glassGradientColors[3]=Color.argb(100,glassColor,glassColor,glassColor);
glassGradientColors[2]=Color.argb(50,glassColor,glassColor, glassColor);
glassGradientColors[1]=Color.argb(0,glassColor,glassColor, glassColor);
glassGradientColors[0]=Color.argb(0,glassColor,glassColor, glassColor);
glassGradientPositions[4] = 1-0.0f;
glassGradientPositions[3] = 1-0.06f;
glassGradientPositions[2] = 1-0.10f;
glassGradientPositions[1] = 1-0.20f;
glassGradientPositions[0] = 1-1.0f;
c. 最后,获得创建线性颜色渐变所使用的颜色,它们将用来表示航空地平仪中的天空和地面。
java代码:
skyHorizonColorFrom = r.getColor(R.color.horizon_sky_from);
skyHorizonColorTo = r.getColor(R.color.horizon_sky_to);
groundHorizonColorFrom = r.getColor(R.color.horizon_ground_from);
groundHorizonColorTo = r.getColor(R.color.horizon_ground_to);
Path skyPath = new Path();
skyPath.addArc(innerBoundingBox, -tiltDegree, (180 + (2 * tiltDegree)));
canvas.rotate(-rollDegree, px, py);
canvas.drawOval(innerBoundingBox, groundPaint);
canvas.drawPath(skyPath, skyPaint);
canvas.drawPath(skyPath, markerPaint);
int markWidth = radius / 3; int startX = center.x - markWidth; int endX = center.x + markWidth;
double h = innerRadius*Math.cos(Math.toRadians(90-tiltDegree)); double justTiltY = center.y - h;
float pxPerDegree = (innerBoundingBox.height()/2)/45f;
for (int i = 90; i >= -90; i -= 10) {
double ypos = justTiltY + i*pxPerDegree;
// 只显示内表盘的刻度
if ((ypos < (innerBoundingBox.top + textHeight)) || (ypos > innerBoundingBox.bottom - textHeight)) continue;
// 为每一个刻度增加画一个直线和一个倾斜角
canvas.drawLine(startX, (float)ypos, endX, (float)ypos, markerPaint);
t displayPos = (int)(tiltDegree - i);
String displayString = String.valueOf(displayPos);
float stringSizeWidth = textPaint.measureText(displayString);
canvas.drawText(displayString, (int)(center.x-stringSizeWidth/2), (int)(ypos)+1, textPaint);
}
markerPaint.setStrokeWidth(2);
canvas.drawLine(center.x - radius / 2, (float)justTiltY, center.x + radius / 2, (float)justTiltY, markerPaint);
markerPaint.setStrokeWidth(1);
// 绘制箭头
Path rollArrow = new Path();
rollArrow.moveTo(center.x - 3, (int)innerBoundingBox.top + 14);
rollArrow.lineTo(center.x, (int)innerBoundingBox.top + 10);
rollArrow.moveTo(center.x + 3, innerBoundingBox.top + 14);
rollArrow.lineTo(center.x, innerBoundingBox.top + 10);
canvas.drawPath(rollArrow, markerPaint);
// 绘制字符串
String rollText = String.valueOf(rollDegree);
double rollTextWidth = textPaint.measureText(rollText);
canvas.drawText(rollText, (float)(center.x - rollTextWidth / 2), innerBoundingBox.top + textHeight + 2, textPaint);
canvas.restore();
canvas.save();
canvas.rotate(180, center.x, center.y);
for (int i = -180; i < 180; i += 10) {
// 每30度显示一个数字值
if (i % 30 == 0) {
String rollString = String.valueOf(i*-1);
float rollStringWidth = textPaint.measureText(rollString);
PointF rollStringCenter = new PointF(center.x-rollStringWidth / 2, innerBoundingBox.top+1+textHeight);
canvas.drawText(rollString, rollStringCenter.x, rollStringCenter.y, textPaint);
}
// 否则,绘制一个标记直线
else { canvas.drawLine(center.x, (int)innerBoundingBox.top, center.x, (int)innerBoundingBox.top + 5, markerPaint);
}
canvas.rotate(10, center.x, center.y);
}
canvas.restore();
【功能说明】该方法用于在画布上绘制直线,通过指定直线的两个端点坐标来绘制。该方法只能绘制单条直线;如果需要同时绘制多条直线,则可以使用drawLines方法。
【基本语法】public void drawLine (float startX, float startY, float stopX, float stopY, Paint paint)
参数说明
startX:起始端点的X坐标。
startY:起始端点的Y坐标。
stopX:终止端点的X坐标。
stopY:终止端点的Y坐标。
paint:绘制直线所使用的画笔。
【实例演示】下面通过代码来演示如何在画布上绘制直线。
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
mPaint.setColor(Color.BLACK); //设置画笔颜色
canvas.drawColor(Color.WHITE); //设置背景颜色
mPaint.setStrokeWidth((float) 1.0); //设置线宽
canvas.drawLine(50, 50, 450, 50, mPaint); //绘制直线
mPaint.setStrokeWidth((float) 5.0); //设置线宽
canvas.drawLine(50, 150, 450, 150, mPaint); //绘制直线
mPaint.setStrokeWidth((float) 10.0); //设置线宽
canvas.drawLine(50, 250, 450, 250, mPaint); //绘制直线
mPaint.setStrokeWidth((float) 15.0); //设置线宽
canvas.drawLine(50, 350, 450, 350, mPaint); //绘制直线
mPaint.setStrokeWidth((float) 20.0); //设置线宽
canvas.drawLine(50, 450, 450, 450, mPaint); //绘制直线
}