建立自訂檢視模式

本程式碼研究室是 Kotlin 進階課程的一部分。只要您按部就班完成程式碼研究室,就能發揮本課程的最大效益。不過,您不一定要這麼做。所有課程程式碼研究室清單均列於進階 Android 版的 Kotlin 程式碼研究室到達網頁中。

引言

Android 提供大量的 View 子類別,例如 ButtonTextViewEditTextImageViewCheckBoxRadioButton。您可以使用這些子類別來建立使用者介面,讓使用者互動並在應用程式中顯示資訊。如果沒有任何 View 子類別均符合您的需求,您可以建立 View 子類別,也就是所謂的自訂視圖。

如要建立自訂檢視模式,您可以擴充現有的 View 子類別 (例如 ButtonEditText),也可以自行建立 View 的子類別。直接擴充 View 可讓您建立 View 的互動式 UI 元素,只要覆寫 onDraw() 方法即可讓 View 繪圖。

建立自訂檢視模式後,您可以將其新增到活動版面配置中,方法與新增 TextViewButton 時相同。

本課程說明如何藉由延長 View 的方式從頭開始建立自訂檢視畫面。

須知事項

  • 如何使用「活動」建立應用程式,並使用 Android Studio 執行應用程式。

課程內容

  • 如何擴充View以建立自訂檢視畫面。
  • 如何繪製圓形的自訂檢視。
  • 如何使用事件監聽器來處理自訂檢視的使用者互動。
  • 如何在版面配置中使用自訂檢視。

執行步驟

  • 展開「View即可建立自訂檢視畫面。
  • 使用繪圖和繪製值來初始化自訂檢視。
  • 如要覆寫檢視畫面,請覆寫 onDraw()
  • 使用事件監聽器提供自訂檢視行為。
  • 將自訂檢視加入版面配置。

CustomFanController 應用程式示範如何透過擴充 View 類別來建立自訂檢視子類別。新的子類別稱為 DialView

App 顯示了一個形似物理機扇控制的圓形 UI 元素,具有設置為 (0)、低 (1)、中 (2) 和高 (3)。對於用戶觸摸視圖,選擇指示器移動到下一個位置:0-1-2-3,然後返回到 0。此外,如果選擇為 1 以上,則視圖中圓形部分的背景顏色會從灰色變成綠色 (表示風扇電源已開啟)。

資料檢視是應用程式 UI 的基本構成要素。View 類別提供許多子類別,也稱為 UI 小工具,包含一般 Android 應用程式使用者介面的許多需求。

UI 建構區塊 (例如 ButtonTextView) 是擴充 View 類別的子類別。為了節省時間和開發作業,您可以延長其中一種 View 子類別。自訂檢視會繼承上層機構單位的外觀和行為,而您可以覆寫特定外觀的行為或畫面。舉例來說,如果您擴展了 EditText 來建立自訂檢視,該視圖的作用就像是 EditText 檢視一樣,但是也可以自訂用來顯示 X 按鈕,這些文字會清除文字輸入欄位中的文字。

您可以擴充任何 View 子類別 (例如 EditText),以便取得自訂檢視,請從中挑選出您最想達成的子類別。接著,您可以在一個以上的版面配置中,使用自訂檢視畫面和其他任何 View 子類別,當做含有屬性的 XML 元素。

如果要從頭開始建立自訂檢視模式,請擴充 View 類別本身。您的程式碼會覆寫 View 方法來定義檢視的外觀和功能。建立自訂畫面的關鍵在於,您有責任將各種大小和形狀的 UI 元素繪製到畫面上。如果您將現有資料檢視 (例如 Button) 設為子類別,該類別會為您處理繪圖。(您稍後可以在這個程式碼研究室中進一步瞭解繪圖功能)。

