mirror of
https://github.com/bitwarden/android.git
synced 2025-12-13 04:42:51 -06:00
Fix topAppBar flicker when text is long (#6098)
This commit is contained in:
parent
dd1dbd0b97
commit
9ddfd376a9
@ -20,11 +20,14 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.layout.SubcomposeLayout
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.text.TextLayoutResult
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastMap
|
||||||
import com.bitwarden.ui.R
|
import com.bitwarden.ui.R
|
||||||
import com.bitwarden.ui.platform.base.util.bottomDivider
|
import com.bitwarden.ui.platform.base.util.bottomDivider
|
||||||
import com.bitwarden.ui.platform.base.util.mirrorIfRtl
|
import com.bitwarden.ui.platform.base.util.mirrorIfRtl
|
||||||
@ -38,12 +41,15 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
|
|||||||
/**
|
/**
|
||||||
* Represents a Bitwarden styled [TopAppBar] that assumes the following components:
|
* Represents a Bitwarden styled [TopAppBar] that assumes the following components:
|
||||||
*
|
*
|
||||||
* - a single navigation control in the upper-left defined by [navigationIcon],
|
* @param title The title to display in the app bar.
|
||||||
* [navigationIconContentDescription], and [onNavigationIconClick].
|
* @param scrollBehavior The [TopAppBarScrollBehavior] to apply to the app bar.
|
||||||
* - a [title] in the middle.
|
* @param navigationIcon The icon to be displayed for the navigation icon button.
|
||||||
* - a [actions] lambda containing the set of actions (usually icons or similar) to display
|
* @param navigationIconContentDescription The content description of the navigation icon button.
|
||||||
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
|
* @param onNavigationIconClick The click action to occur when the navigation icon button is tapped.
|
||||||
* defining the layout of the actions.
|
* @param modifier The [Modifier] applied to the app bar.
|
||||||
|
* @param windowInsets The window insets to apply to the app bar.
|
||||||
|
* @param dividerStyle Applies a bottom divider based on the [TopAppBarDividerStyle] provided.
|
||||||
|
* @param actions A [Composable] lambda of action to display in the app bar.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -77,17 +83,16 @@ fun BitwardenTopAppBar(
|
|||||||
/**
|
/**
|
||||||
* Represents a Bitwarden styled [TopAppBar] that assumes the following components:
|
* Represents a Bitwarden styled [TopAppBar] that assumes the following components:
|
||||||
*
|
*
|
||||||
* - an optional single navigation control in the upper-left defined by [navigationIcon].
|
* @param title The title to display in the app bar.
|
||||||
* - a [title] in the middle.
|
* @param scrollBehavior The [TopAppBarScrollBehavior] to apply to the app bar.
|
||||||
* - a [actions] lambda containing the set of actions (usually icons or similar) to display
|
* @param navigationIcon The option [NavigationIcon] to display the navigation icon button.
|
||||||
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
|
* @param modifier The [Modifier] applied to the app bar.
|
||||||
* defining the layout of the actions.
|
* @param windowInsets The window insets to apply to the app bar.
|
||||||
* - if the title text causes an overflow in the standard material [TopAppBar] a [MediumTopAppBar]
|
* @param dividerStyle Applies a bottom divider based on the [TopAppBarDividerStyle] provided.
|
||||||
* will be used instead, droping the title text to a second row beneath the [navigationIcon] and
|
* @param actions A [Composable] lambda of action to display in the app bar.
|
||||||
* [actions].
|
* @param minimumHeight The minimum height of the app bar.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Suppress("LongMethod")
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BitwardenTopAppBar(
|
fun BitwardenTopAppBar(
|
||||||
title: String,
|
title: String,
|
||||||
@ -100,87 +105,179 @@ fun BitwardenTopAppBar(
|
|||||||
actions: @Composable RowScope.() -> Unit = {},
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
minimumHeight: Dp = 48.dp,
|
minimumHeight: Dp = 48.dp,
|
||||||
) {
|
) {
|
||||||
var titleTextHasOverflow by remember {
|
var titleTextHasOverflow by remember(key1 = title) { mutableStateOf(false) }
|
||||||
mutableStateOf(false)
|
// Without this sub-compose layout, there would be flickering when displaying the
|
||||||
}
|
// MediumTopAppBar because the regular TopAppBar would be displayed first.
|
||||||
|
SubcomposeLayout(modifier = modifier) { constraints ->
|
||||||
val navigationIconContent: @Composable () -> Unit = remember(navigationIcon) {
|
// We assume a regular TopAppBar and only if it is overflowing do we use a MediumTopAppBar.
|
||||||
{
|
// Once we determine the overflow is occurring, we will not measure the regular one again
|
||||||
navigationIcon?.let {
|
// unless the title changes or a configuration change occurs.
|
||||||
BitwardenStandardIconButton(
|
val placeables = if (titleTextHasOverflow) {
|
||||||
painter = it.navigationIcon,
|
this
|
||||||
contentDescription = it.navigationIconContentDescription,
|
.subcompose(
|
||||||
onClick = it.onNavigationIconClick,
|
slotId = "mediumTopAppBarContent",
|
||||||
modifier = Modifier
|
content = {
|
||||||
.testTag(tag = "CloseButton")
|
InternalMediumTopAppBar(
|
||||||
.mirrorIfRtl(),
|
title = title,
|
||||||
)
|
windowInsets = windowInsets,
|
||||||
}
|
scrollBehavior = scrollBehavior,
|
||||||
}
|
navigationIcon = navigationIcon,
|
||||||
}
|
minimumHeight = minimumHeight,
|
||||||
val customModifier = modifier
|
actions = actions,
|
||||||
.testTag(tag = "HeaderBarComponent")
|
dividerStyle = dividerStyle,
|
||||||
.scrolledContainerBottomDivider(
|
)
|
||||||
topAppBarScrollBehavior = scrollBehavior,
|
|
||||||
enabled = when (dividerStyle) {
|
|
||||||
TopAppBarDividerStyle.NONE -> false
|
|
||||||
TopAppBarDividerStyle.STATIC -> false
|
|
||||||
TopAppBarDividerStyle.ON_SCROLL -> true
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.bottomDivider(
|
|
||||||
enabled = when (dividerStyle) {
|
|
||||||
TopAppBarDividerStyle.NONE -> false
|
|
||||||
TopAppBarDividerStyle.STATIC -> true
|
|
||||||
TopAppBarDividerStyle.ON_SCROLL -> false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (titleTextHasOverflow) {
|
|
||||||
MediumTopAppBar(
|
|
||||||
windowInsets = windowInsets,
|
|
||||||
colors = bitwardenTopAppBarColors(),
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
navigationIcon = navigationIconContent,
|
|
||||||
collapsedHeight = minimumHeight,
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = BitwardenTheme.typography.titleLarge,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
maxLines = 2,
|
|
||||||
modifier = Modifier.testTag(tag = "PageTitleLabel"),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = customModifier,
|
|
||||||
actions = actions,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
TopAppBar(
|
|
||||||
windowInsets = windowInsets,
|
|
||||||
colors = bitwardenTopAppBarColors(),
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
navigationIcon = navigationIconContent,
|
|
||||||
expandedHeight = minimumHeight,
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = BitwardenTheme.typography.titleLarge,
|
|
||||||
maxLines = 1,
|
|
||||||
softWrap = false,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.testTag(tag = "PageTitleLabel"),
|
|
||||||
onTextLayout = {
|
|
||||||
titleTextHasOverflow = it.hasVisualOverflow
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
.fastMap { it.measure(constraints = constraints) }
|
||||||
modifier = customModifier,
|
} else {
|
||||||
actions = actions,
|
this
|
||||||
|
.subcompose(
|
||||||
|
slotId = "defaultTopAppBarContent",
|
||||||
|
content = {
|
||||||
|
InternalDefaultTopAppBar(
|
||||||
|
title = title,
|
||||||
|
windowInsets = windowInsets,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = navigationIcon,
|
||||||
|
minimumHeight = minimumHeight,
|
||||||
|
actions = actions,
|
||||||
|
dividerStyle = dividerStyle,
|
||||||
|
onTitleTextLayout = { titleTextHasOverflow = it.hasVisualOverflow },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.fastMap { it.measure(constraints = constraints) }
|
||||||
|
}
|
||||||
|
layout(
|
||||||
|
width = constraints.maxWidth,
|
||||||
|
height = placeables.maxOfOrNull { it.height } ?: 0,
|
||||||
|
) {
|
||||||
|
placeables.fastMap { it.place(x = 0, y = 0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun InternalMediumTopAppBar(
|
||||||
|
title: String,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
navigationIcon: NavigationIcon?,
|
||||||
|
windowInsets: WindowInsets,
|
||||||
|
dividerStyle: TopAppBarDividerStyle,
|
||||||
|
actions: @Composable RowScope.() -> Unit,
|
||||||
|
minimumHeight: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
MediumTopAppBar(
|
||||||
|
windowInsets = windowInsets,
|
||||||
|
colors = bitwardenTopAppBarColors(),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = { NavigationIconButton(navigationIcon = navigationIcon) },
|
||||||
|
collapsedHeight = minimumHeight,
|
||||||
|
title = { TitleText(title = title) },
|
||||||
|
actions = actions,
|
||||||
|
modifier = modifier.topAppBarModifier(
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
dividerStyle = dividerStyle,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun InternalDefaultTopAppBar(
|
||||||
|
title: String,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
navigationIcon: NavigationIcon?,
|
||||||
|
windowInsets: WindowInsets,
|
||||||
|
dividerStyle: TopAppBarDividerStyle,
|
||||||
|
actions: @Composable RowScope.() -> Unit,
|
||||||
|
minimumHeight: Dp,
|
||||||
|
onTitleTextLayout: (TextLayoutResult) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
windowInsets = windowInsets,
|
||||||
|
colors = bitwardenTopAppBarColors(),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = { NavigationIconButton(navigationIcon = navigationIcon) },
|
||||||
|
expandedHeight = minimumHeight,
|
||||||
|
title = {
|
||||||
|
TitleText(
|
||||||
|
title = title,
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false,
|
||||||
|
onTextLayout = onTitleTextLayout,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = actions,
|
||||||
|
modifier = modifier.topAppBarModifier(
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
dividerStyle = dividerStyle,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NavigationIconButton(
|
||||||
|
navigationIcon: NavigationIcon?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
navigationIcon?.let {
|
||||||
|
BitwardenStandardIconButton(
|
||||||
|
painter = it.navigationIcon,
|
||||||
|
contentDescription = it.navigationIconContentDescription,
|
||||||
|
onClick = it.onNavigationIconClick,
|
||||||
|
modifier = modifier
|
||||||
|
.testTag(tag = "CloseButton")
|
||||||
|
.mirrorIfRtl(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TitleText(
|
||||||
|
title: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
maxLines: Int = 2,
|
||||||
|
softWrap: Boolean = true,
|
||||||
|
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = BitwardenTheme.typography.titleLarge,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = maxLines,
|
||||||
|
softWrap = softWrap,
|
||||||
|
onTextLayout = onTextLayout,
|
||||||
|
modifier = modifier.testTag(tag = "PageTitleLabel"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun Modifier.topAppBarModifier(
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
dividerStyle: TopAppBarDividerStyle,
|
||||||
|
): Modifier = this
|
||||||
|
.testTag(tag = "HeaderBarComponent")
|
||||||
|
.scrolledContainerBottomDivider(
|
||||||
|
topAppBarScrollBehavior = scrollBehavior,
|
||||||
|
enabled = when (dividerStyle) {
|
||||||
|
TopAppBarDividerStyle.NONE -> false
|
||||||
|
TopAppBarDividerStyle.STATIC -> false
|
||||||
|
TopAppBarDividerStyle.ON_SCROLL -> true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.bottomDivider(
|
||||||
|
enabled = when (dividerStyle) {
|
||||||
|
TopAppBarDividerStyle.NONE -> false
|
||||||
|
TopAppBarDividerStyle.STATIC -> true
|
||||||
|
TopAppBarDividerStyle.ON_SCROLL -> false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user