as

Settings
Sign out
Notifications
Alexa
Amazonアプリストア
AWS
ドキュメント
Support
Contact Us
My Cases
デバイス
ビルド
テスト
公開
関連情報
ドキュメント

Share:

Edit the Blog Post Header component above this, then place your content here, then fill out the Related Articles section below, if possible. You should have at least one related article and ideally, all three.

Feel free to add supplementary content to the sidebar at right, but please retain the Twitter component (it can live at the bottom of your added content). 

This text component can be edited or deleted as necessary.

Related Articles

Related article title here

Related articles only have an image (the squarer version of the banner) and a title. This text can be deleted. Place 1-3 related articles.

Subscribe

多様なユーザーに配慮した設計 2:FireデバイスにVoiceViewを実装する方法

Amazon Developer Nov 18, 2025
Share:
Fire tablet Fire TV How to
Blog_Header_Post_Img

前回の記事で、可能な限り多くのユーザーにアプリを利用してもらうための設計についてご説明しました。そのための主な方法の1つがVoiceViewです。

目の不自由なユーザーがVoiceViewを使用すると、デバイスに話しかけるだけでアプリの視覚的要素を操作できます。この機能は、Amazon Fireデバイスに組み込まれています。

ここでは、VoiceViewを実装し、可能な限りアプリのアクセシビリティを高めるためのコードサンプルをご紹介します。

Fire OSにおけるVoiceView固有の「extras」の使用方法

Fire OSのVoiceView固有の拡張機能を使用することで、Androidアクセシビリティフレームワークを拡張できます。これにより、スクリーンリーダーのユーザーに、さらに豊富なコンテキスト情報を提供できます。ここで重要なのが、次のようなAccessibilityNodeInfoextrasバンドルを使用してキーを追加するという手法です。

 

  • com.amazon.accessibility.describedBy:コンテナなど、画面上のテキストがない静的なコンテンツアイテムに関する説明を提供します。
  • com.amazon.accessibility.hint: 画面の操作方法に関する支援を必要とする可能性がある目が不自由なユーザーに、使い方に関するヒントを提供します。

 

例えば次のようになります。

Copied to clipboard
public void onInitializeAccessibilityNodeInfo(
  View host,
  AccessibilityNodeInfo info) {
  super.onInitializeAccessibilityNodeInfo(host, info);

  info
    .getExtras()
    .putString(
      "com.amazon.accessibility.describedBy",
      "この行には話題の映画が示されています。"
    );

  info
    .getExtras()
    .putString(
      "com.amazon.accessibility.usageHint.touch", // Fireタブレットの場合
      "タップして長押しするとオプションが表示されます。ダブルタップすると選択できます。"
    );
      
  info
    .getExtras()
    .putString(
      "com.amazon.accessibility.usageHint.remote", // Fire TVの場合
      "探すには左または右を押してください"
    )
});

ReactやJetpack Composeを使用する開発者の方々は、これらのフレームワークではAccessibilityNodeInfoの直接操作が抽象化されていることに注意が必要です。このような場合は、同等の機能を実現するため、各フレームワークが提供するガイドラインに従って実装してください。たとえば、Jetpack Composeの場合、コンポーネントにセマンティックプロパティを定義するという方法があります。これは、AccessibilityNodeInfoextrasを使用する方法と似ています。

カスタムビューとアクセシビリティAPI

カスタムUIコンポーネントを作成する場合、アクセシビリティを手動で実装する必要があります。たとえば、CircularSeekBarというカスタムコンポーネントがあるとします。これは円形の進捗状況セレクターで、ユーザーは円形のトラックの周囲にあるつまみをドラッグして、0%~100%の間で値を設定できます。

UI component process selector

この視覚的デザインは、目の見えるユーザーには直感的なものです。しかし、VoiceViewを利用している目の不自由なユーザーの場合、このコンポーネントの機能を理解して操作するための別の方法が必要になります。そこで、複数の主要なユーザー補助機能を実装します。

 

まず、キーボードやスクリーンリーダーで確実にこのビューに移動できるようにします。具体的には、クラス定義でisFocusable=trueを設定します。

Copied to clipboard
class CircularSeekBar @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private var progress = 0
    private val maxProgress = 100

    init {
        isFocusable = true
    }

次に、アクセシビリティサービスに対してこのビューを説明します。onInitializeAccessibilityNodeInfoをオーバーライドし、これが0~100の範囲のシークバーであり、ユーザーが前後にスクロールして値を調整できることをVoiceViewに伝えます。

 

