Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion shared/src/commonMain/kotlin/dev/bilby/ui/player/PlayerShell.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -336,6 +344,8 @@ fun PlayerShell(
var duration by remember { mutableLongStateOf(0L) }
var dragPosition by remember { mutableStateOf<Long?>(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

Expand Down Expand Up @@ -589,6 +599,7 @@ fun PlayerShell(
controlsVisible = controlsVisible,
positionMillis = displayPosition,
bufferedPositionMillis = bufferedPosition,
controlBarHeight = controlBarHeight,
durationMillis = duration,
speed = userSpeed,
actions = actions,
Expand Down Expand Up @@ -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) {
Expand Down
79 changes: 56 additions & 23 deletions shared/src/commonMain/kotlin/dev/bilby/ui/video/BilbyPlayer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -401,13 +408,14 @@ fun BilbyPlayer(
*
* 两种排法,按画面大小分:
*
* - **窄**(单栏内嵌,以及竖屏视频的全屏):一行 ——「时间 进度条 时长 [弹幕] ⚙ ⛶」。倍速、
* 清晰度、字幕收进 ⚙ 打开的整块设置面板。内嵌画面只有两百来 dp 高,两行控制条加上顶上的
* 返回键,中间就放不下播放键了。
* - **宽**(全屏,或双栏里的内嵌画面,且宽度够):进度条独占一行,下面一行是读数和几枚写着
* 当前档位的 chip(「1.5x」「1080P」),倍速打开贴边选择器,其余各开面板中对应的一段;
* 弹幕开关旁边的 ⚙ 只开弹幕那一段。
* 不只看宽度:单栏横屏的内嵌画面也有四五百 dp 宽,高度却放不下两行。
* - **窄**(单栏内嵌):一行 ——「时间 进度条 时长 [弹幕] ⚙ ⛶」。倍速、清晰度、字幕收进 ⚙
* 打开的整块设置面板。内嵌画面只有两百来 dp 高,两行控制条加上顶上的返回键,中间就放不下
* 播放键了。
* - **堆叠**(全屏 —— 横竖屏皆是,以及双栏里的内嵌画面):进度条独占一行,下面一行是读数
* 与控件。全屏后进度条是这一屏最主要的操作,单行排法里它被挤在时间和三枚按钮中间只剩
* 两百来 dp,拖着费劲;独占一行后整屏宽都是它的。控件按宽度分两档:够宽(≥480)时换成
* 写着当前档位的 chip(「1.5x」「1080P」)加弹幕设置键;竖屏手机的宽度放不下那些,保持
* 紧凑的 [弹幕] ⚙ ⛶ 三键,档位照常在面板里调。
*
* 原先是一行按钮加一套"量一遍装不装得下、装不下就给不给档名、再装不下就分两行"的估算,
* 窄屏上是六七个一样大的白色线框挤在右下角。现在窄的时候档位全在面板里,估算就不需要了。
Expand Down Expand Up @@ -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) {
Expand All @@ -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(
Expand Down Expand Up @@ -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,
Expand All @@ -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 不画(面板里那一段同样不画)。
Expand All @@ -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)
}
}
Expand Down
Loading