Android框架API提供了2D绘图API,允许你在画布或者以存在的视图上渲染你自己的图像。下面是两种典型做法:
- 绘制你的图像或者动画到布局中视图对象中。这种方式下,你的图像由系统普通视图层绘制进程处理 - 你只是简单的定义了视图中的图像。
- 直接绘制图像到画布,这种方法中,你自己调用合适的类的onDraw()方法,或者一个画布的draw...()方法(比如drawPicture())。同时,你要控制所有的动画。
当你只是绘制一些简单的图像,而不需要动态改变他们,或者不是一个高性能游戏的一部分,那么选择第一种方式是最好的。例如,当你需要展示一个静态图片或一个预定义的动画时,你只需要绘制图像到View中。
当你的程序需要像电子游戏那样需要不停的重新绘制场景时,你需要一个画布,这里有一些实现方法:
- 在UI进程中创建一个自定义View组件,调用invalidate()和onDraw()处理。
- 或者在一个单独的进程中,管理一个SurfaceView,在画布中执行绘制,你不需要请求invalidate()方法。
使用画布
当你想在你的程序中执行特别的绘制,或者控制图片动画,你需要通过Canvas实现。一个画布就像一个虚拟的表面,你可以在上面绘制任何东西 - 它会响应你所有的绘制。通过画布,你实际上是在窗口中的一个位图上执行绘图。
当处理一个SurfaceView对象时,你可以通过SurfaceHolder.lockCanvas()获得一个画布,然后你只需要把所有的绘制事件放在画布提供给你onDraw()方法中就可以了。不过,如果你想创建一个新的画布的话,你需要先定义一个位图,然后使用这个位图做为画布:
Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
现在画布将会绘制在定义好的位图上。绘制好后,你也可以通过Canvas.drawBitmao(Bitmap, ...)函数传递一个位图给另外一个画布。推荐使用画布提供给你的View.onDraw()或者SurfaceHolder.lockCanvas()实现绘图。
Canvas有自己的一些绘制函数,如drawBitmap(...), drawRect(...), drawText(...)等等。而另外一些类也有draw()函数。比如,你有一些图像对象需要放到画布中,这些图像有自己的draw()方法,不过需要你的画布做为参数。
在View上
如果你的程序不是一些多进程或者帧数比较快的程序(贪吃蛇,象棋,或者一些慢动画程序等)。你只需要创建一个自定义视图组件,然后使用View.onDraw()绘制画布。更方便的是,Android已经提供了一个已经定义好了的画布给你。
首先,扩展View类,定义onDraw()函数。Android框架会调用这个onDraw()函数绘制View本身。你需要通过Canvas执行所有的绘图调用,Canvas会传递到onDraw()函数中。
android会在需要的时候调用onDraw(),在程序准备绘制前,你必须确保View已经调用了invalidate()让视图无效了。就是说,你的View需要被绘制,然后Android就调用onDraw()方法。
在视图组件的onDraw()方法中,你可以使用各种各样的Canvas.draw...()函数,或者其他类的draw()函数。一旦onDraw()完成,Android框架就使用画布去绘制一个位图。
提示:为了区别主线程的invalidate(),其他线程使用postInvalidate()方法。
关于扩展View类,你可以查看 Building Custom Components。
例子可以看贪吃蛇游戏,在<your-sdk-directory>/samples/Snake目录中。
在SurfaceView上
SurfaceView是一个特殊的View子类,它在View层提供了一个专门的绘制表面。目的是提供一个程序第二线程,以便程序不需要等待系统View层的绘制,而是提供另外一个线程在自己的区域中绘制自己的画布。
首先,你需要继承SurfaceView创建一个新类。这个类需要实现SurfaceHolder.Callback,这个子类是一个接口,用来通知基本的Surface信息。比如被创建,被改变,或者被销毁。这些事件非常重要,他们让你知道,什么时候你可以开始绘制,你是否要基于新的surface属性做调整,什么时候停止绘制和清理一下任务。SurfaceView类中也有定义第二线程类的地方,这个线程类执行所有的绘制进程。
我们通过处理SurfaceHolder来代替直接吃了Surface。所以,当SurfaceView初始化时,调用getHolder()取得SurfaceHolder。然后你需要通知SurfaceHolder,你希望通过调用addCallback()来接收SurfaceHolder的回调。最后重写SurfaceView中的每个SurfaceHolder.Callback函数。
为了在第二线程中绘制Surface画布,你必须传递SurfaceHolder和使用lockCanvas()检索你画布。现在你可以通过SurfaceHolder取得画布并进行绘画了。完成绘画后,调用unlockCanvasAndPost()提交你的Canvas对象。Surface现在可以绘制画布了。每次执行重绘都需要按顺序执行锁定和解锁。
提示:每次从SurfaceHolder取得画布,先前的画布状态都会被保存下来。为了实现动画,你需要重绘整个画布。比如,你可以使用drawColor()填充一个颜色,或者使用drawBitmap()设置一个背景图像来清除先前的状态。不然你就会看到先前的绘画痕迹。
例子你可以看月球登陆游戏,在<your-sdk-directory>/samples/LunarLander/目录。
绘图资源
Android提供一个定制的2D图像库来绘制形状和图片。android.graphics.drawable包是最常用的2D图像绘制类。
这里讨论基本的绘制图像的方法,关于使用图片资源制作动画,请看前面一章:【Android API指南】动画和图像(3) - 图片动画
一个Drawable是“一些可绘制的东西”的抽象。你会发现Drawable类的扩展就是定义各种类型的图像,包括BitmapDrawable, ShapeDrawable, PictureDrawable, LayerDrawable等等。当然,你也可以继承他们,定义自己的绘制对象。
有三种方法去定义和实例化一个绘图资源:使用一个保存在工程资源目录的图片;使用一个定义了资源属性的XML文件;或者使用一般类的构造器。下面我们讨论前两种技术(对于有经验的程序员,使用构造器不是什么新鲜事)。
从资源图像中创建
支持的文件类型有PNG(优先的),JPG(可接受的),GIF(不赞成使用)。一般用来指定程序的图标,logo,或者其它图像。
要使用图片资源,只要把图片放入res/drawable/目录下。然后你就可以再代码或者XML中引用它们了。不管哪种方法,都会使用一个资源ID,这个ID就是资源的名称,不包括后缀名。
提示:在构建程序的过程中,res/drawable/中的图像文件会被aapt工具自动无损压缩。例如,一个真彩PNG图像不需要超过256位色彩,那么就可能被转换为8位PNG图像。这样做的好处就是不减少图片质量的同时减低了存储。如果你想用比特流的方式读取图像然后转换为位图,那么你需要把图像放到res/raw/目录中,这个目标不会被优化处理。
代码中的例子
下面的代码展示了怎么从资源文件中取得图像,然后添加到布局中。
LinearLayout mLinearLayout;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 创建一个线性布局
mLinearLayout = new LinearLayout(this);
// 实例化一个ImageView,设置它的属性
ImageView i = new ImageView(this);
i.setImageResource(R.drawable.my_image);
i.setAdjustViewBounds(true); // 设置Imageview适应图片的大小
i.setLayoutParams(new Gallery.LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT));
// 添加ImageView到布局中,设置布局为内容视图
mLinearLayout.addView(i);
setContentView(mLinearLayout);
}
另外一种情况,你可能想像图像对象一样操作一个图像资源,那么像下面这样创建一个图像就可以:
Resources res = mContext.getResources();
Drawable myImage = res.getDrawable(R.drawable.my_image);
提示:不管你实例化多少次每个独一无二的资源,这个资源都只保持一种状态。例如,如果你实例化相同的一个图片对象,然后改变了它的一个属性(比如透明度),那么会影响到两个实例化的对象。所以处理多实例化图像资源时,最好使用一个补间动画代替直接的改变。
XML中的例子
下面是在XML中添加视图资源的例子:
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="#55ff0000"
android:src="@drawable/my_image"/>
从资源XML中创建
现在你应该已经熟悉Android开发用户界面的原则了,那么你也知道了用XML定义对象的强大和灵活。如果你想创建一个绘图对象,它不是依靠最初的变量被定义,那么使用XML定义图像资源是很好的选择,尽管你可能想在程序运行中改变图像的属性,那么你也可以先在XML中定义,然后等它初始化后再修改属性。
一旦你已经在XML文件中定义图像资源,那么需要保存到res/drawable/目录中,然后通过调用Resources.getDrawable()方法,传递XML文件的资源ID来检索和实例化对象。
任何图像子类都支持inflate()方法,这个方法可以被定义在XML中。然后再程序中被实例化。每个图像都支持利用特别的XML属性来定义对象属性。更多信息可以查看类的文档。
例子
下面的XML定义了一个TransitionDrawable:
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/image_expand">
<item android:drawable="@drawable/image_collapse">
</transition>
文件保存为res/drawable/expand_collapse.xml,下面的代码将实例化这个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);
参考Drawable类了解更多支持的XML属性的信息。
形状图像
当你想动态绘制一些2D图形时,ShapeDrawable对象会很适合你,它可以绘制你能想到的原始的图形和样式。
ShapeDrawable是一个Drawable的扩展,你可以使用Drawable的方法,比如使用setBackgroundDrawable()去定义一个视图背景。当然,你也可以在自定义的View上绘制图形。由于ShapeDrawable有自己的draw()函数,你可以创建一个View的子类,在View.onDraw()方法中绘制ShapeDrawable。下面是一个基本的View类扩展:
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被定义为OvalShape,然后设置一个颜色和图形范围。如果不设置范围的话,图形不会被绘制,不设置颜色的,默认是黑色。
使用自定义的View可以使用任何你喜欢的方法绘制图形,我们可以再Activity中绘制图形:
CustomDrawableView mCustomDrawableView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mCustomDrawableView = new CustomDrawableView(this);
setContentView(mCustomDrawableView);
}
如果你想在XML中绘制的,CustomDrawable类必须覆盖View(Content, AttributeSet)构造函数,因为通过XML实例化一个View时构造函数会被调用。像这样在XML添加CustomDrawable:
<com.example.shapedrawable.CustomDrawableView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
ShapeDrawable类允许你使用公开方法定义各种图像属性。一些属性你可能希望用来调整透明度,颜色过滤器,抖动,不透明和颜色。
你也可以使用XML定义一些原始形状。更多信息可以查看
Drawable Resources 文档。
Nine-patch
NinePatchDrawable图像是一个可拉伸的位图,作为View的背景,这个位图会根据内容自动拉伸。标准的Android按钮就是使用的NinePatch。一个NinePatch图像是一个包含了1个像素边的标准PNG图像。需要以.9.png作为扩展名,然后保存到res/drawable目录中。
那个1像素的边框是用来定义可拉伸区域和静态区域的。使用一像素的黑线在图像的左边和上边指定可拉伸的区域。你可以有多个可拉伸区域,他们的相对比例是相同的。最大的区域,拉伸后也是最大的。
你也可以定义可选拉伸区域(有效区域,填充线),就是右边和下面的线。如果一个View对象使用NinePatch图像做为背景,并指定了文本,那么它会被拉伸以包含所有的文本,不过区域是有右边和下面的线决定的。如果没有指定填充线,那么使用左边和上面的线来定义。
我们要明白两种线的不同,左边和上边的线是定义了拉伸图像时可以重复的像素区域,下边和右边的线是定义了内容可以存放的相对区域。
下面是一个简单按钮图。
图的上部分,灰色虚线定义的就是拉伸时可重复扩展的区域,图的下部分中,粉红方框表示的就是可绘制内容的区域。就是说当粉色方框不够宽时,就按照上面画的线的区域扩展,当粉色框不够高时,就安装左边的线画的区域扩展。
Draw-9-patch工具提供一个方便的所见即所得的图像编辑器。如果定义的可拉伸区域会产生变形等风险,工具还会发出警告。
XML中使用的例子
下面是添加NinePatch图像到一组按钮的例子,图像保存在res/drawable/my_button_background.9.png
<Button id="@+id/tiny"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerInParent="true"
android:text="Tiny"
android:textSize="8sp"
android:background="@drawable/my_button_background"/>
<Button id="@+id/big"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerInParent="true"
android:text="Biiiiiiig text!"
android:textSize="30sp"
android:background="@drawable/my_button_background"/>
设置宽高为wrap_content就是为了让按钮整齐地保护文本。
下面是运行结果: