讀古今文學網 > Android程序設計:第2版 > 自己動手開發部件 >

自己動手開發部件

如前所述,部件是android.view.View的常用簡稱,它通常是視圖樹的葉子節點。視圖樹的內部節點,雖然可能包含複雜的代碼,但通常這些節點在用戶交互方面更簡單。部件這個術語雖然不正式,但是對於討論包含用戶關心的信息和行為的用戶界面的工作部件是有用的。

你無需創建一個新的部件就可以實現很多功能。在本書中,我們已經構建的應用,使用的部件都是已有的,或者是已有部件的簡單子類。這些應用只是構建視圖樹,通過代碼或XML文件的佈局資源來展開。

第9章將探討MicroJobs應用,它包含一個視圖,該視圖的內容是在地圖上標出一個名稱列表。當在地圖中添加其他位置時,新的顯示名稱的部件會被動態添加到列表中。這種動態變化的佈局使用的也是已存在的部件,沒有創建新的部件。從圖形上看,MicroJobs應用的功能是向樹結構中添加盒子,或從樹結構中刪除盒子,如第6章的圖6-3所示。

本章將介紹如何自己動手創建部件,這需要探究視圖(View)的結構。TextView、Button和DatePicker都是Android UI工具箱提供的部件。可以把自己的部件實現成這些部件的子類,或者直接創建一個View的子類。

更複雜的部件,如可以嵌套其他部件,其本身需要繼承View。一個非常複雜的部件,可能會作為接口工具在多處實現(甚至在多個應用中使用),可能是整個類包,但只有一個類是View類的子類。

本章要介紹的是圖形,因此內容是關於模型-視圖-控制器(MVC)模式中視圖這一部分的。部件也包含控制器代碼,這也是一種良好的設計模式,因為它把行為及在屏幕上和展現相關的所有代碼集中起來了。本章只介紹View的實現。控制器的實現在第6章已經探討過了。

關於圖形方面,可以分成兩個基礎部分:在屏幕上尋找空間,以及在該空間上繪圖。第一個任務是佈局(layout)。葉子部件使用onMeasure方法聲明其空間需求,Android UI框架會在正確的時間調用該方法。第二個任務是真正渲染部件,通過部件的onDraw方法實現。

佈局

Android框架的佈局(layout)機制中的大多數繁重的任務是通過容器視圖(container view)實現的。容器視圖也是一個視圖,其特別之處在於它包含其他視圖,它在視圖樹中是內部節點,屬於ViewGroup的子類。Android框架工具箱提供了各種各樣的複雜容器視圖,為屏幕佈局提供了強大的自適應策略。簡單舉幾個例子,如LinearLayout和RelativeLayout,都是相對易於使用並且很難重新正確實現的容器視圖。既然這些便捷、強大的容器視圖已經存在了,你可能不需要實現在這裡所探討的容器視圖或佈局算法。然而,瞭解它們是如何工作的(即Android UI框架管理佈局的過程)有助於構建正確的、健壯的部件。

例8-1顯示了一個非常簡單的部件。如果把該部件添加到一些Activity的視圖樹中,則該部件會用青色填充分配給它的空間。在我們探討創建更複雜的部件之前,先來仔細看看該實例如何完成繪圖中的兩個基本任務:佈局和描繪。先來分析佈局過程。在P219「Canvas繪畫」一節中將描述繪圖過程。

例8-1:一個簡單的部件


public class TrivialWidget extends View {
    public TrivialWidget(Context context) {
        super(context);
        setMinimumWidth(100);
        setMinimumHeight(20);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(
            getSuggestedMinimumWidth,
            getSuggestedMinimumHeight);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawColor(Color.CYAN);
    }
}
  

因為部件的空間需求會動態變化,所以動態佈局是必要的。舉個例子,在支持GPS的應用中,有個部件的功能是顯示你所在城市的名稱。當你從Ely到Post Mills時,部件會接收到位置變化的通知。但是,當它準備重新描繪城市名稱時,它可能注意到沒有足夠的空間來顯示新城鎮的全名。它會請求獲得更多的空間對屏幕顯示進行重新描繪,如果屏幕上有足夠空間的話。

佈局事實上是非常複雜的,要做到正確很困難。使某個葉子部件在某台設備上工作正常可能不是很困難。但是,要使部件做到使其子節點在不同的設備上的顯示都正常,甚至能夠自適應屏幕尺寸的變化,那是非常困難的。

當在視圖樹的某些視圖上調用requestLayout方法時,會對佈局過程執行初始化。通常情況下,當部件需要更多的空間時,它本身會調用requestLayout。然而,在應用的任何地方都可以調用該方法,表示當前屏幕的某些視圖不再有足夠的空間。

requestLayout方法引起Android UI框架向UI事件隊列中插入一個事件。當執行到該事件時,框架允許每個容器視圖詢問其孩子部件需要多少空間。這個過程可以分解成兩個階段:測量孩子視圖需要的空間;把孩子視圖調整到新的空間。所有視圖都必須實現第一個階段,但是第二個階段只有那些需要管理孩子視圖佈局的容器視圖才需要實現。

測量

