カスタムビューの作成

この Codelab は、Kotlin を使った高度な Android 開発コースの一部です。Codelab を順番に進めると、このコースを最大限に活用できますが、これは必須ではありません。コースの Codelab はすべて、Kotlin を使った高度な Android 開発の Codelab ランディング ページに記載されています。

はじめに

Android は、ButtonTextViewEditTextImageViewCheckBoxRadioButton など、多数の View サブクラスを提供しています。これらのサブクラスを使用して、ユーザー操作を可能にし、アプリ内で情報を表示する UI を構築できます。View サブクラスのいずれもニーズを満たさない場合は、カスタム ビューと呼ばれる View サブクラスを作成できます。

カスタムビューを作成するには、既存の View サブクラス(ButtonEditText など)を拡張するか、View の独自のサブクラスを作成します。View を直接拡張することで、ViewonDraw() メソッドをオーバーライドして描画することで、任意のサイズと形状のインタラクティブな UI 要素を作成できます。

カスタムビューを作成したら、TextViewButton を追加するのと同じ方法で、アクティビティ レイアウトに追加できます。

このレッスンでは、View を拡張してカスタムビューを最初から作成する方法について説明します。

前提となる知識

  • Activity を含むアプリを作成し、Android Studio を使用して実行する方法。

学習内容

  • View を拡張してカスタムビューを作成する方法。
  • 円形のカスタムビューを描画する方法。
  • リスナーを使用してカスタムビューに対するユーザー操作を処理する方法。
  • レイアウトでカスタムビューを使用する方法。

演習内容

  • View を拡張してカスタムビューを作成します。
  • 描画とペイントの値を使用してカスタムビューを初期化します。
  • onDraw() をオーバーライドしてビューを描画します。
  • リスナーを使用して、カスタム ビューの動作を提供します。
  • カスタムビューをレイアウトに追加します。

CustomFanController アプリは、View クラスを拡張してカスタムビュー サブクラスを作成する方法を示しています。新しいサブクラスは DialView と呼ばれます。

アプリには、オフ(0)、弱(1)、中(2)、強(3)の設定がある、物理的な扇風機のコントロールに似た円形の UI 要素が表示されます。ユーザーがビューをタップすると、選択インジケーターが次の位置(0-1-2-3)に移動し、0 に戻ります。また、選択が 1 以上の場合は、ビューの円形部分の背景色が灰色から緑色に変わり(ファンの電源が入っていることを示します)、

ビューは、アプリの UI の基本的な構成要素です。View クラスには、UI ウィジェットと呼ばれる多くのサブクラスが用意されており、一般的な Android アプリのユーザー インターフェースのニーズの多くに対応しています。

ButtonTextView などの UI ビルディング ブロックは、View クラスを拡張するサブクラスです。時間と開発の手間を省くため、これらの View サブクラスのいずれかを拡張できます。カスタムビューは親の外観と動作を継承し、変更する動作または外観の側面をオーバーライドできます。たとえば、EditText を拡張してカスタムビューを作成した場合、そのビューは EditText ビューと同じように動作しますが、テキスト入力フィールドからテキストをクリアする X ボタンを表示するようにカスタマイズすることもできます。

EditText などの View サブクラスを拡張してカスタムビューを取得できます。目的を達成するのに最も近いものを選択してください。その後、1 つ以上のレイアウトで、他の View サブクラスと同様に、属性を持つ XML 要素としてカスタムビューを使用できます。

独自のカスタムビューをゼロから作成するには、View クラス自体を拡張します。コードは View メソッドをオーバーライドして、ビューの外観と機能を定義します。独自のカスタムビューを作成するうえで重要なのは、任意のサイズと形状の UI 要素全体を画面に描画する責任があるということです。Button などの既存のビューをサブクラス化する場合、そのクラスが描画を処理します。(描画については、この Codelab で後ほど詳しく説明します)。

カスタムビューを作成する一般的な手順は次のとおりです。

  • View を拡張するカスタムビュー クラス、または View サブクラス(ButtonEditText など)を拡張するカスタムビュー クラスを作成します。
  • 既存の View サブクラスを拡張する場合は、変更する動作または外観の側面のみをオーバーライドします。
  • View クラスを拡張する場合は、新しいクラスで onDraw()onMeasure() などの View メソッドをオーバーライドして、カスタム ビューの形状を描画し、その外観を制御します。
  • ユーザー操作に応答するコードを追加し、必要に応じてカスタムビューを再描画します。
  • アクティビティの XML レイアウトで、カスタムビュー クラスを UI ウィジェットとして使用します。ビューのカスタム属性を定義して、さまざまなレイアウトでビューをカスタマイズすることもできます。