如要建立自訂檢視,請按照下列一般步驟操作:

  • 建立延伸 View 的自訂檢視類別,或擴充 View 子類別 (例如 ButtonEditText)。
  • 如果您延長現有 View 子類別,即可只覆寫您要變更的外觀行為或面向。
  • 如果您延長 View 類別的形狀,就會繪製自訂視圖的形狀,並藉由覆寫新類別中的 View 方法 (例如 onDraw()onMeasure()) 來控制其外觀。
  • 加入程式碼以回應使用者互動,並視需要重新繪製自訂檢視。
  • 使用自訂檢視類別做為活動 XML 版面配置的 UI 小工具。您也可以為視圖定義自訂屬性,為不同的版面配置提供自訂檢視。

在這項工作中,您會執行下列作業:

  • 建立使用 ImageView 做為自訂檢視的暫時預留位置的應用程式。
  • 展開「View即可建立自訂檢視畫面。
  • 使用繪圖和繪製值來初始化自訂檢視。

步驟 1:使用 ImageView 預留位置建立應用程式

  1. 使用空白活動範本建立標題為 CustomFanController 的 Kotlin 應用程式。確認套件名稱為 com.example.android.customfancontroller
  2. 開啟「文字」分頁中的 activity_main.xml 以編輯 XML 程式碼。
  3. 使用這組程式碼取代現有的 TextView。這段文字是自訂檢視畫面活動中的標籤。
<TextView
       android:id="@+id/customViewLabel"
       android:textAppearance="@style/Base.TextAppearance.AppCompat.Display3"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:padding="16dp"
       android:textColor="@android:color/black"
       android:layout_marginStart="8dp"
       android:layout_marginEnd="8dp"
       android:layout_marginTop="24dp"
       android:text="Fan Control"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent"/>
  1. 將這個 ImageView 元素新增至版面配置。這是您在這個程式碼研究室中建立的自訂資料檢視預留位置。
<ImageView
       android:id="@+id/dialView"
       android:layout_width="200dp"
       android:layout_height="200dp"
       android:background="@android:color/darker_gray"
       app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       android:layout_marginLeft="8dp"
       android:layout_marginRight="8dp"
       android:layout_marginTop="8dp"/>
  1. 擷取兩個 UI 元素中的字串和維度資源。
  2. 按一下 [設計] 分頁標籤。版面配置應如下所示:

步驟 2. 建立自訂視圖類別

  1. 建立名為「DialView」的新 Kotlin 類別。
  2. 修改類別定義以擴充 View。當系統提示時匯入 android.view.View
  3. 點選 View,然後按一下紅色燈泡。選擇 [Add Android View Builders using '@JvmOverloads']。Android Studio 會新增來自 View 類別的建構函式。@JvmOverloads 註解會指示 Kotlin 編譯器產生這個函式的超載,以取代預設參數值。
class DialView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
  1. DialView 類別定義上方的匯入作業下方,新增頂層 enum 來代表可用的風扇速度。請注意,此 enum 的類型為 Int,因為其值為字串資源,而不是實際的字串。Android Studio 會在每個值中顯示缺少字串資源的錯誤,請稍後修正。
private enum class FanSpeed(val label: Int) {
   OFF(R.string.fan_off),
   LOW(R.string.fan_low),
   MEDIUM(R.string.fan_medium),
   HIGH(R.string.fan_high);
}
  1. enum 下方新增這些常數。使用這些功能來繪製撥號指標和標籤時,
private const val RADIUS_OFFSET_LABEL = 30      
private const val RADIUS_OFFSET_INDICATOR = -35
  1. DialView 類別中,定義幾個必要變數,以便繪製自訂資料檢視。收到要求時匯入 android.graphics.PointF
private var radius = 0.0f                   // Radius of the circle.
private var fanSpeed = FanSpeed.OFF         // The active selection.
// position variable which will be used to draw label and indicator circle position
private val pointPosition: PointF = PointF(0.0f, 0.0f)
  • radius 是圓圈目前的半徑。系統會在畫面上繪圖時設定這個值。
  • fanSpeed 是風扇的目前速度,這是 FanSpeed 列舉值之一。預設值為 OFF
  • 最後,postPosition 則是 X、Y 點,用來繪製畫面上多個視圖的元素。

