Android绘图必杀技---Canvas和Drawables
原文自:http://android.eoe.cn/topic/ui
Android框架提供一系列2D绘画API,它允许你在画布上渲染自定义的图像和定制已经存在的视图的外型与体验。当绘制2D图像时,你将会使用代表性的两种方法:
a.* 通过布局在视图对象里绘制你的图像或者动画* 。这种方法,你的图像句柄被系统标准视图层绘制进程控制。你简单定义将图像插入视图中。
b.* 直接在画布上绘制图像* 。此方法,你要亲自调用相应类的onDraw()方法 (passing it your Canvas), 或者其中一个画布draw开头的方法(比如drawPicture())。这样做,你还:在控制任意的动画。
在你绘制简单图像时,方法a是最好的选择。它不需要不断改变也不属于高性能游戏。比如,你在视图中想要绘制静态的或者预先确定的动画在另外的静态应用中。更多信息请查看Drawables。
当你的应用需要规律性的在画布上重绘时,方法b更为合适。就像电子游戏,你要自己重绘画布。然而,我们还有其他方法来实现:
* 在UI Activity的同一线程里创建的自定义视图组建中,调用invalidate()再控制onDraw()回调.
* 或者,在单独的线程中, 管理一个SurfaceView并在画布中绘图(你不需要请求invalidate())。
当你开发一个应用专门来完成绘制和控制图像的动画,你应该使用画布。画布工作机制就像一个接口,你表面图像将会被绘制。它控制所有绘画调用。通过画布,隐藏的位图(Bitmap)v完成了绘图。它被放在窗体里。
在onDraw()的回调方法事件中绘图,你只需要调用画布即可。当处理SurfaceView对象时,你也可以从SurfaceHolder.lock Canvas()获取一个画布。(所有这些场景将在下面的章节中讨论。)然而,你需要创建一个新的画布,你必须定义一个实际执行的位图Bitmap。这个位图Bitmap是画布所必须的。你可以像这样设置一个新画布:
Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
现在你的画布将绘制你定义的位图Bitmap。你还可以将这个位图Bitmap移到另外一个画布,使用Canvas.drawBitmap(Bitmap,...)方法。通过View.onDraw()或SurfaceHolder.lockCanvas()获得画布绘制图像的方法是值得推荐的。(请看下面章节).
Canvas类有自己的设置绘画方法。像drawBitmap(...), drawRect(...), drawText(...)等等。你会用到的其他类也会有draw()方法。比如,你有可能有一些Drawable对象需要放到画布中。Drawable有自己的draw()方法,并将Canvas作为参数。
* View 绘图*
如果你的应用不需要大量的处理或者帧速率(或许是一个棋类游戏,贪吃蛇,或者其他),你应该考虑创建一个自定义的View组件并通过View.onDraw()在画布中绘制。Android框架提供了预定义的Canvas来完成绘图调用,这么做是最方便的。
首先, 继承View类(或者其子类)并且定义onDraw()回调方法。这个方法在接到Android框架绘图请求时被调用。通过onDraw()回调执行所有画布绘图方法。
Android框架必须只调用onDraw()方法。每时每刻你的应用准备被绘制,你必须调用invalidate()使View无效。这表面你将会看到你的View被绘制,Android将会随即调用你的onDraw()方法(尽管不能保证实时的回调)。
在你的View组件onDraw()里,使用画布进行所有的绘制,使用各种Canvas.draw...()方法,或者其他类的draw()方法他们以Canvas作为参数。当onDraw()完成后,Android框架使用你的Canvas绘制位图Bitmap由系统控制.
* 注意: 为了从一个非主Activity的线材请求invalidate,你必须调用postInvalidate()) .
继承View类的信息,请看自定义组件。
对于一个简单的应用程序,请看贪吃蛇, 在SDK案例文件: /samples/Snake/.
* SurfaceView 绘图*
SurfaceView是View的一个特殊的子类,提供一个专用的绘图表面,在View层。应用程序的次级线材中加入绘制, 这样应用就不需要等到View层绘制完才能被请求。而且,副线程引用的SurfaceView可以绘制和控制自己的画布。
首先,你要创建一个SurfaceView的子类。这个类同时要实现SurfaceHolder.Callback。这个接口将反馈你底层的信息,比如什么时候创建,改变或者销毁。知道这些事件非常重要,你可以知道什么时候开始绘制,你是否需要对新外观属性的进行调整,和当你停止绘制并销毁一些任务。在SurfaceView类是定义副线程的好地方,它执行画布的所有绘制过程。
你应该通过SurfaceHolder,而非直接控制Surface对象。这样,当你的SurfaceView初始化,通过getHolder()获得SurfaceHolder。通知SurfaceHolder你要接收SurfaceHolder回调(SurfaceHolder.Callback)通过addCallback()。在SurfaceView class里覆盖所有SurfaceHolder.Callback方法。
为了在副线程中绘制界面画布,你必须传递线程到SurfaceHandler并且通过lockCanvas()获得画布。你现在可以通过SurfaceHolder获得画布并且在上面绘图了。当你在画布上绘制时, 调用unlockCanvasAndPost() ,传递到你的Canvas对象中。界面现在将绘制这个画布只要你关闭它。执行一系列的加锁解锁在你每次想要重绘时。
注: 在每次你从SurfaceHolder获得画布时,Canvas之前的状态将会被保留。为了绘制正确的动画效果,你必须重绘所有的界面。比如,你可以通过填充颜色drawColor()来清除画布之前的状态,或者由drawBitmap()设置背景图片。否则,你将会看到之前执行的痕迹。
关于程序例子,请看登月者游戏,在SDK例子目录下: /samples/LunarLander/。或者,在案例代码篇章浏览源代码。
Android为图形图像提供自定义2D图像处理库。你可以在android.graphics.drawable包里找到针对二位绘图的公共类。
本篇章讨论使用Drawable对象绘图的基础,和如何使用Drawable类的多个子类。使用Drawables来绘制帧动画,请看Drawable Animation。
Drawable的基本含义是内容是可绘的,你会发现Drawable类被扩展用于各种各样的可绘制图像,包括BitmapDrawable, ShapeDrawable, PictureDrawable, LayerDrawable等等。当然,你也可以继承它们来定义自己的Drawable对象。
有三种方法定义和实例化一个Drawable:使用项目资源中的图像;使用XML文件定义Drawable属性;或者使用一般的类构造函数。下面,我们将讨论前面两种方法(使用构造函数并没有什么新颖的地方)。
* 由资源图像创建*
一个简单的方法添加图像到你的应用中去,就是从你的项目资源里引用图片文件。支持PNG(首选),JPG(还行)和GIF(最好不要)文件类型。当你添加图标、logo时这个方法是首选的。
为了使用图像资源,你要添加文件到项目目录res/drawable/下。从那你可以通过代码或者XML布局引用它们。不管怎样,都要通过资源ID来引用它,ID即为不包含扩展名的文件名(比如,my_image.png就为my_image)。
* 注: 在编译过程中,res/drawable/里的图像资源会被aapt工具自动无失真图像压缩。
比如,一个真彩色PNG文件它不需要大于256色会被调色板修改为8位PNG。这样相同质量的图片所占的内存就会更小。所以要注意图片的二进制文件在编译时会改变。如果你计划读取图片数据流来转换为位图,将他们放到res/raw/文件夹里, 这里他们不会被优化。
代码例子
下面的代码片段演示了怎样使用ImageView通过drawable资源来使用图片并将其添加到布局layout中。
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);
注:
项目中所有唯一的资源只有一个维护状态,不管你用它实例化了多少对象。比如,你由同一个资源图片实例化了2个Drawable对象,然后改变其中一个的属性 (比如透明度),然后它也会影响到另外一个。所以,在多个实例使用一个图片资源时,不要直接转换为Drawable,你应该执行tween animation。
XML例子
下面的XML片段展示怎么在XML layout中,向ImageView中添加图像资源。
更多关于项目资源的信息,请看Resources and Assets。
* 由资源中的XML文件创建*
现在,您应该熟悉了Android的用户界面开发应遵循的一些规则。因此,你知道由XML来定义对象是多么方便灵活了吧。这种方法不管Views还是 Drawables都适用。如果你要创建一个Drawable对象,它不依赖于某个变量或者用户的交互,那么在XML里定义它是个好办法。即时你希望用户在体验时去改变它的属性, 你应该考虑在XML里定义, 你可以不断改变它的属性直到它被实例化。
你一旦在XML里定义了Drawable,在项目文件夹res/drawable/里保存。然后,通过调用Resources.getDrawable()来获取并实例化它,传入XML文件的资源ID。(请看下面的例子)
所有Drawable子类支持inflate()方法,在XML里定义和实例化。利用特定的XML标签来定义对象属性。
例子:
在XML里定义TransitionDrawable:
XML文件保存为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会持续向前移动一秒:
transition.startTransition(1000);
* Shape Drawable*
当你动态的绘制一些二维图像时,ShapeDrawable对象会给你很大的帮助。你能以编程的方式绘制基本形状和风格。
ShapeDrawable是Drawable的扩展,所以在用到Drawable的时候你同样可以使用ShapeDrawable。比如设置View的背景时,setBackgroundDrawable() 。 当然,你也可以在自定义的View里绘制你的模型。因为ShapeDrawable有自己的draw()方法,你可以在View子类onDraw()方法里绘制ShapeDrawable。
下面是最基本的例子,绘制ShapeDrawable对象:
public class CustomDrawableView extends View {
private ShapeDrawable mDrawable;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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(椭圆模型)。然后设置颜色和大小(界限)。 如果你不设置大小,模型不会被绘制。如果你不设置颜色,它将默认为黑色。
在Activity里通过代码绘制自定义的View:
CustomDrawableView mCustomDrawableView;
1 2 3 4 5 6 | protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mCustomDrawableView = new CustomDrawableView(this);
setContentView(mCustomDrawableView);
} |
如果你喜欢通过XML布局文件定义drawable来代替直接在Activity直接写代码,那它必须覆盖 View(Context, AttributeSet)构造方法,它在View从XML里获取初始化时被调用。然后添加CustomDrawable元素在XML里:
ShapeDrawable类允许你通过drawable的public方法设置不同的属性。像调整透明度,颜色,抖动,滤色器等。
你也可以由XML定义drawable shapes。更多信息请看Drawable Resources
* Nine-patch*
NinePatchDrawable图像是可伸展的位图。当你将它作为背景时,Android会自动适应View的大小。NinePatch的一个案例是背景使用标准Android按钮。按钮必须能根据不同的长度自动伸展。NinePatch drawable是标准的PNG格式,在最外面一圈额外增加1px的边框。
它必须以.9.png为扩展名,并且保存在res/drawable/目录下。
这个1px的边框就是用来定义图片中可扩展的和静态不变的区域。在边框的左上角画一个1像素的黑线来表面伸缩区域。你想要多少伸缩区域都可以:他们的绝对尺寸保持相同,所以最大的区域永远是最大的。
你也可以定义一个可选的可移动部分在图片上,通过下面和右边的线。如果一个View对象设置NinePatch作为背景,然后改变View上的文字, 它会自己伸展,所有的文字将会适应你通过右边和下面的线定义的区域。如果不包括填充线, Android使用左边和上面的线定义可绘区域。
为了阐明这两条线的不同,左和上的线定义图片像素允许被复制以便拉伸图片。下方和右边的线定义相对区域在图片的内容区域。
下面是简单的按钮使用例子:
http://developer.android.com/images/ninepatch_raw.png
NinePatch通过左上线来定义伸缩区域,并且通过下右线来定义可绘区域。第一幅图,灰色虚线确定的图片区域将会被拉伸。第二幅图的粉色区域 确定一个View里的内容显示区域。如果内容不适合这个区域,图片将会被拉伸。
Draw 9-patch工具提供了简便的方法来创建我们的NinePatch图片,使用WYSIWYG图形工具。
Example XML
注意宽和高设置为wrap_content使按钮内容自动调整。