[go: nahoru, domu]

blob: 71a31c5a69b7936a3dc9ba365174f43aaa56c7b6 [file] [log] [blame]
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.material3.adaptive
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.DrawerDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemColors
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.NavigationDrawerItemColors
import androidx.compose.material3.NavigationDrawerItemDefaults
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailDefaults
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.NavigationRailItemColors
import androidx.compose.material3.NavigationRailItemDefaults
import androidx.compose.material3.PermanentDrawerSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass.Companion.Compact
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass.Companion.Expanded
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ParentDataModifierNode
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Density
import androidx.compose.ui.util.fastMap
/**
* The Navigation Suite Scaffold wraps the provided content and places the adequate provided
* navigation component on the screen according to the current [NavigationSuiteType].
*
* Example default usage:
* @sample androidx.compose.material3.adaptive.samples.NavigationSuiteScaffoldSample
* Example custom configuration usage:
* @sample androidx.compose.material3.adaptive.samples.NavigationSuiteScaffoldCustomConfigSample
*
* @param navigationSuite the navigation component to be displayed, typically [NavigationSuite]
* @param modifier the [Modifier] to be applied to the navigation suite scaffold
* @param containerColor the color used for the background of the navigation suite scaffold. Use
* [Color.Transparent] to have no color
* @param contentColor the preferred color for content inside the navigation suite scaffold.
* Defaults to either the matching content color for [containerColor], or to the current
* [LocalContentColor] if [containerColor] is not a color from the theme
* @param content the content of your screen
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
fun NavigationSuiteScaffold(
navigationSuite: @Composable NavigationSuiteScaffoldScope.() -> Unit,
modifier: Modifier = Modifier,
containerColor: Color = MaterialTheme.colorScheme.background,
contentColor: Color = contentColorFor(containerColor),
content: @Composable () -> Unit = {},
) {
Surface(modifier = modifier, color = containerColor, contentColor = contentColor) {
NavigationSuiteScaffoldLayout(
navigationSuite = navigationSuite,
content = content
)
}
}
/**
* Layout for a [NavigationSuiteScaffold]'s content.
*
* @param navigationSuite the navigation suite of the [NavigationSuiteScaffold]
* @param content the main body of the [NavigationSuiteScaffold]
* @throws [IllegalArgumentException] if there is more than one [NavigationSuiteAlignment] for the
* given navigation component
*/
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
private fun NavigationSuiteScaffoldLayout(
navigationSuite: @Composable NavigationSuiteScaffoldScope.() -> Unit,
content: @Composable () -> Unit = {}
) {
Layout(
contents = listOf({ NavigationSuiteScaffoldScopeImpl.navigationSuite() }, content)
) { (navigationMeasurables, contentMeasurables), constraints ->
val navigationPlaceables = navigationMeasurables.map { it.measure(constraints) }
val alignments = navigationPlaceables.fastMap {
(it.parentData as NavigationSuiteParentData).alignment
}.filterNotNull()
if (alignments.all { alignments[0] != it }) {
throw IllegalArgumentException("There should be only one NavigationSuiteAlignment.")
}
val alignment = alignments.firstOrNull() ?: NavigationSuiteAlignment.StartVertical
val layoutHeight = constraints.maxHeight
val layoutWidth = constraints.maxWidth
val contentPlaceables = contentMeasurables.map { it.measure(
if (alignment == NavigationSuiteAlignment.TopHorizontal ||
alignment == NavigationSuiteAlignment.BottomHorizontal
) {
constraints.copy(
minHeight = layoutHeight - navigationPlaceables.maxOf { it.height },
maxHeight = layoutHeight - navigationPlaceables.maxOf { it.height }
)
} else {
constraints.copy(
minWidth = layoutWidth - navigationPlaceables.maxOf { it.width },
maxWidth = layoutWidth - navigationPlaceables.maxOf { it.width }
)
}
) }
layout(layoutWidth, layoutHeight) {
when (alignment) {
NavigationSuiteAlignment.StartVertical -> {
// Place the navigation component at the start of the screen.
navigationPlaceables.forEach {
it.placeRelative(0, 0)
}
// Place content to the side of the navigation component.
contentPlaceables.forEach {
it.placeRelative(navigationPlaceables.maxOf { it.width }, 0)
}
}
NavigationSuiteAlignment.EndVertical -> {
// Place the navigation component at the end of the screen.
navigationPlaceables.forEach {
it.placeRelative(
layoutWidth - navigationPlaceables.maxOf { it.width },
0
)
}
// Place content at the start of the screen.
contentPlaceables.forEach {
it.placeRelative(0, 0)
}
}
NavigationSuiteAlignment.TopHorizontal -> {
// Place the navigation component at the start of the screen.
navigationPlaceables.forEach {
it.placeRelative(0, 0)
}
// Place content below the navigation component.
contentPlaceables.forEach {
it.placeRelative(0, navigationPlaceables.maxOf { it.height })
}
}
NavigationSuiteAlignment.BottomHorizontal -> {
// Place content above the navigation component.
contentPlaceables.forEach {
it.placeRelative(0, 0)
}
// Place the navigation component at the bottom of the screen.
navigationPlaceables.forEach {
it.placeRelative(
0,
layoutHeight - navigationPlaceables.maxOf { it.height })
}
}
}
}
}
}
/**
* The default Material navigation component according to the current [NavigationSuiteType] to be
* used with the [NavigationSuiteScaffold].
*
* For specifics about each navigation component, see [NavigationBar], [NavigationRail], and
* [PermanentDrawerSheet].
*
* @param modifier the [Modifier] to be applied to the navigation component
* @param layoutType the current [NavigationSuiteType] of the [NavigationSuiteScaffold]. Defaults to
* [NavigationSuiteDefaults.calculateFromAdaptiveInfo]
* @param colors [NavigationSuiteColors] that will be used to determine the container (background)
* color of the navigation component and the preferred color for content inside the navigation
* component
* @param content the content inside the current navigation component, typically
* [NavigationSuiteScope.item]s
*/
@ExperimentalMaterial3AdaptiveApi
@Composable
fun NavigationSuiteScaffoldScope.NavigationSuite(
modifier: Modifier = Modifier,
layoutType: NavigationSuiteType =
NavigationSuiteDefaults.calculateFromAdaptiveInfo(WindowAdaptiveInfoDefault),
colors: NavigationSuiteColors = NavigationSuiteDefaults.colors(),
content: NavigationSuiteScope.() -> Unit
) {
val scope by rememberStateOfItems(content)
when (layoutType) {
NavigationSuiteType.NavigationBar -> {
NavigationBar(
modifier = modifier.alignment(NavigationSuiteDefaults.NavigationBarAlignment),
containerColor = colors.navigationBarContainerColor,
contentColor = colors.navigationBarContentColor
) {
scope.itemList.forEach {
NavigationBarItem(
modifier = it.modifier,
selected = it.selected,
onClick = it.onClick,
icon = { NavigationItemIcon(icon = it.icon, badge = it.badge) },
enabled = it.enabled,
label = it.label,
alwaysShowLabel = it.alwaysShowLabel,
colors = it.colors?.navigationBarItemColors
?: NavigationBarItemDefaults.colors(),
interactionSource = it.interactionSource
)
}
}
}
NavigationSuiteType.NavigationRail -> {
NavigationRail(
modifier = modifier.alignment(NavigationSuiteDefaults.NavigationRailAlignment),
containerColor = colors.navigationRailContainerColor,
contentColor = colors.navigationRailContentColor
) {
scope.itemList.forEach {
NavigationRailItem(
modifier = it.modifier,
selected = it.selected,
onClick = it.onClick,
icon = { NavigationItemIcon(icon = it.icon, badge = it.badge) },
enabled = it.enabled,
label = it.label,
alwaysShowLabel = it.alwaysShowLabel,
colors = it.colors?.navigationRailItemColors
?: NavigationRailItemDefaults.colors(),
interactionSource = it.interactionSource
)
}
}
}
NavigationSuiteType.NavigationDrawer -> {
PermanentDrawerSheet(
modifier = modifier.alignment(NavigationSuiteDefaults.NavigationDrawerAlignment),
drawerContainerColor = colors.navigationDrawerContainerColor,
drawerContentColor = colors.navigationDrawerContentColor
) {
scope.itemList.forEach {
NavigationDrawerItem(
modifier = it.modifier,
selected = it.selected,
onClick = it.onClick,
icon = it.icon,
badge = it.badge,
label = { it.label?.invoke() ?: Text("") },
colors = it.colors?.navigationDrawerItemColors
?: NavigationDrawerItemDefaults.colors(),
interactionSource = it.interactionSource
)
}
}
}
}
}
/** The scope associated with the [NavigationSuiteScaffold]. */
@ExperimentalMaterial3AdaptiveApi
interface NavigationSuiteScaffoldScope {
/**
* [Modifier] that should be applied to the [NavigationSuite] of the [NavigationSuiteScaffold]
* in order to determine its alignment on the screen.
*
* @param alignment the desired [NavigationSuiteAlignment]
*/
fun Modifier.alignment(alignment: NavigationSuiteAlignment): Modifier
}
/**
* Represents the alignment of the navigation component of the [NavigationSuiteScaffold].
*
* The alignment informs the Navigation Suite Scaffold how to properly place the expected navigation
* component on the screen in relation to the Navigation Suite Scaffold's content.
*/
@ExperimentalMaterial3AdaptiveApi
enum class NavigationSuiteAlignment {
/** The navigation component is vertical and positioned at the start of the screen. */
StartVertical,
/** The navigation component is vertical and positioned at the end of the screen. */
EndVertical,
/** The navigation component is horizontal and positioned at the top of the screen. */
TopHorizontal,
/** The navigation component is horizontal and positioned at the bottom of the screen. */
BottomHorizontal
}
/** The scope associated with the [NavigationSuite]. */
@ExperimentalMaterial3AdaptiveApi
interface NavigationSuiteScope {
/**
* This function sets the parameters of the default Material navigation item to be used with the
* Navigation Suite Scaffold. The item is called in [NavigationSuite], according to the
* current [NavigationSuiteType].
*
* For specifics about each item component, see [NavigationBarItem], [NavigationRailItem], and
* [NavigationDrawerItem].
*
* @param selected whether this item is selected
* @param onClick called when this item is clicked
* @param icon icon for this item, typically an [Icon]
* @param modifier the [Modifier] to be applied to this item
* @param enabled controls the enabled state of this item. When `false`, this component will not
* respond to user input, and it will appear visually disabled and disabled to accessibility
* services. Note: as of now, for [NavigationDrawerItem], this is always `true`.
* @param label the text label for this item
* @param alwaysShowLabel whether to always show the label for this item. If `false`, the label will
* only be shown when this item is selected. Note: for [NavigationDrawerItem] this is always `true`
* @param badge optional badge to show on this item
* @param colors [NavigationSuiteItemColors] that will be used to resolve the colors used for this
* item in different states.
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this item. You can create and pass in your own `remember`ed instance to observe
* [Interaction]s and customize the appearance / behavior of this item in different states
*/
fun item(
selected: Boolean,
onClick: () -> Unit,
icon: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
label: @Composable (() -> Unit)? = null,
alwaysShowLabel: Boolean = true,
badge: (@Composable () -> Unit)? = null,
colors: NavigationSuiteItemColors? = null,
interactionSource: MutableInteractionSource = MutableInteractionSource()
)
}
/**
* Class that describes the different navigation suite types of the [NavigationSuiteScaffold].
*
* The [NavigationSuiteType] informs the [NavigationSuite] of what navigation component to expect.
*/
@JvmInline
@ExperimentalMaterial3AdaptiveApi
value class NavigationSuiteType private constructor(private val description: String) {
override fun toString(): String {
return description
}
companion object {
/**
* A navigation suite type that instructs the [NavigationSuite] to expect a [NavigationBar].
*
* @see NavigationBar
*/
val NavigationBar = NavigationSuiteType(description = "NavigationBar")
/**
* A navigation suite type that instructs the [NavigationSuite] to expect a
* [NavigationRail].
*
* @see NavigationRail
*/
val NavigationRail = NavigationSuiteType(description = "NavigationRail")
/**
* A navigation suite type that instructs the [NavigationSuite] to expect a
* [PermanentDrawerSheet].
*
* @see PermanentDrawerSheet
*/
val NavigationDrawer = NavigationSuiteType(description = "NavigationDrawer")
}
}
/** Contains the default values used by the [NavigationSuite]. */
@ExperimentalMaterial3AdaptiveApi
object NavigationSuiteDefaults {
/**
* Returns the expected [NavigationSuiteType] according to the provided [WindowAdaptiveInfo].
* Usually used with the [NavigationSuite].
*
* @param adaptiveInfo the provided [WindowAdaptiveInfo]
* @see NavigationSuite
*/
fun calculateFromAdaptiveInfo(adaptiveInfo: WindowAdaptiveInfo): NavigationSuiteType {
return with(adaptiveInfo) {
if (posture.isTabletop || windowSizeClass.heightSizeClass == Compact) {
NavigationSuiteType.NavigationBar
} else if (windowSizeClass.widthSizeClass == Expanded) {
NavigationSuiteType.NavigationRail
} else {
NavigationSuiteType.NavigationBar
}
}
}
/** Default alignment for the [NavigationSuiteType.NavigationBar]. */
val NavigationBarAlignment = NavigationSuiteAlignment.BottomHorizontal
/** Default alignment for the [NavigationSuiteType.NavigationRail]. */
val NavigationRailAlignment = NavigationSuiteAlignment.StartVertical
/** Default alignment for the [NavigationSuiteType.NavigationDrawer]. */
val NavigationDrawerAlignment = NavigationSuiteAlignment.StartVertical
/**
* Creates a [NavigationSuiteColors] with the provided colors for the container color, according
* to the Material specification.
*
* Use [Color.Transparent] for the navigation*ContainerColor to have no color. The
* navigation*ContentColor will default to either the matching content color for
* navigation*ContainerColor, or to the current [LocalContentColor] if navigation*ContainerColor
* is not a color from the theme.
*
* @param navigationBarContainerColor the default container color for the [NavigationBar]
* @param navigationBarContentColor the default content color for the [NavigationBar]
* @param navigationRailContainerColor the default container color for the [NavigationRail]
* @param navigationRailContentColor the default content color for the [NavigationRail]
* @param navigationDrawerContainerColor the default container color for the
* [PermanentDrawerSheet]
* @param navigationDrawerContentColor the default content color for the [PermanentDrawerSheet]
*/
@Composable
fun colors(
navigationBarContainerColor: Color = NavigationBarDefaults.containerColor,
navigationBarContentColor: Color = contentColorFor(navigationBarContainerColor),
navigationRailContainerColor: Color = NavigationRailDefaults.ContainerColor,
navigationRailContentColor: Color = contentColorFor(navigationRailContainerColor),
navigationDrawerContainerColor: Color = DrawerDefaults.containerColor,
navigationDrawerContentColor: Color = contentColorFor(navigationDrawerContainerColor),
): NavigationSuiteColors =
NavigationSuiteColors(
navigationBarContainerColor = navigationBarContainerColor,
navigationBarContentColor = navigationBarContentColor,
navigationRailContainerColor = navigationRailContainerColor,
navigationRailContentColor = navigationRailContentColor,
navigationDrawerContainerColor = navigationDrawerContainerColor,
navigationDrawerContentColor = navigationDrawerContentColor
)
}
/**
* Represents the colors of a [NavigationSuite].
*
* For specifics about each navigation component colors see [NavigationBarDefaults],
* [NavigationRailDefaults], and [DrawerDefaults].
*
* @param navigationBarContainerColor the container color for the [NavigationBar] of the
* [NavigationSuite]
* @param navigationBarContentColor the content color for the [NavigationBar] of the
* [NavigationSuite]
* @param navigationRailContainerColor the container color for the [NavigationRail] of the
* [NavigationSuite]
* @param navigationRailContentColor the content color for the [NavigationRail] of the
* [NavigationSuite]
* @param navigationDrawerContainerColor the container color for the [PermanentDrawerSheet] of the
* [NavigationSuite]
* @param navigationDrawerContentColor the content color for the [PermanentDrawerSheet] of the
* [NavigationSuite]
*/
@ExperimentalMaterial3AdaptiveApi
class NavigationSuiteColors
internal constructor(
val navigationBarContainerColor: Color,
val navigationBarContentColor: Color,
val navigationRailContainerColor: Color,
val navigationRailContentColor: Color,
val navigationDrawerContainerColor: Color,
val navigationDrawerContentColor: Color
)
/**
* Represents the colors of a [NavigationSuiteScope.item].
*
* For specifics about each navigation item colors see [NavigationBarItemColors],
* [NavigationRailItemColors], and [NavigationDrawerItemColors].
*
* @param navigationBarItemColors the [NavigationBarItemColors] associated with the
* [NavigationBarItem] of the [NavigationSuiteScope.item]
* @param navigationRailItemColors the [NavigationRailItemColors] associated with the
* [NavigationRailItem] of the [NavigationSuiteScope.item]
* @param navigationDrawerItemColors the [NavigationDrawerItemColors] associated with the
* [NavigationDrawerItem] of the [NavigationSuiteScope.item]
*/
@ExperimentalMaterial3AdaptiveApi
class NavigationSuiteItemColors
internal constructor(
val navigationBarItemColors: NavigationBarItemColors,
val navigationRailItemColors: NavigationRailItemColors,
val navigationDrawerItemColors: NavigationDrawerItemColors,
)
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
internal object NavigationSuiteScaffoldScopeImpl : NavigationSuiteScaffoldScope {
override fun Modifier.alignment(alignment: NavigationSuiteAlignment): Modifier {
return this.then(
AlignmentElement(alignment = alignment)
)
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
internal class AlignmentElement(
val alignment: NavigationSuiteAlignment
) : ModifierNodeElement<AlignmentNode>() {
override fun create(): AlignmentNode {
return AlignmentNode(alignment)
}
override fun update(node: AlignmentNode) {
node.alignment = alignment
}
override fun InspectorInfo.inspectableProperties() {
name = "alignment"
value = alignment
}
override fun hashCode(): Int = alignment.hashCode()
override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? AlignmentElement ?: return false
return alignment == otherModifier.alignment
}
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
internal class AlignmentNode(
var alignment: NavigationSuiteAlignment
) : ParentDataModifierNode, Modifier.Node() {
override fun Density.modifyParentData(parentData: Any?) =
((parentData as? NavigationSuiteParentData) ?: NavigationSuiteParentData()).also {
it.alignment = alignment
}
}
/** Parent data associated with children. */
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
internal data class NavigationSuiteParentData(
var alignment: NavigationSuiteAlignment? = null
)
internal val IntrinsicMeasurable.navigationSuiteParentData: NavigationSuiteParentData?
get() = parentData as? NavigationSuiteParentData
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
internal val NavigationSuiteParentData?.alignment: NavigationSuiteAlignment
get() = this?.alignment ?: NavigationSuiteAlignment.StartVertical
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
internal expect val WindowAdaptiveInfoDefault: WindowAdaptiveInfo
@Composable
get
private interface NavigationSuiteItemProvider {
val itemsCount: Int
val itemList: MutableVector<NavigationSuiteItem>
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private class NavigationSuiteItem constructor(
val selected: Boolean,
val onClick: () -> Unit,
val icon: @Composable () -> Unit,
val modifier: Modifier,
val enabled: Boolean,
val label: @Composable (() -> Unit)?,
val alwaysShowLabel: Boolean,
val badge: (@Composable () -> Unit)?,
val colors: NavigationSuiteItemColors?,
val interactionSource: MutableInteractionSource
)
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private class NavigationSuiteScopeImpl : NavigationSuiteScope,
NavigationSuiteItemProvider {
override fun item(
selected: Boolean,
onClick: () -> Unit,
icon: @Composable () -> Unit,
modifier: Modifier,
enabled: Boolean,
label: @Composable (() -> Unit)?,
alwaysShowLabel: Boolean,
badge: (@Composable () -> Unit)?,
colors: NavigationSuiteItemColors?,
interactionSource: MutableInteractionSource
) {
itemList.add(
NavigationSuiteItem(
selected = selected,
onClick = onClick,
icon = icon,
modifier = modifier,
enabled = enabled,
label = label,
alwaysShowLabel = alwaysShowLabel,
badge = badge,
colors = colors,
interactionSource = interactionSource
)
)
}
override val itemList: MutableVector<NavigationSuiteItem> = mutableVectorOf()
override val itemsCount: Int
get() = itemList.size
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
private fun rememberStateOfItems(
content: NavigationSuiteScope.() -> Unit
): State<NavigationSuiteItemProvider> {
val latestContent = rememberUpdatedState(content)
return remember {
derivedStateOf { NavigationSuiteScopeImpl().apply(latestContent.value) }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun NavigationItemIcon(
icon: @Composable () -> Unit,
badge: (@Composable () -> Unit)? = null,
) {
if (badge != null) {
BadgedBox(badge = { badge.invoke() }) {
icon()
}
} else {
icon()
}
}