このタスクでは、次のことを行います。

  • カスタムビューの一時的なプレースホルダとして ImageView を使用してアプリを作成します。
  • View を拡張してカスタムビューを作成します。
  • 描画とペイントの値を使用してカスタムビューを初期化します。

ステップ 1: ImageView プレースホルダを含むアプリを作成する

  1. Empty Activity テンプレートを使用して、タイトルが 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 要素をレイアウトに追加します。これは、この Codelab で作成するカスタムビューのプレースホルダです。
<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 constructors 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 を追加します。値は実際の文字列ではなく文字列リソースであるため、この enumInt 型であることに注意してください。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 列挙型の値の 1 つです。デフォルトでは、この値は 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>

カスタムビューを作成したら、それを描画する必要があります。EditText などの View サブクラスを拡張すると、そのサブクラスはビューの外観と属性を定義し、画面上に描画します。そのため、ビューを描画するコードを記述する必要はありません。代わりに、親のメソッドをオーバーライドしてビューをカスタマイズできます。

View を拡張して)ビューをゼロから作成する場合は、画面が更新されるたびにビュー全体を描画し、描画を処理する View メソッドをオーバーライドする必要があります。View を拡張するカスタムビューを適切に描画するには、次の操作を行う必要があります。

  • onSizeChanged() メソッドをオーバーライドして、ビューが最初に表示されたときと、ビューのサイズが変更されるたびに、ビューのサイズを計算します。
  • onDraw() メソッドをオーバーライドして、Paint オブジェクトでスタイル設定された Canvas オブジェクトを使用して、カスタムビューを描画します。
  • ビューの描画方法を変更するユーザーのクリックに応答するときに invalidate() メソッドを呼び出して、ビュー全体を無効にします。これにより、onDraw() の呼び出しが強制的に行われ、ビューが再描画されます。

onDraw() メソッドは、画面が更新されるたびに呼び出されます。これは 1 秒間に何度も発生する可能性があります。パフォーマンス上の理由と視覚的な不具合を避けるため、onDraw() 内で行う処理は最小限に抑える必要があります。特に、onDraw() 内での割り当ては避けてください。割り当てによってガベージ コレクションが発生し、視覚的なスタッタリングが引き起こされることがあります。

Canvas クラスと Paint クラスには、便利な描画ショートカットが多数用意されています。

  • drawText() を使用してテキストを描画します。setTypeface() を呼び出して書体を指定し、setColor() を呼び出してテキストの色を指定します。
  • drawRect()drawOval()drawArc() を使用してプリミティブな図形を描画します。setStyle() を呼び出して、図形を塗りつぶすのか、枠線を付けるのか、その両方を行うのかの設定を変更します。
  • drawBitmap() を使用してビットマップを描画します。

CanvasPaint については、後の Codelab で詳しく説明します。Android での View の描画方法について詳しくは、Android の View の描画方法をご覧ください。

このタスクでは、onSizeChanged() メソッドと onDraw() メソッドを使用して、ファン コントローラのカスタムビュー(ダイヤル自体、現在の位置インジケーター、インジケーター ラベル)を画面に描画します。また、ダイヤルのインジケーター ラベルの現在の X、Y 座標を計算するヘルパー メソッド computeXYForSpeed(), も作成します。

