From 15c225fa89c34ee8b7a415f3f8cf44987c934293 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sat, 5 Nov 2022 15:45:09 +0100 Subject: [PATCH] #381 fixed inconsistent background height for multi-script subtitles --- CHANGELOG.md | 1 + .../common/basic/text_background_painter.dart | 76 +++++++++++++++++++ .../settings/video/subtitle_sample.dart | 30 +++++--- .../viewer/visual/subtitle/ass_parser.dart | 2 +- .../viewer/visual/subtitle/subtitle.dart | 11 ++- 5 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 lib/widgets/common/basic/text_background_painter.dart 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, + ), ), ); }