系統會在這裡建立並初始化這些值,而不是實際繪製檢視畫面,以確保實際執行繪圖步驟的速度很快。

  1. DialView 類別定義中,也可使用幾個基本樣式初始化 Paint 物件。在要求時匯入 android.graphics.Paintandroid.graphics.Typeface。如同先前使用變數,這裡的樣式會在這裡初始化,以加快繪圖步驟。
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
   style = Paint.Style.FILL
   textAlign = Paint.Align.CENTER
   textSize = 55.0f
   typeface = Typeface.create( "", Typeface.BOLD)
}
  1. 開啟 res/values/strings.xml 並新增適用於風扇速度的字串資源:
<string name="fan_off">off</string>
<string name="fan_low">1</string>
<string name="fan_medium">2</string>
<string name="fan_high">3</string>

建立自訂檢視後,您必須能夠繪製。當您擴充 View 子類別 (例如 EditText) 時,該子類別會定義檢視的外觀和屬性,並在螢幕上自行繪製。因此您不必編寫程式碼就能繪製檢視。您可以覆寫父項的方法,以自訂您的檢視。

如果您要從頭開始建立自己的畫面 (延伸 View),就必須在每次螢幕重新整理時繪製整個檢視畫面,並覆寫處理繪圖的 View 方法。為了正確繪製延長 View 的自訂檢視,您必須:

  • 計算 onSizeChanged() 方法。
  • 使用 Paint 物件的樣式設定 Canvas 物件,覆寫 onDraw() 方法來繪製自訂檢視。
  • 回應使用者點選的 invalidate() 方法,變更檢視畫面的繪製方式以使整個檢視畫面失效,進而強制呼叫 onDraw() 以重新檢視視圖。

每次重新整理螢幕時都會呼叫 onDraw() 方法,每秒數次。為避免效能問題並避免視覺效果問題,請盡可能在 onDraw() 下完成工作。請特別注意,請不要在 onDraw() 中配置配置,因為分配的可能會導致垃圾影像集可能會造成影像不流暢。

CanvasPaint 類別提供許多實用的繪圖快速鍵:

你稍後會在程式碼研究室中進一步瞭解 CanvasPaint。如要進一步瞭解 Android 如何繪製檢視,請參閱 Android 如何繪製檢視

在這項工作中,您將使用 onSizeChanged()onDraw() 方法,將風扇控制器自訂檢視畫面在螢幕上繪製 (包括撥號本身、目前位置指標和指標標籤)。你也會建立輔助方法 computeXYForSpeed(),,以計算撥號指標上目前標籤的 X,Y 位置。

步驟 1:計算位置並繪製檢視

  1. DialView 類別的初始化下方,覆寫 View 類別的 onSizeChanged() 方法,即可計算自訂檢視區塊撥號的大小。匯入 kotlinmath.min

    只要資料檢視的大小有所變更,系統就會呼叫 onSizeChanged() 方法,包括在版面配置膨脹時第一次繪製。覆寫 onSizeChanged() 即可計算自訂檢視區塊的大小、尺寸和任何其他值,而不是在每次繪製時重新計算這些項目的大小。在此情況下,您需使用 onSizeChanged() 計算圓環的目前半徑。
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
   radius = (min(width, height) / 2.0 * 0.8).toFloat()
}
  1. onSizeChanged() 下方,加入這段程式碼來定義 PointF 類別的 computeXYForSpeed() 擴充功能函式。在要求時匯入 kotlin.math.coskotlin.math.sinPointF 類別上的這項擴充功能會計算螢幕標籤和目前指標 (0、1、2 或 3) 的 X、Y 座標 (假設該表目前的 FanSpeed 位置和半徑)。您將在onDraw().中使用這個電話號碼
private fun PointF.computeXYForSpeed(pos: FanSpeed, radius: Float) {
   // Angles are in radians.
   val startAngle = Math.PI * (9 / 8.0)   
   val angle = startAngle + pos.ordinal * (Math.PI / 4)
   x = (radius * cos(angle)).toFloat() + width / 2
   y = (radius * sin(angle)).toFloat() + height / 2
}
  1. 使用 CanvasPaint 類別覆寫 onDraw() 方法來在螢幕上顯示視圖。在要求時匯入 android.graphics.Canvas。這是骨骼覆寫:
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   
}
  1. 根據 onDraw() 的風扇速度 (OFF) 或其他值,將這一行加到 onDraw() 中,將繪製顏色設為灰色 (Color.GRAY) 或綠色 (Color.GREEN)。在要求時匯入 android.graphics.Color
