Skip to content

Commit 9ddfd37

Browse files
Fix topAppBar flicker when text is long (#6098)
1 parent dd1dbd0 commit 9ddfd37

File tree

1 file changed

+183
-86
lines changed

1 file changed

+183
-86
lines changed

ui/src/main/kotlin/com/bitwarden/ui/platform/components/appbar/BitwardenTopAppBar.kt

Lines changed: 183 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ import androidx.compose.runtime.remember
2020
import androidx.compose.runtime.setValue
2121
import androidx.compose.ui.Modifier
2222
import androidx.compose.ui.graphics.painter.Painter
23+
import androidx.compose.ui.layout.SubcomposeLayout
2324
import androidx.compose.ui.platform.testTag
25+
import androidx.compose.ui.text.TextLayoutResult
2426
import androidx.compose.ui.text.style.TextOverflow
2527
import androidx.compose.ui.tooling.preview.Preview
2628
import androidx.compose.ui.unit.Dp
2729
import androidx.compose.ui.unit.dp
30+
import androidx.compose.ui.util.fastMap
2831
import com.bitwarden.ui.R
2932
import com.bitwarden.ui.platform.base.util.bottomDivider
3033
import com.bitwarden.ui.platform.base.util.mirrorIfRtl
@@ -38,12 +41,15 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
3841
/**
3942
* Represents a Bitwarden styled [TopAppBar] that assumes the following components:
4043
*
41-
* - a single navigation control in the upper-left defined by [navigationIcon],
42-
* [navigationIconContentDescription], and [onNavigationIconClick].
43-
* - a [title] in the middle.
44-
* - a [actions] lambda containing the set of actions (usually icons or similar) to display
45-
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
46-
* defining the layout of the actions.
44+
* @param title The title to display in the app bar.
45+
* @param scrollBehavior The [TopAppBarScrollBehavior] to apply to the app bar.
46+
* @param navigationIcon The icon to be displayed for the navigation icon button.
47+
* @param navigationIconContentDescription The content description of the navigation icon button.
48+
* @param onNavigationIconClick The click action to occur when the navigation icon button is tapped.
49+
* @param modifier The [Modifier] applied to the app bar.
50+
* @param windowInsets The window insets to apply to the app bar.
51+
* @param dividerStyle Applies a bottom divider based on the [TopAppBarDividerStyle] provided.
52+
* @param actions A [Composable] lambda of action to display in the app bar.
4753
*/
4854
@OptIn(ExperimentalMaterial3Api::class)
4955
@Composable
@@ -77,17 +83,16 @@ fun BitwardenTopAppBar(
7783
/**
7884
* Represents a Bitwarden styled [TopAppBar] that assumes the following components:
7985
*
80-
* - an optional single navigation control in the upper-left defined by [navigationIcon].
81-
* - a [title] in the middle.
82-
* - a [actions] lambda containing the set of actions (usually icons or similar) to display
83-
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
84-
* defining the layout of the actions.
85-
* - if the title text causes an overflow in the standard material [TopAppBar] a [MediumTopAppBar]
86-
* will be used instead, droping the title text to a second row beneath the [navigationIcon] and
87-
* [actions].
86+
* @param title The title to display in the app bar.
87+
* @param scrollBehavior The [TopAppBarScrollBehavior] to apply to the app bar.
88+
* @param navigationIcon The option [NavigationIcon] to display the navigation icon button.
89+
* @param modifier The [Modifier] applied to the app bar.
90+
* @param windowInsets The window insets to apply to the app bar.
91+
* @param dividerStyle Applies a bottom divider based on the [TopAppBarDividerStyle] provided.
92+
* @param actions A [Composable] lambda of action to display in the app bar.
93+
* @param minimumHeight The minimum height of the app bar.
8894
*/
8995
@OptIn(ExperimentalMaterial3Api::class)
90-
@Suppress("LongMethod")
9196
@Composable
9297
fun BitwardenTopAppBar(
9398
title: String,
@@ -100,87 +105,179 @@ fun BitwardenTopAppBar(
100105
actions: @Composable RowScope.() -> Unit = {},
101106
minimumHeight: Dp = 48.dp,
102107
) {
103-
var titleTextHasOverflow by remember {
104-
mutableStateOf(false)
105-
}
106-
107-
val navigationIconContent: @Composable () -> Unit = remember(navigationIcon) {
108-
{
109-
navigationIcon?.let {
110-
BitwardenStandardIconButton(
111-
painter = it.navigationIcon,
112-
contentDescription = it.navigationIconContentDescription,
113-
onClick = it.onNavigationIconClick,
114-
modifier = Modifier
115-
.testTag(tag = "CloseButton")
116-
.mirrorIfRtl(),
108+
var titleTextHasOverflow by remember(key1 = title) { mutableStateOf(false) }
109+
// Without this sub-compose layout, there would be flickering when displaying the
110+
// MediumTopAppBar because the regular TopAppBar would be displayed first.
111+
SubcomposeLayout(modifier = modifier) { constraints ->
112+
// We assume a regular TopAppBar and only if it is overflowing do we use a MediumTopAppBar.
113+
// Once we determine the overflow is occurring, we will not measure the regular one again
114+
// unless the title changes or a configuration change occurs.
115+
val placeables = if (titleTextHasOverflow) {
116+
this
117+
.subcompose(
118+
slotId = "mediumTopAppBarContent",
119+
content = {
120+
InternalMediumTopAppBar(
121+
title = title,
122+
windowInsets = windowInsets,
123+
scrollBehavior = scrollBehavior,
124+
navigationIcon = navigationIcon,
125+
minimumHeight = minimumHeight,
126+
actions = actions,
127+
dividerStyle = dividerStyle,
128+
)
129+
},
130+
)
131+
.fastMap { it.measure(constraints = constraints) }
132+
} else {
133+
this
134+
.subcompose(
135+
slotId = "defaultTopAppBarContent",
136+
content = {
137+
InternalDefaultTopAppBar(
138+
title = title,
139+
windowInsets = windowInsets,
140+
scrollBehavior = scrollBehavior,
141+
navigationIcon = navigationIcon,
142+
minimumHeight = minimumHeight,
143+
actions = actions,
144+
dividerStyle = dividerStyle,
145+
onTitleTextLayout = { titleTextHasOverflow = it.hasVisualOverflow },
146+
)
147+
},
117148
)
118-
}
149+
.fastMap { it.measure(constraints = constraints) }
150+
}
151+
layout(
152+
width = constraints.maxWidth,
153+
height = placeables.maxOfOrNull { it.height } ?: 0,
154+
) {
155+
placeables.fastMap { it.place(x = 0, y = 0) }
119156
}
120157
}
121-
val customModifier = modifier
122-
.testTag(tag = "HeaderBarComponent")
123-
.scrolledContainerBottomDivider(
124-
topAppBarScrollBehavior = scrollBehavior,
125-
enabled = when (dividerStyle) {
126-
TopAppBarDividerStyle.NONE -> false
127-
TopAppBarDividerStyle.STATIC -> false
128-
TopAppBarDividerStyle.ON_SCROLL -> true
129-
},
130-
)
131-
.bottomDivider(
132-
enabled = when (dividerStyle) {
133-
TopAppBarDividerStyle.NONE -> false
134-
TopAppBarDividerStyle.STATIC -> true
135-
TopAppBarDividerStyle.ON_SCROLL -> false
136-
},
137-
)
158+
}
138159

139-
if (titleTextHasOverflow) {
140-
MediumTopAppBar(
141-
windowInsets = windowInsets,
142-
colors = bitwardenTopAppBarColors(),
160+
@OptIn(ExperimentalMaterial3Api::class)
161+
@Composable
162+
private fun InternalMediumTopAppBar(
163+
title: String,
164+
scrollBehavior: TopAppBarScrollBehavior,
165+
navigationIcon: NavigationIcon?,
166+
windowInsets: WindowInsets,
167+
dividerStyle: TopAppBarDividerStyle,
168+
actions: @Composable RowScope.() -> Unit,
169+
minimumHeight: Dp,
170+
modifier: Modifier = Modifier,
171+
) {
172+
MediumTopAppBar(
173+
windowInsets = windowInsets,
174+
colors = bitwardenTopAppBarColors(),
175+
scrollBehavior = scrollBehavior,
176+
navigationIcon = { NavigationIconButton(navigationIcon = navigationIcon) },
177+
collapsedHeight = minimumHeight,
178+
title = { TitleText(title = title) },
179+
actions = actions,
180+
modifier = modifier.topAppBarModifier(
143181
scrollBehavior = scrollBehavior,
144-
navigationIcon = navigationIconContent,
145-
collapsedHeight = minimumHeight,
146-
title = {
147-
Text(
148-
text = title,
149-
style = BitwardenTheme.typography.titleLarge,
150-
overflow = TextOverflow.Ellipsis,
151-
maxLines = 2,
152-
modifier = Modifier.testTag(tag = "PageTitleLabel"),
153-
)
154-
},
155-
modifier = customModifier,
156-
actions = actions,
157-
)
158-
} else {
159-
TopAppBar(
160-
windowInsets = windowInsets,
161-
colors = bitwardenTopAppBarColors(),
182+
dividerStyle = dividerStyle,
183+
),
184+
)
185+
}
186+
187+
@OptIn(ExperimentalMaterial3Api::class)
188+
@Composable
189+
private fun InternalDefaultTopAppBar(
190+
title: String,
191+
scrollBehavior: TopAppBarScrollBehavior,
192+
navigationIcon: NavigationIcon?,
193+
windowInsets: WindowInsets,
194+
dividerStyle: TopAppBarDividerStyle,
195+
actions: @Composable RowScope.() -> Unit,
196+
minimumHeight: Dp,
197+
onTitleTextLayout: (TextLayoutResult) -> Unit,
198+
modifier: Modifier = Modifier,
199+
) {
200+
TopAppBar(
201+
windowInsets = windowInsets,
202+
colors = bitwardenTopAppBarColors(),
203+
scrollBehavior = scrollBehavior,
204+
navigationIcon = { NavigationIconButton(navigationIcon = navigationIcon) },
205+
expandedHeight = minimumHeight,
206+
title = {
207+
TitleText(
208+
title = title,
209+
maxLines = 1,
210+
softWrap = false,
211+
onTextLayout = onTitleTextLayout,
212+
)
213+
},
214+
actions = actions,
215+
modifier = modifier.topAppBarModifier(
162216
scrollBehavior = scrollBehavior,
163-
navigationIcon = navigationIconContent,
164-
expandedHeight = minimumHeight,
165-
title = {
166-
Text(
167-
text = title,
168-
style = BitwardenTheme.typography.titleLarge,
169-
maxLines = 1,
170-
softWrap = false,
171-
overflow = TextOverflow.Ellipsis,
172-
modifier = Modifier.testTag(tag = "PageTitleLabel"),
173-
onTextLayout = {
174-
titleTextHasOverflow = it.hasVisualOverflow
175-
},
176-
)
177-
},
178-
modifier = customModifier,
179-
actions = actions,
217+
dividerStyle = dividerStyle,
218+
),
219+
)
220+
}
221+
222+
@Composable
223+
private fun NavigationIconButton(
224+
navigationIcon: NavigationIcon?,
225+
modifier: Modifier = Modifier,
226+
) {
227+
navigationIcon?.let {
228+
BitwardenStandardIconButton(
229+
painter = it.navigationIcon,
230+
contentDescription = it.navigationIconContentDescription,
231+
onClick = it.onNavigationIconClick,
232+
modifier = modifier
233+
.testTag(tag = "CloseButton")
234+
.mirrorIfRtl(),
180235
)
181236
}
182237
}
183238

239+
@Composable
240+
private fun TitleText(
241+
title: String,
242+
modifier: Modifier = Modifier,
243+
maxLines: Int = 2,
244+
softWrap: Boolean = true,
245+
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
246+
) {
247+
Text(
248+
text = title,
249+
style = BitwardenTheme.typography.titleLarge,
250+
overflow = TextOverflow.Ellipsis,
251+
maxLines = maxLines,
252+
softWrap = softWrap,
253+
onTextLayout = onTextLayout,
254+
modifier = modifier.testTag(tag = "PageTitleLabel"),
255+
)
256+
}
257+
258+
@OptIn(ExperimentalMaterial3Api::class)
259+
@Composable
260+
private fun Modifier.topAppBarModifier(
261+
scrollBehavior: TopAppBarScrollBehavior,
262+
dividerStyle: TopAppBarDividerStyle,
263+
): Modifier = this
264+
.testTag(tag = "HeaderBarComponent")
265+
.scrolledContainerBottomDivider(
266+
topAppBarScrollBehavior = scrollBehavior,
267+
enabled = when (dividerStyle) {
268+
TopAppBarDividerStyle.NONE -> false
269+
TopAppBarDividerStyle.STATIC -> false
270+
TopAppBarDividerStyle.ON_SCROLL -> true
271+
},
272+
)
273+
.bottomDivider(
274+
enabled = when (dividerStyle) {
275+
TopAppBarDividerStyle.NONE -> false
276+
TopAppBarDividerStyle.STATIC -> true
277+
TopAppBarDividerStyle.ON_SCROLL -> false
278+
},
279+
)
280+
184281
@OptIn(ExperimentalMaterial3Api::class)
185282
@Preview
186283
@Composable

0 commit comments

Comments
 (0)