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 f82d961c4..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 @@ -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( @@ -740,6 +751,8 @@ fun UnifiedPlayerSheetV2( currentPositionProvider = positionToDisplayProvider, isFavorite = isFavorite, shouldRenderFullPlayer = shouldRenderFullPlayer, + currentHorizontalPaddingStartPxProvider = currentHorizontalPaddingStartPxProvider, + currentHorizontalPaddingEndPxProvider = currentHorizontalPaddingEndPxProvider, onShowQueueClicked = sheetActionHandlers.openQueueSheet, onQueueDragStart = sheetActionHandlers.beginQueueDrag, onQueueDrag = sheetActionHandlers.dragQueueBy, 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)