ステップ 1. 位置を計算してビューを描画する

  1. DialView クラスで、初期化の下に View クラスの onSizeChanged() メソッドをオーバーライドして、カスタムビューのダイヤルのサイズを計算します。kotlin をインポートします。リクエストされたら、math.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.sin をインポートします。PointF クラスのこの拡張関数は、現在の FanSpeed の位置とダイヤルの半径を指定して、テキストラベルと現在のインジケーター(0、1、2、3)の画面上の X 座標と Y 座標を計算します。これは 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. onDraw() メソッドをオーバーライドして、Canvas クラスと Paint クラスを使用して画面にビューをレンダリングします。リクエストされたら、android.graphics.Canvas をインポートします。スケルトン オーバーライドは次のとおりです。
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   
}
  1. onDraw() の内部に次の行を追加して、ファン速度が OFF かどうかによって、ペイントの色を灰色(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() メソッドでダイヤルの円を描画します。このメソッドは、現在のビューの幅と高さを使用して、円の中心、円の半径、現在のペイントの色を特定します。width プロパティと height プロパティは View スーパークラスのメンバーであり、ビューの現在の寸法を示します。
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
  1. 次のコードを追加して、drawCircle() メソッドでファン速度インジケーター マークの小さな円を描画します。この部分では PointF を使用します。computeXYforSpeed() 拡張メソッド。現在のファンの速度に基づいてインジケーターの中心の 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. アプリを実行します。アクティビティにファン制御ビューが表示されます。

最後のタスクは、ユーザーがビューをタップしたときにカスタムビューがアクションを実行できるようにすることです。タップするたびに、選択インジケーターがオフ、1、2、3、オフの順に移動します。また、選択が 1 以上の場合は、背景が灰色から緑色に変わり、ファンの電源がオンになっていることを示します。

カスタムビューをクリック可能にするには、次の操作を行います。

  • ビューの isClickable プロパティを true に設定します。これにより、カスタムビューがクリックに応答できるようになります。
  • View クラスの performClick() を実装して、ビューがクリックされたときにオペレーションを実行します。
  • invalidate() メソッドを呼び出します。これにより、Android システムに onDraw() メソッドを呼び出してビューを再描画するよう指示します。

通常、標準の Android ビューでは、ユーザーがビューをクリックしたときにアクションを実行するために OnClickListener() を実装します。カスタムビューの場合は、代わりに View クラスの performClick() メソッドを実装し、super を呼び出します。performClick(). デフォルトの performClick() メソッドは onClickListener() も呼び出すため、アクションを performClick() に追加して、onClickListener() を、カスタムビューを使用する可能性のある他のデベロッパーによるさらなるカスタマイズのために残しておくことができます。

  1. DialView.ktFanSpeed 列挙型内で、現在のファンの速度をリストの次の速度(OFF から LOWMEDIUMHIGH に変更し、その後 OFF に戻る)に変更する拡張関数 next() を追加します。完全な列挙型は次のようになります。
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() が呼び出されます。

次の 2 行では、next() メソッドでファンの速度を上げ、現在の速度(オフ、1、2、3)を表す文字列リソースにビューのコンテンツの説明を設定しています。

最後に、invalidate() メソッドはビュー全体を無効化し、onDraw() を呼び出してビューを再描画します。ユーザー操作など、なんらかの理由でカスタムビューの内容が変更され、その変更を表示する必要がある場合は、invalidate(). を呼び出します。

  1. アプリを実行します。DialView 要素をタップして、インジケーターをオフから 1 に移動します。ダイヤルが緑色に変わります。タップするたびに、インジケーターが次の位置に移動します。インジケーターがオフに戻ると、ダイヤルが再び灰色になります。

この例では、カスタムビューでカスタム属性を使用する基本的なメカニズムを示します。DialView クラスのカスタム属性を定義します。各ファン ダイヤルの位置に異なる色を指定します。

  1. res/values/attrs.xml を作成して開きます。
  2. <resources> 内に <declare-styleable> リソース要素を追加します。
  3. <declare-styleable> リソース要素内に、各属性に 1 つずつ、nameformat を含む 3 つの attr 要素を追加します。format は型のようなもので、この場合は 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 の属性を追加し、その値を次の色に設定します。カスタム属性は android 名前空間ではなく schemas.android.com/apk/res/your_app_package_name 名前空間に属しているため、android: ではなく app: をカスタム属性の接頭辞として使用します(app:fanColor1 など)。
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 では、TextViewButton などの標準 UI ビューに、デフォルトでいくつかのユーザー補助機能が用意されています。ただし、カスタムビューを作成する場合は、画面上のコンテンツの音声説明などのユーザー補助機能をカスタムビューでどのように提供するかを検討する必要があります。

このタスクでは、Android のスクリーン リーダーである TalkBack について学び、DialView カスタムビューの読み上げ可能なヒントと説明を含めるようにアプリを変更します。

ステップ 1. TalkBack を使ってみる

TalkBack は Android に組み込まれているスクリーン リーダーです。TalkBack を有効にすると、Android が画面要素を読み上げるため、ユーザーは画面を見ずに Android デバイスを操作できます。視覚障がいのあるユーザーは、アプリの使用に TalkBack を必要とする可能性があります。

このタスクでは、スクリーン リーダーの仕組みとアプリの操作方法を理解するために、TalkBack を有効にします。

  1. Android デバイスまたはエミュレータで、[設定] > [ユーザー補助] > [TalkBack] に移動します。
  2. [OFF] 切り替えボタンをタップして TalkBack をオンにします。
  3. [OK] をタップして権限を確認します。
  4. 求められた場合は、デバイスのパスワードを確認します。TalkBack を初めて実行する場合は、チュートリアルが開始されます。(古いデバイスではチュートリアルを利用できない場合があります)。
  5. 目を閉じてチュートリアルを操作すると、理解しやすくなります。チュートリアルをもう一度見るには、[設定] > [ユーザー補助] > [TalkBack] > [設定] > [TalkBack チュートリアルを起動] を選択します。
  6. CustomFanController アプリをコンパイルして実行するか、デバイスの [概要] ボタンまたは [最近] ボタンで開きます。TalkBack をオンにすると、アプリの名前とラベル TextView(「Fan Control」)のテキストが読み上げられます。ただし、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. performClick() メソッドで、invalidate() の直前に updateContentDescription() の呼び出しをもう一つ追加します。
override fun performClick(): Boolean {
   if (super.performClick()) return true
   fanSpeed = fanSpeed.next()
   updateContentDescription()
   invalidate()
   return true
}
  1. アプリをコンパイルして実行し、TalkBack がオンになっていることを確認します。タップしてダイヤルビューの設定を変更します。TalkBack が現在のラベル(オフ、1、2、3)と「ダブルタップして有効にします」というフレーズを読み上げるようになります。

ステップ 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() 関数をオーバーライドし、スーパークラスのメソッドを呼び出します。メッセージが表示されたら、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." です。これらの文字列リソースは後の手順で作成します。
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 未満の場合)または「ダブルタップしてリセット」(ファンの速度がすでに「高」または 3 の場合)に変わります。「ダブルタップして...」というプロンプトは、TalkBack サービス自体によって提供されます。

