画布和可绘制对象

Android框架API提供了一组2D描画API,使用这些API能够在一个画布(canvas)上渲染自己的定制图形,也能够修改那些既存的View对象,来定制它们的外观和视觉效果。在绘制2D图形时,通常要使用以下两种方法中的一种:

  • 把图形或动画绘制到布局中的一个View对象中。在这种方式中,图形的绘制是由系统通常的绘制View层次数据的过程来处理的---只需简单的定义要绘制到View对象内的图形即可。

  • 把图形直接绘制在一个画布对象上(Canvas对象)。这种方法,要亲自调用相应类的onDraw()方法(把图形传递给Canvas对象),或者调用Canvas对象的一个draw…()方法(如drawPicture())。在这个过程中,还可以控制任何动画。

当想要把不需要动态变化和没有游戏性能要求的一个简单的图形绘制到View对象时,方法一是最好的选择。例如,在想要在一个静态的应用程序中,显示一个静态图形或预定义动画时,就应该用方法1把图形绘制到一个View对象中。
当应用程序需要经常重新绘制自己的时候,使用方法2把图形绘制到Canvas中,是一个比较好的选择。像视频游戏这样的应用程序,就应该在它们自己的Canvas对象上绘制图形。但是,有更多的方法来完成绘制任务:

  • 在与UI的Activity相同的线程中,创建布局中一个定制的View对象组件,就先要调用invalidate()方法,然后处理onDraw()回调方法;

  • 在一个独立的线程中,管理着SurfaceView对象,并且使用线程来执行把图形绘制到Canvas对象上的任务(不需要请求invalidate()方法)。

用Canvas对象来绘制图形(Draw with a Canvas)

当要编写专业的绘图或控制图形动画的应用程序时,应该使用Canvas对象来尽心绘制操作。Canvas用一个虚拟的平面来工作,以便把图形绘制在实际的表面上---它持有所有的用draw开头的方法调用。通过Canvas对象,实际上是执行一个底层的位图绘制处理,这个位图被放置到窗口中。

在onDraw()回调方法的绘制事件中,会提供一个Canvas对象,并且只需要把要绘制的内容交给Canvas对象就可以了。在处理SurfaceView对象时,还可以从SurfaceHolder.lockCanvas()方法来获取一个Canvas对象。但是,如果需要创建一个新的Canvas对象,那么就必须在实际执行绘制处理的Canvas对象上定义Bitmap对象。对于Canvas对象来说,这个Bitmap对象是始终必须的,应该像以下示例这样建立一个新的Canvas对象:

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

现在就可以在被定义的Bitmap对象上绘图了。在Canvas对象上绘制图形之后,能够用Canvas.drawBitmap(Bitmap, …)的一个方法,把该Bitmap对象绘制到另一个Canvas对象中。通过View.onDraw()方法或SufaceHolder.lockCanvas()方法提供的Canvas对象来完成最终的图形绘制处理是被推荐的。

Canvas类有可以使用的自己的一组绘图方法,如drawBitmap(…)、drawRect(…)、drawText(…)等。还可以使用其他的有draw()方法类。例如,可能想要把某些Drawable对象放到Canvas对象上。Drawable类就有带有Canvas对象作为参数的draw()方法。

在View对象上绘图

如果应用程序不需要大量的图形处理或很高的帧速率(如一个棋类游戏、Snake游戏或另外的慢动画类应用程序),那么就应该考虑创建一个定制的View组件,并且用该组件的View.onDraw()方法的Canvas参数来进行图形绘制。这么做最大的方便是,Android框架会提供一个预定义的Canvas对象,该对象用来放置绘制图形的调用。

从继承View类(或其子类)开始,并定义onDraw()回调方法。系统会调用该方法来完成View对象自己的绘制请求。这也是通过Canvas对象来执行所有的图形绘制调用的地方,这个Canvas对象是由onDraw()回调方法传入的。

Android框架只在必要的时候才会调用onDraw()方法,每次请求应用程序准备完成图形绘制任务时,必须通过调用invalidate()方法让该View对象失效。这表明可以在该View对象上进行图形绘制处理了,然后Android系统会调用该View对象的onDraw()方(尽管不保证该回调方法会立即被调用)。

在定制的View组件的onDraw()方法内部,使用给定的Canvas对象来完成所有的图形绘制处理(如Canvas.draw…()方法或把该Canvas对象作为参数传递给其他类的draw()方法)。一旦onDraw()方法被执行完成,Android框架就会使用这个Canvas对象来绘制一个有系统处理的Bitmap对象。