測量階段的目標是使每個視圖能夠動態請求理想情況下描繪所需要的空間。UI框架從調用視圖樹的根節點視圖的measure方法開始執行這個過程。每個容器視圖詢問其孩子視圖需要多少空間。該調用會以深度優先的方式遞歸到所有的後代,因此每個孩子視圖在父親節點之前先計算其需要的空間。父親節點的大小基於孩子節點的大小來計算,然後向上匯報給其父親節點,直到樹的根節點。

例如,在P167「組裝圖形界面」一節中,最上方的LinearLayout詢問每個嵌套的LinearLayout部件需要多少空間。這些部件又詢問嵌套在它內部的Button或EditText需要多少空間。每個孩子節點告訴其父親節點它們所需的空間大小。父親節點把孩子節點需要的空間加起來,再加上它們自己填充的大小,然後把總和報告給最上層的LinearLayout。

因為框架必須保證這個過程中所有視圖的某些行為,所以measure方法是final類型,不能夠被覆蓋,而measure方法會調用onMeasure方法,部件可以通過覆蓋onMeasure方法來聲明其所需要的空間。

onMeasure方法的參數是父親節點能夠提供的空間大小:寬度和高度,以像素為單位。Android框架假定所有的視圖大小都在[0,2 30]像素區間內,因此,整型參數的高字節位被用來存儲傳遞測量規格模式(measurement specification mode)。onMeasure方法雖然只有2個參數,但從邏輯上看實際上是4個參數:寬度規格模式、寬度、高度規格模式和高度。不要嘗試自己通過移位操作來獲取參數!相反,應該使用靜態方法MeasureSpec.getMode和MeasureSpec.getSize來獲取。

規格模式描述了容器視圖希望孩子節點如何解釋其關聯的大小,它包含3個不同的值:

MeasureSpec.EXACTLY

容器視圖調用方已經指定了孩子視圖的精確大小。

MeasureSpec.AT_MOST

容器視圖調用方設置了最大值,孩子視圖可以請求更少的空間。

MeasureSpec.UNSPECIFIED

容器視圖對孩子視圖沒有限制,孩子視圖可以隨意請求大小。

部件需要將它所需要的空間告訴其視圖樹中的父親節點。首先,部件調用setMeasuredDimensions設置其高度和寬度屬性。然後,其父親節點可以調用方法getMeasuredHeight和getMeasuredWidth來獲取這些屬性。如果你的實現覆蓋了onMeasure方法,但是沒有調用setMeasuredDimensions方法,則measure方法會拋出IllegalStateException異常,而不會正常執行。

onMeasure方法繼承自View,其實現中必須調用setMeasuredDimensions,在每個方向設置一個值。如果父親視圖指定模式為MeasureSpec.UNSPECIFIED,則孩子視圖的setMeasuredDimensions方法使用視圖的默認大小,其值由getSuggestedMinimumWidth或getSuggestedMinimumHeight提供。如果父親視圖指定的是MeasureSpec.EXACTLY或MeasureSpec.AT_MOST,則孩子視圖的默認大小使用父親視圖給出的大小。這種策略非常合理,它使得部件在測量階段的處理只需要簡單地設置成getSuggestedMinimumWidth和getSuggestedMinimumHeight所返回的值。

你要實現的部件可能無法獲取其請求大小的空間。假設有個寬度為100像素的視圖,它包含3個孩子視圖。如果其孩子視圖請求的寬度總和小於等於100像素,那可能很容易調整;但是,如果每個孩子視圖請求50像素,那麼父親視圖就無法滿足全部需求。

容器視圖可以完全控制如何給孩子視圖分配大小。在前面給出的例子中,它可以採取「平均」的方式,給每個孩子分配33像素;也可以給最左邊的孩子視圖分配50像素,剩下兩個視圖都分配25像素。實際上,它還可以把100像素全部給某個孩子視圖,其他兩個視圖一個像素都沒有。無論是哪種方式,父親視圖最終都需要確定每個孩子的邊界矩形框的大小和位置。

容器視圖控制分配給部件的空間大小的另一個例子是如例8-1所示的部件示例。該部件總是請求其想要的空間大小,不管分配給它的空間是多少(和默認實現不同)。該策略對於記住要加到工具箱容器的部件很方便,特別是實現了gravity的LinearLayout。gravity是一些視圖用來指定其子元素的排列方式的一個屬性。當你第一次使用其中某個容器時,當發現默認情況下定制的部件中只描繪了第一個部件時,你可能會感到很驚訝。可以使用setGravity方法把屬性修改成Gravity.FILL,或讓你的部件指定請求的空間量來解決這個問題。

還應該注意的是,容器視圖在單個測量階段,可能會多次調用孩子視圖的measure方法。作為onMeasure方法實現的一部分,一個巧妙的容器視圖,想要把部件水平排列,可能會調用每個孩子部件的measure方法,其模式為MEASURE_SPEC.UNSPECIFIED,寬度為0,從而找到該部件想要的大小。一旦它收集到每個孩子視圖期望的寬度,它會對這些寬度求和,比較求和值和實際可用的寬度(在父親視圖調用其measure方法時會指定)。現在,它可能調用每個孩子部件的measure方法,把模式設置成MeasureSpec.AT_MOST,寬度設置成實際可用的空間。因為可以多次調用measure方法,所以onMeasure方法的實現必須是冪等(idempotent)性的,而且不能夠改變應用的狀態。

