第3章 Canvas和 Drawables
Android框架API提供一套2D绘制的API,允许你在canvas(画布)上渲染你自定义的图形或修改已存在的View并定制它们的外观,当绘制2D图形时,你通常会在下面二选一:
A.从你的布局里绘制你的图形或动画到一个View对象中。用这种方式,你的图形由系统正常的View层次绘制过程来处理,你只需要简单的定义图形到View中即可。
B.绘制你的图形到Canvas。这种方式你自己可以调用相应的类的onDraw()方法,或Canvas中的draw...()方法,如drawImage()。这样做时,你也能控制任何动画。
选项A绘制到一个View中,当你想要绘制一个简单的图形并不需要动态改变时,它是最好的选择。但它不适用于高性能游戏。例如,当你想要显示一个静态图形或预先定义好的动画时,你应该绘制你的图形到一个View中,即使用A方案。
选项B绘制到一个Canvas中,当你想要应用程序需要定期重绘它自己时,它是最好的选择。应用程序如电子游戏应该画在自己的画布上,然而不只有一种方法来做这个:
◆在同一个线程中,如你的UI Activity,其中在你的布局中创建了一个自定义View组件,调用invalidate()并且然后它会处理onDraw()回调
◆或者在一个单一的线程中,其中你管理一个SurfacveView并以线程最快的速度执行绘制(你不需要调用invalidate())。
3.1 用一个Canvas绘制
当你编写一个应用程序,其中你想要执行特定的绘制或控制图形动画,你应该通过一个Canvas来绘制。一个Canvas为如一个接口工作实际上她在表面绘制,它拥有你所有“draw”的调用。通过Canvas,你的绘制实际上是在一个潜在的位图上执行的,并放置到窗口。到头来你在onDraw()回调方法中绘制,Canvas为你提供绘制的地方。当处理SurfaceView对象时,你也能从SurfaceHolder.lockCanvas()获得一个Canvas。然而如果你需要创建一个新的Canvas,那么你必须在实际绘制的地方定义Bitmap。这个Bitmap总是被Canvas需求。你能想代码清单3-1那样启动一个新的Canvas:
Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(b);
代码清单3-1
现在你的Canvas将绘制到已定义的Bitmao中。绘制到Canvas上后,你能使用Canvas.drawBitmap(Bitmap,...)方法把你的Bitmap搬到另一个Canvas中。建议你最后绘制的图形通过一个Canvas对象绘制,你可以通过View.onDraw()或者SurfaceHolder.lockCanvas()。Canvas类有它自己的一套绘制方法比如drawBitmap(), drawRect(...)
,drawText(...)和更多。其他类可能也有draw()方法。例如,你可能有一些想要放入Canvas中的Drawable对象。这些Drawable对象都有它自己的draw()方法,他需要你的Canvas作为一个参数。
3.1.1 在一个View上
如果你的应用程序不需要大量处理或不需要高帧率(如一个象棋游戏,贪吃蛇游戏,或另一个缓慢运动的应用程序),那么你应该考虑创建一个自定义的View组件并用View.onDraw()方法绘制在Canvas上。这样最方面的地方就是Android框架已经帮你准备好了Canvas,你可以省去一些不必要的工作。首先继承View类并重写onDraw回调方法。当View绘制自己时,它就会调它自己的onDraw()方法。Android框架只会调用onDraw().每次你的应用程序准备绘制时,你必须请求你的View无效,所以这就是为什么我们需要调用invalidate(),它的意思就是让你的View无效,从而回调onDraw()方法来重绘。注意如果在非主UI线程中调用的话,你可以调用postInvalidate()方法。
3.1.2 在一个SurfaceView上
SurfaceView是一个特殊的View子类,它提供专用的表层绘制。其目的是独立与主线程,在自己开的线程下处理绘制问题 ,因此应用程序就不需要依赖系统内置的机制来绘制。开始你需要创建一个新的类继承SurfaceView。这个类也将实现SurfaceHolder.Callback这个接口。这个接口的实现是为了通知关于Surface底层的信息。如画布被创建,改变,摧毁。这些事件很重要,类似于Activity的生命周期,所以你应该在这些事件方法中处理一些必要的工作。当你的SurfaceView被初始化时,你应该通过getHolder()来获得一个SurfaceHolder。通过调用addCallback()这样你就能接收SurfaceHolder的回调事件。然后在你的SurfaceView中重写SurfaceHolder.Callback方法。为了再你开的线程中绘制Canvas,你必须通过线程中的SurfaceHandler并使用lockCanvas()重新得到Canvas。你能通过SurfaceHolder马上获得Canvas并进行必要的绘制。一旦你用Canvas绘制完成,
可以调用unlockCanvasAndPost()解锁。然后你每次重绘时,就需要有这样的循环了,锁定-解锁。注意:每一次你从SurfaceHolder中重新获得Canvas时,先前的Canvas状态将被保留。为了正确的激活你的图形,你必须重绘整个surface。例如,你能通过drawColor()或通过drawBitmap()设置一些背景图来清空先前的Cnavas状态。否则,你会看到上次的画面残留。
3.2 Drawables
Android提供了一个自定义的2D图形库用来绘制图形和图像。android.graphics.drawable包中你可以找到一些常用的类,它们用于二维绘制
。下面我们讨论使用Drawable对象的基础,它用来绘制图形并怎样使用它的两个子类。一个Drawable是一个常规抽象概念,表示其能被绘制。你将发现这个Drawable类扩展定义了各种特定种类的可绘制图形。包括BitmapDrawable,ShapeDrawable,PictureDrawable,LayerDrawable等。当然你也可以扩展定义你自定义的Drawable对象用来以独特的方式表现。有三种方法定义并初始化一个Drawable:使用一个图像保存到你项目的资源里,使用一个XML文件来定义Drawable属性,使用类构造函数。下面我们将讨论前面两项,对于使用构造函数没什么讲的。
3.2.1 从资源图像中创建
添加图形到你的应用程序中一个简单的方法就是通过从项目资源中引用一个图像文件,支持PNG(首选),JPG(可接受),GIF(不建议)。这种技术显然将是优先使用于应用程序icon,logo,或其他图形比如用在一个游戏中。为了使用一个Image资源,你只需要把它添加到你的res/drawable/目录下即可。这样你可以在代码或XML中引用它们。注意:放在res/drawable/目录下的图像资源可能被自动优化。例如一个真彩色并不没有超过256色的PNG图片可能被转换为8位的PNG的调色板。这样图像质量相同,但内存需要更少。所以要注意图像放在这个文件夹下编译时二进制格式可能被改变。如果你计划使用bit流来读取一个图像转换成bitmap,那么你应该把图像放在res/raw中,因为在这图片不会被自动优化。
3.2.2 例子代码
以下代码片段告诉你怎样从drawable资源中使用一个图像构建一个ImageView,并把他放入布局中 ,如代码清单3-2所示:
LinearLayout mLinearLayout; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 创建一个 LinearLayout在其中添加ImageView mLinearLayout = new LinearLayout(this); // 初始化一个ImageView并设置他的属性 ImageView i = new ImageView(this); i.setImageResource(R.drawable.my_image); i.setAdjustViewBounds(true); //设置ImageView的边界来匹配Drawable的尺寸 i.setLayoutParams(new Gallery.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); //使用LinearLayout对象的addView方法把,imageview添加到LinearLayout中,然后显示 mLinearLayout.addView(i); setContentView(mLinearLayout); }
代码清单3-2
有些情况下,你可能想要把图像资源作为一个Drawable对象处理。为此,下面代码告诉你如何做,如代码清单3-3所示:
Resources res = mContext.getResources();
Drawable myImage = res.getDrawable(R.drawable.my_image);
代码清单3-3
注意:不管有多少个对象实例化这个资源,每一个唯一的资源仅能维持一个状态。例如,如果你使用相同的图像资源实例化两个Drawable对象,为一个Drawable改变它的aplha属性,那么它也会影响到另一个Drawable对象。所以对于同一个图像资源实例多个Drawable对象的情况下,如果你需要分开处理,你可以使用渐变动画
3.2.3 XML例子
下面代码片段,告诉你在ImageView中如何添加一个Drawable对象,(tint只是一个好玩的属性),如代码青岛那3-4所示:
<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:tint="#55ff0000" android:src="@drawable/my_image"/>
代码清单3-4
3.2.4 从XML创建Drawable
到目前为止,你应该熟悉Android UI开发的原则了。因此,你应该理解在XML中定义对象的强大与灵活性。这个理念也能用于Drawable。如果有一个你想创建Drawable对象 ,可能你会在代码中使用变量或通过用户交互来创建。但是在XML定义创建一个Drawable对象是一个更好的选择。特别是你想要你的Drawable对象在用户体验中改变它的属性,你只需要在XML中修改一次即可。一旦你在XML中定义了Drawable,保存这个XML文件在res/drawable目录下。然后通过调用Resources.getDrawable()来获得并初始化对象,里面传入XML文件资源ID即可。任意Drawable子类支持inflate()方法,它能通过传入定义的XML资源文件来实例化Drawable对象。下面让我们看下代码清单3-5是如何定义TransitionDrawable对象的:
<transition xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/image_expand"> <item android:drawable="@drawable/image_collapse"> </transition>
代码清单3-5
这个文件的位置是res/drawable/expand_collapse.xml
,下面代码实例化TransitionDrawable 并设置它作为ImageView的内容,如代码清单3-6所示:
Resources res = mContext.getResources(); TransitionDrawable transition = (TransitionDrawable) res.getDrawable(R.drawable.expand_collapse); ImageView image = (ImageView) findViewById(R.id.toggle_image); image.setImageDrawable(transition);
代码清单3-6
然后这个转变能维持一秒:
transition.startTransition(1000);
3.3 Shape Drawable
当你想要动态绘制二维图形时,一个ShapeDrawable 对象将可能适合你的需要。用一个ShapeDrawable你能动态绘制简单的形状和风格,表现方式可能任你想象。ShapeDrawable是Drawable的扩展,因此你能使用View. setBackgroundDrawable()方法。当然,你也能使用其他方法绘制形状,比如把它作为一个自定义View。因为ShapeDrawable有它自己的draw()方法,你能创建一个View的子类来绘制ShapeDrawable,只要在View.onDraw()中调用
ShapeDrawable.draw()即可。让我们看下代码清单3-7所示:
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); } }
代码清单3-7
在构造函数中,ShapeDrawable作为一个OvalShape(椭圆形)。然后设置它的颜色和边界。如果你没有设置边界,形状将不会被绘制,反之如果你没有设置颜色,它会默认为黑色。用定义的自定义View,它能使用任意方式绘制。下面是以为编程方式绘制一个形状,如代码清单3-8所示:
CustomDrawableView mCustomDrawableView; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mCustomDrawableView = new CustomDrawableView(this); setContentView(mCustomDrawableView); }
代码清单3-8
如果你想从XML布局中绘制drawable,而不是从Activity中的话,你可以使用View(Context, AttributeSet)这个构造函数。这样其实是Android内部把XML中的对象可以通过
inflate()方法来传入XML文件资源ID实例化View。具体如代码清单3-9所示:
<com.example.shapedrawable.CustomDrawableView android:layout_width="fill_parent" android:layout_height="wrap_content" />
代码清单3-9
ShapeDrawable类允许你定义变量属性。比如一些alpha透明度,颜色,滤镜,渐变色等。你也能使用XML定义原始的drawable形状