Created
January 27, 2021 16:42
-
-
Save daohoangson/17ab33dcc2c21b3e2b2864bccf3d4279 to your computer and use it in GitHub Desktop.
Flutter demo with TextPainter and PageView
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/material.dart'; | |
import 'alice_in_wonderland.dart'; | |
void main() { | |
runApp(MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Flutter Demo', | |
home: Home(), | |
); | |
} | |
} | |
class Home extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text('Pager Demo'), | |
), | |
body: Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: SafeArea( | |
child: LayoutBuilder( | |
builder: (context, bc) => Pager( | |
size: bc.biggest, | |
// https://gist.githubusercontent.com/phillipj/4944029/raw/75ba2243dd5ec2875f629bf5d79f6c1e4b5a8b46/alice_in_wonderland.txt | |
text: kText | |
.replaceAll('\n\n ', 'xXx') | |
.replaceAll('\n', ' ') | |
.replaceAll('xXx', '\n\n'), | |
), | |
), | |
), | |
), | |
); | |
} | |
} | |
class Pager extends StatefulWidget { | |
final Size size; | |
final String text; | |
const Pager({Key key, this.size, this.text}) : super(key: key); | |
@override | |
State<Pager> createState() => _PagerState(); | |
} | |
class _PagerState extends State<Pager> { | |
final currentPage = ValueNotifier(1); | |
final indeces = <int>[]; | |
@override | |
void didChangeDependencies() { | |
super.didChangeDependencies(); | |
_calculate(); | |
} | |
@override | |
void didUpdateWidget(covariant Pager oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
_calculate(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final theme = Theme.of(context); | |
return Stack( | |
children: [ | |
PageView.builder( | |
itemBuilder: (context, index) { | |
final x2 = index * 2; | |
return Text(widget.text.substring(indeces[x2], indeces[x2 + 1])); | |
}, | |
itemCount: indeces.length ~/ 2, | |
onPageChanged: (page) => currentPage.value = page + 1, | |
), | |
Positioned( | |
bottom: 16, | |
left: 0, | |
right: 0, | |
child: Center( | |
child: Container( | |
color: theme.disabledColor, | |
padding: const EdgeInsets.all(8.0), | |
child: AnimatedBuilder( | |
animation: currentPage, | |
builder: (_, __) => Text( | |
'${currentPage.value} / ${indeces.length ~/ 2}', | |
style: TextStyle(color: theme.colorScheme.onBackground), | |
), | |
), | |
), | |
), | |
), | |
], | |
); | |
} | |
void _calculate() { | |
final stopwatch = Stopwatch()..start(); | |
indeces | |
..clear() | |
..addAll(Splitter( | |
style: DefaultTextStyle.of(context).style, | |
textFull: widget.text, | |
textDirection: Directionality.of(context), | |
textScaleFactor: MediaQuery.of(context).textScaleFactor, | |
viewport: Size(widget.size.width, widget.size.height), | |
).split(0)); | |
print(stopwatch.elapsed); | |
} | |
} | |
class Splitter { | |
final TextStyle style; | |
final String textFull; | |
final TextDirection textDirection; | |
final double textScaleFactor; | |
final Size viewport; | |
Splitter({ | |
this.style, | |
this.textFull, | |
this.textDirection, | |
this.textScaleFactor, | |
this.viewport, | |
}); | |
List<int> split([int startIndex = 0]) { | |
final text = textFull.substring(startIndex); | |
final size0 = _layout(text); | |
if (size0.height <= viewport.height) { | |
// text fits viewport, no chopping required | |
return [startIndex, startIndex + text.length]; | |
} | |
final newLineOffset = _fit(text, '\n'); | |
final spaceOffset = _fit(text, ' ', newLineOffset ?? 0); | |
final offset = spaceOffset ?? newLineOffset; | |
if (offset != null) { | |
return [ | |
startIndex, | |
startIndex + offset, | |
...split(startIndex + offset + 1), | |
]; | |
} else { | |
// couldn't split in anyway... Just take the entire text | |
return [startIndex, startIndex + text.length]; | |
} | |
} | |
int _fit(String text, Pattern pattern, [int offset = 0]) { | |
int goodOffset; | |
while (true) { | |
offset = text.indexOf(pattern, offset + 1); | |
if (offset == -1) { | |
// couldn't fit | |
return null; | |
} | |
final size = _layout(text.substring(0, offset)); | |
if (size.height < viewport.height) { | |
goodOffset = offset; | |
} else { | |
return goodOffset; | |
} | |
} | |
} | |
Size _layout(String text) { | |
final painter = TextPainter( | |
text: TextSpan(text: text, style: style), | |
textDirection: textDirection, | |
textScaleFactor: textScaleFactor, | |
)..layout( | |
minWidth: viewport.width, | |
maxWidth: viewport.width, | |
); | |
return painter.size; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment