From a01b83af5510b0ad24b9acb6a1f5eae09bc350b5 Mon Sep 17 00:00:00 2001 From: theov Date: Fri, 5 Jun 2026 15:52:53 -0300 Subject: [PATCH 1/2] perf: expand player sheet benchmarks and optimize expansion layout Updates the benchmark suite to cover multiple audio formats and refactors the player sheet layout logic to improve animation stability and prevent jank during transitions. - Added specialized macrobenchmark tests for FLAC, MP3, M4A, and OPUS playlists in `PlayerSheetAnimationBenchmarks`. - Implemented `playFromPlaylist` and navigation helpers in `MacrobenchmarkScope` to automate playlist selection using localized text patterns or coordinate-based fallbacks. - Refactored `UnifiedPlayerSheetV2` layout constraints to use full-screen width measurement during expansion, applying a negative horizontal offset to maintain alignment while preventing layout shifts. - Simplified backstack navigation in `BaselineProfileGenerator` by making `pressBackAndWait` unconditional for optional surfaces. - Expanded UI automation patterns to include localized strings for "Expand menu" and "Playlists" across multiple languages. --- .../components/UnifiedPlayerSheetV2.kt | 13 ++- .../BaselineProfileGenerator.kt | 7 +- .../PlayerSheetAnimationBenchmarks.kt | 101 +++++++++++++++++- 3 files changed, 112 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt index f82d961c4..c1af80c43 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt @@ -688,16 +688,27 @@ fun UnifiedPlayerSheetV2( // Measures the actual player content with full screen height targetContentHeightPx // so that it can render correctly, while reporting targetHeightPx to the outer // clip/background/shadow so that they are perfectly constrained to the miniplayer card bounds. + // During drag/animation, we measure at stable full-screen constraints to prevent jank. .layout { measurable, constraints -> val targetContentHeightPx = containerHeight.roundToPx() + val fraction = playerContentExpansionFraction.value + val startPaddingPx = currentHorizontalPaddingStartPxProvider().toInt() + val measureWidth = if (fraction > 0f) { + screenWidthPx.roundToInt() + } else { + constraints.maxWidth + } val placeable = measurable.measure( constraints.copy( + minWidth = measureWidth, + maxWidth = measureWidth, minHeight = targetContentHeightPx, maxHeight = targetContentHeightPx ) ) layout(constraints.maxWidth, constraints.maxHeight) { - placeable.placeRelative(0, 0) + val xOffset = if (fraction > 0f) -startPaddingPx else 0 + placeable.placeRelative(xOffset, 0) } } .miniPlayerDismissHorizontalGesture( diff --git a/baselineprofile/src/main/java/com/theveloper/pixelplay/baselineprofile/BaselineProfileGenerator.kt b/baselineprofile/src/main/java/com/theveloper/pixelplay/baselineprofile/BaselineProfileGenerator.kt index 0d0ad42a4..67d65920d 100644 --- a/baselineprofile/src/main/java/com/theveloper/pixelplay/baselineprofile/BaselineProfileGenerator.kt +++ b/baselineprofile/src/main/java/com/theveloper/pixelplay/baselineprofile/BaselineProfileGenerator.kt @@ -374,13 +374,8 @@ class BaselineProfileGenerator { return } waitForUi(EXTRA_UI_WAIT_MS) - val openedBackStackSurface = hasTextOrDescription(pattern(BACK_NAV_ALTERNATIVES)) body() - if (openedBackStackSurface || hasTextOrDescription(pattern(BACK_NAV_ALTERNATIVES))) { - pressBackAndWait() - } else { - Log.d(TAG, "Optional surface '$labelPattern' did not navigate; staying on current app surface.") - } + pressBackAndWait() } private fun MacrobenchmarkScope.openSettingsFromHome(): Boolean { diff --git a/baselineprofile/src/main/java/com/theveloper/pixelplay/baselineprofile/PlayerSheetAnimationBenchmarks.kt b/baselineprofile/src/main/java/com/theveloper/pixelplay/baselineprofile/PlayerSheetAnimationBenchmarks.kt index dfe2795f4..f449d7785 100644 --- a/baselineprofile/src/main/java/com/theveloper/pixelplay/baselineprofile/PlayerSheetAnimationBenchmarks.kt +++ b/baselineprofile/src/main/java/com/theveloper/pixelplay/baselineprofile/PlayerSheetAnimationBenchmarks.kt @@ -14,6 +14,7 @@ import androidx.test.uiautomator.UiObject2 import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import java.util.Locale import java.util.regex.Pattern @RunWith(AndroidJUnit4::class) @@ -25,6 +26,30 @@ class PlayerSheetAnimationBenchmarks { @Test fun playerSheetOpenCloseGestures() { + runPlayerSheetBenchmarkForPlaylist(null) + } + + @Test + fun playerSheetOpenCloseGestures_FLAC() { + runPlayerSheetBenchmarkForPlaylist("FLAC") + } + + @Test + fun playerSheetOpenCloseGestures_MP3() { + runPlayerSheetBenchmarkForPlaylist("MP3") + } + + @Test + fun playerSheetOpenCloseGestures_M4A() { + runPlayerSheetBenchmarkForPlaylist("M4A") + } + + @Test + fun playerSheetOpenCloseGestures_OPUS() { + runPlayerSheetBenchmarkForPlaylist("OPUS") + } + + private fun runPlayerSheetBenchmarkForPlaylist(playlistName: String?) { val packageName = benchmarkTargetPackageName() benchmarkRule.measureRepeated( @@ -54,7 +79,11 @@ class PlayerSheetAnimationBenchmarks { Thread.sleep(BENCHMARK_REBUILD_WAIT_MS) dismissBenchmarkBlockingDialogs() } - ensureSongIsReady() + if (playlistName != null) { + playFromPlaylist(playlistName) + } else { + ensureSongIsReady() + } libraryRebuiltForThisRun = true openHomeTab() waitForSheetState(SHEET_COLLAPSED_PATTERN, "setup after opening Home") @@ -85,6 +114,71 @@ class PlayerSheetAnimationBenchmarks { } } + private fun androidx.benchmark.macro.MacrobenchmarkScope.playFromPlaylist(playlistName: String) { + if (isExpandedSheetVisible()) { + collapseExpandedPlayer() + waitForSheetState(SHEET_COLLAPSED_PATTERN, "collapsing existing player during setup") + } + + openLibraryTab() + waitForLibraryContent() + + openLibraryTabDropdown() + selectPlaylistsTabInSheet() + + Thread.sleep(DEFAULT_WAIT_MS) + + val playlistButton = findByTextOrDescription(pattern(playlistName), SHORT_WAIT_MS) + if (playlistButton != null) { + click(playlistButton) + } else { + val y = when (playlistName.uppercase(Locale.US)) { + "FLAC" -> 300 + "M4A" -> 410 + "MP3" -> 520 + "OPUS" -> 630 + else -> 300 + } + tap(device.displayWidth / 2, y) + } + Thread.sleep(DEFAULT_WAIT_MS) + + tap(device.displayWidth / 2, (device.displayHeight * 0.30f).toInt()) + waitForAnySheetState("after selecting first song in playlist $playlistName") + + if (isExpandedSheetVisible()) { + collapseExpandedPlayer() + waitForSheetState(SHEET_COLLAPSED_PATTERN, "collapsing newly selected song") + } + + if (!isCollapsedSheetVisible()) { + throw IllegalStateException( + "A playlist song was tapped, but the UnifiedPlayerSheetV2 mini-player did not appear. " + + "Visible UI: ${visibleUiSnapshot()}" + ) + } + } + + private fun androidx.benchmark.macro.MacrobenchmarkScope.openLibraryTabDropdown() { + findByTextOrDescription(EXPAND_MENU_PATTERN, SHORT_WAIT_MS)?.let { + click(it) + Thread.sleep(DEFAULT_WAIT_MS) + return + } + tap((device.displayWidth * 0.52f).toInt(), 100) + Thread.sleep(DEFAULT_WAIT_MS) + } + + private fun androidx.benchmark.macro.MacrobenchmarkScope.selectPlaylistsTabInSheet() { + findByTextOrDescription(PLAYLISTS_TAB_GRID_PATTERN, SHORT_WAIT_MS)?.let { + click(it) + Thread.sleep(DEFAULT_WAIT_MS) + return + } + tap((device.displayWidth * 0.75f).toInt(), (device.displayHeight * 0.27f).toInt()) + Thread.sleep(DEFAULT_WAIT_MS) + } + private fun androidx.benchmark.macro.MacrobenchmarkScope.ensureSongIsReady() { if (isCollapsedSheetVisible()) return if (isExpandedSheetVisible()) { @@ -212,7 +306,6 @@ class PlayerSheetAnimationBenchmarks { return } repeat(4) { - if (!hasTextOrDescription(BACK_PATTERN)) return@repeat device.pressKeyCode(KeyEvent.KEYCODE_BACK) Thread.sleep(DEFAULT_WAIT_MS) findByTextOrDescription(HOME_TAB_PATTERN, TINY_WAIT_MS)?.let { home -> @@ -393,6 +486,10 @@ class PlayerSheetAnimationBenchmarks { private val EMPTY_LIBRARY_PATTERN = pattern( "No songs|No valid songs|Sin canciones|No se encontraron canciones|Empty" ) + private val EXPAND_MENU_PATTERN = pattern( + "Expand menu|Expandir men[uú]|Men[uü] aufklappen|D[eé]velopper le menu|Perluas menu|Espandi menu|메뉴 확장|Utvid meny|Развернуть menu|展开菜单" + ) + private val PLAYLISTS_TAB_GRID_PATTERN = pattern("PLAYLISTS|Playlists|Listas de reproducci[oó]n") private fun pattern(alternatives: String): Pattern = Pattern.compile(".*($alternatives).*", Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE) From 8a62bbacf1f464d5246a1a4f822d2cb3a080d026 Mon Sep 17 00:00:00 2001 From: theov Date: Fri, 5 Jun 2026 16:09:19 -0300 Subject: [PATCH 2/2] ui: implement dynamic horizontal padding for mini player expansion Refactors the mini player layout within the unified player sheet to dynamically adjust its width and horizontal position based on expansion progress. This ensures the mini player correctly accounts for screen padding as it transitions to the full player view. - Added `currentHorizontalPaddingStartPxProvider` and `currentHorizontalPaddingEndPxProvider` parameters to the player layer components. - Implemented a custom `.layout` modifier in `UnifiedPlayerSheetLayers.kt` to calculate and apply horizontal offsets and width constraints during expansion. - Used the `playerContentExpansionFraction` to conditionally apply padding, ensuring the mini player maintains its intended width and alignment. - Updated `UnifiedPlayerSheetV2.kt` to pass the padding providers into the player sheet's layer hierarchy. --- .../components/UnifiedPlayerSheetLayers.kt | 24 +++++++++++++++++++ .../components/UnifiedPlayerSheetV2.kt | 2 ++ 2 files changed, 26 insertions(+) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetLayers.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetLayers.kt index f5fcfd550..2aed35222 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetLayers.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetLayers.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.layout import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp @@ -62,6 +63,8 @@ internal fun BoxScope.UnifiedPlayerMiniAndFullLayers( currentPositionProvider: () -> Long, isFavorite: Boolean, shouldRenderFullPlayer: Boolean = true, + currentHorizontalPaddingStartPxProvider: () -> Float, + currentHorizontalPaddingEndPxProvider: () -> Float, onShowQueueClicked: () -> Unit, onQueueDragStart: () -> Unit, onQueueDrag: (Float) -> Unit, @@ -89,6 +92,27 @@ internal fun BoxScope.UnifiedPlayerMiniAndFullLayers( alpha = (1f - playerContentExpansionFraction.value * 2f) .coerceIn(0f, 1f) } + .layout { measurable, constraints -> + val fraction = playerContentExpansionFraction.value + val startPaddingPx = currentHorizontalPaddingStartPxProvider().toInt().coerceAtLeast(0) + val endPaddingPx = currentHorizontalPaddingEndPxProvider().toInt().coerceAtLeast(0) + + val targetWidth = if (fraction > 0f) { + (constraints.maxWidth - startPaddingPx - endPaddingPx).coerceAtLeast(0) + } else { + constraints.maxWidth + } + val placeable = measurable.measure( + constraints.copy( + minWidth = targetWidth, + maxWidth = targetWidth + ) + ) + layout(constraints.maxWidth, constraints.maxHeight) { + val xOffset = if (fraction > 0f) startPaddingPx else 0 + placeable.placeRelative(xOffset, 0) + } + } .zIndex(miniPlayerZIndex) ) { val isMiniPlayerVisible by remember { diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt index c1af80c43..dfd3cd1b2 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/UnifiedPlayerSheetV2.kt @@ -751,6 +751,8 @@ fun UnifiedPlayerSheetV2( currentPositionProvider = positionToDisplayProvider, isFavorite = isFavorite, shouldRenderFullPlayer = shouldRenderFullPlayer, + currentHorizontalPaddingStartPxProvider = currentHorizontalPaddingStartPxProvider, + currentHorizontalPaddingEndPxProvider = currentHorizontalPaddingEndPxProvider, onShowQueueClicked = sheetActionHandlers.openQueueSheet, onQueueDragStart = sheetActionHandlers.beginQueueDrag, onQueueDrag = sheetActionHandlers.dragQueueBy,