セマンティクス

コンポーザブルが持つ主な情報(Text コンポーザブルのテキスト文字列など)以外にも、UI 要素に関する補足情報を用意すると便利です。

Compose のコンポーネントの意味と役割に関する情報は、セマンティクスと呼ばれます。これは、ユーザー補助、自動入力、テストなどのサービスにコンポーザブルに関する追加のコンテキストを提供する方法です。たとえば、カメラ アイコンは視覚的には単なる画像ですが、セマンティックな意味は「写真を撮る」かもしれません。

適切なセマンティクスと適切な Compose API を組み合わせることで、コンポーネントに関する情報をユーザー補助サービスにできるだけ多く提供できます。ユーザー補助サービスは、その情報をユーザーにどのように表示するかを決定します。

Material と Compose の UI と Foundation の API には、特定のロールと機能に従うセマンティクスが組み込まれていますが、特定の要件に応じて、既存の API のセマンティクスを変更したり、カスタム コンポーネントに新しいセマンティクスを設定したりすることもできます。

セマンティック プロパティ

セマンティック プロパティは、対応するコンポーザブルの意味を伝えます。たとえば、Text コンポーザブルにはセマンティック プロパティ text が含まれています。それが、このコンポーザブルの意味だからです。Icon には、アイコンの意味をテキストで伝える contentDescription プロパティが含まれます(デベロッパーが設定した場合)。

セマンティクス プロパティがコンポーザブルの意味を伝える方法について考えてみましょう。Switch について考えてみましょう。これは次のように表示されます。

図 1. 「オン」状態と「オフ」状態の Switch

この要素の意味は、「これはスイッチです。切り替え可能な要素であり、現在オンの状態です。クリックすると、操作できます。」と説明できます。

これこそがセマンティクス プロパティが使用される目的です。このスイッチ要素のセマンティクス ノードには、Layout Inspector に表示されている、以下のプロパティが含まれます。

スイッチ コンポーザブルのセマンティクス プロパティを表示している Layout Inspector
図 2. Switch コンポーザブルのセマンティクス プロパティを表示している Layout Inspector。

Role は要素のタイプを示します。StateDescription は、「オン」状態をどのように表現すればよいかを表します。デフォルトでは「On」という単語をローカライズしたものですが、コンテキストに基づいて、もっと具体的な値(「有効」など)にもできます。ToggleableState は、スイッチの現在の状態です。OnClick プロパティは、この要素の操作に使用されるメソッドを表します。

アプリで各コンポーザブルのセマンティクス プロパティを追跡することで、アプリの可能性が大きく広がります。

  • ユーザー補助サービスは、このプロパティを使用して、画面に表示される UI を表し、ユーザーが操作できるようにします。スイッチ コンポーザブルの場合、TalkBack は「オン、スイッチ、ダブルタップして切り替え」と読み上げます。ユーザーは画面をダブルタップして、スイッチをオフに切り替えられます。
  • テスト フレームワークは、このプロパティを使用してノードを検出し、それを操作して、アサーションを行います。
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

Compose foundation ライブラリの上に構築されたコンポーザブルと修飾子には、デフォルトで関連するプロパティがすでに設定されています。必要に応じて、これらのプロパティを手動で変更して、特定のユースケースでのユーザー補助のサポートを改善したり、コンポーザブルの統合または消去戦略を変更したりできます。

コンポーネントの特定のコンテンツ タイプをユーザー補助サービスに通知するには、さまざまなセマンティクスを適用できます。これらの追加により、主要なセマンティック情報がサポートされ、ユーザー補助サービスがコンポーネントの表示、読み上げ、操作方法を微調整できるようになります。

セマンティクス プロパティの一覧については、SemanticsProperties オブジェクトをご覧ください。利用可能なユーザー補助アクションの一覧については、SemanticsActions オブジェクトをご覧ください。

見出し

アプリには、長い記事やニュース ページなど、テキスト中心のコンテンツを含む画面がよくあります。通常、これらの画面は、ヘッダーを使用してさまざまなサブセクションに分かれています。

スクロール可能なコンテナに記事テキストを表示したブログ投稿。
図 3. スクロール可能なコンテナで記事テキストを表示したブログ投稿。

ユーザー補助を必要とするユーザーにとって、そのような画面を簡単に操作するのは困難です。ナビゲーション エクスペリエンスを向上させるため、一部のユーザー補助サービスでは、セクションや見出し間を直接移動できます。これを有効にするには、セマンティクス プロパティを定義して、コンポーネントが heading であることを示します。

@Composable
private fun Subsection(text: String) {
    Text(
        text = text,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier.semantics { heading() }
    )
}

アラートとポップアップ

コンポーネントがアラートやポップアップ(Snackbar など)の場合は、新しい構造やコンテンツの更新をユーザーに伝えることができることをユーザー補助サービスに通知できます。

