0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

material3.adaptive.layout をみてみる(not navigation 3)

Posted at

navigation3 で Sceneによって 画面単位で list detailといったAdaptive layoutが汲みやすくなる。 が、そもそも現状でAdaptiveLayoutの実装を知らんので軽くまとめる。
以下三つ

  • NavigationSuiteScaffold
  • ListDetailPaneScaffold
  • SupportingPaneScaffold

NavigationSuiteScaffold

NavigationSuiteScaffold は 「画面サイズやデバイスの姿勢に応じて、NavigationBarと NavigationRailを自動で切り替えるコンテナ」 として機能

基本イメージ

Scaffold(
  bottomBar = { NavigationBar { ... } }
) { innerPadding ->
  NavHost( ... )
}

Scaffold(
    topBar = { TopAppBar(...) }
) { innerPadding ->
    NavigationSuiteScaffold(
        modifier = Modifier.padding(innerPadding),
        navigationSuiteItems = { ... }
    ) {
        NavHost(
            navController,
            startDestination = "home",
        ) {... }
    }
}

になる。
NavigationSuiteScaffold自体はcontentPaddingを渡してくれないので Scaffoldでラップする必要がある。

compact medium以上

layoutType: NavigationSuiteType パラメータ

NavigationSuiteTypeでどのNavigationUIを使うかを指定できる。
用意されているのは NavigationBar / NavigationRail / NavigationDrawer / None の4つ

windowSizeClass から 独自に判定挟むってこともできる。

val adaptiveInfo = androidx.compose.material3.adaptive.currentWindowAdaptiveInfo()
    // androidx.compose.material3.adaptive:adaptive version 1.2.0-beta and later
    val customType = with(adaptiveInfo) {
        if (windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND)) {
            NavigationSuiteType.NavigationDrawer
        } else {
            NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(adaptiveInfo)
        }
    }

デフォルトはこれ

layoutType: NavigationSuiteType = NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(WindowAdaptiveInfoDefault) // WindowAdaptiveInfoDefault = currentWindowAdaptiveInfo()
fun calculateFromAdaptiveInfo(adaptiveInfo: WindowAdaptiveInfo): NavigationSuiteType {
    return with(adaptiveInfo) {
        if (
            windowPosture.isTabletop ||
                windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT
        ) {
            NavigationSuiteType.NavigationBar
        } else if (
            windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED ||
                windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.MEDIUM
        ) {
            NavigationSuiteType.NavigationRail
        } else {
            NavigationSuiteType.NavigationBar
        }
    }
}

よしなに計算してくれている。
Modal使いたかったり、以下のみたいに特定のrouteでRailもBarも消したいって時以外はデフォルトでもよさそうな。

val customType = with(adaptiveInfo) {
    if (isTopLevelRoute) {
        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(this)
    } else {
        NavigationSuiteType.None
    }
}

ListDetailPaneScaffold

List - Detail のレイアウトを作成する。
navigationと統合した NavigableListDetailPaneScaffoldを使うと楽。

コールサイト視点だと これが

NavHost(
    navController = navController,
    startDestination = AppRoute.Home,
    modifier = modifier
) {
    composable<AppRoute.List> {
        ListScreen(navController)
    }
    composable<AppRoute.Detail> {
        val detail = it.toRoute<AppRoute.Detail>()
        DetailScreen(detail.id, navController)
    }
    ...
}

こう一本化できる。

NavHost(
    navController = navController,
    startDestination = AppRoute.Home,
    modifier = modifier
) {
    composable<AppRoute.List> {
        ListDetailScreen(navController)
    }
    ...
}

中身

