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 が、このプロパティを使用して、画面に表示されている内容を読み上げることで、ユーザーはスムーズに操作できるようになります。スイッチなら、「オン、スイッチ、ダブルタップして切り替え」と説明すると、ユーザーは画面をダブルタップして、スイッチをオフに切り替えられます。
-
テスト フレームワークは、このプロパティを使用してノードを検出し、それを操作して、アサーションを行います。スイッチのテストの例を次に示します。
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 つの異なるツリーのことを言っています。mergeDescendants
が true
に設定されている場合に子孫ノードをマージする、マージされたセマンティクス ツリーと、マージを適用せず、すべてのノードはそのまましておく、マージされていないセマンティクス ツリーです。ユーザー補助サービスでは、マージされていないツリーを使用し、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. マージされたツリーには、行ノード内のリストに複数のテキストがある。マージされていないツリーには、テキスト コンポーザブルごとに別々のノードがある。
セマンティクス ツリーの調整
前述のように、特定のセマンティクス プロパティをオーバーライドまたはクリアしたり、ツリーのマージ動作を変更したりできます。これは、独自のカスタム コンポーネントを作成する場合に特に重要です。適切なプロパティとマージ動作を設定しないと、アプリがユーザー補助対応でなくなり、テストも想定どおりに動作しなくなる場合があります。セマンティクス ツリーを調整する必要がある一般的なユースケースの詳細については、ユーザー補助機能のドキュメントをご覧ください。テストの詳細については、テストガイドをご覧ください。