// Set dial background color to green if selection not off.
paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
  1. 加入這個代碼,即可透過 drawCircle() 方法在撥號盤上畫圓。此方法會使用目前檢視寬度和高度來尋找圓形的中心點、圓形半徑和目前的油漆顏色。widthheight 屬性是 View 父類別的成員,代表視圖目前的維度。
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
  1. 加入以下程式碼,即可為風扇速度指標符號畫出一個小圓圈,同時加上 drawCircle() 方法。這部分使用 PointFcomputeXYforSpeed() 擴充方法,可根據目前風扇速度計算指標中心的 X、Y 座標。
// Draw the indicator circle.
val markerRadius = radius + RADIUS_OFFSET_INDICATOR
pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
paint.color = Color.BLACK
canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
  1. 最後,在卡盤周圍的適度位置拉伸扇扇速度標籤 (0, 1, 2, 3)。此方法會再次呼叫 PointF.computeXYForSpeed() 以取得每個標籤的位置,並每次重複使用 pointPosition 物件以避免分配。使用 drawText() 繪製標籤。
// Draw the text labels.
val labelRadius = radius + RADIUS_OFFSET_LABEL
for (i in FanSpeed.values()) {
   pointPosition.computeXYForSpeed(i, labelRadius)
   val label = resources.getString(i.label)
   canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
}

完成的 onDraw() 方法如下所示:

override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   // Set dial background color to green if selection not off.
   paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
   // Draw the dial.
   canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
   // Draw the indicator circle.
   val markerRadius = radius + RADIUS_OFFSET_INDICATOR
   pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
   paint.color = Color.BLACK
   canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
   // Draw the text labels.
   val labelRadius = radius + RADIUS_OFFSET_LABEL
   for (i in FanSpeed.values()) {
       pointPosition.computeXYForSpeed(i, labelRadius)
       val label = resources.getString(i.label)
       canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
   }
}

步驟 2:將檢視加入版面配置

如要在應用程式的 UI 中加入自訂檢視,請在活動 XML 版面配置中指定該元素。如同使用任何其他 UI 元素,您可以使用 XML 元素屬性來控制其外觀和行為。

  1. activity_main.xml 中,將 dialViewImageView 標記變更為 com.example.android.customfancontroller.DialView,並刪除 android:background 屬性。DialView 和原始 ImageView 都會沿用 View 類別的標準屬性,因此不需變更任何其他屬性。新的 DialView 元素如下所示:
<com.example.android.customfancontroller.DialView
       android:id="@+id/dialView"
       android:layout_width="@dimen/fan_dimen"
       android:layout_height="@dimen/fan_dimen"
       app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       android:layout_marginLeft="@dimen/default_margin"
       android:layout_marginRight="@dimen/default_margin"
       android:layout_marginTop="@dimen/default_margin" />
  1. 執行應用程式。活動控制畫面會顯示在活動中。

最終工作就是啟用自訂檢視,以便在使用者輕觸檢視時執行動作。每次觸摸都必須將選擇指示器移動到下一個位置:off-1-2-3,然後返回到斷緣。此外,如果選擇為 1 或更高,請將背景為灰色由綠色為綠色,表示風扇電源已打開。

如要將自訂資料檢視設為可點擊,您必須:

  • 將資料檢視的 isClickable 屬性設為 true。如此一來,自訂檢視就能回應點擊。
  • 導入View類別的 performClick(),以便在使用者點擊資料檢視時執行相關作業。
  • 呼叫 invalidate() 方法。這會讓 Android 系統呼叫 onDraw() 方法以重新檢視視圖。

