NTTドコモR&Dの技術ブログです。

Android XRのViewの実装を助けてくれるComposable達

※ 本記事は 2026/3/31 以前にNTTコノキューにて記載した記事になります

0. はじめに

この記事はNTTコノキュー アドベントカレンダー2025の記事です。

こんにちは、NTTコノキューの佐々木です。

今回は以前の記事の続いて、AndroidXRのViewを作成する際に役に立つComposable達を紹介します。

以前の記事からさらに踏み込んで、1つのViewで2つのモードの表示をするために助けとなるTipsを紹介します。

本記事は、普通のAndroidアプリなら実装できるエンジニアに向けての記事となります。 既存のViewの構成に少しだけ手を入れてAndroidXRでも使えるViewが作成できるようになることを目指します。

1. 記事で紹介するComposableを使う際の注意点

早速Composableを紹介したいのですが、その前にとても重要な注意点があるので、まずはそちらを説明します。

内容は非常にシンプルで、今回紹介するComposableは全て「androidx.xr.compose.material3」のライブラリで定義されているものです。

太字にしている「xr」が非常に重要でして、実は「androidx.compose.material3」というライブラリが存在し、しかも同名のComposableが存在します。

「NavigationRail」を例に挙げると以下の2つが存在します。

  • androidx.xr.compose.material3.NavigationRail

  • androidx.compose.material3.NavigationRail

2つ並べると違いが分かりますが、実際に実装するときは上記の「xr」の有無に気付けない可能性があります。

今回紹介するのは、ライブラリのパスに「xr」がある方のComposableになります。

FullSpaceモードに切り替えた時に、Viewに変化が起きなかった場合は、このライブラリのパスを疑ってみて下さい。

2. FullScreenモードで自動で表示が変わるComposable達

それでは、HomeSpaceモードで使えて、同じ実装でFullScreenモードにすると自動で表示が切り替わる便利なComposableを紹介します。

SpaceToggleButton

まず最初に紹介するのは、HomeSpaceモードとFullSpaceモードを切り替えるボタンを提供する「SpaceToggleButton」です。

HomeSpaceモードとFullSpaceモードを切り替えるボタン自体はこのComposableを使わなくても自作できますが、「SpageToggleButton」を使えばもっと簡単に実装できるようになります。

以下、簡単な実装例になります。


SpaceToggleButton { isFullSpace ->
    if (isFullSpace) {
        // FullSpaceモード中に表示するボタンのView
    } else {
        // HomeSpaceモード中に表示するボタンのView
    }
}

これだけでモードを切り替えるボタンが実装できます。

他にもパラメータとしてModifierやIconToggleButtonColorsをパラメータに渡して見た目をある程度いじれます。

AndroidXRのViewを作成する際に、最初の方に「SpaceToggleButton」を実装しておくと以降の実装で各モードのViewを確認しやすくなるのでおすすめです。

次はNavigationRailとNavigationBarです。

両者は使い方がほぼ同じなので一緒に紹介します。

以前の記事でOrbiterを使って、メインコンテンツに紐づく形で上下左右に浮遊するViewを実装する方法を説明しましたが、今回紹介するComposableはより簡単に左側と下側限定ですがメインコンテンツに紐づくViewが作成できます。

まずは、「NavigationRail」の説明から始めます。

NavigationRailとはViewの横に表示される複数のメニューからなるViewです。

詳細は公式ドキュメントマテリアルデザインのガイドラインをご覧ください。

このNavigationRailを簡単に実装できるComposableが「NavigationRail」になります。

早速実装例を見てみましょう。


enum class NavigationType {
    LABEL1, LABEL2, LABEL3, LABEL4
}

NavigationRail(
    containerColor = if (isFullScreenMode) NavigationRailDefaults.ContainerColor else Color.Transparent,
    header = {
        // ヘッダーに表示するアイテムを定義        
    },
) {
    NavigationType.entries.forEachIndexed { index, selected ->
        val isCurrent = selectedItem == selected
        NavigationRailItem(
            icon = { /* アイコンView */ },
            label = { /* テキストView */ },
            selected = isCurrent,
            onClick = {
                // クリック時の動作
                // selectedItem = selectedのようにクリックした箇所は最低限残したい
            }
        )
    }
}

enumを定義していますが、これはNavigationRailに表示するメニューを定義できればどんな形でも問題ありません。

これで、マテリアルデザインのガイドラインで紹介されていたようなNavigationRailが実装できます。

実際に「NavigationRail」を使って実装したViewの例は以下のようになります。

HomeSpaceモード

FullSpaceモード

画像には他にもいくつかXR用の実装が入っているため、今回の説明は画像左部分のメニューを見て欲しいです。

