tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit c2446dba1bbbd73f79803707e2d8a9acd8f15cf8
parent ecc2f60f296d13c9e17b9936257bcab828c5715f
Author: Noah Bond <nbond@mozilla.com>
Date:   Thu, 18 Dec 2025 03:04:12 +0000

Bug 2003682 - Optimize the thumbnail size calculation of the tab grid thumbnails r=android-reviewers,jdelorenzo

During the redesign, we needed some more robust thumbnail calculations in order to support the updated design specs. This led to the individual tab grid items being wrapped with `BoxWithConstraints`, so each item could get its precise measurement to calculate the thumbnail size. However, this led to a performance degradation when combining this into a `LazyVerticalGrid`, as these measurements were occurring frequently and redundantly, as all grid items were of equal size.

To solve for this, the parent `LazyVerticalGrid` is now wrapped by the `BoxWithConstraints`, and the measurement happens once for _all_ tab items.

Differential Revision: https://phabricator.services.mozilla.com/D276571

Diffstat:
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabitems/TabGridItem.kt | 62+++++++++++++++++++++++++++++---------------------------------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabpage/TabLayout.kt | 148++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
2 files changed, 116 insertions(+), 94 deletions(-)

diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabitems/TabGridItem.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabitems/TabGridItem.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -40,7 +39,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -68,11 +66,14 @@ import org.mozilla.fenix.tabstray.TabsTrayTestTag import org.mozilla.fenix.tabstray.ext.toDisplayTitle import org.mozilla.fenix.tabstray.ui.sharedTabTransition import org.mozilla.fenix.theme.FirefoxTheme -import kotlin.math.max import mozilla.components.ui.icons.R as iconsR +/** + * The padding around the thumbnail inside a tab grid item. + */ +val GridItemThumbnailPadding = 4.dp + private val TabContentCardShape = RoundedCornerShape(16.dp) -private val ThumbnailPadding = 4.dp private val ThumbnailShape = RoundedCornerShape( topStart = 4.dp, topEnd = 4.dp, @@ -87,6 +88,7 @@ private val TabHeaderFaviconSize = 12.dp * long clicks, multiple selection, and media controls. * * @param tab The given tab to be render as view a grid item. + * @param thumbnailSizePx The size of the tab's thumbnail in pixels. * @param isSelected Indicates if the item should be render as selected. * @param multiSelectionEnabled Indicates if the item should be render with multi selection options, * enabled. @@ -101,6 +103,7 @@ private val TabHeaderFaviconSize = 12.dp @Composable fun TabGridItem( tab: TabSessionState, + thumbnailSizePx: Int = 50, isSelected: Boolean = false, multiSelectionEnabled: Boolean = false, multiSelectionSelected: Boolean = false, @@ -110,31 +113,24 @@ fun TabGridItem( onClick: (tab: TabSessionState) -> Unit, onLongClick: ((tab: TabSessionState) -> Unit)? = null, ) { - BoxWithConstraints { - val density = LocalDensity.current - val thumbnailWidth = this.constraints.minWidth - with(density) { 2 * ThumbnailPadding.roundToPx() } - val thumbnailHeight = (thumbnailWidth / gridItemAspectRatio).toInt() - val thumbnailSize = max(thumbnailWidth, thumbnailHeight) - - SwipeToDismissBox2( - state = swipeState, - backgroundContent = {}, - onItemDismiss = { - onCloseClick(tab) - }, - ) { - TabContent( - tab = tab, - thumbnailSize = thumbnailSize, - isSelected = isSelected, - multiSelectionEnabled = multiSelectionEnabled, - multiSelectionSelected = multiSelectionSelected, - shouldClickListen = shouldClickListen, - onCloseClick = onCloseClick, - onClick = onClick, - onLongClick = onLongClick, - ) - } + SwipeToDismissBox2( + state = swipeState, + backgroundContent = {}, + onItemDismiss = { + onCloseClick(tab) + }, + ) { + TabContent( + tab = tab, + thumbnailSize = thumbnailSizePx, + isSelected = isSelected, + multiSelectionEnabled = multiSelectionEnabled, + multiSelectionSelected = multiSelectionSelected, + shouldClickListen = shouldClickListen, + onCloseClick = onCloseClick, + onClick = onClick, + onLongClick = onLongClick, + ) } } @@ -206,7 +202,7 @@ private fun TabContent( .wrapContentHeight(), verticalAlignment = Alignment.CenterVertically, ) { - Spacer(modifier = Modifier.width(FirefoxTheme.layout.space.static50 + ThumbnailPadding)) + Spacer(modifier = Modifier.width(FirefoxTheme.layout.space.static50 + GridItemThumbnailPadding)) val icon = tab.content.icon if (icon != null) { @@ -298,7 +294,7 @@ private fun TabContent( Card( modifier = Modifier .aspectRatio(gridItemAspectRatio) - .padding(horizontal = ThumbnailPadding), + .padding(horizontal = GridItemThumbnailPadding), shape = ThumbnailShape, ) { Thumbnail( @@ -307,7 +303,7 @@ private fun TabContent( ) } - Spacer(modifier = Modifier.height(ThumbnailPadding)) + Spacer(modifier = Modifier.height(GridItemThumbnailPadding)) } } } @@ -317,7 +313,7 @@ private fun TabContent( * The width to height ratio of the tab grid item. In landscape mode, the width to height ratio is * 2:1 and in portrait mode, the width to height ratio is 4:5. */ -private val gridItemAspectRatio: Float +val gridItemAspectRatio: Float @Composable @ReadOnlyComposable get() = if (LocalContext.current.isLandscape()) { 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 @@ -10,6 +10,8 @@ import androidx.compose.animation.rememberSplineBasedDecay import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -47,6 +49,7 @@ import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import mozilla.components.browser.state.state.ContentState @@ -61,9 +64,12 @@ import org.mozilla.fenix.tabstray.browser.compose.createGridReorderState import org.mozilla.fenix.tabstray.browser.compose.createListReorderState import org.mozilla.fenix.tabstray.browser.compose.detectGridPressAndDragGestures import org.mozilla.fenix.tabstray.browser.compose.detectListPressAndDrag +import org.mozilla.fenix.tabstray.ui.tabitems.GridItemThumbnailPadding import org.mozilla.fenix.tabstray.ui.tabitems.TabGridItem import org.mozilla.fenix.tabstray.ui.tabitems.TabListItem +import org.mozilla.fenix.tabstray.ui.tabitems.gridItemAspectRatio import org.mozilla.fenix.theme.FirefoxTheme +import kotlin.math.max // Key for the span item at the bottom of the tray, used to make the item not reorderable. const val SPAN_ITEM_KEY = "span" @@ -212,77 +218,97 @@ private fun TabGrid( } } - LazyVerticalGrid( - columns = GridCells.Fixed(count = numberOfGridColumns), - modifier = modifier - .fillMaxSize() - .detectGridPressAndDragGestures( - gridState = state, - reorderState = reorderState, - shouldLongPressToDrag = shouldLongPress, + BoxWithConstraints { + LazyVerticalGrid( + columns = GridCells.Fixed(count = numberOfGridColumns), + modifier = modifier + .fillMaxSize() + .detectGridPressAndDragGestures( + gridState = state, + reorderState = reorderState, + shouldLongPressToDrag = shouldLongPress, + ), + state = state, + contentPadding = PaddingValues( + horizontal = if (LocalContext.current.isLandscape()) { + 52.dp + } else { + FirefoxTheme.layout.space.static200 + }, + vertical = 24.dp, ), - state = state, - contentPadding = PaddingValues( - horizontal = if (LocalContext.current.isLandscape()) { - 52.dp - } else { - FirefoxTheme.layout.space.static200 - }, - vertical = 24.dp, - ), - verticalArrangement = Arrangement.spacedBy(space = FirefoxTheme.layout.space.static200), - horizontalArrangement = Arrangement.spacedBy(space = FirefoxTheme.layout.space.static200), - ) { - header?.let { - item(key = HEADER_ITEM_KEY, span = { GridItemSpan(maxLineSpan) }) { - header() + verticalArrangement = Arrangement.spacedBy(space = FirefoxTheme.layout.space.static200), + horizontalArrangement = Arrangement.spacedBy(space = horizontalGridPadding), + ) { + header?.let { + item(key = HEADER_ITEM_KEY, span = { GridItemSpan(maxLineSpan) }) { + header() + } } - } - itemsIndexed( - items = tabs, - key = { _, tab -> tab.id }, - ) { index, tab -> - val decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay() - val density = LocalDensity.current - val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl - val swipeState = remember(isInMultiSelectMode, !state.isScrollInProgress) { - SwipeToDismissState2( - density = density, - enabled = !isInMultiSelectMode && !state.isScrollInProgress, - decayAnimationSpec = decayAnimationSpec, - isRtl = isRtl, - ) - } - val swipingActive by remember(swipeState.swipingActive) { - mutableStateOf(swipeState.swipingActive) - } + itemsIndexed( + items = tabs, + key = { _, tab -> tab.id }, + ) { index, tab -> + val decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay() + val density = LocalDensity.current + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + val swipeState = remember(isInMultiSelectMode, !state.isScrollInProgress) { + SwipeToDismissState2( + density = density, + enabled = !isInMultiSelectMode && !state.isScrollInProgress, + decayAnimationSpec = decayAnimationSpec, + isRtl = isRtl, + ) + } + val swipingActive by remember(swipeState.swipingActive) { + mutableStateOf(swipeState.swipingActive) + } - DragItemContainer( - state = reorderState, - position = index + if (header != null) 1 else 0, - key = tab.id, - swipingActive = swipingActive, - ) { - TabGridItem( - tab = tab, - isSelected = tab.id == selectedTabId, - multiSelectionEnabled = isInMultiSelectMode, - multiSelectionSelected = selectionMode.selectedTabs.any { it.id == tab.id }, - shouldClickListen = reorderState.draggingItemKey != tab.id, - swipeState = swipeState, - onCloseClick = onTabClose, - onClick = onTabClick, - ) + DragItemContainer( + state = reorderState, + position = index + if (header != null) 1 else 0, + key = tab.id, + swipingActive = swipingActive, + ) { + TabGridItem( + tab = tab, + thumbnailSizePx = thumbnailSizePx, + isSelected = tab.id == selectedTabId, + multiSelectionEnabled = isInMultiSelectMode, + multiSelectionSelected = selectionMode.selectedTabs.any { it.id == tab.id }, + shouldClickListen = reorderState.draggingItemKey != tab.id, + swipeState = swipeState, + onCloseClick = onTabClose, + onClick = onTabClick, + ) + } } - } - item(key = SPAN_ITEM_KEY, span = { GridItemSpan(maxLineSpan) }) { - Spacer(modifier = Modifier.height(tabListBottomPadding)) + item(key = SPAN_ITEM_KEY, span = { GridItemSpan(maxLineSpan) }) { + Spacer(modifier = Modifier.height(tabListBottomPadding)) + } } } } +private val horizontalGridPadding: Dp + @ReadOnlyComposable + @Composable + get() = FirefoxTheme.layout.space.static200 + +private val BoxWithConstraintsScope.thumbnailSizePx: Int + @ReadOnlyComposable + @Composable + get() { + val density = LocalDensity.current + val totalSpacing = horizontalGridPadding * (numberOfGridColumns - 1) + + GridItemThumbnailPadding * numberOfGridColumns * 2 + val thumbnailWidth = constraints.maxWidth - with(density) { totalSpacing.roundToPx() } + val thumbnailHeight = (thumbnailWidth / gridItemAspectRatio).toInt() + return max(thumbnailWidth, thumbnailHeight) + } + @Suppress("LongParameterList", "LongMethod", "CognitiveComplexMethod") @Composable private fun TabList(