optimize orientation container position
This commit is contained in:
+8
-3
@@ -68,7 +68,7 @@ private object DrawerLayout {
|
||||
val headerToGroupsGap = 10.dp
|
||||
val groupRowGap = 12.dp
|
||||
val groupsToMenuGap = 10.dp
|
||||
val groupButtonWidth = 120.dp
|
||||
// 宽度不固定:按容器宽度两等分,才能和功能菜单的左右边距对齐。设计稿为 120dp。
|
||||
val groupButtonHeight = 50.dp
|
||||
val groupButtonGap = 10.dp
|
||||
val menuOuterPaddingH = 8.dp
|
||||
@@ -221,7 +221,10 @@ fun AppDrawer(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
groupRows.forEachIndexed { index, row ->
|
||||
// 必须撑满并等分:固定宽度会被外层 Column 的 CenterHorizontally 居中,
|
||||
// 平板上抽屉变宽后就和下面左对齐的功能菜单差出一截边距。
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(DrawerLayout.groupButtonGap),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
@@ -232,7 +235,7 @@ fun AppDrawer(
|
||||
selected = currentGroup == group.key,
|
||||
onClick = { onGroupSelect(group.key) },
|
||||
modifier = Modifier
|
||||
.width(DrawerLayout.groupButtonWidth)
|
||||
.weight(1f)
|
||||
.height(DrawerLayout.groupButtonHeight),
|
||||
)
|
||||
}
|
||||
@@ -313,7 +316,9 @@ private fun GroupButton(
|
||||
AsyncImage(
|
||||
model = Res.getUri(group.asset.path),
|
||||
contentDescription = group.key.displayName,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
// Fit 而非 FillWidth:按钮改为等分后在平板上更宽,
|
||||
// 撑满宽度会把 logo 顶出 50dp 的高度被裁掉。
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
+63
-27
@@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
@@ -467,6 +468,23 @@ fun FullPlayerScreen(
|
||||
}
|
||||
}
|
||||
|
||||
/** 横屏左栏整体右移一些,不要贴着屏幕边。 */
|
||||
private val LandscapeBodyStartPadding = 48.dp
|
||||
|
||||
/** 横屏封面上限。平板上可用高度能到 280dp 以上,卡住它免得封面占满整栏。 */
|
||||
private val LandscapeCoverMaxSize = 250.dp
|
||||
|
||||
/** 封面区再内缩一点,连带下方同宽的内容一起偏右。 */
|
||||
private val LandscapeCoverInsetStart = 24.dp
|
||||
|
||||
/**
|
||||
* 封面边长未知时先撑满;已知后与封面同宽,左右边缘才对得上。
|
||||
* 偏移量要和封面那边保持一致,否则两者又错开。
|
||||
*/
|
||||
private fun Modifier.coverAlignedWidth(side: Dp?): Modifier =
|
||||
offset(x = LandscapeCoverInsetStart)
|
||||
.then(if (side != null) width(side) else fillMaxWidth())
|
||||
|
||||
@Composable
|
||||
private fun LandscapePlayerBody(
|
||||
current: Music?,
|
||||
@@ -499,12 +517,15 @@ private fun LandscapePlayerBody(
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding()
|
||||
.padding(
|
||||
start = safeStart + 28.dp,
|
||||
start = safeStart + LandscapeBodyStartPadding,
|
||||
end = safeEnd + 28.dp,
|
||||
top = 12.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
) {
|
||||
// 封面是受高度约束的正方形,通常比左栏窄;下方内容跟随它的实际边长,
|
||||
// 否则 fillMaxWidth 会让两者左右边缘差出一截。
|
||||
var coverSide by remember { mutableStateOf<Dp?>(null) }
|
||||
Column(
|
||||
Modifier
|
||||
.weight(0.42f)
|
||||
@@ -513,6 +534,7 @@ private fun LandscapePlayerBody(
|
||||
.padding(end = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
// 收起按钮保持贴左栏边缘,不跟封面对齐。
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(40.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -529,19 +551,22 @@ private fun LandscapePlayerBody(
|
||||
contentDescription = tr("collapse"),
|
||||
)
|
||||
}
|
||||
Box(
|
||||
// 直接用 BoxWithConstraints 承担居中:套在 Box 里再 fillMaxSize 的话,
|
||||
// 外层的 contentAlignment 不起作用,封面会左上角对齐。
|
||||
BoxWithConstraints(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.offset(x = LandscapeCoverInsetStart),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||
val side = maxWidth
|
||||
.coerceAtMost(maxHeight)
|
||||
.coerceAtMost(300.dp)
|
||||
.coerceAtLeast(1.dp)
|
||||
PlayerCoverArt(music = current, size = side)
|
||||
}
|
||||
val side = maxWidth
|
||||
.coerceAtMost(maxHeight)
|
||||
.coerceAtMost(LandscapeCoverMaxSize)
|
||||
.coerceAtLeast(1.dp)
|
||||
// 下方内容宽度不影响它们的高度,所以这次写入会立刻收敛。
|
||||
if (coverSide != side) coverSide = side
|
||||
PlayerCoverArt(music = current, size = side)
|
||||
}
|
||||
Text(
|
||||
text = current?.musicName ?: tr("not_playing"),
|
||||
@@ -553,9 +578,7 @@ private fun LandscapePlayerBody(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
modifier = Modifier.coverAlignedWidth(coverSide),
|
||||
)
|
||||
val artist = current?.artist.orEmpty()
|
||||
if (artist.isNotBlank()) {
|
||||
@@ -569,8 +592,8 @@ private fun LandscapePlayerBody(
|
||||
fontSize = 12.sp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 2.dp),
|
||||
.coverAlignedWidth(coverSide)
|
||||
.padding(vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
PlayerFuncButtons(
|
||||
@@ -582,6 +605,8 @@ private fun LandscapePlayerBody(
|
||||
onToggleLove = onToggleLove,
|
||||
onAddSong = onAddSong,
|
||||
onToggleTranslate = onToggleTranslate,
|
||||
modifier = Modifier.coverAlignedWidth(coverSide),
|
||||
horizontalPadding = 0.dp,
|
||||
)
|
||||
PlayerSeekBar(
|
||||
thumbColor = thumbColor,
|
||||
@@ -589,12 +614,14 @@ private fun LandscapePlayerBody(
|
||||
inactiveTrack = inactiveTrack,
|
||||
timeColor = timeColor,
|
||||
onSeek = { AppContainer.player.seekTo(it) },
|
||||
modifier = Modifier.coverAlignedWidth(coverSide),
|
||||
trackInset = 0.dp,
|
||||
labelInset = 0.dp,
|
||||
)
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
.coverAlignedWidth(coverSide)
|
||||
.height(64.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
@@ -1174,13 +1201,15 @@ private fun PlayerFuncButtons(
|
||||
onAddSong: () -> Unit,
|
||||
onToggleTranslate: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
/** 横屏左栏要和控制键左右对齐,传 0。 */
|
||||
horizontalPadding: Dp = 16.dp,
|
||||
) {
|
||||
val btnBg = Color.Transparent
|
||||
val btnIcon = iconTint
|
||||
Row(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = horizontalPadding),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
@@ -1287,6 +1316,11 @@ private fun PlayerSeekBar(
|
||||
inactiveTrack: Color,
|
||||
timeColor: Color,
|
||||
onSeek: (Long) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
/** 轨道两端留白。横屏左栏要和控制键对齐,传 0。 */
|
||||
trackInset: Dp = 24.dp,
|
||||
/** 时间文字的左右内边距。 */
|
||||
labelInset: Dp = 16.dp,
|
||||
) {
|
||||
val positionMs = rememberPlayerField { it.positionMs }
|
||||
val durationMs = rememberPlayerField { it.durationMs }
|
||||
@@ -1296,14 +1330,18 @@ private fun PlayerSeekBar(
|
||||
var dragValue by remember { mutableFloatStateOf(0f) }
|
||||
val display = if (dragging) dragValue else positionMs.toFloat().coerceIn(0f, max)
|
||||
val fraction = (display / max).coerceIn(0f, 1f)
|
||||
val thumbRadius = 10.dp
|
||||
// 滑块中心至少离边一个半径,否则贴边时会被画到容器外;
|
||||
// 轨道本身可以贴边,两者的留白因此要分开算。
|
||||
val thumbInset = maxOf(trackInset, thumbRadius)
|
||||
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Column(modifier.fillMaxWidth()) {
|
||||
BoxWithConstraints(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.pointerInput(max) {
|
||||
val inset = 24.dp.toPx()
|
||||
.pointerInput(max, thumbInset) {
|
||||
val inset = thumbInset.toPx()
|
||||
fun xToValue(x: Float): Float {
|
||||
val usable = (size.width - inset * 2).coerceAtLeast(1f)
|
||||
return ((x - inset) / usable).coerceIn(0f, 1f) * max
|
||||
@@ -1321,11 +1359,9 @@ private fun PlayerSeekBar(
|
||||
}
|
||||
},
|
||||
) {
|
||||
val thumbRadius = 10.dp
|
||||
val overlayPad = 24.dp
|
||||
val trackHeight = 4.dp
|
||||
Canvas(Modifier.fillMaxSize()) {
|
||||
val inset = overlayPad.toPx()
|
||||
val inset = trackInset.toPx()
|
||||
val y = size.height / 2f
|
||||
val trackW = (size.width - inset * 2f).coerceAtLeast(0f)
|
||||
val top = y - trackHeight.toPx() / 2f
|
||||
@@ -1348,7 +1384,7 @@ private fun PlayerSeekBar(
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.offset(x = overlayPad - thumbRadius + (maxWidth - overlayPad * 2) * fraction)
|
||||
.offset(x = thumbInset - thumbRadius + (maxWidth - thumbInset * 2) * fraction)
|
||||
.size(thumbRadius * 2)
|
||||
.shadow(1.dp, CircleShape)
|
||||
.background(thumbColor, CircleShape),
|
||||
@@ -1357,7 +1393,7 @@ private fun PlayerSeekBar(
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.padding(horizontal = labelInset),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(formatMs(display.roundToLong()), color = timeColor, fontSize = 12.sp)
|
||||
|
||||
+7
-4
@@ -66,12 +66,15 @@ fun ProvideDesignDensity(content: @Composable () -> Unit) {
|
||||
Box(Modifier.fillMaxSize())
|
||||
return@CompositionLocalProvider
|
||||
}
|
||||
val heightDp = heightPx / base.density
|
||||
val widthPx = containerPx.width
|
||||
val isLandscape = widthPx > 0 && widthPx > heightPx
|
||||
// 按高度比例缩放;不低于设计尺寸;平板上限 1.35。忽略系统字体缩放(对齐 Flutter textScaler = 1.0)。
|
||||
// 横屏高度变矮时下限仍是 1,不会按变矮的高度放大或缩小。
|
||||
val scale = (heightDp / Const.UI_HEIGHT).coerceIn(1f, 1.35f)
|
||||
// 按长边比例缩放;不低于设计尺寸;平板上限 1.35。忽略系统字体缩放(对齐 Flutter textScaler = 1.0)。
|
||||
//
|
||||
// 取长边而不是当前窗口高度,是为了让横竖屏共用同一个 dp 坐标系。
|
||||
// 按窗口高度算的话,横屏高度变成短边,scale 会从上限 1.35 掉回 1,
|
||||
// 同一个 dp 在横屏下物理尺寸要小四分之一。
|
||||
val longestDp = maxOf(widthPx, heightPx) / base.density
|
||||
val scale = (longestDp / Const.UI_HEIGHT).coerceIn(1f, 1.35f)
|
||||
val scaled = remember(base.density, scale) {
|
||||
Density(
|
||||
density = base.density * scale,
|
||||
|
||||
Reference in New Issue
Block a user