diff --git a/shared/src/commonMain/kotlin/dev/bilby/ui/player/PlayerShell.kt b/shared/src/commonMain/kotlin/dev/bilby/ui/player/PlayerShell.kt index 8add06a..495f72e 100644 --- a/shared/src/commonMain/kotlin/dev/bilby/ui/player/PlayerShell.kt +++ b/shared/src/commonMain/kotlin/dev/bilby/ui/player/PlayerShell.kt @@ -62,8 +62,11 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.foundation.focusable @@ -131,6 +134,11 @@ class PlayerShellScope internal constructor( val positionMillis: Long, /** 已缓冲到哪儿,画在进度条上。拖拽中不跟着拖走 —— 它说的是流,不是手指。 */ val bufferedPositionMillis: Long, + /** + * 底部控制条实测的高度。overlay 层要让开控制条的东西(字幕)按它抬,不再猜档位 —— + * 一行与两行排法差着五十来 dp,常量只按得了一种。收起着时保留上一次的高度。 + */ + val controlBarHeight: Dp, val durationMillis: Long, val speed: Float, internal val actions: PlayerShellActions, @@ -336,6 +344,8 @@ fun PlayerShell( var duration by remember { mutableLongStateOf(0L) } var dragPosition by remember { mutableStateOf(null) } var resumeAfterDrag by remember { mutableStateOf(false) } + var controlBarHeight by remember(isFullscreen, pip) { mutableStateOf(0.dp) } + val density = LocalDensity.current val userSpeed = speedController?.speed ?: player.playbackSpeed @@ -589,6 +599,7 @@ fun PlayerShell( controlsVisible = controlsVisible, positionMillis = displayPosition, bufferedPositionMillis = bufferedPosition, + controlBarHeight = controlBarHeight, durationMillis = duration, speed = userSpeed, actions = actions, @@ -1136,7 +1147,9 @@ fun PlayerShell( exit = slideOutVertically(spatialOffsetSpec) { it / 2 } + fadeOut(effectsSpec), modifier = Modifier.align(Alignment.BottomCenter).hoverable(controlsHover), ) { - scope.controlBar() + Box(Modifier.onSizeChanged { controlBarHeight = with(density) { it.height.toDp() } }) { + scope.controlBar() + } } if (speedController != null && !pip) { diff --git a/shared/src/commonMain/kotlin/dev/bilby/ui/video/BilbyPlayer.kt b/shared/src/commonMain/kotlin/dev/bilby/ui/video/BilbyPlayer.kt index f2e20d5..d0eff8c 100644 --- a/shared/src/commonMain/kotlin/dev/bilby/ui/video/BilbyPlayer.kt +++ b/shared/src/commonMain/kotlin/dev/bilby/ui/video/BilbyPlayer.kt @@ -103,9 +103,12 @@ private val ControlRowOverlap = 12.dp * 字幕离画面底沿多远。两档:控制条在屏上时要整体抬到它上面去,不然字幕压在进度条和 * 那排按钮上;控制条收起后只留一点边距,贴太近会被圆角或手势条切到。 * - * 88:控制条最矮的形态(内嵌的一行)约 76dp 高,再留一档间距。 + * 抬起量取实测的 [PlayerShellScope.controlBarHeight] 加一档间距 —— 一行与两行排法差着五十来 + * dp,常量按单行写死,竖屏全屏两行时字幕就被压在进度条上。88 是兜底:控制条首次展开、高度 + * 还没量到的那几帧别让它反而沉下去(单行内嵌约 76dp 加间距)。 */ private val SubtitleBottomWithControls = 88.dp +private val SubtitleControlsGap = Spacing.Tight private val SubtitleBottomBare = 24.dp /** 字幕轨启用时,右下角投票面板抬升让开一行字幕槽的高度,避免窄宽度下与居中字幕横向重叠。 */ private val SubtitleBandClearance = 48.dp @@ -271,7 +274,11 @@ fun BilbyPlayer( // 让开控制条那一下要跟着控制条一起动。控制条自己是 spring 展开的([PlayerShell]), // 字幕原先是硬切:点一下画面,控制条缓缓升起,而字幕已经在上一帧跳到了新位置。 // 用 spatial 那一档 —— 这是位移,不是透明度。 - val subtitleBottomTarget = if (controlsVisible && !locked) SubtitleBottomWithControls else SubtitleBottomBare + val subtitleBottomTarget = if (controlsVisible && !locked) { + maxOf(SubtitleBottomWithControls, controlBarHeight + SubtitleControlsGap) + } else { + SubtitleBottomBare + } val subtitleBottom by animateDpAsState( targetValue = subtitleBottomTarget, animationSpec = MaterialTheme.motionScheme.fastSpatialSpec(), @@ -401,13 +408,14 @@ fun BilbyPlayer( * * 两种排法,按画面大小分: * - * - **窄**(单栏内嵌,以及竖屏视频的全屏):一行 ——「时间 进度条 时长 [弹幕] ⚙ ⛶」。倍速、 - * 清晰度、字幕收进 ⚙ 打开的整块设置面板。内嵌画面只有两百来 dp 高,两行控制条加上顶上的 - * 返回键,中间就放不下播放键了。 - * - **宽**(全屏,或双栏里的内嵌画面,且宽度够):进度条独占一行,下面一行是读数和几枚写着 - * 当前档位的 chip(「1.5x」「1080P」),倍速打开贴边选择器,其余各开面板中对应的一段; - * 弹幕开关旁边的 ⚙ 只开弹幕那一段。 - * 不只看宽度:单栏横屏的内嵌画面也有四五百 dp 宽,高度却放不下两行。 + * - **窄**(单栏内嵌):一行 ——「时间 进度条 时长 [弹幕] ⚙ ⛶」。倍速、清晰度、字幕收进 ⚙ + * 打开的整块设置面板。内嵌画面只有两百来 dp 高,两行控制条加上顶上的返回键,中间就放不下 + * 播放键了。 + * - **堆叠**(全屏 —— 横竖屏皆是,以及双栏里的内嵌画面):进度条独占一行,下面一行是读数 + * 与控件。全屏后进度条是这一屏最主要的操作,单行排法里它被挤在时间和三枚按钮中间只剩 + * 两百来 dp,拖着费劲;独占一行后整屏宽都是它的。控件按宽度分两档:够宽(≥480)时换成 + * 写着当前档位的 chip(「1.5x」「1080P」)加弹幕设置键;竖屏手机的宽度放不下那些,保持 + * 紧凑的 [弹幕] ⚙ ⛶ 三键,档位照常在面板里调。 * * 原先是一行按钮加一套"量一遍装不装得下、装不下就给不给档名、再装不下就分两行"的估算, * 窄屏上是六七个一样大的白色线框挤在右下角。现在窄的时候档位全在面板里,估算就不需要了。 @@ -474,7 +482,9 @@ private fun PlayerControlBar( } BoxWithConstraints(modifier = container) { - val wide = largePlayer && !pip && maxWidth >= Breakpoints.StackedControlBar + // 堆叠:全屏或双栏里占满整列的内嵌画面,进度条才值得独占一行。宽一档(≥480)再摆 chip。 + val stacked = largePlayer && !pip + val wide = stacked && maxWidth >= Breakpoints.StackedControlBar val roomy = maxWidth >= InlineDanmakuBarMinWidth var composing by remember { mutableStateOf(false) } if (pip) { @@ -483,7 +493,7 @@ private fun PlayerControlBar( seekBar(Modifier.weight(1f)) TimeLabel(duration, secondary = true, modifier = Modifier.padding(end = Spacing.Tight)) } - } else if (!wide) { + } else if (!stacked) { Row(verticalAlignment = Alignment.CenterVertically) { if (composing && danmakuField != null) { ControlBarDanmakuField( @@ -534,14 +544,8 @@ private fun PlayerControlBar( // 「发弹幕」键,点开之后 chip 让位、输入框铺开,同窄屏胶囊写弹幕时视图切换退场。 val showField = danmakuField != null && (roomy || composing) Row(verticalAlignment = Alignment.CenterVertically) { - Row( - verticalAlignment = Alignment.Bottom, - modifier = Modifier.padding(start = Spacing.Tight), - ) { - TimeLabel(position, large = true) - TimeLabel(duration, large = true, secondary = true, prefix = " / ") - } - if (showField) { + if (composing && danmakuField != null && !wide) { + // 不够宽时写弹幕:输入框顶掉读数铺整行,同上面窄排法。 ControlBarDanmakuField( draft = danmakuField.draft, onDraftChange = danmakuField.onDraftChange, @@ -554,13 +558,38 @@ private fun PlayerControlBar( danmakuField.onComposingChange(focused) if (!focused) composing = false }, - autoFocus = !roomy, + autoFocus = true, modifier = Modifier.weight(1f).padding(horizontal = Spacing.Cozy), ) } else { - Spacer(Modifier.weight(1f)) + Row( + verticalAlignment = Alignment.Bottom, + modifier = Modifier.padding(start = Spacing.Tight), + ) { + TimeLabel(position, large = true) + TimeLabel(duration, large = true, secondary = true, prefix = " / ") + } + if (showField) { + ControlBarDanmakuField( + draft = danmakuField.draft, + onDraftChange = danmakuField.onDraftChange, + maxLength = danmakuField.maxLength, + sending = danmakuField.sending, + error = danmakuField.error, + onSend = danmakuField.onSend, + onFocusChange = { focused -> + onDanmakuFieldFocusChange(focused) + danmakuField.onComposingChange(focused) + if (!focused) composing = false + }, + autoFocus = !roomy, + modifier = Modifier.weight(1f).padding(horizontal = Spacing.Cozy), + ) + } else { + Spacer(Modifier.weight(1f)) + } } - if (roomy || !composing) { + if (wide && (roomy || !composing)) { SettingChips( speed = speed, // 只有一档时没什么可换,chip 不画(面板里那一段同样不画)。 @@ -579,7 +608,11 @@ private fun PlayerControlBar( } } DanmakuButton(danmakuEnabled, onDanmakuEnabledChange, isFullscreen) - if (hasDanmakuSettings) DanmakuSettingsButton { onOpenSettings(PlayerSettingsSection.Danmaku) } + if (wide) { + if (hasDanmakuSettings) DanmakuSettingsButton { onOpenSettings(PlayerSettingsSection.Danmaku) } + } else { + PlayerSettingsButton { onOpenSettings(null) } + } FullscreenButton(isFullscreen, onFullscreenToggle) } }