Copied to clipboard
override fun onInitializeAccessibilityNodeInfo(
  info: AccessibilityNodeInfo
) {
  super.onInitializeAccessibilityNodeInfo(info)

  // 下位互換性を高めるために互換性のあるラッパーを使用する。
  val infoCompat = AccessibilityNodeInfoCompat.wrap(info)

  // アクセシビリティサービスに対して、ビューがSeekBarであることを伝える。
  infoCompat.className = "android.widget.SeekBar"
  infoCompat.roleDescription = "シークバー"
  infoCompat.contentDescription = "円形の進捗状況セレクター"

  // ビューの範囲(min、max、currentの値)について説明する。
  infoCompat.rangeInfo = 
    AccessibilityNodeInfoCompat.RangeInfoCompat.obtain(
      AccessibilityNodeInfoCompat.RangeInfoCompat.RANGE_TYPE_INT,
      0f,
      maxProgress.toFloat(),
      progress.toFloat()
    )

  // このビューで実行可能なアクションを宣言する。
  // これにより、ユーザーがスクリーンリーダーのジェスチャーで値を調整できるようになる。
  infoCompat.addAction(
    AccessibilityNodeInfoCompat
      .AccessibilityActionCompat
      .ACTION_SCROLL_FORWARD
  )
  infoCompat.addAction(
    AccessibilityNodeInfoCompat
      .AccessibilityActionCompat
      .ACTION_SCROLL_BACKWARD
  )
  infoCompat.isScrollable = true
}

performAccessibilityActionをオーバーライドして、VoiceViewのユーザーを対象とした実際の機能を提供します。たとえば、ユーザーが上下にスワイプして値を5ずつ変更し、即座に音声フィードバックを得られるようにします。

Copied to clipboard
override fun performAccessibilityAction(
  action: Int,
  arguments: Bundle?
): Boolean {
  val currentProgress = getProgress()

  // アクションに基づいて新しい進捗状況を判断する。
  val newProgress = when (action) {
    AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD -> {
      (currentProgress + 5).coerceAtMost(getMaxProgress())
    }
    AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD -> {
      (currentProgress - 5).coerceAtLeast(0)
    }
    else -> return super.performAccessibilityAction(action, arguments)
  }

  // 進捗状況が変わったら、更新して新しい値を音声で通知する。
  if (newProgress != currentProgress
    setProgress(newProgress
    val accessibilityManager =
      context.getSystemService(Context.ACCESSIBILITY_SERVICE)
      as AccessibilityManager
    if (accessibilityManager.isEnabled) {
      val event = AccessibilityEvent
        .obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
      event.text.add("Progress: $newProgress%")
      accessibilityManager.sendAccessibilityEvent(event)
    }
    return true
  }

  // アクションが処理されなかった場合はfalseを返す。
  return super.performAccessibilityAction(action, arguments)
}

D-padリモコンで操作するFire TVアプリにこの手法を適用する場合は、明示的にperformAccessibilityActionをトリガーしてD-Padのキーの押下を処理するコードを含めることをお勧めします。例えば次のようになります。

Copied to clipboard
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
  return when (keyCode) {
    KeyEvent.KEYCODE_DPAD_LEFT -> {
      performAccessibilityAction(
        AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD, null
      )
    }
    KeyEvent.KEYCODE_DPAD_RIGHT -> {
      performAccessibilityAction(
        AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD, null
      )
    }
    else -> super.onKeyDown(keyCode, event)
  }
}

アクセシビリティメニューに表示されるカスタムアクションを追加することもできます。MainActivityで、カスタムコンポーネントを含むコンテナにAccessibilityDelegateCompatを関連付けます。

Copied to clipboard
ViewCompat.setAccessibilityDelegate(
  seekBarContainer,
  object : AccessibilityDelegateCompat() {

    /**
     * このメソッドは、アクセシビリティサービスがビューのノード情報を 
     * 構築するときに呼び出される。それを使用してカスタムアクションを追加する。
     */
    override fun onInitializeAccessibilityNodeInfo(
      host: View,
      info: AccessibilityNodeInfoCompat
    ) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val actionLabel = "進捗状況のリセット"
      val customAction =
        AccessibilityNodeInfoCompat.AccessibilityActionCompat(
          R.id.action_reset_progress,  // res/values/ids.xmlで定義されたID
          actionLabel
        )
      info.addAction(customAction)
    }

    /**
     * このメソッドは、ユーザーがカスタムアクセシビリティアクションを
     * トリガーするときに呼び出される。
     */
    override fun performAccessibilityAction(
      host: View,
      action: Int,
      args: Bundle?
    ): Boolean {
      // 実行されたアクションが自分たちのカスタムアクションであるかどうかを確認する。
      if (action == R.id.action_reset_progress) {
        // アクションを実行:シークバーの進捗状況を0にリセットする。
        circularSeekBar.setProgress(0)
        val accessibilityManager =
          host.context.getSystemService(Context.ACCESSIBILITY_SERVICE)
            as AccessibilityManager
        if (accessibilityManager.isEnabled) {
            val event = AccessibilityEvent
              .obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
            event.text.add("Progress reset to 0%")
            accessibilityManager.sendAccessibilityEvent(event)
        }
        return true // trueを返し、アクションが処理されたことを示す。
}
      // 自分たちのアクションではない場合は、デフォルトの実装に処理させる。
      return super.performAccessibilityAction(host, action, args)
    }
  }
)

