[go: nahoru, domu]

blob: 3996bd30969e7973d87dba9dcd5063f089c92368 [file] [log] [blame]
/*
* Copyright 2019 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.ui.material
import androidx.compose.Composable
import androidx.compose.composer
import androidx.compose.unaryPlus
import androidx.ui.core.CurrentTextStyleProvider
import androidx.ui.core.Semantics
import androidx.ui.core.Text
import androidx.ui.core.dp
import androidx.ui.core.sp
import androidx.ui.foundation.Clickable
import androidx.ui.foundation.SimpleImage
import androidx.ui.layout.Container
import androidx.ui.layout.FlexRow
import androidx.ui.layout.MainAxisAlignment
import androidx.ui.layout.Row
import androidx.ui.layout.WidthSpacer
import androidx.ui.material.surface.Surface
import androidx.ui.graphics.Color
import androidx.ui.layout.Align
import androidx.ui.layout.Alignment
import androidx.ui.layout.Center
import androidx.ui.layout.ConstrainedBox
import androidx.ui.layout.DpConstraints
import androidx.ui.layout.EdgeInsets
import androidx.ui.layout.Stack
import androidx.ui.material.BottomAppBar.FabPosition
import androidx.ui.material.BottomAppBar.FabPosition.Center
import androidx.ui.material.BottomAppBar.FabPosition.End
import androidx.ui.material.ripple.Ripple
import androidx.ui.painting.Image
import androidx.ui.text.TextStyle
/**
* A TopAppBar displays information and actions relating to the current screen and is placed at the
* top of the screen.
*
* @sample androidx.ui.material.samples.SimpleTopAppBar
*
* @param title The title to be displayed in the center of the TopAppBar
* @param color An optional color for the TopAppBar. By default [MaterialColors.primary] will be
* used.
* @param navigationIcon The navigation icon displayed at the start of the TopAppBar
* @param contextualActions A list representing the contextual actions to be displayed at the end of
* the TopAppBar. Any remaining actions that do not fit on the TopAppBar should typically be
* displayed in an overflow menu at the end.
* @param action A specific item action to be displayed at the end of the TopAppBar - this will be
* called for items in [contextualActions] up to the maximum number of icons that can be displayed.
* @param T the type of item in [contextualActions]
*/
@Composable
fun <T> TopAppBar(
title: @Composable() () -> Unit = {},
color: Color = +themeColor { primary },
navigationIcon: @Composable() () -> Unit = {},
contextualActions: List<T>? = null,
action: @Composable() (T) -> Unit = {}
// TODO: support overflow menu here with the remainder of the list
) {
BaseTopAppBar(
color = color,
startContent = navigationIcon,
title = title,
endContent = {
if (contextualActions != null) {
AppBarActions(MaxIconsInTopAppBar, contextualActions, action)
}
}
)
}
@Composable
private fun BaseTopAppBar(
color: Color = +themeColor { primary },
startContent: @Composable() () -> Unit,
title: @Composable() () -> Unit,
endContent: @Composable() () -> Unit
) {
BaseAppBar(color) {
FlexRow(mainAxisAlignment = MainAxisAlignment.SpaceBetween) {
inflexible {
// TODO: what should the spacing be when there is no icon provided here?
startContent()
WidthSpacer(width = 32.dp)
}
expanded(1f) {
CurrentTextStyleProvider(value = +themeTextStyle { h6 }) {
title()
}
}
inflexible {
endContent()
}
}
}
}
object BottomAppBar {
/**
* The possible positions for the [FloatingActionButton] embedded in the [BottomAppBar], if a
* [FloatingActionButton] is specified.
*/
enum class FabPosition {
/**
* Positioned in the center of the [BottomAppBar], overlapping the content of the
* BottomAppBar
*/
Center,
/**
* Positioned at the end of the [BottomAppBar], overlapping the content of the
* BottomAppBar
*/
End,
/**
* Positioned in the center of the [BottomAppBar], with an inset cutting into the content of
* the BottomAppBar
*/
CenterCut,
}
}
/**
* A BottomAppBar displays actions relating to the current screen and is placed at the bottom of
* the screen. It can also optionally display a [FloatingActionButton], which is either overlaid
* on top of the BottomAppBar, or inset, carving a notch in the BottomAppBar.
*
* The location of the actions displayed by the BottomAppBar depends on the [FabPosition] /
* existence of the [FloatingActionButton]. When the [FloatingActionButton] is:
*
* - not set: the [navigationIcon] is displayed at the start, and the [contextualActions] are
* displayed at the end
*
* @sample androidx.ui.material.samples.SimpleBottomAppBarNoFab
*
* - [Center] or [CenterCut] aligned: the [navigationIcon] is displayed at the start, and the
* [contextualActions] are displayed at the end
*
* @sample androidx.ui.material.samples.SimpleBottomAppBarCenterFab
*
* - [End] aligned: the [contextualActions] are displayed at the start, and no navigation icon is
* supported
*
* @sample androidx.ui.material.samples.SimpleBottomAppBarEndFab
*
* @param color An optional color for the BottomAppBar. By default [MaterialColors.primary]
* will be used.
* @param navigationIcon The navigation icon displayed in the BottomAppBar. Note that if
* [fabPosition] is set to [End], this parameter must be null / not set.
* @param floatingActionButton The [FloatingActionButton] displayed in the BottomAppBar. The
* position of this fab will be [Center] aligned by default. You can set [fabPosition] to change
* the position.
* @param fabPosition The [FabPosition] of the [floatingActionButton]. This can be [Center],
* [CenterCut], or [End].
* @param contextualActions A list representing the contextual actions to be displayed in the
* BottomAppBar. Any remaining actions that do not fit on the BottomAppBar should typically be
* displayed in an overflow menu.
* @param action A specific item action to be displayed in the BottomAppBar - this will be called
* for items in [contextualActions] up to the maximum number of icons that can be displayed.
* @param T the type of item in [contextualActions]
*/
// TODO: b/137311217 - type inference for nullable lambdas currently doesn't work
@Suppress("USELESS_CAST")
@Composable
fun <T> BottomAppBar(
color: Color = +themeColor { primary },
navigationIcon: (@Composable() () -> Unit)? = null as @Composable() (() -> Unit)?,
floatingActionButton: (@Composable() () -> Unit)? = null as @Composable() (() -> Unit)?,
fabPosition: FabPosition = Center,
contextualActions: List<T>? = null,
action: @Composable() (T) -> Unit = {}
// TODO: support overflow menu here with the remainder of the list
) {
require(navigationIcon == null || fabPosition != End) {
"Using a navigation icon with an end-aligned FloatingActionButton is not supported"
}
val actions = { maxIcons: Int ->
@Composable {
if (contextualActions != null) {
AppBarActions(maxIcons, contextualActions, action)
}
}
}
val navigationIconComposable = @Composable {
if (navigationIcon != null) {
navigationIcon()
}
}
if (floatingActionButton == null) {
BaseBottomAppBar(
color = color,
startContent = navigationIconComposable,
endContent = actions(MaxIconsInBottomAppBarNoFab)
)
return
}
when (fabPosition) {
End -> BaseBottomAppBar(
color = color,
startContent = actions(MaxIconsInBottomAppBarEndFab),
fab = { Align(Alignment.CenterRight) { floatingActionButton() } }
)
// TODO: support CenterCut
else -> BaseBottomAppBar(
color = color,
startContent = navigationIconComposable,
fab = { Center { floatingActionButton() } },
endContent = actions(MaxIconsInBottomAppBarCenterFab)
)
}
}
// TODO: b/137311217 - type inference for nullable lambdas currently doesn't work
@Suppress("USELESS_CAST")
@Composable
private fun BaseBottomAppBar(
color: Color = +themeColor { primary },
startContent: @Composable() () -> Unit = {},
fab: (@Composable() () -> Unit)? = null as @Composable() (() -> Unit)?,
endContent: @Composable() () -> Unit = {}
) {
val appBar = @Composable { BaseBottomAppBarWithoutFab(color, startContent, endContent) }
if (fab == null) {
appBar()
} else {
ConstrainedBox(
constraints = DpConstraints(
minHeight = BottomAppBarHeightWithFab,
maxHeight = BottomAppBarHeightWithFab
)
) {
Stack {
aligned(Alignment.BottomCenter) {
appBar()
}
aligned(Alignment.TopCenter) {
Container(
height = AppBarHeight,
padding = EdgeInsets(left = AppBarPadding, right = AppBarPadding)
) {
fab()
}
}
}
}
}
}
@Composable
private fun BaseBottomAppBarWithoutFab(
color: Color,
startContent: @Composable() () -> Unit,
endContent: @Composable() () -> Unit
) {
BaseAppBar(color) {
FlexRow(mainAxisAlignment = MainAxisAlignment.SpaceBetween) {
inflexible {
startContent()
// TODO: if startContent() doesn't have any layout, then the endContent won't be
// placed at the end, so we need to trick it with a spacer
WidthSpacer(width = 1.dp)
}
inflexible { endContent() }
}
}
}
/**
* An empty App Bar that expands to the parent's width.
*
* For an App Bar that follows Material spec guidelines to be placed on the top of the screen, see
* [TopAppBar].
*/
@Composable
private fun BaseAppBar(color: Color, children: @Composable() () -> Unit) {
Semantics(
container = true
) {
Surface(color = color) {
Container(height = AppBarHeight, expanded = true, padding = EdgeInsets(AppBarPadding)) {
children()
}
}
}
}
@Composable
private fun <T> AppBarActions(
actionsToDisplay: Int,
contextualActions: List<T>,
action: @Composable() (T) -> Unit
) {
if (contextualActions.isEmpty()) {
return
}
// Split the list depending on how many actions we are displaying - if actionsToDisplay is
// greater than or equal to the number of actions provided, overflowActions will be empty.
val (shownActions, overflowActions) = contextualActions.withIndex().partition {
it.index < actionsToDisplay
}
Row {
shownActions.forEach { (index, shownAction) ->
action(shownAction)
if (index != shownActions.lastIndex) {
WidthSpacer(width = 24.dp)
}
}
if (overflowActions.isNotEmpty()) {
WidthSpacer(width = 24.dp)
// TODO: use overflowActions to build menu here
Container(width = 12.dp) {
Text(text = "${overflowActions.size}", style = TextStyle(fontSize = 15.sp))
}
}
}
}
/**
* A correctly sized clickable icon that can be used inside [TopAppBar] and [BottomAppBar] for
* either the navigation icon or the actions.
*
* @param icon The icon to be displayed
* @param onClick the lambda to be invoked when this icon is pressed
*/
@Composable
fun AppBarIcon(icon: Image, onClick: () -> Unit) {
Ripple(bounded = false) {
Clickable(onClick = onClick) {
Center {
Container(width = ActionIconDiameter, height = ActionIconDiameter) {
SimpleImage(icon)
}
}
}
}
}
private val ActionIconDiameter = 24.dp
private val AppBarHeight = 56.dp
private val BottomAppBarHeightWithFab = 84.dp
private val AppBarPadding = 16.dp
private const val MaxIconsInTopAppBar = 2
private const val MaxIconsInBottomAppBarCenterFab = 2
private const val MaxIconsInBottomAppBarEndFab = 4
private const val MaxIconsInBottomAppBarNoFab = 4