アラートのようなコンポーネントは、liveRegion セマンティクス プロパティでマークできます。これにより、ユーザー補助サービスはこのコンポーネントまたはその子コンポーネントの変更をユーザーに自動的に通知できます。

PopupAlert(
    message = "You have a new message",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Polite
    }
)

画面上のアラートや重要な変更コンテンツにユーザーの注意を短時間だけ引き付ける必要がある場合は、ほとんどの場合 liveRegionMode.Polite を使用する必要があります。

フィードバックの妨げにならないように、liveRegion.Assertive は控えめに使用してください。時間に敏感なコンテンツをユーザーに知らせることが重要である場合に使用する必要があります。

PopupAlert(
    message = "Emergency alert incoming",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Assertive
    }
)

カウントダウン タイマーなど、頻繁に更新されるコンテンツにはライブリージョンを使用しないでください。フィードバックを頻繁に表示してユーザーに負担をかけないようにしてください。

ウィンドウのようなコンポーネント

ModalBottomSheet と同様に、ウィンドウのようなカスタム コンポーネントは、周囲のコンテンツと区別するために追加のシグナルが必要です。これには、paneTitle セマンティクスを使用できます。これにより、関連するウィンドウやペインの変更が、その主なセマンティック情報とともに、ユーザー補助サービスによって適切に表現されるようになります。

ShareSheet(
    message = "Choose how to share this photo",
    modifier = Modifier
        .fillMaxWidth()
        .align(Alignment.TopCenter)
        .semantics { paneTitle = "New bottom sheet" }
)

なお、コンポーネントで マテリアル 3 が paneTitle を使用する方法をご覧ください。

エラー コンポーネント

エラーのようなコンポーネントなど、他のコンテンツ タイプの場合は、ユーザーのアクセシビリティ要件に応じて、メインのセマンティック情報を拡張することをおすすめします。エラー状態を定義するときに、その error セマンティクスをユーザー補助サービスに通知し、拡張されたエラー メッセージを提供できます。

この例では、TalkBack はメインのエラー テキスト情報を読み上げ、その後に追加の拡張メッセージが読み上げられます。

Error(
    errorText = "Fields cannot be empty",
    modifier = Modifier
        .semantics {
            error("Please add both email and password")
        }
)

進捗状況のトラッキング コンポーネント

進行状況を追跡するカスタム コンポーネントの場合は、現在の進行状況の値、範囲、ステップサイズなど、進行状況の変化をユーザーに通知することをおすすめします。これは progressBarRangeInfo セマンティクスで行うことができます。これにより、ユーザー補助サービスは進行状況の変化を認識し、それに応じてユーザーを更新できます。支援技術によって、進行状況の増減を示す方法が異なる場合もあります。

ProgressInfoBar(
    modifier = Modifier
        .semantics {
            progressBarRangeInfo =
                ProgressBarRangeInfo(
                    current = progress,
                    range = 0F..1F
                )
        }
)

リストとアイテムの情報

アイテムの多いカスタム リストやグリッドでは、アイテムの合計数やインデックスなどの詳細な情報も、ユーザー補助サービスに提供すると便利です。

リストとアイテムにそれぞれ collectionInfo セマンティクスと collectionItemInfo セマンティクスを使用すると、この長いリストで、ユーザーはテキスト セマンティクス情報に加えて、コレクション全体の中で現在どのアイテムのインデックスにいるのかをユーザーに知らせることができます。

MilkyWayList(
    modifier = Modifier
        .semantics {
            collectionInfo = CollectionInfo(
                rowCount = milkyWay.count(),
                columnCount = 1
            )
        }
) {
    milkyWay.forEachIndexed { index, text ->
        Text(
            text = text,
            modifier = Modifier.semantics {
                collectionItemInfo =
                    CollectionItemInfo(index, 0, 0, 0)
            }
        )
    }
}

状態の説明

コンポーザブルでは、Android フレームワークによって使用されるセマンティクスの stateDescription を定義して、コンポーザブルの状態を読み上げることができます。たとえば、切り替え可能なコンポーザブルの状態は、「オン」または「オフ」のいずれかです。Compose が使用するデフォルトの状態説明ラベルをオーバーライドしたい場合もあります。これを行うには、コンポーザブルを切り替え可能として定義する前に、状態説明ラベルを明示的に指定します。

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
    val stateSubscribed = stringResource(R.string.subscribed)
    val stateNotSubscribed = stringResource(R.string.not_subscribed)
    Row(
        modifier = Modifier
            .semantics {
                // Set any explicit semantic properties
                stateDescription = if (selected) stateSubscribed else stateNotSubscribed
            }
            .toggleable(
                value = selected,
                onValueChange = { onToggle() }
            )
    ) {
        /* ... */
    }
}

カスタム アクション

