diff --git a/CHANGELOG.md b/CHANGELOG.md
index 854159ab9..72737e27e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ All notable changes to this project will be documented in this file.
- rendering of panoramas with inconsistent metadata
- failing scan of items copied to SD card on older devices
- unreplaceable covers set before v1.7.1
+- inconsistent background height for multi-script subtitles
## [v1.7.1] - 2022-10-09
diff --git a/lib/widgets/common/basic/text_background_painter.dart b/lib/widgets/common/basic/text_background_painter.dart
new file mode 100644
index 000000000..e738614fc
--- /dev/null
+++ b/lib/widgets/common/basic/text_background_painter.dart
@@ -0,0 +1,76 @@
+import 'dart:ui' as ui;
+
+import 'package:collection/collection.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+
+// as of Flutter v3.3.7, text style background does not have consistent height
+// when rendering multi-script text, so we paint the background behind via a stack instead
+class TextBackgroundPainter extends StatelessWidget {
+ final List spans;
+ final TextStyle style;
+ final TextAlign textAlign;
+ final Widget child;
+
+ const TextBackgroundPainter({
+ super.key,
+ required this.spans,
+ required this.style,
+ required this.textAlign,
+ required this.child,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final backgroundColor = style.backgroundColor;
+ if (backgroundColor == null || backgroundColor.alpha == 0) {
+ return child;
+ }
+
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ final paragraph = RenderParagraph(
+ TextSpan(
+ children: spans,
+ style: style,
+ ),
+ textAlign: textAlign,
+ textDirection: Directionality.of(context),
+ textScaleFactor: MediaQuery.textScaleFactorOf(context),
+ )..layout(constraints, parentUsesSize: true);
+
+ final textLength = spans.map((v) => v.text?.length ?? 0).sum;
+ final allBoxes = paragraph.getBoxesForSelection(
+ TextSelection(baseOffset: 0, extentOffset: textLength),
+ boxHeightStyle: ui.BoxHeightStyle.max,
+ );
+
+ // merge boxes to avoid artifacts at box edges, from anti-aliasing and rounding hacks
+ final lineRects = groupBy(allBoxes, (v) => v.top).entries.map((kv) {
+ final top = kv.key;
+ final lineBoxes = kv.value;
+ return Rect.fromLTRB(
+ lineBoxes.map((v) => v.left).min,
+ top,
+ lineBoxes.map((v) => v.right).max,
+ lineBoxes.first.bottom,
+ );
+ });
+
+ return Stack(
+ children: [
+ ...lineRects.map((rect) {
+ return Positioned.fromRect(
+ rect: rect,
+ child: ColoredBox(
+ color: backgroundColor,
+ ),
+ );
+ }),
+ child,
+ ],
+ );
+ },
+ );
+ }
+}
diff --git a/lib/widgets/settings/video/subtitle_sample.dart b/lib/widgets/settings/video/subtitle_sample.dart
index 9b6d2d888..16399c7b5 100644
--- a/lib/widgets/settings/video/subtitle_sample.dart
+++ b/lib/widgets/settings/video/subtitle_sample.dart
@@ -1,6 +1,7 @@
import 'package:aves/model/settings/settings.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/basic/outlined_text.dart';
+import 'package:aves/widgets/common/basic/text_background_painter.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/borders.dart';
import 'package:aves/widgets/viewer/visual/subtitle/subtitle.dart';
@@ -12,8 +13,13 @@ class SubtitleSample extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final textSpans = [
+ TextSpan(text: context.l10n.settingsSubtitleThemeSample),
+ ];
+
return Consumer(
builder: (context, settings, child) {
+ final textAlign = settings.subtitleTextAlignment;
final outlineColor = Colors.black.withOpacity(settings.subtitleTextColor.opacity);
final shadows = [
Shadow(
@@ -34,7 +40,7 @@ class SubtitleSample extends StatelessWidget {
),
height: 128,
child: AnimatedAlign(
- alignment: _getAlignment(settings.subtitleTextAlignment),
+ alignment: _getAlignment(textAlign),
curve: Curves.easeInOutCubic,
duration: const Duration(milliseconds: 400),
child: Padding(
@@ -42,20 +48,24 @@ class SubtitleSample extends StatelessWidget {
child: AnimatedDefaultTextStyle(
style: TextStyle(
color: settings.subtitleTextColor,
- backgroundColor: settings.subtitleBackgroundColor,
fontSize: settings.subtitleFontSize,
shadows: settings.subtitleShowOutline ? shadows : null,
),
- textAlign: settings.subtitleTextAlignment,
+ textAlign: textAlign,
duration: const Duration(milliseconds: 200),
- child: OutlinedText(
- textSpans: [
- TextSpan(
- text: context.l10n.settingsSubtitleThemeSample,
+ child: Builder(
+ builder: (context) => TextBackgroundPainter(
+ spans: textSpans,
+ style: DefaultTextStyle.of(context).style.copyWith(
+ backgroundColor: settings.subtitleBackgroundColor,
+ ),
+ textAlign: textAlign,
+ child: OutlinedText(
+ textSpans: textSpans,
+ outlineWidth: settings.subtitleShowOutline ? 1 : 0,
+ outlineColor: outlineColor,
),
- ],
- outlineWidth: settings.subtitleShowOutline ? 1 : 0,
- outlineColor: outlineColor,
+ ),
),
),
),
diff --git a/lib/widgets/viewer/visual/subtitle/ass_parser.dart b/lib/widgets/viewer/visual/subtitle/ass_parser.dart
index 9bbd71ce3..b15414474 100644
--- a/lib/widgets/viewer/visual/subtitle/ass_parser.dart
+++ b/lib/widgets/viewer/visual/subtitle/ass_parser.dart
@@ -389,7 +389,7 @@ class AssParser {
);
}
- static String _replaceChars(String text) => text.replaceAll(r'\h', noBreakSpace).replaceAll(r'\N', '\n');
+ static String _replaceChars(String text) => text.replaceAll(r'\h', noBreakSpace).replaceAll(r'\N', '\n').trim();
static int? _parseAlpha(String param) {
final match = alphaPattern.firstMatch(param);
diff --git a/lib/widgets/viewer/visual/subtitle/subtitle.dart b/lib/widgets/viewer/visual/subtitle/subtitle.dart
index e9a5cac3e..a90bf5424 100644
--- a/lib/widgets/viewer/visual/subtitle/subtitle.dart
+++ b/lib/widgets/viewer/visual/subtitle/subtitle.dart
@@ -1,5 +1,6 @@
import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/basic/outlined_text.dart';
+import 'package:aves/widgets/common/basic/text_background_painter.dart';
import 'package:aves/widgets/viewer/video/controller.dart';
import 'package:aves/widgets/viewer/visual/state.dart';
import 'package:aves/widgets/viewer/visual/subtitle/ass_parser.dart';
@@ -42,7 +43,6 @@ class VideoSubtitles extends StatelessWidget {
];
final baseStyle = TextStyle(
color: settings.subtitleTextColor,
- backgroundColor: settings.subtitleBackgroundColor,
fontSize: settings.subtitleFontSize,
shadows: settings.subtitleShowOutline ? baseShadows : null,
);
@@ -243,7 +243,14 @@ class VideoSubtitles extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Align(
alignment: Alignment(alignX, alignY),
- child: child,
+ child: TextBackgroundPainter(
+ spans: spans,
+ style: DefaultTextStyle.of(context).style.merge(spans.first.style!.copyWith(
+ backgroundColor: settings.subtitleBackgroundColor,
+ )),
+ textAlign: textAlign,
+ child: child,
+ ),
),
);
}