通过翻译开发指南学习画布和图片

1.前言


安卓框架提供了一组2D绘图的APIs,允许在画布上渲染自定义图形或者修改已存在的View来定义外观和感觉。绘制2D图形通常有两种途径:
  a.给布局中的View对象绘制图形和动画。由系统的视图层次结构处理绘制过程,你只需定义视图内的图形。
  b.直接将图形绘制到画布上。你自己调用适当类的 onDraw() 方法(传入画布)或者Canvas对象的 draw...() 方法(就像 drawPicture() 方法),这样也可以控制任何动画。
  选项“a”,当绘制的是不需要动态变化的简单图形和非性能密集型游戏的一部分时,在视图上绘制是最好的选择。例如,显示静态图形或预定义的动画。详细信息看图形这一章节。
  选项“b”,当应用需要经常重绘时,在画布上绘制是较好的选择。例如,电子游戏等。有两种方式去实现:

  • 与UI Activity同一线程时,给布局创建自定义视图组件,需要调用 invalidate() 方法和处理 onDraw() 方法的回调。
  • 一个单独的线程时,管理一个SurfaceView,在画布上以线程支持的最快速度绘制(不需要请求 invalidate())。

2.使用画布绘制


写程序时,若希望执行专门的绘图和/或控制图形的动画,应该在画布上绘制。画布只是表面的封装,负责所有 draw...() 方法的调用,图形传递给实际的Surface上,摆放在窗口中的底层的位图。
  如果在 onDraw() 回调方法内绘制,调用提供的Canvas的 draw...() 方法即可。处理SurfaceView对象时,通过 SurfaceHolder.lockCanvas() 方法获取Canvas对象。(这两种情况在下面的章节中都有讨论)如果需要创建新的Canvas对象,必须定义实际执行绘制的Bitmap对象。画布总是需要位图配合,创建新画布如下:

Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);

现在画布将在给定的位图上绘制,绘制完后,通过 drawBitmap(Bitmap,...) 方法可将位图带给另外画布。建议在 View.onDraw() 或 SurfaceHolder.lockCanvas() 方法提供的画布上绘制最终图形(详见下面的章节)。

2.1.绘制在View上

如果你的应用不需要大量的处理或好高的帧速率(也许是象棋游戏,贪吃蛇游戏或者缓慢的动画应用),应该创建自定义视图组件和在 View.onDraw() 方法内用画布绘图。这样做方便的是,安卓系统将提供预定义的Canvas对象来调用 draw...() 方法。
  首先,继承View类(或其子类)和定义 onDraw() 回调方法。当视图绘制自己时,安卓系统将调用这个方法,通过传入的Canvas对象执行所有的绘制操作。
  安卓系统只会在必要时调用 onDraw() 方法。每当应用准备好绘制时,调用 invalidate() 方法废除当前的视图,同时告知安卓系统调用 onDraw() 方法(不能保证回调是瞬时的)。
  在视图组件的 onDraw() 方法内,使用提供的Canvas对象的各种 draw...() 方法或其它类的 draw() 方法(以提供的Canvas对象为参数)完成所有绘图。一旦 onDraw() 方法完成,安卓系统将使用画布绘制位图。

注意:为了从非主线程刷新界面,必须调用 postInvalidate() 方法。

关于扩展View类的更多信息,阅读 read Building Custom Components

2.2.绘制在SurfaceView上

SurfaceView是View的一个专注于绘图的子类,目的是在应用的子线程中提供绘图功能,那样应用不需要等待系统视图结构的绘制完成。反而,与SurfaceView相关联的子线程可以按照自己的频率在画布上绘制。
  首先,需要创建一个类继承SurfaceView,同时实现SurfaceHolder.Callback接口。它可以提供Surface的底层信息,例如,什么时候被创建、被改变或者被销毁。这些信息很重要,可以知道何时开始绘图,是否需要根据新的Surface属性进行调整,何时停止绘图,以及杀死某些任务。在SurfaceView类内部定义子线程,以便在你的画布上执行所有的绘图过程。
  对于Surface的操作应该通过SurfaceHolder而不是直接处理。当SurfaceView被初始化后,调用 getHolder() 方法获取SurfaceHolder。通过 addCallback() 方法(参数为this)可以将回调对象传入SurfaceHolder以获取通知,而被回调的方法需在SurfaceView类中重写。
  为了在子线程中通过画布绘制,需要将SurfaceHolder对象传进线程,并调用它的 lockCanvas() 方法获取画布,有了画布就可以进行必要的绘制操作了。画完后,调用 unlockCanvasAndPost() 方法解锁和传递画布对象,这样,内容才会显示到画布上。每当要重绘时,先锁定画布再解锁画布,代码看这篇文章

注意:每次从SurfaceHolder获取画布,画布之前的状态将会被保留。为了正确展示动画,你必须重绘整个Surface。比如,调用 drawColor() 方法填充一种颜色或者调用 drawBitmap() 方法设置一个背景图像来清除画布之前的状态。否则,会在画布上看到之前绘制过的痕迹。

3.图形


安卓提供了一个自定义2D图形库,用于绘制形状和图像。这些在两个维度上绘制的公共类放在 android.graphics.drawable 包中。
  本文讨论使用Drawable对象绘制图形的基本知识和如何使用Drawable子类。关于使用图形完成帧动画,详见 Drawable Animation
  图形通常指可以绘制的东西,它的子类定义了各种具体的可绘图形,包括BitmapDrawable,ShapeDrawable,PictureDrawable,LayerDrawable等。当然,也可以继承这些类,来实现自己想要的、具有独特行为的Drawable对象。
  有三种方式定义和实例化Drawable对象:使用保存在项目资源中的图像;使用XML文件定义Drawable属性;使用正常的类构造函数。下面,将分别讨论前两种技术(第三种就是代码的调用,功能最全也不难)。