注意:如果多次執行某個動作的效果和一次執行的效果相同,我們就說這個動作是「冪等性」的。例如,x=3這個語句就是冪等性的,因為不管你執行多少次,x的結果都是3。但是,x=x+1不是冪等性的,因為x的值取決於該語句所執行的次數。

容器視圖的onMeasure方法的實現很可能是相當複雜的。所有容器視圖的超類ViewGroup,並沒有提供默認的實現。每個Android UI框架容器視圖包含自己的實現。如果你考慮實現一個容器視圖,那麼可能會考慮繼承某個框架容器視圖。相反,如果你從頭開始實現,則可能還是需要為每個孩子視圖調用measure方法,並考慮使用ViewGroup提供的輔助方法:measureChild、measureChildren和measureChildWithMargins。

在測量階段的最後,容器視圖與其他部件一樣,必須調用setMeasuredDimensions方法報告其需要的空間大小。

佈置

一旦視圖樹中的所有容器視圖有機會聲明其每個孩子視圖的大小,框架就啟動第二次佈局,即佈置調整其孩子的空間。同樣,除非你自己實現了容器視圖,否則你很可能永遠都沒有機會實現自己的調整代碼。這一節介紹的是其底層過程,瞭解其內部機制有助於更好地瞭解它會如何影響你的部件。View中實現的默認方法適用於傳統的葉子部件,如例9-1所示。

因為視圖的onMeasure方法可能會被調用多次,所以框架必須使用另一種方法來指明測量階段是否結束,容器視圖必須確定其孩子視圖的最終位置。和測量階段類似,佈置階段是通過兩種方法實現的。框架調用視圖樹根節點的final方法layout。layout方法執行所有視圖都有的處理,然後調用onLayout,它定制部件覆蓋實現自己的行為。定制的onLayout的實現至少必須計算它在描繪時會提供給每個孩子節點的邊界矩形框,並順序調用每個孩子視圖的layout方法(因為孩子視圖可能是其他部件的父親視圖)。這個過程可能是非常複雜的。如果你的部件需要調整孩子視圖,你可能會考慮讓它基於已有的容器,比如LinearLayout或RelativeLayout。

值得再次強調的是,部件請求的空間大小是不能確保的。它必須準備好以實際分配給它的空間大小進行描繪。如果它嘗試在父親節點所分配的空間之外描繪,則這些超出部分會被剪輯矩形框裁掉(在本章後面會探討它)。要很好地控制描繪空間,比如精確填充分配給它的空間,部件必須實現onLayout並記錄分配的空間的維度,或者查看Canvas的剪輯矩形框,Canvas是onDraw方法的參數。

Canvas繪畫

我們已經探討了部件是如何在屏幕上分配空間並繪製的。下面一起來實現幾個部件,看它們是如何工作的。

既然你已經瞭解了視圖的測量和佈置(arrangement),Android UI框架處理繪製的方式應該很熟悉了。當應用的某些部分認為當前的屏幕繪製由於狀態變化過時了,那麼它就會調用View的invalidate方法。該調用會在事件隊列中插入一個重新繪製事件。

當重新繪製事件被處理時,框架會調用視圖樹根節點的draw方法。這個調用會按照前序遍歷的方式迭代執行,每個視圖先繪製自己,然後再調用孩子視圖的draw方法。這意味著葉子視圖是在其父親視圖之後繪製的,依此類推。在樹的較下方的View是在那些靠近樹的根節點的視圖上繪製的。

View.draw方法調用onDraw,每個子類可以通過覆蓋onDraw方法來實現自己定制的渲染。當調用你的widget的onDraw方法時,它必須根據應用的當前狀態進行渲染並返回。雖然View.draw和ViewGroup.dispatchDraw(負責視圖樹的遍歷)都不是final類型,但是如果覆蓋它們,將會給自己帶來麻煩!

為了防止繪製超出範圍,Android UI框架維護了視圖的一些狀態信息,稱為剪輯矩形框(clip rectangle)。剪輯矩形框是Android UI框架的一個關鍵概念,它是調用組件的圖形渲染方法所傳遞的狀態參數的一部分。它包含位置和大小,可以通過畫布方法獲取並調整。它就像一個模具,組件通過它執行所有的繪製:組件只能在剪輯矩形框可見的畫布部分進行繪製。只要正確地設置了剪輯矩形框的大小、形狀和位置,Android UI框架就可以防止組件的繪製超出其邊界,也可以防止組件對已經正確繪製的區域進行重新繪製。

在Android API 7中提供了另一個優化工具:Eclair。如果一個視圖是非透明的(其矩形框填充都是非透明的對象)應該重載視圖方法isOpaque,返回布爾值true。這樣,widget就會告訴繪製算法不需要對其他視圖進行渲染。即使只是在一個不是很複雜的視圖樹中,這也會減少一個像素被繪製次數的80%或75%(即原來需要繪製四次或五次,現在只需要繪製一次)。這個優化帶來的明顯的效果是滾動條拖曳變得很平滑,不再遲鈍了!

在探討繪製的細節之前,我們再次說明一下Android的單線程MVC設計模式。有兩個基本規則:

·繪製代碼應該在onDraw方法之內。當調用onDraw方法時,部件應該完全繪製自己,顯示程序狀態。