一般而言,使用標準 Android 檢視模式時,您必須導入 OnClickListener(),以便在使用者點擊該檢視畫面時執行特定動作。針對自訂檢視,您可以改為導入 View 類別的 performClick() 方法,然後呼叫 superperformClick(). 預設的 performClick() 方法也呼叫了 onClickListener(),因此您可以將動作新增到 performClick(),並保留 onClickListener(),以便您或其他可能使用自訂檢視的開發人員加以自訂。

  1. DialView.kt 中,在 FanSpeed 列舉內加入擴充功能函式 next(),將目前的風扇速度變更為清單中的下一個速度 (從 OFF 變更為 LOWMEDIUMHIGH,然後再返回 OFF)。完整的列舉現在看起來會像這樣:
private enum class FanSpeed(val label: Int) {
   OFF(R.string.fan_off),
   LOW(R.string.fan_low),
   MEDIUM(R.string.fan_medium),
   HIGH(R.string.fan_high);

   fun next() = when (this) {
       OFF -> LOW
       LOW -> MEDIUM
       MEDIUM -> HIGH
       HIGH -> OFF
   }
}
  1. DialView 類別的 onSizeChanged() 方法前面,新增 init() 區塊。如果將資料檢視的 isClickable 屬性設為 true,該資料檢視會接受使用者輸入內容。
init {
   isClickable = true
}
  1. 在下方使用 init(), 覆寫以下程式碼中的 performClick() 方法。
override fun performClick(): Boolean {
   if (super.performClick()) return true

   fanSpeed = fanSpeed.next()
   contentDescription = resources.getString(fanSpeed.label)
  
   invalidate()
   return true
}

呼叫 super。您必須先啟動 performClick(),如此即可啟用無障礙事件和呼叫 onClickListener()

接下來的兩行會使用 next() 方法增加風扇的速度,並將視圖的內容說明設為代表目前速度 (關閉、1、2 或 3) 的字串資源。

簡單來說,invalidate() 方法會使整個檢視畫面失效,強制呼叫 onDraw() 以重新檢視視圖。如果自訂資料檢視中的內容因任何理由 (包括使用者互動) 而有變化,而需要顯示變更,請呼叫 invalidate().

  1. 執行應用程式。輕觸 DialView 元素即可將指標從 1 移至 1。撥號鍵盤應變成綠色。每次輕觸時,指標都會移至下一個位置。當指示燈恢復為關閉狀態時,表情圖示應再次變成灰色。

本範例顯示了使用自訂資料檢視使用自訂屬性的基本機制。您可以為各個「DialView」類別定義自訂屬性,並為每個支持者撥號位置指定不同的顏色。

  1. 建立並開啟res/values/attrs.xml
  2. <resources> 中新增 <declare-styleable> 資源元素。
  3. <declare-styleable> 資源元素中,加入三個 attr 元素,每個屬性各一個,搭配 nameformatformat 就像類型,在本例中為 color
<?xml version="1.0" encoding="utf-8"?>
<resources>
       <declare-styleable name="DialView">
           <attr name="fanColor1" format="color" />
           <attr name="fanColor2" format="color" />
           <attr name="fanColor3" format="color" />
       </declare-styleable>
</resources>
  1. 開啟 activity_main.xml 版面配置檔案。
  2. DialView 中,加入 fanColor1fanColor2fanColor3 的屬性,並將值設為下方顯示的顏色。使用自訂屬性屬於 schemas.android.com/apk/res/your_app_package_name 命名空間而非 android 命名空間時,請使用 app: 做為自訂屬性的介面 (位於 app:fanColor1),而不是 android:
app:fanColor1="#FFEB3B"
app:fanColor2="#CDDC39"
app:fanColor3="#009688"

為了使用 DialView 類別中的屬性,您必須擷取這些屬性。系統會將這些資訊儲存在 AttributeSet 中,並在建立後傳送到您的課程 (如果有的話)。您將擷取 init 中的屬性,並將屬性值指派給本機變數進行快取。

  1. 開啟 DialView.kt 類別檔案。
  2. DialView 內宣告變數來快取屬性值。
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0
  1. init 區塊中,使用 withStyledAttributes 擴充功能函式新增下列程式碼。由您提供屬性和檢視,以及設定本機變數。匯入 withStyledAttributes 也會匯入正確的 getColor() 函式。
