@@ -20,11 +20,14 @@ import androidx.compose.runtime.remember
2020import androidx.compose.runtime.setValue
2121import androidx.compose.ui.Modifier
2222import androidx.compose.ui.graphics.painter.Painter
23+ import androidx.compose.ui.layout.SubcomposeLayout
2324import androidx.compose.ui.platform.testTag
25+ import androidx.compose.ui.text.TextLayoutResult
2426import androidx.compose.ui.text.style.TextOverflow
2527import androidx.compose.ui.tooling.preview.Preview
2628import androidx.compose.ui.unit.Dp
2729import androidx.compose.ui.unit.dp
30+ import androidx.compose.ui.util.fastMap
2831import com.bitwarden.ui.R
2932import com.bitwarden.ui.platform.base.util.bottomDivider
3033import 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
9297fun 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