Merge "Added IconTest and IconButtonTest." into androidx-main
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index 0d1dc6f..c0b208a 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -37,11 +37,14 @@
* corresponding block below
*/
implementation(libs.kotlinStdlibCommon)
+ implementation(project(":compose:foundation:foundation-layout"))
- api("androidx.compose.foundation:foundation:1.0.1")
+ api(project(":compose:foundation:foundation"))
+ api("androidx.compose.material:material-icons-core:1.0.2")
api("androidx.compose.material:material-ripple:1.0.0")
api("androidx.compose.runtime:runtime:1.0.1")
api("androidx.compose.ui:ui-graphics:1.0.1")
+ api(project(":compose:ui:ui"))
api("androidx.compose.ui:ui-text:1.0.1")
testImplementation(libs.testRules)
@@ -49,8 +52,7 @@
testImplementation(libs.junit)
testImplementation(libs.truth)
- // TODO: Enable when Material 3 samples are in place.
- // implementation(project(":compose:material3:material3:material3-samples"))
+ androidTestImplementation(project(":compose:material3:material3:material3-samples"))
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":test:screenshot:screenshot"))
androidTestImplementation(libs.testRules)
@@ -77,6 +79,7 @@
implementation(libs.kotlinStdlibCommon)
api(project(":compose:foundation:foundation"))
+ api(project(":compose:material:material-icons-core"))
api(project(":compose:material:material-ripple"))
api(project(":compose:runtime:runtime"))
api(project(":compose:ui:ui-graphics"))
@@ -99,8 +102,7 @@
}
androidAndroidTest.dependencies {
- // TODO: Enable when Material 3 samples are in place.
- // implementation(project(":compose:material3:material3:material3-samples"))
+ implementation(project(":compose:material3:material3:material3-samples"))
implementation(project(":compose:test-utils"))
implementation(project(":test:screenshot:screenshot"))
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/IconButtonSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/IconButtonSamples.kt
new file mode 100644
index 0000000..5a724ca
--- /dev/null
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/IconButtonSamples.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 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.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconToggleButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.graphics.Color
+
+@Sampled
+@Composable
+fun IconButtonSample() {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+ }
+}
+
+@Sampled
+@Composable
+fun IconToggleButtonSample() {
+ var checked by remember { mutableStateOf(false) }
+
+ IconToggleButton(checked = checked, onCheckedChange = { checked = it }) {
+ val tint by animateColorAsState(if (checked) Color(0xFFEC407A) else Color(0xFFB0BEC5))
+ Icon(Icons.Filled.Favorite, contentDescription = "Localized description", tint = tint)
+ }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt
new file mode 100644
index 0000000..4b15249
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconButtonTest.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2021 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
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.samples.IconButtonSample
+import androidx.compose.material3.samples.IconToggleButtonSample
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertIsOff
+import androidx.compose.ui.test.assertIsOn
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTouchHeightIsEqualTo
+import androidx.compose.ui.test.assertTouchWidthIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.click
+import androidx.compose.ui.test.hasClickAction
+import androidx.compose.ui.test.isToggleable
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+/**
+ * Test for [IconButton] and [IconToggleButton].
+ */
+class IconButtonTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun iconButton_size() {
+ val width = 48.dp
+ val height = 48.dp
+ rule
+ .setMaterialContentForSizeAssertions {
+ IconButtonSample()
+ }
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun iconButton_defaultSemantics() {
+ rule.setMaterialContent {
+ IconButtonSample()
+ }
+ rule.onNode(hasClickAction()).apply {
+ assertIsEnabled()
+ }
+ }
+
+ @Test
+ fun iconButton_disabledSemantics() {
+ rule.setMaterialContent {
+ IconButton(onClick = {}, enabled = false) {}
+ }
+ rule.onNode(hasClickAction()).apply {
+ assertIsNotEnabled()
+ }
+ }
+
+ @Test
+ fun iconButton_materialIconSize_iconPositioning() {
+ val diameter = 24.dp
+ rule.setMaterialContent {
+ Box {
+ IconButton(onClick = {}) {
+ Box(Modifier.size(diameter).testTag("icon"))
+ }
+ }
+ }
+
+ // Icon should be centered inside the IconButton
+ rule.onNodeWithTag("icon", useUnmergedTree = true)
+ .assertLeftPositionInRootIsEqualTo(24.dp / 2)
+ .assertTopPositionInRootIsEqualTo(24.dp / 2)
+ }
+
+ @Test
+ fun iconButton_customIconSize_iconPositioning() {
+ val width = 36.dp
+ val height = 14.dp
+ rule.setMaterialContent {
+ Box {
+ IconButton(onClick = {}) {
+ Box(Modifier.size(width, height).testTag("icon"))
+ }
+ }
+ }
+
+ // Icon should be centered inside the IconButton
+ rule.onNodeWithTag("icon", useUnmergedTree = true)
+ .assertLeftPositionInRootIsEqualTo((48.dp - width) / 2)
+ .assertTopPositionInRootIsEqualTo((48.dp - height) / 2)
+ }
+
+ @Test
+ fun iconToggleButton_size() {
+ val width = 48.dp
+ val height = 48.dp
+ rule
+ .setMaterialContentForSizeAssertions {
+ IconToggleButtonSample()
+ }
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun iconToggleButton_defaultSemantics() {
+ rule.setMaterialContent {
+ IconToggleButtonSample()
+ }
+ rule.onNode(isToggleable()).apply {
+ assertIsEnabled()
+ assertIsOff()
+ performClick()
+ assertIsOn()
+ }
+ }
+
+ @Test
+ fun iconToggleButton_disabledSemantics() {
+ rule.setMaterialContent {
+ IconToggleButton(checked = false, onCheckedChange = {}, enabled = false) {}
+ }
+ rule.onNode(isToggleable()).apply {
+ assertIsNotEnabled()
+ assertIsOff()
+ }
+ }
+
+ @Test
+ fun iconToggleButton_materialIconSize_iconPositioning() {
+ val diameter = 24.dp
+ rule.setMaterialContent {
+ Box {
+ IconToggleButton(checked = false, onCheckedChange = {}) {
+ Box(Modifier.size(diameter).testTag("icon"))
+ }
+ }
+ }
+
+ // Icon should be centered inside the IconButton
+ rule.onNodeWithTag("icon", useUnmergedTree = true)
+ .assertLeftPositionInRootIsEqualTo(24.dp / 2)
+ .assertTopPositionInRootIsEqualTo(24.dp / 2)
+ }
+
+ @Test
+ fun iconToggleButton_customIconSize_iconPositioning() {
+ val width = 36.dp
+ val height = 14.dp
+ rule.setMaterialContent {
+ Box {
+ IconToggleButton(checked = false, onCheckedChange = {}) {
+ Box(Modifier.size(width, height).testTag("icon"))
+ }
+ }
+ }
+
+ // Icon should be centered inside the IconButton
+ rule.onNodeWithTag("icon", useUnmergedTree = true)
+ .assertLeftPositionInRootIsEqualTo((48.dp - width) / 2)
+ .assertTopPositionInRootIsEqualTo((48.dp - height) / 2)
+ }
+
+ @Test
+ fun iconToggleButton_clickInMinimumTouchTarget(): Unit = with(rule.density) {
+ val tag = "iconToggleButton"
+ var checked by mutableStateOf(false)
+ rule.setMaterialContent {
+ // Box is needed because otherwise the control will be expanded to fill its parent
+ Box(Modifier.fillMaxSize()) {
+ IconToggleButton(
+ checked = checked,
+ onCheckedChange = { checked = it },
+ modifier = Modifier.align(Alignment.Center).requiredSize(2.dp).testTag(tag)
+ ) {
+ Box(Modifier.size(2.dp))
+ }
+ }
+ }
+ rule.onNodeWithTag(tag)
+ .assertIsOff()
+ .assertWidthIsEqualTo(2.dp)
+ .assertHeightIsEqualTo(2.dp)
+ .assertTouchWidthIsEqualTo(48.dp)
+ .assertTouchHeightIsEqualTo(48.dp)
+ .performTouchInput {
+ click(position = Offset(-1f, -1f))
+ }.assertIsOn()
+ }
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt
new file mode 100644
index 0000000..ef8ae3e
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/IconTest.kt
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2020 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
+
+import android.os.Build
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertContentDescriptionEquals
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class IconTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun vector_materialIconSize_dimensions() {
+ val width = 24.dp
+ val height = 24.dp
+ val vector = Icons.Filled.Menu
+ rule
+ .setMaterialContentForSizeAssertions {
+ Icon(vector, null)
+ }
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun vector_customIconSize_dimensions() {
+ val width = 35.dp
+ val height = 83.dp
+ val vector = ImageVector.Builder(
+ defaultWidth = width, defaultHeight = height,
+ viewportWidth = width.value, viewportHeight = height.value
+ ).build()
+ rule
+ .setMaterialContentForSizeAssertions {
+ Icon(vector, null)
+ }
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun image_noIntrinsicSize_dimensions() {
+ val width = 24.dp
+ val height = 24.dp
+ rule
+ .setMaterialContentForSizeAssertions {
+ val image = with(LocalDensity.current) {
+ ImageBitmap(width.roundToPx(), height.roundToPx())
+ }
+
+ Icon(image, null)
+ }
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun image_withIntrinsicSize_dimensions() {
+ val width = 35.dp
+ val height = 83.dp
+
+ rule
+ .setMaterialContentForSizeAssertions {
+ val image = with(LocalDensity.current) {
+ ImageBitmap(width.roundToPx(), height.roundToPx())
+ }
+
+ Icon(image, null)
+ }
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun painter_noIntrinsicSize_dimensions() {
+ val width = 24.dp
+ val height = 24.dp
+ val painter = ColorPainter(Color.Red)
+ rule
+ .setMaterialContentForSizeAssertions {
+ Icon(painter, null)
+ }
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun painter_withIntrinsicSize_dimensions() {
+ val width = 35.dp
+ val height = 83.dp
+
+ rule
+ .setMaterialContentForSizeAssertions {
+ val image = with(LocalDensity.current) {
+ ImageBitmap(width.roundToPx(), height.roundToPx())
+ }
+
+ val bitmapPainter = BitmapPainter(image)
+ Icon(bitmapPainter, null)
+ }
+ .assertWidthIsEqualTo(width)
+ .assertHeightIsEqualTo(height)
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun iconScalesToFitSize() {
+ // Image with intrinsic size of 24dp
+ val width = 24.dp
+ val height = 24.dp
+ val testTag = "testTag"
+ var expectedIntSize: IntSize? = null
+ rule.setMaterialContent {
+ val image: ImageBitmap
+ with(LocalDensity.current) {
+ image = createBitmapWithColor(
+ this,
+ width.roundToPx(),
+ height.roundToPx(),
+ Color.Red
+ )
+ }
+ Icon(
+ image,
+ null,
+ // Force Icon to be 50dp
+ modifier = Modifier.requiredSize(50.dp).testTag(testTag),
+ tint = Color.Unspecified
+ )
+ with(LocalDensity.current) {
+ val dimension = 50.dp.roundToPx()
+ expectedIntSize = IntSize(dimension, dimension)
+ }
+ }
+
+ rule.onNodeWithTag(testTag)
+ .captureToImage()
+ // The icon should be 50x50 and fill the whole size with red pixels
+ .assertPixels(expectedSize = expectedIntSize!!) {
+ Color.Red
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun iconUnspecifiedTintColorIgnored() {
+ val width = 35.dp
+ val height = 83.dp
+ val testTag = "testTag"
+ rule.setMaterialContent {
+ val image: ImageBitmap
+ with(LocalDensity.current) {
+ image = createBitmapWithColor(
+ this,
+ width.roundToPx(),
+ height.roundToPx(),
+ Color.Red
+ )
+ }
+ Icon(image, null, modifier = Modifier.testTag(testTag), tint = Color.Unspecified)
+ }
+
+ // With no color provided for a tint, the icon should render the original pixels
+ rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Red }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun iconSpecifiedTintColorApplied() {
+ val width = 35.dp
+ val height = 83.dp
+ val testTag = "testTag"
+ rule.setMaterialContent {
+ val image: ImageBitmap
+ with(LocalDensity.current) {
+ image = createBitmapWithColor(
+ this,
+ width.roundToPx(),
+ height.roundToPx(),
+ Color.Red
+ )
+ }
+ Icon(image, null, modifier = Modifier.testTag(testTag), tint = Color.Blue)
+ }
+
+ // With a tint color provided, all pixels should be blue
+ rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue }
+ }
+
+ @Test
+ fun defaultSemanticsWhenContentDescriptionProvided() {
+ val testTag = "TestTag"
+ rule.setContent {
+ Icon(
+ bitmap = ImageBitmap(100, 100),
+ contentDescription = "qwerty",
+ modifier = Modifier.testTag(testTag)
+ )
+ }
+
+ rule.onNodeWithTag(testTag)
+ .assertContentDescriptionEquals("qwerty")
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Image))
+ }
+
+ private fun createBitmapWithColor(
+ density: Density,
+ width: Int,
+ height: Int,
+ color: Color
+ ): ImageBitmap {
+ val size = Size(width.toFloat(), height.toFloat())
+ val image = ImageBitmap(width, height)
+ CanvasDrawScope().draw(
+ density,
+ LayoutDirection.Ltr,
+ Canvas(image),
+ size
+ ) {
+ drawRect(color)
+ }
+ return image
+ }
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
index 00fd482..59534d3 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
@@ -43,6 +43,8 @@
* [androidx.compose.material.icons.Icons]. If using a custom icon, note that the typical size for
* the internal icon is 24 x 24 dp.
*
+ * @sample androidx.compose.material3.samples.IconButtonSample
+ *
* @param onClick the lambda to be invoked when this icon is pressed
* @param modifier optional [Modifier] for this IconButton
* @param enabled whether or not this IconButton will handle input events and appear enabled for
@@ -88,6 +90,8 @@
* An [IconButton] with two states, for icons that can be toggled 'on' and 'off', such as a bookmark
* icon, or a navigation icon that opens a drawer.
*
+ * @sample androidx.compose.material3.samples.IconToggleButtonSample
+ *
* @param checked whether this IconToggleButton is currently checked
* @param onCheckedChange callback to be invoked when this icon is selected
* @param modifier optional [Modifier] for this IconToggleButton
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index c9c8637..ffecff2 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -58,6 +58,7 @@
samples(project(":compose:foundation:foundation-layout:foundation-layout-samples"))
samples(project(":compose:foundation:foundation:foundation-samples"))
docs(project(":compose:material3:material3"))
+ samples(project(":compose:material3:material3:material3-samples"))
docs(project(":compose:material:material"))
docs(project(":compose:material:material-icons-core"))
samples(project(":compose:material:material-icons-core:material-icons-core-samples"))