1.前言
安卓框架提供了一組2D繪圖的APIs,允許在畫布上渲染自定義圖形或者修改已存在的View來定義外觀和感覺。繪制2D圖形通常有兩種途徑:
a.給布局中的View對象繪制圖形和動畫。由系統(tǒng)的視圖層次結(jié)構(gòu)處理繪制過程,你只需定義視圖內(nèi)的圖形。
b.直接將圖形繪制到畫布上。你自己調(diào)用適當類的 onDraw() 方法(傳入畫布)或者Canvas對象的 draw...() 方法(就像 drawPicture() 方法),這樣也可以控制任何動畫。
選項“a”,當繪制的是不需要動態(tài)變化的簡單圖形和非性能密集型游戲的一部分時,在視圖上繪制是最好的選擇。例如,顯示靜態(tài)圖形或預(yù)定義的動畫。詳細信息看圖形這一章節(jié)。
選項“b”,當應(yīng)用需要經(jīng)常重繪時,在畫布上繪制是較好的選擇。例如,電子游戲等。有兩種方式去實現(xiàn):
- 與UI Activity同一線程時,給布局創(chuàng)建自定義視圖組件,需要調(diào)用 invalidate() 方法和處理 onDraw() 方法的回調(diào)。
- 一個單獨的線程時,管理一個SurfaceView,在畫布上以線程支持的最快速度繪制(不需要請求 invalidate())。
2.使用畫布繪制
寫程序時,若希望執(zhí)行專門的繪圖和/或控制圖形的動畫,應(yīng)該在畫布上繪制。畫布只是表面的封裝,負責所有 draw...() 方法的調(diào)用,圖形傳遞給實際的Surface上,擺放在窗口中的底層的位圖。
如果在 onDraw() 回調(diào)方法內(nèi)繪制,調(diào)用提供的Canvas的 draw...() 方法即可。處理SurfaceView對象時,通過 SurfaceHolder.lockCanvas() 方法獲取Canvas對象。(這兩種情況在下面的章節(jié)中都有討論)如果需要創(chuàng)建新的Canvas對象,必須定義實際執(zhí)行繪制的Bitmap對象。畫布總是需要位圖配合,創(chuàng)建新畫布如下:
Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
現(xiàn)在畫布將在給定的位圖上繪制,繪制完后,通過 drawBitmap(Bitmap,...) 方法可將位圖帶給另外畫布。建議在 View.onDraw() 或 SurfaceHolder.lockCanvas() 方法提供的畫布上繪制最終圖形(詳見下面的章節(jié))。
2.1.繪制在View上
如果你的應(yīng)用不需要大量的處理或好高的幀速率(也許是象棋游戲,貪吃蛇游戲或者緩慢的動畫應(yīng)用),應(yīng)該創(chuàng)建自定義視圖組件和在 View.onDraw() 方法內(nèi)用畫布繪圖。這樣做方便的是,安卓系統(tǒng)將提供預(yù)定義的Canvas對象來調(diào)用 draw...() 方法。
首先,繼承View類(或其子類)和定義 onDraw() 回調(diào)方法。當視圖繪制自己時,安卓系統(tǒng)將調(diào)用這個方法,通過傳入的Canvas對象執(zhí)行所有的繪制操作。
安卓系統(tǒng)只會在必要時調(diào)用 onDraw() 方法。每當應(yīng)用準備好繪制時,調(diào)用 invalidate() 方法廢除當前的視圖,同時告知安卓系統(tǒng)調(diào)用 onDraw() 方法(不能保證回調(diào)是瞬時的)。
在視圖組件的 onDraw() 方法內(nèi),使用提供的Canvas對象的各種 draw...() 方法或其它類的 draw() 方法(以提供的Canvas對象為參數(shù))完成所有繪圖。一旦 onDraw() 方法完成,安卓系統(tǒng)將使用畫布繪制位圖。
注意:為了從非主線程刷新界面,必須調(diào)用 postInvalidate() 方法。
關(guān)于擴展View類的更多信息,閱讀 read Building Custom Components。
2.2.繪制在SurfaceView上
SurfaceView是View的一個專注于繪圖的子類,目的是在應(yīng)用的子線程中提供繪圖功能,那樣應(yīng)用不需要等待系統(tǒng)視圖結(jié)構(gòu)的繪制完成。反而,與SurfaceView相關(guān)聯(lián)的子線程可以按照自己的頻率在畫布上繪制。
首先,需要創(chuàng)建一個類繼承SurfaceView,同時實現(xiàn)SurfaceHolder.Callback接口。它可以提供Surface的底層信息,例如,什么時候被創(chuàng)建、被改變或者被銷毀。這些信息很重要,可以知道何時開始繪圖,是否需要根據(jù)新的Surface屬性進行調(diào)整,何時停止繪圖,以及殺死某些任務(wù)。在SurfaceView類內(nèi)部定義子線程,以便在你的畫布上執(zhí)行所有的繪圖過程。
對于Surface的操作應(yīng)該通過SurfaceHolder而不是直接處理。當SurfaceView被初始化后,調(diào)用 getHolder() 方法獲取SurfaceHolder。通過 addCallback() 方法(參數(shù)為this)可以將回調(diào)對象傳入SurfaceHolder以獲取通知,而被回調(diào)的方法需在SurfaceView類中重寫。
為了在子線程中通過畫布繪制,需要將SurfaceHolder對象傳進線程,并調(diào)用它的 lockCanvas() 方法獲取畫布,有了畫布就可以進行必要的繪制操作了。畫完后,調(diào)用 unlockCanvasAndPost() 方法解鎖和傳遞畫布對象,這樣,內(nèi)容才會顯示到畫布上。每當要重繪時,先鎖定畫布再解鎖畫布,代碼看這篇文章。
注意:每次從SurfaceHolder獲取畫布,畫布之前的狀態(tài)將會被保留。為了正確展示動畫,你必須重繪整個Surface。比如,調(diào)用 drawColor() 方法填充一種顏色或者調(diào)用 drawBitmap() 方法設(shè)置一個背景圖像來清除畫布之前的狀態(tài)。否則,會在畫布上看到之前繪制過的痕跡。
3.圖形
安卓提供了一個自定義2D圖形庫,用于繪制形狀和圖像。這些在兩個維度上繪制的公共類放在 android.graphics.drawable 包中。
本文討論使用Drawable對象繪制圖形的基本知識和如何使用Drawable子類。關(guān)于使用圖形完成幀動畫,詳見 Drawable Animation。
圖形通常指可以繪制的東西,它的子類定義了各種具體的可繪圖形,包括BitmapDrawable,ShapeDrawable,PictureDrawable,LayerDrawable等。當然,也可以繼承這些類,來實現(xiàn)自己想要的、具有獨特行為的Drawable對象。
有三種方式定義和實例化Drawable對象:使用保存在項目資源中的圖像;使用XML文件定義Drawable屬性;使用正常的類構(gòu)造函數(shù)。下面,將分別討論前兩種技術(shù)(第三種就是代碼的調(diào)用,功能最全也不難)。
3.1.使用圖像資源創(chuàng)建
向應(yīng)用程序添加圖形的簡單方法是引用項目資源中的圖像文件,支持的文件類型有:PNG(首選),JPG(可接受),GIF(不建議)。這種技術(shù)顯然更適合應(yīng)用程序圖標、徽標或其它情況(如在游戲中使用)。
使用圖像資源,僅僅需要將文件添加到你項目的 res/drawable/ 目錄下,再從代碼和XML布局中引用。無論哪種方式,都涉及到使用資源ID,一種沒有文件類型擴展名的文件名(例如,my_image.png 用my_image引用)。
注意:放在 res/drawable/ 目錄下的圖像資源可能會被aapt工具在編譯過程中使用圖像無損壓縮進行自動優(yōu)化,比如,一個真的不需要超過256種顏色的PNG可能會被顏色調(diào)色板轉(zhuǎn)換成8位的PNG。在圖像質(zhì)量不變的情況下,可以使用較少的內(nèi)存。因此,需要認識到,這個目錄下的圖像二進制文件在編譯的過程中會改變。如果計劃用位流讀取圖像轉(zhuǎn)換成位圖,最好將圖像放到 res/raw/ 目錄下,避免被優(yōu)化。
示例代碼(使用圖形資源創(chuàng)建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對象來處理圖像資源。那么,通過資源創(chuàng)建Drawable對象如下:
Resources res = mContext.getResources();
Drawable myImage = res.getDrawable(R.drawable.my_image);
項目中每個獨特的資源,不管實例化多少個不同的對象,只能維持一種狀態(tài)。例如,對同一個圖像資源實例化兩個Drawable對象,改變其中一個對象的一個屬性(拿透明度來說),將會影響另一個。所以,處理一個對象的多個資源時,使用補間動畫來改變圖形比直接操作要好。
示例XML(在XML布局中給ImageView添加圖形資源)
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="#55ff0000"
android:src="@drawable/my_image"/>
關(guān)于使用項目資源的詳細信息,閱讀 Resources and Assets。
3.2.使用XML資源創(chuàng)建
若對安卓開發(fā)用戶界面的原則熟悉,應(yīng)該清楚在XML中定義對象所固有的能力和靈活性,這種理念從View貫穿到Drawable。如果想創(chuàng)建的Drawable對象不依賴于應(yīng)用程序代碼或者用戶交互,那么在XML中定義是個好的選擇。即使期望圖形隨著用戶的使用而改變屬性,你也應(yīng)該認識到在XML中定義對象可以隨時修改初始化時的屬性。
一旦在XML中定義Drawable對象,文件需保存到項目的 res/drawable/ 目錄下。然后調(diào)用 Resources.getDrawable() 方法,根據(jù)XML文件的資源ID獲取和初始化對象。
任何支持 inflate() 方法的Drawable子類能夠被XML定義和被應(yīng)用初始化,它們特有的XML屬性能夠找到對應(yīng)的對象屬性(詳見類參考)。
示例
// 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并設(shè)置為ImageView的內(nèi)容
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.形狀圖片
當想要動態(tài)地繪制兩個維度的圖形時,ShapeDrawable對象將滿足需求,可以通過編程的方式繪制原始形狀和定義任何樣式。
ShapeDrawable繼承自Drawable類,可以被當作Drawable對象使用,比如,調(diào)用 setBackgroundDrawable() 方法給視圖設(shè)置背景。當然也可以給自定義視圖繪制自己的形狀,但記得添加到自己的布局中。因為ShapeDrawable對象有自己的 draw() 方法,所以在View子類的 View.onDraw() 方法中調(diào)用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);
}
}
在構(gòu)造方法中,ShapeDrawable對象被定義為橢圓形,然后給了顏色和大小。如果不設(shè)置形狀,將不會繪制;如果不設(shè)置顏色,將默認黑色。
在自定義視圖中可以繪制任何想要的樣子。我們可以將上面示例的形狀用代碼繪制到Activity中:
CustomDrawableView mCustomDrawableView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mCustomDrawableView = new CustomDrawableView(this);
setContentView(mCustomDrawableView);
}
若要通過XML布局自定義圖形而不是在Activity中設(shè)置,CustomDrawableView類必須重寫 View(Context, AttributeSet) 構(gòu)造函數(shù),因為從XML實例化View對象時將會調(diào)用。接著將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章節(jié)。
5.可伸縮圖片
NinePatchDrawable是一個可伸縮的位圖圖像,在標準PNG圖像的基礎(chǔ)上加了1像素寬的邊框。當它作為背景時,安卓會自動調(diào)整大小以適應(yīng)視圖中的內(nèi)容。它必須以.9.png為擴展名保存在項目的 res/drawable/ 目錄下。
NinePatch圖分為兩個部分,左、上為定義拉伸區(qū),右、下為定義內(nèi)邊距(里面是內(nèi)容區(qū)),如下圖:

將它作為Button的背景圖時,實際效果如下:

6.矢量圖片
VectorDrawable對象由定義在XML文件中的一系列點、線、曲線和相關(guān)的顏色信息組成。安卓5.0(API 21)開始,VectorDrawable和AnimatedVectorDrawable這兩個類支持矢量圖形作為圖形資源。之前若想使用,支持庫23.2或更高也提供矢量圖形和動態(tài)矢量圖形的支持。
關(guān)于使用矢量圖形系統(tǒng)APIs或矢量圖形支持庫的更多信息,前往 Vector Drawable。