3.1.使用图像资源创建

向应用程序添加图形的简单方法是引用项目资源中的图像文件,支持的文件类型有:PNG(首选),JPG(可接受),GIF(不建议)。这种技术显然更适合应用程序图标、徽标或其它情况(如在游戏中使用)。
  使用图像资源,仅仅需要将文件添加到你项目的 res/drawable/ 目录下,再从代码和XML布局中引用。无论哪种方式,都涉及到使用资源ID,一种没有文件类型扩展名的文件名(例如,my_image.png 用my_image引用)。

注意:放在 res/drawable/ 目录下的图像资源可能会被aapt工具在编译过程中使用图像无损压缩进行自动优化,比如,一个真的不需要超过256种颜色的PNG可能会被颜色调色板转换成8位的PNG。在图像质量不变的情况下,可以使用较少的内存。因此,需要认识到,这个目录下的图像二进制文件在编译的过程中会改变。如果计划用位流读取图像转换成位图,最好将图像放到 res/raw/ 目录下,避免被优化。

示例代码(使用图形资源创建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对象来处理图像资源。那么,通过资源创建Drawable对象如下:

Resources res = mContext.getResources();
Drawable myImage = res.getDrawable(R.drawable.my_image);

项目中每个独特的资源,不管实例化多少个不同的对象,只能维持一种状态。例如,对同一个图像资源实例化两个Drawable对象,改变其中一个对象的一个属性(拿透明度来说),将会影响另一个。所以,处理一个对象的多个资源时,使用补间动画来改变图形比直接操作要好。

示例XML(在XML布局中给ImageView添加图形资源)

<ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:tint="#55ff0000"
        android:src="@drawable/my_image"/>

关于使用项目资源的详细信息,阅读 Resources and Assets

3.2.使用XML资源创建

若对安卓开发用户界面的原则熟悉,应该清楚在XML中定义对象所固有的能力和灵活性,这种理念从View贯穿到Drawable。如果想创建的Drawable对象不依赖于应用程序代码或者用户交互,那么在XML中定义是个好的选择。即使期望图形随着用户的使用而改变属性,你也应该认识到在XML中定义对象可以随时修改初始化时的属性。
  一旦在XML中定义Drawable对象,文件需保存到项目的 res/drawable/ 目录下。然后调用 Resources.getDrawable() 方法,根据XML文件的资源ID获取和初始化对象。
  任何支持 inflate() 方法的Drawable子类能够被XML定义和被应用初始化,它们特有的XML属性能够找到对应的对象属性(详见类参考)。
  示例

// 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>

// 实例化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);

4.形状图片


当想要动态地绘制两个维度的图形时,ShapeDrawable对象将满足需求,可以通过编程的方式绘制原始形状和定义任何样式。
  ShapeDrawable继承自Drawable类,可以被当作Drawable对象使用,比如,调用 setBackgroundDrawable() 方法给视图设置背景。当然也可以给自定义视图绘制自己的形状,但记得添加到自己的布局中。因为ShapeDrawable对象有自己的 draw() 方法,所以在View子类的 View.onDraw() 方法中调用ShapeDrawable对象的 draw() 方法。下面是对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对象被定义为椭圆形,然后给了颜色和大小。如果不设置形状,将不会绘制;如果不设置颜色,将默认黑色。
  在自定义视图中可以绘制任何想要的样子。我们可以将上面示例的形状用代码绘制到Activity中:

CustomDrawableView mCustomDrawableView;

protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  mCustomDrawableView = new CustomDrawableView(this);

  setContentView(mCustomDrawableView);
}

若要通过XML布局自定义图形而不是在Activity中设置,CustomDrawableView类必须重写 View(Context, AttributeSet) 构造函数,因为从XML实例化View对象时将会调用。接着将CustomDrawableView元素添加到Activity的XML布局中,如下:

<com.example.shapedrawable.CustomDrawableView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        />

ShapeDrawable类(像 android.graphics.drawable 包中其它Drawable类型一样)允许通过公开的方法定义各种属性,包括alpha、滤色器、震动、不透明度和颜色。
  使用XML也可以定义原始形状,详细信息参考 Drawable Resources文档中ShapeDrawable章节。

5.可伸缩图片


NinePatchDrawable是一个可伸缩的位图图像,在标准PNG图像的基础上加了1像素宽的边框。当它作为背景时,安卓会自动调整大小以适应视图中的内容。它必须以.9.png为扩展名保存在项目的 res/drawable/ 目录下。
  NinePatch图分为两个部分,左、上为定义拉伸区,右、下为定义内边距(里面是内容区),如下图:

NinePatch.png

将它作为Button的背景图时,实际效果如下:

Button.png

6.矢量图片


VectorDrawable对象由定义在XML文件中的一系列点、线、曲线和相关的颜色信息组成。安卓5.0(API 21)开始,VectorDrawable和AnimatedVectorDrawable这两个类支持矢量图形作为图形资源。之前若想使用,支持库23.2或更高也提供矢量图形和动态矢量图形的支持。
  关于使用矢量图形系统APIs或矢量图形支持库的更多信息,前往 Vector Drawable

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容