merge: W54D —— 分享确认面板正文预览改成三行窗口 + 内部滚动(发送前能读到整段正文)

This commit is contained in:
AskaEth committed 2026-10-06 21:26:33 +08:00
commit f3c1121887
2 files changed
+102 -16

No files matched your search

@@ -15,11 +15,15 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import top.yeij.cyrene.domain.chat.DocumentChips
@@ -247,15 +251,15 @@ const val SHARE_CONFIRM_CAPTION_PLACEHOLDER = "加一句描述…"
* 所以预览区自己带一个高度上限 + 内部滚动:内容少时它按内容高(上限不生效),内容多时它滚动、
* 输入框与按钮原地不动。
*
* ## 正文为什么只给三行
* ## 正文给三行,但三行之外是「滚」不是「省略号」
* 预览只是「分享进来的是什么」的一眼,而用户要的是按一下发送:把一段几万字的正文整个铺开,这个面板
* 就成了一个阅读器。三行之外用省略号收口,不假装那是全文。
* 就成了一个阅读器。所以正文那一窗**恒为三行高**([SHARE_PREVIEW_TEXT_LINES]),面板不随正文长短
* 变化。
*
* ⚠️ W53X3 之后**面板里再没有第二个地方放着这段正文**:输入框已经改成「用户附加的描述」(初值恒为
* 空),不再是正文的可编辑副本。也就是说超过三行的分享正文在面板上只看得到开头——**这不影响发出去的
* 内容**([shareConfirmBody] 用的是完整的那一段,一字不丢),但它确实少了一处「发送前读全文」的地方。
* 这是 W53X 那一版的老行为,本批只把它如实写下来、**没有顺手改**:正文块要不要能展开属于观感取舍,
* 得由主人拍板。
* 但三行之外**不再收成省略号**(W54D 主人拍板改掉了 W53X 的老行为):W53X3 之后**面板里再没有第二
* 个地方放着这段正文**——输入框是「用户附加的描述」(初值恒为空),不再是正文的可编辑副本。于是截断
* 成省略号就等于「按发送之前读不到要发的那段正文」,而用户点这颗「发送」要确认的恰恰是它。所以这一窗
* 内部可滚([ShareConfirmBody]):**面板的高度一分不涨,正文滚着读完**。
*/
@Composable
private fun ShareConfirmPreviews(preview: ShareConfirmPreview) {
@@ -303,22 +307,70 @@ private fun ShareConfirmPreviews(preview: ShareConfirmPreview) {
style = MiuixTheme.textStyles.footnote2,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
)
Text(
text = preview.text,
style = MiuixTheme.textStyles.footnote1,
maxLines = SHARE_PREVIEW_TEXT_LINES,
overflow = TextOverflow.Ellipsis,
)
ShareConfirmBody(preview.text)
}
}
}
}
/**
* 正文那一窗:**高度恒为三行,再长就在这一窗里滚**(W54D 主人拍板,见 [ShareConfirmPreviews])。
*
* ## 为什么量行高,而不是写一个 dp 常量
* 这个上限必须**正好三行**:矮了会在字身上切一刀(滚动停在一个被切开的字形上),高了就等于把「三行」
* 这个上限悄悄放宽。而三行有多高没法提前写出来——Miuix 的 `footnote1` 只给了 13sp 的字号,
* `lineHeight` 是 `Unspecified`,行高由系统字体的 ascent/descent 决定、随「大字体」设置一起缩放。
* 一个 dp 常量在默认字号档上勉强对得上,到了大字号档就会矮一大截(行数不足三行、末行被切)。
* 所以这里改为**量**:先按没有上限排一次,从 `TextLayoutResult` 上取第三行的底边当上限。
*
* 上限只写一次(`cap` 还是 [Dp.Infinity] 时才改),所以不会来回跳:拿到上限后重新排版,正文的宽度
* 与样式都没变,行位置与首帧一致,量出来的值也一样。
*
* ⚠️ 代价是**首帧先按完整正文排一次**、下一帧才收到三行:正文很长时,这一窗理论上会有一帧高出一截、
* 然后回落。另一条路——写死一个 dp 上限——没有这一帧,但在非默认字号档上必然切字,两害相权取了
* 「量」。这一帧在实际渲染里看不看得见(弹层这时正在进场,可能被动画盖住)**没有在真机上验过**,
* 留给主人真机验收。
*
* ## 两层滚动怎么分工
* 这一窗自己滚(正文读完),外层 [ShareConfirmPreviews] 也滚(图片 + 文件芯片 + 这一块合起来的高度
* 上限)。手指停在正文上时先滚正文、滚到底再把余量交给外层——`verticalScroll` 本来就走嵌套滚动,
* 不用额外接线。
*/
@Composable
private fun ShareConfirmBody(text: String) {
val density = LocalDensity.current
var cap by remember(text) { mutableStateOf(Dp.Infinity) }
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = cap)
.verticalScroll(rememberScrollState()),
) {
Text(
text = text,
style = MiuixTheme.textStyles.footnote1,
onTextLayout = { layout ->
if (cap == Dp.Infinity && layout.lineCount > 0) {
// 正文不到三行时上限就取全文的高度(`heightIn` 是个上限,短了本来也不生效)。
val lastVisible = layout.lineCount.coerceAtMost(SHARE_PREVIEW_TEXT_LINES) - 1
cap = with(density) { layout.getLineBottom(lastVisible).toDp() }
}
},
)
}
}
/** 预览区的高度上限(见 [ShareConfirmPreviews]:超出的部分靠内部滚动看)。 */
private val SHARE_PREVIEW_MAX_HEIGHT = 150.dp
/** 预览里正文给几行(超出的部分收成省略号,见 [ShareConfirmPreviews] 的 KDoc)。 */
private const val SHARE_PREVIEW_TEXT_LINES = 3
/**
* 预览里正文那一窗给几行(W54D 起**不再截断**,超出的部分在窗内滚动,见 [ShareConfirmBody])。
*
* `internal` 是为了让 `ShareConfirmTest` 能把这个数钉成字面量:面板的滚动行为单测够不着(本模块没有
* Compose UI 测试的依赖),能钉住的只有「这一窗是三行」这条判据本身。
*/
internal const val SHARE_PREVIEW_TEXT_LINES = 3
/**
* 分享进来的一份文件 ⇒ 待发附件。
@@ -32,6 +32,13 @@ import top.yeij.cyrene.domain.share.SharedDocument
* ⚠️ 「输入框初值为空」本身是 Compose 状态(`ChatScreen` 里那格 `remember`),单测碰不到——它由
* 主人真机验收(输入框空着、预览里有正文)。这份测试能钉住的是它的**后果**:描述为空时,拼出来的
* 正文就是分享正文本身、而且只出现**一次**(见 `the caption is not a copy of the shared text`)。
*
* ## W54D 又改掉了什么
* 正文那一窗从「三行 + 省略号」改成「三行窗口 + 内部滚动」(主人拍板):面板里再没有第二处放着这段
* 正文,收成省略号就等于「按发送之前读不到要发的那段正文」。滚动是渲染层的事、单测够不着(本模块
* 没有 Compose UI 测试的依赖),所以这里钉的是它的两个前提——**正文完整地进到预览**(一个字都不少)
* 与**这一窗仍然是三行**(面板不随正文变高),见 `the whole body reaches the preview …` 与
* `the preview window stays three lines tall`。
*/
class ShareConfirmTest {
@@ -54,6 +61,33 @@ class ShareConfirmTest {
assertFalse(preview.isEmpty)
}
@Test
fun `the whole body reaches the preview even when it is longer than the window`() {
val long = (1..40).joinToString("\n") { "第 $it 行" }
// W54D 主人拍板:预览的正文那一窗恒为三行高,但**里面能滚**——三行之外不再收成省略号。这条钉
// 的是那一窗的输入:纯函数层把**完整**的正文交给渲染层(这一段一个字都不许在这里少),少了就
// 一定读不到——面板里再没有第二处放着这段正文(输入框只剩「用户附加的描述」)。
val preview = shareConfirmPreview(SharedContent(text = long))
assertEquals(long, preview.text)
assertEquals(40, preview.text.lines().size)
assertTrue(
"这一档必须真的超过三行,否则上面那条断言什么也证明不了",
preview.text.lines().size > SHARE_PREVIEW_TEXT_LINES,
)
// 发出去的那一段同样一个字不少(两边同源,见 [shareConfirmBody])。
assertEquals(long, shareConfirmBody(SharedContent(text = long), ""))
}
@Test
fun `the preview window stays three lines tall`() {
// 3 这个字面量就是这条测试的全部内容:滚动是渲染层的事、单测够不着(本模块没有 Compose UI
// 测试的依赖),能钉住的只有「这一窗是三行」这条判据本身。改大 ⇒ 面板会随长正文变高(主人
// 拍板的高度上限没了);改小 ⇒ 正文的可见窗口白缩一行。
assertEquals(3, SHARE_PREVIEW_TEXT_LINES)
}
@Test
fun `blank pieces count as nothing to show`() {
val preview = shareConfirmPreview(