summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt337
-rw-r--r--packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBarTest.kt259
2 files changed, 199 insertions, 397 deletions
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
index 9a344c3d7f14..2ae3b569bc70 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
@@ -41,23 +41,25 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
-import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.TopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.NonRestartableComposable
-import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.LastBaseline
import androidx.compose.ui.layout.Layout
@@ -66,6 +68,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.heading
+import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
@@ -80,11 +83,10 @@ import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
-private val windowInsets: WindowInsets
+private val safeDrawingWindowInsets: WindowInsets
@Composable
@NonRestartableComposable
- get() = WindowInsets.safeDrawing
- .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
+ get() = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
@Composable
internal fun CustomizedTopAppBar(
@@ -97,14 +99,12 @@ internal fun CustomizedTopAppBar(
titleTextStyle = MaterialTheme.typography.titleMedium,
navigationIcon = navigationIcon,
actions = actions,
- windowInsets = windowInsets,
+ windowInsets = safeDrawingWindowInsets,
colors = topAppBarColors(),
)
}
-/**
- * The customized LargeTopAppBar for Settings.
- */
+/** The customized LargeTopAppBar for Settings. */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun CustomizedLargeTopAppBar(
@@ -124,7 +124,7 @@ internal fun CustomizedLargeTopAppBar(
navigationIcon = navigationIcon,
actions = actions,
colors = topAppBarColors(),
- windowInsets = windowInsets,
+ windowInsets = safeDrawingWindowInsets,
pinnedHeight = ContainerHeight,
scrollBehavior = scrollBehavior,
)
@@ -134,38 +134,41 @@ internal fun CustomizedLargeTopAppBar(
private fun Title(title: String, maxLines: Int = Int.MAX_VALUE) {
Text(
text = title,
- modifier = Modifier.padding(
- start = SettingsDimension.itemPaddingAround,
- end = SettingsDimension.itemPaddingEnd,
- )
- .semantics { heading() },
+ modifier =
+ Modifier.padding(
+ start = SettingsDimension.itemPaddingAround,
+ end = SettingsDimension.itemPaddingEnd,
+ )
+ .semantics { heading() },
overflow = TextOverflow.Ellipsis,
maxLines = maxLines,
)
}
@Composable
-private fun topAppBarColors() = TopAppBarColors(
- containerColor = MaterialTheme.colorScheme.settingsBackground,
- scrolledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
- navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
- titleContentColor = MaterialTheme.colorScheme.onSurface,
- actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
-)
+private fun topAppBarColors() =
+ TopAppBarColors(
+ containerColor = MaterialTheme.colorScheme.settingsBackground,
+ scrolledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
+ navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
+ titleContentColor = MaterialTheme.colorScheme.onSurface,
+ actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
/**
* Represents the colors used by a top app bar in different states.
+ *
* This implementation animates the container color according to the top app bar scroll state. It
* does not animate the leading, headline, or trailing colors.
*
- * @constructor create an instance with arbitrary colors, see [TopAppBarColors] for a
- * factory method using the default material3 spec
* @param containerColor the color used for the background of this BottomAppBar. Use
- * [Color.Transparent] to have no color.
+ * [Color.Transparent] to have no color.
* @param scrolledContainerColor the container color when content is scrolled behind it
* @param navigationIconContentColor the content color used for the navigation icon
* @param titleContentColor the content color used for the title
* @param actionIconContentColor the content color used for actions
+ * @constructor create an instance with arbitrary colors, see [TopAppBarColors] for a factory method
+ * using the default material3 spec
*/
@Stable
private class TopAppBarColors(
@@ -180,11 +183,11 @@ private class TopAppBarColors(
* Represents the container color used for the top app bar.
*
* A [colorTransitionFraction] provides a percentage value that can be used to generate a color.
- * Usually, an app bar implementation will pass in a [colorTransitionFraction] read from
- * the [TopAppBarState.collapsedFraction] or the [TopAppBarState.overlappedFraction].
+ * Usually, an app bar implementation will pass in a [colorTransitionFraction] read from the
+ * [TopAppBarState.collapsedFraction] or the [TopAppBarState.overlappedFraction].
*
* @param colorTransitionFraction a `0.0` to `1.0` value that represents a color transition
- * percentage
+ * percentage
*/
@Stable
fun containerColor(colorTransitionFraction: Float): Color {
@@ -233,29 +236,35 @@ private fun SingleRowTopAppBar(
colors: TopAppBarColors,
) {
// Wrap the given actions in a Row.
- val actionsRow = @Composable {
- Row(
- horizontalArrangement = Arrangement.End,
- verticalAlignment = Alignment.CenterVertically,
- content = actions
- )
- }
+ val actionsRow =
+ @Composable {
+ Row(
+ horizontalArrangement = Arrangement.End,
+ verticalAlignment = Alignment.CenterVertically,
+ content = actions
+ )
+ }
// Compose a Surface with a TopAppBarLayout content.
- Surface(color = colors.scrolledContainerColor) {
+ Box(
+ modifier =
+ Modifier.drawBehind { drawRect(color = colors.scrolledContainerColor) }
+ .semantics { isTraversalGroup = true }
+ .pointerInput(Unit) {}
+ ) {
val height = LocalDensity.current.run { ContainerHeight.toPx() }
TopAppBarLayout(
- modifier = Modifier
- .windowInsetsPadding(windowInsets)
- // clip after padding so we don't show the title over the inset area
- .clipToBounds(),
+ modifier =
+ Modifier.windowInsetsPadding(windowInsets)
+ // clip after padding so we don't show the title over the inset area
+ .clipToBounds(),
heightPx = height,
navigationIconContentColor = colors.navigationIconContentColor,
titleContentColor = colors.titleContentColor,
actionIconContentColor = colors.actionIconContentColor,
title = title,
titleTextStyle = titleTextStyle,
- titleAlpha = 1f,
+ titleAlpha = { 1f },
titleVerticalArrangement = Arrangement.Center,
titleBottomPadding = 0,
hideTitleSemantics = false,
@@ -271,7 +280,7 @@ private fun SingleRowTopAppBar(
* composables.
*
* @throws [IllegalArgumentException] if the given [MaxHeightWithoutTitle] is equal or smaller than
- * the [pinnedHeight]
+ * the [pinnedHeight]
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -308,62 +317,67 @@ private fun TwoRowsTopAppBar(
// Sets the app bar's height offset limit to hide just the bottom title area and keep top title
// visible when collapsed.
- SideEffect {
- if (scrollBehavior?.state?.heightOffsetLimit != pinnedHeightPx - maxHeightPx.floatValue) {
- scrollBehavior?.state?.heightOffsetLimit = pinnedHeightPx - maxHeightPx.floatValue
- }
- }
+ scrollBehavior?.state?.heightOffsetLimit = pinnedHeightPx - maxHeightPx.floatValue
// Obtain the container Color from the TopAppBarColors using the `collapsedFraction`, as the
// bottom part of this TwoRowsTopAppBar changes color at the same rate the app bar expands or
// collapse.
// This will potentially animate or interpolate a transition between the container color and the
// container's scrolled color according to the app bar's scroll state.
- val colorTransitionFraction = scrollBehavior?.state?.collapsedFraction ?: 0f
- val appBarContainerColor = colors.containerColor(colorTransitionFraction)
+ val colorTransitionFraction = { scrollBehavior?.state?.collapsedFraction ?: 0f }
+ val appBarContainerColor = { colors.containerColor(colorTransitionFraction()) }
// Wrap the given actions in a Row.
- val actionsRow = @Composable {
- Row(
- horizontalArrangement = Arrangement.End,
- verticalAlignment = Alignment.CenterVertically,
- content = actions
- )
- }
- val topTitleAlpha = TopTitleAlphaEasing.transform(colorTransitionFraction)
- val bottomTitleAlpha = 1f - colorTransitionFraction
+ val actionsRow =
+ @Composable {
+ Row(
+ horizontalArrangement = Arrangement.End,
+ verticalAlignment = Alignment.CenterVertically,
+ content = actions
+ )
+ }
+ val topTitleAlpha = { TopTitleAlphaEasing.transform(colorTransitionFraction()) }
+ val bottomTitleAlpha = { 1f - colorTransitionFraction() }
// Hide the top row title semantics when its alpha value goes below 0.5 threshold.
// Hide the bottom row title semantics when the top title semantics are active.
- val hideTopRowSemantics = colorTransitionFraction < 0.5f
+ val hideTopRowSemantics by
+ remember(colorTransitionFraction) { derivedStateOf { colorTransitionFraction() < 0.5f } }
val hideBottomRowSemantics = !hideTopRowSemantics
// Set up support for resizing the top app bar when vertically dragging the bar itself.
- val appBarDragModifier = if (scrollBehavior != null && !scrollBehavior.isPinned) {
- Modifier.draggable(
- orientation = Orientation.Vertical,
- state = rememberDraggableState { delta ->
- scrollBehavior.state.heightOffset += delta
- },
- onDragStopped = { velocity ->
- settleAppBar(
- scrollBehavior.state,
- velocity,
- scrollBehavior.flingAnimationSpec,
- scrollBehavior.snapAnimationSpec
- )
- }
- )
- } else {
- Modifier
- }
+ val appBarDragModifier =
+ if (scrollBehavior != null && !scrollBehavior.isPinned) {
+ Modifier.draggable(
+ orientation = Orientation.Vertical,
+ state =
+ rememberDraggableState { delta -> scrollBehavior.state.heightOffset += delta },
+ onDragStopped = { velocity ->
+ settleAppBar(
+ scrollBehavior.state,
+ velocity,
+ scrollBehavior.flingAnimationSpec,
+ scrollBehavior.snapAnimationSpec
+ )
+ }
+ )
+ } else {
+ Modifier
+ }
- Surface(modifier = modifier.then(appBarDragModifier), color = appBarContainerColor) {
+ Box(
+ modifier =
+ modifier
+ .then(appBarDragModifier)
+ .drawBehind { drawRect(color = appBarContainerColor()) }
+ .semantics { isTraversalGroup = true }
+ .pointerInput(Unit) {}
+ ) {
Column {
TopAppBarLayout(
- modifier = Modifier
- .windowInsetsPadding(windowInsets)
- // clip after padding so we don't show the title over the inset area
- .clipToBounds(),
+ modifier =
+ Modifier.windowInsetsPadding(windowInsets)
+ // clip after padding so we don't show the title over the inset area
+ .clipToBounds(),
heightPx = pinnedHeightPx,
navigationIconContentColor = colors.navigationIconContentColor,
titleContentColor = colors.titleContentColor,
@@ -378,27 +392,37 @@ private fun TwoRowsTopAppBar(
actions = actionsRow,
)
TopAppBarLayout(
- modifier = Modifier
- // only apply the horizontal sides of the window insets padding, since the top
- // padding will always be applied by the layout above
- .windowInsetsPadding(windowInsets.only(WindowInsetsSides.Horizontal))
- .clipToBounds(),
- heightPx = maxHeightPx.floatValue - pinnedHeightPx +
- (scrollBehavior?.state?.heightOffset ?: 0f),
+ modifier =
+ Modifier
+ // only apply the horizontal sides of the window insets padding, since the
+ // top
+ // padding will always be applied by the layout above
+ .windowInsetsPadding(windowInsets.only(WindowInsetsSides.Horizontal))
+ .clipToBounds(),
+ heightPx =
+ maxHeightPx.floatValue - pinnedHeightPx +
+ (scrollBehavior?.state?.heightOffset ?: 0f),
navigationIconContentColor = colors.navigationIconContentColor,
titleContentColor = colors.titleContentColor,
actionIconContentColor = colors.actionIconContentColor,
title = {
- Box(modifier = Modifier.onGloballyPositioned { coordinates ->
- val measuredMaxHeightPx = density.run {
- MaxHeightWithoutTitle.toPx() + coordinates.size.height.toFloat()
- }
- // Allow larger max height for multi-line title, but do not reduce
- // max height to prevent flaky.
- if (measuredMaxHeightPx > defaultMaxHeightPx) {
- maxHeightPx.floatValue = measuredMaxHeightPx
- }
- }) { title() }
+ Box(
+ modifier =
+ Modifier.onGloballyPositioned { coordinates ->
+ val measuredMaxHeightPx =
+ density.run {
+ MaxHeightWithoutTitle.toPx() +
+ coordinates.size.height.toFloat()
+ }
+ // Allow larger max height for multi-line title, but do not reduce
+ // max height to prevent flaky.
+ if (measuredMaxHeightPx > defaultMaxHeightPx) {
+ maxHeightPx.floatValue = measuredMaxHeightPx
+ }
+ }
+ ) {
+ title()
+ }
},
titleTextStyle = titleTextStyle,
titleAlpha = bottomTitleAlpha,
@@ -420,21 +444,21 @@ private fun TwoRowsTopAppBar(
* @param modifier a [Modifier]
* @param heightPx the total height this layout is capped to
* @param navigationIconContentColor the content color that will be applied via a
- * [LocalContentColor] when composing the navigation icon
+ * [LocalContentColor] when composing the navigation icon
* @param titleContentColor the color that will be applied via a [LocalContentColor] when composing
- * the title
+ * the title
* @param actionIconContentColor the content color that will be applied via a [LocalContentColor]
- * when composing the action icons
+ * when composing the action icons
* @param title the top app bar title (header)
* @param titleTextStyle the title's text style
* @param modifier a [Modifier]
* @param titleAlpha the title's alpha
* @param titleVerticalArrangement the title's vertical arrangement
* @param titleBottomPadding the title's bottom padding
- * @param hideTitleSemantics hides the title node from the semantic tree. Apply this
- * boolean when this layout is part of a [TwoRowsTopAppBar] to hide the title's semantics
- * from accessibility services. This is needed to avoid having multiple titles visible to
- * accessibility services at the same time, when animating between collapsed / expanded states.
+ * @param hideTitleSemantics hides the title node from the semantic tree. Apply this boolean when
+ * this layout is part of a [TwoRowsTopAppBar] to hide the title's semantics from accessibility
+ * services. This is needed to avoid having multiple titles visible to accessibility services at
+ * the same time, when animating between collapsed / expanded states.
* @param navigationIcon a navigation icon [Composable]
* @param actions actions [Composable]
* @param titleScaleDisabled whether the title font scaling is disabled. Default is disabled.
@@ -448,7 +472,7 @@ private fun TopAppBarLayout(
actionIconContentColor: Color,
title: @Composable () -> Unit,
titleTextStyle: TextStyle,
- titleAlpha: Float,
+ titleAlpha: () -> Float,
titleVerticalArrangement: Arrangement.Vertical,
titleBottomPadding: Int,
hideTitleSemantics: Boolean,
@@ -458,41 +482,33 @@ private fun TopAppBarLayout(
) {
Layout(
{
- Box(
- Modifier
- .layoutId("navigationIcon")
- .padding(start = TopAppBarHorizontalPadding)
- ) {
+ Box(Modifier.layoutId("navigationIcon").padding(start = TopAppBarHorizontalPadding)) {
CompositionLocalProvider(
LocalContentColor provides navigationIconContentColor,
content = navigationIcon
)
}
Box(
- Modifier
- .layoutId("title")
+ Modifier.layoutId("title")
.padding(horizontal = TopAppBarHorizontalPadding)
- .then(if (hideTitleSemantics) Modifier.clearAndSetSemantics { } else Modifier)
- .graphicsLayer(alpha = titleAlpha)
+ .then(if (hideTitleSemantics) Modifier.clearAndSetSemantics {} else Modifier)
+ .graphicsLayer { alpha = titleAlpha() }
) {
ProvideTextStyle(value = titleTextStyle) {
CompositionLocalProvider(
LocalContentColor provides titleContentColor,
- LocalDensity provides with(LocalDensity.current) {
- Density(
- density = density,
- fontScale = if (titleScaleDisabled) 1f else fontScale,
- )
- },
+ LocalDensity provides
+ with(LocalDensity.current) {
+ Density(
+ density = density,
+ fontScale = if (titleScaleDisabled) 1f else fontScale,
+ )
+ },
content = title
)
}
}
- Box(
- Modifier
- .layoutId("actionIcons")
- .padding(end = TopAppBarHorizontalPadding)
- ) {
+ Box(Modifier.layoutId("actionIcons").padding(end = TopAppBarHorizontalPadding)) {
CompositionLocalProvider(
LocalContentColor provides actionIconContentColor,
content = actions
@@ -502,20 +518,24 @@ private fun TopAppBarLayout(
modifier = modifier
) { measurables, constraints ->
val navigationIconPlaceable =
- measurables.first { it.layoutId == "navigationIcon" }
+ measurables
+ .first { it.layoutId == "navigationIcon" }
.measure(constraints.copy(minWidth = 0))
val actionIconsPlaceable =
- measurables.first { it.layoutId == "actionIcons" }
+ measurables
+ .first { it.layoutId == "actionIcons" }
.measure(constraints.copy(minWidth = 0))
- val maxTitleWidth = if (constraints.maxWidth == Constraints.Infinity) {
- constraints.maxWidth
- } else {
- (constraints.maxWidth - navigationIconPlaceable.width - actionIconsPlaceable.width)
- .coerceAtLeast(0)
- }
+ val maxTitleWidth =
+ if (constraints.maxWidth == Constraints.Infinity) {
+ constraints.maxWidth
+ } else {
+ (constraints.maxWidth - navigationIconPlaceable.width - actionIconsPlaceable.width)
+ .coerceAtLeast(0)
+ }
val titlePlaceable =
- measurables.first { it.layoutId == "title" }
+ measurables
+ .first { it.layoutId == "title" }
.measure(constraints.copy(minWidth = 0, maxWidth = maxTitleWidth))
// Locate the title's baseline.
@@ -538,19 +558,23 @@ private fun TopAppBarLayout(
// Title
titlePlaceable.placeRelative(
x = max(TopAppBarTitleInset.roundToPx(), navigationIconPlaceable.width),
- y = when (titleVerticalArrangement) {
- Arrangement.Center -> (layoutHeight - titlePlaceable.height) / 2
- // Apply bottom padding from the title's baseline only when the Arrangement is
- // "Bottom".
- Arrangement.Bottom ->
- if (titleBottomPadding == 0) layoutHeight - titlePlaceable.height
- else layoutHeight - titlePlaceable.height - max(
- 0,
- titleBottomPadding - titlePlaceable.height + titleBaseline
- )
- // Arrangement.Top
- else -> 0
- }
+ y =
+ when (titleVerticalArrangement) {
+ Arrangement.Center -> (layoutHeight - titlePlaceable.height) / 2
+ // Apply bottom padding from the title's baseline only when the Arrangement
+ // is "Bottom".
+ Arrangement.Bottom ->
+ if (titleBottomPadding == 0) layoutHeight - titlePlaceable.height
+ else
+ layoutHeight -
+ titlePlaceable.height -
+ max(
+ 0,
+ titleBottomPadding - titlePlaceable.height + titleBaseline
+ )
+ // Arrangement.Top
+ else -> 0
+ }
)
// Action icons
@@ -562,7 +586,6 @@ private fun TopAppBarLayout(
}
}
-
/**
* Settles the app bar by flinging, in case the given velocity is greater than zero, and snapping
* after the fling settles.
@@ -587,9 +610,9 @@ private suspend fun settleAppBar(
if (flingAnimationSpec != null && abs(velocity) > 1f) {
var lastValue = 0f
AnimationState(
- initialValue = 0f,
- initialVelocity = velocity,
- )
+ initialValue = 0f,
+ initialVelocity = velocity,
+ )
.animateDecay(flingAnimationSpec) {
val delta = value - lastValue
val initialHeightOffset = state.heightOffset
@@ -603,9 +626,7 @@ private suspend fun settleAppBar(
}
// Snap if animation specs were provided.
if (snapAnimationSpec != null) {
- if (state.heightOffset < 0 &&
- state.heightOffset > state.heightOffsetLimit
- ) {
+ if (state.heightOffset < 0 && state.heightOffset > state.heightOffsetLimit) {
AnimationState(initialValue = state.heightOffset).animateTo(
if (state.collapsedFraction < 0.5f) {
0f
@@ -613,7 +634,9 @@ private suspend fun settleAppBar(
state.heightOffsetLimit
},
animationSpec = snapAnimationSpec
- ) { state.heightOffset = value }
+ ) {
+ state.heightOffset = value
+ }
}
}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBarTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBarTest.kt
index ea69eabbe971..0a4f0d937600 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBarTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBarTest.kt
@@ -37,15 +37,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
-import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
-import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
@@ -53,33 +49,24 @@ import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.swipeRight
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.height
-import androidx.compose.ui.unit.width
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.testutils.rootWidth
import com.android.settingslib.spa.testutils.setContentForSizeAssertions
import com.google.common.truth.Truth.assertThat
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@OptIn(ExperimentalMaterial3Api::class)
-@Ignore("b/346785755")
@RunWith(AndroidJUnit4::class)
class CustomizedAppBarTest {
- @get:Rule
- val rule = createComposeRule()
+ @get:Rule val rule = createComposeRule()
@Test
fun smallTopAppBar_expandsToScreen() {
rule
- .setContentForSizeAssertions {
- CustomizedTopAppBar(title = { Text("Title") })
- }
+ .setContentForSizeAssertions { CustomizedTopAppBar(title = { Text("Title") }) }
.assertHeightIsEqualTo(ContainerHeight)
.assertWidthIsEqualTo(rule.rootWidth())
}
@@ -88,51 +75,12 @@ class CustomizedAppBarTest {
fun smallTopAppBar_withTitle() {
val title = "Title"
rule.setContent {
- Box(Modifier.testTag(TopAppBarTestTag)) {
- CustomizedTopAppBar(title = { Text(title) })
- }
+ Box(Modifier.testTag(TopAppBarTestTag)) { CustomizedTopAppBar(title = { Text(title) }) }
}
rule.onNodeWithText(title).assertIsDisplayed()
}
@Test
- fun smallTopAppBar_default_positioning() {
- rule.setContent {
- Box(Modifier.testTag(TopAppBarTestTag)) {
- CustomizedTopAppBar(
- navigationIcon = {
- FakeIcon(Modifier.testTag(NavigationIconTestTag))
- },
- title = {
- Text("Title", Modifier.testTag(TitleTestTag))
- },
- actions = {
- FakeIcon(Modifier.testTag(ActionsTestTag))
- }
- )
- }
- }
- assertSmallDefaultPositioning()
- }
-
- @Test
- fun smallTopAppBar_noNavigationIcon_positioning() {
- rule.setContent {
- Box(Modifier.testTag(TopAppBarTestTag)) {
- CustomizedTopAppBar(
- title = {
- Text("Title", Modifier.testTag(TitleTestTag))
- },
- actions = {
- FakeIcon(Modifier.testTag(ActionsTestTag))
- }
- )
- }
- }
- assertSmallPositioningWithoutNavigation()
- }
-
- @Test
fun smallTopAppBar_titleDefaultStyle() {
var textStyle: TextStyle? = null
var expectedTextStyle: TextStyle? = null
@@ -188,29 +136,6 @@ class CustomizedAppBarTest {
assertThat(actionsColor).isEqualTo(expectedActionsColor)
}
- @Test
- fun largeTopAppBar_scrolled_positioning() {
- val content = @Composable { scrollBehavior: TopAppBarScrollBehavior? ->
- Box(Modifier.testTag(TopAppBarTestTag)) {
- CustomizedLargeTopAppBar(
- navigationIcon = {
- FakeIcon(Modifier.testTag(NavigationIconTestTag))
- },
- title = "Title",
- actions = {
- FakeIcon(Modifier.testTag(ActionsTestTag))
- },
- scrollBehavior = scrollBehavior,
- )
- }
- }
- assertLargeScrolledHeight(
- MaxHeightWithoutTitle + DefaultTitleHeight,
- MaxHeightWithoutTitle + DefaultTitleHeight,
- content,
- )
- }
-
@OptIn(ExperimentalMaterial3Api::class)
@Test
fun topAppBar_enterAlways_allowHorizontalScroll() {
@@ -221,14 +146,10 @@ class CustomizedAppBarTest {
}
rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- }
+ rule.runOnIdle { assertThat(state.firstVisibleItemIndex).isEqualTo(1) }
rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- }
+ rule.runOnIdle { assertThat(state.firstVisibleItemIndex).isEqualTo(0) }
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -241,14 +162,10 @@ class CustomizedAppBarTest {
}
rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- }
+ rule.runOnIdle { assertThat(state.firstVisibleItemIndex).isEqualTo(1) }
rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- }
+ rule.runOnIdle { assertThat(state.firstVisibleItemIndex).isEqualTo(0) }
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -257,21 +174,14 @@ class CustomizedAppBarTest {
lateinit var state: LazyListState
rule.setContent {
state = rememberLazyListState()
- MultiPageContent(
- TopAppBarDefaults.pinnedScrollBehavior(),
- state
- )
+ MultiPageContent(TopAppBarDefaults.pinnedScrollBehavior(), state)
}
rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(1)
- }
+ rule.runOnIdle { assertThat(state.firstVisibleItemIndex).isEqualTo(1) }
rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
- rule.runOnIdle {
- assertThat(state.firstVisibleItemIndex).isEqualTo(0)
- }
+ rule.runOnIdle { assertThat(state.firstVisibleItemIndex).isEqualTo(0) }
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -285,11 +195,7 @@ class CustomizedAppBarTest {
)
}
) { contentPadding ->
- LazyRow(
- Modifier
- .fillMaxSize()
- .testTag(LazyListTag), state
- ) {
+ LazyRow(Modifier.fillMaxSize().testTag(LazyListTag), state) {
items(2) { page ->
LazyColumn(
modifier = Modifier.fillParentMaxSize(),
@@ -308,146 +214,19 @@ class CustomizedAppBarTest {
}
/**
- * Checks the app bar's components positioning when it's a [CustomizedTopAppBar]
- * or a larger app bar that is scrolled up and collapsed into a small
- * configuration and there is no navigation icon.
- */
- private fun assertSmallPositioningWithoutNavigation(isCenteredTitle: Boolean = false) {
- val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
- val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
-
- val titleNode = rule.onNodeWithTag(TitleTestTag)
- // Title should be vertically centered
- titleNode.assertTopPositionInRootIsEqualTo((appBarBounds.height - titleBounds.height) / 2)
- if (isCenteredTitle) {
- // Title should be horizontally centered
- titleNode.assertLeftPositionInRootIsEqualTo(
- (appBarBounds.width - titleBounds.width) / 2
- )
- } else {
- // Title should now be placed 16.dp from the start, as there is no navigation icon
- // 4.dp padding for the whole app bar + 12.dp inset
- titleNode.assertLeftPositionInRootIsEqualTo(4.dp + 12.dp)
- }
-
- rule.onNodeWithTag(ActionsTestTag)
- // Action should still be placed at the end
- .assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
- }
-
- /**
- * Checks the app bar's components positioning when it's a [CustomizedTopAppBar].
- */
- private fun assertSmallDefaultPositioning(isCenteredTitle: Boolean = false) {
- val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
- val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
- val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
-
- rule.onNodeWithTag(NavigationIconTestTag)
- // Navigation icon should be 4.dp from the start
- .assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding)
- // Navigation icon should be centered within the height of the app bar.
- .assertTopPositionInRootIsEqualTo(
- appBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
- )
-
- val titleNode = rule.onNodeWithTag(TitleTestTag)
- // Title should be vertically centered
- titleNode.assertTopPositionInRootIsEqualTo((appBarBounds.height - titleBounds.height) / 2)
- if (isCenteredTitle) {
- // Title should be horizontally centered
- titleNode.assertLeftPositionInRootIsEqualTo(
- (appBarBounds.width - titleBounds.width) / 2
- )
- } else {
- // Title should be 56.dp from the start
- // 4.dp padding for the whole app bar + 48.dp icon size + 4.dp title padding.
- titleNode.assertLeftPositionInRootIsEqualTo(4.dp + FakeIconSize + 4.dp)
- }
-
- rule.onNodeWithTag(ActionsTestTag)
- // Action should be placed at the end
- .assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
- // Action should be 8.dp from the top
- .assertTopPositionInRootIsEqualTo(
- appBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
- )
- }
-
- /**
- * Checks that changing values at a [CustomizedLargeTopAppBar] scroll behavior
- * affects the height of the app bar.
- *
- * This check partially and fully collapses the app bar to test its height.
- *
- * @param appBarMaxHeight the max height of the app bar [content]
- * @param appBarMinHeight the min height of the app bar [content]
- * @param content a Composable that adds a CustomizedLargeTopAppBar
- */
- @OptIn(ExperimentalMaterial3Api::class)
- private fun assertLargeScrolledHeight(
- appBarMaxHeight: Dp,
- appBarMinHeight: Dp,
- content: @Composable (TopAppBarScrollBehavior?) -> Unit
- ) {
- val fullyCollapsedOffsetDp = appBarMaxHeight - appBarMinHeight
- val partiallyCollapsedOffsetDp = fullyCollapsedOffsetDp / 3
- var partiallyCollapsedHeightOffsetPx = 0f
- var fullyCollapsedHeightOffsetPx = 0f
- lateinit var scrollBehavior: TopAppBarScrollBehavior
- rule.setContent {
- scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
- with(LocalDensity.current) {
- partiallyCollapsedHeightOffsetPx = partiallyCollapsedOffsetDp.toPx()
- fullyCollapsedHeightOffsetPx = fullyCollapsedOffsetDp.toPx()
- }
-
- content(scrollBehavior)
- }
-
- // Simulate a partially collapsed app bar.
- rule.runOnIdle {
- scrollBehavior.state.heightOffset = -partiallyCollapsedHeightOffsetPx
- scrollBehavior.state.contentOffset = -partiallyCollapsedHeightOffsetPx
- }
- rule.waitForIdle()
- rule.onNodeWithTag(TopAppBarTestTag)
- .assertHeightIsEqualTo(
- appBarMaxHeight - partiallyCollapsedOffsetDp
- )
-
- // Simulate a fully collapsed app bar.
- rule.runOnIdle {
- scrollBehavior.state.heightOffset = -fullyCollapsedHeightOffsetPx
- // Simulate additional content scroll beyond the max offset scroll.
- scrollBehavior.state.contentOffset =
- -fullyCollapsedHeightOffsetPx - partiallyCollapsedHeightOffsetPx
- }
- rule.waitForIdle()
- // Check that the app bar collapsed to its min height.
- rule.onNodeWithTag(TopAppBarTestTag).assertHeightIsEqualTo(appBarMinHeight)
- }
-
- /**
* An [IconButton] with an [Icon] inside for testing positions.
*
* An [IconButton] is defaulted to be 48X48dp, while its child [Icon] is defaulted to 24x24dp.
*/
- private val FakeIcon = @Composable { modifier: Modifier ->
- IconButton(
- onClick = { /* doSomething() */ },
- modifier = modifier.semantics(mergeDescendants = true) {}
- ) {
- Icon(ColorPainter(Color.Red), null)
+ private val FakeIcon =
+ @Composable { modifier: Modifier ->
+ IconButton(
+ onClick = { /* doSomething() */ },
+ modifier = modifier.semantics(mergeDescendants = true) {}
+ ) {
+ Icon(ColorPainter(Color.Red), null)
+ }
}
- }
-
- private fun expectedActionPosition(appBarWidth: Dp): Dp =
- appBarWidth - AppBarStartAndEndPadding - FakeIconSize
-
- private val FakeIconSize = 48.dp
- private val AppBarStartAndEndPadding = 4.dp
- private val AppBarTopAndBottomPadding = (ContainerHeight - FakeIconSize) / 2
private val LazyListTag = "lazyList"
private val TopAppBarTestTag = "bar"