注意:为了在一个线程中而不是主Activity的线程中发出一个失效请求,必须调用postInvalidate()。

示例程序,请看Snake游戏,它在SDK示例代码文件:/samples/Snake/。

在SurfaceView对象上绘图

SurfaceView对象是一个特殊的View类的子类,它在View层次树内提供了一个专用的图形绘制平面。这个图形绘制表面的主要目的是给应用程序提供一个辅助线程,以便应用程序不需要等待完成对系统的View层次树的绘制。相反,引用SurfaceView对象的辅助线程能够按照自己的节奏,把自己绘制在Canvas对象上。

首先需要创建一个继承SurfaceView类的子类。该子类还应该实现SurfaceHolder.Callback类,它是一个能够通知底层Surface类所发生的信息的接口。如Surface的创建、变化或销毁等。这些事件对于了解什么时候能够开始绘制图形、是否需要基于新的表面属性来进行调整、以及什么时候终止图形绘制和杀死某些任务,是至关重要的。在SurfaceView子类的内部也是定义辅助线程类的好地方,它会执行所有的把图形绘制到Canvas对象上的处理。

不要直接处理Surface对象,应该通过SurfaceHolder对象来处理它。因此,在SurfaceView子类被初始化的时候,要通过调用getHolder()方法来获得SurfaceHolder对象。然后应该通过调用addCallback()方法,来通知SurfaceHolder对象,它所能够接收SurfaceHolder回调对象(SurfaceHolder.Callback),然后再重写SurfaceView子类内部的每个SurfaceHolder.CallBacke方法。

为了能够在辅助线程内把图形绘制到Surface对象的Canvas对象上,必须把SurfaceHondler对象和用lockCanvas()方法获取的Canvas对象传递给辅助线程。现在就能够用给定的SurfaceHolder对象和Canvas对象开始图形绘制工作了,一旦用该Canvas对象完成了图形绘制任务,就要调用unlockCanvasAndPost()方法,把绘图用Canvas对象传递给该方法。现在Surface对象就会离开绘制图形的Canvas对象。每次想要重新绘制图形时,都要执行这个锁定和解锁的过程。

注意:每个通过SurfaceHolder对象获取的Canvas对象,它之前的状态都会被保留。为了正确的处理该图形,必须重新绘制整个Surface对象。例如,能够清除之前用drawColor()方法填充在Canvas对象中的颜色,或者是用drawBitmap()方法设置的背景图片。否则,就会看到之前被执行的图形绘制的轨迹。

示例应用程序,请看Lunar Lander游戏,它在SDK的示例文件夹:/samples/LunarLander/。

图形绘制

Android为绘制图形和图片提供了一个定制的2D图形类库。android.graphics.drawable包中能够找到用于绘制二维图形的共同的类。