目の見えるユーザーがタッチで操作する場合も、最終の値を音声で通知してフィードバックを送ります。これは、カスタムコンポーネントのonTouchEvent定義内で処理できます。

 

Copied to clipboard
override fun onTouchEvent(event: MotionEvent): Boolean {
  when (event.action) {

    …

    MotionEvent.ACTION_UP -> {
      // ユーザーがタッチ操作で指を離したときに、
      // アクセシビリティのための音声通知を送る。
      // これにより、スクリーンリーダーのユーザーに、選択された最終の値を伝える。
      val accessibilityManager =
        context.getSystemService(Context.ACCESSIBILITY_SERVICE)
        as AccessibilityManager
      if (accessibilityManager.isEnabled) {
        val accessibilityEvent = AccessibilityEvent
          .obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
        accessibilityEvent.text.add("Progress set to $progress%")
        accessibilityManager.sendAccessibilityEvent(accessibilityEvent)
      }
      // アクセシビリティ基準を満たすためにperformClickを呼び出す。
      performClick()
      return true
    }
  }
  return super.onTouchEvent(event)
}

アクセシビリティナビゲーションとフォーカス移動順序

キーボード操作やスクリーンリーダーを利用するユーザーには、直感的なナビゲーションエクスペリエンスが不可欠です。ボタンが不規則に配置されたレイアウトを考えてみましょう。インターフェイスに機能的な意味を持たない装飾用の画像を追加する場合があります。たとえば、次のようなものです。

 

navigation experience

適切なフォーカス管理を行わない場合、VoiceViewのユーザーが複雑なナビゲーションパターンに直面したり、装飾的なコンテンツについて不要な音声通知を受け取ったりします。フォーカス制御とコンテンツのフィルタリングを適切に実装することで、ユーザーが理解しやすく、予測可能なナビゲーション操作を実現してください。

 

そのためには、レイアウトでandroid:importantForAccessibility="no"を使用します。これにより、ナビゲーション中に装飾的な画像をすべてスキップするようVoiceViewに指示できます。その結果、ユーザーがインターフェイスを理解するうえで不要な「画像」という音声通知を聞く必要がなくなります。レイアウトXMLで、次のように指定します。

Copied to clipboard
<ImageView
  android:layout_width="100dp"
  android:layout_height="100dp"
  android:src="@android:drawable/ic_menu_gallery"
  android:importantForAccessibility="no"
  android:layout_gravity="center_horizontal"
  android:layout_marginTop="16dp"
  android:layout_marginBottom="16dp"
  android:contentDescription="@null" />

こちらのレイアウトでは、要素がランダムに並んでいます。

non-linear arrangement

この場合、デフォルトの位置ベースのナビゲーションを、論理的な順序に基づいた動作で上書きしてください。この実装ではnextFocus*属性が鍵となります。

Copied to clipboard
<!-- Center Button -->
<Button
  android:id="@+id/button_center"
  android:text="中央"
  android:nextFocusForward="@id/button_right"
  android:nextFocusUp="@id/button_left"
  android:nextFocusDown="@id/button_right"
  android:nextFocusLeft="@id/button_left"
  android:nextFocusRight="@id/button_right" />

<!-- Right Button -->
<Button
  android:id="@+id/button_right"
  android:text="右"
  android:nextFocusForward="@id/button_left"
  android:nextFocusUp="@id/button_center"
  android:nextFocusDown="@id/button_left"
  android:nextFocusLeft="@id/button_center"
  android:nextFocusRight="@id/button_left" />

<!-- Left Button -->
<Button
  android:id="@+id/button_left"
  android:text="左"
  android:nextFocusForward="@id/button_center"
  android:nextFocusUp="@id/button_right"
  android:nextFocusDown="@id/button_center"
  android:nextFocusLeft="@id/button_right"
  android:nextFocusRight="@id/button_center" />