@ExperimentalMaterial3AdaptiveApi
@Composable
fun ListDetailScreen(navController: NavController) {
    val navigator = rememberListDetailPaneScaffoldNavigator<String>()
    val scope = rememberCoroutineScope()

    NavigableListDetailPaneScaffold(
        navigator = navigator,
        listPane = {
            AnimatedPane {
                ListPaneContent(
                    items = items,
                    onItemClick = { id ->
                        scope.launch {
                            navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, id)
                        }
                    }
                )
            }
        },
        detailPane = {
            AnimatedPane {
                val id = navigator.currentDestination?.contentKey
                if (id != null) {
                    DetailPaneContent(
                        itemId = id,
                        onOpenMain = { mainId -> navController.navigate(AppRoute.MainContent(mainId)) }
                    )
                } else {
                    // placeholder
                    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                        Text("No item selected")
                    }
                }
            }
        }
    )
}

コツは単発で使うComposable(ListやDetail)をnavControllerに依存させないでおくこと。
rememberListDetailPaneScaffoldNavigatorのThreePaneScaffoldNavigator<T>という別のnavigatorを使うので、Paneに指定するComposableでnavControllerを使ったロジックがあると List と Detailの決号部分の引き剥がしが必要になる。

rememberListDetailPaneScaffoldNavigatorのパラメータ

fun <T> rememberListDetailPaneScaffoldNavigator(
    scaffoldDirective: PaneScaffoldDirective =
        calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()),
    adaptStrategies: ThreePaneScaffoldAdaptStrategies =
        ListDetailPaneScaffoldDefaults.adaptStrategies(),
    isDestinationHistoryAware: Boolean = true,
    initialDestinationHistory: List<ThreePaneScaffoldDestinationItem<T>> =
        DefaultListDetailPaneHistory,
): ThreePaneScaffoldNavigator<T>
  • scaffoldDirective
    windowSizeやposture に応じて、いくつのpaneを並べるか指定する。デフォルト実装の計算が参考。
fun calculatePaneScaffoldDirective(
    windowAdaptiveInfo: WindowAdaptiveInfo,
    verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating,
): PaneScaffoldDirective {
    val maxHorizontalPartitions: Int
    val horizontalPartitionSpacerSize: Dp
    val defaultPanePreferredWidth: Dp
    when (windowAdaptiveInfo.windowSizeClass.minWidth) {
        WindowSizeClass.WidthSizeClasses.Compact -> {
            maxHorizontalPartitions = 1
            horizontalPartitionSpacerSize = 0.dp
            defaultPanePreferredWidth = PaneScaffoldDirective.DefaultPreferredWidth
        }
        WindowSizeClass.WidthSizeClasses.Medium -> {
            maxHorizontalPartitions = 1
            horizontalPartitionSpacerSize = 0.dp
            defaultPanePreferredWidth = PaneScaffoldDirective.DefaultPreferredWidth
        }
        WindowSizeClass.WidthSizeClasses.Expanded -> {
            maxHorizontalPartitions = 2
            horizontalPartitionSpacerSize = 24.dp
            defaultPanePreferredWidth = PaneScaffoldDirective.DefaultPreferredWidth
        }
        else -> {
            maxHorizontalPartitions = 3
            horizontalPartitionSpacerSize = 24.dp
            defaultPanePreferredWidth = PaneScaffoldDirective.DefaultPreferredWidthXL
        }
    }
    ...
    return PaneScaffoldDirective(...

  • adaptStrategies: ThreePaneScaffoldAdaptStrategies

3paneの出し方の戦略をカスタマイズできる。
AdaptStrategy 3種類

•	Hide … 出せない時は隠す(デフォルト挙動)
•	Reflow(role) … 出せない時は指定ペインの下へ合流(1ペイン構成時などに、List と Detail を同じペインで切替表示させたい等)
•	Levitate(alignment, scrim) … 出せない時は上に浮かせる(ダイアログ/シートのように)

例えばこれで,List側に合流させるみたいな面白いことができる

adaptStrategies = ThreePaneScaffoldAdaptStrategies(
    // List
    primaryPaneAdaptStrategy = AdaptStrategy.Hide, // 出せない時は隠す
    // Detail
    secondaryPaneAdaptStrategy = AdaptStrategy.Reflow(ThreePaneScaffoldRole.Primary), // 出せない時はPrimaryへ合流
    // extra
    tertiaryPaneAdaptStrategy  = AdaptStrategy.Hide
)
デフォルト
  • isDestinationHistoryAware: Boolean = true

以下っぽいが基本はtrueでよい。

•	true:2 ペイン時に List を展開したまま Detail へ遷移しても、Back で自然に戻るなど、履歴に“レイアウト変化”が反映される。
•	false:レイアウト変化をまたぐ履歴をスキップ気味。Back 可能性や可視ペインの扱いが簡素化される。
  • initialDestinationHistory: List>

ナビゲータの初期バックスタック

例えば以下とすると、Detailがすでに入った状態で表示される。

val initialStack = listOf(
    ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.List),
    ThreePaneScaffoldDestinationItem(ListDetailPaneScaffoldRole.Detail, contentKey = "initialId")
)
val navigator = rememberListDetailPaneScaffoldNavigator(
    initialDestinationHistory = initialStack
)

SupportingPaneScaffold

実装の構造や仕組みについては、ほぼListDetailと同じ。

val navigator = rememberSupportingPaneScaffoldNavigator<String>()
val scope = rememberCoroutineScope()

NavigableSupportingPaneScaffold(
    navigator = navigator,
    mainPane = {
        AnimatedPane {
            val hasSupporting = navigator.currentDestination?.contentKey != null
            MainPaneContent(
                itemId = itemId,
                onOpenSub = { id ->
                    scope.launch {
                        navigator.navigateTo(SupportingPaneScaffoldRole.Supporting, id)
                    }
                },
                showSubButton = !hasSupporting // Hide button when SubContent is showing
            )
        }
    },
    supportingPane = {
        AnimatedPane {
            val subContentId = navigator.currentDestination?.contentKey
            if (subContentId != null) {
                SubPaneContent(
                    parentId = subContentId,
                    onClose = {
                        scope.launch {
                            navigator.navigateBack()
                        }
                    }
                )
            } else {
                Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                    Text("No sub content selected")
                }
            }
        }
    }
)
compact expanded

