1.前言
安卓框架提供了一组2D绘图的APIs,允许在画布上渲染自定义图形或者修改已存在的View来定义外观和感觉。绘制2D图形通常有两种途径:
a.给布局中的View对象绘制图形和动画。由系统的视图层次结构处理绘制过程,你只需定义视图内的图形。
b.直接将图形绘制到画布上。你自己调用适当类的 onDraw() 方法(传入画布)或者Canvas对象的 draw...() 方法(就像 drawPicture() 方法),这样也可以控制任何动画。
选项“a”,当绘制的是不需要动态变化的简单图形和非性能密集型游戏的一部分时,在视图上绘制是最好的选择。例如,显示静态图形或预定义的动画。详细信息看图形这一章节。
选项“b”,当应用需要经常重绘时,在画布上绘制是较好的选择。例如,电子游戏等。有两种方式去实现:
- 与UI Activity同一线程时,给布局创建自定义视图组件,需要调用 invalidate() 方法和处理 onDraw() 方法的回调。
- 一个单独的线程时,管理一个SurfaceView,在画布上以线程支持的最快速度绘制(不需要请求 invalidate())。
2.使用画布绘制
写程序时,若希望执行专门的绘图和/或控制图形的动画,应该在画布上绘制。画布只是表面的封装,负责所有 draw...() 方法的调用,图形传递给实际的Surface上,摆放在窗口中的底层的位图。
如果在 onDraw() 回调方法内绘制,调用提供的Canvas的 draw...() 方法即可。处理SurfaceView对象时,通过 SurfaceHolder.lockCanvas() 方法获取Canvas对象。(这两种情况在下面的章节中都有讨论)如果需要创建新的Canvas对象,必须定义实际执行绘制的Bitmap对象。画布总是需要位图配合,创建新画布如下:
Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
现在画布将在给定的位图上绘制,绘制完后,通过 drawBitmap(Bitmap,...) 方法可将位图带给另外画布。建议在 View.onDraw() 或 SurfaceHolder.lockCanvas() 方法提供的画布上绘制最终图形(详见下面的章节)。
2.1.绘制在View上
如果你的应用不需要大量的处理或好高的帧速率(也许是象棋游戏,贪吃蛇游戏或者缓慢的动画应用),应该创建自定义视图组件和在 View.onDraw() 方法内用画布绘图。这样做方便的是,安卓系统将提供预定义的Canvas对象来调用 draw...() 方法。
首先,继承View类(或其子类)和定义 onDraw() 回调方法。当视图绘制自己时,安卓系统将调用这个方法,通过传入的Canvas对象执行所有的绘制操作。
安卓系统只会在必要时调用 onDraw() 方法。每当应用准备好绘制时,调用 invalidate() 方法废除当前的视图,同时告知安卓系统调用 onDraw() 方法(不能保证回调是瞬时的)。
在视图组件的 onDraw() 方法内,使用提供的Canvas对象的各种 draw...() 方法或其它类的 draw() 方法(以提供的Canvas对象为参数)完成所有绘图。一旦 onDraw() 方法完成,安卓系统将使用画布绘制位图。
注意:为了从非主线程刷新界面,必须调用 postInvalidate() 方法。
关于扩展View类的更多信息,阅读 read Building Custom Components。
2.2.绘制在SurfaceView上
SurfaceView是View的一个专注于绘图的子类,目的是在应用的子线程中提供绘图功能,那样应用不需要等待系统视图结构的绘制完成。反而,与SurfaceView相关联的子线程可以按照自己的频率在画布上绘制。
首先,需要创建一个类继承SurfaceView,同时实现SurfaceHolder.Callback接口。它可以提供Surface的底层信息,例如,什么时候被创建、被改变或者被销毁。这些信息很重要,可以知道何时开始绘图,是否需要根据新的Surface属性进行调整,何时停止绘图,以及杀死某些任务。在SurfaceView类内部定义子线程,以便在你的画布上执行所有的绘图过程。
对于Surface的操作应该通过SurfaceHolder而不是直接处理。当SurfaceView被初始化后,调用 getHolder() 方法获取SurfaceHolder。通过 addCallback() 方法(参数为this)可以将回调对象传入SurfaceHolder以获取通知,而被回调的方法需在SurfaceView类中重写。
为了在子线程中通过画布绘制,需要将SurfaceHolder对象传进线程,并调用它的 lockCanvas() 方法获取画布,有了画布就可以进行必要的绘制操作了。画完后,调用 unlockCanvasAndPost() 方法解锁和传递画布对象,这样,内容才会显示到画布上。每当要重绘时,先锁定画布再解锁画布,代码看这篇文章。
注意:每次从SurfaceHolder获取画布,画布之前的状态将会被保留。为了正确展示动画,你必须重绘整个Surface。比如,调用 drawColor() 方法填充一种颜色或者调用 drawBitmap() 方法设置一个背景图像来清除画布之前的状态。否则,会在画布上看到之前绘制过的痕迹。
3.图形
安卓提供了一个自定义2D图形库,用于绘制形状和图像。这些在两个维度上绘制的公共类放在 android.graphics.drawable 包中。
本文讨论使用Drawable对象绘制图形的基本知识和如何使用Drawable子类。关于使用图形完成帧动画,详见 Drawable Animation。
图形通常指可以绘制的东西,它的子类定义了各种具体的可绘图形,包括BitmapDrawable,ShapeDrawable,PictureDrawable,LayerDrawable等。当然,也可以继承这些类,来实现自己想要的、具有独特行为的Drawable对象。
有三种方式定义和实例化Drawable对象:使用保存在项目资源中的图像;使用XML文件定义Drawable属性;使用正常的类构造函数。下面,将分别讨论前两种技术(第三种就是代码的调用,功能最全也不难)。
3.1.使用图像资源创建
向应用程序添加图形的简单方法是引用项目资源中的图像文件,支持的文件类型有:PNG(首选),JPG(可接受),GIF(不建议)。这种技术显然更适合应用程序图标、徽标或其它情况(如在游戏中使用)。
使用图像资源,仅仅需要将文件添加到你项目的 res/drawable/ 目录下,再从代码和XML布局中引用。无论哪种方式,都涉及到使用资源ID,一种没有文件类型扩展名的文件名(例如,my_image.png 用my_image引用)。
注意:放在 res/drawable/ 目录下的图像资源可能会被aapt工具在编译过程中使用图像无损压缩进行自动优化,比如,一个真的不需要超过256种颜色的PNG可能会被颜色调色板转换成8位的PNG。在图像质量不变的情况下,可以使用较少的内存。因此,需要认识到,这个目录下的图像二进制文件在编译的过程中会改变。如果计划用位流读取图像转换成位图,最好将图像放到 res/raw/ 目录下,避免被优化。
示例代码(使用图形资源创建ImageView并添加到布局中)
LinearLayout mLinearLayout;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create a LinearLayout in which to add the ImageView
mLinearLayout = new LinearLayout(this);
// Instantiate an ImageView and define its properties
ImageView i = new ImageView(this);
i.setImageResource(R.drawable.my_image);
i.setAdjustViewBounds(true); // set the ImageView bounds to match the Drawable's dimensions
i.setLayoutParams(new Gallery.LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT));
// Add the ImageView to the layout and set the layout as the content view
mLinearLayout.addView(i);
setContentView(mLinearLayout);
}
有些情况下,可能想要用Drawable对象来处理图像资源。那么,通过资源创建Drawable对象如下:
Resources res = mContext.getResources();
Drawable myImage = res.getDrawable(R.drawable.my_image);
项目中每个独特的资源,不管实例化多少个不同的对象,只能维持一种状态。例如,对同一个图像资源实例化两个Drawable对象,改变其中一个对象的一个属性(拿透明度来说),将会影响另一个。所以,处理一个对象的多个资源时,使用补间动画来改变图形比直接操作要好。
示例XML(在XML布局中给ImageView添加图形资源)
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="#55ff0000"
android:src="@drawable/my_image"/>
关于使用项目资源的详细信息,阅读 Resources and Assets。
3.2.使用XML资源创建
若对安卓开发用户界面的原则熟悉,应该清楚在XML中定义对象所固有的能力和灵活性,这种理念从View贯穿到Drawable。如果想创建的Drawable对象不依赖于应用程序代码或者用户交互,那么在XML中定义是个好的选择。即使期望图形随着用户的使用而改变属性,你也应该认识到在XML中定义对象可以随时修改初始化时的属性。
一旦在XML中定义Drawable对象,文件需保存到项目的 res/drawable/ 目录下。然后调用 Resources.getDrawable() 方法,根据XML文件的资源ID获取和初始化对象。
任何支持 inflate() 方法的Drawable子类能够被XML定义和被应用初始化,它们特有的XML属性能够找到对应的对象属性(详见类参考)。
示例
// TransitionDrawable在XML中的使用
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/image_expand">
<item android:drawable="@drawable/image_collapse">
</transition>
// 实例化TransitionDrawable并设置为ImageView的内容
Resources res = mContext.getResources();
TransitionDrawable transition = (TransitionDrawable)res.getDrawable(R.drawable.expand_collapse);
ImageView image = (ImageView) findViewById(R.id.toggle_image);
image.setImageDrawable(transition);
// 运行变换一秒钟
transition.startTransition(1000);
4.形状图片
当想要动态地绘制两个维度的图形时,ShapeDrawable对象将满足需求,可以通过编程的方式绘制原始形状和定义任何样式。
ShapeDrawable继承自Drawable类,可以被当作Drawable对象使用,比如,调用 setBackgroundDrawable() 方法给视图设置背景。当然也可以给自定义视图绘制自己的形状,但记得添加到自己的布局中。因为ShapeDrawable对象有自己的 draw() 方法,所以在View子类的 View.onDraw() 方法中调用ShapeDrawable对象的 draw() 方法。下面是对View类最基本的扩展,绘制一个ShapeDrawable:
public class CustomDrawableView extends View {
private ShapeDrawable mDrawable;
public CustomDrawableView(Context context) {
super(context);
int x = 10;
int y = 10;
int width = 300;
int height = 50;
mDrawable = new ShapeDrawable(new OvalShape());
mDrawable.getPaint().setColor(0xff74AC23);
mDrawable.setBounds(x, y, x + width, y + height);
}
protected void onDraw(Canvas canvas) {
mDrawable.draw(canvas);
}
}
在构造方法中,ShapeDrawable对象被定义为椭圆形,然后给了颜色和大小。如果不设置形状,将不会绘制;如果不设置颜色,将默认黑色。
在自定义视图中可以绘制任何想要的样子。我们可以将上面示例的形状用代码绘制到Activity中:
CustomDrawableView mCustomDrawableView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mCustomDrawableView = new CustomDrawableView(this);
setContentView(mCustomDrawableView);
}
若要通过XML布局自定义图形而不是在Activity中设置,CustomDrawableView类必须重写 View(Context, AttributeSet) 构造函数,因为从XML实例化View对象时将会调用。接着将CustomDrawableView元素添加到Activity的XML布局中,如下:
<com.example.shapedrawable.CustomDrawableView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
ShapeDrawable类(像 android.graphics.drawable 包中其它Drawable类型一样)允许通过公开的方法定义各种属性,包括alpha、滤色器、震动、不透明度和颜色。
使用XML也可以定义原始形状,详细信息参考 Drawable Resources文档中ShapeDrawable章节。
5.可伸缩图片
NinePatchDrawable是一个可伸缩的位图图像,在标准PNG图像的基础上加了1像素宽的边框。当它作为背景时,安卓会自动调整大小以适应视图中的内容。它必须以.9.png为扩展名保存在项目的 res/drawable/ 目录下。
NinePatch图分为两个部分,左、上为定义拉伸区,右、下为定义内边距(里面是内容区),如下图:
将它作为Button的背景图时,实际效果如下:
6.矢量图片
VectorDrawable对象由定义在XML文件中的一系列点、线、曲线和相关的颜色信息组成。安卓5.0(API 21)开始,VectorDrawable和AnimatedVectorDrawable这两个类支持矢量图形作为图形资源。之前若想使用,支持库23.2或更高也提供矢量图形和动态矢量图形的支持。
关于使用矢量图形系统APIs或矢量图形支持库的更多信息,前往 Vector Drawable。