Compose のセマンティクス

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

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

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

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

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

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

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

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

意味論的意味を持つ UI ツリーのすべてのノードには、セマンティクス ツリーの並列ノードがあります。セマンティクス ツリーのノードには、対応するコンポーザブルの意味を伝えるプロパティが含まれています。たとえば、Text コンポーザブルにはセマンティック プロパティ text が含まれています。それが、このコンポーザブルの意味だからです。Icon には、Icon の意味をテキストで伝える contentDescription プロパティが含まれます(デベロッパーが設定した場合)。Compose foundation ライブラリの上に構築されたコンポーザブルと修飾子には、適切なプロパティがすでに設定されています。必要に応じて、semantics 修飾子と clearAndSetSemantics 修飾子を使ってプロパティを設定またはオーバーライドできます。たとえば、ノードにカスタムのユーザー補助アクションを追加したり、切り替え可能な要素に代替の状態説明を設定したり、特定のテキスト コンポーザブルが見出しとみなされる必要があることを示したりできます。

セマンティクス ツリーを視覚化するには、Layout Inspector ツールを使用するか、テスト内で printToLog() メソッドを使用します。これにより、Logcat 内に現在のセマンティクス ツリーが出力されます。

class MyComposeTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun MyTest() {
        // Start the app
        composeTestRule.setContent {
            MyTheme {
                Text("Hello world!")
            }
        }
        // Log the full semantics tree
        composeTestRule.onRoot().printToLog("MY TAG")
    }
}

このテストの出力は次のようになります。

    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=63.0, r=221.0, b=120.0)px
     |-Node #2 at (l=0.0, t=63.0, r=221.0, b=120.0)px
       Text = '[Hello world!]'
       Actions = [GetTextLayoutResult]

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

図 3. 「オン」状態のスイッチと「オフ」状態のスイッチ。

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

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

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

Role は、調べている要素のタイプを示します。StateDescription は、「オン」状態をどのように表現すればよいかを表します。デフォルトでは「On」という単語をローカライズしたものですが、コンテキストに基づいて、もっと具体的な値(「有効」など)にもできます。ToggleableState は、スイッチの現在の状態です。OnClick プロパティは、この要素の操作に使用されるメソッドを表します。セマンティクス プロパティの一覧については、SemanticsProperties オブジェクトをご覧ください。利用可能なユーザー補助アクションの一覧については、SemanticsActions オブジェクトをご覧ください。

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

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

マージされたセマンティクス ツリーとマージされていないセマンティクス ツリー

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

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

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

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

コンポーザブルと修飾子は、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 では、ビューフィルタで選択することで、マージされたセマンティクス ツリーとマージされていないセマンティクス ツリーの両方を表示できます。

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

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

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

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

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

マージの動作

コンポーザブルで子孫のマージが指示された場合、このマージは正確にはどのように行われるのでしょうか。

各セマンティクス プロパティには、明確なマージ戦略があります。たとえば、ContentDescription プロパティの場合、子孫の ContentDescription 値がすべてリストに追加されます。セマンティクス プロパティのマージ戦略を確認するには、SemanticsProperties.kt でその mergePolicy の実装を確認してください。プロパティでは、常に親または子の値を採用することも、値をリストまたは文字列にマージすることもできますが、まったくマージせずに例外をスローすることも、他のカスタムのマージ戦略を使用することもできます。

重要なのは、それ自体が mergeDescendants = true を設定した子孫は、マージの対象外であるということです。例を見てみましょう。

図 6. 画像、テキスト、ブックマーク アイコンのあるリスト項目。

ここにクリック可能なリストアイテムがあります。ユーザーが行をタップすると、記事の詳細ページに移動し、そこで記事を読むことができます。リストアイテム内には、この記事をブックマークするボタンがあります。この場合、クリック可能な要素がネストされているため、ボタンはマージされたツリー内に単独で表示されます。行の残りのコンテンツはマージされます。

図 7. マージされたツリーには、行ノード内のリストに複数のテキストがある。マージされていないツリーには、テキスト コンポーザブルごとに別々のノードがある。

セマンティクス ツリーの調整

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