Canvas and Drawables
安卓提供了一组绘制二维图形的 API(参考官方文档:Canvas and Drawables | Android Developers),这组 API 允许开发者通过将自定义图形绘制到画布上或修改现有视图来实现视图的定制,绘制二维图形,通常有以下两种方式:
- 在布局文件中引入自定义视图或动画,这种方式的图形绘制会交由系统来处理,你只需要将自定义图形引入视图。
- 直接将图形绘制到画布上,这种方式的绘制就需要开发者重写并调用对应类的 onDraw(android.graphics.Canvas) 方法,或者调用 Canvas 的一个 draw...() 方法(例如 drawPicture (Picture picture, Rect dst))。在图形绘制的同时,我们也可以对动画进行控制。
当绘制的图形既不需要动态变化,也不是一个高性能游戏的一部分时(比如,当你想在一个静态应用中显示一个静态视图或预定义的动画时,你应该将图形引入到视图中)第一种视图绘制的方式是最好的选择。当程序中的视图需要定期重绘自身时(比如:一些视频游戏就需要不断的去重绘自身以达到动画交互的效果),第二种将是最优的选择。实现第二种绘制的方式有两种:
- 在 Activity 的 UI 线程中,在布局文件中创建自定义视图,调用自定义视图的 invalidate() 方法,然后由该视图的回调方法 onDraw() 来进行重绘。
- 或者在一个单独的线程中,通过管理一个 SurfaceView 让该线程尽可能快的去进行重绘(不需要调用 invalidate() 方法)。
Draw with a Canvas
当你编写的程序需要进行图形绘制或对图形的动画进行控制时,你需要通过画布来完成这些操作。画布适合作为将图形绘制到实际表面上的接口,因为它拥有所有的 draw 方法。通过画布绘制,实际上是将图形绘制在了屏幕的位图上。在回调方法 onDraw() 中进行绘制时,画布会由 onDraw() 方法提供,我们只需要将绘图放置到画布上即可。当在处理 SurfaceView 对象时,我们可以从 SurfaceHolder.lockCanvas() 方法获取一个画布。(这两个场景将在以下部分中讨论)然而,如果你需要创建一个新的画布,就必须定义一个可以用来展示图形绘制的位图。Canvas 的绘制需要位图支持,你可以按照以下方式建立一个画布:
Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
画布创建好后,就可以利用画布在之前定义的位图上进行图形绘制了。画布完成绘制后,可以使用 Canvas.drawBitmap(Bitmap,...) 方法中的任何一个将位图绘制到其他画布上。但推荐的做法是尽量使用由 View.onDraw() 或 SurfaceHolder.lockCanvas() 方法提供的画布来进行最终图形的绘制。(见下节)
Canvas 类为我们提供了一套绘图方法,如 drawBitmap(...), drawRect(...), drawText(...) 等。你可能使用的其他类也有 draw() 方法。比如,你可能有一些需要绘制到 Canvas 上的 Drawable 对象。Drawable 有一个可以将 Canvas 作为参数的 draw() 方法。
On a View
如果你的应用不需要处理大量的帧运算(可能是一个象棋游戏、贪食蛇、或者是其他包含缓慢动画的应用),这时你就需要创建一个自定义控件并在该控件的 View.onDraw() 方法中使用 Canvas 进行图形绘制。Android Framework 层为此提供了一个可以执行各种绘制方法的预处理画布。
实现自定义控件,需要继承 View 类(或其子类)并重写 onDraw() 方法。该方法将在视图进行绘制的时候由 Android framework 层调用。你可以通过 onDraw() 方法中提供的 Canvas 调用所有的绘制方法。
Android Framework 层只在必要的时候调用 onDraw()方法。每当你的应用准备要进行绘制时,你必须通过调用 invalidate() 方法来确保这个 View 是无效的。执行这个方法表明你想让视图进行重绘,然后 Android 就会调用 onDraw()方法进行视图重绘(但并不保证这个回调会马上执行)。
在自定义视图组件的 onDraw() 方法中,可以使用给定的 Canvas 执行各种 Canvas.draw...() 方法,或者使用其他类的 onDraw() 方法(在其他类中将 Canvas 作为参数传入)进行绘制。一旦 onDraw() 方法执行结束,Android Framework 层将会把由 Canvas 绘制的位图交给系统来处理。
▐ 注:如果是从一个线程中(非 UI 线程)去请求视图失效,必须调用 postInvalidate()。
有关扩展View类的信息,请参考(参考官方文档:Building Custom Components | Android Developers)。
有关示例应用程序,请参阅贪吃蛇游戏,在 SDK 示例文件夹:<you're-sdk-directory>/samples/Snake/(源码:Snake)。
On a SurfaceView
SurfaceView 是 View 的一个特殊子类,它在视图层提供了一个专用的绘制平面。其目的是可以将该平面提供给应用的辅助线程,从而使应用无需等待系统视图层准备完毕就可以直接绘制。相反,一个引用了 SurfaceView 的辅助线程可以以自身的速度在 Canvas 上进行绘制。
首先,你需要创建一个类继承 SurfaceView。同时,这个类还应该实现 SurfaceHolder.Callback。这个子类是一个监控 Surface 何时被创建、修改、销毁事件的接口。这些事件十分重要,通过这些事件我们能知道何时可以开始绘制,是否需要根据新的表面特性进行调整,以及何时停止绘制并强制结束一些任务。在 SurfaceView 的继承类里还可以定义执行绘制动作的辅助线程类。
你应该通过 SurfaceHolder 来处理 Surface 对象,而不是直接处理它。所以,当你的 SurfaceView 进行初始化时,需要通过调用 getHolder() 方法来获取 SurfaceHolder。然后你应该调用 addCallback() 方法来通知 SurfaceHolder 你想接收 SurfaceHolder 的回调方法(来自:SurfaceHolder.Callback)。最后再重写 SurfaceView 继承类里的每一个 SurfaceHolder.Callback 方法。
如果要从子线程中将图形绘制到画布表面,就必须将 SurfaceHandler 传递到子线程,并通过 lockCanvas() 方法获取 Canvas。现在,你就可以在 SurfaceHolder 提供给你的 Canvas 上绘制图形了。如果用 Canvas 完成了绘制,需要调用 unlockCanvasAndPost() 并将 Canvas 对象传递给该方法。Surface 将会以你传递的 Canvas 对象进行绘制。每次程序需要重绘的时候都需要对 Canvas 顺序执行锁定和解锁操作。
▐ 注:每次从 SurfaceHolder 获得的 Canvas 都会保留先前的状态,为了确保图形绘制正确,你必须重绘整个 surface。例如,你可以使用 drawColor() 填充颜色或使用 drawBitmap() 设置一个背景图片来清除 Canvas 的先前状态。否则,你会看到先前执行绘制时的痕迹。
有关示例应用程序,请参阅贪 Lunar Lander 游戏,在 SDK 示例文件夹:<your-sdk-directory>/samples/LunarLander/(源码:LunarLander)。或参考 Sample Code。
Drawables
Android 为图形和图像的绘制提供了一个定制的 2D 图形库。在 android.graphics.drawable 包下可以找到用于绘制 2 纬图形的常用类。
本文讨论了使用 Drawable 对象来绘制图形,以及如何使用 Drawable 子类的基本知识。有关使用 Drawables 完成帧动画的信息,请参考 Drawable Animation。
Drawable 是 “一些可以被绘制事物” 的抽象。你会发现在 Drawable 类基础上又扩展了各种特定的图形绘制子类,包括 BitmapDrawable,ShapeDrawable,PictureDrawable,LayerDrawable 等。同时,你也可以通过继承这些子类来自定义 Drawable 对象。
有三种方式来定义和实例化一个 Drawable:使用一张保存在项目资源文件夹中的图片;使用一个定义了 Drawable 属性的 XML 文件;或使用普通类的构造函数。下面,我们将对前 2 种技术进行讨论(对于有经验的开发者来说使用构造函数并不陌生)。
Creating from resource images
在应用中添加图形的简单方法是从项目资源文件中引用一个图片文件。支持的文件类型有 PNG(首选),JPG(可接受)和 GIF(不推荐)。应用的 icon、logo 或游戏中的图片资源应该优先选用这种技术。
使用图像资源,需要将文件添加到项目的 res/drawable/ 目录下。代码或 XML 文件都可以引用该目录下的图像资源。无论那种方式的引用,都只会引用文件的资源 ID,而不是包含扩展名的文件(例如:my_image.png 的引用 ID 为 my_image)。
▐ 注:在 build 过程中,AAPT 会自动对 res/drawable/ 文件夹下的图片资源进行无损压缩。例如:一个不超过 256 色的真彩 PNG 图片或许会被调色板转换为一个 8 位的 PNG 图片。这样的压缩会使图片具有相同的显示效果,但却占用更少的内存资源。所以需要明确的是在 res/drawable/ 目录下的图像二进制文件会在 build 过程中发生改变。如果你想以流的形式读取图片,并将流转换为一个位图,可以将图片放置到 res/raw/ 目录下,在这个目录下的图片不会被优化。
Example code
下面的代码片段演示了如何从 drawable 资源中获取一张图片来绘制一个 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 对象来处理。要想这样做,可以像下面的代码一样从 resource 中获取一个 Drawable 对象:
Resources res = mContext.getResources();
Drawable myImage = res.getDrawable(R.drawable.my_image);
▐ 注:项目中的每个资源,不论你为它实例化多少不同的对象,这个资源都只维护一个状态。例如:你使用相同的图片资源实例化了 2 个 Drawable 对象,然后改变其中一个 Drawable 对象的属性(如 Alpha),那么另一个 Drawable 也会受到影响。所以在处理同一个图片资源的多个实例时,应该执行一个补间动画,而不是直接去改变 Drawable。
Example XML
下面的代码片段演示了如何将一个资源对象添加到 XML 布局的 ImageView 控件上。
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/my_image"
android:tint="#55ff0000" />
更多关于使用项目资源的信息,请参考 Resources and Assets。
Creating from resource XML
现在,你应该对开发用户界面的原则比较熟悉了。因此,你也应该明白在 XML 文件中定义视图对象的作用和灵活性。这种理念从 Views 延续到 Drawables。如果你想创建一个 Drawable 对象,并且这个对象不依赖于应用程序代码定义的变量或用户的交互,那么在 XML 文件中定义 Drawable 是最好的选择。即使你希望 Drawable 在用户与应用的交互过程中改变自身的属性,你也应该考虑在 XML 中定义 Drawable 对象,因为你可以在它实例化后随时修改其属性。
定义了 Drawable 的 XMl 文件需要保存在项目的 res/drawable/ 目录下。然后,调用 Resources.getDrawable() 来获取和实例化对象,这个过程需要传递 XML 文件的资源 ID(参见下面的例子)。
所有支持 inflate() 方法的 Drawable 子类都可以定义在 XML 文件中并可以被实例化。支持 XML 属性扩展的 Drawable 可以通过 XML 属性来定义对象的属性(详情请参考类的引用)。如何在 XML 文件中定义对象属性可参阅每个 Drawable 子类的说明文档。
Example
下面是定义了 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>
将此 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);
然后设置过渡时间为 1 秒:
transition.startTransition(1000);
更多关于每个 Drawable 支持的 XML 属性请参考上面列出的 Drawable 类。
Shape Drawable
ShapeDrawable 对象可以满足动态绘制二维图形的需求,用 ShapeDrawable,开发者可以通过代码的方式来绘制图形和定义风格。
ShapeDrawable 对 Drawable 进行了扩展,因此你可以对一个需要 Drawable 的地方使用该对象(或许是一个视图的背景,可以调用 setBackgroundDrawable())。当然,你也可以把它当成要添加到布局文件上的自定义图形来绘制。因为 ShapeDrawable 有自己的 draw() 方法,所以你可以创建一个 View 的子类,并在该子类的 View.onDraw() 方法中使用 ShapeDrawable 进行图形绘制。下面是对 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 的形状为 OvalShape(椭圆形),然后再为该图形设置颜色和边界。如果不设置边界,图形将不会被绘制,如果不设置颜色,将会默认为黑色。
开发者可以以自己喜欢的方式来绘制自定义视图,我们可以用上面的代码在 Activity 中绘制图形:
CustomDrawableView mCustomDrawableView; protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mCustomDrawableView = new CustomDrawableView(this); setContentView(mCustomDrawableView);
}
如果你想在 XML 布局中绘制自定义视图,就必须重写自定义视图类的 View(Context, AttributeSet) 构造方法,因为这个方法会在 XML 实例化视图的时候被调用。然后就可以在 XML 中添加一个自定义视图的元素进去了,代码如下:
<com.example.shapedrawable.CustomDrawableView
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
ShapeDrawable 类允许通过公用方法来定义 drawable 的各种属性,比如:Alpha透明度(alpha transparency),滤色镜(color filter),抖动(dither),不透明度(opacity)和颜色(color)。
你还可以通过 XML 来定义基本图形。更多信息,请参考 Drawable Resources。
Nine-patch
NinePatchDrawable 图形是一种可拉伸的位图图像,如果你将这种图形作为控件背景,Android 将自动调整图形大小以适应视图内容。一个使用 NinePatch 的例子是 Android 标准按钮的背景,这个背景可以自适应不同长度的字符串。一个 NinePatch drawable 是一个包含1像素边框的标准 PNG 图片,它的扩展名必须为 .9.png,并且它应该存放到项目的 res/drawable/ 目录下。
边界用于定义图像的伸缩性和静态区域。所以能通过1像素或更宽的黑线在边框的左边和顶部来设置可拉伸的部分(另一边界应该是透明或白色的),你可以根据你的需要来设置可拉伸部分:它们的相对大小保持不变,所以最大的部分依然保持最大。
还可以在图片的右侧和底部画线(实际上是填充线)来指定可拉伸部分。如果一个 View 对象以 NinePatch 作为自身的背景并且设置了文本,NinePatch 将在底部线和右侧线的交汇区域进行拉伸以适应文字的显示。如果没有填充线,Android 将使用左侧和顶部线来定义这个绘制区域。
不同的线有着不同的作用,图片左侧和顶部的线定义了图片的哪些像素可以被复制用以拉伸图片。图片底部和右侧的线定义了视图内容放置的区域。
下面是一个用于按钮的 NinePatch 示例文件:
NinePatch 的左侧和顶部的线定义了一个可拉伸区域,底部和右侧的线定义了一个绘制区域。第一张图片中灰色虚线标识了图片拉伸的时候哪些像素会被复制。第二章图片的粉红色矩形标识了视图的内容将被绘制在什么地方。如果这个绘制区域和视图内容尺寸不匹配,那么图像将被拉伸以适应视图内容。
Draw 9-patch 是一个所见即所得图形编辑器,它提供了一个非常方便的方式来创建 NinePatch 图片。它甚至还对拉伸区域像素复制结果的风险提出警告。
Example XML
下面是一些演示如何为按钮添加 NinePatch 图片的 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:background="@drawable/my_button_background"
android:text="Tiny"
android:textSize="8sp" /> <Button
id="@+id/big"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerInParent="true"
android:background="@drawable/my_button_background"
android:text="Biiiiiiig text!"
android:textSize="30sp" />
注意,宽高都设置为“wrap_content”,就可以使该按钮整齐地适应文本内容。