デフォルトでcompactでも supporting pane が表示になる。


私の感覚だとCompactは普通にシングルペインで表示して欲しいので、前述のThreePaneScaffoldAdaptStrategiesで改変しておくと良いのかもしれない。

val directive = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo())
val navigator = rememberSupportingPaneScaffoldNavigator<String>(
    scaffoldDirective = directive,
    adaptStrategies = ThreePaneScaffoldAdaptStrategies(
        // main
        primaryPaneAdaptStrategy = AdaptStrategy.Hide,
        // supporting
        secondaryPaneAdaptStrategy = AdaptStrategy.Hide,
        // extra
        tertiaryPaneAdaptStrategy = AdaptStrategy.Hide
    )
)

NavigableSupportingPaneScaffold(
    navigator = navigator,
    mainPane = {
        AnimatedPane {
            val hasSupporting = navigator.currentDestination?.contentKey != null
            MainPaneContent(
                itemId = itemId,
                onOpenSub = { id ->
                    // paneの数でナビゲートを分岐する
                    if (directive.maxHorizontalPartitions == 1) {
                        navController.navigate(AppRoute.SubContent)
                    } else {
                        scope.launch {
                            navigator.navigateTo(SupportingPaneScaffoldRole.Supporting, id)
                        }
                    }
                    
                },
                showSubButton = !hasSupporting // Hide button when SubContent is showing
            )
        }
    },
    supportingPane = {
        AnimatedPane {
            val subContentId = navigator.currentDestination?.contentKey
            if (subContentId != null) {
                SubPaneContent(
                    parentId = subContentId,
                    onClose = {
                        scope.launch {
                            navigator.navigateBack()
                        }
                    }
                )
            } else {
                Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                    Text("No sub content selected")
                }
            }
        }
    }
)

おわり

Resizable Emulator 便利

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?