drop_cap_text.dart 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. // 完整 DropCapText 组件,支持 DropCapPosition.bottomRight,支持 \n 换行符自动避让右下角图像
  2. import 'dart:math';
  3. import 'package:flutter/material.dart';
  4. enum DropCapMode {
  5. inside,
  6. upwards,
  7. aside,
  8. baseline,
  9. }
  10. enum DropCapPosition {
  11. start,
  12. end,
  13. bottomRight,
  14. }
  15. class DropCap extends StatelessWidget {
  16. final Widget child;
  17. final double width, height;
  18. const DropCap(
  19. {super.key,
  20. required this.child,
  21. required this.width,
  22. required this.height});
  23. @override
  24. Widget build(BuildContext context) =>
  25. SizedBox(width: width, height: height, child: child);
  26. }
  27. class DropCapText extends StatelessWidget {
  28. final String data;
  29. final DropCapMode mode;
  30. final TextStyle? style, dropCapStyle;
  31. final TextAlign textAlign;
  32. final DropCap? dropCap;
  33. final EdgeInsets dropCapPadding;
  34. final Offset indentation;
  35. final bool forceNoDescent, parseInlineMarkdown;
  36. final TextDirection textDirection;
  37. final DropCapPosition? dropCapPosition;
  38. final int dropCapChars;
  39. final int? maxLines;
  40. final TextOverflow overflow;
  41. const DropCapText(
  42. this.data, {
  43. super.key,
  44. this.mode = DropCapMode.inside,
  45. this.style,
  46. this.dropCapStyle,
  47. this.textAlign = TextAlign.start,
  48. this.dropCap,
  49. this.dropCapPadding = EdgeInsets.zero,
  50. this.indentation = Offset.zero,
  51. this.dropCapChars = 1,
  52. this.forceNoDescent = false,
  53. this.parseInlineMarkdown = false,
  54. this.textDirection = TextDirection.ltr,
  55. this.overflow = TextOverflow.clip,
  56. this.maxLines,
  57. this.dropCapPosition,
  58. });
  59. @override
  60. Widget build(BuildContext context) {
  61. final textStyle = const TextStyle(fontSize: 14, height: 1.3).merge(style);
  62. final capStyle = TextStyle(
  63. color: textStyle.color,
  64. fontSize: textStyle.fontSize! * 5.5,
  65. fontFamily: textStyle.fontFamily,
  66. fontWeight: textStyle.fontWeight,
  67. fontStyle: textStyle.fontStyle,
  68. height: 1,
  69. ).merge(dropCapStyle);
  70. final mdData = parseInlineMarkdown ? MarkdownParser(data) : null;
  71. final dropCapStr = (mdData?.plainText ?? data)
  72. .substring(0, dropCap != null ? 0 : dropCapChars);
  73. final mdRest = parseInlineMarkdown ? mdData!.subchars(dropCapChars) : null;
  74. final restData = data.substring(dropCap != null ? 0 : dropCapChars);
  75. double capWidth, capHeight;
  76. if (dropCap != null) {
  77. capWidth = dropCap!.width;
  78. capHeight = dropCap!.height;
  79. } else {
  80. final capPainter = TextPainter(
  81. text: TextSpan(text: dropCapStr, style: capStyle),
  82. textDirection: textDirection,
  83. )..layout();
  84. capWidth = capPainter.width;
  85. capHeight = capPainter.height;
  86. if (forceNoDescent) {
  87. final metrics = capPainter.computeLineMetrics();
  88. if (metrics.isNotEmpty) capHeight -= metrics[0].descent * 0.95;
  89. }
  90. }
  91. capWidth += dropCapPadding.horizontal;
  92. capHeight += dropCapPadding.vertical;
  93. return LayoutBuilder(builder: (context, constraints) {
  94. final restParagraphs = restData.split('\n');
  95. final children = <Widget>[];
  96. for (int p = 0; p < restParagraphs.length; p++) {
  97. final para = restParagraphs[p];
  98. final span = TextSpan(text: para, style: textStyle);
  99. final tp = TextPainter(
  100. text: span,
  101. textDirection: textDirection,
  102. textAlign: textAlign,
  103. )..layout(maxWidth: constraints.maxWidth);
  104. final lines = tp.computeLineMetrics();
  105. final lineHeight = tp.preferredLineHeight;
  106. if (dropCapPosition == DropCapPosition.bottomRight &&
  107. p == restParagraphs.length - 1) {
  108. final dropCapLines = (capHeight / lineHeight).ceil();
  109. final splitLine = max(0, lines.length - dropCapLines);
  110. int charSplit = 0;
  111. double yPos = 0;
  112. for (int i = 0; i < lines.length; i++) {
  113. if (i == splitLine) break;
  114. yPos += lines[i].height;
  115. final pos = tp.getPositionForOffset(
  116. Offset(tp.width - 1, yPos - lines[i].descent));
  117. charSplit = pos.offset;
  118. }
  119. final beforeText = para.substring(0, min(charSplit, para.length));
  120. final afterText = para.substring(min(charSplit, para.length));
  121. if (beforeText.isNotEmpty) {
  122. children.add(RichText(
  123. text: TextSpan(text: beforeText, style: textStyle),
  124. textDirection: textDirection,
  125. textAlign: textAlign,
  126. ));
  127. }
  128. children.add(Row(
  129. crossAxisAlignment: lines.length > 1
  130. ? CrossAxisAlignment.end
  131. : CrossAxisAlignment.start,
  132. children: [
  133. Expanded(
  134. child: RichText(
  135. text: TextSpan(text: afterText, style: textStyle),
  136. textDirection: textDirection,
  137. textAlign: textAlign,
  138. ),
  139. ),
  140. Padding(
  141. padding: dropCapPadding,
  142. child: dropCap ??
  143. RichText(
  144. textDirection: textDirection,
  145. textAlign: textAlign,
  146. text: TextSpan(text: dropCapStr, style: capStyle),
  147. ),
  148. ),
  149. ],
  150. ));
  151. } else {
  152. children.add(RichText(
  153. text: span,
  154. textDirection: textDirection,
  155. textAlign: textAlign,
  156. ));
  157. }
  158. if (p < restParagraphs.length - 1) {
  159. children.add(const SizedBox(height: 4));
  160. }
  161. }
  162. return Column(
  163. crossAxisAlignment: CrossAxisAlignment.start,
  164. mainAxisSize: MainAxisSize.min,
  165. children: children,
  166. );
  167. });
  168. }
  169. }
  170. // 以下 MarkdownParser 与 MarkdownSpan 与 Markup 类保持原样即可
  171. class MarkdownParser {
  172. final String data;
  173. late List<MarkdownSpan> spans;
  174. String plainText = '';
  175. List<TextSpan> toTextSpanList() => spans.map((s) => s.toTextSpan()).toList();
  176. MarkdownParser subchars(int startIndex, [int? endIndex]) {
  177. final subspans = <MarkdownSpan>[];
  178. int skip = startIndex;
  179. for (var span in spans) {
  180. if (skip <= 0) {
  181. subspans.add(span);
  182. } else if (span.text.length < skip) {
  183. skip -= span.text.length;
  184. } else {
  185. subspans.add(MarkdownSpan(
  186. style: span.style,
  187. markups: span.markups,
  188. text: span.text.substring(skip),
  189. ));
  190. skip = 0;
  191. }
  192. }
  193. return MarkdownParser(subspans.map((e) => e.text).join());
  194. }
  195. MarkdownParser(this.data) {
  196. plainText = '';
  197. spans = [MarkdownSpan(text: '', markups: [], style: const TextStyle())];
  198. bool bold = false, italic = false, underline = false;
  199. const b = '**', i = '_', u = '++';
  200. void addSpan(String markup, bool isOpening) {
  201. final markups = [Markup(markup, isOpening)];
  202. if (bold && markup != b) markups.add(Markup(b, true));
  203. if (italic && markup != i) markups.add(Markup(i, true));
  204. if (underline && markup != u) markups.add(Markup(u, true));
  205. spans.add(MarkdownSpan(
  206. text: '',
  207. markups: markups,
  208. style: TextStyle(
  209. fontWeight: bold ? FontWeight.bold : null,
  210. fontStyle: italic ? FontStyle.italic : null,
  211. decoration: underline ? TextDecoration.underline : null,
  212. ),
  213. ));
  214. }
  215. bool check(int i, String m) =>
  216. data.substring(i, min(i + m.length, data.length)) == m;
  217. for (int c = 0; c < data.length; c++) {
  218. if (check(c, b)) {
  219. bold = !bold;
  220. addSpan(b, bold);
  221. c += b.length - 1;
  222. } else if (check(c, i)) {
  223. italic = !italic;
  224. addSpan(i, italic);
  225. c += i.length - 1;
  226. } else if (check(c, u)) {
  227. underline = !underline;
  228. addSpan(u, underline);
  229. c += u.length - 1;
  230. } else {
  231. spans.last.text += data[c];
  232. plainText += data[c];
  233. }
  234. }
  235. }
  236. }
  237. class MarkdownSpan {
  238. final TextStyle style;
  239. final List<Markup> markups;
  240. String text;
  241. MarkdownSpan(
  242. {required this.text, required this.style, required this.markups});
  243. TextSpan toTextSpan() => TextSpan(text: text, style: style);
  244. }
  245. class Markup {
  246. final String code;
  247. final bool isActive;
  248. Markup(this.code, this.isActive);
  249. }