Merge "Fix expandedItem's behavior when they have buttons." into androidx-main
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/CurvedBoxTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/CurvedBoxTest.kt
index cc730a6..55ac19f 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/CurvedBoxTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/CurvedBoxTest.kt
@@ -21,6 +21,7 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.size
+import androidx.compose.testutils.assertDoesNotContainColor
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.LayoutCoordinates
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/ExpandableTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/ExpandableTest.kt
new file mode 100644
index 0000000..b79b17d
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/ExpandableTest.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.foundation
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
+import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults
+import androidx.wear.compose.foundation.lazy.ScalingLazyListState
+import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+
+class ExpandableTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun initially_collapsed() =
+        verifyExpandable(
+            setupState = { rememberExpandableState(initiallyExpanded = false) },
+            bitmapAssert = {
+                assertDoesContainColor(COLLAPSED_COLOR)
+            }
+        )
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun initially_expanded() =
+        verifyExpandable(
+            setupState = { rememberExpandableState(initiallyExpanded = true) },
+            bitmapAssert = {
+                assertDoesContainColor(EXPANDED_COLOR)
+            }
+        )
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun expand() =
+        verifyExpandable(
+            setupState = { rememberExpandableState(initiallyExpanded = false) },
+            bitmapAssert = {
+                assertDoesContainColor(EXPANDED_COLOR)
+            }
+        ) { state ->
+            state.expanded = true
+            waitForIdle()
+        }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun collapse() =
+        verifyExpandable(
+            setupState = { rememberExpandableState(initiallyExpanded = true) },
+            bitmapAssert = {
+                assertDoesContainColor(COLLAPSED_COLOR)
+            }
+        ) { state ->
+            state.expanded = false
+            waitForIdle()
+        }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun collapsed_click() = verifyClick(false)
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun expanded_click() = verifyClick(true)
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    private fun verifyClick(initiallyExpanded: Boolean) {
+        val clicked = mutableListOf<Boolean>()
+        verifyExpandable(
+            setupState = { rememberExpandableState(initiallyExpanded = initiallyExpanded) },
+            bitmapAssert = {
+                assertEquals(listOf(initiallyExpanded), clicked)
+            },
+            expandableContent = { expanded ->
+                Box(modifier = Modifier.fillMaxSize().clickable {
+                    clicked.add(expanded)
+                })
+            }
+        ) { _ ->
+            onNodeWithTag(TEST_TAG).performClick()
+            waitForIdle()
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    private fun verifyExpandable(
+        setupState: @Composable () -> ExpandableState,
+        bitmapAssert: ImageBitmap.() -> Unit,
+        expandableContent: @Composable (Boolean) -> Unit = { },
+        act: ComposeTestRule.(ExpandableState) -> Unit = { }
+    ) {
+        // Arrange - set up the content for the test including expandable content
+        var slcState: ScalingLazyListState? = null
+        var state: ExpandableState? = null
+        rule.setContent {
+            state = setupState()
+            Box(
+                Modifier
+                    .testTag(TEST_TAG)
+                    .size(100.dp)) {
+                ScalingLazyColumn(
+                    state = rememberScalingLazyListState().also { slcState = it },
+                    // We can only test expandableItem inside a ScalingLazyColumn, but we can make
+                    // it behave mostly as it wasn't there.
+                    scalingParams = ScalingLazyColumnDefaults
+                        .scalingParams(edgeScale = 1f, edgeAlpha = 1f),
+                    autoCentering = null,
+                    verticalArrangement = Arrangement.spacedBy(space = 0.dp),
+                ) {
+                    expandableItem(state!!) { expanded ->
+                        Box(
+                            Modifier
+                                .fillMaxWidth()
+                                .height(100.dp)
+                                .background(
+                                    if (expanded) EXPANDED_COLOR else COLLAPSED_COLOR
+                                )
+                        ) {
+                            expandableContent(expanded)
+                        }
+                    }
+                }
+            }
+        }
+        rule.waitUntil { slcState?.initialized?.value ?: false }
+
+        // Act - exercise the expandable if required for the test.
+        with(rule) {
+            act(state!!)
+        }
+
+        // Assert - verify the object under test worked correctly
+        rule.onNodeWithTag(TEST_TAG)
+            .captureToImage()
+            .apply { bitmapAssert() }
+    }
+
+    private val EXPANDED_COLOR = Color.Red
+    private val COLLAPSED_COLOR = Color.Green
+}
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt
index 0da21e5..f781f3a 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt
@@ -35,12 +35,25 @@
 
 /**
  * Checks whether [expectedColor] does not exist in current [ImageBitmap]
- */
+ *
 fun ImageBitmap.assertDoesNotContainColor(expectedColor: Color) {
     val histogram = histogram()
     if (histogram.containsKey(expectedColor)) {
         throw AssertionError("Expected color $expectedColor exists in current bitmap")
     }
+}*/
+
+/**
+ * Checks whether [expectedColor] exist in current [ImageBitmap], covering at least the given ratio
+ * of the image
+ */
+fun ImageBitmap.assertDoesContainColor(expectedColor: Color, expectedRatio: Float = 0.75f) {
+    val histogram = histogram()
+    val ratio = (histogram.getOrDefault(expectedColor, 0L)).toFloat() / (width * height)
+    if (ratio < expectedRatio) {
+        throw AssertionError("Expected color $expectedColor with ratio $expectedRatio." +
+            " Actual ratio = $ratio")
+    }
 }
 
 private fun ImageBitmap.histogram(): MutableMap<Color, Long> {
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/Expandable.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/Expandable.kt
index db910b1..20ebcae 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/Expandable.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/Expandable.kt
@@ -199,8 +199,14 @@
             val off1 = (width - placeables[1].width) / 2
 
             layout(width, height) {
-                placeables[0].placeWithLayer(off0, 0) { alpha = 1 - progress }
-                placeables[1].placeWithLayer(off1, 0) { alpha = progress }
+                if (progress < 1f) {
+                    placeables[0].placeWithLayer(off0, 0, zIndex = 1 - progress) {
+                        alpha = 1 - progress
+                    }
+                }
+                if (progress > 0f) {
+                    placeables[1].placeWithLayer(off1, 0, zIndex = progress) { alpha = progress }
+                }
             }
         }
     }
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ExpandableDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ExpandableDemo.kt
index 2d24e25..a676141 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ExpandableDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ExpandableDemo.kt
@@ -26,6 +26,7 @@
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.remember
@@ -45,6 +46,7 @@
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipDefaults
 import androidx.wear.compose.material.CompactChip
+import androidx.wear.compose.material.ListHeader
 import androidx.wear.compose.material.MaterialTheme
 import androidx.wear.compose.material.Text
 
@@ -117,6 +119,7 @@
 @Composable
 fun ExpandableText() {
     val state = rememberExpandableState()
+    val state2 = rememberExpandableState()
 
     ContainingScalingLazyColumn {
         expandableItem(state) { expanded ->
@@ -132,6 +135,22 @@
             )
         }
         expandButton(state, outline = false)
+
+        demoSeparator()
+        item {
+            ListHeader {
+                Text("Inline expandable.")
+            }
+        }
+        expandableItem(state2) { expanded ->
+            Row(verticalAlignment = CenterVertically) {
+                Text(if (expanded) "Expanded" else "Collapsed")
+                Spacer(Modifier.width(10.dp))
+                Button(onClick = { state2.expanded = !expanded }) {
+                    Text(if (expanded) "-" else "+")
+                }
+            }
+        }
     }
 }