完成した Codelab のコードをダウンロードします。

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


または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。

ZIP をダウンロード

  • EditText などの View サブクラスのルック アンド フィールと動作を継承するカスタムビューを作成するには、そのサブクラスを拡張する新しいクラスを追加し、サブクラスのメソッドの一部をオーバーライドして調整します。
  • 任意のサイズと形状のカスタムビューを作成するには、View を拡張する新しいクラスを追加します。
  • onDraw() などの View メソッドをオーバーライドして、ビューの形状と基本的な外観を定義します。
  • invalidate() を使用して、ビューの描画または再描画を強制します。
  • パフォーマンスを最適化するには、onDraw() で使用する前に、描画とペイントに必要な変数を割り当て、値を代入します(メンバー変数の初期化など)。
  • カスタムビューのインタラクティブな動作を提供するには、OnClickListener() ではなく、performClick() をカスタムビューにオーバーライドします。これにより、カスタムビュー クラスを使用する可能性のあるあなたや他の Android デベロッパーが onClickListener() を使用してさらに動作を提供できるようになります。
  • 他の UI 要素と同様に、外観を定義する属性を使用して、カスタムビューを XML レイアウト ファイルに追加します。
  • values フォルダに attrs.xml ファイルを作成して、カスタム属性を定義します。これで、XML レイアウト ファイルでカスタムビューのカスタム属性を使用できるようになります。

Udacity コース:

Android デベロッパー ドキュメント:

動画:

このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。

  • 必要に応じて宿題を与える
  • 宿題の提出方法を生徒に伝える
  • 宿題を採点する

インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。

この Codelab に独力で取り組む場合は、これらの宿題を自由に使用して知識をテストしてください。

問題 1

カスタムビューに最初にサイズが割り当てられたときに位置、寸法、その他の値を計算するには、どのメソッドをオーバーライドしますか?

onMeasure()

onSizeChanged()

invalidate()

onDraw()

問題 2

属性値が変更された後、UI スレッドから呼び出すメソッドはどれですか?このメソッドは、ビューを onDraw() で再描画することを指定します。

▢ onMeasure()

▢ onSizeChanged()

▢ invalidate()

▢ getVisibility()

問題 3

カスタムビューにインタラクティブな機能を追加するには、どの View メソッドをオーバーライドする必要がありますか?

▢ setOnClickListener()

▢ onSizeChanged()

▢ isClickable()

▢ performClick()

このコースの他の Codelab へのリンクについては、Kotlin を使った高度な Android 開発の Codelab のランディング ページをご覧ください。