カスタム アクションは、スワイプして閉じる、ドラッグ&ドロップなど、より複雑なタッチスクリーン操作に使用できます。これらの操作は、運動障害やその他の障害のあるユーザーにとって操作が難しい場合があります。

スワイプして閉じるジェスチャーをより簡単に利用できるようにするには、カスタム アクションにリンクし、閉じるアクションとラベルを渡します。

SwipeToDismissBox(
    modifier = Modifier.semantics {
        // Represents the swipe to dismiss for accessibility
        customActions = listOf(
            CustomAccessibilityAction(
                label = "Remove article from list",
                action = {
                    removeArticle()
                    true
                }
            )
        )
    },
    state = rememberSwipeToDismissBoxState(),
    backgroundContent = {}
) {
    ArticleListItem()
}

TalkBack などのユーザー補助サービスは、コンポーネントをハイライト表示し、メニューで利用可能な他のアクションがあることをヒントとして示します。このヒントは、スワイプして閉じるアクションを表します。

TalkBack の操作メニューの可視化
図 4. TalkBack アクション メニューの可視化。

カスタム アクションのもう 1 つのユースケースは、利用可能なアクションが複数あるアイテムを含む長いリストです。ユーザーがアイテムごとに各アクションを個別に反復するのは面倒な場合があります。

=スイッチ アクセスのナビゲーションを画面上に可視化
図 5. 画面上のスイッチ アクセスのナビゲーションの可視化。

ナビゲーション エクスペリエンスを改善するには、コンテナのカスタム アクションを使用して、個々の移動からアクションを移動し、別のアクション メニューに移動します。これは、スイッチ アクセスや音声アクセスなどのインタラクション ベースの支援技術に特に役立ちます。

ArticleListItemRow(
    modifier = Modifier
        .semantics {
            customActions = listOf(
                CustomAccessibilityAction(
                    label = "Open article",
                    action = {
                        openArticle()
                        true
                    }
                ),
                CustomAccessibilityAction(
                    label = "Add to bookmarks",
                    action = {
                        addToBookmarks()
                        true
                    }
                ),
            )
        }
) {
    Article(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = openArticle,
    )
    BookmarkButton(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = addToBookmarks,
    )
}

このような場合は、カスタム アクションに移動するため、元の子のセマンティクスを clearAndSetSemantics 修飾子で手動で消去してください。

スイッチ アクセスを例に取ると、コンテナを選択するとメニューが開き、使用可能なネストされたアクションが一覧表示されます。

記事リストアイテムのスイッチ アクセスのハイライト
図 6. 記事リストアイテムのスイッチ アクセスのハイライト
スイッチ アクセスの操作メニューのビジュアリゼーション。
図 7. スイッチ アクセスの操作メニューの可視化。

セマンティクス ツリー

コンポジションはアプリの UI の記述であり、コンポーザブルの実行により生成されます。コンポジションは、UI を記述するコンポーザブルで構成されるツリー構造です。

Composition の隣には、セマンティクス ツリーという並列ツリーがあります。このツリーは、ユーザー補助サービスとテスト フレームワークから認識しやすい別の方法で UI を記述します。ユーザー補助サービスは、このツリーを使用して、特定のニーズを持つユーザーにアプリを説明します。テスト フレームワークは、このツリーを使用してアプリを操作し、アサーションを行います。セマンティクス ツリーには、コンポーザブルを描画する方法に関する情報はありませんが、コンポーザブルの意味論的意味に関する情報が含まれています。

典型的な UI 階層とそのセマンティクス ツリー
図 8. 典型的な UI 階層とそのセマンティクス ツリー。

アプリが Compose foundation とマテリアル ライブラリのコンポーザブルと修飾子で構成されている場合は、セマンティクス ツリーが自動的に入力、生成されます。ただし、カスタムの低レベルのコンポーザブルを追加する場合は、手動でそのセマンティクスを提供する必要があります。また、ツリーが画面上の要素の意味を正しく表現していないか、完全に表現していない場合もあります。その場合は、ツリーを調整できます。

たとえば、このカスタム カレンダーのコンポーザブルを考えてみましょう。

日にちの要素を選択可能なカスタム カレンダーのコンポーザブル
図 9. 日にちの要素を選択可能なカスタム カレンダーのコンポーザブル。

この例では、カレンダー全体が 1 つの下位レベルのコンポーザブルとして実装され、Layout コンポーザブルを使用して Canvas に直接描画しています。何もしないでおくと、コンポーザブルのコンテンツと、カレンダー内でのユーザーの選択に関する情報が、ユーザー補助サービスに十分に提供されません。たとえば、ユーザーが 17 の日にちをクリックしても、ユーザー補助機能のフレームワークが受け取るのは、カレンダー全体のコントロールを説明する情報だけです。この場合、TalkBack ユーザー補助サービスは単に「カレンダー」または「4 月のカレンダー」と通知するだけで、どの日にちが選択されたかユーザーにはわかりません。このコンポーザブルのユーザー補助を充実させるには、セマンティック情報を手動で追加する必要があります。