この実装により、ナビゲーションの順序が予測可能になります。特にFire TVでは、タッチ操作ではなく方向キーによる操作が主となるため、重要な要素となります。

テキスト以外の要素へのラベル付け

視覚的なデザインでは、効率的に意味を伝えるためにアイコンや画像を使用することが珍しくありません。しかし、目の見えるユーザーにとって明白なものが、スクリーンリーダーのユーザーにとっても同じであるとは限りません。VoiceViewがラベルの付いていないボタンや画像に直面すると、「ラベルのないボタン」または単に「画像」と音声で通知する可能性があります。 これはあまり有用な情報ではなく、ユーザーが混乱することになります。

 

また、インターフェイスに、目の見えるユーザーならほぼ例外なく見分けられるであろう歯車のアイコンのみを使用した「設定」ボタンがあるところを想像してみてください。適切にラベルが付けられていない場合、この要素にフォーカスすると、VoiceViewのユーザーは「ラベルのないボタン」という説明を聞くことになります。これでは、このボタンの機能を理解することは不可能です。このギャップを埋めるために、 情報や機能を伝えるテキスト以外の要素すべてに代替テキストを用意しましょう。

 

android:contentDescription属性で、明確かつ説明的なラベルを指定します。

Copied to clipboard
<ImageButton
  android:id="@+id/settings_button"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:src="@android:drawable/ic_menu_preferences"
  android:contentDescription="設定" />

コンテンツの説明は、視覚的デザインを介して情報を伝えるすべての要素に追加する必要があります。

 

  • ImageButton要素: アクションを実行するアイコン
  • ImageView要素: 情報を伝える画像(装飾用でないもの)
  • カスタムビュー: 視覚的デザインで意味を伝えるコンポーネント
  • 対話型グラフィック: ユーザーが操作できるグラフ、図、その他の視覚的要素

 

効果的な説明を記述するには、外観ではなく機能に焦点を当てることが重要です。「どのように見えるか」ではなく「何をするか」に対する回答となる説明である必要があります。

 

  • 非推奨: 「歯車アイコン」や「赤いボタン」
  • 推奨: 「設定」や「アイテムの削除」

コンテンツの動的なアップデートの処理

最近のアプリは、ステータスメッセージの表示、リストへの新規アイテムの追加、バックグラウンドでの処理の完了など、コンテンツの動的なアップデートを頻繁に行います。目の見えるユーザーはこうした変更を即座に把握できますが、スクリーンリーダーのユーザーの場合、明示的に音声で通知されなければ重要なアップデートを見逃す可能性があります。こうした音声通知がない場合、アプリで情報のギャップが生じ、一部のユーザーがアプリ内の状態を把握できず混乱し続けることになります。

 

ページ全体を更新しなくてもコンテンツが変更されるアプリがあります。たとえば、バックグラウンドでプロフィールのアップデートが完了するようなものです。その場合、AccessibilityEvent.TYPE_ANNOUNCEMENTを使用することが重要です。