context.withStyledAttributes(attrs, R.styleable.DialView) {
   fanSpeedLowColor = getColor(R.styleable.DialView_fanColor1, 0)
   fanSpeedMediumColor = getColor(R.styleable.DialView_fanColor2, 0)
   fanSeedMaxColor = getColor(R.styleable.DialView_fanColor3, 0)
}
  1. 使用 onDraw() 中的本機變數,根據目前的風扇速度設定調節顏色。將下方顯示繪製顏色的線條 (paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN) 換成以下程式碼。
paint.color = when (fanSpeed) {
   FanSpeed.OFF -> Color.GRAY
   FanSpeed.LOW -> fanSpeedLowColor
   FanSpeed.MEDIUM -> fanSpeedMediumColor
   FanSpeed.HIGH -> fanSeedMaxColor
} as Int
  1. 執行應用程式、按一下撥號鍵盤後,各個位置的色彩設定會有所不同 (如下所示)。

如要進一步瞭解自訂資料檢視屬性,請參閱建立資料檢視類別一文。

協助工具是一套設計、實作與測試技術,可讓所有使用者 (包括身心障礙者) 使用您的應用程式。

影響他人使用 Android 裝置的常見身心障礙包括視障、低視能、色盲、失真或聽力受損,以及行動不便的使用者技能受限。開發在無障礙環境中開發應用程式時,除了改善身心障礙使用者以外,您還要改善所有使用者的使用體驗。

在預設情況下,Android 會在標準 UI 檢視模式中提供多項無障礙功能,例如 TextViewButton。但是在建立自訂檢視時,您需要考慮自訂檢視如何提供其他功能,例如螢幕上內容的語音說明。

在這項工作中,您將瞭解 TalkBack 和 Android 螢幕閱讀器,並修改應用程式,加入適用於 DialView 自訂檢視畫面的語音提示和說明。

步驟 1. 探索 TalkBack

TalkBack 是 Android 的內建螢幕閱讀器。由於 Android 會朗讀螢幕元素,因此啟用 TalkBack 後,使用者不需看到螢幕就能與 Android 裝置互動。視障使用者可能必須搭配 TalkBack 使用您的應用程式。

在這項工作中,您必須啟用 TalkBack 來瞭解螢幕閱讀器的運作方式和瀏覽應用程式的方式。

  1. 在 Android 裝置或模擬器中,依序前往 [設定] > [協助工具] > [TalkBack]
  2. 輕觸 [開啟/關閉] 切換按鈕以開啟 TalkBack。
  3. 輕觸 [確定] 以確認權限。
  4. 如果系統顯示提示,請確認裝置密碼。如果你是第一次執行 TalkBack,系統會啟動教學課程。(如果您使用的是舊版裝置,則可能無法使用教學課程)。
  5. 閉上眼睛時,這樣做是很實用的方法。如果日後需要再次開啟教學課程,請前往 [設定] > [協助工具] > [TalkBack] > [設定] > [設定] > [啟動 TalkBack 教學課程]
  6. 編譯並執行 CustomFanController 應用程式,或是使用裝置上的 [總覽] 或 [近期存取] 按鈕開啟應用程式。請注意,開啟 TalkBack 時,系統會發出應用程式名稱,並且加上「TextView」標籤文字 (「風扇控制」)。不過,如果您輕觸 DialView 視圖本身,系統就不會讀出關於檢視狀態 (撥號的目前設定) 或是輕觸視圖來啟動的動作。

步驟 2:為撥號標籤新增內容說明

內容說明可說明應用程式內瀏覽的意義和用途。這些標籤可讓螢幕閱讀器 (例如 Android 的 TalkBack 功能) 準確地瞭解每個元素的功能。如果是靜態資料檢視 (例如 ImageView),您可以在版面配置檔案的 contentDescription 屬性中加入內容說明。文字檢視 (TextViewEditText) 會自動使用檢視中的文字做為內容說明。