本文讨论使用Drawable对象来绘制图形的基础知识,以及如何使用Drawable类的子类。关于使用Drawable对象来绘制帧动画的信息,请看绘制动画文档(http://developer.android.com/guide/topics/graphics/drawable-animation.html)
Drawable对象是对图形绘制的一般化抽象,你会发现很多用于绘制特殊类型图形的Drawable类的子类,包括BitmapDrawable、ShapDrawable、PictureDrawable、LayerDrawable等。当然,你也可以继承这些类,来定义自己的具有独特行为的Drawable对象。

有三种方法来定义和初始化一个Drawable对象:

  • 使用保存在项目资源中的一个图片

  • 使用定义Drawable对象属性的XML文件

  • 使用普通的类构造器

以下我们将重点讨论前两种技术(使用构造器技术对于开发者来说并不陌生)。

从资源图片中创建一个Drawable对象

通过引用项目资源中的一个图片文件,把图形添加到应用程序中是一种简单的方法。支持的图片格式包括:PNG(推荐格式)、JPG(可接受格式)、GIF(不推荐格式)。这种技术主要用于应用的图标、Logo或游戏中所使用的那些图形。
要使用图片资源,只需把图片添加到项目的res/drawable/目录中。应用程序代码或XML布局会从那儿来引用这些图片。无论在那儿使用图片,都要使用该图片资源的ID,这个ID是不带文件类型扩展名的文件名(如:my_image指向了my_image.png文件)。

注意:被放置在res/drawable/目录下的图片资源,在编译期间,能够被aapt工具自动的优化压缩成无损的图片。例如,一个不多于256色的真彩色PNG图片,能够被转换成一个带有调色板的8位PNG图片。这样就获得同等品质的图片,但却需要更少的内存。因此,要了解放置在这个目录下的图片,在编译期间的这种改变。如果为了把图片转换成一个位图,而计划用位流的形式来读取一张图片,就要把该图片放到res/raw/文件夹中,这个文件夹中的图片不会被优化。

示例代码

以下代码片段演示了如何创建一个使用绘制资源图片的ImageView对象,并把该对象添加到布局中:

LinearLayout mLinearLayout;

  protectedvoid onCreate(Bundle savedInstanceState){
  super.onCreate(savedInstanceState);

  // Create a LinearLayout in which to add the ImageView
  mLinearLayout =newLinearLayout(this);

  // Instantiate an ImageView and define its properties
  ImageView i =newImageView(this);
  i.setImageResource(R.drawable.my_image);
  i.setAdjustViewBounds(true);// set the ImageView bounds to match the Drawable's dimensions
  i.setLayoutParams(newGallery.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对象,那么只要改变了其中一个Drawable对象一个属性(如,透明度),另一个也会受到影响。因此在处理一个图片资源的多个实例时,要用补间动画(tween animation)来替代直接的Drawable对象的传递。

示例XML

以下XML片段显示了如何把一个绘制资源添加到XML布局中的一个ImageView元素中:

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

从资源XML中创建一个Drawable对象

现在,你应该已经熟悉了Android的用户界面的开发原则。因此也就理解了定义XML中对象所带来的强大功能和灵活性。这种方法从View对象到Drawable对象都适用。如果创建了一个Drawable对象,它初始并不依赖应用程序代码或用户界面中定义的一个变量,而是把它定义在一个XML中,这是一个好的选择。即使是在用户与应用程序交互期间要改变Drawable对象的属性,也应该考虑把该对象定义在XML中,这样一旦它被实例化,就可以随时来修改它的属性。

一旦用XML来定义Drawable对象,就要把该定义保存在工程的res/drawable/目录中,然后通过调用Resources.getDrawable()方法来获取和实例化该对象。

任何提供了inflate()方法的Drawable子类都能够被定义在XML中,并能够被应用程序实例化。每个提供XML填充能力的Drawable对象,都采用了特殊的XML属性来帮助定义对象的属性。有关每个Drawable子类如何在XML中定义的信息,请看对应类文档。

示例

以下XML定义了一个TransitionDrawable对象:

 <transitionxmlns:android="http://schemas.android.com/apk/res/android">
      <itemandroid:drawable="@drawable/image_expand">
      <itemandroid: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);

然后,这个过渡效果能够使用以下代码来运行:

transition.startTransition(1000);

形状绘制

在想要动态的绘制一些二维图形的时候,ShapeDrawable对象将会满足你的需要。用ShapeDrawable对象能够编程绘制任何能够想象得到的原始形状和主题样式。

ShapeDrawable类是Drawable类的一个子类,因此能够在任何期望使用Drawable对象的地方使用ShapeDrawable对象---如用setBackgroundDrawable()方法设置View对象的背景。当然,也可以用绘制的形状作为自己定制的View对象,然后把它添加到你的布局中。因为ShapeDrawable类有自己的draw()方法,所以能够在View.onDraw()方法执行期间创建一个绘制ShapeDrawable图形的View子类。以下代码只是这种处理一个基本的扩展,它用ShapeDrawable对象来绘制一个View视窗:

publicclassCustomDrawableViewextendsView{
      privateShapeDrawable mDrawable;

      publicCustomDrawableView(Context context){
      super(context);

      int x =10;
      int y =10;
      int width =300;
      int height =50;

      mDrawable =newShapeDrawable(newOvalShape());
      mDrawable.getPaint().setColor(0xff74AC23);
      mDrawable.setBounds(x, y, x + width, y + height);
      }

      protectedvoid onDraw(Canvas canvas){
      mDrawable.draw(canvas);
      }
}

在上例的构造器中,ShapeDrawable是作为一个OvalShape对象来定义的,然后给它设定了一个颜色和边框。如果不设置边框,那么形状就不会被绘制;如果没有设置颜色,那么默认的颜色是黑色。

用这个定制的View对象,能够绘制任何想要的形状。在上面的例子中,我们在一个Activity中用编程的方式绘制了一个形状:

CustomDrawableView mCustomDrawableView;

      protectedvoid onCreate(Bundle savedInstanceState){
      super.onCreate(savedInstanceState);
      mCustomDrawableView =newCustomDrawableView(this);

      setContentView(mCustomDrawableView);
      }

如果想要从XML布局中,而不是在Activity中来绘制这个定制的图形,那么CustomDrawable类必须重写View(Context, AttributeSet)构造器,该构造器会在从XML中填充View对象时被调用。然后把这个CustomDrawable元素添加到XML中,如:

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

ShapeDrawable类(像在android.graphics.drawable包中的其他一些Drawable类型)允许用公共的方法定义各种属性。其中有些属性可能需要调整,包括:透明度、颜色过滤、抖动、不透明和颜色等。

也能够使用XML定义初始的绘制形状。更多的信息,请阅读绘图资源(Drawable Resources)文档中的形状绘制(Shape Drawables)一节

Nine-patch

NinePatchDrawable图形是可拉伸的位图图片,Android系统会根据View对象中的内容来自动的调整背景图片。使用NinePatch图片的一个例子就是标准Android按钮的背景图片---按钮必须根据字符串的长度来拉伸背景图片。NinePathc图形绘制的是一个标准的PNG图片,它包含了一个像素宽的边框。图片文件的扩展名必须是.9.png,并且要保存到工程的res/drawable/目录中。

边框被用于定义图片的拉伸和静态区域。通过在边框的左边和上边(其他边框的像素应该完成透明或是白色的)绘制一个或多个1像素宽的黑线来指定一个可拉伸的区域。可有多个可拉伸的区域,但它们的相对尺寸都相同,最大的区域始终要保持最大的区域。

还可以通过绘制右边线和底边线来定义一个图片的可选绘制区域(有效的填充线)。如果一个View对象设置NinePatch图片作为它的背景,并且给该View对象指定了文本,那么它就会自我拉伸,以便所有的文本都能够被填充在由右边线和底边线(如果包括的话)所设计的内部区域,如果不包括填充线,Android系统会使用左边线和上边线来定义该绘制区域。

要澄清不同边线间的差异,为了拉伸图片,左边线和上边线定义的图片的像素被允许复制。底边线和右边线定义了图片内相对区域,View对象的内容被允许放到这个区域内。

下图是用于定义按钮的一个NinePatch图片文件:

这个NinePatch图片用左边线和上边线定义了一个可拉伸的区域,用右边线和底边线定义了一个可绘图的区域。为了拉伸图片,在上面的那个图片中,灰色的点划线定指定了图片将要被重复的区域。在下面的那个图片中,粉色的矩形指明了View的内容被允许放置的区域。如果该区域不同完全填充View对象的内容,那么该图片就会被拉伸,直到内容被完全填充。

Draw 9-patch工具,使用WYSIWYG图形编辑器,提供非常方便的创建NinePatch图片的方法。如果定义的可拉伸区域在绘制构件的过程中存在像素复制的风险,它甚至会产生一个警告。

示例XML

该布局XML演示了如何把一个NinePatch图片添加到一对按钮中(NinePatch图片被保存在res/drawable/my_button_background.9.png中):

<Buttonid="@+id/tiny"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerInParent="true"
        android:text="Tiny"
        android:textSize="8sp"
        android:background="@drawable/my_button_background"/>

<Buttonid="@+id/big"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerInParent="true"
        android:text="Biiiiiiig text!"
        android:textSize="30sp"
        android:background="@drawable/my_button_background"/>

要注意的是,宽度和高度属性都被设置成了”wrap_content”,以便按钮能够根据文本尺寸来调整大小。!

以下是使用上面显示的图片和XML定义所展现的两个按钮。注意,按钮的宽度和高度是如何根据文本的尺寸来拉伸背景图片的。

矢量图片(机翻T.T)

一个VectorDrawable与一个单独的的矢量图形代替多个PNG资源。点、线、曲线以及其相关的颜色信息作为一个集合定义在一个XML文件中

矢量图形的主要优势是其的可扩展性。在大小不同的屏幕中使用矢量图形可以不损图像质量,这样可以减小您的APP尺寸以及减少开发人员的工作量。You can also use vector images for animation by using multiple XML files instead of multiple images for each display resolution.

让我们通过一个例子来了解一下。一个100dp x 100dp的图像在一个较小的屏幕中可以渲染的很好,在更大的设备上,应用程序可能需要使用一个400dp x 400dp的图像。通常情况下,开发人员需要为一个图像创建多个多个版本,以满足不同的屏幕密度。这种方法加大了工作量也让我们的APP体积变得更大。

在Android 5.0(API级别21)中,通过VectorDrawable和AnimatedVectorDrawable这两个类来将矢量图形作为一个图形资源,更多使用VectorDrawable和AnimatedVectorDrawable的信息可以去阅读这些: About VectorDrawable class and About AnimatedVectorDrawable class

VectorDrawable类

VectorDrawable定义了一个静态的Drawable对象。类似于SVG格式,每个矢量图形被定义为一个树的一层,它是由路径和组对象组成。Each path contains the geometry of the object's outline and group contains details for transformation. All paths are drawn in the same order as they appear in the XML file.

XML例子

这里是一个简单的VectorDrawable XML文件显示在充电模式电池图像。

<!-- res/drawable/battery_charging.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    <!-- intrinsic size of the drawable -->
    android:height="24dp"
    android:width="24dp"
    <!-- size of the virtual canvas -->
    android:viewportWidth="24.0"
    android:viewportHeight="24.0">
   <group
         android:name="rotationGroup"
         android:pivotX="10.0"
         android:pivotY="10.0"
         android:rotation="15.0" >
      <path
        android:name="vect"
        android:fillColor="#FF000000"
        android:pathData="M15.67,4H14V2h-4v2H8.33C7.6,4 7,4.6 7,5.33V9h4.93L13,7v2h4V5.33C17,4.6 16.4,4 15.67,4z"
        android:fillAlpha=".3"/>
      <path
        android:name="draw"
        android:fillColor="#FF000000"
        android:pathData="M13,12.5h2L11,20v-5.5H9L11.93,9H7v11.67C7,21.4 7.6,22 8.33,22h7.33c0.74,0 1.34,-0.6 1.34,-1.33V9h-4v3.5z"/>
   </group>
</vector>

此XML呈现以下图像:

关于AnimatedVectorDrawable类

AnimatedVectorDrawable adds animation to the properties of a vector graphic. You can define an animated vector graphic as three separate resource files or as a single XML file defining the entire drawable. Let's look at both the approaches for better understanding: Multiple XML files and Single XML file.

多个XML文件

通过使用这种方法,您可以定义三个独立的xml文件:

  • 一个VectorDrawable XML文件。

  • An AnimatedVectorDrawable XML file that defines the target VectorDrawable, the target paths and groups to animate, the properties, and the animations defined as ObjectAnimator objects or AnimatorSet objects.

  • 一个动画的XML文件。

多个XML文件的例子

下面的XML文件演示了矢量图形的动画。

  • vectordrawable的XML文件:vd.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
   android:height="64dp"
   android:width="64dp"
   android:viewportHeight="600"
   android:viewportWidth="600" >
   <group
      android:name="rotationGroup"
      android:pivotX="300.0"
      android:pivotY="300.0"
      android:rotation="45.0" >
      <path
         android:name="vectorPath"
         android:fillColor="#000000"
         android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
   </group>
</vector>
  • animatedvectordrawable的XML文件:avd.xml
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
   android:drawable="@drawable/vd" >
     <target
         android:name="rotationGroup"
         android:animation="@anim/rotation" />
     <target
         android:name="vectorPath"
         android:animation="@anim/path_morph" />
</animated-vector>
  • 在用于animatedvectordrawable的XML文件,XML文件:rotation.xml和path_morph.xml动画师
<objectAnimator
   android:duration="6000"
   android:propertyName="rotation"
   android:valueFrom="0"
   android:valueTo="360" />
<set xmlns:android="http://schemas.android.com/apk/res/android">

```xml

#### 单一的XML文件
通过使用这种方法,您可以通过XML捆绑格式将相关的XML文件合并到一个单一的XML文件中。在构建应用程序时,AAPT标签创建动画载体单独的资源和参考他们。这种方法需要建立工具24或更高,并且输出是向后兼容的。

**一个单一的XML文件的例子**
```xml
<animated-vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt">
    <aapt:attr name="android:drawable">
        <vector
            android:width="24dp"
            android:height="24dp"
            android:viewportWidth="24"
            android:viewportHeight="24">
            <path
                android:name="root"
                android:strokeWidth="2"
                android:strokeLineCap="square"
                android:strokeColor="?android:colorControlNormal"
                android:pathData="M4.8,13.4 L9,17.6 M10.4,16.2 L19.6,7" />
        </vector>
    </aapt:attr>
    <target android:name="root">
        <aapt:attr name="android:animation">
            <objectAnimator
                android:propertyName="pathData"
                android:valueFrom="M4.8,13.4 L9,17.6 M10.4,16.2 L19.6,7"
                android:valueTo="M6.4,6.4 L17.6,17.6 M6.4,17.6 L17.6,6.4"
                android:duration="300"
                android:interpolator="@android:interpolator/fast_out_slow_in"
                android:valueType="pathType" />
        </aapt:attr>
    </target>
</animated-vector>
Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1