·當onDraw方法被調用時,部件應該盡快繪製。onDraw調用中間不適合運行複雜的數據庫查詢或與某些遠程網絡服務交互。需要繪製的所有狀態都應該緩存,以便繪製。長時間運行的任務應該在不同的線程中執行,以及使用一種在P186「高級連接:聚集和線程化」一節中所描述的機制。視圖中緩存的模形狀態信息有時稱為視圖模型(view model)。

Android UI框架在繪圖時使用4個主要的類。如果要實現定制的部件,並執行自己的繪製,需要對這4個類非常熟悉:

Canvas(類android.graphics.Canvas的子類)

Canvas在現實生活中沒有明確的類比。可以把它想成一個複雜的畫架,可以旋轉方向、彎曲甚至可以通過有趣的方式弄皺你繪製的圖紙。它包含繪製需要的模具——剪輯矩形框。它還可以對繪製的圖形進行擴展,類似相片放大器。它還可以執行其他的轉換操作,這些操作更難通過類比來說明:對顏色進行映射及沿路徑繪製文本。

Paint(類android.graphics.Paint的子類)

在繪製時需要Paint這個工具。它控制顏色、透明度和畫筆的大小。它還可以控制繪製的文本的字體、大小和格式。

Bitmap(類android.graphics.Bitmap的子類)

在Bitmap上進行繪製。它顯示繪製時的實際像素。

Drawable(可能是android.graphics.drawable.Drawable的子類)

Drawable是要繪製的事物:矩形框或圖像。雖然你所繪製的不全是Drawable(比如文本不是),但是很多屬於Drawable,尤其是那些複雜的圖形。

例8-1是只使用Canvas作為onDraw方法的參數所繪製的繪製。為了繪製一些更有意義的圖形,我們至少需要Paint。Paint提供了對所繪製圖形的顏色和透明度(alpha)的控制。它還控制繪製這些圖形所使用的畫筆的大小。當和文本繪製方法一起使用時,Paint控制文本的字體、大小和格式。Paint還包含很多其他功能,有些將在P235「Bling」一節中給出。儘管如此,例8-2還是可以作為瞭解Paint的入門示例。該例子設置了Paint控制的兩個參數(顏色和畫筆寬度),先描繪粗的垂直線,然後是一系列水平線。每條綠線的alpha值(和RGB web顏色的第4個值功能相同)逐漸減少,使得看起來更透明。要瞭解更多有用的屬性,請參看Paint類文檔。

例8-2:使用Paint


@Override
protected void onDraw(Canvas canvas) {
    canvas.drawColor(Color.WHITE);
    Paint paint = new Paint;
    canvas.drawLine(33, 0, 33, 100, paint);
    paint.setColor(Color.RED);
    paint.setStrokeWidth(10);
    canvas.drawLine(56, 0, 56, 100, paint);
    paint.setColor(Color.GREEN);
    paint.setStrokeWidth(5);
    for (int y = 30, alpha = 255; alpha > 2; alpha >>= 1, y += 10) {
        paint.setAlpha(alpha);
        canvas.drawLine(0, y, 100, y, paint);
    }
}
  

圖8-1顯示了示例代碼所創建的圖形。

圖8-1:Paint輸出

除了Paint,繪製出一個實用的部件,還有另外幾個必要的工具。例如,例8-3中的代碼繪製出的就是例6-7中的部件。雖然不是很複雜,但是它包含了功能完備的部件所有要素。它處理佈局,使用高亮(不管視圖是否包含用戶焦點),顯示其關聯的模型的狀態。部件描繪了一系列的點,其信息保存在一個私有數組中。每個點指定其本身的x和y坐標,以及直徑和顏色。OnDraw函數重新設置Paint的顏色,使用其他參數指定由畫布的drawCircle方法繪製的圓。

例8-3:點部件


package com.oreilly.android.intro.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.view.View;
import com.oreilly.android.intro.model.Dot;
import com.oreilly.android.intro.model.Dots;
public class DotView extends View {
    private final Dots dots;
    /**
     * @param context the rest of the application
     * @param dots the dots we draw
     */
    public DotView(Context context, Dots dots) {
        super(context);
        this.dots = dots;
        setMinimumWidth(180);
        setMinimumHeight(200);
        setFocusable(true);
    }
    /** @see android.view.View#onMeasure(int, int) */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(
            getSuggestedMinimumWidth,
            getSuggestedMinimumHeight);
    }
    /** @see android.view.View#onDraw(android.graphics.Canvas) */
    @Override protected void onDraw(Canvas canvas) {
        canvas.drawColor(Color.WHITE);
        Paint paint = new Paint;
        paint.setStyle(Style.STROKE);
        paint.setColor(hasFocus ? Color.BLUE : Color.GRAY);
        canvas.drawRect(0, 0, getWidth - 1, getHeight - 1, paint);
        paint.setStyle(Style.FILL);
        for (Dot dot : dots.getDots) {
            paint.setColor(dot.getColor);
            canvas.drawCircle(
                dot.getX,
                dot.getY,
                dot.getDiameter,
                paint);
        }
    }
}
  

通過Paint,我們有足夠的空間來探索Canvas方法。但是,有兩組功能需要特別注意。

