commit bb59f1cb896835c631c5b9b0e8b7674c504a1a0d
parent cf6d075e3308f0fb83b37501831a3244a48a6aff
Author: iorgamgabriel <iorgamgabriel@yahoo.com>
Date: Tue, 11 Nov 2025 07:20:18 +0000
Bug 1983111 - [Tab Management Phase 1] Make the status bar transparent when Tab Manager is scrolled. r=android-reviewers,calu
Differential Revision: https://phabricator.services.mozilla.com/D271071
Diffstat:
3 files changed, 206 insertions(+), 143 deletions(-)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/banner/TabsTrayBanner.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/banner/TabsTrayBanner.kt
@@ -8,8 +8,11 @@ package org.mozilla.fenix.tabstray.ui.banner
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CenterAlignedTopAppBar
@@ -75,6 +78,7 @@ private val TabIndicatorRoundedCornerDp = 100.dp
* @param syncedTabCount The total number of open synced tabs.
* @param selectionMode [TabsTrayState.Mode] indicating the current selection mode (e.g., normal, multi-select).
* @param isInDebugMode True for debug variant or if secret menu is enabled for this session.
+ * @param statusBarHeight The height of the system status bar.
* @param shouldShowTabAutoCloseBanner Whether the tab auto-close banner should be displayed.
* @param shouldShowLockPbmBanner Whether the lock private browsing mode banner should be displayed.
* @param scrollBehavior Defines how the [TabPageBanner] should behave when the content under it is scrolled.
@@ -101,6 +105,7 @@ fun TabsTrayBanner(
syncedTabCount: Int,
selectionMode: Mode,
isInDebugMode: Boolean,
+ statusBarHeight: Dp,
shouldShowTabAutoCloseBanner: Boolean,
shouldShowLockPbmBanner: Boolean,
scrollBehavior: TopAppBarScrollBehavior,
@@ -158,6 +163,7 @@ fun TabsTrayBanner(
normalTabCount = normalTabCount,
privateTabCount = privateTabCount,
syncedTabCount = syncedTabCount,
+ statusBarHeight = statusBarHeight,
scrollBehavior = scrollBehavior,
onTabPageIndicatorClicked = onTabPageIndicatorClicked,
)
@@ -167,6 +173,8 @@ fun TabsTrayBanner(
!hasAcknowledgedAutoCloseBanner && showTabAutoCloseBanner -> {
onTabAutoCloseBannerShown()
+ BannerPadding(scrollBehavior = scrollBehavior, statusBarHeight = statusBarHeight)
+
HorizontalDivider()
Banner(
@@ -185,6 +193,8 @@ fun TabsTrayBanner(
}
!hasAcknowledgedPbmLockBanner && shouldShowLockPbmBanner -> {
+ BannerPadding(scrollBehavior = scrollBehavior, statusBarHeight = statusBarHeight)
+
// After this bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1965545
// is resolved, we should swap the button 1 and button 2 click actions.
Banner(
@@ -206,6 +216,18 @@ fun TabsTrayBanner(
}
}
+@Composable
+private fun BannerPadding(
+ scrollBehavior: TopAppBarScrollBehavior,
+ statusBarHeight: Dp,
+) {
+ val padding by remember(statusBarHeight, scrollBehavior.state.collapsedFraction) {
+ derivedStateOf { statusBarHeight * scrollBehavior.state.collapsedFraction }
+ }
+
+ Spacer(modifier = Modifier.height(padding))
+}
+
/**
* Banner displayed when in [Mode.Normal].
*
@@ -213,6 +235,7 @@ fun TabsTrayBanner(
* @param normalTabCount The amount of open Normal tabs.
* @param privateTabCount The amount of open Private tabs.
* @param syncedTabCount The amount of synced tabs.
+ * @param statusBarHeight The height of the system status bar.
* @param scrollBehavior Defines how the [TabPageBanner] should behave when the content under it is scrolled.
* @param onTabPageIndicatorClicked Invoked when the user clicks on a tab page button. Passes along the
* [Page] that was clicked.
@@ -224,6 +247,7 @@ private fun TabPageBanner(
normalTabCount: Int,
privateTabCount: Int,
syncedTabCount: Int,
+ statusBarHeight: Dp,
scrollBehavior: TopAppBarScrollBehavior,
onTabPageIndicatorClicked: (Page) -> Unit,
) {
@@ -232,95 +256,104 @@ private fun TabPageBanner(
CenterAlignedTopAppBar(
title = {
- PrimaryTabRow(
- selectedTabIndex = selectedTabIndex,
- modifier = Modifier.fillMaxWidth(),
- contentColor = MaterialTheme.colorScheme.primary,
- containerColor = Color.Transparent,
- indicator = {
- TabRowDefaults.PrimaryIndicator(
- modifier = Modifier.tabIndicatorOffset(
- selectedTabIndex = selectedTabIndex,
- matchContentSize = true,
- ),
- width = Dp.Unspecified,
- shape = RoundedCornerShape(
- topStart = TabIndicatorRoundedCornerDp,
- topEnd = TabIndicatorRoundedCornerDp,
- ),
- )
- },
- divider = {},
- ) {
- val privateTabDescription = stringResource(
- id = R.string.tabs_header_private_tabs_counter_title,
- privateTabCount.toString(),
- )
- val normalTabDescription = stringResource(
- id = R.string.tabs_header_normal_tabs_counter_title,
- normalTabCount.toString(),
- )
- val syncedTabDescription = stringResource(
- id = R.string.tabs_header_synced_tabs_counter_title,
- syncedTabCount.toString(),
- )
-
- Tab(
- selected = selectedPage == Page.PrivateTabs,
- onClick = { onTabPageIndicatorClicked(Page.PrivateTabs) },
+ Column {
+ Spacer(
modifier = Modifier
- .testTag(TabsTrayTestTag.PRIVATE_TABS_PAGE_BUTTON)
- .semantics {
- contentDescription = privateTabDescription
- }
- .height(RowHeight),
- unselectedContentColor = inactiveColor,
+ .height(statusBarHeight)
+ .fillMaxWidth(),
+ )
+ PrimaryTabRow(
+ selectedTabIndex = selectedTabIndex,
+ modifier = Modifier.fillMaxWidth(),
+ contentColor = MaterialTheme.colorScheme.primary,
+ containerColor = Color.Transparent,
+ indicator = {
+ TabRowDefaults.PrimaryIndicator(
+ modifier = Modifier.tabIndicatorOffset(
+ selectedTabIndex = selectedTabIndex,
+ matchContentSize = true,
+ ),
+ width = Dp.Unspecified,
+ shape = RoundedCornerShape(
+ topStart = TabIndicatorRoundedCornerDp,
+ topEnd = TabIndicatorRoundedCornerDp,
+ ),
+ )
+ },
+ divider = {},
) {
- Text(
- text = stringResource(id = R.string.tabs_header_private_tabs_title),
- style = FirefoxTheme.typography.button,
+ val privateTabDescription = stringResource(
+ id = R.string.tabs_header_private_tabs_counter_title,
+ privateTabCount.toString(),
)
- }
-
- Tab(
- selected = selectedPage == Page.NormalTabs,
- onClick = { onTabPageIndicatorClicked(Page.NormalTabs) },
- modifier = Modifier
- .testTag(TabsTrayTestTag.NORMAL_TABS_PAGE_BUTTON)
- .semantics {
- contentDescription = normalTabDescription
- }
- .height(RowHeight),
- unselectedContentColor = inactiveColor,
- ) {
- Text(
- text = stringResource(R.string.tabs_header_normal_tabs_title),
- style = FirefoxTheme.typography.button,
+ val normalTabDescription = stringResource(
+ id = R.string.tabs_header_normal_tabs_counter_title,
+ normalTabCount.toString(),
)
- }
-
- Tab(
- selected = selectedPage == Page.SyncedTabs,
- onClick = { onTabPageIndicatorClicked(Page.SyncedTabs) },
- modifier = Modifier
- .testTag(TabsTrayTestTag.SYNCED_TABS_PAGE_BUTTON)
- .semantics {
- contentDescription = syncedTabDescription
- }
- .height(RowHeight),
- unselectedContentColor = inactiveColor,
- ) {
- Text(
- text = stringResource(id = R.string.tabs_header_synced_tabs_title),
- style = FirefoxTheme.typography.button,
+ val syncedTabDescription = stringResource(
+ id = R.string.tabs_header_synced_tabs_counter_title,
+ syncedTabCount.toString(),
)
+
+ Tab(
+ selected = selectedPage == Page.PrivateTabs,
+ onClick = { onTabPageIndicatorClicked(Page.PrivateTabs) },
+ modifier = Modifier
+ .testTag(TabsTrayTestTag.PRIVATE_TABS_PAGE_BUTTON)
+ .semantics {
+ contentDescription = privateTabDescription
+ }
+ .height(RowHeight),
+ unselectedContentColor = inactiveColor,
+ ) {
+ Text(
+ text = stringResource(id = R.string.tabs_header_private_tabs_title),
+ style = FirefoxTheme.typography.button,
+ )
+ }
+
+ Tab(
+ selected = selectedPage == Page.NormalTabs,
+ onClick = { onTabPageIndicatorClicked(Page.NormalTabs) },
+ modifier = Modifier
+ .testTag(TabsTrayTestTag.NORMAL_TABS_PAGE_BUTTON)
+ .semantics {
+ contentDescription = normalTabDescription
+ }
+ .height(RowHeight),
+ unselectedContentColor = inactiveColor,
+ ) {
+ Text(
+ text = stringResource(R.string.tabs_header_normal_tabs_title),
+ style = FirefoxTheme.typography.button,
+ )
+ }
+
+ Tab(
+ selected = selectedPage == Page.SyncedTabs,
+ onClick = { onTabPageIndicatorClicked(Page.SyncedTabs) },
+ modifier = Modifier
+ .testTag(TabsTrayTestTag.SYNCED_TABS_PAGE_BUTTON)
+ .semantics {
+ contentDescription = syncedTabDescription
+ }
+ .height(RowHeight),
+ unselectedContentColor = inactiveColor,
+ ) {
+ Text(
+ text = stringResource(id = R.string.tabs_header_synced_tabs_title),
+ style = FirefoxTheme.typography.button,
+ )
+ }
}
}
},
- expandedHeight = RowHeight,
+ expandedHeight = RowHeight + statusBarHeight,
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
),
+ // Allow this TopAppBar to be drawn behind the status bar instead of stopping at it.
+ windowInsets = TopAppBarDefaults.windowInsets.only(WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior,
)
}
@@ -540,6 +573,7 @@ private fun TabsTrayBannerPreviewRoot(
syncedTabCount = 0,
selectionMode = state.mode,
isInDebugMode = false,
+ statusBarHeight = 50.dp,
shouldShowTabAutoCloseBanner = shouldShowTabAutoCloseBanner,
shouldShowLockPbmBanner = shouldShowLockPbmBanner,
scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(),
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabpage/TabLayout.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabpage/TabLayout.kt
@@ -335,7 +335,6 @@ private fun TabList(
.padding(
start = TabListPadding,
end = TabListPadding,
- top = TabListPadding,
)
.clip(TabListCornerShape)
.background(MaterialTheme.colorScheme.surface)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabstray/TabsTray.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabstray/TabsTray.kt
@@ -6,9 +6,17 @@
package org.mozilla.fenix.tabstray.ui.tabstray
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.BottomAppBarDefaults
@@ -68,6 +76,7 @@ import org.mozilla.fenix.tabstray.ui.syncedtabs.OnTabCloseClick as OnSyncedTabCl
* BottomAppBar.
*/
private val ScaffoldFabOffsetCorrection = 4.dp
+private const val SPACER_BACKGROUND_ALPHA = 0.75f
/**
* Top-level UI for displaying the Tabs Tray feature.
@@ -188,6 +197,11 @@ fun TabsTray(
.sumOf { deviceSection: SyncedTabsListItem.DeviceSection -> deviceSection.tabs.size }
}
+ val systemBarsInsets = WindowInsets.systemBars.asPaddingValues()
+ val statusBarHeight = remember(systemBarsInsets) {
+ systemBarsInsets.calculateTopPadding()
+ }
+
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val bottomAppBarScrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
@@ -216,6 +230,7 @@ fun TabsTray(
syncedTabCount = syncedTabCount,
selectionMode = tabsTrayState.mode,
isInDebugMode = isInDebugMode,
+ statusBarHeight = statusBarHeight,
shouldShowTabAutoCloseBanner = shouldShowTabAutoCloseBanner,
shouldShowLockPbmBanner = shouldShowLockPbmBanner,
scrollBehavior = topAppBarScrollBehavior,
@@ -261,71 +276,83 @@ fun TabsTray(
floatingActionButtonPosition = FabPosition.EndOverlay,
containerColor = MaterialTheme.colorScheme.surface,
) { paddingValues ->
- HorizontalPager(
- modifier = Modifier
- .padding(paddingValues)
- .fillMaxSize(),
- state = pagerState,
- beyondViewportPageCount = 2,
- userScrollEnabled = false,
- ) { position ->
- when (Page.positionToPage(position)) {
- Page.NormalTabs -> {
- NormalTabsPage(
- normalTabs = tabsTrayState.normalTabs,
- inactiveTabs = tabsTrayState.inactiveTabs,
- selectedTabId = tabsTrayState.selectedTabId,
- selectionMode = tabsTrayState.mode,
- inactiveTabsExpanded = tabsTrayState.inactiveTabsExpanded,
- displayTabsInGrid = displayTabsInGrid,
- onTabClose = onTabClose,
- onTabClick = onTabClick,
- onTabLongClick = onTabLongClick,
- shouldShowInactiveTabsAutoCloseDialog = shouldShowInactiveTabsAutoCloseDialog,
- onInactiveTabsHeaderClick = onInactiveTabsHeaderClick,
- onDeleteAllInactiveTabsClick = onDeleteAllInactiveTabsClick,
- onInactiveTabsAutoCloseDialogShown = onInactiveTabsAutoCloseDialogShown,
- onInactiveTabAutoCloseDialogCloseButtonClick = onInactiveTabAutoCloseDialogCloseButtonClick,
- onEnableInactiveTabAutoCloseClick = onEnableInactiveTabAutoCloseClick,
- onInactiveTabClick = onInactiveTabClick,
- onInactiveTabClose = onInactiveTabClose,
- onMove = onMove,
- shouldShowInactiveTabsCFR = shouldShowInactiveTabsCFR,
- onInactiveTabsCFRShown = onInactiveTabsCFRShown,
- onInactiveTabsCFRClick = onInactiveTabsCFRClick,
- onInactiveTabsCFRDismiss = onInactiveTabsCFRDismiss,
- onTabDragStart = {
- tabsTrayStore.dispatch(TabsTrayAction.ExitSelectMode)
- },
- )
- }
+ Box {
+ HorizontalPager(
+ modifier = Modifier
+ .padding(paddingValues)
+ .fillMaxSize(),
+ state = pagerState,
+ beyondViewportPageCount = 2,
+ userScrollEnabled = false,
+ ) { position ->
+ when (Page.positionToPage(position)) {
+ Page.NormalTabs -> {
+ NormalTabsPage(
+ normalTabs = tabsTrayState.normalTabs,
+ inactiveTabs = tabsTrayState.inactiveTabs,
+ selectedTabId = tabsTrayState.selectedTabId,
+ selectionMode = tabsTrayState.mode,
+ inactiveTabsExpanded = tabsTrayState.inactiveTabsExpanded,
+ displayTabsInGrid = displayTabsInGrid,
+ onTabClose = onTabClose,
+ onTabClick = onTabClick,
+ onTabLongClick = onTabLongClick,
+ shouldShowInactiveTabsAutoCloseDialog = shouldShowInactiveTabsAutoCloseDialog,
+ onInactiveTabsHeaderClick = onInactiveTabsHeaderClick,
+ onDeleteAllInactiveTabsClick = onDeleteAllInactiveTabsClick,
+ onInactiveTabsAutoCloseDialogShown = onInactiveTabsAutoCloseDialogShown,
+ onInactiveTabAutoCloseDialogCloseButtonClick = onInactiveTabAutoCloseDialogCloseButtonClick,
+ onEnableInactiveTabAutoCloseClick = onEnableInactiveTabAutoCloseClick,
+ onInactiveTabClick = onInactiveTabClick,
+ onInactiveTabClose = onInactiveTabClose,
+ onMove = onMove,
+ shouldShowInactiveTabsCFR = shouldShowInactiveTabsCFR,
+ onInactiveTabsCFRShown = onInactiveTabsCFRShown,
+ onInactiveTabsCFRClick = onInactiveTabsCFRClick,
+ onInactiveTabsCFRDismiss = onInactiveTabsCFRDismiss,
+ onTabDragStart = {
+ tabsTrayStore.dispatch(TabsTrayAction.ExitSelectMode)
+ },
+ )
+ }
- Page.PrivateTabs -> {
- PrivateTabsPage(
- privateTabs = tabsTrayState.privateTabs,
- selectedTabId = tabsTrayState.selectedTabId,
- selectionMode = tabsTrayState.mode,
- displayTabsInGrid = displayTabsInGrid,
- privateTabsLocked = isPbmLocked,
- onTabClose = onTabClose,
- onTabClick = onTabClick,
- onTabLongClick = onTabLongClick,
- onMove = onMove,
- onUnlockPbmClick = onUnlockPbmClick,
- )
- }
+ Page.PrivateTabs -> {
+ PrivateTabsPage(
+ privateTabs = tabsTrayState.privateTabs,
+ selectedTabId = tabsTrayState.selectedTabId,
+ selectionMode = tabsTrayState.mode,
+ displayTabsInGrid = displayTabsInGrid,
+ privateTabsLocked = isPbmLocked,
+ onTabClose = onTabClose,
+ onTabClick = onTabClick,
+ onTabLongClick = onTabLongClick,
+ onMove = onMove,
+ onUnlockPbmClick = onUnlockPbmClick,
+ )
+ }
- Page.SyncedTabs -> {
- SyncedTabsPage(
- isSignedIn = isSignedIn,
- syncedTabs = tabsTrayState.syncedTabs,
- onTabClick = onSyncedTabClick,
- onTabClose = onSyncedTabClose,
- onSignInClick = onSignInClick,
- )
+ Page.SyncedTabs -> {
+ SyncedTabsPage(
+ isSignedIn = isSignedIn,
+ syncedTabs = tabsTrayState.syncedTabs,
+ onTabClick = onSyncedTabClick,
+ onTabClose = onSyncedTabClose,
+ onSignInClick = onSignInClick,
+ )
+ }
}
}
}
+ Spacer(
+ Modifier
+ .height(statusBarHeight)
+ .fillMaxWidth()
+ .background(
+ MaterialTheme.colorScheme.surface.copy(
+ SPACER_BACKGROUND_ALPHA,
+ ),
+ ),
+ )
}
}
@@ -526,9 +553,9 @@ private class TabsTrayStateParameterProvider : PreviewParameterProvider<TabsTray
showInactiveTabsAutoCloseDialog = true,
),
// TabsTray Private Tabs Preview
- TabsTrayPreviewModel(
- selectedPage = Page.PrivateTabs,
- privateTabs = generateFakeTabsList(isPrivate = true),
+ TabsTrayPreviewModel(
+ selectedPage = Page.PrivateTabs,
+ privateTabs = generateFakeTabsList(isPrivate = true),
),
// TabsTray Synced Tab Preview
TabsTrayPreviewModel(
@@ -569,7 +596,10 @@ private data class TabsTrayPreviewModel(
val isSignedIn: Boolean = true,
)
-private fun generateFakeTabsList(tabCount: Int = 10, isPrivate: Boolean = false): List<TabSessionState> =
+private fun generateFakeTabsList(
+ tabCount: Int = 10,
+ isPrivate: Boolean = false,
+): List<TabSessionState> =
List(tabCount) { index ->
TabSessionState(
id = "tabId$index-$isPrivate",