自訂檢視區塊元件

Android 會根據基本的版面配置類別 ViewViewGroup,提供複雜且強大的元件化模型來建構 UI。首先,這個平台提供了各種預先建構的 View 和 ViewGroup 子類別,分別稱為「小工具」和「版面配置」,可用來建構 UI。

部分可用小工具清單包括 ButtonTextViewEditTextListViewCheckBoxRadioButtonGallerySpinner,以及專用 AutoCompleteTextViewImageSwitcherTextSwitcher

可用的版面配置包括 LinearLayoutFrameLayoutRelativeLayout 等。如需更多範例,請參閱常見版面配置物件

如果預先建立的所有小工具和版面配置都不符合您的需求,您可以建立自己的 View 子類別。如果只需要對現有的小工具或版面配置進行微幅調整,只需簡單將小工具或版面配置加入子類別,並覆寫其方法即可。

建立自己的 View 子類別可讓您精確控制畫面元素的外觀和功能。為讓您理解您對自訂檢視區塊的控制權,以下範例展示了您可對自訂檢視區塊執行的操作:

  • 您可以建立完全自訂顯示的 View 類型,例如使用 2D 圖形顯示的「音量控制」旋鈕,這類似於模擬的電子控制項。
  • 您可以將一組 View 元件合併為一個新的元件,類似於 ComboBox (彈出式視窗清單和可用輸入文字欄位的組合),或者雙窗格選取器控制項 (左側和右側窗格中各有一個清單,您可以重新指派哪個項目應位於哪個清單中),等等。
  • 您可以覆寫 EditText 元件在螢幕上的顯示方式 (記事本教學課程使用此方法來建立效果良好的劃線型記事本頁面)。
  • 您可以擷取按下按鍵之類的其他事件,並以自訂方式進行處理 (例如在遊戲中)。

以下章節說明了如何建立自訂檢視區塊並在應用程式中使用。如需詳細的參考資訊,請參閱 View 類別。

基本做法

以下總覽大致說明了建立自有檢視區塊的須知事項:

  1. 使用自己的類別擴充現有 View 類別或子類別。
  2. 覆寫父類別中的某些方法。要覆寫的父類別方法以「on」開頭,例如 onDraw()onMeasure()onKeyDown()。這與 ActivityListActivity 中的 on... 事件類似,您可以覆寫生命週期和其他功能掛鉤。
  3. 使用新的擴充功能類別。完成之後,您可以在建立擴充功能類別的檢視區塊中使用它們。

提示:擴充功能類別可定義為使用它們的活動中的內部類別。這種做法可以控制對它們的存取權限,因而十分有用,但並非必要 (因為您或許想要建立新的公開檢視區塊,以在應用程式內的更大範圍內使用)。

完全自訂元件

您可以使用完全自訂元件來建立圖形元件,並依需求顯示。這可以是看起來像傳統模擬量測計的圖形 VU 計量器,或者可以是伴唱文字檢視區塊,會有一個活力球沿著字詞移動,讓您在卡拉 OK 機上唱歌。無論哪種方式,您都可能希望這些內建元件永遠不要執行某些操作,無論您如何組合它們。

幸運的是,您可以輕鬆建立元件,使其具有您想要的外觀和行為,能夠限制它們的可能只有您的想象力、螢幕的大小和可用的處理能力 (請記住,您的應用程式最終需要在某個裝置上執行,而該裝置的處理能力可能大大低於您的電腦工作站)。

如何建立完全自訂的元件:

  1. 毫不意外,您可以擴充的最常見檢視區塊正是 View,所以通常是先擴充該檢視區塊,以建立新的上層元件。
  2. 您可以提供一個建構函式,從 XML 中擷取屬性和參數,也可以使用您自己的此類屬性和參數 (例如 VU 計量器的色彩和範圍,或指針的寬度和阻尼等)。
  3. 您可以建立自己的事件監聽器、屬性存取子和修飾詞,以及在元件類別中建立更複雜的行為。
  4. 大部分人都想要覆寫 onMeasure(),如果您希望元件顯示某些內容,則還需要覆寫 onDraw()。雖然兩者都有預設行為,但預設 onDraw() 不執行任何動作,而預設 onMeasure() 會一律將大小設定為 100x100,這可能不是您想要的大小。
  5. 您可以視需要覆寫其他 on... 方法。

擴充 onDraw()onMeasure()

onDraw() 方法提供 Canvas,您可以在上面實作任何所需項目,例如 2D 圖形、其他標準或自訂元件、樣式化文字,或是您可以想到的任何內容。

注意:這不適用於 3D 圖形。若您想使用 3D 圖形,則必須擴充 SurfaceView,而非 View,並從另一個執行緒進行繪製。詳情請參閱 GLSurfaceViewActivity 範例。

onMeasure() 有點複雜。onMeasure() 是元件與其容器之間顯示合約的重要部件。您必須覆寫 onMeasure(),才能有效且準確地報告其所含零件的測量結果。加上父項的限制要求 (這些要求傳遞至 onMeasure() 方法),以及在計算完成後使用測得的寬度和高度呼叫 setMeasuredDimension() 方法的要求,使得情況更加複雜。如果您無法透過覆寫的 onMeasure() 方法呼叫此方法,在測量時就會產生例外狀況。