繪製文本

在Canvas方法中,最重要的當屬那些繪製文本的方法。Canvas的有些功能在其他類中也有,但文本渲染功能是它特有的。要在部件中放置文本,必須使用Canvas類(或者繼承了該類的其他部件)。

Canvas提供了一些渲染文本的功能,借助這些功能可以很方便地對文本中每個字符的位置進行佈置。其方法是成對出現的:一個以String為參數,另一個以char數組為參數。在某些情況下,還有幾種其他方式。例如,繪製文本的最簡單的方式是傳遞文本開始的x坐標和y坐標,以及指定其字體、顏色和其他屬性的Paint類(見圖8-4)。

例8-4:一組文本繪製方法


public void drawText(String text, float x, float y, Paint paint)
public void drawText(char text, int index, int count, float x,
                     float y, Paint paint)
  

第一種方法只需要一個String參數來傳遞文本,第二種方法使用了3個參數:char數組、表示要繪製的數組的第一個字符的偏移及要繪製的文本的字符數。

如果你想要一些比簡單的水平文本更豐富的功能,可以沿著幾何線繪製它甚至把字符放到想要的任何位置。例8-5包含onDraw方法,它說明了3個文本渲染方法的使用。其輸出如圖8-2所示。

例8-5:3種繪製文本的方式


@Override
protected void onDraw(Canvas canvas) {
    canvas.drawColor(Color.WHITE);
    Paint paint = new Paint;
    paint.setColor(Color.RED);
    canvas.drawText("Android", 25, 30, paint);
    Path path = new Path;
    path.addArc(new RectF(10, 50, 90, 200), 240, 90);
    paint.setColor(Color.CYAN);
    canvas.drawTextOnPath("Android", path, 0, 0, paint);
    float pos = new float {
        20, 80,
        29, 83,
        36, 80,
        46, 83,
        52, 80,
        62, 83,
        68, 80
    };
    paint.setColor(Color.GREEN);
    canvas.drawPosText("Android", pos, paint);
}
  

圖8-2:繪製文本的3種輸出

你可能已經注意到了,最基礎的功能drawText只是在傳遞的坐標處開始繪製文本。但是,DrawTextOnPath方法可以沿著任何路徑繪製文本。示例路徑只是一個弧形。它也可以繪製直線或貝賽爾曲線。

對於DrawTextOnPath還無法滿足的功能,Canvas提供DrawPosText方法,它支持指定文本中每個字符的位置。注意,字符位置是通過數組元素指定的:x1,y1,x2,y2...

矩陣轉換

第二組有趣的Canvas方法是矩陣轉換及相關的方法:rotate、scale和skew。這些方法對所繪製的圖形使用那些其他環境下眾所周知的一些三維圖形轉換方式來處理它。該方法使得單一圖形的繪製方式看起來像是在和繪製的圖形一起運動。

例8-6中的小應用演示說明了Canvas的坐標轉換功能。

例8-6:在畫布中使用轉換功能


import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.View;
import android.widget.LinearLayout;
public class TranformationalActivity extends Activity {
    private interface Transformation {
        void transform(Canvas canvas);
        String describe;
    }
    private static class TransformedViewWidget extends View {
1
        private final Transformation transformation;
        public TransformedViewWidget(Context context, Transformation xform) {
            super(context);
            transformation = xform;
2
            setMinimumWidth(160);
            setMinimumHeight(105);
        }
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(
                getSuggestedMinimumWidth,
                getSuggestedMinimumHeight);
        }
        @Override
        protected void onDraw(Canvas canvas) {
3
            canvas.drawColor(Color.WHITE);
            Paint paint = new Paint;
            canvas.save;
4
            transformation.transform(canvas);
5
            paint.setTextSize(12);
            paint.setColor(Color.GREEN);
            canvas.drawText("Hello", 40, 55, paint);
            paint.setTextSize(16);
            paint.setColor(Color.RED);
            canvas.drawText("Android", 35, 65, paint);
            canvas.restore;
6
            paint.setColor(Color.BLACK);
            paint.setStyle(Paint.Style.STROKE);
            Rect r = canvas.getClipBounds;
            canvas.drawRect(r, paint);
            paint.setTextSize(10);
            paint.setColor(Color.BLUE);
            canvas.drawText(transformation.describe, 5, 100, paint);
        }
}
    @Override
    public void onCreate(Bundle savedInstanceState) {
7
        super.onCreate(savedInstanceState);
        setContentView(R.layout.transformed);
        LinearLayout v1 = (LinearLayout) findViewById(R.id.v_left);
8
        v1.addView(new TransformedViewWidget(
9
            this,
            new Transformation {
十
                @Override public String describe { return "identity"; }
                @Override public void transform(Canvas canvas) { }
            } ));
        v1.addView(new TransformedViewWidget(
9
            this,
            new Transformation {
十
                @Override public String describe { return "rotate(-30)"; }
                @Override public void transform(Canvas canvas) {
                    canvas.rotate(-30.0F);
                } }));
        v1.addView(new TransformedViewWidget(
9
            this,
            new Transformation {
十
                @Override public String describe { return "scale(.5,.8)"; }
                @Override public void transform(Canvas canvas) {
                    canvas.scale(0.5F, .8F);
                } }));
        v1.addView(new TransformedViewWidget(
9
            this,
            new Transformation {
十
                @Override public String describe { return "skew(.1,.3)"; }
                @Override public void transform(Canvas canvas) {
                    canvas.skew(0.1F, 0.3F);
                } }));
        LinearLayout v2 = (LinearLayout) findViewById(R.id.v_right);
⑪
        v2.addView(new TransformedViewWidget(
⑫
            this,
            new Transformation {
十
                @Override public String describe { return "translate(30,10)"; }
                @Override public void transform(Canvas canvas) {
                    canvas.translate(30.0F, 10.0F);
                } }));
        v2.addView(new TransformedViewWidget(
⑫
            this,
            new Transformation 
十
                @Override public String describe {
                    return "translate(110,-20),rotate(85)";
                }
                @Override public void transform(Canvas canvas) {
                    canvas.translate(110.0F, -20.0F);
                    canvas.rotate(85.0F);
                } }));
        v2.addView(new TransformedViewWidget(
⑫
            this,
            new Transformation {
十
                @Override public String describe {
                    return "translate(-50,-20),scale(2,1.2)";
                }
                @Override public void transform(Canvas canvas) {
                    canvas.translate(-50.0F, -20.0F);
                    canvas.scale(2F, 1.2F);
                } }));
        v2.addView(new TransformedViewWidget(
⑫
            this,
            new Transformation {
十
                @Override public String describe { return "complex"; }
                @Override public void transform(Canvas canvas) {
                    canvas.translate(-100.0F, -100.0F);
                    canvas.scale(2.5F, 2F);
                    canvas.skew(0.1F, 0.3F);
                } }));
    }
}
  