Rowの中に定義されていた「NavigationRail」がFullSpaceモードに切り替えると自動で、Vメインコンテンツの左側外に移動します。

見た目的にはOrbiterを利用したものとほぼ同じになりますし、内部処理的にもOrbiterを使っているので、以前紹介した記事の方法でも同じことが可能ですが、1からViewを作る場合はこちらの方が実装が簡単になります。

それでは、今回の実装で使ったそれぞれのパラメータの説明をします。

containerColor

NavigationRailの背景色を設定します。

特に設定しなくてもデフォルトの色が指定されているので問題ないのですが、今回、HomeSpaceモードではメインコンテンツのViewの背景色を使いたかったので、HomeSpaceモードでは背景色が透明になるように設定しています。

header

NavigationRailの上部に付随するComposableを設定できます。

前述した画像を例に言うと「ピンク色の背景の鉛筆アイコンのボタン」が「header」にて定義したViewになります。

なお、「header」は設定しないことも可能です。 その場合はピンクのボタンがなくなり、取る詰めされた状態で表示されます。

この「header」はマテリアルデザインガイドラインのTypesで説明されている「②Spatialized FAB rail」の「鉛筆アイコンのボタン」部分を実現するためのものです。

そのため、後述するcontentととの間には必ず空白が存在し、前述したcontainerColorで背景色を設定してもheader部分には何の影響も与えません。

自由にComposableを設定できますが、上記の動きになることを注意しましょう。

content

NavigationRailのメインのcomposableを設定する所です。

実装例では末尾のラムダ構文で省略されていますが、NavigationTypeのenum関数のfor文を回している部分のことです。

後述する「NavigationRailItem」を複数回定義してメニューを作成していますが、別に「NavigationRailItem」を使わなくても構いません。好きなようにcomposableを定義して、複数回呼び出せばその回数の分縦にViewが積み上がっていく形になります。

前述したcontainerColorはこのcontentで作られるViewの背景色を設定します。

NavigationRailItem

パラメータではありませんが「NavigationRail」と一緒に使うことが多いので紹介します。

マテリアルデザインのガイドラインで紹介されている「画像とラベルをセットにしたレイアウト」を提供してくれるもので、簡単にマテリアルデザインに沿ったNavigationRailのボタンが実装できます。

パラメータを簡単に説明しますと以下のようになります。

  • icon:IconのComposableを定義することでアイコン画像を設定できる
  • label:にはTextのComposableを定義することでラベルを設定できる(省略可能)
  • selected:booleanを設定することで選択/非選択の色が自動でViewに反映される
  • onClick:アイコン画像クリック時の動作を設定できる

凝ったボタンを定義したいのであれば、自作でもいいと思いますが、簡単にマテリアルデザインのボタンが定義できるのでおすすめです。

以上が「NavigationRail」の説明になります。

続いてNavigationBarですが、NavigationRailを理解してしまうと非常に簡単で、NavigationRailを横向きにして、画面下部に表示するものがNavigationBarになります。

詳細は公式ドキュメントマテリアルデザインのガイドラインをご覧ください。

実装例の画像もNavigationRailの項目で紹介した画像の中あり、画面下部のメニューがNavigationBarになります。

パラメータもheaderがないだけで、NavigationRailと同じになります。

唯一の違いはNavigationRailItemがないことですが、全く同じパラメータを持つNavigationBarItemが存在しているため、やはりNavigationRailとほぼ同じと言ってしまっていいでしょう。

実装例を載せておきますが、NavigationRailとほぼ同じということが分かると思います。