大致來說,實作 onMeasure() 看起來會像這樣:

  1. 系統會使用寬度和高度測量規格 (widthMeasureSpecheightMeasureSpec 參數,二者都是代表尺寸的整數代碼) 呼叫覆寫的 onMeasure() 方法,這些規格應視為所得寬度和高度測量結果的限制要求。您可以閱讀 View.onMeasure(int, int) 的參考說明文件,找到這些規格要求的完整限制 (這份參考說明文件還很好地解釋了整個測量作業)。
  2. 元件的 onMeasure() 方法應計算顯示元件所需的測量寬度和高度。應盡量遵守傳遞的規格,不過也可以選擇超出規格 (在這種情況下,父項可選擇要採取的動作,例如剪輯、捲動、擲回例外狀況,或要求 onMeasure() 使用不同的測量規格再試一次)。
  3. 計算寬度和高度後,您必須使用計算得出的測量結果呼叫 setMeasuredDimension(int width, int height) 方法。否則將擲回例外狀況。

以下內容概略說明了架構在檢視區塊上呼叫的其他標準方法:

類別 方法 說明
創作 建構函式 當透過程式碼建立檢視區塊時,系統會呼叫一種形式的建構函式;當透過版面配置檔案加載檢視區塊時,系統會呼叫另一種形式的建構函式。第二種形式應剖析並套用版面配置檔案中定義的所有屬性。
onFinishInflate() 將在從 XML 中加載檢視區塊及其所有子項之後呼叫。
版面配置 onMeasure(int, int) 呼叫此項以確定該檢視區塊及其所有子項的大小要求。
onLayout(boolean, int, int, int, int) 當該檢視區塊應當向其所有子項指派大小和位置時呼叫。
onSizeChanged(int, int, int, int) 當該檢視區塊的大小變更時呼叫。
繪圖 onDraw(Canvas) 在檢視區塊應顯示其內容時呼叫。
事件處理 onKeyDown(int, KeyEvent) 當發生新的按鍵事件時呼叫。
onKeyUp(int, KeyEvent) 當發生按鍵向上事件時呼叫。
onTrackballEvent(MotionEvent) 當發生軌跡球動作事件時呼叫。
onTouchEvent(MotionEvent) 當發生觸控螢幕動作事件時呼叫。
焦點 onFocusChanged(boolean, int, Rect) 當檢視區塊獲得或失去焦點時呼叫。
onWindowFocusChanged(boolean) 當包含檢視區塊的視窗獲得或失去焦點時呼叫。
附加 onAttachedToWindow() 當檢視區塊附加至視窗時呼叫。
onDetachedFromWindow() 當檢視區塊從視窗卸離時呼叫。
onWindowVisibilityChanged(int) 當包含檢視區塊的視窗顯示設定變更時呼叫。

複合控制項

如果您不想建立完全自訂的元件,而是想將一組現有控制項組合成可重複使用的元件,則可以建立複合元件 (或複合控制項)。概括而言,這會將多個基礎控制項 (或檢視區塊) 合併為一個邏輯項目群組,該群組可視為單一項目。舉例來說,「下拉式方塊」可視為單行 EditText 欄位和具有附加 PopupList 的鄰近按鈕的組合。如果按下按鈕並從清單中選取項目,該項目就會填入 EditText 欄位,如果使用者願意,他們也可以直接在 EditText 中輸入任何內容。

在 Android 中,已經有兩個其他檢視區塊可以達成此目的:SpinnerAutoCompleteTextView。不過,下拉式方塊的概念更易於理解。

如何建立複合元件:

  1. 常見的起始點是某種類型的版面配置,可以建立一個擴充版面配置的類別。如果是下拉式方塊,我們可能會使用水平方向的 LinearLayout。請注意,可以在其中內嵌其他版面配置,因此複合元件可以任意構建且可能十分複雜。就像使用「活動」一樣,您可以使用宣告式 (基於 XML) 方法建立內含元件,也可以透過程式化方式利用程式碼進行內嵌。
  2. 在新類別的建構函式中,擷取父類別所需的參數,並先將其傳遞至父類別建構函式。然後設定要在新元件中使用的其他檢視區塊;您可以在這裡建立 EditText 欄位和 PopupList。您也可以在 XML 中引入自己的屬性及參數,建構函式將從中提取這些資訊並使用。
  3. 您還可以為內涵檢視區塊可能產生的事件建立事件監聽器,例如用於清單項目點擊事件監聽器的事件監聽器方法,以便在選取清單時更新 TextText 的內容。
  4. 您也可以建立含有存取子和修飾詞的屬性,例如允許在元件中先設定 EditText 值,並視需要查詢其內容。
  5. 擴充版面配置時,您無需覆寫 onDraw()onMeasure() 方法,因為版面配置的預設行為可能運作正常。不過,您還是可以視需要覆寫這些方法。
  6. 您可以覆寫 onKeyDown() 之類的其他 on... 方法,這樣,當按下特定按鍵時,可以在下拉式方塊的彈出式清單中選擇某些預設值。

