我们已经介绍了Canvas,在那里,已经学习了如何创建自己的View。在第7章中也使用了Canvas来为MapView标注覆盖。
画布(Canvas)是图形编程中一个很普通的概念,通常由三个基本的绘图组件组成:Canvas 提供了绘图方法,可以向底层的位图绘制基本图形。Paint 也称为"刷子",Paint可以指定如何将基本图形绘制到位图上。Bitmap 绘图的表面。Android绘图API支持透明度、渐变填充、圆边矩形和抗锯齿。遗憾的是,由于资源限制,它还不支持矢量图形,它使用的是传统光栅样式的重新绘图。这种光栅方法的结果是提高了效率,但是改变一个Paint对象不会影响已经画好的基本图形,它只会影响新的元素。提示:如果你拥有Windows开发背景,那么Android的2D绘图能力大致相当于GDI+的能力。1. 可以画什么?Canvas类封装了用作绘图表面的位图;它还提供了draw*方法来实现设计。下面的列表提供了对可用的基本图形的简要说明,但并没有深入地探讨每一个draw方法的详细内容:drawARGB / drawRGB / drawColor 使用单一的颜色填充画布。drawArc 在一个矩形区域的两个角之间绘制一个弧。drawBitmap 在画布上绘制一个位图。可以通过指定目标大小或者使用一个矩阵来改变目标位图的外观。drawBitmapMesh 使用一个mesh(网)来绘制一个位图,它可以通过移动网中的点来操作目标的外观。drawCircle 以给定的点为圆心,绘制一个指定半径的圆。drawLine(s) 在两个点之间画一条(多条)直线。drawOval 以指定的矩形为边界,画一个椭圆。drawPaint 使用指定的Paint填充整个CanvasdrawPath 绘制指定的Path。Path对象经常用来保存一个对象中基本图形的集合。drawPicture 在指定的矩形中绘制一个Picture对象。drawPosText 绘制指定了每一个字符的偏移量的文本字符串。drawRect 绘制一个矩形。drawRoundRect 绘制一个圆角矩形。drawText 在Canvas上绘制一个文本串。文本的字体、大小和渲染属性都设置在用来渲染文本的Paint对象中。drawTextOnPath 在一个指定的path上绘制文本。drawVertices 绘制一系列三角形面片,通过一系列顶点来指定它们。这些绘图方法中的每一个都需要指定一个Paint对象来渲染它。在下面的部分中,将学习如何创建和修改Paint对象,从而在绘图中完成大部分工作。2. 从Paint中完成工作Paint类相当于一个笔刷和调色板。它可以选择如何使用上面描述的draw方法来渲染绘制在画布上的基本图形。通过修改Paint对象,可以在绘图的时候控制颜色、样式、字体和特殊效果。最简单地,setColor可以让你选择一个Paint的颜色,而Paint对象的样式(使用setStyle控制)则可以决定是绘制绘图对象的轮廓(STROKE),还是只填充每一部分(FILL),或者是两者都做(STROKE_AND_FILL)除了这些简单的控制之外,Paint类还支持透明度,另外,它也可以通过使用各种各样的阴影、过滤器和效果进行修改,从而提供由更丰富的、复杂的画笔和颜料组成的调色板。Android SDK包含了一些非常好的实例,它们说明了Paint类中可用的大部分功能。你可以在API demos的graphics子目录中找到它们:sdk root folder]\samples\ApiDemos\src\com\android\samples\graphics在下面的部分中,将学习和使用其中的部分功能。这些部分只是简单地罗列了它们能实现的效果(例如渐变和边缘浮雕),而没有详细地列出所有可能的情况。使用透明度Android中的所有颜色都包含了一个不透明组件(alpha通道)。当创建一个颜色的时候,可以使用argb或者parseColor方法来定义它的alpha值,如下所示:Java代码:- // 使用红色,并让它50%透明
- int opacity = 127;
- int intColor = Color.argb(opacity, 255, 0, 0);
- int parsedColor = Color.parseColor("#7FFF0000");
或者,也可以使用setAlpha方法来设置已存在的Paint对象的透明度:
Java代码:- // 让颜色50%透明
- int opacity = 127;
- myPaint.setAlpha(opacity);
创建一个不是100%透明的颜色意味着,使用它绘制的任何基本图形都将是部分透明的--也就是说,在它下面绘制的所有基本图形都是部分可见的。可以在任何使用了颜色的类或者方法中使用透明效果,包括Paint、Shader和Mask Filter。Shader介绍Shader类的派生类可以创建允许使用多种固体颜色填充绘图对象的Paint。对Shader最常见的使用是定义渐变填充;渐变是在2D图像中添加深度和纹理的最佳方式之一。Android包含了一个Bitmap Shader和一个Compose Shader,同时,还包含了三个渐变的Shader。试图用语言来描述绘图的效果本来就是没有意义的,所以看一下图就应该可以知道每一种Shader是如何工作的。图中从左到右依次代表的是LinearGradient、RadialGradient和 SweepGradient.提示:没有包含的是ComposerShader,它可以创建多个Shader和BitmapShader的组合,从而可以在一个位图图像的基础上创建一个绘图刷。要在绘图的时候使用一个Shader,可以使用setShader方法将其应用到一个Paint中,如下面的代码所示:Paint shaderPaint = new Paint(); shaderPaint.setShader(myLinearGradient);你使用这个Paint所绘制的任何东西都将使用你指定的Shader进行填充,而不是使用Paint本身的颜色进行填充。定义渐变Shader如上所示,使用渐变Shader可以让你使用交替改变的颜色来填充图片;你可以将颜色渐变定义为两种颜色的简单交替,如下所示:java代码:
- int colorFrom = Color.BLACK;
- int colorTo = Color.WHITE;
- LinearGradient linearGradientShader = new LinearGradient(x1, y1, x2, y2, colorFrom, colorTo, TileMode.CLAMP);
或者,你还可以定义更复杂的按照设定比例进行分布的颜色序列,如下面的RadialGradientShader例子所示:java代码:
- 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);
每一种渐变Shader(线性的、辐射形的和扫描状的)都可以使用以上这两种技术来定义渐变填充。使用Shader TileModes渐变Shader的画刷大小既可以显式地使用有边界的矩形来定义,也可以使用中心点和半径长度来定义。Bitmap Shader可以通过它的位图大小来决定它的画刷大小。如果Shader画刷所定义的区域比要填充的区域小,那么TileMode将会决定如何处理剩余的区域:CLAMP 使用Shader的边界颜色来填充剩余的空间。MIRROR 在水平和垂直方向上拉伸Shader图像,这样每一个图像就都能与上一个缝合了。REPEAT 在水平和垂直方向上重复Shader图像,但不拉伸它。使用MaskFilterMaskFilter类可以为Paint分配边缘效果。对MaskFilter的扩展可以对一个Paint边缘的alpha通道应用转换。Android包含了下面几种MaskFilter:BlurMaskFilter 指定了一个模糊的样式和半径来处理Paint的边缘。EmbossMaskFilter 指定了光源的方向和环境光强度来添加浮雕效果。要应用一个MaskFilter,可以使用setMaskFilter方法,并传递给它一个MaskFilter对象。下面的例子是对一个已经存在的Paint应用一个EmbossMaskFilter:java代码:
- // 设置光源的方向
- 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);
DashPathEffect 可以使用DashPathEffect来创建一个虚线的轮廓(短横线/小圆点),而不是使用实线。你还可以指定任意的虚/实线段的重复模式。
DiscretePathEffect 与DashPathEffect相似,但是添加了随机性。当绘制它的时候,需要指定每一段的长度和与原始路径的偏离度。PathDashPathEffect 这种效果可以定义一个新的形状(路径)并将其用作原始路径的轮廓标记。下面的效果可以在一个Paint中组合使用多个Path Effect。SumPathEffect 顺序地在一条路径中添加两种效果,这样每一种效果都可以应用到原始路径中,而且两种结果可以结合起来。ComposePathEffect 将两种效果组合起来应用,先使用第一种效果,然后在这种效果的基础上应用第二种效果。对象形状的PathEffect的改变会影响到形状的区域。这就能够保证应用到相同形状的填充效果将会绘制到新的边界中。使用setPathEffect方法可以把PathEffect应用到Paint对象中,如下所示:java代码:- borderPaint.setPathEffect(new CornerPathEffect(5));
- AvoidXfermode avoid = new AvoidXfermode(Color.BLUE, 10, AvoidXfermode.Mode. AVOID); borderPen.setXfermode(avoid);
- myPaint.setSubpixelText(true);
- myPaint.setAntiAlias(true);
- myActivity.requestWindowFeature(Window.FEATURE_OPENGL);
高级指南针表盘的例子已经创建了一个简单的指南针。而在上一章,你又回到了这个例子,对它进行了扩展从而使它够使用加速计硬件来显示横向和纵向方向。
那些例子中的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));
- 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;
- 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;
- 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);
(6) 创建用来填充圆的每个部分(地面和天空)的路径。每一部分的比例应该与形式化之后的俯仰值有关。 java代码: 复制代码 (7) 将Canvas围绕圆心,按照与当前翻转角相反的方向进行旋转,并且使用在第(4)步中所创建的Paint来绘制天空和地面路径。 java代码: 复制代码 (8) 接下来是盘面标记,首先计算水平的水平标记的起止点。 java代码: 复制代码 (9) 要让水平值更易于读取,应该保证俯仰角刻度总是从当前值开始。下面的代码计算了天空和地面的接口在水平面上的位置: java代码: 复制代码 (10) 找到表示每一个倾斜角的像素的数目。 java代码: 复制代码 (11) 现在遍历180度,以当前的倾斜值为中心,给出一个可能的俯仰角的滑动刻度。 java代码: 复制代码 (12) 现在,在大地/天空接口处绘制一条更粗的线。在画线之前,改变markerPaint对象的线条粗度(然后把它设置回以前的值)。 java代码: 复制代码 (13) 要让用户能够更容易地读取精确的翻转值,应该画一个箭头,并显示一个文本字符串来表示精确值。 创建一个新的Path,并使用moveTo/lineTo方法构建一个开放的箭头,它指向直线的前方。然后绘制路径和一个文本字符串来展示当前的翻转。 java代码: 复制代码 (14) 将Canvas旋转到正上方,这样就可以绘制其他的盘面标记了。 java代码: 复制代码 (15) 每次将Canvas旋转10度,然后画一个标记或者一个值,直到画完翻转值表盘为止。当完成表盘之后,把Canvas恢复为正上方的方向。 java代码:
- 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();