Skip to content

Commit 70d39c8

Browse files
committed
OPT: 优化解析卡片排版密度与内联标记样式
排版紧凑化: - 段间距 18→12,header→正文 10→6,bullet 之间 10→5 - IconBox 24→20,icon 14→12,与 label 间距 8→6 - bullet ▸ padding top 2→1 / right 8→6,行高 1.55→1.4 - 整片信息密度提升约 30%,告别"稀稀拉拉"观感 内联标记重做: - 反引号引用改为 TextStyle.background 高亮(primaryContainer + onPrimaryContainer), 跟随文本流自然换行,解决长短语在 chip 盒子里强制断行的问题 - IPA 音标保留 WidgetSpan chip 但背景换成中性灰 surfaceContainerHighest, monospace、正常字重,与反引号主色高亮形成色相 + 形态双层级 - 两种标记互不喧宾夺主,色相分明
1 parent 1ffd851 commit 70d39c8

2 files changed

Lines changed: 64 additions & 54 deletions

File tree

lib/widgets/practice/sentence_annotation_card.dart

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -855,9 +855,9 @@ class _AnalysisContent extends StatelessWidget {
855855
crossAxisAlignment: CrossAxisAlignment.start,
856856
children: [
857857
for (var idx = 0; idx < visible.length; idx++) ...[
858-
if (idx > 0) const SizedBox(height: 18),
858+
if (idx > 0) const SizedBox(height: 12),
859859
_buildSectionHeader(theme, visible[idx]),
860-
const SizedBox(height: 10),
860+
const SizedBox(height: 6),
861861
_buildSectionBody(theme, fields[visible[idx].fieldIndex]),
862862
],
863863
],
@@ -873,15 +873,15 @@ class _AnalysisContent extends StatelessWidget {
873873
child: Row(
874874
children: [
875875
Container(
876-
width: 24,
877-
height: 24,
876+
width: 20,
877+
height: 20,
878878
decoration: BoxDecoration(
879879
color: cs.primary.withValues(alpha: 0.10),
880-
borderRadius: BorderRadius.circular(6),
880+
borderRadius: BorderRadius.circular(5),
881881
),
882-
child: Icon(s.icon, size: 14, color: cs.primary),
882+
child: Icon(s.icon, size: 12, color: cs.primary),
883883
),
884-
const SizedBox(width: 8),
884+
const SizedBox(width: 6),
885885
Text(
886886
s.label,
887887
style: theme.textTheme.labelLarge?.copyWith(
@@ -899,7 +899,7 @@ class _AnalysisContent extends StatelessWidget {
899899
final cs = theme.colorScheme;
900900
final body = theme.textTheme.bodySmall?.copyWith(
901901
color: cs.onSurfaceVariant,
902-
height: 1.55,
902+
height: 1.4,
903903
);
904904

905905
final items = field.split('\n').where((s) => s.trim().isNotEmpty).toList();
@@ -911,7 +911,7 @@ class _AnalysisContent extends StatelessWidget {
911911
crossAxisAlignment: CrossAxisAlignment.start,
912912
children: [
913913
for (var i = 0; i < items.length; i++) ...[
914-
if (i > 0) const SizedBox(height: 10),
914+
if (i > 0) const SizedBox(height: 5),
915915
_buildBulletItem(theme, items[i], body),
916916
],
917917
],
@@ -938,7 +938,7 @@ class _AnalysisContent extends StatelessWidget {
938938
final value = m?.group(2)?.trim();
939939

940940
final bullet = Padding(
941-
padding: const EdgeInsets.only(top: 2, right: 8),
941+
padding: const EdgeInsets.only(top: 1, right: 6),
942942
child: Text(
943943
'▸',
944944
style: TextStyle(
@@ -988,42 +988,32 @@ class _AnalysisContent extends StatelessWidget {
988988
}
989989
final codeContent = m.group(1);
990990
if (codeContent != null) {
991-
spans.add(WidgetSpan(
992-
alignment: PlaceholderAlignment.middle,
993-
child: Container(
994-
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
995-
decoration: BoxDecoration(
996-
color: cs.surfaceContainerHighest.withValues(alpha: 0.6),
997-
borderRadius: BorderRadius.circular(4),
998-
),
999-
child: Text(
1000-
codeContent,
1001-
style: TextStyle(
1002-
fontFamily: 'monospace',
1003-
fontFamilyFallback: const ['Menlo', 'Courier'],
1004-
fontSize: (body?.fontSize ?? 13) - 0.5,
1005-
color: cs.onSurface,
1006-
height: 1.2,
1007-
),
1008-
),
991+
// 反引号引用:用 primaryContainer 作为字形背后的扁平高亮色,沿文本流
992+
// 自然换行;不使用 WidgetSpan 盒子,避免长短语撑出强制断行。
993+
spans.add(TextSpan(
994+
text: codeContent,
995+
style: TextStyle(
996+
background: Paint()..color = cs.primaryContainer,
997+
color: cs.onPrimaryContainer,
1009998
),
1010999
));
10111000
} else {
1001+
// IPA 音标:保留 chip 盒子(monospace),但用中性灰色背景,不喧宾夺主
10121002
spans.add(WidgetSpan(
10131003
alignment: PlaceholderAlignment.middle,
10141004
child: Container(
1015-
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
1005+
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
10161006
decoration: BoxDecoration(
1017-
color: cs.primaryContainer.withValues(alpha: 0.35),
1007+
color: cs.surfaceContainerHighest,
10181008
borderRadius: BorderRadius.circular(4),
10191009
),
10201010
child: Text(
10211011
m.group(2)!,
10221012
style: TextStyle(
10231013
fontFamily: 'monospace',
10241014
fontFamilyFallback: const ['Menlo', 'Courier'],
1025-
fontSize: 11,
1026-
color: cs.onPrimaryContainer,
1015+
fontSize: (body?.fontSize ?? 13) - 1,
1016+
color: cs.onSurface,
10271017
height: 1.2,
10281018
),
10291019
),
@@ -1038,7 +1028,7 @@ class _AnalysisContent extends StatelessWidget {
10381028
return spans;
10391029
}
10401030

1041-
/// 整段文本(含反引号 chip 和 IPA chip)渲染为 Text.rich
1031+
/// 整段文本(含反引号高亮 badge 和 IPA 斜体)渲染为 Text.rich
10421032
Widget _richWithIpa(ThemeData theme, String text, TextStyle? body) {
10431033
return Text.rich(
10441034
TextSpan(style: body, children: _inlineSpans(theme, text, body)),

test/widgets/sentence_annotation_card_test.dart

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -410,16 +410,37 @@ void main() {
410410
});
411411

412412
group('SentenceAnnotationCard — 内联标记渲染', () {
413-
/// 找到 fontFamily 为 monospace 的 Text widget(即反引号或 IPA chip 内的 Text)
414-
Finder findChipText(String content) => find.byWidgetPredicate(
413+
/// 查找符合反引号样式的 TextSpan:文本匹配 + 设置了 background Paint
414+
bool hasBadgeSpan(String content) {
415+
bool found = false;
416+
for (final el in find.byType(Text).evaluate()) {
417+
final w = el.widget as Text;
418+
final root = w.textSpan;
419+
if (root == null) continue;
420+
root.visitChildren((span) {
421+
if (span is TextSpan &&
422+
span.text == content &&
423+
span.style?.background != null) {
424+
found = true;
425+
return false;
426+
}
427+
return true;
428+
});
429+
if (found) break;
430+
}
431+
return found;
432+
}
433+
434+
/// 找到 IPA chip 内的 monospace Text
435+
Finder findIpaChip(String content) => find.byWidgetPredicate(
415436
(w) =>
416437
w is Text &&
417438
w.style?.fontFamily == 'monospace' &&
418439
w.data == content,
419440
);
420441

421-
/// 找到任意 monospace Text,用于断言"没有任何 chip"
422-
final anyChipFinder = find.byWidgetPredicate(
442+
/// 找到任意 monospace Text,用于断言"没有任何 IPA chip"
443+
final anyIpaChipFinder = find.byWidgetPredicate(
423444
(w) => w is Text && w.style?.fontFamily == 'monospace',
424445
);
425446

@@ -442,48 +463,48 @@ void main() {
442463

443464
testWidgets('IPA 识别 — 含音节分界点', (tester) async {
444465
await pumpAnalysisCard(tester, '音标:/ˈɪŋ.ɡlɪʃ/ 是英语的发音');
445-
expect(findChipText('/ˈɪŋ.ɡlɪʃ/'), findsOneWidget);
466+
expect(findIpaChip('/ˈɪŋ.ɡlɪʃ/'), findsOneWidget);
446467
});
447468

448469
testWidgets('IPA 识别 — 含连字符', (tester) async {
449470
await pumpAnalysisCard(tester, '音标:/pre-ˈfɪks/ 是前缀');
450-
expect(findChipText('/pre-ˈfɪks/'), findsOneWidget);
471+
expect(findIpaChip('/pre-ˈfɪks/'), findsOneWidget);
451472
});
452473

453474
testWidgets('IPA 识别 — 单音节弱读', (tester) async {
454475
await pumpAnalysisCard(tester, '音标:/tə/ 是弱读形式');
455-
expect(findChipText('/tə/'), findsOneWidget);
476+
expect(findIpaChip('/tə/'), findsOneWidget);
456477
});
457478

458479
testWidgets('IPA 否决 — 表示或者的斜杠两侧带空格', (tester) async {
459480
await pumpAnalysisCard(tester, '或者:and / or 表示选择');
460-
expect(anyChipFinder, findsNothing);
481+
expect(anyIpaChipFinder, findsNothing);
461482
});
462483

463484
testWidgets('IPA 否决 — 含中文与斜杠', (tester) async {
464485
await pumpAnalysisCard(tester, '搭配:English / 英语 互译');
465-
expect(anyChipFinder, findsNothing);
486+
expect(anyIpaChipFinder, findsNothing);
466487
});
467488

468489
testWidgets('IPA 否决 — 路径不被误判', (tester) async {
469490
await pumpAnalysisCard(tester, '路径:/path/to/file 是文件路径');
470-
expect(anyChipFinder, findsNothing);
491+
expect(anyIpaChipFinder, findsNothing);
471492
});
472493

473494
testWidgets('IPA 否决 — 冠词 a/an', (tester) async {
474495
await pumpAnalysisCard(tester, '冠词:a/an 视下一词首音决定');
475-
expect(anyChipFinder, findsNothing);
496+
expect(anyIpaChipFinder, findsNothing);
476497
});
477498

478-
testWidgets('反引号 chip 渲染单个词', (tester) async {
499+
testWidgets('反引号渲染为内联 badge(背景色 + 自然换行)', (tester) async {
479500
await pumpAnalysisCard(tester, '词义:`run` 表示经营');
480-
expect(findChipText('run'), findsOneWidget);
501+
expect(hasBadgeSpan('run'), isTrue);
481502
});
482503

483-
testWidgets('反引号与 IPA 同一行混排,各自正确分段', (tester) async {
504+
testWidgets('反引号与 IPA 同一行混排:前者 badge,后者灰色 chip', (tester) async {
484505
await pumpAnalysisCard(tester, '弱读:`have` 常听起来像 /əv/ 这样');
485-
expect(findChipText('have'), findsOneWidget);
486-
expect(findChipText('/əv/'), findsOneWidget);
506+
expect(hasBadgeSpan('have'), isTrue);
507+
expect(findIpaChip('/əv/'), findsOneWidget);
487508
});
488509

489510
/// 把所有渲染的 RichText 的可见文本拼起来,用于检验"反引号是否还在屏上"
@@ -509,14 +530,13 @@ void main() {
509530
// 清洗后的 key 文本应当出现在渲染结果中
510531
expect(rendered.contains('helped to 的弱读'), isTrue);
511532
// value 中的 IPA chip 不受影响
512-
expect(findChipText('/tə/'), findsOneWidget);
533+
expect(findIpaChip('/tə/'), findsOneWidget);
513534
});
514535

515-
testWidgets('客户端清洗 — value 中的反引号保留(仍渲染为 chip)',
516-
(tester) async {
536+
testWidgets('客户端清洗 — value 中的反引号保留(渲染为 badge)', (tester) async {
517537
await pumpAnalysisCard(tester, '词义:`run` 表示经营');
518-
// value 中的 `run` 应渲染为 chip
519-
expect(findChipText('run'), findsOneWidget);
538+
// value 中的 `run` 应渲染为带背景色的内联 badge
539+
expect(hasBadgeSpan('run'), isTrue);
520540
});
521541
});
522542
}

0 commit comments

Comments
 (0)