この Codelab は、Kotlin を使った高度な Android 開発コースの一部です。Codelab を順番に進めると、このコースを最大限に活用できますが、これは必須ではありません。コースの Codelab はすべて、Kotlin を使った高度な Android 開発の Codelab ランディング ページに記載されています。
はじめに
Android は、Button
、TextView
、EditText
、ImageView
、CheckBox
、RadioButton
など、多数の View
サブクラスを提供しています。これらのサブクラスを使用して、ユーザー操作を可能にし、アプリ内で情報を表示する UI を構築できます。View
サブクラスのいずれもニーズを満たさない場合は、カスタム ビューと呼ばれる View
サブクラスを作成できます。
カスタムビューを作成するには、既存の View
サブクラス(Button
や EditText
など)を拡張するか、View
の独自のサブクラスを作成します。View
を直接拡張することで、View
の onDraw()
メソッドをオーバーライドして描画することで、任意のサイズと形状のインタラクティブな UI 要素を作成できます。
カスタムビューを作成したら、TextView
や Button
を追加するのと同じ方法で、アクティビティ レイアウトに追加できます。
このレッスンでは、View
を拡張してカスタムビューを最初から作成する方法について説明します。
前提となる知識
- Activity を含むアプリを作成し、Android Studio を使用して実行する方法。
学習内容
View
を拡張してカスタムビューを作成する方法。- 円形のカスタムビューを描画する方法。
- リスナーを使用してカスタムビューに対するユーザー操作を処理する方法。
- レイアウトでカスタムビューを使用する方法。
演習内容
CustomFanController アプリは、View
クラスを拡張してカスタムビュー サブクラスを作成する方法を示しています。新しいサブクラスは DialView
と呼ばれます。
アプリには、オフ(0)、弱(1)、中(2)、強(3)の設定がある、物理的な扇風機のコントロールに似た円形の UI 要素が表示されます。ユーザーがビューをタップすると、選択インジケーターが次の位置(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
であることを確認します。 - [テキスト] タブで
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 要素で文字列リソースとディメンション リソースを抽出します。
- [デザイン] タブをクリックします。レイアウトは次のようになります。
ステップ 2. カスタムビュー クラスを作成する
DialView
という名前の新しい Kotlin クラスを作成します。View
を拡張するようにクラス定義を変更します。メッセージが表示されたら、android.view.View
をインポートします。- [
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) {
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
列挙型の値の 1 つです。デフォルトでは、この値は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()
メソッドをオーバーライドして、ビューが最初に表示されたときと、ビューのサイズが変更されるたびに、ビューのサイズを計算します。onDraw()
メソッドをオーバーライドして、Paint
オブジェクトでスタイル設定されたCanvas
オブジェクトを使用して、カスタムビューを描画します。- ビューの描画方法を変更するユーザーのクリックに応答するときに
invalidate()
メソッドを呼び出して、ビュー全体を無効にします。これにより、onDraw()
の呼び出しが強制的に行われ、ビューが再描画されます。
onDraw()
メソッドは、画面が更新されるたびに呼び出されます。これは 1 秒間に何度も発生する可能性があります。パフォーマンス上の理由と視覚的な不具合を避けるため、onDraw()
内で行う処理は最小限に抑える必要があります。特に、onDraw()
内での割り当ては避けてください。割り当てによってガベージ コレクションが発生し、視覚的なスタッタリングが引き起こされることがあります。
Canvas
クラスと Paint
クラスには、便利な描画ショートカットが多数用意されています。
drawText()
を使用してテキストを描画します。setTypeface()
を呼び出して書体を指定し、setColor()
を呼び出してテキストの色を指定します。drawRect()
、drawOval()
、drawArc()
を使用してプリミティブな図形を描画します。setStyle()
を呼び出して、図形を塗りつぶすのか、枠線を付けるのか、その両方を行うのかの設定を変更します。drawBitmap()
を使用してビットマップを描画します。
Canvas
と Paint
については、後の Codelab で詳しく説明します。Android での View の描画方法について詳しくは、Android の View の描画方法をご覧ください。
このタスクでは、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
}
onDraw()
メソッドをオーバーライドして、Canvas
クラスとPaint
クラスを使用して画面にビューをレンダリングします。リクエストされたら、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" />
- アプリを実行します。アクティビティにファン制御ビューが表示されます。
最後のタスクは、ユーザーがビューをタップしたときにカスタムビューがアクションを実行できるようにすることです。タップするたびに、選択インジケーターがオフ、1、2、3、オフの順に移動します。また、選択が 1 以上の場合は、背景が灰色から緑色に変わり、ファンの電源がオンになっていることを示します。
カスタムビューをクリック可能にするには、次の操作を行います。
- ビューの
isClickable
プロパティをtrue
に設定します。これにより、カスタムビューがクリックに応答できるようになります。 View
クラスのperformClick
()
を実装して、ビューがクリックされたときにオペレーションを実行します。invalidate()
メソッドを呼び出します。これにより、Android システムにonDraw()
メソッドを呼び出してビューを再描画するよう指示します。
通常、標準の Android ビューでは、ユーザーがビューをクリックしたときにアクションを実行するために OnClickListener()
を実装します。カスタムビューの場合は、代わりに View
クラスの performClick
()
メソッドを実装し、super
を呼び出します。performClick().
デフォルトの performClick()
メソッドは onClickListener()
も呼び出すため、アクションを performClick()
に追加して、onClickListener()
を、カスタムビューを使用する可能性のある他のデベロッパーによるさらなるカスタマイズのために残しておくことができます。
DialView.kt
のFanSpeed
列挙型内で、現在のファンの速度をリストの次の速度(OFF
からLOW
、MEDIUM
、HIGH
に変更し、その後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
}
}
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()
メソッドでファンの速度を上げ、現在の速度(オフ、1、2、3)を表す文字列リソースにビューのコンテンツの説明を設定しています。
最後に、invalidate()
メソッドはビュー全体を無効化し、onDraw()
を呼び出してビューを再描画します。ユーザー操作など、なんらかの理由でカスタムビューの内容が変更され、その変更を表示する必要がある場合は、invalidate().
を呼び出します。
- アプリを実行します。
DialView
要素をタップして、インジケーターをオフから 1 に移動します。ダイヤルが緑色に変わります。タップするたびに、インジケーターが次の位置に移動します。インジケーターがオフに戻ると、ダイヤルが再び灰色になります。
この例では、カスタムビューでカスタム属性を使用する基本的なメカニズムを示します。DialView
クラスのカスタム属性を定義します。各ファン ダイヤルの位置に異なる色を指定します。
res/values/attrs.xml
を作成して開きます。<resources>
内に<declare-styleable>
リソース要素を追加します。<declare-styleable>
リソース要素内に、各属性に 1 つずつ、name
とformat
を含む 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
名前空間ではなく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
で属性を取得し、属性値をキャッシュ保存用のローカル変数に割り当てます。
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 ビューに、デフォルトでいくつかのユーザー補助機能が用意されています。ただし、カスタムビューを作成する場合は、画面上のコンテンツの音声説明などのユーザー補助機能をカスタムビューでどのように提供するかを検討する必要があります。
このタスクでは、Android のスクリーン リーダーである TalkBack について学び、DialView
カスタムビューの読み上げ可能なヒントと説明を含めるようにアプリを変更します。
ステップ 1. TalkBack を使ってみる
TalkBack は Android に組み込まれているスクリーン リーダーです。TalkBack を有効にすると、Android が画面要素を読み上げるため、ユーザーは画面を見ずに Android デバイスを操作できます。視覚障がいのあるユーザーは、アプリの使用に TalkBack を必要とする可能性があります。
このタスクでは、スクリーン リーダーの仕組みとアプリの操作方法を理解するために、TalkBack を有効にします。
- Android デバイスまたはエミュレータで、[設定] > [ユーザー補助] > [TalkBack] に移動します。
- [OFF] 切り替えボタンをタップして TalkBack をオンにします。
- [OK] をタップして権限を確認します。
- 求められた場合は、デバイスのパスワードを確認します。TalkBack を初めて実行する場合は、チュートリアルが開始されます。(古いデバイスではチュートリアルを利用できない場合があります)。
- 目を閉じてチュートリアルを操作すると、理解しやすくなります。チュートリアルをもう一度見るには、[設定] > [ユーザー補助] > [TalkBack] > [設定] > [TalkBack チュートリアルを起動] を選択します。
CustomFanController
アプリをコンパイルして実行するか、デバイスの [概要] ボタンまたは [最近] ボタンで開きます。TalkBack をオンにすると、アプリの名前とラベルTextView
(「Fan Control」)のテキストが読み上げられます。ただし、DialView
ビュー自体をタップした場合は、ビューの状態(ダイヤルの現在の設定)や、ビューをタップして有効にしたときに実行されるアクションに関する情報は読み上げられません。
ステップ 2. ダイヤル ラベルのコンテンツの説明を追加する
コンテンツの説明は、アプリ内のビューの意味と目的を説明するものです。このラベルにより、Android の TalkBack 機能などのスクリーン リーダーが各要素の機能を正確に説明できるようになります。ImageView
などの静的ビューの場合は、contentDescription
属性を使用して、レイアウト ファイル内のビューにコンテンツの説明を追加できます。テキスト ビュー(TextView
と EditText
)は、ビュー内のテキストをコンテンツの説明として自動的に使用します。
カスタムのファン制御ビューでは、ビューがクリックされるたびにコンテンツの説明を動的に更新して、現在のファン設定を示す必要があります。
DialView
クラスの下部に、引数や戻り値の型がない関数updateContentDescription()
を宣言します。
fun updateContentDescription() {
}
updateContentDescription()
内で、カスタムビューのcontentDescription
プロパティを現在のファンの速度(オフ、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 がオンになっていることを確認します。タップしてダイヤルビューの設定を変更します。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
オブジェクト内で、AccessibilityNodeInfoCompat
オブジェクトを使用してonInitializeAccessibilityNodeInfo()
関数をオーバーライドし、スーパークラスのメソッドを呼び出します。メッセージが表示されたら、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
で、「変更」と「リセット」の文字列リソースを追加します。
<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 の View 描画方法
onMeasure()
onSizeChanged()
onDraw()
Canvas
Paint
drawText()
setTypeface()
setColor()
drawRect()
drawOval()
drawArc()
drawBitmap()
setStyle()
invalidate()
- 表示
- 入力イベント
- Paint
- Kotlin 拡張ライブラリ android-ktx
withStyledAttributes
- Android KTX ドキュメント
- Android KTX 最初の発表に関するブログ
- カスタムビューのユーザー補助機能を強化する
AccessibilityDelegateCompat
AccessibilityNodeInfoCompat
AccessibilityNodeInfoCompat.AccessibilityActionCompat
動画:
このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。
- 必要に応じて宿題を与える
- 宿題の提出方法を生徒に伝える
- 宿題を採点する
インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。
この Codelab に独力で取り組む場合は、これらの宿題を自由に使用して知識をテストしてください。
問題 1
カスタムビューに最初にサイズが割り当てられたときに位置、寸法、その他の値を計算するには、どのメソッドをオーバーライドしますか?
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ onDraw()
問題 2
属性値が変更された後、UI スレッドから呼び出すメソッドはどれですか?このメソッドは、ビューを onDraw()
で再描画することを指定します。
▢ onMeasure()
▢ onSizeChanged()
▢ invalidate()
▢ getVisibility()
問題 3
カスタムビューにインタラクティブな機能を追加するには、どの View
メソッドをオーバーライドする必要がありますか?
▢ setOnClickListener()
▢ onSizeChanged()
▢ isClickable()
▢ performClick()
このコースの他の Codelab へのリンクについては、Kotlin を使った高度な Android 開発の Codelab のランディング ページをご覧ください。