为了更好的学习和理解Canvas和Drawable,翻译了谷歌官方API指南的文章。自:http://developer.android.com/intl/zh-cn/guide/topics/graphics/2d-graphics.html 有错误欢迎指出。不过,该文章只能算是一个引导,还是没有更深入的详细写出Canvas、Paint、Bitmap和Xfermode等对象之间的关系。计划在进一步研究一下API和应用之后,再写一篇总结。
安卓API指南——Canvas和Drawable
正文:
安卓framework的API提供了一系列供2D绘图的API,允许用户在Canvas画布上渲染自定义的图画,或是修改已有的View来自定义它们的外观。在2D绘图时,通常有以下两种方法:
1. 在用户布局中的一个View对象上绘制图像或动画。这种方式是由系统的常规View层绘图程序完成的,用户只需简单定义图像即可。
2. 直接在Canvas上绘图。该方式需要用户调用相应类的onDraw方法(在其中传入用户的Canvas),或是调用Canvas的drawXXX()方法(如drawPicture())。这种方法同样可以绘制任意的动画。
如果用户只需要绘制一些简单的图形,该图形既不需要需要动态改变,也不是那种注重图像表现的游戏应用的话,方法1(即向View上画图)是最好的选择。比如,用户希望表现另一静态应用中的静态图片或提前定义好的动画,就可以把它们绘制在View上。
更多信息请参阅Drawable类。
在画布上画图,则是在应用需要经常重绘自己时最好的方法。比如游戏就应该采用画布绘图。有两种方法能够实现这种画图:
1. 用户在布局中自定义View控件时,可以在和UI Activity同一个线程中,调用invalidate()方法,随后处理回调onDraw()方法。
2. 在处理SurfaceView的子线程中,倾线程最大的速度在Canvas上绘图。(无需invalidate方法)
使用Canvas绘图
当用户编写一款需要专门细致绘图或控制动画的应用时,应该采用Canvas绘图的方式。Canvas此时作用类似内存或接口,承载了用户所有的绘图回调方法,然后加入到图片该绘制的区域中。通过Canvas图片实际上会在底层的一张窗体中的Bitmap上绘制。
在onDraw回调方法中绘图时,Canvas是提供好的,用户只需在其上调用绘图方法即可。
如果是处理一个SurfaceView对象,也可以从SurfaceHolder.lockCanvas()方法中返回一张Canvas。(这两种情况都会在下文中论述)
不过如果要创建一个新的Vancas,则必须定义绘图实际发生的Bitmap。也就是说,Canvas永远都需要一个Bitmap。用户可以通过以下方式声明一个新的Canvas。
Bitmap b =Bitmap.createBitmap(100,100,Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
如此用户的Canvas就会在定义的Bitmap上被绘制。在该Canvas绘制完毕后,用户还可以通过Canvas.drawBitmap(Bitmap,…)相关的方法把
这张Bitmap传给另一个Canvas。推荐大家使用View.onDraw方法或是SurfaceHolder.lockCanvas提供的Canvas来完成最终的绘图(见下文)。
Canvas类拥有自己的一套绘图方法供用户使用,如drawBitmap、drawRect、drawText等等等等。其他有的类也有draw方法。比如用户可能需要在Canvas上添加一些Drawable对象。Drawable就有自己的draw方法,此时Canvas可作为它的参数被传入。
On a View 在View上绘图
如果应用并不需要大量的处理或多高的帧速(比如象棋、贪吃蛇或其他慢动画应用),就可以考虑建立自定义View控件,并在View的onDraw()方法中用Canvas绘图。这时最爽的地方就是Android framework会提供给用户一个预先定义好的Canvas,来让用户调用绘图方法。
先继承View类(或它相关的子类)随后定义onDraw()方法。该方法会在需要View绘制自身时,被Androidframework调用。此时就可以通过该方法传入的Canvas来调用所有用户需要的绘图方法。
Android framework只会在必要时调用onDraw。当应用准备好被绘制时,必须通过invalidate()方法来将View无效化。如此一来就表示用户希望View被绘制,Android也就会调用onDraw方法(但无法保证该回调会瞬间执行)。
在View控件的onDraw方法内,通过各种Canvas.drawXXX()方法或其他类的以用户的Canvas为参数的draw()方法,使用Canvas来完成用户需要的全部绘图。当用户的onDraw()方法写完时,Android的framework会在系统中使用用户的Canvas来绘制一张Bitmap。
注意:如果用户使用主线程以外的线程来设置invalidate,则必须调用postInvalidate()方法。
关于继承View类的更多信息,可以阅读创建自定义控件。
如果希望获得示例应用,可以在SDK的samples文件夹中找到贪吃蛇游戏。
在SurfaceView上绘图
SurfaceView是View的特殊子类,会在View视图层提供给用户一张专用的绘图界面。
该绘图界面是给应用的子线程使用的,如此一来
应用便无需等待系统的View层准备好才能绘图。这样
子线程可以引用SurfaceView 在自身的Canvas上以自己的速度画图。
首先要创建一个类,继承SurfaceView。该类应该实现SurfaceHolder.Callback。该接口会反馈给用户底层Surface的信息,比如被创建、被更改或被销毁等。这些重点事件可以让用户获悉何时可以开始绘图,是否需要根据新的界面设置进行调整,以及何时停止绘图、或是杀死一些进程等。在SurfaceView类中也可以很好地定义子线程,在其中使用Canvas来完成全部绘图。
用户应通过SurfaceHolder来对Surface对象进行处理,而非直接去操作它。当SurfaceView初始化时,便应调用getHolder()方法来获取SurfaceHolder。随后要调用addCallback(this)方法来告诉SurfaceHolder自己希望接收SurfaceHolder回调(通过SurfaceHolder.Callback接口)。然后在自定义的SurfaceView类中复写SurfaceHolder.Callback的方法。
为了在子线程中对Surface Cnvas进行绘图,必须给线程传入SurfaceHanler,并通过lockCanvas方法取回Canvas。现在可以通过SurfaceHolder拿到Canvas,并做必要的绘图。当对Canvas完成绘图后,调用unlockCanvasAndPost方法,传入刚才的
Canvas对象。Surface便会开始绘制Canvas。如果希望重绘,就要再次配对使用lock和unlock Canvas的过程。
注意:每次从SurfaceHolder获得Canvas时,Canvas之前的状态都会被保留。为了准确绘制图像,必须重绘整个界面。比如用户可以通过drawColor方法充满一个颜色来清除掉之前的状态,或是使用drawBitmap()来设置一张背景图。否则用户就会发现上次绘图遗留的痕迹。
可以通过SDK samples文件夹里的LunarLander游戏或浏览Sample Code区的源码来查看示例应用。
Drawables
安卓提供了一个自定义2D图像Library来绘制图形和图片。Android.graphics.drawable包中存放了一些用来绘制2D图片的常见类。
本文讨论的是使用Drawable对象来绘制图像的基础,以及如何使用Drawable的几个子类。如果希望了解如何使用Drawable来完成帧动画,请查看DrawableAnimation。
Drawable是一个抽象概念,表示“可以被画出来的物体”。Drawable类定义了很多专门的可绘制图像,包括BitmapDrawable、ShapDrawable、PictureDrawable、LayerDrawable等等。当然,用户还可以通过自己的方式来自定义Drawable对象。
有三种方法可以定义并实例化Drawable:使用在工程资源中保存的图片;使用XML文件来定义Drawable的属性;或使用正常的构造方法。以下会讨论前两种方式(使用构造方法对于有经验的开发者来说司空见惯啦)。
使用资源图片
为应用添加图像的简单方法就是从工程资源中引用一张图片。支持PNG(推荐)、JPG(可接受)和GIF(不推荐)格式。该方法特别适合于应用图标、标志或其他游戏中使用的该类图片。
要使用图片资源,只需要在res/drawable目录下添加图片资源即可。这样一来就可以在JAVA代码或是XML布局中引用。不过两者都需要使用资源ID来引用。
注意:在res/drawable文件夹中存放的图片资源可能会被aapt工具在创建过程中自动无损压缩。比如不需要256色以上的真色PNG可能会被调色板直接转为一张8位PNG。如此就生成了一张质量相同但需要内存更少的图片。因此需要注意,在该路径下存放的图片会在创建中被改变。如果用户打算把图片当作位流读取,从而将之转成bitmap的话,可以把图片放在res/raw文件夹,这样就不会被自动压缩了。
示例代码
以下的代码展示了如何创建一个使用drawable图像资源的ImageView,并将其添加到布局中。
LinearLayout mLinearLayout;
protected void onCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
// 创建添加ImageView的LinearLayout
mLinearLayout = new LinearLayout(this);
//实例化ImageView并定义配置
ImageView i =newImageView(this);
i.setImageResource(R.drawable.my_image);
i.setAdjustViewBounds(true);//设置ImageView的边界,使之适合Drawable的像素。
i.setLayoutParams(newGallery.LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT));
//将ImageView添加到布局中并把布局设为content view
mLinearLayout.addView(i);
setContentView(mLinearLayout);
}
其他情况下,可能希望将图片资源作为Drawable对象处理。如此可以通过该资源创建Drawable,如:
Resources res = mContext.getResources();
Drawable myImage = res.getDrawable(R.drawable.my_image);
注意!每个工程中的资源,无论创建多少实例,也只能有一种状态。比如如果用户通过同一个图片资源实例化了两个Drawable对象,随后改变了其中之一的某个属性(比如透明度),则也会同样对另一个对象造成影响。因此当处理一张图片资源的多个对象时,应不要直接改变Drawable,而是使用补间动画(tween animation)。
Example XML示例XML
下方的XML代码展示了如何将资源Drawable在XML布局中添加到一个ImageView中(红字部分是图好玩加的)。
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="#55ff0000"
android:src="@drawable/my_image"/>
关于使用工程资源的更多信息,查看Resources and Assets。
Creating from resource XML从资源XML中创建
用户应该已经对安卓的UI开发原则比较熟悉了。也应该了解了在XML中定义对象的灵活和强大之处。这一原则对View通用,当然也包括Drawable。如果用户希望创建一个Drawable的对象,而又不希望依赖应用代码或UI定义的那些变量的话,在XML中定义该Drawable是一个不错的选择。即便用户希望在应用的UE中改变该Drawable的属性,也仍应考虑在XML中定义该对象,如此一来可以在它实例化时,修改它的属性。
当在XML中定义了Drawable后,可以在res/drawable路径下保存文件。随后调用Resources.getDrawable()方法,传入XML文件的资源ID(见下方的例子)来获取对象实例。
任何支持inflate方法的Drawable子类都可以在XML文件里定义,并在应用中被实例化。支持XML inflate的Drawable会使用XML的属性,来定义该对象的属性值(可参看类引用来查看这些属性)。查阅每个Drawable子类的说明文档可以获悉如何在XML中定义它。
Example
以下是XML代码,定义了TransitionDrawable
利用保存在res/drawable/expand_collapse.xml中的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文件中的属性。
Shape Drawable图形绘制
需要动态绘制一些2D图像时,ShapeDrawable对象也许能满足要求。通过SHapeDrawable可以从代码绘制基本的形状,并通过图像的方法调整它们的风格。
SHapeDrawable是Drawable的扩展,因此可以使用Drawable的地方都可以用它——比如View的背景(可以通过setBackGroundDrawable()设置)。当然也可以将形状作为自定义View来绘制,添加到布局之中。ShapeDrawable拥有自己的draw方法,因此也可以自定义一个View的子类,并在它的onDraw方法里来绘制SHapeDrawable。以下是一个基本的View继承类,它将ShapeDrawable绘制成一个View:
public classCustomDrawableViewextendsView{
private ShapeDrawable mDrawable;
public CustomDrawableView(Context context){
super(context);
int x =10;
int y =10;
int width =300;
int height =50;
mDrawable = new ShapeDrawable(newOvalShape());
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(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
mCustomDrawableView = new CustomDrawableView(this);
setContentView(mCustomDrawableView);
}
如果希望从XML布局中绘制该自定义Drawable,则该CustomDrawable必须复写View的View(Context,AttributeSet)构造方法,该方法会在View通过XML被inflate成一个View对象时调用。随后在XML中加入CustomDrawable节点,如:
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
ShapeDrawable类(类似drawable包中的很多其他Drawable类型)允许用户使用公共方法来定义drawable的多个属性。用户可以借此来调整透明度、颜色过滤、抖动色、不透明度和颜色等。
还可以使用XML文件来定义基本形状。这方面的信息可以在Drawable Resources文档中关于ShapeDrawable的栏目下查看。
Nine-patch图像
NinePatchDrawable图像是可拉伸的bitmap图片,如将它设为View的背景图,安卓会自动将该图片的尺寸重设,来适应View的内容。NinePatch的一个应用例子就是安卓基础的按钮——按钮必须拉伸到适应各个字符串的长度。NinePatchDrawable是一种常规的PNG图片,包括额外的1像素宽的边界。它必须以.9.png的后缀被保存,并存放至应用的res/drawable目录下。
边界是用来定义图片的可拉伸及静态区域的。用户要通过在左边和上方的边界(另一个边界的像素应为全透明或白色)绘制1条(或以上)的1像素宽黑线来指定可拉伸区域。
可以设定任意数量的可拉伸区域:它们的相对尺寸会保持相同,因此最大的区域一般会保留为最大的。
还可以定义图片的可选drawable区域(paddingline)通过在右边和下方绘制一条线。如果View对象以NinePatch作为背景图,随后调整View中的文本,则图片会自动拉伸,以将全部文本包裹在被右边和下方的线定义出的区域内(如果包括在内的话)。如果边隔的线并没有被包括在内,安卓会使用左边和顶部的线来定义可绘制的区域。
线的区别:左边和顶部的线是用来定义图片的哪些像素可以被复用,从而拉伸图像。而右方和底部的线则定义了图片内部的相对区域,可以包裹View的内容。
下面是一个示例NinePatch,用来定义一个按钮:
该NinePatch左边和顶部的线定义了拉伸区域,而右方和底部的线则定义了可绘制区域。上方的图中,灰色虚线定义了图片可以拉伸的区域。而下图中
粉色的矩形则定义了View的内容被绘制的区域。如果该区域放不下内容,则图片就会被拉伸至可以为止。
Draw 9-patch工具利用WYSIWYG图像编辑器提供给了用户方便创建自己NinePatch的方法。如果用户定义的可拉伸区域,在像素复用时可能有风险,导致绘制图片出问题,它甚至还会报错。
示例XML代码
下面是一些XML文件的示例代码,演示了如何把一张NinePatch图片加载到一组按钮上。(NinePatch图像的保存路径为res/drawable/my_button_background.9.png
注意宽高都要设置为wrap_content,这样按钮才会合适地包裹在文本周围。
下方的两个按钮即是使用上方的XML和NinePatch图片渲染出来的。注意按钮的宽高是如何随着文字而变化,以及背景图片是如何拉伸去适应文字的。