如果是自訂風扇控制視圖,則每次有人點擊檢視時,都必須動態更新內容說明,以指出目前的風扇設定。

  1. DialView 類別的底部,宣告沒有引數或傳回類型的函式 updateContentDescription()
fun updateContentDescription() {
}
  1. updateContentDescription() 內,將自訂檢視的 contentDescription 屬性變更為與目前風扇速度相關的字串資源 (關閉、1、2 或 3)。這些標籤與 onDraw() 中使用的標籤相同,當畫面上繪圖時。
fun updateContentDescription() {
   contentDescription = resources.getString(fanSpeed.label)
}
  1. 向上捲動到 init() 區塊,並在該區塊結尾新增 updateContentDescription()。這會在初始化檢視時初始化內容說明。
init {
   isClickable = true
   // ...

   updateContentDescription()
}
  1. 請在 invalidate() 之前透過 performClick() 方法,再次加入 updateContentDescription() 的呼叫。
override fun performClick(): Boolean {
   if (super.performClick()) return true
   fanSpeed = fanSpeed.next()
   updateContentDescription()
   invalidate()
   return true
}
  1. 編譯並執行應用程式,並確認 TalkBack 已開啟。輕按以變更撥號畫面的設定

步驟 3:加入點擊動作的詳細資訊

你可以在 TalkBack 中停止顯示自己的畫面。但是,如果您的檢視功能可以指出「可以」啟用 (「輕觸兩下」即可啟用),同時也要說明「啟用」會啟動的情況 (「輕觸兩下即可進行變更」或「輕觸兩下即可重設」) 將十分實用。

如要這麼做,您可透過無障礙代表您可以透過協助工具委派功能,透過組成功能 (而非沿用設定) 自訂應用程式的無障礙功能。

在這項工作中,您將使用 Android Jetpack 程式庫 (androidx.*) 中的協助工具類別,確保回溯相容性。

  1. DialView.ktinit 區塊中,將視圖的無障礙委派對象設為新的 AccessibilityDelegateCompat 物件。在要求時匯入 androidx.core.view.ViewCompatandroidx.core.view.AccessibilityDelegateCompat。這項策略可大幅提高應用程式的回溯相容性。
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   
})
  1. AccessibilityDelegateCompat 物件中,使用 AccessibilityNodeInfoCompat 物件覆寫 onInitializeAccessibilityNodeInfo() 函式,並呼叫 Super's 方法。當系統提示時匯入 androidx.core.view.accessibility.AccessibilityNodeInfoCompat
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)

   }  
})

每個檢視都含有無障礙功能節點的樹狀結構,該區塊不一定會對應至實際的檢視區塊元件。Android 的無障礙服務會瀏覽這些節點,找出檢視相關資訊 (例如支援朗讀內容說明,或是可對該資料檢視執行的操作)。建立自訂檢視時,你可能需要覆寫節點資訊,才能提供自訂的自訂資訊。在這種情況下,您必須覆寫節點資訊,指出檢視動作的動作有自訂的資訊。

  1. onInitializeAccessibilityNodeInfo() 中建立新的 AccessibilityNodeInfoCompat.AccessibilityActionCompat 物件,並將它指派給 customClick 變數。將 AccessibilityNodeInfo.ACTION_CLICK 常數與預留位置字串傳送至建構函式。在要求時匯入 AccessibilityNodeInfo
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
         AccessibilityNodeInfo.ACTION_CLICK,
        "placeholder"
      )
   }  
})