總而言之,將版面配置用作自訂控制項的基礎有以下優點:

  • 您可以使用宣告式 XML 檔案指定版面配置,方式與活動畫面類似;或者,您也可以透過程式化方式建立檢視區塊,並利用程式碼將其嵌入版面配置中。
  • onDraw()onMeasure() 方法 (以及大多數其他 on... 方法) 都有可能具有適用的行為,因此您無需覆寫這些方法。
  • 總而言之,您可以快速建構任何複雜程度的複合檢視區塊,並將其視為單一元件重複使用。

修改現有檢視區塊類型

在某些情況下,還可以使用一種更簡單的方法來建立自訂檢視區塊。如果元件已經與您想要的效果十分接近,只需擴充元件並覆寫想要變更的行為即可。您能夠對完全自訂元件執行的所有操作,都可以在現有元件上執行,但是從檢視區塊階層中更專業的類別入手,您還可以免費獲得許多行為,它們可能正好能夠滿足您的需求。

舉例來說,記事本應用程式示範了使用 Android 平台的許多方面。其中一個就是擴充 TextText 檢視區塊,以製作劃線的記事本。這並不是一個最佳範例,而且用於執行此操作的 API 可能會變更,但這個範例表明了基本原則。

如果您還沒有這樣做,請將記事本範例匯入 Android Studio (或使用提供的連結查看來源)。請特別留意 NoteEditor.java 檔案中 LinedEditText 的定義。

在這個檔案中,需要留意以下事項:

  1. 定義

    該類別的定義如下:
    public static class LinedEditText extends EditText

    • LinedEditText 定義為 NoteEditor 活動中的內部類別,但對外公開,因此,在需要時可從 NoteEditor 類別外部以 NoteEditor.LinedEditText 身分存取該類別。
    • 這是 static,意味著其不會產生所謂「合成方法」,該方法讓其能夠存取父類別中的資料,這進而表示它能夠真正作為單獨的類別行事,而非與 NoteEditor 強烈相關的類別。如果內部類別不需要存取外部類別的狀態,則可以使用這種更簡潔的方法來建立內部類別,使產生的類別更小,並允許其他類別輕鬆使用它。
    • 它會擴充 EditText,也就是我們在此範例中選擇自訂的檢視區塊。完成後,新類別將能夠替代常規的 EditText 檢視區塊。
  2. 類別初始化

    如同往常,系統會先呼叫父類別。此外,這不是預設建構函式,而是參數化建構函式。當從 XML 版面配置檔案加載 EditText 時,使用這些參數建立了 EditText,因此,我們的建構函式必須擷取這些參數,並將其傳遞至父類別建構函式。

  3. 已覆寫的方法

    這個範例僅覆寫一個方法 (onDraw()),但在您建立自訂元件時,可能需要覆寫其他方法。

    在這個範例中,覆寫 onDraw() 方法讓我們能夠在 EditText 檢視區塊畫布上繪製藍線 (畫布將傳遞至覆寫的 onDraw() 方法)。在方法結束前,系統會呼叫 Super.onDraw() 方法。需要叫用父類別方法,在本範例中,我們會在繪製想要包含的行後再執行此操作。

  4. 使用自訂元件

    現在,我們擁有自訂元件了,但該怎麼使用呢?在記事本範例中,會直接從宣告式版面配置使用自訂元件,所以,我們看一下 res/layout 資料夾中的 note_editor.xml

    <view xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.example.android.notepad.NoteEditor$LinedEditText"
        android:id="@+id/note"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent"
        android:padding="5dp"
        android:scrollbars="vertical"
        android:fadingEdge="vertical"
        android:gravity="top"
        android:textSize="22sp"
        android:capitalize="sentences"
    />
    
    • 自訂元件建立作為 XML 中的通用檢視區塊,並使用完整套件指定類別。另請注意,我們使用 NoteEditor$LinedEditText 標記來參照定義的內部類別,這是在 Java 程式設計語言中參照內部類別的標準方法。

      如果您的自訂檢視區塊元件未定義為內部類別,則可使用 XML 元素名稱宣告檢視區塊元件,並排除 class 屬性。例如:

      <com.example.android.notepad.LinedEditText
        id="@+id/note"
        ... />
      

      可以看到,LinedEditText 類別現已成為獨立的類別檔案。當類別以巢狀形式嵌入 NoteEditor 類別時,這項技術將無法運作。

    • 定義中的其他屬性和參數會傳遞至自訂元件建構函式中,然後會傳遞至 EditText 建構函式,這樣您就會為 EditText 檢視區塊使用相同的參數。您也可以自行新增參數,我們會在下方說明這一點。

這樣就大功告成了。當然,這是一個簡單的案例,但關鍵點已經表明,如果您想要更複雜的自訂元件,建立過程也會更加複雜。

更複雜的元件可能會覆寫更多 on... 方法,並引入部分自己的輔助程式方法,同時會大量自訂屬性和行為。唯一的限制就是您的想像力,以及元件需要執行的操作。