上面的示例代碼的運行結果如圖8-3所示。

以下是代碼的關鍵點解釋:

1 定義新的widget的TransformedViewWidget。

2 根據構造函數的第二個參數執行實際轉換。

3 TransformedViewWidget的onDraw方法。

4 在執行任何轉換之前,都應先用save函數把當前的繪製狀態保存到棧中。

5 執行構造函數第二個參數中指定的轉換操作。

6 恢復在第4項中保存的先前狀態,準備繪製矩形框和標籤。

7 Activity的onCreate方法。

8 為左側的部件創建容器視圖。

9 對TransformedViewWidget實例化,並將其添加到左側的列中。

十 創建轉換,作為TransformedViewWidget構造函數的參數。

 為右側的部件創建容器視圖。

 對TransformedViewWidget實例化,添加到右側列中。

圖8-3:轉換後的視圖

這個小的應用程序引入了一些新的思想。對於視圖和部件,應用定義了TransformedViewWidget,並創建了8個實例。對於佈局,應用創建了兩個視圖,名為v1和v2,從數據源獲取參數。然後給每個LinearLayout視圖添加4個實例。該例子說明了應用如何把基於源的視圖和動態視圖結合起來。注意,佈局視圖的創建和新widget的構造函數都是在Activity的onCreate方法中完成的。

該應用在部件和父親視圖之間做到了良好的分離,使得這個部件非常靈活。一些簡單的對象是直接在TransformedViewWidget的onDraw方法中定義的範圍內繪製的:

·白色背景

·單詞hello,字體大小為12號,綠色

·單詞Android,字體大小為16號,紅色

·黑色框

·藍色標籤

在這塊代碼的中間部分,onDraw方法執行了調用者所指定的轉換。應用定義其自己的接口,名稱為Transformation;以及以Transformation為參數的TransformedViewWidget構造函數。下面將很快說明調用者是如何真正執行轉換的。

首先,應該查看onDraw在轉換過程中是如何保存它自己的文本的。在這個例子中,需要最後繪製框架和標籤,這樣它們就在其他部件所繪製的圖形的上方繪製了,即使這些繪製可能存在重疊。我們不希望轉換影響到框架或標籤。

幸運的是,Canvas維護了一個內部棧,可以通過該棧保存或恢復轉換矩陣、剪輯矩形框及Canvas中所有其他可變的狀態元素。通過棧,onDraw方法調用Canvas.save方法保存其轉換之前的狀態,調用Canvas.restore方法恢復之前所保存的狀態。

應用的剩餘部分控制了應用於TransformedViewWidget的每個實例。部件的每個新實例都是通過自己的Transformation匿名實例創建的。處於標籤為identity的圖形區域中的對象不做任何轉換。其他7個區域打上轉換標籤。

Canvas轉換的基礎方法是setMatrix和concatMatrix。通過這兩個方法可以創建各種轉換。使用getMatrix方法,可以創建動態構建的矩陣用於後期使用。這個例子中所給出的方法——translate、rotate、scale和skew,是在當前Canvas狀態中添加個性化的、帶限定條件的矩陣時的方便方法。

雖然開始階段可能不是很明顯,但是這些轉換功能可能是非常有用的。它們可以讓你的應用根據三維對像轉變其可視化的點。例如,很顯然,查看標籤為scale(.5,.8)的方框和標籤為identity的方框的方法相同,但是其視覺差別很大。不難想像,在標籤為skew(.1,.3)的框內的圖形可能是沒有經過轉換的,但是它是從上方稍側邊上查看的。對任何對像縮放或轉換會使用戶感覺該對像動了。傾斜和旋轉會讓人感覺該對像被打開了。