NavigationBar(
    containerColor = if (isFullScreenMode) NavigationRailDefaults.ContainerColor else Color.Transparent,
) {
    NavigationType.entries.forEach {
        val isCurrent = selectedItem == it
        NavigationBarItem(
            icon = { /* アイコンView */ },
            label = { /* テキストView */ },
            selected = isCurrent,
            onClick = {
                // クリック時の動作
                // selectedItem = selectedのようにクリックした箇所は最低限残したい}
        )
    }
}

以上が「NavigationBar」の説明になります。

注意点

「NavigationRail」と「NavigationBar」の使い方を説明しましたが、1つ注意点があります。

それが、「NavigationRail」と「NavigationBar」は内部で「Orbiter」を使っているということです。

以前の記事で「Orbiter」の使い方を説明しましたが、「Orbiter」と「NavigationRail」、「NavigationBar」を一緒に使うと表示する場所が被って上手く描画されない場合があります。

そのため「NavigationRail」を使うのであれば「Orbiterのpositionがstart」のものは使わないようにしましょう。

同様に「NavigationBar」を使うのであれば「Orbiterのpositionがbottom」のものは使わないようにしましょう。

それ以外の「Orbiter」のpositionがend、topのものは問題なく利用できます。

HorizontalFloatingToolbar

HorizontalFloatingToolbarとは簡単にいうとNavigationBarを画面上に浮かせた状態で表示させたものです。

NavigationBarと違って全体のメニュー以外にも使う事がありますが、今回紹介するComposableとしての「HorizontalFloatingToolbar」はNavigationBarとほぼ同じ立ち位置になります。

とはいえNavigationBarにはない機能もありますので紹介していきます。

HorizontalFloatingToolbarは2つのタイプが用意されていて、それぞれ以下のように実装します。

Type.1:左右に開閉ボタンを用意

var expanded by rememberSaveable { mutableStateOf(true) }
var selectedItem by remember { mutableStateOf(NavigationType.LABEL1) }

HorizontalFloatingToolbar(
    expanded = expanded,
    floatingActionButton = {
        // ボタンViewを定義(開閉ボタン)
        // onClickでexpandedを切り替える機能が欲しい
    },
    floatingActionButtonPosition = FloatingToolbarHorizontalFabPosition.Start
) {
    NavigationType.entries.forEach {
        val isCurrent = selectedItem == it
        // ボタンViewを定義(メニュー)
        // onClickでselectedItemを切り替える機能が欲しい
    }
}

Type.2:中央に開閉ボタンを用意

var expanded by rememberSaveable { mutableStateOf(true) }
var selectedItem by remember { mutableStateOf(NavigationType.LABEL1) }

HorizontalFloatingToolbar(
    expanded = expanded,
    leadingContent = {
        NavigationType.LABEL1.let {
            val isCurrent = selectedItem == it
            // ボタンViewを定義(メニュー)
            // onClickでselectedItemを切り替える機能が欲しい
        }
        NavigationType.LABEL2.let {
            val isCurrent = selectedItem == it
            // ボタンViewを定義(メニュー)
            // onClickでselectedItemを切り替える機能が欲しい
        }
    },
    trailingContent = {
        NavigationType.LABEL3.let {
            val isCurrent = selectedItem == it
            // ボタンViewを定義(メニュー)
            // onClickでselectedItemを切り替える機能が欲しい
        }
        NavigationType.LABEL4.let {
            val isCurrent = selectedItem == it
            // ボタンViewを定義(メニュー)
            // onClickでselectedItemを切り替える機能が欲しい
        }
    }
) {
    // ボタンViewを定義(開閉ボタン)
    // onClickでexpandedを切り替える機能が欲しい
}

この実装コードですが、「開閉ボタン」をどこに置くかで実装に使うTypeが変わります。

開閉ボタンをToolbarの左右どちらかに置きたい場合はType1を、開閉ボタンをToolbarの真ん中に置きたい場合はType2を使います。

さて、唐突に出てきた「開閉ボタン」ですが、実はHorizontalFloatingToolbarはexpandedを切り替えることで、一部ボタンを残してToolbarを閉じたり開いたりすることが可能です。

それぞれ実際のViewを見てみましょう。

まずは、Type.1の場合(今回の例は開閉ボタンを左に配置)。

開いている状態

閉じている状態

次に、Type.2の場合。

開いている状態

閉じている状態

アニメーション付きの画像を用意できなくて申し訳ないですが、上記のようにHorizontalFloatingToolbarは鉛筆ボタンを押すことで周りのメニューが表示されたり消えたりできます。

なお、この開閉動作はデフォルトでアニメーション付きですが、Type.2のアニメーションが若干怪しく、気になる人は実際に試してみて欲しいです。

そして、この鉛筆ボタンの位置でHorizontalFloatingToolbarの実装するTypeを変えることになります。 自分の実装したいレイアウトに合わせて実装Typeを切り替えてください。

また、FullSpaceモードに切り替えた時の動きですが、これはNavigationBarと同じでOrbiterのpositionがbottomの位置に移動します。 そのため、NavigationBarとHorizontalFloatingToolbarは一緒に使うことができません。

それでは、各パラメータの説明です。

Type.1とType.2がありますが、同じ役割のものを一緒に紹介する形とします。

expanded

Type.1とType.2の両方にあるViewの開閉状態を管理するパラメータです。

booleanで設定でき、trueなら開く、falseなら閉じるになります。

この true / false を切り替えるだけで、HorizontalFloatingToolbarは自動でアニメーションをしつつ開閉の表示を切り替えてくれます。

Type.1:floatingActionButton / Type.2:content(末尾のラムダ構文で省略)

HorizontalFloatingToolbarで開閉されても残り続けるViewをComposableで設定するパラメータです。

Type.1とType.2で設定するパラメータのKeyは異なりますが、基本的に同じボタンViewで問題なく動作します。

開閉しても残り続けるViewのため、ここで定義するViewのonClickにexpandedを切り替える動作を設定することが推奨されます。

なお、Type.1で気になるのが開閉する際に、floatingActionButtonが真ん中に移動することです。 何らかの工夫が必要なのだと思いますが、個人的には開閉ボタンを左右に置くなら、開閉ボタンに集まる形で閉じる動きをさせたいです。

Type.1 content(末尾のラムダ構文で省略) / Type.2 leadingContent、trailingContent

HorizontalFloatingToolbarで閉じた場合は消えるViewをComposableで設定するパラメータです。

Type.1はComposableのリストを全て渡せばいいのですが、Type.2の場合だと開閉ボタンを起点に2つに分割する形で実装します。 leadingContentが開閉ボタンの左に表示するView。trailingContentが開閉ボタンの右に表示するViewになります。

閉じた際に隠れてしまうViewなので、Toolbarを開閉する機能の実装はしない方がいいでしょう。

floatingActionButtonPosition

Type.1でのみ設定するパラメータで、開閉ボタンを左右のどちらに表示するかを設定できます。

パラメータは以下の2パターンで設定します。

  • FloatingToolbarHorizontalFabPosition.Start

  • FloatingToolbarHorizontalFabPosition.End

Startでは左側に、Endでは右側に開閉ボタンが配置されるようになります。

デフォルトではEndが設定されています。

以上が「HorizontalFloatingToolbar」の説明になります。

補足

HorizontalFloatingToolbarと「Horizontal」という言葉が付いていることから分かるように、VerticalFloatingToolbarも存在します。

パラメータ等はHorizontalFloatingToolbarとほぼ同じです。役割としてはNavigationRailと被る形で、FullSpaceモードではOrbiterのpositionがstartの位置に移動します。

説明する内容がほぼ同じになるので今回は紹介は省きました。

BasicAlertDialog

「BasicAlertDialog」はダイアログを提供するComposableです。

今更ダイアログ?と思う方もいるかもしれませんが、実は既存のAlertDialogはAndroid XR では使うことができません。 FullSpaceモードで表示しようとするとクラッシュしてしまいます。

そこで、XRでも使えるようにしたのが「BasicAlertDialog」になります。

とはいえ今までと実装が大きく変わるという訳ではありません。

具体的な実装例と共に紹介します。


BasicAlertDialog(
    onDismissRequest = {
        // ダイアログが閉じる際に行う処理を実装する
    },
    properties = SpatialDialogProperties(), //ここでエレベーションとかを設定できる
) {
    // ダイアログの見た目を実装する
}

上記の通り非常にシンプルな実装になります。

パラメータについて説明していきます。

onDissmissRequest

ダイアログが閉じる際に共通で行う処理を実装します。

ダイアログが表示されてるか否か等のフラグを管理したりする際に使用することが多いと思います。

必須のパラメータですので、設定忘れに注意しましょう。

properties

ダイアログの設定の一部をここで実装します。

実装例では「SpatialDialogProperties()」を渡していますが、デフォルトも同じパラメータを渡しています。

あまり自由度のあるものではなく、「SpatialDialogProperties()」を通して以下の5つのパラメータを設定できます。

  • dismissOnBackPress:スマホの戻るボタンを押された時にダイアログを閉じるか否か

  • dismissOnClickOutside:ダイアログの外側をタップされた時にダイアログを閉じるか否か

  • usePlatformDefaultWidth:ダイアログがプラットフォームのデフォルトの幅を使用するか否か

  • elevation:DPで設定するダイアログのz軸の高さ

  • restingLevelAnimationSpec:ダイアログの表示アニメーションの速度の変化?

基本的には「dismissOnBackPress」「dismissOnClickOutside」「elevation」の3つを変更することがあると思います。

ただ、elevationは設定するパラメータによっては意図せぬ表示になることがあるので、デフォルトのままにしておいた方がいいでしょう。

content(末尾のラムダ構文で省略)

ダイアログの実際の見た目を設定します。

composableでダイアログのレイアウトを実装しこのcontentに設定します。

基本的には今までのダイアログのレイアウトをここで実装すれば、既存で実装していたダイアログをそのまま使えると思います。

以上が「BasicAlertDialog」の説明になります。

3. まとめ

以上がAndroid XRのViewの実装を助けてくれるComposable達になります。

他にもいくつか紹介していないものがありますが、公式ドキュメントにまとめられているので、興味が出てきたら覗いてみてください。

今まで紹介してきたComposable達を使うことで、1つのレイアウトの実装からHomeSpaceとFullSpaceの両モードで使えるViewを生成する助けになると思います。 是非記事内で紹介したComposableを使って、Android XRのViewを実装してみてください。