[go: nahoru, domu]

blob: 54b6215f501d3c7e57e6adc3a4b3a286abdaecf3 [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.Immutable
import androidx.ui.core.Alignment
import androidx.ui.core.CurrentTextStyleProvider
import androidx.ui.core.DensityAmbient
import androidx.ui.core.LastBaseline
import androidx.ui.core.Text
import androidx.ui.foundation.Clickable
import androidx.ui.foundation.SimpleImage
import androidx.ui.foundation.shape.RectangleShape
import androidx.ui.foundation.shape.corner.CircleShape
import androidx.ui.geometry.Offset
import androidx.ui.geometry.Rect
import androidx.ui.graphics.Color
import androidx.ui.graphics.Image
import androidx.ui.graphics.Outline
import androidx.ui.graphics.Path
import androidx.ui.graphics.PathOperation
import androidx.ui.graphics.Shape
import androidx.ui.graphics.addOutline
import androidx.ui.layout.AlignmentLineOffset
import androidx.ui.layout.Arrangement
import androidx.ui.layout.Container
import androidx.ui.layout.EdgeInsets
import androidx.ui.layout.LayoutAlign
import androidx.ui.layout.LayoutHeight
import androidx.ui.layout.LayoutSize
import androidx.ui.layout.LayoutWidth
import androidx.ui.layout.Padding
import androidx.ui.layout.Row
import androidx.ui.layout.Spacer
import androidx.ui.layout.Wrap
import androidx.ui.material.BottomAppBar.FabConfiguration
import androidx.ui.material.BottomAppBar.FabDockedPosition
import androidx.ui.material.ripple.Ripple
import androidx.ui.material.surface.Surface
import androidx.ui.material.surface.primarySurface
import androidx.ui.semantics.Semantics
import androidx.ui.text.TextStyle
import androidx.ui.unit.Density
import androidx.ui.unit.Dp
import androidx.ui.unit.IntPxSize
import androidx.ui.unit.PxPosition
import androidx.ui.unit.PxSize
import androidx.ui.unit.dp
import androidx.ui.unit.sp
import androidx.ui.unit.toPxSize
import kotlin.math.sqrt
/**
* A TopAppBar displays information and actions relating to the current screen and is placed at the
* top of the screen.
*
* This TopAppBar displays only a title and navigation icon, use the other TopAppBar overload if
* you want to display actions as well.
*
* @sample androidx.ui.material.samples.SimpleTopAppBarNavIcon
*
* @param title The title to be displayed in the center of the TopAppBar
* @param color The background color for the TopAppBar. Use [Color.Transparent] to have no color.
* @param contentColor The preferred content color provided by this TopAppBar to its children.
* Defaults to either the matching `onFoo` color for [color], or if [color] is not a color from
* the theme, this will keep the same value set above this TopAppBar.
* @param elevation the elevation of this TopAppBar.
* @param navigationIcon The navigation icon displayed at the start of the TopAppBar
*/
@Composable
fun TopAppBar(
title: @Composable() () -> Unit,
color: Color = MaterialTheme.colors().primarySurface,
contentColor: Color = contentColorFor(color),
elevation: Dp = TopAppBarElevation,
navigationIcon: @Composable() (() -> Unit)? = null
) {
BaseTopAppBar(
color = color,
contentColor = contentColor,
elevation = elevation,
startContent = navigationIcon,
title = {
// Text color comes from the underlying Surface
CurrentTextStyleProvider(value = MaterialTheme.typography().h6, children = title)
},
endContent = null
)
}
/**
* A TopAppBar displays information and actions relating to the current screen and is placed at the
* top of the screen.
*
* This TopAppBar has space for a title, navigation icon, and actions. Use the other TopAppBar
* overload if you only want to display a title and navigation icon.
*
* @sample androidx.ui.material.samples.SimpleTopAppBarNavIconWithActions
*
* @param title The title to be displayed in the center of the TopAppBar
* @param color The background color for the TopAppBar. Use [Color.Transparent] to have no color.
* @param contentColor The preferred content color provided by this TopAppBar to its children.
* Defaults to either the matching `onFoo` color for [color], or if [color] is not a color from
* the theme, this will keep the same value set above this TopAppBar.
* @param elevation the elevation of this TopAppBar.
* @param navigationIcon The navigation icon displayed at the start of the TopAppBar
* @param actionData A list of data representing the 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. This list will be transformed into icons / overflow
* menu items by [action]. For example, you may choose to represent an action with a sealed class
* containing an icon and text, so you can easily handle events when the action is pressed.
* @param action A specific action that will be displayed at the end of the TopAppBar - this
* will be called for items in [actionData] up to the maximum number of icons that can be displayed.
* This parameter essentially transforms data in [actionData] to an icon / menu item that
* will actually be displayed to the user.
* @param T the type of data in [actionData]
*/
@Composable
fun <T> TopAppBar(
title: @Composable() () -> Unit,
actionData: List<T>,
color: Color = MaterialTheme.colors().primarySurface,
contentColor: Color = contentColorFor(color),
elevation: Dp = TopAppBarElevation,
navigationIcon: @Composable() (() -> Unit)? = null,
action: @Composable() (T) -> Unit
// TODO: support overflow menu here with the remainder of the list
) {
BaseTopAppBar(
color = color,
contentColor = contentColor,
elevation = elevation,
startContent = navigationIcon,
title = {
// Text color comes from the underlying Surface
CurrentTextStyleProvider(value = MaterialTheme.typography().h6, children = title)
},
endContent = getActions(actionData, MaxIconsInTopAppBar, action)
)
}
@Composable
private fun BaseTopAppBar(
color: Color,
contentColor: Color,
elevation: Dp,
startContent: @Composable() (() -> Unit)?,
title: @Composable() () -> Unit,
endContent: @Composable() (() -> Unit)?
) {
BaseAppBar(color, contentColor, elevation, RectangleShape) {
Row(arrangement = Arrangement.SpaceBetween) {
// We only want to reserve space here if we have some start content
if (startContent != null) {
Container(
modifier = LayoutHeight.Fill,
width = AppBarTitleStartPadding,
alignment = Alignment.CenterLeft,
children = startContent
)
}
// TODO(soboleva): rework this once AlignmentLineOffset is a modifier
Container(LayoutFlexible(1f) + LayoutAlign.BottomLeft) {
AlignmentLineOffset(
alignmentLine = LastBaseline,
after = with(DensityAmbient.current) { AppBarTitleBaselineOffset.toDp() }
) {
Semantics(container = true) {
// TODO: AlignmentLineOffset requires a child, so in case title() is
// empty we just add an empty wrap here - should be fixed when we move to
// modifiers.
Wrap(children = title)
}
}
}
if (endContent != null) {
Container(
modifier = LayoutHeight.Fill,
alignment = Alignment.Center,
children = endContent
)
}
}
}
}
object BottomAppBar {
/**
* Configuration for a [FloatingActionButton] in a [BottomAppBar].
*
* This is state that is usually passed down to BottomAppBar by [Scaffold] or by another
* scaffold-like component that is aware of [FloatingActionButton] existence and its size and
* position.
*
* When cutoutShape is provided in BottomAppBar, a cutout / notch will be 'carved' into the
* BottomAppBar based on FabConfiguration provided, with some extra space on all sides.
*
* If you use BottomAppBar with [Scaffold], a typical cutout for
* FAB may look like:
* @sample androidx.ui.material.samples.SimpleBottomAppBarCutoutWithScaffold
*
* @param fabSize the size of the FAB that will be shown on top of BottomAppBar
* @param fabTopLeftPosition the top left coordinate of the [FloatingActionButton] on the
* screen for BottomAppBar to carve a right cutout if desired
* @param fabDockedPosition the docked position of the [FloatingActionButton] in the
* [BottomAppBar]
*/
@Immutable
data class FabConfiguration(
internal val fabSize: IntPxSize,
internal val fabTopLeftPosition: PxPosition,
internal val fabDockedPosition: FabDockedPosition
)
/**
* The possible positions for a [FloatingActionButton] docked to a [BottomAppBar].
*
* The layout of icons within the [BottomAppBar] will depend on the chosen position of the
* [FloatingActionButton].
*/
enum class FabDockedPosition {
/**
* Positioned in the center of the [BottomAppBar]. A minimum of one and a maximum of two
* additional actions can be placed on the right side of the bar, and navigation icon will
* be placed on the left side.
*/
Center,
/**
* Positioned at the end of the [BottomAppBar]. A maximum of four additional
* actions will be shown on the left side of the bar and there should be no navigation icon.
*/
End
}
}
/**
* 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 cutout in the BottomAppBar.
*
* The location of the actions displayed by the BottomAppBar depends on the position / existence
* of a [FloatingActionButton], configured with [fabConfiguration]. When [fabConfiguration] is:
*
* - `null`: the [navigationIcon] is displayed at the start, and the actions are displayed
* at the end
*
* - not null and has [FabDockedPosition.Center] fabPosition: the [navigationIcon] is displayed at
* the start, and the actions are displayed at the end
*
* - not null and has [FabDockedPosition.End] fabPosition: the actions are displayed at the start,
* and no navigation icon is supported - setting a navigation icon here will throw an exception.
*
* See [BottomAppBar anatomy](https://material.io/components/app-bars-bottom/#anatomy) for more
* information.
*
* @sample androidx.ui.material.samples.SimpleBottomAppBarNoFab
*
* @param color The background color for the BottomAppBar. Use [Color.Transparent] to have no color.
* @param contentColor The preferred content color provided by this BottomAppBar to its children.
* Defaults to either the matching `onFoo` color for [color], or if [color] is not a color from
* the theme, this will keep the same value set above this BottomAppBar.
* @param navigationIcon The navigation icon displayed in the BottomAppBar. Note that if
* [fabConfiguration] has fabPosition [FabDockedPosition.End], this parameter must be null /
* not set.
* @param fabConfiguration The [FabConfiguration] that controls where the [FloatingActionButton]
* is placed inside the BottomAppBar. This is used both to determine the cutout position for
* BottomAppBar (if cutoutShape is non-null) and to choose proper layout for BottomAppBar. If
* null, BottomAppBar will show no cutout and no-FAB layout of icons.
* @param cutoutShape the shape of the cutout that will be added to the BottomAppBar - this
* should typically be the same shape used inside the [FloatingActionButton]. This shape will be
* drawn with an offset around all sides. If null, where will be no cutout.
* @param actionData A list of data representing the actions to be displayed at the end of
* the BottomAppBar. Any remaining actions that do not fit on the BottomAppBar should typically be
* displayed in an overflow menu at the end. This list will be transformed into icons / overflow
* menu items by [action]. For example, you may choose to represent an action with a sealed class
* containing an icon and text, so you can easily handle events when the action is pressed.
* @param action A specific action that will be displayed at the end of the BottomAppBar - this
* will be called for items in [actionData] up to the maximum number of icons that can be displayed.
* This parameter essentially transforms data in [actionData] to an icon / menu item that
* will actually be displayed to the user.
* @param T the type of data in [actionData]
*/
@Composable
fun <T> BottomAppBar(
color: Color = MaterialTheme.colors().primarySurface,
contentColor: Color = contentColorFor(color),
navigationIcon: @Composable() (() -> Unit)? = null,
fabConfiguration: FabConfiguration? = null,
cutoutShape: Shape? = null,
actionData: List<T> = emptyList(),
action: @Composable() (T) -> Unit = {}
// TODO: support overflow menu here with the remainder of the list
) {
require(
fabConfiguration == null || navigationIcon == null ||
fabConfiguration.fabDockedPosition != FabDockedPosition.End
) {
"Using a navigation icon with an end-aligned FloatingActionButton is not supported"
}
if (fabConfiguration == null) {
BaseBottomAppBar(
color = color,
contentColor = contentColor,
startContent = navigationIcon,
endContent = getActions(actionData, MaxIconsInBottomAppBarNoFab, action),
shape = RectangleShape
)
} else {
val shape =
if (cutoutShape != null) {
BottomAppBarCutoutShape(
cutoutShape,
fabConfiguration.fabTopLeftPosition,
fabConfiguration.fabSize.toPxSize()
)
} else {
RectangleShape
}
when (fabConfiguration.fabDockedPosition) {
FabDockedPosition.End -> BaseBottomAppBar(
color = color,
contentColor = contentColor,
startContent = getActions(actionData, MaxIconsInBottomAppBarEndFab, action),
endContent = null,
shape = shape
)
FabDockedPosition.Center -> BaseBottomAppBar(
color = color,
contentColor = contentColor,
startContent = navigationIcon,
endContent = getActions(actionData, MaxIconsInBottomAppBarCenterFab, action),
shape = shape
)
}
}
}
// TODO: consider exposing this in the shape package, for a generic cutout shape - might be useful
// for custom components.
/**
* A [Shape] that represents a bottom app bar with a cutout. The cutout drawn will be [cutoutShape]
* increased in size by [BottomAppBarCutoutOffset] on all sides.
*/
private data class BottomAppBarCutoutShape(
val cutoutShape: Shape,
val fabPosition: PxPosition,
val fabSize: PxSize
) : Shape {
override fun createOutline(size: PxSize, density: Density): Outline {
val boundingRectangle = Path().apply {
addRect(Rect.fromLTRB(0f, 0f, size.width.value, size.height.value))
}
val path = Path().apply {
addCutoutShape(density)
// Subtract this path from the bounding rectangle
op(boundingRectangle, this, PathOperation.difference)
}
return Outline.Generic(path)
}
/**
* Adds the filled [cutoutShape] to the [Path]. The path can the be subtracted from the main
* rectangle path used for the app bar, to create the resulting cutout shape.
*/
private fun Path.addCutoutShape(density: Density) {
// The gap on all sides between the FAB and the cutout
val cutoutOffset = with(density) { BottomAppBarCutoutOffset.toPx() }
val cutoutSize = PxSize(
width = fabSize.width + (cutoutOffset * 2),
height = fabSize.height + (cutoutOffset * 2)
)
val cutoutStartX = fabPosition.x.value - cutoutOffset.value
val cutoutEndX = cutoutStartX + cutoutSize.width.value
val cutoutRadius = cutoutSize.height.value / 2f
// Shift the cutout up by half its height, so only the bottom half of the cutout is actually
// cut into the app bar
val cutoutStartY = -cutoutRadius
addOutline(cutoutShape.createOutline(cutoutSize, density))
shift(Offset(cutoutStartX, cutoutStartY))
// TODO: consider exposing the custom cutout shape instead of just replacing circle shapes?
if (cutoutShape == CircleShape) {
val edgeRadius = with(density) { BottomAppBarRoundedEdgeRadius.toPx().value }
// TODO: possibly support providing a custom vertical offset?
addRoundedEdges(cutoutStartX, cutoutEndX, cutoutRadius, edgeRadius, 0f)
}
}
/**
* Adds rounded edges to the [Path] representing a circular cutout in a BottomAppBar.
*
* Adds a curve for the left and right edges, with a straight line drawn between them - this
* combined with the cutout shape results in the overall cutout path that can be subtracted
* from the bounding rect of the app bar.
*
* @param cutoutStartPosition the absolute start position of the cutout
* @param cutoutEndPosition the absolute end position of the cutout
* @param cutoutRadius the radius of the cutout's circular edge - for a typical circular FAB
* this will just be the radius of the circular cutout, but in the case of an extended FAB, we
* can model this as two circles on either side attached to a rectangle.
* @param roundedEdgeRadius how far from the points where the cutout intersects with the app bar
* should the rounded edges be drawn to.
* @param verticalOffset how far the app bar is from the center of the cutout circle
*/
private fun Path.addRoundedEdges(
cutoutStartPosition: Float,
cutoutEndPosition: Float,
cutoutRadius: Float,
roundedEdgeRadius: Float,
verticalOffset: Float
) {
// Where the cutout intersects with the app bar, as if the cutout is not vertically aligned
// with the app bar, the intersect will not be equal to the radius of the circle.
val appBarInterceptOffset = calculateCutoutCircleYIntercept(cutoutRadius, verticalOffset)
val appBarInterceptStartX = cutoutStartPosition + (cutoutRadius + appBarInterceptOffset)
val appBarInterceptEndX = cutoutEndPosition - (cutoutRadius + appBarInterceptOffset)
// How far the control point is away from the cutout intercept. We set this to be as small
// as possible so that we have the most 'rounded' curve.
val controlPointOffset = 1f
// How far the control point is away from the center of the radius of the cutout
val controlPointRadiusOffset = appBarInterceptOffset - controlPointOffset
// The coordinates offset from the center of the radius of the cutout, where we should
// draw the curve to
val (curveInterceptXOffset, curveInterceptYOffset) = calculateRoundedEdgeIntercept(
controlPointRadiusOffset,
verticalOffset,
cutoutRadius
)
// Convert the offset relative to the center of the cutout circle into an absolute
// coordinate, by adding the radius of the shape to get a pure relative offset from the
// leftmost edge, and then positioning it next to the cutout
val curveInterceptStartX = cutoutStartPosition + (curveInterceptXOffset + cutoutRadius)
val curveInterceptEndX = cutoutEndPosition - (curveInterceptXOffset + cutoutRadius)
// Convert the curveInterceptYOffset which is relative to the center of the cutout, to an
// absolute position
val curveInterceptY = curveInterceptYOffset - verticalOffset
// Where the rounded edge starts
val roundedEdgeStartX = appBarInterceptStartX - roundedEdgeRadius
val roundedEdgeEndX = appBarInterceptEndX + roundedEdgeRadius
moveTo(roundedEdgeStartX, 0f)
quadraticBezierTo(
appBarInterceptStartX - controlPointOffset,
0f,
curveInterceptStartX,
curveInterceptY
)
lineTo(curveInterceptEndX, curveInterceptY)
quadraticBezierTo(appBarInterceptEndX + controlPointOffset, 0f, roundedEdgeEndX, 0f)
close()
}
}
/**
* Helper to make the following equations easier to read
*/
@Suppress("NOTHING_TO_INLINE")
private inline fun square(x: Float) = x * x
/**
* Returns the relative y intercept for a circle with the given [cutoutRadius] and [verticalOffset]
*
* Returns the leftmost intercept, so this will be a negative number that when added to the circle's
* absolute origin will give the absolute position of the left intercept, where the circle meets
* the app bar.
*
* Explanation:
* First construct the equation for a circle with given radius and vertical offset:
* x^2 + (y-verticalOffset)^2 = radius^2
*
* We want to find the y intercept where the cutout hits the top edge of the bottom app bar, so
* rearrange and set y to 0:
*
* x^2 = radius^2 - (0-verticalOffset)^2
*
* We are only interested in the left most (negative x) solution as we mirror this for the right
* edge later.
*/
@Suppress("NOTHING_TO_INLINE")
internal inline fun calculateCutoutCircleYIntercept(
cutoutRadius: Float,
verticalOffset: Float
): Float {
return -sqrt(square(cutoutRadius) - square(verticalOffset))
}
// TODO: Consider extracting this into the shape package / similar, might be useful for cutouts in
// general.
/**
* For a given control point on a quadratic bezier curve, calculates the required intercept
* point to create a smooth curve between the rounded edges near the cutout, and the actual curve
* that is part of the cutout.
*
* This returns the relative offset from the center of a circle with radius that is half the
* height of the cutout.
*
* Explanation and derivation comes from the Flutter team: https://goo.gl/Ufzrqn
*
* @param controlPointX the horizontal offset of the control point from the center of the circle
* @param verticalOffset the vertical offset of the top edge of the app bar from the center of the
* circle. I.e, if this is 2f, then the top edge of the app bar is 2f below the center. If 0f, the
* top edge of the app bar is in centered inside the circle.
* @param radius the radius of the circle - essentially the 'depth' of the cutout
*/
@Suppress("UnnecessaryVariable")
internal fun calculateRoundedEdgeIntercept(
controlPointX: Float,
verticalOffset: Float,
radius: Float
): Pair<Float, Float> {
val a = controlPointX
val b = verticalOffset
val r = radius
// expands to a2b2r2 + b4r2 - b2r4
val discriminant = square(b) * square(r) * (square(a) + square(b) - square(r))
val divisor = square(a) + square(b)
// the '-b' part of the quadratic solution
val bCoefficient = a * square(r)
// Two solutions for the x coordinate relative to the midpoint of the circle
val xSolutionA = (bCoefficient - sqrt(discriminant)) / divisor
val xSolutionB = (bCoefficient + sqrt(discriminant)) / divisor
// Get y coordinate from r2 = x2 + y2 -> y2 = r2 - x2
val ySolutionA = sqrt(square(r) - square(xSolutionA))
val ySolutionB = sqrt(square(r) - square(xSolutionB))
// If the vertical offset is 0, the vertical center of the circle lines up with the top edge of
// the bottom app bar, so both solutions are identical.
// If the vertical offset is not 0, there are two distinct solutions: one that will meet in the
// top half of the circle, and one that will meet in the bottom half of the circle. As the app
// bar is always on the bottom edge of the circle, we are always interested in the bottom half
// solution. To calculate which is which, it depends on whether the vertical offset is positive
// or negative.
val (xSolution, ySolution) = if (b > 0) {
// When the offset is positive, the top edge of the app bar is below the center of the
// circle. The largest solution will be the one closest to the bottom of the circle, so we
// pick that.
if (ySolutionA > ySolutionB) xSolutionA to ySolutionA else xSolutionB to ySolutionB
} else {
// When the offset is negative, the top edge of the app bar is above the center of the
// circle. The smallest solution will be the one closest to the top of the circle, so we
// pick that.
if (ySolutionA < ySolutionB) xSolutionA to ySolutionA else xSolutionB to ySolutionB
}
// If the calculated x coordinate is further away from the origin than the control point, the
// curve will fold back on itself. In this scenario, we actually join the circle above the
// center, so invert the y coordinate.
val adjustedYSolution = if (xSolution < controlPointX) -ySolution else ySolution
return xSolution to adjustedYSolution
}
@Composable
private fun BaseBottomAppBar(
color: Color,
contentColor: Color,
shape: Shape,
startContent: @Composable() (() -> Unit)?,
endContent: @Composable() (() -> Unit)?
) {
BaseAppBar(color, contentColor, BottomAppBarElevation, shape) {
Padding(top = AppBarPadding, bottom = AppBarPadding) {
Row(LayoutSize.Fill, arrangement = Arrangement.SpaceBetween) {
// Using wrap so that even if startContent is null or emits no layout nodes,
// we will still force end content to be placed at the end of the row.
Wrap(alignment = Alignment.Center, children = startContent ?: {})
if (endContent != null) {
Wrap(alignment = Alignment.Center, children = 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,
contentColor: Color,
elevation: Dp,
shape: Shape,
children: @Composable() () -> Unit
) {
Surface(color = color, contentColor = contentColor, elevation = elevation, shape = shape) {
Container(
height = AppBarHeight,
expanded = true,
padding = EdgeInsets(left = AppBarPadding, right = AppBarPadding),
children = children
)
}
}
/**
* @return [AppBarActions] if [actionData] is not empty, else `null`
*/
private fun <T> getActions(
actionData: List<T>,
numberOfActions: Int,
action: @Composable() (T) -> Unit
): @Composable() (() -> Unit)? {
return if (actionData.isEmpty()) {
null
} else {
@Composable {
AppBarActions(numberOfActions, actionData, action)
}
}
}
@Composable
private fun <T> AppBarActions(
actionsToDisplay: Int,
actionData: List<T>,
action: @Composable() (T) -> Unit
) {
// 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) = actionData.withIndex().partition {
it.index < actionsToDisplay
}
Row {
shownActions.forEach { (index, shownAction) ->
action(shownAction)
if (index != shownActions.lastIndex) {
Spacer(LayoutWidth(24.dp))
}
}
if (overflowActions.isNotEmpty()) {
Spacer(LayoutWidth(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) {
Container(width = ActionIconDiameter, height = ActionIconDiameter) {
Ripple(bounded = false) {
Clickable(onClick = onClick) {
SimpleImage(icon)
}
}
}
}
private val ActionIconDiameter = 24.dp
private val AppBarHeight = 56.dp
private val AppBarPadding = 16.dp
private val AppBarTitleStartPadding = 72.dp - AppBarPadding
private val AppBarTitleBaselineOffset = 20.sp
private val BottomAppBarElevation = 8.dp
// TODO: clarify elevation in surface mapping - spec says 0.dp but it appears to have an
// elevation overlay applied in dark theme examples.
private val TopAppBarElevation = 4.dp
// The gap on all sides between the FAB and the cutout
private val BottomAppBarCutoutOffset = 8.dp
// How far from the notch the rounded edges start
private val BottomAppBarRoundedEdgeRadius = 4.dp
private const val MaxIconsInTopAppBar = 2
private const val MaxIconsInBottomAppBarCenterFab = 2
private const val MaxIconsInBottomAppBarEndFab = 4
private const val MaxIconsInBottomAppBarNoFab = 4