Copied to clipboard
updateButton.setOnClickListener {

  // ここで、何らかのバックグラウンドアクション(データの保存など)が発生する。

  // AccessibilityEvent.TYPE_ANNOUNCEMENTを使用してユーザーに結果を伝える。
  // 画面上に表示されているテキストを変更する必要はない。

  val accessibilityManager =
    context.getSystemService(Context.ACCESSIBILITY_SERVICE)
    as AccessibilityManager
  if (accessibilityManager.isEnabled) {
    val event = AccessibilityEvent
      .obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
    event.text.add("Your profile has been updated.")
    accessibilityManager.sendAccessibilityEvent(event)
  }

 

新しいコンテンツが表示される、アプリのディスプレイ内の領域についてはどうすればよいでしょう。 このシナリオでは、コンテナをライブリージョンに指定して新しいコンテンツを自動的に音声で通知します。例えば次のようになります。

Copied to clipboard
<LinearLayout
  android:id="@+id/live_region_container"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:layout_marginTop="16dp"
  android:orientation="vertical"
  android:accessibilityLiveRegion="polite" />

このコンテナに新しいコンテンツが追加されると、VoiceViewが自動的に音声で通知します。

Copied to clipboard
addContentItemButton.setOnClickListener {
  itemCount++
  val newTextView = TextView(this)
  newTextView.text = "新しいアイテム#$itemCountが追加されました。"
  // このコンテナにはandroid:accessibilityLiveRegion="polite"が指定されているため、
  // 追加されるとすぐに、スクリーンリーダーがこの新しいTextViewの
  // テキストを自動的に読み上げる。
  container.addView(newTextView)
}

android:accessibilityLiveRegion属性は、さまざまな音声通知動作をサポートしています。

 

  • polite: VoiceViewによる現在の読み上げが終了するのを待ってから通知します。これは一般的なケースで推奨される方法です。
  • assertive: 現在の読み上げを中断して即座に通知します。重要なアラートの場合にのみ使用してください。
  • off: ライブリージョン動作を無効にします。

スクリーンリーダーに適したリストアイテムの設定

多くのアプリのインターフェイスにリストは欠かせません。曲のライブラリ、製品カタログ、連絡先リストなど、あらゆるものに使用されています。しかし、スクリーンリーダーのデフォルト動作では、こうしたリストの操作が非効率的です。VoiceViewがリストアイテム内のすべてのテキスト要素を個別のフォーカス可能要素として扱う場合、ユーザーは1つのアイテムを処理するまでに複数回スワイプする必要があります。これではすぐにストレスがたまってしまいます。

 

たとえば、曲のリストを使用する音楽アプリがあるとします。各リストアイテムには曲名とアーティスト名が含まれており、それぞれ個別のテキスト要素として表示されます。

Screenshot for list item

アクセシブルデザインではない場合、VoiceViewのユーザーは1曲当たり2回(曲名で1回、アーティスト名で1回)スワイプする必要があります。このようなナビゲーションには手間がかかりますし、ユーザーは複数のフォーカスポイント間で情報を記憶している必要があります。

 

関連情報をグループ化することで、より使いやすく分かりやすい操作性を実現できます。各リストアイテムのルートレイアウトをフォーカス可能に設定し、子要素をまとめてグループ化しましょう。

Copied to clipboard
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:padding="16dp"
  android:focusable="true"
  android:background="?android:attr/selectableItemBackground">

  <!--
    ルートレイアウトでandroid:focusable="true"を設定することで、
    このリストアイテム全体を単一の対話型要素として扱うように
    VoiceViewに指示する。これによって子TextViewがグループ化され 
    より整合性のあるスクリーンリーダーエクスペリエンスが実現する。contentDescriptionは
    プログラムでアダプターに設定される。
  -->

  <TextView
    android:id="@+id/song_title"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="曲名"
    android:textSize="18sp"
    android:textStyle="bold"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

  <TextView
    android:id="@+id/song_artist"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:text="アーティスト名"
    android:textSize="14sp"
    android:layout_marginTop="4dp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/song_title"
    app:layout_constraintEnd_toEndOf="parent"

</androidx.constraintlayout.widget.ConstraintLayout>

鍵となる属性「android:focusable="true"」により、リストアイテム全体を、個別の子要素としてではなく、単一のフォーカス可能なユニットとして扱うようVoiceViewに指示します。

 

その後、リストオブジェクトのRecyclerViewアダプターで、関連情報すべてを含む包括的なコンテンツの説明を作成します。

Copied to clipboard
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
  val song = songs[position]
  holder.titleTextView.text = song.title
  holder.artistTextView.text = song.artist

  // デフォルトでは、スクリーンリーダーは曲名とアーティスト名を
  // 2つのフォーカス可能な個別要素として扱う。これらを親ビュー(XMLでフォーカス可能と
  // マーク済み)の単一のcontentDescriptionにまとめることで、
  // ユーザーはアイテムの情報をまとめて聞くことが可能になる。
  holder.view.contentDescription = 
    "曲:${song.title}、アーティスト:${song.artist}"
}

このアプローチにより、2つの個別の音声通知(「Bohemian Rhapsody」の後に「Queen」)を1つの包括的な音声通知(「曲: Bohemian Rhapsody、アーティスト: Queen」)に変換することができます。

 

重要なのは、 contentDescriptionを作成するときは、明確かつ予測可能なパターンを使用することです。例えば次のようになります。

 

  • 音楽: "曲:[title]、アーティスト:[artist]"
  • 製品: "製品:[name]、価格:[price]"
  • 問い合わせ先: "担当者:[name]、電話:[number]"
  • ニュース: "記事:[headline]、日付:[date]"

 

このようなアプローチで開発を行うことで、リストナビゲーションの効率が向上し、使いやすくなります。スクリーンリーダーのユーザーも、ストレスを感じることなくコンテンツをすばやくブラウズできます。

VoiceViewを推奨する理由

アプリへのVoiceViewの実装は、可能な限り多くのユーザーに自分のアプリを利用してもらうための効果的な方法の1つであり、開発者とユーザーの両方にとって重要です。

 

その他のリソース

関連記事

ニュースレターを購読してみませんか?

最新のAmazon開発者向けニュース、業界の動向、ブログの記事をお届けします。