この Codelab は、Kotlin での高度な Android 開発コースの一部です。Codelab を順番に進めていくと、このコースを最大限に活用できますが、これは必須ではありません。すべてのコース Codelab は Kotlin での Codelab の高度な Codelab のランディング ページに掲載されています。
はじめに
Android には、Button
、TextView
、EditText
、ImageView
、CheckBox
、RadioButton
など、多数の View
サブクラスがあります。これらのサブクラスを使用すると、ユーザー操作を実現し、アプリに情報を表示する UI を作成できます。どの View
サブクラスもニーズに合わない場合は、カスタムビューと呼ばれる View
サブクラスを作成できます。
カスタムビューを作成するには、既存の View
サブクラス(Button
や EditText
など)を拡張するか、View
の独自のサブクラスを作成します。View
を直接拡張することで、任意のサイズと形状のインタラクティブな UI 要素を作成できます。これを行うには、View
の onDraw()
メソッドをオーバーライドして描画します。
カスタムビューを作成したら、TextView
や Button
を追加するのと同じ方法でアクティビティ レイアウトに追加できます。
このレッスンでは、View
を拡張して、カスタムビューをゼロから作成する方法を説明します。
前提となる知識
- アクティビティを含むアプリを作成し、Android Studio を使用して実行する方法。
学習内容
View
を拡張してカスタムビューを作成する方法- 円形のカスタムビューを描画する方法。
- リスナーを使用してカスタムビューでのユーザー操作を処理する方法。
- レイアウトでカスタムビューを使用する方法
演習内容
CustomFanController アプリは、View
クラスを拡張してカスタムビュー サブクラスを作成する方法を示します。新しいサブクラスは DialView
と呼ばれます。
アプリに、物理的なファン制御に似た円形の UI 要素(オフ(0)、低(1)、中(2)、高(3)の設定)が表示される。ユーザーがビューをタップすると、選択インジケーターが次の位置(0-1-2-3)に移動し、0 に戻ります。また、選択値が 1 以上の場合、ビューの円形部分の背景色がグレーから緑色に変わります(ファンの電源が入っていることを示します)。
ビューはアプリの UI の基本的な構成要素です。View
クラスには UI ウィジェットと呼ばれる多くのサブクラスがあり、一般的な Android アプリのユーザー インターフェースにおける多くのニーズに対応しています。
Button
や TextView
などの UI 構成要素は、View
クラスを拡張するサブクラスです。時間や開発の労力を節約するため、これらの View
サブクラスのいずれかを拡張できます。カスタムビューは親の外観と動作を継承し、変更する外観の動作や側面をオーバーライドすることができます。たとえば、EditText
を拡張してカスタムビューを作成する場合、そのビューは、EditText
ビューと同様に機能しますが、テキスト入力フィールドからテキストを消去する X ボタンなど、カスタマイズすることも可能です。
EditText
などの任意の View
サブクラスを拡張して、カスタムビュー(目的に応じて最も近いものを選択)を取得できます。カスタムビューは、1 つ以上のレイアウト内の他の View
サブクラスと同様に、属性を持つ XML 要素として使用できます。
独自のカスタムビューをゼロから作成するには、View
クラス自体を拡張します。コードは View
メソッドをオーバーライドして、ビューの外観と機能を定義します。独自のカスタムビューを作成する際は、任意のサイズと形状の UI 要素全体を画面に描画する必要があります。Button
などの既存のビューをサブクラス化した場合は、そのクラスが描画を処理します。(描画については、この Codelab で後ほど詳しく学習します)。
カスタムビューを作成する一般的な手順は次のとおりです。
View
を拡張するか、View
サブクラス(Button
やEditText
など)を拡張するカスタムビュー クラスを作成します。- 既存の
View
サブクラスを拡張する場合は、変更する外観や動作の側面のみをオーバーライドします。 View
クラスを拡張する場合は、新しいクラスのonDraw()
やonMeasure()
などのView
メソッドをオーバーライドすることで、カスタムビューの形状を描画して外観を制御します。- ユーザー操作に応答し、必要に応じてカスタムビューを再描画するコードを追加します。
- カスタムビュー クラスは、アクティビティの XML レイアウトの UI ウィジェットとして使用します。ビューのカスタム属性を定義して、さまざまなレイアウトでビューをカスタマイズすることもできます。
このタスクでは、次のことを行います。
- カスタムビューの一時的なプレースホルダとして
ImageView
でアプリを作成します。 View
を拡張してカスタムビューを作成します。- 描画値と描画値でカスタムビューを初期化します。
ステップ 1: ImageView プレースホルダを使用するアプリを作成する
- Empty Activity テンプレートを使用して、
CustomFanController
というタイトルの Kotlin アプリを作成します。パッケージ名がcom.example.android.customfancontroller
であることを確認します。 - [Text] タブで
activity_main.xml
を開き、XML コードを編集します。 - 既存の
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"/>
- この
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"/>
- 両方の UI 要素内の文字列リソースとディメンション リソースを抽出します。
- [Design] タブをクリックします。レイアウトは次のようになります。
手順 2. カスタムビュー クラスを作成する
DialView
という新しい Kotlin クラスを作成します。- クラス定義を変更して
View
を拡張します。メッセージが表示されたら、android.view.View
をインポートします。 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) {
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);
}
enum
の下に、次の定数を追加します。ダイヤル インジケーターやラベルの描画の一部として使用します。
private const val RADIUS_OFFSET_LABEL = 30
private const val RADIUS_OFFSET_INDICATOR = -35
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 ポイントになります。
実際の描画ステップができるだけ高速に実行されるように、これらの値は、ビューが実際に描画されるときではなく、ここで作成および初期化されます。
- また、
DialView
クラス定義内で、いくつかの基本的なスタイルでPaint
オブジェクトを初期化します。リクエストされたら、android.graphics.Paint
とandroid.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)
}
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()
を使用してビットマップを描画します。
Canvas
と Paint
については、後の Codelab で詳しく説明します。Android がビューを描画する方法の詳細については、Android がビューを描画する方法をご覧ください。
このタスクでは、onSizeChanged()
メソッドと onDraw()
メソッドを使用して、ファン コントローラのカスタムビュー(ダイヤル自体、現在の位置インジケーター、インジケーター ラベル)を画面に描画します。ダイヤル メソッドのインジケーター ラベルの現在の X,Y の位置を計算するヘルパー メソッド computeXYForSpeed(),
も作成します。
ステップ 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()
}
onSizeChanged()
の下に、次のコードを追加して、PointF
クラスのcomputeXYForSpeed()
拡張関数を定義します。リクエストされたら、kotlin.math.cos
とkotlin.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
}
Canvas
クラスとPaint
クラスを使用して、ビューを画面にレンダリングするようにonDraw()
メソッドをオーバーライドします。リクエストされたら、android.graphics.Canvas
をインポートします。スケルトンのオーバーライドは次のとおりです。
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
}
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
- 次のコードを追加して、
drawCircle()
メソッドを使用して、ダイヤルの円を描画します。このメソッドは、現在のビューの幅と高さを使用して、円の中心、円の半径、現在の描画色を検出します。width
プロパティとheight
プロパティはView
スーパークラスのメンバーであり、ビューの現在のディメンションを示します。
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
- 次のコードを追加して、ファンの速度インジケーターのマークを小さくし、
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)
- 最後に、ダイヤルの適切な位置にファン速度ラベル(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 要素の属性を使用して外観や動作を制御します。
activity_main.xml
で、dialView
のImageView
タグを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" />
- アプリを実行します。アクティビティにファン制御ビューが表示されます。
最後のタスクとして、ユーザーがビューをタップしたときに、カスタムビューでそのアクションを行えるようにします。タップするたびに、選択インジケーターが次の位置(off-1-2-3)に戻り、オフに戻ります。また、選択範囲が 1 以上なら、背景がグレーから緑色に変わり、ファンの電源がオンになったことを示します。
カスタムビューをクリック可能にする方法は次のとおりです。
- ビューの
isClickable
プロパティをtrue
に設定します。これにより、カスタムビューがクリックに応答できるようになります。 View
クラスのperformClick
()
を実装して、ビューがクリックされたときに操作を実行できるようにします。invalidate()
メソッドを呼び出します。これにより、Android システムはonDraw()
メソッドを呼び出してビューを再描画します。
通常は、標準の Android ビューで、ユーザーがそのビューをクリックしたときにアクションを実行する OnClickListener()
を実装します。カスタムビューの場合は、代わりに View
クラスの performClick
()
メソッドを実装し、super
を呼び出します。performClick().
デフォルトの performClick()
メソッドは onClickListener()
も呼び出します。そのため、アクションを performClick()
に追加し、カスタムビューを使用する他のデベロッパーが onClickListener()
をさらにカスタマイズできるようにすることができます。
DialView.kt
のFanSpeed
列挙型内に拡張関数next()
を追加します。これにより、現在のファン速度をリスト内の次の速度に変更します(OFF
からLOW
、MEDIUM
、HIGH
、次に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
}
}
DialView
クラスの、onSizeChanged()
メソッドの直前にinit()
ブロックを追加します。ビューのisClickable
プロパティを true に設定すると、そのビューはユーザー入力を受け入れるようになります。
init {
isClickable = true
}
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().
を呼び出します。
- アプリを実行して、
DialView
要素をタップしてインジケーターをオフから 1 にします。ダイヤルが緑色に変わります。タップするたびにインジケーターが次の位置に移動します。インジケーターが消えると、ダイヤルが再び灰色に変わります。
この例は、カスタムビューでカスタム属性を使用する基本的な仕組みを示しています。DialView
クラスのカスタム属性を、ファンのダイヤル位置ごとに異なる色で定義します。
res/values/attrs.xml
を作成して開きます。<resources>
内に<declare-styleable>
リソース要素を追加します。<declare-styleable>
リソース要素内で、name
とformat
を使用して、属性ごとに 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>
activity_main.xml
レイアウト ファイルを開きます。DialView
に、fanColor1
、fanColor2
、fanColor3
の属性を追加し、各値を下記の色に設定します。android:
ではなくapp:
をカスタム属性の接頭辞として使用します(カスタム属性は、android
名前空間ではなくschemas.android.com/apk/res/
your_app_package_name
名前空間に属しているため)。
app:fanColor1="#FFEB3B"
app:fanColor2="#CDDC39"
app:fanColor3="#009688"
DialView
クラスの属性を使用するには、属性を取得する必要があります。これらは AttributeSet
に保存され、作成時にクラスに渡されます(存在する場合)。init
で属性を取得し、属性値をキャッシュするためのローカル変数に割り当てます。
DialView.kt
クラスファイルを開きます。DialView
内で、属性値をキャッシュに保存する変数を宣言します。
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0
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)
}
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
- アプリを実行して、ダイヤルをクリックすると、以下のように、位置ごとに色の設定が異なる必要があります。
カスタムビュー属性の詳細については、ビュークラスの作成をご覧ください。
ユーザー補助とは、障がいのあるユーザーを含む、誰もがアプリを使用できるようにする設計、実装、テストの手法のことです。
人間による Android デバイスの使用に影響を与える可能性のある一般的な障害には、目の見えない状態、ロービジョン、色覚異常、聴覚障がい、難聴、運動機能の障がいなどがあります。ユーザー補助を念頭に置いてアプリを開発するには、こうした障がいのあるユーザーだけでなく、他のすべてのユーザーにとってもユーザー エクスペリエンスを向上させる必要があります。
Android では、TextView
や Button
など、標準の UI ビューにいくつかのユーザー補助機能がデフォルトで用意されています。ただし、カスタムビューを作成する際は、そのカスタムビューによって画面上のコンテンツの説明などのユーザー補助機能を提供する仕組みを考慮する必要があります。
このタスクでは、TalkBack(Android のスクリーン リーダー)について学び、DialView
カスタムビュー用のヒントと説明を音声で確認できるようにアプリを変更します。
手順 1. TalkBack を使ってみる
TalkBack は Android に組み込まれているスクリーン リーダーです。TalkBack を有効にすると、ユーザーは画面を見ることなく Android デバイスを操作できます。Android では画面要素が読み上げられるためです。視覚障がいのあるユーザーは、アプリの使用に TalkBack を必要とする可能性があります。
このタスクでは、TalkBack を有効にして、スクリーン リーダーの仕組みとアプリの操作方法を理解します。
- Android デバイスまたはエミュレータで、[設定] > [ユーザー補助] > [TalkBack] に移動します。
- [オン/オフ] の切り替えボタンをタップして、TalkBack をオンにします。
- [OK] をタップして権限を確定します。
- 必要に応じてデバイスのパスワードを確認します。TalkBack を初めて実行する場合は、チュートリアルが開始されます。(古いデバイスではチュートリアルがご利用いただけない場合があります)。
- 目を閉じてチュートリアルを見るとわかりやすくなります。チュートリアルをもう一度見るには、[設定] > [ユーザー補助] > [TalkBack] > [設定] > [TalkBack チュートリアルを起動] に移動します。
CustomFanController
アプリをコンパイルして実行するか、デバイス上の [Overview] または [Recents] ボタンを使用して開きます。TalkBack がオンになると、アプリの名前と、「TextView
」(ラベル「ファンの制御」)のテキストがアナウンスされます。ただし、DialView
ビューをタップしても、ビューの状態(ダイヤルの現在の設定)や、ビューをタップして有効にした操作についての情報は読み上げられません。
ステップ 2: ダイヤルラベルの内容説明を追加する
コンテンツの説明には、アプリのビューの意味や目的を伝えます。これらのラベルにより、Android の TalkBack 機能などのスクリーン リーダーで、各要素の機能を正確に説明できます。ImageView
などの静的ビューの場合、contentDescription
属性を使用してレイアウト ファイルのビューにコンテンツの説明を追加できます。テキストビュー(TextView
と EditText
)では、コンテンツのテキストとしてビュー内のテキストが自動的に使用されます。
カスタムファン制御ビューの場合は、ビューがクリックされるたびにコンテンツの説明を動的に更新し、現在のファン設定を示す必要があります。
DialView
クラスの一番下で、引数または戻り値の型のない関数updateContentDescription()
を宣言します。
fun updateContentDescription() {
}
updateContentDescription()
内で、カスタムビューのcontentDescription
プロパティを、現在のファン速度に関連付けられている文字列リソース(off、1、2、または 3)に変更します。これらは、画面にダイヤルを描画するときにonDraw()
で使用されるものと同じラベルです。
fun updateContentDescription() {
contentDescription = resources.getString(fanSpeed.label)
}
- 上方向にスクロールして
init()
ブロックを作成し、そのブロックの最後にupdateContentDescription()
の呼び出しを追加します。これにより、ビューが初期化されるときにコンテンツの説明が初期化されます。
init {
isClickable = true
// ...
updateContentDescription()
}
performClick()
メソッドのinvalidate()
の直前に、updateContentDescription()
に対する別の呼び出しを追加します。
override fun performClick(): Boolean {
if (super.performClick()) return true
fanSpeed = fanSpeed.next()
updateContentDescription()
invalidate()
return true
}
- アプリをコンパイルして実行し、TalkBack がオンになっていることを確認します。ダイヤルビューの設定をタップして変更すると、現在のラベル(オフ、1、2、3)とフレーズ「ダブルタップしてアクティブ化」がアナウンスされます。
ステップ 3: クリック アクションの詳細情報を追加する
停止すると TalkBack で使用できるようになります。ただし、ビューをアクティベートできることを示すには(ダブルタップしてアクティブにする)だけでなく、ビューがアクティブになったときに何が起こるか(例: ダブルタップで変更" ダブルタップでリセット)を説明できるようにすることも役立ちます。
これを行うには、ユーザー補助のデリゲートを介して、ビューの操作に関する情報(ここではクリックまたはタップのアクション)をユーザー補助ノード情報オブジェクトに追加します。ユーザー補助デリゲートを使用すると、継承ではなくコンポジションを通じて、アプリのユーザー補助機能に関連する機能をカスタマイズできます。
このタスクでは、Android Jetpack ライブラリのユーザー補助クラス(androidx.*
)を使用して、下位互換性を確保します。
DialView.kt
のinit
ブロックで、ビューのユーザー補助デリゲートを新しいAccessibilityDelegateCompat
オブジェクトとして設定します。リクエストされたら、androidx.core.view.ViewCompat
とandroidx.core.view.AccessibilityDelegateCompat
をインポートします。この方法により、アプリ内の下位互換性を最大限に高めることができます。
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
})
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 のユーザー補助サービスは、ビューに関する情報(読み上げ可能なコンテンツの説明や、そのビューに対して実行できるアクションなど)を見つけるためにそれらのノードをナビゲートします。カスタムビューを作成する際、ユーザー補助用のカスタム情報を提供するためにノード情報をオーバーライドすることが必要になる場合もあります。ノード情報をオーバーライドして、ビューのアクションのカスタム情報があることを示します。
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 が使用する文字列が必要です。
"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)
)
}
})
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)
}
})
res/values/strings.xml
に、「Change」と「Reset」の文字列リソースを追加します。
<string name="change">Change</string>
<string name="reset">Reset</string>
- アプリをコンパイルして実行し、TalkBack がオンになっていることを確認します。「ダブルタップして有効にする」というフレーズが、「ダブルタップして変更する」(ファンの速度が「3」以下の場合)または「ダブルタップしてリセットする」(ファンの速度がすでに高いか「3」の場合)のいずれかになったことに注目してください。「ダブルタップして...」というプロンプトは TalkBack サービス自体から提供されます。
完成した Codelab のコードをダウンロードします。
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-custom-views
リポジトリを ZIP ファイルとしてダウンロードして解凍し、Android Studio で開くこともできます。
EditText
などのView
サブクラスのデザインや動作を継承するカスタムビューを作成するには、そのサブクラスを拡張する新しいクラスを追加して、サブクラスの一部のメソッドをオーバーライドして調整を行います。- 任意のサイズと形状のカスタムビューを作成するには、
View
を拡張する新しいクラスを追加します。 onDraw()
などのView
メソッドをオーバーライドして、ビューの形状と基本的な外観を定義します。invalidate()
を使って、ビューを強制的に描画または再描画します。- パフォーマンスを最適化するには、変数を割り当て、描画や描画に必要な値を
onDraw()
で使用する前に指定します(メンバー変数の初期化などで必要)。 - カスタムビューに
OnClickListener
() ではなくperformClick()
をオーバーライドして、ビューのインタラクティブな動作を実現します。これにより、カスタムビュー クラスを使用する Android デベロッパーは、onClickListener()
を使用してさらなる動作を実現できます。 - カスタムビューは、他の UI 要素と同様に、XML レイアウト ファイルの外観を定義する属性を追加します。
values
フォルダにattrs.xml
ファイルを作成し、カスタム属性を定義します。その後、XML レイアウト ファイルでカスタムビューのカスタム属性を使用できます。
Udacity コース:
Android デベロッパー ドキュメント:
- カスタムビューの作成
@JvmOverloads
- カスタム コンポーネント
- Android のビューの描画方法
onMeasure()
onSizeChanged()
onDraw()
Canvas
Paint
drawText()
setTypeface()
setColor()
drawRect()
drawOval()
drawArc()
drawBitmap()
setStyle()
invalidate()
- ビュー
- 入力イベント
- ペイント
- Kotlin 拡張機能ライブラリ android-ktx
withStyledAttributes
- Android KTX のドキュメント
- Android KTX のオリジナル発表のブログ
- カスタムビューのユーザー補助機能を強化する
AccessibilityDelegateCompat
AccessibilityNodeInfoCompat
AccessibilityNodeInfoCompat.AccessibilityActionCompat
動画:
このセクションでは、インストラクターが主導するコースの一環として、この Codelab に取り組む生徒の課題について説明します。教師は以下のことを行えます。
- 必要に応じて課題を割り当てます。
- 宿題の提出方法を生徒に伝える。
- 宿題を採点します。
教師はこれらの提案を少しだけ使うことができます。また、他の課題は自由に割り当ててください。
この Codelab にご自分で取り組む場合は、これらの課題を使用して知識をテストしてください。
問題 1
カスタムビューに最初にサイズが割り当てられているときに位置やディメンションなどの値を計算する場合、どのメソッドをオーバーライドしますか。
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ onDraw()
問題 2
onDraw()
を使ってビューを再描画することを示すには、属性値を変更した後、UI スレッドからどのメソッドを呼び出しますか。
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ getVisibility()
問題 3
カスタムビューにインタラクティビティを追加するには、どの View
メソッドをオーバーライドする必要がありますか。
▢ setOnClickListener()
▢ onSizeChanged()
▢ isClickable()
▢ performClick()
このコースの他の Codelab へのリンクについては、Kotlin Codelab の高度な Codelab のランディング ページをご覧ください。