當你考慮把這些轉換功能應用到畫布上的所有對象上時(線條、文字甚至圖形),其在應用中的重要性變得更加明顯。縮略圖的實現可以簡單地把所有事物的顯示縮放到原尺寸大小的10%,雖然這種方式可能不是最優的。顯示你在開車時所看到的左側事物的應用可以通過對幾個圖形進行縮放和傾斜來實現。

Drawable

Drawable是個可以在畫布上渲染自己的對象。因為Drawable在渲染時可以完全控制,即使是非常複雜的渲染過程也可以執行封裝,因此非常易於使用。

例8-7和例8-8顯示了使用Drawable實現圖8-3所示的例子所需要的變換。繪製紅色和綠色文本的代碼已經重構到HelloAndroidTextDrawable類中,通過widget的onDraw方法渲染。

例8-7:使用TextDrawable


private static class HelloAndroidTextDrawable extends Drawable {
    private ColorFilter filter;
    private int opacity;
    public HelloAndroidTextDrawable {}
    @Override
    public void draw(Canvas canvas) {
        Paint paint = new Paint;
        paint.setColorFilter(filter);
        paint.setAlpha(opacity);
        paint.setTextSize(12);
        paint.setColor(Color.GREEN);
        canvas.drawText("Hello", 40, 55, paint);
        paint.setTextSize(16);
        paint.setColor(Color.RED);
        canvas.drawText("Android", 35, 65, paint);
}
    @Override
    public int getOpacity { return PixelFormat.TRANSLUCENT; }
    @Override
    public void setAlpha(int alpha) { }
    @Override
    public void setColorFilter(ColorFilter cf) { }
}
  

使用新的Drawable實現只需要對例子中的onDraw方法做很少的變換。

例8-8:使用Drawable widget


package com.oreilly.android.intro.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.View;
/**A widget that renders a drawable with a transformation */
public class TransformedViewWidget extends View {
    /** A transformation */
    public interface Transformation {
        /** @param canvas */
        void transform(Canvas canvas);
        /** @return text description of the transform. */
        String describe;
    }
    private final Transformation transformation;
    private final Drawable drawable;
    /**
     * Render the passed drawable, transformed.
     *
     * @param context app context
     * @param draw the object to be drawn, in transform
     * @param xform the transformation
     */
    public TransformedViewWidget(
        Context context,
        Drawable draw,
        Transformation xform)
    {
        super(context);
        drawable = draw;
        transformation = xform;
        setMinimumWidth(160);
        setMinimumHeight(135);
    }
    /** @see android.view.View#onMeasure(int, int) */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(
            getSuggestedMinimumWidth,
            getSuggestedMinimumHeight);
    }
    /** @see android.view.View#onDraw(android.graphics.Canvas) */
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawColor(Color.WHITE);
        canvas.save;
        transformation.transform(canvas);
        drawable.draw(canvas);
        canvas.restore;
        Paint paint = new Paint;
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        Rect r = canvas.getClipBounds;
        canvas.drawRect(r, paint);
        paint.setTextSize(10);
        paint.setColor(Color.BLUE);
        canvas.drawText(
            transformation.describe,
            5,
            getMeasuredHeight - 5,
            paint);
    }
}
  

這段代碼顯示了Drawable的強大之處。TransformedViewWidget可以對任何Drawable對像進行轉換,不管繪製的是什麼。它不再和原始的硬編碼文本關聯。它可以用於對之前示例的文本及相片進行轉換,如圖8-4所示。它甚至還可以用於對Drawable動畫進行轉換。

圖8-4:對包含相片的視圖進行轉換

Drawable使得複雜的圖形技術(如動畫)變得可追蹤。此外,因為它們把渲染過程完全封裝起來了,所以利用Drawable可以把複雜的渲染分解成小的可重用的組件。

思考一下,擴展一下前面的例子,使得每個圖形在一分鐘內褪成白色。當然,修改例9-8中的代碼可以實現這個功能。還可以實現一個不同的且很有意思的新的Drawable。

我們將創建一個名為FaderDrawable的新Drawable,其構造函數的參數是Drawable的引用。此外,它必須包含一個時間概念,可以是一個整數,我們稱之為t,它的值隨著計時器變化。每當調用FaderDrawable的draw方法時,它首先調用其目標draw方法。但是,下一步它會在同一個區域內用白色繪製,根據t的取值來決定繪製的透明度(alpha值)(如例9-2所示)。隨著時間推移,t值變大,白色變得更不透明,目標Drawable就會逐漸褪成白色。

這個FaderDrawable示例說明了Drawable的一些重要功能。首先,FaderDrawable可以重複使用。它可以淡出任何Drawable對象。還要注意的是,由於FaderDrawable擴展了Drawable,任何Drawable可用的地方都可以使用FaderDrawable。在渲染過程中使用Drawable的任何代碼都可以使用FaderDrawable,不需要做任何修改。

當然,FaderDrawable本身也可以封裝。實際上,只需要構建一系列的Drawable封裝,就可以實現非常複雜的效果。Android工具箱提供了Drawable封裝來支持這個策略,包括ClipDrawable、RotateDrawable和ScaleDrawable。

