Adds BottomAppBar and updates TopAppBar
Bug: b/135743820
Test: AppBarTest
Change-Id: Icaaf9d7a137bd76cc8bcccd9b3fa33013b7c11cc
diff --git a/ui/ui-material/api/1.0.0-alpha01.txt b/ui/ui-material/api/1.0.0-alpha01.txt
index e385709..17ead5a 100644
--- a/ui/ui-material/api/1.0.0-alpha01.txt
+++ b/ui/ui-material/api/1.0.0-alpha01.txt
@@ -3,16 +3,23 @@
public final class AppBarKt {
ctor public AppBarKt();
- method public static void AppBar(androidx.ui.graphics.Color color, kotlin.jvm.functions.Function0<kotlin.Unit> children);
- method public static void AppBarLeadingIcon();
- method public static void TopAppBar(String? title = null, androidx.ui.graphics.Color color = +themeColor({
+ method public static void AppBarIcon(androidx.ui.painting.Image icon, kotlin.jvm.functions.Function0<kotlin.Unit> onClick);
+ method public static <T> void BottomAppBar(androidx.ui.graphics.Color color = +themeColor({
primary
-}), java.util.List<androidx.ui.core.Dp> icons = emptyList());
- method public static void TopAppBar(androidx.ui.graphics.Color color = +themeColor({
+}), kotlin.jvm.functions.Function0<kotlin.Unit>? navigationIcon = (kotlin.jvm.functions.Function0<? extends kotlin.Unit>)null, kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton = (kotlin.jvm.functions.Function0<? extends kotlin.Unit>)null, androidx.ui.material.BottomAppBar.FabPosition fabPosition = Center, java.util.List<? extends T>? contextualActions = null, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action = {});
+ method public static <T> void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title = {}, androidx.ui.graphics.Color color = +themeColor({
primary
-}), kotlin.jvm.functions.Function0<kotlin.Unit> leadingIcon, kotlin.jvm.functions.Function0<kotlin.Unit> titleTextLabel, kotlin.jvm.functions.Function0<kotlin.Unit> trailingIcons);
- method public static void TopAppBarTitleTextLabel(String title);
- method public static void TopAppBarTrailingIcons(java.util.List<androidx.ui.core.Dp> icons);
+}), kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon = {}, java.util.List<? extends T>? contextualActions = null, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action = {});
+ }
+
+ public final class BottomAppBar {
+ field public static final androidx.ui.material.BottomAppBar! INSTANCE;
+ }
+
+ public enum BottomAppBar.FabPosition {
+ enum_constant public static final androidx.ui.material.BottomAppBar.FabPosition Center;
+ enum_constant public static final androidx.ui.material.BottomAppBar.FabPosition CenterCut;
+ enum_constant public static final androidx.ui.material.BottomAppBar.FabPosition End;
}
public final class ButtonKt {
diff --git a/ui/ui-material/api/current.txt b/ui/ui-material/api/current.txt
index e385709..17ead5a 100644
--- a/ui/ui-material/api/current.txt
+++ b/ui/ui-material/api/current.txt
@@ -3,16 +3,23 @@
public final class AppBarKt {
ctor public AppBarKt();
- method public static void AppBar(androidx.ui.graphics.Color color, kotlin.jvm.functions.Function0<kotlin.Unit> children);
- method public static void AppBarLeadingIcon();
- method public static void TopAppBar(String? title = null, androidx.ui.graphics.Color color = +themeColor({
+ method public static void AppBarIcon(androidx.ui.painting.Image icon, kotlin.jvm.functions.Function0<kotlin.Unit> onClick);
+ method public static <T> void BottomAppBar(androidx.ui.graphics.Color color = +themeColor({
primary
-}), java.util.List<androidx.ui.core.Dp> icons = emptyList());
- method public static void TopAppBar(androidx.ui.graphics.Color color = +themeColor({
+}), kotlin.jvm.functions.Function0<kotlin.Unit>? navigationIcon = (kotlin.jvm.functions.Function0<? extends kotlin.Unit>)null, kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton = (kotlin.jvm.functions.Function0<? extends kotlin.Unit>)null, androidx.ui.material.BottomAppBar.FabPosition fabPosition = Center, java.util.List<? extends T>? contextualActions = null, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action = {});
+ method public static <T> void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title = {}, androidx.ui.graphics.Color color = +themeColor({
primary
-}), kotlin.jvm.functions.Function0<kotlin.Unit> leadingIcon, kotlin.jvm.functions.Function0<kotlin.Unit> titleTextLabel, kotlin.jvm.functions.Function0<kotlin.Unit> trailingIcons);
- method public static void TopAppBarTitleTextLabel(String title);
- method public static void TopAppBarTrailingIcons(java.util.List<androidx.ui.core.Dp> icons);
+}), kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon = {}, java.util.List<? extends T>? contextualActions = null, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action = {});
+ }
+
+ public final class BottomAppBar {
+ field public static final androidx.ui.material.BottomAppBar! INSTANCE;
+ }
+
+ public enum BottomAppBar.FabPosition {
+ enum_constant public static final androidx.ui.material.BottomAppBar.FabPosition Center;
+ enum_constant public static final androidx.ui.material.BottomAppBar.FabPosition CenterCut;
+ enum_constant public static final androidx.ui.material.BottomAppBar.FabPosition End;
}
public final class ButtonKt {
diff --git a/ui/ui-material/api/restricted_1.0.0-alpha01.txt b/ui/ui-material/api/restricted_1.0.0-alpha01.txt
index e385709..17ead5a 100644
--- a/ui/ui-material/api/restricted_1.0.0-alpha01.txt
+++ b/ui/ui-material/api/restricted_1.0.0-alpha01.txt
@@ -3,16 +3,23 @@
public final class AppBarKt {
ctor public AppBarKt();
- method public static void AppBar(androidx.ui.graphics.Color color, kotlin.jvm.functions.Function0<kotlin.Unit> children);
- method public static void AppBarLeadingIcon();
- method public static void TopAppBar(String? title = null, androidx.ui.graphics.Color color = +themeColor({
+ method public static void AppBarIcon(androidx.ui.painting.Image icon, kotlin.jvm.functions.Function0<kotlin.Unit> onClick);
+ method public static <T> void BottomAppBar(androidx.ui.graphics.Color color = +themeColor({
primary
-}), java.util.List<androidx.ui.core.Dp> icons = emptyList());
- method public static void TopAppBar(androidx.ui.graphics.Color color = +themeColor({
+}), kotlin.jvm.functions.Function0<kotlin.Unit>? navigationIcon = (kotlin.jvm.functions.Function0<? extends kotlin.Unit>)null, kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton = (kotlin.jvm.functions.Function0<? extends kotlin.Unit>)null, androidx.ui.material.BottomAppBar.FabPosition fabPosition = Center, java.util.List<? extends T>? contextualActions = null, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action = {});
+ method public static <T> void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title = {}, androidx.ui.graphics.Color color = +themeColor({
primary
-}), kotlin.jvm.functions.Function0<kotlin.Unit> leadingIcon, kotlin.jvm.functions.Function0<kotlin.Unit> titleTextLabel, kotlin.jvm.functions.Function0<kotlin.Unit> trailingIcons);
- method public static void TopAppBarTitleTextLabel(String title);
- method public static void TopAppBarTrailingIcons(java.util.List<androidx.ui.core.Dp> icons);
+}), kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon = {}, java.util.List<? extends T>? contextualActions = null, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action = {});
+ }
+
+ public final class BottomAppBar {
+ field public static final androidx.ui.material.BottomAppBar! INSTANCE;
+ }
+
+ public enum BottomAppBar.FabPosition {
+ enum_constant public static final androidx.ui.material.BottomAppBar.FabPosition Center;
+ enum_constant public static final androidx.ui.material.BottomAppBar.FabPosition CenterCut;
+ enum_constant public static final androidx.ui.material.BottomAppBar.FabPosition End;
}
public final class ButtonKt {
diff --git a/ui/ui-material/api/restricted_current.txt b/ui/ui-material/api/restricted_current.txt
index e385709..17ead5a 100644
--- a/ui/ui-material/api/restricted_current.txt
+++ b/ui/ui-material/api/restricted_current.txt
@@ -3,16 +3,23 @@
public final class AppBarKt {
ctor public AppBarKt();
- method public static void AppBar(androidx.ui.graphics.Color color, kotlin.jvm.functions.Function0<kotlin.Unit> children);
- method public static void AppBarLeadingIcon();
- method public static void TopAppBar(String? title = null, androidx.ui.graphics.Color color = +themeColor({
+ method public static void AppBarIcon(androidx.ui.painting.Image icon, kotlin.jvm.functions.Function0<kotlin.Unit> onClick);
+ method public static <T> void BottomAppBar(androidx.ui.graphics.Color color = +themeColor({
primary
-}), java.util.List<androidx.ui.core.Dp> icons = emptyList());
- method public static void TopAppBar(androidx.ui.graphics.Color color = +themeColor({
+}), kotlin.jvm.functions.Function0<kotlin.Unit>? navigationIcon = (kotlin.jvm.functions.Function0<? extends kotlin.Unit>)null, kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton = (kotlin.jvm.functions.Function0<? extends kotlin.Unit>)null, androidx.ui.material.BottomAppBar.FabPosition fabPosition = Center, java.util.List<? extends T>? contextualActions = null, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action = {});
+ method public static <T> void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title = {}, androidx.ui.graphics.Color color = +themeColor({
primary
-}), kotlin.jvm.functions.Function0<kotlin.Unit> leadingIcon, kotlin.jvm.functions.Function0<kotlin.Unit> titleTextLabel, kotlin.jvm.functions.Function0<kotlin.Unit> trailingIcons);
- method public static void TopAppBarTitleTextLabel(String title);
- method public static void TopAppBarTrailingIcons(java.util.List<androidx.ui.core.Dp> icons);
+}), kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon = {}, java.util.List<? extends T>? contextualActions = null, kotlin.jvm.functions.Function1<? super T,kotlin.Unit> action = {});
+ }
+
+ public final class BottomAppBar {
+ field public static final androidx.ui.material.BottomAppBar! INSTANCE;
+ }
+
+ public enum BottomAppBar.FabPosition {
+ enum_constant public static final androidx.ui.material.BottomAppBar.FabPosition Center;
+ enum_constant public static final androidx.ui.material.BottomAppBar.FabPosition CenterCut;
+ enum_constant public static final androidx.ui.material.BottomAppBar.FabPosition End;
}
public final class ButtonKt {
diff --git a/ui/ui-material/integration-tests/material-demos/src/main/java/androidx/ui/material/demos/AppBarActivity.kt b/ui/ui-material/integration-tests/material-demos/src/main/java/androidx/ui/material/demos/AppBarActivity.kt
index 0efb2ba..bc7ec4d 100644
--- a/ui/ui-material/integration-tests/material-demos/src/main/java/androidx/ui/material/demos/AppBarActivity.kt
+++ b/ui/ui-material/integration-tests/material-demos/src/main/java/androidx/ui/material/demos/AppBarActivity.kt
@@ -26,11 +26,15 @@
import androidx.ui.core.Text
import androidx.ui.core.dp
import androidx.ui.layout.Column
+import androidx.ui.layout.FlexColumn
import androidx.ui.layout.HeightSpacer
import androidx.ui.material.MaterialTheme
-import androidx.ui.material.TopAppBar
+import androidx.ui.material.samples.SimpleBottomAppBarCenterFab
+import androidx.ui.material.samples.SimpleBottomAppBarEndFab
+import androidx.ui.material.samples.SimpleBottomAppBarNoFab
+import androidx.ui.material.samples.SimpleTopAppBar
import androidx.ui.material.themeTextStyle
-import androidx.ui.graphics.Color
+import androidx.ui.painting.imageFromResource
class AppBarActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -38,25 +42,39 @@
setContent {
CraneWrapper {
MaterialTheme {
- AppBarDemo()
+ val favouriteImage = { imageFromResource(resources, R.drawable.ic_favorite) }
+ val navigationImage = { imageFromResource(resources, R.drawable.ic_menu) }
+ FlexColumn {
+ expanded(1f) {
+ Column {
+ SpacedText("TopAppBar")
+ HeightSpacer(height = 28.dp)
+ SimpleTopAppBar(favouriteImage, navigationImage)
+ }
+ Column {
+ SpacedText("BottomAppBar - No FAB")
+ HeightSpacer(height = 28.dp)
+ SimpleBottomAppBarNoFab(favouriteImage, navigationImage)
+ }
+ Column {
+ SpacedText("BottomAppBar - Center FAB")
+ SimpleBottomAppBarCenterFab(favouriteImage, navigationImage)
+ }
+ Column {
+ SpacedText("BottomAppBar - End FAB")
+ SimpleBottomAppBarEndFab(favouriteImage)
+ }
+ }
+ }
}
}
}
}
-}
-@Composable
-fun AppBarDemo() {
- Column {
- TopAppBar(title = "Default")
- HeightSpacer(height = 24.dp)
- TopAppBar(title = "Custom color", color = Color(0xFFE91E63.toInt()))
- HeightSpacer(height = 24.dp)
- TopAppBar(title = "Custom icons", icons = listOf(24.dp, 24.dp))
+ @Composable
+ private fun SpacedText(text: String) {
HeightSpacer(height = 12.dp)
- Text(text = "No title", style = +themeTextStyle { h6 })
- TopAppBar(icons = listOf(24.dp, 24.dp))
- HeightSpacer(height = 24.dp)
- TopAppBar(title = "Too many icons", icons = listOf(24.dp, 24.dp, 24.dp, 24.dp))
+ Text(text, style = +themeTextStyle { h6 })
+ HeightSpacer(height = 12.dp)
}
-}
\ No newline at end of file
+}
diff --git a/ui/ui-material/integration-tests/material-demos/src/main/res/drawable-hdpi/ic_menu.png b/ui/ui-material/integration-tests/material-demos/src/main/res/drawable-hdpi/ic_menu.png
new file mode 100644
index 0000000..ba90d6d
--- /dev/null
+++ b/ui/ui-material/integration-tests/material-demos/src/main/res/drawable-hdpi/ic_menu.png
Binary files differ
diff --git a/ui/ui-material/integration-tests/material-demos/src/main/res/drawable-mdpi/ic_menu.png b/ui/ui-material/integration-tests/material-demos/src/main/res/drawable-mdpi/ic_menu.png
new file mode 100644
index 0000000..05e063c
--- /dev/null
+++ b/ui/ui-material/integration-tests/material-demos/src/main/res/drawable-mdpi/ic_menu.png
Binary files differ
diff --git a/ui/ui-material/integration-tests/material-demos/src/main/res/drawable-xhdpi/ic_menu.png b/ui/ui-material/integration-tests/material-demos/src/main/res/drawable-xhdpi/ic_menu.png
new file mode 100644
index 0000000..ad9bab1
--- /dev/null
+++ b/ui/ui-material/integration-tests/material-demos/src/main/res/drawable-xhdpi/ic_menu.png
Binary files differ
diff --git a/ui/ui-material/integration-tests/material-demos/src/main/res/drawable-xxhdpi/ic_menu.png b/ui/ui-material/integration-tests/material-demos/src/main/res/drawable-xxhdpi/ic_menu.png
new file mode 100644
index 0000000..c43059e
--- /dev/null
+++ b/ui/ui-material/integration-tests/material-demos/src/main/res/drawable-xxhdpi/ic_menu.png
Binary files differ
diff --git a/ui/ui-material/integration-tests/samples/src/main/java/androidx/ui/material/samples/AppBarSamples.kt b/ui/ui-material/integration-tests/samples/src/main/java/androidx/ui/material/samples/AppBarSamples.kt
new file mode 100644
index 0000000..369ca53
--- /dev/null
+++ b/ui/ui-material/integration-tests/samples/src/main/java/androidx/ui/material/samples/AppBarSamples.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.Composable
+import androidx.compose.composer
+import androidx.ui.core.Text
+import androidx.ui.graphics.Color
+import androidx.ui.material.AppBarIcon
+import androidx.ui.material.BottomAppBar
+import androidx.ui.material.FloatingActionButton
+import androidx.ui.material.TopAppBar
+import androidx.ui.painting.Image
+
+@Suppress("UNUSED_VARIABLE")
+@Sampled
+@Composable
+fun SimpleTopAppBar(getMyActionImage: () -> Image, getMyNavigationImage: () -> Image) {
+ val someActionImage: Image = getMyActionImage()
+ val someNavigationImage: Image = getMyNavigationImage()
+
+ val navigationIcon: @Composable() () -> Unit = {
+ AppBarIcon(someNavigationImage) { /* doSomething()*/ }
+ }
+ val contextualActions = listOf("Action 1" to someActionImage, "action 2" to someActionImage)
+
+ TopAppBar(
+ title = { Text("Simple TopAppBar") },
+ navigationIcon = navigationIcon,
+ contextualActions = contextualActions
+ ) { actionData ->
+ val (actionTitle, actionImage) = actionData
+ AppBarIcon(actionImage) { /* doSomething()*/ }
+ }
+}
+
+@Suppress("UNUSED_VARIABLE")
+@Sampled
+@Composable
+fun SimpleBottomAppBarNoFab(getMyActionImage: () -> Image, getMyNavigationImage: () -> Image) {
+ val someActionImage: Image = getMyActionImage()
+ val someNavigationImage: Image = getMyNavigationImage()
+
+ val navigationIcon: @Composable() () -> Unit = {
+ AppBarIcon(someNavigationImage) { /* doSomething()*/ }
+ }
+ val contextualActions = listOf("Action 1" to someActionImage, "action 2" to someActionImage)
+
+ BottomAppBar(
+ navigationIcon = navigationIcon,
+ contextualActions = contextualActions
+ ) { actionData ->
+ val (actionTitle, actionImage) = actionData
+ AppBarIcon(actionImage) { /* doSomething()*/ }
+ }
+}
+
+@Suppress("UNUSED_VARIABLE")
+@Sampled
+@Composable
+fun SimpleBottomAppBarCenterFab(getMyActionImage: () -> Image, getMyNavigationImage: () -> Image) {
+ val someActionImage: Image = getMyActionImage()
+ val someNavigationImage: Image = getMyNavigationImage()
+
+ val navigationIcon: @Composable() () -> Unit = {
+ AppBarIcon(someNavigationImage) { /* doSomething()*/ }
+ }
+ val contextualActions = listOf("Action 1" to someActionImage, "action 2" to someActionImage)
+
+ BottomAppBar(
+ navigationIcon = navigationIcon,
+ floatingActionButton = {
+ FloatingActionButton(
+ color = Color.Black,
+ icon = someActionImage,
+ /** doSomething() */ })
+ },
+ fabPosition = BottomAppBar.FabPosition.Center,
+ contextualActions = contextualActions
+ ) { actionData ->
+ val (actionTitle, actionImage) = actionData
+ AppBarIcon(actionImage) { /* doSomething()*/ }
+ }
+}
+
+@Suppress("UNUSED_VARIABLE")
+@Sampled
+@Composable
+fun SimpleBottomAppBarEndFab(getMyActionImage: () -> Image) {
+ val someActionImage: Image = getMyActionImage()
+ val contextualActions = listOf("Action 1" to someActionImage, "action 2" to someActionImage)
+
+ BottomAppBar(
+ floatingActionButton = {
+ FloatingActionButton(
+ color = Color.Black,
+ icon = someActionImage,
+ /** doSomething() */ })
+ },
+ fabPosition = BottomAppBar.FabPosition.End,
+ contextualActions = contextualActions
+ ) { actionData ->
+ val (actionTitle, actionImage) = actionData
+ AppBarIcon(actionImage) { /* doSomething()*/ }
+ }
+}
diff --git a/ui/ui-material/src/androidTest/java/androidx/ui/material/AppBarTest.kt b/ui/ui-material/src/androidTest/java/androidx/ui/material/AppBarTest.kt
new file mode 100644
index 0000000..e33ff72
--- /dev/null
+++ b/ui/ui-material/src/androidTest/java/androidx/ui/material/AppBarTest.kt
@@ -0,0 +1,397 @@
+/*
+ * 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.test.filters.SmallTest
+import androidx.ui.core.dp
+import androidx.ui.core.withDensity
+import androidx.ui.layout.Container
+import com.google.common.truth.Truth
+import androidx.compose.unaryPlus
+import androidx.ui.core.LayoutCoordinates
+import androidx.ui.core.OnChildPositioned
+import androidx.ui.core.PxPosition
+import androidx.ui.core.Semantics
+import androidx.ui.core.Text
+import androidx.ui.core.currentTextStyle
+import androidx.ui.core.ipx
+import androidx.ui.core.toPx
+import androidx.ui.foundation.ColoredRect
+import androidx.ui.graphics.Color
+import androidx.ui.text.TextStyle
+import androidx.ui.test.assertCountEquals
+import androidx.ui.test.assertIsVisible
+import androidx.ui.test.createComposeRule
+import androidx.ui.test.findAll
+import androidx.ui.test.findByText
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+// TODO: remove when tests are uncommented
+@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
+@SmallTest
+@RunWith(JUnit4::class)
+class AppBarTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private val appBarHeight = 56.dp
+
+ @Test
+ fun topAppBar_expandsToScreen() {
+ val dm = composeTestRule.displayMetrics
+ composeTestRule
+ .setMaterialContentAndTestSizes {
+ TopAppBar<Nothing>()
+ }
+ .assertHeightEqualsTo(appBarHeight)
+ .assertWidthEqualsTo { dm.widthPixels.ipx }
+ }
+
+ @Test
+ fun topAppBar_withTitle() {
+ val title = "Title"
+ composeTestRule.setMaterialContent {
+ TopAppBar<Nothing>(title = { Text(title) })
+ }
+ findByText(title).assertIsVisible()
+ }
+
+ @Test
+ fun topAppBar_defaultPositioning() {
+ var appBarCoords: LayoutCoordinates? = null
+ var navigationIconCoords: LayoutCoordinates? = null
+ var titleCoords: LayoutCoordinates? = null
+ var actionCoords: LayoutCoordinates? = null
+ composeTestRule.setMaterialContent {
+ Container {
+ OnChildPositioned( coords ->
+ appBarCoords = coords
+ }) {
+ TopAppBar(
+ navigationIcon = {
+ OnChildPositioned( coords ->
+ navigationIconCoords = coords
+ }) {
+ FakeIcon()
+ }
+ },
+ title = {
+ OnChildPositioned( coords ->
+ titleCoords = coords
+ }) {
+ Text("title")
+ }
+ },
+ contextualActions = createImageList(1),
+ action = {
+ OnChildPositioned( coords ->
+ actionCoords = coords
+ }) { it() }
+ }
+ )
+ }
+ }
+ }
+
+ withDensity(composeTestRule.density) {
+ // Navigation icon should be at the beginning
+ val navigationIconPositionX = navigationIconCoords!!.localToGlobal(PxPosition.Origin).x
+ val navigationIconExpectedPositionX = 16.dp.toIntPx().toPx()
+ Truth.assertThat(navigationIconPositionX).isEqualTo(navigationIconExpectedPositionX)
+
+ // Title should be next
+ val titlePositionX = titleCoords!!.localToGlobal(PxPosition.Origin).x
+ val titleExpectedPositionX =
+ navigationIconPositionX + navigationIconCoords!!.size.width + 32.dp.toIntPx()
+ Truth.assertThat(titlePositionX).isEqualTo(titleExpectedPositionX)
+
+ // Action should be placed at the end
+ val actionPositionX = actionCoords!!.localToGlobal(PxPosition.Origin).x
+ val actionExpectedPositionX =
+ appBarCoords!!.size.width - 16.dp.toIntPx() - 24.dp.toIntPx()
+ Truth.assertThat(actionPositionX).isEqualTo(actionExpectedPositionX)
+ }
+ }
+
+ @Test
+ fun topAppBar_oneAction() {
+ val tag = "action"
+ val numberOfActions = 1
+ composeTestRule.setMaterialContent {
+ Container {
+ TopAppBar(
+ contextualActions = createImageList(numberOfActions),
+ action = { action ->
+ Semantics(testTag = tag) { action() }
+ }
+ )
+ }
+ }
+
+ findAll { testTag == tag }.assertCountEquals(numberOfActions)
+ }
+
+ @Test
+ fun topAppBar_fiveActions_onlyTwoShouldBeVisible() {
+ val tag = "action"
+ val numberOfActions = 5
+ val maxNumberOfActions = 2
+ composeTestRule.setMaterialContent {
+ Container {
+ TopAppBar(
+ contextualActions = createImageList(numberOfActions),
+ action = { action ->
+ Semantics(testTag = tag) { action() }
+ }
+ )
+ }
+ }
+
+ findAll { testTag == tag }.assertCountEquals(maxNumberOfActions)
+ }
+
+ @Test
+ fun topAppBar_titleDefaultStyle() {
+ var textStyle: TextStyle? = null
+ var h6Style: TextStyle? = null
+ composeTestRule.setMaterialContent {
+ Container {
+ TopAppBar<Nothing>(
+ title = {
+ textStyle = +currentTextStyle()
+ h6Style = +themeTextStyle { h6 }
+ }
+ )
+ }
+ }
+ Truth.assertThat(textStyle!!.fontSize).isEqualTo(h6Style!!.fontSize)
+ Truth.assertThat(textStyle!!.fontFamily).isEqualTo(h6Style!!.fontFamily)
+ }
+
+ @Test
+ fun bottomAppBar_expandsToScreen() {
+ val dm = composeTestRule.displayMetrics
+ composeTestRule
+ .setMaterialContentAndTestSizes {
+ BottomAppBar<Nothing>()
+ }
+ .assertHeightEqualsTo(appBarHeight)
+ .assertWidthEqualsTo { dm.widthPixels.ipx }
+ }
+
+ @Test
+ fun bottomAppBar_noFab_positioning() {
+ var appBarCoords: LayoutCoordinates? = null
+ var navigationIconCoords: LayoutCoordinates? = null
+ var actionCoords: LayoutCoordinates? = null
+ composeTestRule.setMaterialContent {
+ Container {
+ OnChildPositioned( coords ->
+ appBarCoords = coords
+ }) {
+ BottomAppBar(
+ navigationIcon = {
+ OnChildPositioned( coords ->
+ navigationIconCoords = coords
+ }) {
+ FakeIcon()
+ }
+ },
+ contextualActions = createImageList(1),
+ action = {
+ OnChildPositioned( coords ->
+ actionCoords = coords
+ }) { it() }
+ }
+ )
+ }
+ }
+ }
+
+ withDensity(composeTestRule.density) {
+ // Navigation icon should be at the beginning
+ val navigationIconPositionX = navigationIconCoords!!.localToGlobal(PxPosition.Origin).x
+ val navigationIconExpectedPositionX = 16.dp.toIntPx().toPx()
+ Truth.assertThat(navigationIconPositionX).isEqualTo(navigationIconExpectedPositionX)
+
+ // TODO: layout rounding issues here depending on the density of the device
+ /*
+ // Action should be placed at the end
+ val actionPositionX = actionCoords!!.localToGlobal(PxPosition.Origin).x
+ val actionExpectedPositionX = appBarCoords!!.size.width.round().toPx() -
+ 16.dp.toIntPx().toPx() - 24.dp.toIntPx().toPx()
+ Truth.assertThat(actionPositionX).isEqualTo(actionExpectedPositionX)
+ */
+ }
+ }
+
+ @Test
+ fun bottomAppBar_centerFab_positioning() {
+ var appBarCoords: LayoutCoordinates? = null
+ var navigationIconCoords: LayoutCoordinates? = null
+ var fabCoords: LayoutCoordinates? = null
+ var actionCoords: LayoutCoordinates? = null
+ composeTestRule.setMaterialContent {
+ Container {
+ OnChildPositioned( coords ->
+ appBarCoords = coords
+ }) {
+ BottomAppBar(
+ navigationIcon = {
+ OnChildPositioned( coords ->
+ navigationIconCoords = coords
+ }) {
+ FakeIcon()
+ }
+ },
+ floatingActionButton = {
+ OnChildPositioned( coords ->
+ fabCoords = coords
+ }) {
+ FakeIcon()
+ }
+ },
+ contextualActions = createImageList(1),
+ action = {
+ OnChildPositioned( coords ->
+ actionCoords = coords
+ }) { it() }
+ }
+ )
+ }
+ }
+ }
+
+ withDensity(composeTestRule.density) {
+ // Navigation icon should be at the beginning
+ val navigationIconPositionX = navigationIconCoords!!.localToGlobal(PxPosition.Origin).x
+ val navigationIconExpectedPositionX = 16.dp.toIntPx().toPx()
+ Truth.assertThat(navigationIconPositionX).isEqualTo(navigationIconExpectedPositionX)
+
+ // TODO: layout rounding issues here depending on the density of the device
+ /*
+ // FAB should be placed in the center
+ val fabPositionX = fabCoords!!.localToGlobal(PxPosition.Origin).x
+ val fabExpectedPositionX = (appBarCoords!!.size.width / 2) - 12.dp.toIntPx()
+ Truth.assertThat(fabPositionX).isEqualTo(fabExpectedPositionX)
+
+ // Action should be placed at the end
+ val actionPositionX = actionCoords!!.localToGlobal(PxPosition.Origin).x
+ val actionExpectedPositionX = appBarCoords!!.size.width.round().toPx() -
+ 16.dp.toIntPx().toPx() - 24.dp.toIntPx().toPx()
+ Truth.assertThat(actionPositionX).isEqualTo(actionExpectedPositionX)
+ */
+ }
+ }
+
+ @Test
+ fun bottomAppBar_endFab_positioning() {
+ var appBarCoords: LayoutCoordinates? = null
+ var fabCoords: LayoutCoordinates? = null
+ var actionCoords: LayoutCoordinates? = null
+ composeTestRule.setMaterialContent {
+ Container {
+ OnChildPositioned( coords ->
+ appBarCoords = coords
+ }) {
+ BottomAppBar(
+ floatingActionButton = {
+ OnChildPositioned( coords ->
+ fabCoords = coords
+ }) {
+ FakeIcon()
+ }
+ },
+ fabPosition = BottomAppBar.FabPosition.End,
+ contextualActions = createImageList(1),
+ action = {
+ OnChildPositioned( coords ->
+ actionCoords = coords
+ }) { it() }
+ }
+ )
+ }
+ }
+ }
+
+ withDensity(composeTestRule.density) {
+ // Action should be placed at the start
+ val actionPositionX = actionCoords!!.localToGlobal(PxPosition.Origin).x
+ val actionExpectedPositionX = 16.dp.toIntPx().toPx()
+ Truth.assertThat(actionPositionX).isEqualTo(actionExpectedPositionX)
+
+ // TODO: layout rounding issues here depending on the density of the device
+ /*
+ // FAB should be placed at the end
+ val fabPositionX = fabCoords!!.localToGlobal(PxPosition.Origin).x
+ val fabExpectedPositionX = appBarCoords!!.size.width.round().toPx() -
+ 16.dp.toIntPx().toPx() - 24.dp.toIntPx().toPx()
+ Truth.assertThat(fabPositionX).isEqualTo(fabExpectedPositionX)
+ */
+ }
+ }
+
+ @Test
+ fun bottomAppBar_oneAction() {
+ val tag = "action"
+ val numberOfActions = 1
+ composeTestRule.setMaterialContent {
+ Container {
+ BottomAppBar(
+ contextualActions = createImageList(numberOfActions),
+ action = { action ->
+ Semantics(testTag = tag) { action() }
+ }
+ )
+ }
+ }
+
+ findAll { testTag == tag }.assertCountEquals(numberOfActions)
+ }
+
+ @Test
+ fun bottomAppBar_fiveActions_onlyFourShouldBeVisible() {
+ val tag = "action"
+ val numberOfActions = 5
+ val maxNumberOfActions = 4
+ composeTestRule.setMaterialContent {
+ Container {
+ BottomAppBar(
+ contextualActions = createImageList(numberOfActions),
+ action = { action ->
+ Semantics(testTag = tag) { action() }
+ }
+ )
+ }
+ }
+
+ findAll { testTag == tag }.assertCountEquals(maxNumberOfActions)
+ }
+
+ private fun createImageList(count: Int) =
+ List<@Composable() () -> Unit>(count) { { FakeIcon() } }
+
+ // Render a red rectangle to simulate an icon
+ @Composable
+ private fun FakeIcon() = ColoredRect(Color.Red, 24.dp, 24.dp)
+}
diff --git a/ui/ui-material/src/androidTest/java/androidx/ui/material/TopAppBarUiTest.kt b/ui/ui-material/src/androidTest/java/androidx/ui/material/TopAppBarUiTest.kt
deleted file mode 100644
index 66e3c40..0000000
--- a/ui/ui-material/src/androidTest/java/androidx/ui/material/TopAppBarUiTest.kt
+++ /dev/null
@@ -1,266 +0,0 @@
-/*
- * 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.test.filters.SmallTest
-import androidx.ui.core.OnChildPositioned
-import androidx.ui.core.dp
-import androidx.ui.core.round
-import androidx.ui.core.withDensity
-import androidx.ui.layout.Container
-import com.google.common.truth.Truth
-import androidx.compose.composer
-import androidx.compose.unaryPlus
-import androidx.ui.core.LayoutCoordinates
-import androidx.ui.core.currentTextStyle
-import androidx.ui.core.ipx
-import androidx.ui.text.TextStyle
-import androidx.ui.test.assertCountEquals
-import androidx.ui.test.assertDoesNotExist
-import androidx.ui.test.assertIsVisible
-import androidx.ui.test.createComposeRule
-import androidx.ui.test.findAll
-import androidx.ui.test.findByTag
-import androidx.ui.test.findByText
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@SmallTest
-@RunWith(JUnit4::class)
-class TopAppBarUiTest {
-
- @get:Rule
- val composeTestRule = createComposeRule()
-
- private val defaultHeight = 56.dp
-
- @Test
- fun topAppBarTest_ExpandsToScreen() {
- val dm = composeTestRule.displayMetrics
- composeTestRule
- .setMaterialContentAndTestSizes {
- TopAppBar()
- }
- .assertHeightEqualsTo(defaultHeight)
- .assertWidthEqualsTo { dm.widthPixels.ipx }
- }
-
- @Test
- fun topAppBarTest_LeadingIconPresent() {
- composeTestRule.setMaterialContent {
- TopAppBar()
- }
- findByTag("Leading icon").assertIsVisible()
- }
-
- @Test
- fun topAppBarTest_TitleTextLabel_noTitle() {
- composeTestRule.setMaterialContent {
- TopAppBar()
- }
- assertDoesNotExist { testTag == "Title" }
- }
-
- @Test
- fun topAppBarTest_TitleTextLabel_withTitle() {
- val title = "Title"
- composeTestRule.setMaterialContent {
- TopAppBar(title = title)
- }
- findByText(title).assertIsVisible()
- }
-
- @Test
- fun topAppBar_defaultPositioning() {
- var leadingIconInfo: LayoutCoordinates? = null
- var titleLabelInfo: LayoutCoordinates? = null
- var trailingIconInfo: LayoutCoordinates? = null
- composeTestRule.setMaterialContent {
- Container {
- TopAppBar(
- leadingIcon = {
- OnChildPositioned( position ->
- leadingIconInfo = position
- }) {
- AppBarLeadingIcon()
- }
- },
- titleTextLabel = {
- OnChildPositioned( position ->
- titleLabelInfo = position
- }) {
- TopAppBarTitleTextLabel("title")
- }
- },
- trailingIcons = {
- OnChildPositioned( position ->
- trailingIconInfo = position
- }) {
- TopAppBarTrailingIcons(listOf(24.dp, 24.dp))
- }
- }
- )
- }
- }
-
- withDensity(composeTestRule.density) {
- val dm = composeTestRule.displayMetrics
-
- // Leading icon should be in the front
- val leadingIconPositionX = leadingIconInfo?.position?.x!!.value
- val leadingIconExpectedPositionX = 0f
- Truth.assertThat(leadingIconPositionX).isEqualTo(leadingIconExpectedPositionX)
-
- // Title should be next
- val titleLabelPositionX = titleLabelInfo?.position?.x!!
- val titleLabelExpectedPositionX = leadingIconInfo?.size?.width!! + 32.dp.toIntPx()
- Truth.assertThat(titleLabelPositionX).isEqualTo(titleLabelExpectedPositionX)
-
- // Trailing icons should be in the second half of the screen
- val trailingIconPositionX = trailingIconInfo?.position?.x!!
- val totalSpaceMiddle = dm.widthPixels / 2f
- Truth.assertThat(trailingIconPositionX.value).isGreaterThan(totalSpaceMiddle)
- }
- }
-
- @Test
- fun topAppBar_noTitlePositioning() {
- var leadingIconInfo: LayoutCoordinates? = null
- var trailingIconInfo: LayoutCoordinates? = null
- composeTestRule.setMaterialContent {
- Container {
- TopAppBar(
- leadingIcon = {
- OnChildPositioned( position ->
- leadingIconInfo = position
- }) {
- AppBarLeadingIcon()
- }
- },
- titleTextLabel = {},
- trailingIcons = {
- OnChildPositioned( position ->
- trailingIconInfo = position
- }) {
- TopAppBarTrailingIcons(listOf(24.dp, 24.dp))
- }
- }
- )
- }
- }
-
- withDensity(composeTestRule.density) {
- val dm = composeTestRule.displayMetrics
-
- // Leading icon should be in the front
- val leadingIconPositionX = leadingIconInfo?.position?.x!!.value
- val leadingIconExpectedPositionX = 0f
- Truth.assertThat(leadingIconPositionX).isEqualTo(leadingIconExpectedPositionX)
-
- // Trailing icons should be in the second half of the screen
- val trailingIconPositionX = trailingIconInfo?.position?.x!!
- val totalSpaceMiddle = dm.widthPixels / 2f
- Truth.assertThat(trailingIconPositionX.value).isGreaterThan(totalSpaceMiddle)
- }
- }
-
- @Test
- fun topAppBar_titleDefaultStyle() {
- var textStyle: TextStyle? = null
- var h6Style: TextStyle? = null
- composeTestRule.setMaterialContent {
- Container {
- TopAppBar(
- leadingIcon = {},
- titleTextLabel = {
- textStyle = +currentTextStyle()
- h6Style = +themeTextStyle { h6 }
- },
- trailingIcons = {}
- )
- }
- }
- Truth.assertThat(textStyle!!.fontSize).isEqualTo(h6Style!!.fontSize)
- Truth.assertThat(textStyle!!.fontFamily).isEqualTo(h6Style!!.fontFamily)
- }
-
- @Test
- fun topAppBarTrailingIcons_noIcons() {
- var trailingIconInfo: LayoutCoordinates? = null
- composeTestRule.setMaterialContent {
- Container {
- OnChildPositioned( position ->
- trailingIconInfo = position
- }) {
- TopAppBarTrailingIcons(emptyList())
- }
- }
- }
- assertDoesNotExist { testTag == "Trailing icon" }
-
- withDensity(composeTestRule.density) {
- val trailingIconWidth = trailingIconInfo?.size?.width?.round()
- Truth.assertThat(trailingIconWidth).isNull()
- }
- }
-
- @Test
- fun topAppBarTrailingIcons_oneIcon() {
- composeTestRule
- .setMaterialContentAndTestSizes {
- TopAppBarTrailingIcons(icons = listOf(24.dp))
- }
- .assertWidthEqualsTo { 24.dp.toIntPx() + 24.dp.toIntPx() }
-
- findByTag("Trailing icon").assertIsVisible()
- assertDoesNotExist { testTag == "Overflow icon" }
- }
-
- @Test
- fun topAppBarTrailingIcons_twoIcons() {
-
- composeTestRule.setMaterialContentAndTestSizes {
- TopAppBarTrailingIcons(icons = listOf(24.dp, 24.dp))
- }.assertWidthEqualsTo { (24.dp.toIntPx() * 2) + (24.dp.toIntPx() * 2) }
-
- findAll { testTag == "Trailing icon" }.apply {
- forEach {
- it.assertIsVisible()
- }
- }.assertCountEquals(2)
- assertDoesNotExist { testTag == "Overflow icon" }
- }
-
- @Test
- fun topAppBarTrailingIcons_threeIcons() {
- composeTestRule
- .setMaterialContentAndTestSizes {
- TopAppBarTrailingIcons(icons = listOf(24.dp, 24.dp, 24.dp))
- }
- .assertWidthEqualsTo { 24.dp.toIntPx() * 2 + 24.dp.toIntPx() * 3 + 12.dp.toIntPx() }
- // icons and spacers + 12.dp overflow
-
- findAll { testTag == "Trailing icon" }.apply {
- forEach {
- it.assertIsVisible()
- }
- }.assertCountEquals(2)
- findByTag("Overflow icon").assertIsVisible()
- }
-}
\ No newline at end of file
diff --git a/ui/ui-material/src/main/java/androidx/ui/material/AppBar.kt b/ui/ui-material/src/main/java/androidx/ui/material/AppBar.kt
index ecb7dde..3996bd3 100644
--- a/ui/ui-material/src/main/java/androidx/ui/material/AppBar.kt
+++ b/ui/ui-material/src/main/java/androidx/ui/material/AppBar.kt
@@ -16,16 +16,16 @@
package androidx.ui.material
-import androidx.compose.Children
import androidx.compose.Composable
import androidx.compose.composer
import androidx.compose.unaryPlus
import androidx.ui.core.CurrentTextStyleProvider
-import androidx.ui.core.Dp
import androidx.ui.core.Semantics
import androidx.ui.core.Text
import androidx.ui.core.dp
-import androidx.ui.foundation.ColoredRect
+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
@@ -33,90 +33,257 @@
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.FlexSize
+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 Top App Bar displays information and actions relating to the current screen and is placed at
- * the top of the screen.
- * This version of the TopAppBar will produce a default bar with a navigation leading icon, an
- * optional title and a set of menu icons.
+ * A TopAppBar displays information and actions relating to the current screen and is placed at the
+ * top of the screen.
*
- * Example usage:
- * TopAppBar(
- * title = "Title",
- * color = +themeColor{ secondary }
- * )
+ * @sample androidx.ui.material.samples.SimpleTopAppBar
*
- * @param title An optional title to display
- * @param color An optional color for the App Bar. By default [MaterialColors.primary] will be used.
- * @param icons An optional list of icons to display on the App Bar.
+ * @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 TopAppBar(
- title: String? = null,
+fun <T> TopAppBar(
+ title: @Composable() () -> Unit = {},
color: Color = +themeColor { primary },
- // TODO: work on menus
- icons: List<Dp> = emptyList()
+ navigationIcon: @Composable() () -> Unit = {},
+ contextualActions: List<T>? = null,
+ action: @Composable() (T) -> Unit = {}
+ // TODO: support overflow menu here with the remainder of the list
) {
- TopAppBar(
+ BaseTopAppBar(
color = color,
- leadingIcon = { AppBarLeadingIcon() },
- titleTextLabel = {
- if (title != null) {
- TopAppBarTitleTextLabel(title)
+ startContent = navigationIcon,
+ title = title,
+ endContent = {
+ if (contextualActions != null) {
+ AppBarActions(MaxIconsInTopAppBar, contextualActions, action)
}
- },
- trailingIcons = { TopAppBarTrailingIcons(icons) }
+ }
)
}
-/**
- * A Top App Bar displays information and actions relating to the current screen and is placed at
- * the top of the screen.
- *
- * Example usage:
- * TopAppBar(
- * color = +themeColor{ secondary },
- * leadingIcon = { MyNavIcon() },
- * titleTextLabel = { Text(text = "Title") },
- * trailingIcons = { TopAppBarTrailingIcons(icons) }
- * )
- *
- * @param color An optional color for the App Bar. By default [MaterialColors.primary] will be used.
- * @param leadingIcon A composable lambda to be inserted in the Leading Icon space. This is usually
- * a navigation icon. A standard implementation is provided by [AppBarLeadingIcon].
- * @param titleTextLabel A composable lambda to be inserted in the title space. This is usually a
- * [Text] element. A standard implementation is provided by [TopAppBarTitleTextLabel]. Default text
- * styling [MaterialTypography.h6] will be used.
- * @param trailingIcons A composable lambda to be inserted at the end of the bar, usually containing
- * a collection of menu icons. A standard implementation is provided by [TopAppBarTrailingIcons].
- */
@Composable
-fun TopAppBar(
+private fun BaseTopAppBar(
color: Color = +themeColor { primary },
- leadingIcon: @Composable() () -> Unit,
- titleTextLabel: @Composable() () -> Unit,
- trailingIcons: @Composable() () -> Unit
+ startContent: @Composable() () -> Unit,
+ title: @Composable() () -> Unit,
+ endContent: @Composable() () -> Unit
) {
- AppBar(color) {
+ BaseAppBar(color) {
FlexRow(mainAxisAlignment = MainAxisAlignment.SpaceBetween) {
inflexible {
- leadingIcon()
+ // TODO: what should the spacing be when there is no icon provided here?
+ startContent()
WidthSpacer(width = 32.dp)
}
- expanded(flex = 1f) {
+ expanded(1f) {
CurrentTextStyleProvider(value = +themeTextStyle { h6 }) {
- titleTextLabel()
+ title()
}
}
inflexible {
- trailingIcons()
+ 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.
*
@@ -124,103 +291,78 @@
* [TopAppBar].
*/
@Composable
-fun AppBar(color: Color, @Children children: @Composable() () -> Unit) {
+private fun BaseAppBar(color: Color, children: @Composable() () -> Unit) {
Semantics(
container = true
) {
Surface(color = color) {
- Container(height = RegularHeight, expanded = true, padding = EdgeInsets(Padding)) {
+ Container(height = AppBarHeight, expanded = true, padding = EdgeInsets(AppBarPadding)) {
children()
}
}
}
}
-/**
- * A component that displays a leading icon for an App Bar following Material spec guidelines.
- *
- * @see [AppBar]
- * @see [TopAppBar]
- */
@Composable
-fun AppBarLeadingIcon() {
- // TODO: Replace with real icon button
- Semantics(testTag = "Leading icon") {
- FakeIcon(24.dp)
- }
-}
-
-/**
- * A component that displays a title as a [Text] element for placement within a Top App Bar
- * following Material spec guidelines.
- *
- * @see [TopAppBar]
- *
- * @param title A title String to display
- */
-@Composable
-fun TopAppBarTitleTextLabel(title: String) {
- Text(text = title)
-}
-
-/**
- * A component that displays a set of menu icons for placement within a Top App Bar following
- * Material spec guidelines.
- *
- * @see [TopAppBar]
- *
- * @param icons A list of icons to display
- */
-@Composable
-fun TopAppBarTrailingIcons(icons: List<Dp>) {
- TrailingIcons(
- numIcons = icons.size,
- maxIcons = MaxIconsInTopAppBar,
- icons = { index ->
- Semantics(testTag = "Trailing icon") {
- // TODO: Replace with real icon button
- FakeIcon(icons[index])
- }
- },
- overflowIcon = {
- Semantics(testTag = "Overflow icon") {
- FakeIcon(12.dp)
- }
- }
- )
-}
-
-// TODO: make public
-@Composable
-internal fun TrailingIcons(
- numIcons: Int,
- maxIcons: Int,
- icons: @Composable() (index: Int) -> Unit,
- overflowIcon: @Composable() () -> Unit
+private fun <T> AppBarActions(
+ actionsToDisplay: Int,
+ contextualActions: List<T>,
+ action: @Composable() (T) -> Unit
) {
- if (numIcons > 0) {
- Row(mainAxisSize = FlexSize.Min) {
- val needsOverflow = numIcons > maxIcons
- val iconsToDisplay = if (needsOverflow) maxIcons else numIcons
- for (index in 0 until iconsToDisplay) {
+ 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)
- icons(index)
}
- if (needsOverflow) {
- WidthSpacer(width = 24.dp)
- overflowIcon()
+ }
+ 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))
}
}
}
}
-// TODO: remove
+/**
+ * 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
-internal fun FakeIcon(size: Dp) {
- ColoredRect(color = Color(0xFFFFFFFF.toInt()), width = size, height = 24.dp)
+fun AppBarIcon(icon: Image, onClick: () -> Unit) {
+ Ripple(bounded = false) {
+ Clickable( {
+ Center {
+ Container(width = ActionIconDiameter, height = ActionIconDiameter) {
+ SimpleImage(icon)
+ }
+ }
+ }
+ }
}
-private val RegularHeight = 56.dp
-private val Padding = 16.dp
-// TODO: IR compiler bug avoids this being const
-private val MaxIconsInTopAppBar = 2
\ No newline at end of file
+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
\ No newline at end of file