Issue
Let's say I have a very long paragraph of text and want to overlay annotations over character 5, 10, and 1500 -- how can I find the locations of those characters?
I considered referencing TextSpan components, however, unlike the rest of Flutter, these are not Widgets and cannot have a GlobalKey.
Solution
Easy peasy with TextPainter and Paragraph (thanks @pskink). See the important caveats for multiline web text at the end.
With TextPainter
import 'package:flutter/material.dart';
final loremIpsum =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Welcome to Flutter',
home: Scaffold(
appBar: AppBar(
title: Text('Welcome to Flutter'),
),
body: Center(
child: Container(
width: 350,
color: Color.fromARGB(100, 0, 0, 0),
child: SelectText(),
),
),
),
);
}
}
class SelectText extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final textPainter = TextPainter(
text: TextSpan(text: loremIpsum),
textDirection: TextDirection.ltr,
);
final width = constraints.maxWidth;
textPainter.layout(
minWidth: 20,
maxWidth: width,
);
final height = textPainter.height;
return Container(
width: width,
height: height,
color: Colors.yellow,
child: GestureDetector(
onTapDown: (details) {
print(
"Selection: ${textPainter.getPositionForOffset(details.localPosition)}");
},
child: CustomPaint(
size: Size(width, height), // Parent width, text height
painter: TextCustomPainter(textPainter),
),
));
});
}
}
class TextCustomPainter extends CustomPainter {
TextPainter textPainter;
TextCustomPainter(this.textPainter, {Listenable? repaint})
: super(repaint: repaint);
@override
void paint(Canvas canvas, Size size) {
textPainter.paint(canvas, Offset(0, 0));
}
@override
bool shouldRepaint(CustomPainter old) {
return false;
}
}
With Paragraph
import 'package:flutter/material.dart';
import 'dart:ui';
import 'dart:ui' as ui;
final loremIpsum =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Welcome to Flutter',
home: Scaffold(
appBar: AppBar(
title: Text('Welcome to Flutter'),
),
body: Center(
child: Container(
width: 350,
color: Color.fromARGB(100, 0, 0, 0),
child: SelectText(),
),
),
),
);
}
}
class SelectText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final TextStyle style = TextStyle(
color: Colors.black,
);
// Set width to max allowed by parent
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(
ui.ParagraphStyle(
fontSize: style.fontSize,
// There unfortunelly are some things to be copied from your common TextStyle to ParagraphStyle :C
fontFamily: style.fontFamily,
// IDK why it is like this, this is somewhat weird especially when there is `pushStyle` which can use the TextStyle...
fontStyle: style.fontStyle,
fontWeight: style.fontWeight,
textAlign: TextAlign.justify,
//maxLines: 25,
))
..pushStyle(style
.getTextStyle()) // To use multiple styles, you must make use of the builder and `pushStyle` and then `addText` (or optionally `pop`).
..addText(loremIpsum);
final width = constraints.maxWidth;
final ui.Paragraph paragraph = paragraphBuilder.build()
..layout(ui.ParagraphConstraints(width: width));
paragraph.layout(ParagraphConstraints(
width: width,
));
final height = paragraph.height;
return Container(
width: width,
height: height,
color: Colors.yellow,
child: GestureDetector(
onTapDown: (details) {
// BUG. On web- position is only correct for first line.
print(
"Selection: ${paragraph.getPositionForOffset(details.localPosition)}");
},
child: CustomPaint(
size: Size(width, height), // Parent width, text height
painter: TextCustomPainter(paragraph),
),
));
});
}
}
class TextCustomPainter extends CustomPainter {
Paragraph paragraph;
TextCustomPainter(this.paragraph, {Listenable repaint})
: super(repaint: repaint);
@override
void paint(Canvas canvas, Size size) {
canvas.drawParagraph(paragraph, const Offset(0, 0));
}
@override
bool shouldRepaint(CustomPainter old) {
return false;
}
}
Web caveats
Both of the above methods are currently broken for web as they only correctly report the text position for the first line of text and are totally broken for multi-line text. See: https://github.com/flutter/flutter/issues/44121
This has been an open bug for web more than a year and there's very slow activity here. Don't expect a fix any time soon. Currently, some bad behavior is fixed in the master branch IFF you compile with both FLUTTER_WEB_USE_EXPERIMENTAL_CANVAS_TEXT and compile with --release
, making your project undebuggable!
flutter build web --release --dart-define=FLUTTER_WEB_USE_EXPERIMENTAL_CANVAS_TEXT=true
Flutter could use a simple addition to TextWidget to make this functionality more easily available :-/
Answered By - user48956
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.