概述:
Android framework提供了一组2D绘制API, 让我们可以在一个canvas上渲染我们自定义的图形, 或者修改View成自定义的外观. 绘制2D图形的经典方式如下:
a. 在自己layout的View上绘制图形或者动画. 这种方式下, 绘制工作由Android的普通View层绘制完成, 我们只需要简单的将图形放入View即可.
b. 在Canvas上绘制自己的图形. 这种方式下, 我们必须调用合适的类的onDraw()方法(传入我们自定义的Canvas), 或者Canvas的某个draw…()方法(比如drawPicture()). 这样一来我们也可以控制所有的动画了.
选项a, 绘制在一个View上. 如果我们绘制简单的不需要动态改变的图形, 或者不是性能密集型游戏的时候, 这种做法是我们的最佳选择. 比如如果我们想要显示一张静态的图片或者一个预定义的动画的时候, 那么我们应该使用这种方式.
选项b, 在一个Canvas上绘制. 如果我们的APP需要经常重绘它自己的时候, 用这种方式更加合适. 通常一款游戏APP应该使用Canvas. 实现这种方式有不止一种方法:
1. 在UI线程中执行. 我们在自己的layout中创建自定义的View组件, 并调用它的invalidate()方法然后实现onDraw()回调方法.
2. 在独立的线程中执行. 我们操作一个SurfaceView, 然后以最快的速度向Canvas中绘制图形. 这种情况下不需要invalidate().
使用Canvas绘图:
当我们写一个需要专业绘图功能或者要对图像进行动画控制的APP的时候, 我们就需要Canvas. 绘制事件的操作在onDraw()方法中, Canvas就像一块画布, 我们需要做的就是将图绘制在Canvas上面. 当处理一个SurfaceView对象的时候, 我们可以通过SurfaceHolder.lockCanvas()方法获取一个Canvas. 如果我们需要创建一个新的Canvas,那么必须在绘制执行之前为其定义一个Bitmap. Bitmap是Canvas必须的. 我们可以这样设置一个新的Canvas:
Bitmap b = Bitmap.createBitmap(100,100,Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
这样我们的Canvas就会将这个bitmap绘制在它上面了. 在绘制完了之后我们可以将Bitmap从一个Canvas通过Canvas.drawBitmap(Bitmap, …)方法转移到另一个Canvas. 官方推荐我们通过View.onDraw()或者SurfaceHolder.lockCanvas()方法绘制最终的图片到一个Canvas.
Canvas类拥有它自己的绘制方法, 比如drawBitmap(…), drawRect(…), drawText(…)等. 其它我们可能用到的类也同样拥有draw()方法. 比如我们可能拥有一些Drawable对象, 我们希望可以将它们放入Canvas显示. Drawable拥有其自己的draw()方法并将Canvas作为它的参数.
在View上绘图:
如果我们的APP不需要大量的处理图像或者高帧率的图像, 那么应该考虑创建一个自定义的View组件并且在View.onDraw()方法中使用Canvas绘制图形. 这样做最方便之处在于Android framework将会为我们提供一个预定义的Canvas让我们在上面绘制想要的东西.
想要实现上面的步骤, 首先需要继承View类(或者其子类), 然后定义onDraw()方法. 该方法将会在View绘制它自己的时候被Android framework调用. 我们可以在这里处理所有的绘制操作. onDraw()方法会传入一个Canvas, 只需要对其进行绘制既可. Android framework将会在需要的时候调用onDraw()方法. 在APP想要重绘的时候, 必须调用invalidate()方法. 这表示我们想要View被绘制, 然后Android将会调用其onDraw()方法.
在View组件的onDraw()内部, 我们可以使用传入的Canvas的draw…()方法, 或者其它类的draw(Canvas)方法来实现所有的绘制操作. 一旦onDraw()方法结束了, Android framework将会使用我们的Canvas来绘制一个系统持有的Bitmap.
如果想要重绘一个非主线程的View, 我们必须调用postInvalidate()方法.
在SurfaceView上绘图:
SurfaceView是一个特别的View子类, 它提供了view层专用的绘图层. 这样可以为绘制surface提供一个应用的辅助线程, 而不需要等待系统View层准备好了才能绘制. 辅助线程可以绘制它自己的Canvas.
想要实现这个, 首先需要创建一个继承SurfaceView的类. 该类还应该实现SurfaceHolder.Callback接口. 这个子类将会通知我们底层Surface的事件, 比如它什么事件创建, 改变或者销毁. 这些事件很重要, 这样我们就可以知道什么时候应该开始绘制, 是否需要根据新的surface属性进行调整, 以及什么时候停止绘制和停止任务. 在我们的SurfaceView类中, 也可以定义自己的辅助线程类, 它将会处理所有的给Canvas的绘制工作.
我们不应该直接处理Surface对象, 而应该通过SurfaceHolder类处理它. 当我们的SurfaceView初始化的时候, 通过getHolder()取得SurfaceHolder. 然后应该通过addCallback()方法提示SurfaceHolder我们将要接收它的回调(从SurfaceHolder.Callback). 然后重写SurfaceHolder.Callback中的方法.
为了从辅助线程中绘制Surface Canvas, 我们必须将SurfaceHandler传递给线程, 然后通过lockCanvas()检索Canvas. 我们现在就可以在Canvas上绘制想要的东西了. 一旦绘制结束之后, 调用unlockCanvasAndPost()方法, 将Canvas对象传入. 现在Surface将会绘制我们的Canvas了. 每次我们想要重新绘制的时候, 都需要再次处理locking和unlocking操作.
Drawable:
Android提供了一个自定义2D图像库来给我们绘制形状和图像. 我们将会在android.graphics.drawable包中找到我们可以用来绘制二维图像的常用类.这个小节将会讨论使用Drawable对象绘制图像和如何使用几个drawable的子类的基本用法.
Drawable表示”一些可以被绘制出来的东西”.它被扩展为多个应用于不同对象的子类, 包括BItmapDrawable, ShapeDrawable, PictureDrawable, LayerDrawable等. 当然我们还可以根据需求来自定义drawable类. 有三种方式可以定义和初始化一个Drawable: 使用一个保存在工程中的图片资源文件; 使用一个定义了drawable属性的XML文件; 使用普通的类构造方法. 下面将介绍前两种方法.
从图片资源文件创建Drawable:
一个很简单的使用图片的方法就是使用代码关联到工程中的图片资源文件. 可以支持的文件类型有: PNG, JPG和GIF. 这种方法十分适用于APP的图标, logo或者其他图片, 比如在游戏中的图片. 想要使用一个图片资源只需要将图片文件添加到工程的res/drawable/目录下. 从那里我们可以从其他XML文件或者代码中关联这个图片资源. 不管在哪, 我们都需要使用资源ID来关联图片资源, ID不带扩展名, 比如my_image.png关联的时候就是my_image. 这里需要注意的是, 保存在res/drawable/目录下的图片文件可能会在编译的时候被aapt工具自动无损压缩. 比如一个真彩色PNG图片不需要超过256色, 那么它可能会被压缩为8位的PNG. 无损压缩会在不损失质量的情况下减少消耗的内存. 所以要留意这一点:在编译的时候, 图像的二进制排列可能会被修改. 如果我们打算作为二进制流来读取一个图片, 需要将图片放在res/raw/文件夹下, 在这里它们不会被优化.
代码栗子: 下面的代码演示了我们应该如何通过图片资源来创建一个ImageView, 并将其加入到layout文件中.
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对象, 然后其中一个修改了它的属性(比如透明度), 那么它也会影响到另一个. 所以当我们处理同一个图像资源的多个实例的时候, 我们应该使用一个tween animation, 而不是直接改变drawable.
XML栗子:
下面的XML文件片段演示了如何在ImageView中添加一个drawable资源:
<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文件中. 就算跟用户交互的时候这个对象的某些属性可能会改变, 我们也应该考虑将其定义在XML文件中, 因为一旦初始化之后我们依然可以修改它的属性.
一旦定义了我们自己的drawable XML文件, 我们应该将它们保存在工程目录的res/drawable/目录下. 然后通过Resource.getDrawable()方法获取并实例化对象. Resource.getDrawable()方法接收一个resource ID作为参数. 任何可以支持inflate()方法的drawable子类都可以定义在XML文件中, 并被APP实例化. 每个drawable
栗子:
这是定义了一个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>
将这个文件保存为res/drawable/expand_collapse.xml, 下列代码将会实例化TransitionDrawable并作为content设置给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可以通过下列代码运行:
transition.startTransition(1000);
Shape drawable:
当我们想要动态绘制二维图像的时候, 一个ShapeDrawable对象或许可以满足我们的需要. 通过一个ShapeDrawable我们可以通过编程绘制其初始形状并使用任何可以想到的样式. ShapeDrawable是Drawable的扩展, 所以我们可以在任何支持Drawable的地方使用它, 比如View的背景, 我们可以通过setBackgroundDrawable()方法来设置. 当然我们还可以使用自定义的View来绘制自己的形状. 因为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 = newShapeDrawable(newOvalShape());
mDrawable.getPaint().setColor(0xff74AC23);
mDrawable.setBounds(x, y, x+ width, y+ height);
}
protected void onDraw(Canvas canvas){
mDrawable.draw(canvas);
}
}
在构造方法中, ShapeDrawable通过OvalShape初始化, 也就是一个椭圆形. 然后对其设置了边界和颜色. 如果我们不设置边界, 那么shape将不会被绘制, 如果不设置颜色, 默认情况是黑色.
定义了自定义的Viwe之后, 它就可以按照我们喜欢的方法绘制了. 比如在Activity中:
CustomDrawableView mCustomDrawableView;
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
mCustomDrawableView = new CustomDrawableView(this);
setContentView(mCustomDrawableView);
}
如果我们想要从XML文件中使用这个自定义的drawable而不是从Activity中. 那么自定义的Drawable必须重写View(Context, AttributeSet)构造方法, 它会在通过XML文件实例化的时候调用. 然后将这个自定义drawable添加到XML中, 像这样:
<com.example.shapedrawable.CustomDrawableView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
ShapeDrawable允许我们通过public方法定义多种属性(就好像其他的android.graphics.drawable包中的drawable类型一样).我们可能想要调整其中的一些属性比如透明度, 颜色过滤器, 颜色等.
Nine-patch:
NinePatchDrawable是一种可以扩展的Bitmap图片, Android将会自动重新调整尺寸来适应自己的内容. 它的一个应用的栗子是Android的标准按钮的背景图, 因为这些按钮必须调整自己的尺寸以适应它上面的字符串. NinePatch drawable是一个包含额外1像素边界的标准PNG图片. 它必须以.9.png作为扩展名, 并且保存在res/drawable/目录下.
它的边界是用来定义图片的可扩展性和静态区域的. 我们通过在顶部和左侧边缘绘制一个(或者多个)1像素宽的黑线来标明图片的扩展性(旁边的像素应该是白色或者全透明的). 我们可以有很多的扩展部分, 它们相对的规模保持不变, 所以最大的永远都是最大的.
我们还可以通过在右侧和底部划线来定义一个可选的drawable区域(使用填充线). 如果有一个View设置了一个NinePatch作为背景图并且指定了View的text, 它将会拉伸自己让所有的text都处于指定的区域内(如果有的话). 如果没有填充线的话, Android将会直接使用左边和下边的线作为它的drawable区域.
左边和上边的线规定的是可拉伸的区域, 当图片被拉伸的时候, 这个部分里面的区域可以被缩放. 而右边和下边的线指定的是填充区域, View的text只会存在于这个指定的区域内. 下面是一个使用NInePatch定义的button:
这个NinePatch通过左边线和上边线定义了一个可拉伸的区域, 并通过右边线和下边线定义了一个drawable区域. 在上半张图片中, 灰色虚线识别的区域在拉伸的时候将会被复制. 在下半张图片中, 粉色的矩形区域内部是允许填充内容的区域, 如果内容超过了该区域, 那么它将会被拉伸.
Draw-9-patch工具通过一个WYSIWYG图片编辑器提供了非常便捷的创建NinePatch的方法.
ExampleXML:
下面是如何将NinePatch在XML文件中添加到button中做背景图片的栗子(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:text="Tiny"
android:textSize="8sp"
android:background="@drawable/my_button_background"/>
<Button id="@+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”, 这样才能根据内容缩放button的尺寸. XML文件中的button显示出来是下图的样子, 留意按钮的高度和宽度如何根据文本的尺寸的变化而变化, 以及背景图片的拉伸情况.
总结:
这里介绍的主要是Canvas, drawable和NinePatch的用法.
Drawable是非常常用的类, 在处理图片的时候总是离不开它, 用法也比较简单, 最常用的莫过于设置背景图片.
NinePatch是只有Android环境下才有的资源类型, 可以设置图片的拉伸区和填充区, 使得图片在拉伸的时候可以保持清晰度, 不至于变形, 默认情况Android的按键就是这种类型. 通常图片尺寸不固定的时候需要使用这种技术, 比如聊天框背景.
Canvas是用来绘制的画布, 可以通过它的方法做一些绘图操作.
参考: https://developer.android.com/guide/topics/graphics/2d-graphics.html