| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- // 完整 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 = <Widget>[];
- 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<MarkdownSpan> spans;
- String plainText = '';
- List<TextSpan> toTextSpanList() => spans.map((s) => s.toTextSpan()).toList();
- MarkdownParser subchars(int startIndex, [int? endIndex]) {
- final subspans = <MarkdownSpan>[];
- 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<Markup> 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);
- }
|