マージされたツリーとマージされていないツリー

前述のように、UI ツリーの各コンポーザブルには、0 個以上のセマンティクス プロパティが設定されます。コンポーザブルにセマンティック プロパティが設定されていない場合は、セマンティクス ツリーの一部としては含まれません。このように、セマンティクス ツリーには実際に意味論的意味を含んでいるノードのみが含まれます。ただし、画面に表示される内容の正しい意味を伝えるには、ノードの特定のサブツリーをマージして 1 つのノードとして扱うと便利な場合があります。こうすると、子孫ノードを個別に扱うのではなく、ノードの集合全体として説明できます。おおまかには、このツリーの各ノードが、ユーザー補助サービスを使用する場合のフォーカス可能な要素を表しています。

このようなコンポーザブルの例として、Button があります。ボタンは、次のように複数の子ノードを含む場合がありますが、その場合でも 1 つの要素として説明できます。

Button(onClick = { /*TODO*/ }) {
    Icon(
        imageVector = Icons.Filled.Favorite,
        contentDescription = null
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

セマンティクス ツリーでは、ボタンの子孫のプロパティはマージされ、ボタンはツリー内で単一のリーフノードとして表されます。

マージされた単一リーフのセマンティクス表現
図 10. マージされた単一リーフ セマンティクス表現。

コンポーザブルと修飾子は、Modifier.semantics (mergeDescendants = true) {} を呼び出すことで、子孫のセマンティクス プロパティをマージしたいことを示すことができます。このプロパティを true に設定することで、セマンティクス プロパティのマージが必要であることが示されます。Button の例では、Button コンポーザブルは、この semantics 修飾子を含む clickable 修飾子を内部で使用します。そのため、ボタンの子孫ノードがマージされます。コンポーザブルでマージ動作を変更する必要がある場合の詳細については、ユーザー補助のドキュメントをご覧ください。

Foundation ライブラリと Material Compose ライブラリの修飾子とコンポーザブルには、このプロパティ セットが用意されています。たとえば clickable 修飾子と toggleable 修飾子を使用すると、その子孫が自動的にマージされます。また、ListItem コンポーザブルも、その子孫をマージします。

ツリーを検査する

セマンティクス ツリーは実際には 2 つの異なるツリーです。mergeDescendantstrue に設定されている場合に子孫ノードをマージする、マージされたセマンティクス ツリーと、マージを適用せず、すべてのノードはそのまましておく、マージされていないセマンティクス ツリーです。ユーザー補助サービスでは、マージされていないツリーを使用し、mergeDescendants プロパティを考慮しながら独自のマージ アルゴリズムを適用します。テスト フレームワークでは、デフォルトでマージされたツリーを使用します。

両方のツリーを printToLog() メソッドで調べることができます。デフォルトでは、前の例と同様に、マージされたツリーがログに出力されます。マージされていないツリーを出力するには、次のように onRoot() マッチャーの useUnmergedTree パラメータを true に設定します。

composeTestRule.onRoot(useUnmergedTree = true).printToLog("MY TAG")

Layout Inspector では、ビューフィルタで選択することで、マージされたセマンティクス ツリーとマージされていないセマンティクス ツリーの両方を表示できます。

マージされたセマンティクス ツリーとマージされていないセマンティクス ツリーの両方を表示できる Layout Inspector の表示オプション
図 11. マージされたセマンティクス ツリーとマージされていないセマンティクス ツリーの両方を表示できる Layout Inspector の表示オプション。

Layout Inspector では、ツリー内のノードごとに、マージされたセマンティクスとそのノードに設定されているセマンティクスの両方が、プロパティ パネルに表示されます。

セマンティクス プロパティが統合され、設定されている
図 12. セマンティクス プロパティが統合され、設定されている。

テスト フレームワークのマッチャーでは、デフォルトで、マージされたセマンティクス ツリーが使用されます。 そのため、Button 内に表示されているテキストを照合することで、Button を操作できます。

composeTestRule.onNodeWithText("Like").performClick()

この動作は、onRoot マッチャーで行ったように、マッチャーの useUnmergedTree パラメータを true に設定することでオーバーライドできます。

ツリーを適応させる

前述のように、特定のセマンティクス プロパティをオーバーライドまたはクリアしたり、ツリーのマージ動作を変更したりできます。これは、独自のカスタム コンポーネントを作成する場合に特に重要です。適切なプロパティとマージ動作を設定しないと、アプリがユーザー補助対応でなくなり、テストも想定どおりに動作しなくなる場合があります。テストの詳細については、テストガイドをご覧ください。