セマンティクス

コンポーザブルが保持する主要な情報(Text コンポーザブルのテキスト文字列など)とは別に、UI 要素に関する補足情報があると役立つ場合があります。

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

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

マテリアル、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 を表現し、ユーザーが操作できるようにします。Switch コンポーザブルの場合、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)
            }
        )
    }
}

状態の説明

コンポーザブルは、セマンティクスの stateDescription を定義できます。Android フレームワークは、これを使用してコンポーザブルの状態を読み取ります。たとえば、切り替え可能なコンポーザブルは、「オン」または「オフ」の状態になります。場合によっては、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. 画面上のスイッチ アクセス ナビゲーションの可視化。

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

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

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

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

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

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

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

セマンティクス プロパティがマージされ、設定される
図 12. セマンティクス プロパティがマージされ、設定されます。

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

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

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

ツリーを調整する

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