// 完整 DropCapText 组件,支持 DropCapPosition.bottomRight,支持 \n 换行符自动避让右下角图像 import 'dart:math'; import 'package:flutter/material.dart'; enum DropCapMode { inside, upwards, aside, baseline, } enum DropCapPosition { start, end, bottomRight, } class DropCap extends StatelessWidget { final Widget child; final double width, height; const DropCap( {super.key, required this.child, required this.width, required this.height}); @override Widget build(BuildContext context) => SizedBox(width: width, height: height, child: child); } class DropCapText extends StatelessWidget { final String data; final DropCapMode mode; final TextStyle? style, dropCapStyle; final TextAlign textAlign; final DropCap? dropCap; final EdgeInsets dropCapPadding; final Offset indentation; final bool forceNoDescent, parseInlineMarkdown; final TextDirection textDirection; final DropCapPosition? dropCapPosition; final int dropCapChars; final int? maxLines; final TextOverflow overflow; const DropCapText( this.data, { super.key, this.mode = DropCapMode.inside, this.style, this.dropCapStyle, this.textAlign = TextAlign.start, this.dropCap, this.dropCapPadding = EdgeInsets.zero, this.indentation = Offset.zero, this.dropCapChars = 1, this.forceNoDescent = false, this.parseInlineMarkdown = false, this.textDirection = TextDirection.ltr, this.overflow = TextOverflow.clip, this.maxLines, this.dropCapPosition, }); @override Widget build(BuildContext context) { final textStyle = const TextStyle(fontSize: 14, height: 1.3).merge(style); final capStyle = TextStyle( color: textStyle.color, fontSize: textStyle.fontSize! * 5.5, fontFamily: textStyle.fontFamily, fontWeight: textStyle.fontWeight, fontStyle: textStyle.fontStyle, height: 1, ).merge(dropCapStyle); final mdData = parseInlineMarkdown ? MarkdownParser(data) : null; final dropCapStr = (mdData?.plainText ?? data) .substring(0, dropCap != null ? 0 : dropCapChars); final mdRest = parseInlineMarkdown ? mdData!.subchars(dropCapChars) : null; final restData = data.substring(dropCap != null ? 0 : dropCapChars); double capWidth, capHeight; if (dropCap != null) { capWidth = dropCap!.width; capHeight = dropCap!.height; } else { final capPainter = TextPainter( text: TextSpan(text: dropCapStr, style: capStyle), textDirection: textDirection, )..layout(); capWidth = capPainter.width; capHeight = capPainter.height; if (forceNoDescent) { final metrics = capPainter.computeLineMetrics(); if (metrics.isNotEmpty) capHeight -= metrics[0].descent * 0.95; } } capWidth += dropCapPadding.horizontal; capHeight += dropCapPadding.vertical; return LayoutBuilder(builder: (context, constraints) { final restParagraphs = restData.split('\n'); final children = []; for (int p = 0; p < restParagraphs.length; p++) { final para = restParagraphs[p]; final span = TextSpan(text: para, style: textStyle); final tp = TextPainter( text: span, textDirection: textDirection, textAlign: textAlign, )..layout(maxWidth: constraints.maxWidth); final lines = tp.computeLineMetrics(); final lineHeight = tp.preferredLineHeight; if (dropCapPosition == DropCapPosition.bottomRight && p == restParagraphs.length - 1) { final dropCapLines = (capHeight / lineHeight).ceil(); final splitLine = max(0, lines.length - dropCapLines); int charSplit = 0; double yPos = 0; for (int i = 0; i < lines.length; i++) { if (i == splitLine) break; yPos += lines[i].height; final pos = tp.getPositionForOffset( Offset(tp.width - 1, yPos - lines[i].descent)); charSplit = pos.offset; } final beforeText = para.substring(0, min(charSplit, para.length)); final afterText = para.substring(min(charSplit, para.length)); if (beforeText.isNotEmpty) { children.add(RichText( text: TextSpan(text: beforeText, style: textStyle), textDirection: textDirection, textAlign: textAlign, )); } children.add(Row( crossAxisAlignment: lines.length > 1 ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ Expanded( child: RichText( text: TextSpan(text: afterText, style: textStyle), textDirection: textDirection, textAlign: textAlign, ), ), Padding( padding: dropCapPadding, child: dropCap ?? RichText( textDirection: textDirection, textAlign: textAlign, text: TextSpan(text: dropCapStr, style: capStyle), ), ), ], )); } else { children.add(RichText( text: span, textDirection: textDirection, textAlign: textAlign, )); } if (p < restParagraphs.length - 1) { children.add(const SizedBox(height: 4)); } } return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: children, ); }); } } // 以下 MarkdownParser 与 MarkdownSpan 与 Markup 类保持原样即可 class MarkdownParser { final String data; late List spans; String plainText = ''; List toTextSpanList() => spans.map((s) => s.toTextSpan()).toList(); MarkdownParser subchars(int startIndex, [int? endIndex]) { final subspans = []; int skip = startIndex; for (var span in spans) { if (skip <= 0) { subspans.add(span); } else if (span.text.length < skip) { skip -= span.text.length; } else { subspans.add(MarkdownSpan( style: span.style, markups: span.markups, text: span.text.substring(skip), )); skip = 0; } } return MarkdownParser(subspans.map((e) => e.text).join()); } MarkdownParser(this.data) { plainText = ''; spans = [MarkdownSpan(text: '', markups: [], style: const TextStyle())]; bool bold = false, italic = false, underline = false; const b = '**', i = '_', u = '++'; void addSpan(String markup, bool isOpening) { final markups = [Markup(markup, isOpening)]; if (bold && markup != b) markups.add(Markup(b, true)); if (italic && markup != i) markups.add(Markup(i, true)); if (underline && markup != u) markups.add(Markup(u, true)); spans.add(MarkdownSpan( text: '', markups: markups, style: TextStyle( fontWeight: bold ? FontWeight.bold : null, fontStyle: italic ? FontStyle.italic : null, decoration: underline ? TextDecoration.underline : null, ), )); } bool check(int i, String m) => data.substring(i, min(i + m.length, data.length)) == m; for (int c = 0; c < data.length; c++) { if (check(c, b)) { bold = !bold; addSpan(b, bold); c += b.length - 1; } else if (check(c, i)) { italic = !italic; addSpan(i, italic); c += i.length - 1; } else if (check(c, u)) { underline = !underline; addSpan(u, underline); c += u.length - 1; } else { spans.last.text += data[c]; plainText += data[c]; } } } } class MarkdownSpan { final TextStyle style; final List markups; String text; MarkdownSpan( {required this.text, required this.style, required this.markups}); TextSpan toTextSpan() => TextSpan(text: text, style: style); } class Markup { final String code; final bool isActive; Markup(this.code, this.isActive); }