2.8 Android图形界面
前面介绍了Android应用界面(Android UI)的相关内容,不过对于一些游戏应用来说,这些UI控件往往派不上用场。此外,一些特殊的Android应用也有可能会使用到比较底层的图形类库,因此,本节我们就来学习Android的图形系统。
Android系统中的图形大致可以分为2D图形和3D图形两类,2D图形的类库在android.graphics包下,本节将会重点介绍;3D图形的类库在android.opengl包下,由于这部分内容和游戏开发关系比较紧密,这部分内容将被放在本书第13章中介绍,感兴趣的朋友可以提前参考13.1.4节中的内容。
2.8.1 画笔(Paint)
首先,让我们来想象一下,当我们绘画的时候,最重要的两样东西是什么?答案应该没有什么悬念,那就是画笔和画布。实际上,在Android系统中绘制图形的原理是相同的,我们同样需要先使用程序构造一把画笔(Paint),然后在画布(Canvas)上进行绘画。
Android系统中的画笔类,即android.graphics包下的Paint类,该类包含了一系列的方法与属性,用于构造绘制图形用的画笔。我们把常用的方法归纳到表2-5中。
表2-5 画笔类常用方法
以上方法常用于画笔初始化的配置逻辑中,接下来让我们来学习Paint画笔类的使用范例,如参考代码清单2-28所示。
代码清单 2-28
public class TestPaintView extends View { ... private Paint mPaint = new Paint(); ... public void onDraw(Canvas canvas) { super.onDraw(canvas); // 设置画笔 mPaint.setAntiAlias(true); mPaint.setColor(Color.RED); mPaint.setAlpha(200); mPaint.setStyle(Paint.Style.FILL); // 绘制矩形 canvas.drawRect(100, 100, 150, 150, mPaint); } ... }
以上视图类TestPaintView继承自View基类,主要的绘制逻辑在onDraw方法中,即使用定制好的实心画笔绘制一个红色的矩形,这里我们可以学习到使用Paint画笔类的正确方法。此外,我们还需要注意,这里在使用setColor方法设置画笔颜色的时候,用到了Color类的预定义颜色常量,我们将这些常用的颜色常量归纳到表2-6中。
表2-6 画笔类颜色常量
2.8.2 画布(Canvas)
设置好画笔和颜色,就可以开始在画布上绘画了,这时我们就需要用到画布类,即Canvas类。该类包含了一系列的方法与属性,用于设置画布的外观,我们把常用的方法归纳到表2-7中。
Canvas类中常用绘制方法的用法比较简单,Android系统已经在View类的onDraw方法中默认传入了canvas对象,我们可以根据需要使用不同的draw方法绘制出不同的图形。比如,代码清单2-29中就使用了drawRect方法绘制了一个矩形。
表2-7 画布类常用方法
然而,游戏应用的画布中通常不只有一个图形,通常需要对其中的某些图形进行特殊处理,比如旋转、变形等,此时需要先使用save方法来保存画布,图形处理完毕之后再调用restore方法来重置、重绘,使用范例如代码清单2-29所示。
代码清单 2-29
public class TestCanvasView extends View { ... private Paint mPaint = new Paint(); ... public void onDraw(Canvas canvas) { super.onDraw(canvas); // 设置画布颜色 canvas.drawColor(Color.BLACK); // 设置画笔 mPaint.setAntiAlias(true); // 剪裁画布 canvas.clipRect(0, 0, 200, 200); // 保存画布 canvas.save(); // 绘制一个矩形 canvas.rotate(10.0f); mPaint.setColor(Color.RED); canvas.drawRect(100, 100, 150, 150, mPaint); // 重置画布 canvas.restore(); // 绘制另一个矩形 mPaint.setColor(Color.BLUE); canvas.drawRect(100, 0, 200, 100, mPaint); } ... }
以上程序绘制了两个矩形。其中,红色的矩形绕着屏幕左上方的顶点顺时间旋转了10°。这里涉及Canvas画布坐标系的知识,我们将在2.8.3节中介绍。另外,我们还可以学习到如何对Canvas画布进行设置、保存、旋转、重置等一系列的操控过程。学习了以上Paint和Canvas类的编程技巧之后,开发者就可以在Android应用和游戏中方便地绘图了。
2.8.3 基础几何图形
前面我们已经学习了画笔(Paint)和画布(Canvas)的基础知识,接下来我们就可以使用这些工具来画图了。实际上,在前面的代码范例中,我们已经介绍了如何使用Canvas对象的drawRect方法来绘制矩形,但是大家可能还不清楚方法中参数值的含义,因此我们先来熟悉Canvas画布的坐标系,如图2-12所示。
图2-12 Canvas坐标系
从以上的坐标系示意图中,我们可以看出以下几个要点。其一,Canvas画布的坐标原点位于整张画布的左上方,点坐标为“(0,0)”;其二,屏幕横向的是X轴,纵向的是Y轴,屏幕内的点坐标都是正数;其三,以矩形为例,我们可以看到绘图方法(drawRect)中的left、top、right、bottom等参数的含义,其他方法中的类似参数的含义都可以依此类推。
另外,在使用Canvas进行绘图的时候还要注意,画布是按照程序逻辑的先后顺序进行渲染的,因此底部图形的渲染逻辑放在前面,渲染逻辑在后面的图形则会层层覆盖上去,使用范例请参考代码清单2-30。
代码清单 2-30
public class TestGraphicsView extends View { ... private Paint mPaint = new Paint(); ... public void onDraw(Canvas canvas) { super.onDraw(canvas); // 设置画布颜色 canvas.drawColor(Color.BLACK); // 设置画笔 mPaint.setAntiAlias(true); // 画圆形 mPaint.setColor(Color.YELLOW); canvas.drawCircle(160, 160, 120, mPaint); // 画矩形 mPaint.setColor(Color.RED); canvas.drawRect(80, 80, 240, 240, mPaint); // 画椭圆 mPaint.setColor(Color.GREEN); RectF rectf = new RectF(); rectf.left = 90; rectf.top = 100; rectf.right = 230; rectf.bottom = 220; canvas.drawOval(rectf, mPaint); // 画多边形 Path path = new Path(); path.moveTo(160, 110); path.lineTo(160-40, 110+80); path.lineTo(160+40, 110+80); path.close(); mPaint.setColor(Color.BLUE); canvas.drawPath(path, mPaint); ... } ... }
在上述代码中,TestGraphicsView类的onDraw方法中依次绘制了圆形、矩形、椭圆和多边形,运行结果如图2-13所示,我们可以很清楚地看到这些基础几何图形的显示效果以及图形渲染的先后顺序。
图2-13 基础几何图形
基础几何图形的绘制是Android图形系统的基础知识。在此基础之上,我们可以把Android UI控件结合到一起,开发出丰富多彩的应用UI界面。当然,我们还可以运用View控件的刷新机制完成一些简单的图形动画,相关内容将在2.8.4节中介绍。
2.8.4 常见图形变换
常见的图形变换包括位移、旋转、缩放、倾斜等,其中,位移变换在开发者掌握了画布坐标系等基础概念的情况下,实现起来是比较简单的;然而,旋转、缩放以及倾斜变换则涉及变换矩阵(Matrix)的概念,这里需要特别解释一下。
Android系统中的变换矩阵实际上是一个3×3的矩阵,专门用于控制图形变换,矩阵中的每个数值都有其特定的含义。Android SDK中的Matrix类位于android.graphics包下,我们可以通过setValue方法直接设置旋转矩阵的二维数组,但是这种用法比较难懂,更简单的用法是使用Matrix类提供的方法来控制旋转矩阵,比如setRotate方法就用于设定旋转的角度。代码清单2-31就展示了Matrix类的用法。
代码清单 2-31
public class TestImageView extends View implements Runnable { private Bitmap star = null; private int starWidth = 0; private int starHeight = 0; private float starAngle = 0.0f; private Matrix starMatrix = new Matrix(); public TestImageView(Context context) { super(context); // 加载资源 Resources res = this.getResources(); star = BitmapFactory.decodeResource(res, R.drawable.star); // 获取原始图片宽高 starWidth = star.getWidth(); starHeight = star.getHeight(); // 开始重绘视图 new Thread(this).start(); } public void onDraw(Canvas canvas) { super.onDraw(canvas); // 重置旋转矩阵 starMatrix.reset(); // 设置旋转角度 starMatrix.setRotate(starAngle); // 重绘旋转的图形 Bitmap starBitmap = Bitmap.createBitmap(star, 0, 0, starWidth, starHeight, starMatrix, true); canvas.drawBitmap(starBitmap, 0, 0, null); } @Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { Thread.sleep(100); starAngle++; // 旋转角度 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 通知主线程更新图像 this.postInvalidate(); } } }
上述代码中的TestImageView类是一个完整的重绘画布视图的例子。首先,该类继承自View基类,同时还包含了一个线程类的run方法,在该方法的逻辑中,每100ms进行一次重绘,即调用postInvalidate方法通知主线程更新图像。其次,在TestImageView类的构造方法中,主要包含了资源初始化的逻辑,这里程序加载了一个五星形状的图像资源文件。另外,在onDraw方法中,我们可以看到starMatrix变换矩阵的常见用法之一,即通过setRotate方法设置旋转的角度。该程序最终的运行效果,就是画出了一个绕着屏幕左上方顺时针旋转的五角星,如图2-14所示。
当然,我们还可以让图像绕着某个中心点旋转,这也不是问题,我们只需要对onDraw方法的逻辑稍做修改即可,修改过的逻辑实现如代码清单2-32所示。
代码清单 2-32
... public void onDraw(Canvas canvas) { super.onDraw(canvas); // 重置旋转矩阵 starMatrix.reset(); // 设置旋转中心 float transX = 100; float transY = 100; float pivotX = starWidth/2; float pivotY = starHeight/2; starMatrix.setRotate(starAngle, pivotX, pivotY); starMatrix.postTranslate(transX, transY); // 重绘旋转的图形 canvas.drawBitmap(star, starMatrix, null); } ...
要让图形绕着其中心旋转,首先要使用setRotate方法设置图形的旋转中心,然后再使用postTranslate方法把图形平移到相应的位置,即坐标(transX,transY)。该实例的运行效果如图2-15所示,我们可以看到屏幕上出现了一个不断自转的五角星。
图2-14 旋转的五角星
图2-15 自转的五角星
当然,除了旋转之外,常见的图形变换还包括大小变换、倾斜变换等,限于篇幅,这里就不做介绍了,有兴趣的读者可以参考Matrix类文档中的preScale、postScale、preSkew、postSkew等方法。这里我们还需要注意的是pre和post系列方法的区别,带有pre前缀的方法表示此变换逻辑需要应用在所有变换逻辑之前,而带有post前缀的方法则表示此变换逻辑会依次往后排列,因此代码清单2-28中的旋转逻辑也可以使用代码清单2-33中的代码替代。
代码清单 2-33
... public void onDraw(Canvas canvas) { ... starMatrix.setTranslate(transX, transY); starMatrix.preRotate(starAngle, pivotX, pivotY); ... } ...