現在,你可能會在思考通過Drawable重新設計整個UI。雖然Drawable很強大,但它們也不是萬能的。在考慮使用Drawable時,有幾點需要記住。

你可能已經注意到,Drawable類和View類有很多功能相同:位置、維度和可見性等。什麼時候View應該直接在Canvas上繪製,什麼時候應該委託給子視圖,以及什麼時候應該委託給一個或多個Drawable對象,這些不太容易把握。有一個DrawableContainer類,它支持在一個父親視圖內把幾個子Drawable結合起來。可以構建Drawable樹來取代目前所構建的View樹。在使用Android UI框架時,需要相信「條條道路通羅馬」,同一個功能有很多實現方式。

View和Drawable之間的區別之一在於Drawable沒有實現View的測量/佈局協議,正如前文所述,該協議支持容器視圖改變其組件的佈局及對視圖大小進行調整。當一個可渲染的對象需要添加、刪除或佈置內部組件的佈局時,很顯然,實現時應該採用View而不是Drawable。

要考慮的第二點是,由於Drawable對像把繪製過程完全封裝起來了,它們不會像String或Rect那樣繪製。比如,沒有Canvas方法在渲染時可以把Drawable對像放置在特定坐標處。你可能還會考慮為了渲染某個圖像兩次,View.onDraw方法應該使用兩個Drawable對像還是使用單個Drawable對像兩次卻只是重新設置其坐標。

但是,還有一個最重要的也可能是更常見的問題。Drawable可以工作的原因在於Drawable接口不包含Drawable內部實現的任何細節。當你的代碼傳遞一個Drawable對像時,無法知道它是要渲染某個圖形還是一系列複雜的效果:旋轉、閃爍、跳動等。當然,這是Drawable的一個很大的優點,但它也可能會帶來問題。

繪製過程大部分是有狀態的。設置Paint,然後繪製它;設置Canvas剪輯區和轉換,然後繪製它。當是Drawable鏈時,那麼必須慎重,因為要確保狀態變化之間不會有衝突。問題在於當構建Drawable鏈時,從對像類型的定義上(都是Drawable對像),無法預見衝突。一個看似微小的變化可能帶來預期之外的效果,而且很難調試。

為了說明這一點,我們假設有兩個Drawable封裝類,一個是要縮小顯示其內容,另一個是要旋轉90°。如果兩個類都是通過把轉換矩陣設置成某個具體值來實現,兩者結合之後的效果可能不理想。更糟的是,如果A封裝B可能工作良好,而如果B封裝A就遭了!有必要仔細查看Drawable文檔,瞭解它是如何實現的。

位圖

位圖(Bitmap)是繪製的4個基礎項的最後一項:要繪製的目標(String、Rect等)、工具Paint、畫布Canvas及存儲繪製結果的位圖。大多數情況下,不需要直接和Bitmap打交道,因為Canvas提供給onDraw方法的參數中已經隱含了一個Bitmap。但是,有些時候可能要直接使用Bitmap。

Bitmap的常見用途是緩存一個繪製很費時而且不會經常變換的繪圖。比如,假設一個繪製程序支持用戶繪製多個圖層。圖層在基本圖像上透明疊加,用戶可以隨便關閉和打開該圖層。如果每次調用onDraw方法都繪製每個圖層代價可能很高。相反,如果基於第一次顯示來渲染整個繪圖,包含所有可見的圖層,然後僅當用戶做出某個圖層變化時,再重新繪製對應的單個圖層。

例8-9顯示了一種繪圖的實現。

例8-9:Bitmap緩存


private class CachingWidget extends View {
   private Bitmap cache;
   public CachingWidget(Context context) {
      super(context);
      setMinimumWidth(200);
      setMinimumHeight(200);
   }
   public void invalidateCache {
      cache = null;
      invalidate;
   }
   @Override
   protected void onDraw(Canvas canvas) {
      if (null == cache) {
          cache = Bitmap.createBitmap(
              getMeasuredWidth,
              getMeasuredHeight,
              Bitmap.Config.ARGB_8888);
          drawCachedBitmap(new Canvas(cache));
      }
      canvas.drawBitmap(cache, 0, 0, new Paint);
   }
   // ... definition of drawCachedBitmap
}
  

該部件通常只把緩存位圖cache複製傳遞給onDraw方法的Canvas。只有當緩存標記為過期時,調用invalidateCache才會真正調用drawCachedBitmap來渲染這個widget。

位圖的最常見的應用方式是作為圖形資源的編程表示。當資源是一個圖形時,Resources.getDrawable會返回BitmapDrawable。

把這兩個思想結合起來,緩存一個圖像並把它封裝到Drawable,也會是非常有趣的。它意味著任何可以繪製的事物也可以延遲處理。一個運用了本章所給出的所有技術的應用都可以繪製出房間裡的傢俱(創建一個位圖),然後繞著它轉圈(採用矩陣轉換)。

注意:有了Honeycomb後,Android的渲染架構有了很大變化。這些變化充分利用了不斷增強的GPU處理能力,創建了一組全新的規則,來優化UI圖形繪製。使用新的圖形繪製機制時,採用緩存位圖的方式可能比按需繪製它們效率更低。因此,在使用位圖緩存前,應該優先考慮使用View.setLayerType。