AccessibilityActionCompat 類別會呈現資料檢視的無障礙設計。典型的操作是指點擊或輕觸,就像這裡一樣,但其他動作可能包括取得或失去焦點、剪貼簿作業 (剪下/複製/貼上),或是在檢視畫面中捲動。此類別的建構函式需要一個動作常數 (此處為 AccessibilityNodeInfo.ACTION_CLICK) 和 TalkBack 所使用的字串,以表示動作。

  1. "placeholder" 字串替換為對 context.getString() 的呼叫,以擷取字串資源。針對特定資源,測試目前的風扇速度。如果目前的速度為 FanSpeed.HIGH,則字串為 "Reset"。如果風扇速度為其他值,則字串為 "Change." You' 您會在後續步驟中建立這些字串資源。
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
         AccessibilityNodeInfo.ACTION_CLICK,
        context.getString(if (fanSpeed !=  FanSpeed.HIGH) R.string.change else R.string.reset)
      )
   }  
})
  1. customClick 定義以括號括住後,請使用 addAction() 方法在節點資訊物件中加入新的無障礙動作。
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
       super.onInitializeAccessibilityNodeInfo(host, info)
       val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
           AccessibilityNodeInfo.ACTION_CLICK,
           context.getString(if (fanSpeed !=  FanSpeed.HIGH) 
                                 R.string.change else R.string.reset)
       )
       info.addAction(customClick)
   }
})
  1. res/values/strings.xml 中,新增「變更」和「重設」的字串資源。
<string name="change">Change</string>
<string name="reset">Reset</string>
  1. 編譯並執行應用程式,並確認 TalkBack 已開啟。注意:詞組「輕觸兩下即可啟用」現在已經是「輕觸兩下來變更」;(如果風扇速度低於「高」或「3」)請注意,TalkBack 服務本身會提供「輕觸兩下...」提示訊息。

下載已完成程式碼研究室的程式碼。

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-custom-views


您也可以選擇以 ZIP 檔案格式下載存放區,再將其解壓縮,然後在 Android Studio 中開啟該檔案。

下載 Zip

  • 如要建立自訂沿用 View 子類別 (例如 EditText) 外觀與行為的自訂檢視,請新增可擴展該子類別的新類別,然後覆寫某些子類別的方法來進行調整。
  • 如要建立自訂大小和形狀的自訂檢視模式,請新增延伸 View 的類別。
  • 覆寫 View 方法 (例如 onDraw()) 來定義視圖的形狀和基本外觀。
  • 使用 invalidate() 可強制繪製繪圖或重新繪製檢視畫面。
  • 如要獲得最佳效能,請分配變數並指派任何必要值,以便在使用 onDraw() 前對其進行繪圖和繪製,例如在初始化成員變數時。
  • performClick() (而非 OnClickListener()) 覆寫至自訂資料檢視,以便提供資料檢視的互動行為。如此一來,您或其他可能使用自訂檢視類別的 Android 開發人員即可使用 onClickListener() 提供進一步的行為。
  • 在 XML 版面配置檔案中加入自訂檢視,以定義屬性的外觀,就如同其他 UI 元素一樣。
  • values 資料夾中建立 attrs.xml 檔案來定義自訂屬性。接著,您即可在 XML 版面配置檔案中使用自訂檢視的自訂屬性。

Udacity 課程:

Android 開發人員說明文件:

影片:

這個部分會列出在代碼研究室中,受老師主導的課程作業的可能學生作業。由老師自行決定要執行下列動作:

  • 視需要指派家庭作業。
  • 告知學生如何提交家庭作業。
  • 批改家庭作業。

老師可視需要使用這些建議,並視情況指派其他合適的家庭作業。

如果您是自行操作本程式碼研究室,歡迎透過這些家庭作業來測試自己的知識。

第 1 題

若要計算自訂檢視在首次指派大小時的位置、維度以及任何其他值,您覆寫哪個方法?

onMeasure()

onSizeChanged()

invalidate()

onDraw()

第 2 題

為表示您是否想利用 onDraw() 來重新檢視視圖?在屬性值變更後,您透過 UI 執行緒呼叫哪種方法?

▢ onMeasure()

▢ onSizeChanged()

▢ invalidate()

▢ getVisibility()

第 3 題

如要針對互動功能添加互動功能,你應該覆寫哪一種 View 方法?

▢ setOnClickListener()

▢ onSizeChanged()

▢ isClickable()

▢ performClick()

如要瞭解本課程中其他程式碼研究室的連結,請參閱 Kotlin 的進階 Android 程式碼研究室到達網頁。