カスタムビューの作成

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

はじめに

Android には、ButtonTextViewEditTextImageViewCheckBoxRadioButton など、多数の View サブクラスがあります。これらのサブクラスを使用すると、ユーザー操作を実現し、アプリに情報を表示する UI を作成できます。どの View サブクラスもニーズに合わない場合は、カスタムビューと呼ばれる View サブクラスを作成できます。

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

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

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

前提となる知識

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

学習内容

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

演習内容

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

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

アプリに、物理的なファン制御に似た円形の UI 要素(オフ(0)、低(1)、中(2)、高(3)の設定)が表示される。ユーザーがビューをタップすると、選択インジケーターが次の位置(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. [Text] タブで 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. [Design] タブをクリックします。レイアウトは次のようになります。

手順 2. カスタムビュー クラスを作成する

  1. DialView という新しい Kotlin クラスを作成します。
  2. クラス定義を変更して View を拡張します。メッセージが表示されたら、android.view.View をインポートします。
  3. View をクリックしてから、赤い電球をクリックします。[Add Android View コンストラクタ s 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>

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

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

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

onDraw() メソッドは、画面が更新されるたびに呼び出されます。1 秒間に複数回呼び出される場合もあります。パフォーマンス上の理由と視覚的な不具合を避けるため、onDraw() で行える操作はできるだけ少なくしてください。特に、onDraw() に割り当てを設定しないでください。割り当てがあると、ガベージ コレクションによって視覚的な途切れが生じることがあります。

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

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

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

このタスクでは、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. Canvas クラスと Paint クラスを使用して、ビューを画面にレンダリングするように onDraw() メソッドをオーバーライドします。リクエストされたら、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. アプリを実行します。アクティビティにファン制御ビューが表示されます。

最後のタスクとして、ユーザーがビューをタップしたときに、カスタムビューでそのアクションを行えるようにします。タップするたびに、選択インジケーターが次の位置(off-1-2-3)に戻り、オフに戻ります。また、選択範囲が 1 以上なら、背景がグレーから緑色に変わり、ファンの電源がオンになったことを示します。

カスタムビューをクリック可能にする方法は次のとおりです。

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

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

  1. DialView.ktFanSpeed 列挙型内に拡張関数 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() を呼び出すことができます。

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

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

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

この例は、カスタムビューでカスタム属性を使用する基本的な仕組みを示しています。DialView クラスのカスタム属性を、ファンのダイヤル位置ごとに異なる色で定義します。

  1. res/values/attrs.xml を作成して開きます。
  2. <resources> 内に <declare-styleable> リソース要素を追加します。
  3. <declare-styleable> リソース要素内で、nameformat を使用して、属性ごとに 1 つずつ、計 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: ではなく app: をカスタム属性の接頭辞として使用します(カスタム属性は、android 名前空間ではなく schemas.android.com/apk/res/your_app_package_name 名前空間に属しているため)。
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 ビューにいくつかのユーザー補助機能がデフォルトで用意されています。ただし、カスタムビューを作成する際は、そのカスタムビューによって画面上のコンテンツの説明などのユーザー補助機能を提供する仕組みを考慮する必要があります。

このタスクでは、TalkBack(Android のスクリーン リーダー)について学び、DialView カスタムビュー用のヒントと説明を音声で確認できるようにアプリを変更します。

手順 1. TalkBack を使ってみる

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

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

  1. Android デバイスまたはエミュレータで、[設定] > [ユーザー補助] > [TalkBack] に移動します。
  2. [オン/オフ] の切り替えボタンをタップして、TalkBack をオンにします。
  3. [OK] をタップして権限を確定します。
  4. 必要に応じてデバイスのパスワードを確認します。TalkBack を初めて実行する場合は、チュートリアルが開始されます。(古いデバイスではチュートリアルがご利用いただけない場合があります)。
  5. 目を閉じてチュートリアルを見るとわかりやすくなります。チュートリアルをもう一度見るには、[設定] > [ユーザー補助] > [TalkBack] > [設定] > [TalkBack チュートリアルを起動] に移動します。
  6. CustomFanController アプリをコンパイルして実行するか、デバイス上の [Overview] または [Recents] ボタンを使用して開きます。TalkBack がオンになると、アプリの名前と、「TextView」(ラベル「ファンの制御」)のテキストがアナウンスされます。ただし、DialView ビューをタップしても、ビューの状態(ダイヤルの現在の設定)や、ビューをタップして有効にした操作についての情報は読み上げられません。

ステップ 2: ダイヤルラベルの内容説明を追加する

コンテンツの説明には、アプリのビューの意味や目的を伝えます。これらのラベルにより、Android の TalkBack 機能などのスクリーン リーダーで、各要素の機能を正確に説明できます。ImageView などの静的ビューの場合、contentDescription 属性を使用してレイアウト ファイルのビューにコンテンツの説明を追加できます。テキストビュー(TextViewEditText)では、コンテンツのテキストとしてビュー内のテキストが自動的に使用されます。

カスタムファン制御ビューの場合は、ビューがクリックされるたびにコンテンツの説明を動的に更新し、現在のファン設定を示す必要があります。

  1. DialView クラスの一番下で、引数または戻り値の型のない関数 updateContentDescription() を宣言します。
fun updateContentDescription() {
}
  1. updateContentDescription() 内で、カスタムビューの contentDescription プロパティを、現在のファン速度に関連付けられている文字列リソース(off、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 がオンになっていることを確認します。ダイヤルビューの設定をタップして変更すると、現在のラベル(オフ、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 オブジェクト内で、onInitializeAccessibilityNodeInfo() 関数を AccessibilityNodeInfoCompat オブジェクトでオーバーライドし、super のメソッドを呼び出します。メッセージが表示されたら、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 に、「Change」と「Reset」の文字列リソースを追加します。
<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

onDraw() を使ってビューを再描画することを示すには、属性値を変更した後、UI スレッドからどのメソッドを呼び出しますか。

▢ onMeasure()

▢ onSizeChanged()

▢ invalidate()

▢ getVisibility()

問題 3

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

▢ setOnClickListener()

▢ onSizeChanged()

▢ isClickable()

▢ performClick()

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