我有多个文本文件很长,我想将它们拆分成页面以便于阅读和导航。这是一个例子:
因此,给定页面的容器大小和作为输入的文本(具有特定字体样式),结果应该是显示此文本所需的总页数。
我知道文本可以像@pskink 提到的那样显示在 ListView 中,但我想让页面像在 Kindle 中一样静态,并提前显示页面总数,以便可以按索引切换到任何页面。
我还发现了一个相关的 Flutter 框架问题,可能与这个问题有关。不确定它是否有限制。
我有多个文本文件很长,我想将它们拆分成页面以便于阅读和导航。这是一个例子:
因此,给定页面的容器大小和作为输入的文本(具有特定字体样式),结果应该是显示此文本所需的总页数。
我知道文本可以像@pskink 提到的那样显示在 ListView 中,但我想让页面像在 Kindle 中一样静态,并提前显示页面总数,以便可以按索引切换到任何页面。
我还发现了一个相关的 Flutter 框架问题,可能与这个问题有关。不确定它是否有限制。
我通过创建一个自定义小部件MultiPageText
来解决这个问题,该小部件根据可用空间将整个文本分布在多个页面上。
由于仅仅为了确定每个页面的截止点而布置多个段落是昂贵的,因此通常会将昂贵的_getPageText()
函数放在单独的隔离中。不幸的是,目前这是不可能的,因为非主隔离不能使用dart:ui
,因此该函数在主隔离中执行。
这是完整的代码,包括一些可以在 DartPad 中运行的注释:
import 'dart:ui' as ui;
import 'dart:math' as math;
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: ExampleMultiPageText(),
),
),
);
}
}
class MultiPageText extends StatefulWidget {
/// The entire text that has to be distributed across one or
/// more pages.
final String fullText;
/// The [TextStyle] that is applied to the [fullText].
final TextStyle textStyle;
/// The size of the entire widget.
///
/// This size's height is the upper limit for the [PageTextContainer]'s that contains
/// each page text. If the [PageNavigatorMenu] is included (i.e. if
/// [usePageNavigation] is `true`), its height (50 pixel) will be deducted
/// leaving the only remaining height for the [PageTextContainer].
final Size size;
/// The padding that will be applied to the [PageTextContainer].
final EdgeInsets paddingTextBox;
/// Whether the [PageNavigatorMenu] will be rendered below the [PageTextContainer].
final bool usePageNavigation;
/// The [PageTextContainer]'s decoration.
final BoxDecoration? decoration;
/// Creates a widget that distributes the provided text across as many pages as necessary.
///
/// Besides the TextContainer that holds the text for the given page, the widget can also
/// contain a PageNavigatorMenu for navigating between the different pages.
const MultiPageText({
Key? key,
required this.fullText,
required this.size,
this.textStyle = const TextStyle(
fontSize: 10,
color: Colors.white,
),
this.paddingTextBox = const EdgeInsets.all(
10,
),
this.usePageNavigation = true,
this.decoration,
}) : super(key: key);
@override
State<MultiPageText> createState() => _MultiPageTextState();
}
class _MultiPageTextState extends State<MultiPageText> {
int _currentPageIndex = 0;
final double _pageNavigatorHeight = 40;
final int _upperLayoutRunsLimit = 20;
late List<String> _pages;
late Size _availableSize;
@override
void initState() {
_pages = _getPageTexts();
super.initState();
}
List<String> _getPageTexts() {
List<String> pages = <String>[];
String remainingText = widget.fullText;
_availableSize = _calculateAvailableSize(
size: widget.size,
padding: widget.paddingTextBox,
usePageNavigation: widget.usePageNavigation,
);
double widthFactor = 0.5;
int retries = 0;
int pageCharacterLimit = _estimatePageCharacterLimit(
size: _availableSize,
textStyle: widget.textStyle,
widthFactor: widthFactor,
);
while (remainingText.isNotEmpty) {
final String pageTextEstimate = _getPageTextEstimate(
text: remainingText,
pageCharacterLimit: pageCharacterLimit,
);
final PageProperties pageProperties = _getPageText(
text: pageTextEstimate,
textStyle: widget.textStyle,
size: _availableSize,
);
if (_shouldOptimizeEstimates(pageProperties.layoutRuns)) {
// Optimize widthFactor for better pageTextEstimates
widthFactor = _updateWidthFactor(
widthFactor: widthFactor,
layoutRuns: pageProperties.layoutRuns,
upperLayoutRunsLimit: _upperLayoutRunsLimit,
);
// Update pageCharacterLimit
pageCharacterLimit = _estimatePageCharacterLimit(
size: _availableSize,
textStyle: widget.textStyle,
widthFactor: widthFactor,
);
}
if (_performRetry(pageProperties.layoutRuns, retries)) {
retries++;
continue;
}
pages.add(pageProperties.text);
remainingText =
remainingText.substring(pageProperties.text.length).trimLeft();
retries = 0;
}
return pages;
}
/// Calculates the available space for the [ui.ParagraphBuilder] (i.e. its width constraint).
///
/// That means subtracting any padding of the enclosing [Container] as well as removing the
/// height of the page navigation (only if [usePageNavigation] is `true`).
Size _calculateAvailableSize({
required Size size,
required EdgeInsets padding,
required bool usePageNavigation,
}) {
double availableHeight = size.height -
(widget.paddingTextBox.top + widget.paddingTextBox.bottom);
if (usePageNavigation) {
availableHeight = availableHeight - _pageNavigatorHeight;
}
final double availableWidth =
size.width - (widget.paddingTextBox.right + widget.paddingTextBox.left);
return Size(availableWidth, availableHeight);
}
/// Updates the [widthFactor] based on the number of actual [layoutRuns].
///
/// If the [upperLayoutRunsLimit] was exceeded, we want to tighten our character estimate
/// (hence increase the [widthFactor] by `0.05`). Otherwise (i.e. if [layoutRuns] = `1`) the
/// constraint should be loosened (decrease the [widthFactor] by `0.05`).
double _updateWidthFactor({
required double widthFactor,
required int layoutRuns,
required int upperLayoutRunsLimit,
}) {
final double newWidthFactor = layoutRuns >= upperLayoutRunsLimit
? widthFactor + 0.05
: widthFactor - 0.05;
return newWidthFactor;
}
/// (Over)Estimates the character limit for a given page.
///
/// The [widthFactor] is automatically chosen and adjusted by the parent function
/// so that the resulting maximum character will be slightly overestimated.
int _estimatePageCharacterLimit({
required Size size,
required TextStyle textStyle,
required double widthFactor,
}) {
final characterHeight = textStyle.fontSize!;
final characterWidth = characterHeight * widthFactor;
return ((size.height * size.width) / (characterHeight * characterWidth))
.ceil();
}
/// Retrieves the part of the [text] that fits within the [pageCharacterLimit] starting
/// from the beginning of the string.
String _getPageTextEstimate({
required String text,
required int pageCharacterLimit,
}) {
final initialPageTextEstimate =
text.substring(0, math.min(pageCharacterLimit + 1, text.length));
final substringIndex =
initialPageTextEstimate.lastIndexOf(RegExp(r"\s+\b|\b\s+|[\.?!]"));
final pageTextEstimate =
text.substring(0, math.min(substringIndex + 1, text.length));
return pageTextEstimate;
}
/// Determines the final text for the given page and returns the respective
/// [PageProperties] with the number of necessary `layoutRuns` for optimization
/// and the [text] itself.
PageProperties _getPageText({
required String text,
required TextStyle textStyle,
required Size size,
}) {
double paragraphHeight = 10000;
String currentText = text;
int layoutRuns = 0;
final RegExp regExp = RegExp(r"\S+[\W]*$");
while (paragraphHeight > size.height) {
final paragraph = ParagraphPainter.layoutParagraph(
text: currentText, textStyle: textStyle, size: size);
paragraphHeight = paragraph.height;
if (paragraphHeight > size.height) {
currentText = currentText.replaceFirst(regExp, '');
}
layoutRuns = layoutRuns + 1;
}
return PageProperties(currentText, layoutRuns);
}
bool _performRetry(int layoutRuns, int retries) {
return layoutRuns == 1 && retries <= 0;
}
bool _shouldOptimizeEstimates(int layoutRuns) {
return layoutRuns > _upperLayoutRunsLimit || layoutRuns == 1;
}
void _updatePageIndex(PageUpdateOperation pageUpdateOperation) {
switch (pageUpdateOperation) {
case PageUpdateOperation.first:
setState(() {
_currentPageIndex = 0;
});
break;
case PageUpdateOperation.previous:
setState(() {
_currentPageIndex--;
});
break;
case PageUpdateOperation.next:
setState(() {
_currentPageIndex++;
});
break;
case PageUpdateOperation.last:
setState(() {
_currentPageIndex = _pages.length - 1;
});
break;
}
}
@override
Widget build(BuildContext context) {
final pageTextContainer = PageTextContainer(
text: _pages[_currentPageIndex],
textStyle: widget.textStyle,
padding: widget.paddingTextBox,
size: _availableSize,
decoration: widget.decoration,
);
return widget.usePageNavigation
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
pageTextContainer,
PageNavigatorMenu(
size: Size(widget.size.width, _pageNavigatorHeight),
currentPageIndex: _currentPageIndex,
pageCount: _pages.length,
updatePageIndex: _updatePageIndex,
),
],
)
: pageTextContainer;
}
}
class PageTextContainer extends StatelessWidget {
final String text;
final TextStyle textStyle;
final EdgeInsets padding;
final Size size;
final BoxDecoration? decoration;
const PageTextContainer({
Key? key,
required this.text,
required this.textStyle,
required this.padding,
required this.size,
this.decoration,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: padding,
decoration: decoration,
child: CustomPaint(
painter: ParagraphPainter(
pageText: text,
textStyle: textStyle,
),
child: SizedBox(
height: size.height,
width: size.width,
),
),
);
}
}
class PageNavigatorMenu extends StatelessWidget {
final Size size;
final int currentPageIndex;
final int pageCount;
final void Function(PageUpdateOperation) updatePageIndex;
const PageNavigatorMenu({
Key? key,
required this.size,
required this.currentPageIndex,
required this.pageCount,
required this.updatePageIndex,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: size.height,
width: size.width,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(
Icons.first_page,
),
onPressed: currentPageIndex > 0
? () => updatePageIndex(
PageUpdateOperation.first,
)
: null,
),
IconButton(
icon: const Icon(
Icons.navigate_before,
),
onPressed: currentPageIndex > 0
? () => updatePageIndex(
PageUpdateOperation.previous,
)
: null,
),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
'Page ${currentPageIndex + 1}',
),
),
),
IconButton(
icon: const Icon(
Icons.navigate_next,
),
onPressed: currentPageIndex < pageCount - 1
? () => updatePageIndex(
PageUpdateOperation.next,
)
: null,
),
IconButton(
icon: const Icon(
Icons.last_page,
),
onPressed: currentPageIndex < pageCount - 1
? () => updatePageIndex(
PageUpdateOperation.last,
)
: null,
),
],
),
);
}
}
class ParagraphPainter extends CustomPainter {
final String pageText;
final TextStyle textStyle;
ParagraphPainter({
required this.pageText,
required this.textStyle,
});
@override
void paint(Canvas canvas, Size size) {
final paragraph = layoutParagraph(
text: pageText,
textStyle: textStyle,
size: size,
);
canvas.drawParagraph(paragraph, Offset.zero);
}
static ui.Paragraph layoutParagraph({
required String text,
required TextStyle textStyle,
required Size size,
}) {
final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(
ui.ParagraphStyle(
fontSize: textStyle.fontSize,
fontFamily: textStyle.fontFamily,
fontStyle: textStyle.fontStyle,
fontWeight: textStyle.fontWeight,
textAlign: TextAlign.left,
),
)
..pushStyle(textStyle.getTextStyle())
..addText(text);
final ui.Paragraph paragraph = paragraphBuilder.build()
..layout(
ui.ParagraphConstraints(width: size.width),
);
return paragraph;
}
@override
bool shouldRepaint(ParagraphPainter oldDelegate) =>
oldDelegate.pageText != pageText || oldDelegate.textStyle != textStyle;
}
class PageProperties {
final String text;
final int layoutRuns;
PageProperties(this.text, this.layoutRuns);
@override
String toString() {
return '''PageProperties(
$text,
$layoutRuns
)''';
}
}
enum PageUpdateOperation {
first,
previous,
next,
last,
}
// Call the widget
class ExampleMultiPageText extends StatelessWidget {
const ExampleMultiPageText({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: MultiPageText(
textStyle: const TextStyle(
fontSize: 10,
color: Colors.white,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(
width: 1.0,
color: Colors.grey,
),
),
usePageNavigation: true,
fullText:
'''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam et mollis orci. Sed ullamcorper leo ipsum, sit amet feugiat neque aliquam at. Vestibulum vehicula elit eget metus iaculis ultrices. Nunc faucibus vehicula justo vitae portaPhasellus vestibulum lectus non tellus accumsan, non dictum tellus bibendum. Nulla ornare eros vitae bibendum pharetra. Fusce sit amet lobortis ex. Proin condimentum imperdiet erat, lacinia suscipit est efficitur sit amet. Nunc laoreet luctus tortor, in accumsan velit. Donec cursus velit vehicula maximus finibus. Donec quis euismod quam. In vel lacus fringilla, rhoncus eros nec, elementum massa. Donec luctus lobortis ullamcorper.
Aenean lacus ligula, rutrum ac felis in, dictum sagittis est. Integer finibus arcu magna, eget bibendum odio dignissim id. Mauris ornare ipsum maximus malesuada efficitur. Duis pulvinar neque a lectus fermentum accumsan non id arcu. Quisque congue lectus eu ante efficitur, ac semper lectus volutpat. Pellentesque dignissim turpis quam, venenatis facilisis sem rutrum non. Praesent tincidunt sodales dolor a maximus. Aliquam sit amet quam vel augue mattis luctus. Duis placerat condimentum aliquam. Quisque bibendum in ipsum non pretium. Nam lobortis libero quam, sed lacinia ex rhoncus non. Fusce viverra felis vitae finibus tincidunt. In hac habitasse platea dictumst. Praesent mollis, turpis at iaculis pulvinar, lectus enim feugiat mi, ultricies auctor lacus sapien sed ipsum.
Etiam ac mi risus. In dictum purus sapien, non tempus magna tempor vel. Suspendisse finibus lectus et sem laoreet dignissim.
Maecenas erat mi, ultrices non sollicitudin non, tristique a est. Vestibulum interdum diam nec justo eleifend tincidunt. Nulla non nulla at nulla suscipit congue.
Mauris est dui, molestie sed tempus ac, accumsan eget urna. Nullam sit amet bibendum lacus, a pellentesque nisl. Aliquam lorem eros, finibus id enim eget, faucibus ultricies erat. Sed sed pulvinar tellus, nec euismod lectus. Quisque libero metus, congue nec suscipit ut, tincidunt eget odio. Aliquam sit amet cursus magna. Nam aliquam ipsum at eleifend auctor. Fusce eu metus dui. Nulla non lacus eros. Cras elementum, ante et tristique faucibus, risus enim dignissim est, id iaculis augue turpis vel massa. Sed cursus ultricies lorem.''',
size: const Size(
200,
350,
),
),
);
}
}
我为这个问题所做的解决方法是,我使用 MediaQuery 从屏幕维度计算了可以容纳在页面上的估计字符数。
var deviceData = MediaQuery.of(context);
var deviceHeight = deviceData.size.height;
var deviceWidth = deviceData.size.width - 60; // 60 - AppBar estimated height
var deviceDimension = deviceHeight * deviceWidth;
/// Compute estimated character limit per page
/// Estimated dimension of each character: textSize * (textSize * 0.8)
/// textSize width estimated dimension is 80% of its height
var pageCharLimit = (deviceDimension / (textSize * (textSize * 0.8))).round();
debugPrint('Character limit per page: $pageCharLimit');
/// Compute pageCount base from the computed pageCharLimit
var pageCount = (textLength / pageCharLimit).round();
debugPrint('Pages: $pageCount');
然后打破文档中的文本,使用String.substring(start,end)
将其添加到列表中
List<String> pageText = [];
var index = 0;
var startStrIndex = 0;
var endStrIndex = pageCharLimit;
while (index < pageCount) {
/// Update the last index to the Document Text length
if (index == pageCount - 1) endStrIndex = textLength;
/// Add String on List<String>
pageText.add(Document.text.substring(startStrIndex, endStrIndex));
/// Update index of Document Text String to be added on [pageText]
if (index < pageCount) {
startStrIndex = endStrIndex;
endStrIndex += pageCharLimit;
}
index++;
}
代码仍然可以改进,因为单词可以在页面之间拆分。该页面还不能确定页面上显示的最后一个字符是否会将单词分成两半。
这是完整的示例。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late int textLength;
static const textSize = 16.0;
@override
void initState() {
textLength = Document.text.length;
debugPrint('Text Length: $textLength');
super.initState();
}
@override
Widget build(BuildContext context) {
var deviceData = MediaQuery.of(context);
var deviceHeight = deviceData.size.height;
var deviceWidth =
deviceData.size.width - 60; // 60 - AppBar estimated height
var deviceDimension = deviceHeight * deviceWidth;
/// Compute estimated character limit per page
/// Estimated dimension of each character: textSize * (textSize * 0.8)
/// textSize width estimated dimension is 80% of its height
var pageCharLimit = (deviceDimension / (textSize * (textSize * 0.8))).round();
debugPrint('Character limit per page: $pageCharLimit');
/// Compute pageCount base from the computed pageCharLimit
var pageCount = (textLength / pageCharLimit).round();
debugPrint('Pages: $pageCount');
List<String> pageText = [];
var index = 0;
var startStrIndex = 0;
var endStrIndex = pageCharLimit;
while (index < pageCount) {
/// Update the last index to the Document Text length
if (index == pageCount - 1) endStrIndex = textLength;
/// Add String on List<String>
pageText.add(Document.text.substring(startStrIndex, endStrIndex));
/// Update index of Document Text String to be added on [pageText]
if (index < pageCount) {
startStrIndex = endStrIndex;
endStrIndex += pageCharLimit;
}
index++;
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView.builder(
itemCount: pageCount,
itemBuilder: (BuildContext context, int index) {
return Container(
padding: const EdgeInsets.fromLTRB(0, 0, 0, 16.0),
child: Card(
child: Container(
padding: const EdgeInsets.all(16.0),
child: Text(
pageText[index],
style: const TextStyle(fontSize: textSize),
),
),
),
);
}),
);
}
}
class Document {
static const text =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Nunc sed velit dignissim sodales ut eu sem. Enim nec dui nunc mattis enim ut tellus elementum sagittis. Augue lacus viverra vitae congue eu. Posuere morbi leo urna molestie at elementum eu. Sed faucibus turpis in eu mi bibendum neque egestas congue. Id volutpat lacus laoreet non curabitur gravida arcu. Ut tristique et egestas quis ipsum suspendisse ultrices gravida. Sit amet mattis vulputate enim nulla. Risus pretium quam vulputate dignissim suspendisse in. Vel pharetra vel turpis nunc eget lorem dolor sed. Ac turpis egestas maecenas pharetra convallis posuere morbi. Quam nulla porttitor massa id neque aliquam vestibulum. Ut tortor pretium viverra suspendisse potenti nullam ac tortor. Quam lacus suspendisse faucibus interdum posuere lorem ipsum. Posuere lorem ipsum dolor sit amet. Imperdiet nulla malesuada pellentesque elit eget gravida cum sociis.'
'\n\nQuam elementum pulvinar etiam non quam lacus suspendisse faucibus. Congue quisque egestas diam in arcu cursus euismod quis. Felis donec et odio pellentesque diam volutpat. Maecenas accumsan lacus vel facilisis volutpat est velit egestas. Leo urna molestie at elementum. Facilisi nullam vehicula ipsum a arcu cursus vitae congue mauris. At imperdiet dui accumsan sit. Porttitor lacus luctus accumsan tortor posuere. Volutpat odio facilisis mauris sit amet massa vitae. Ut eu sem integer vitae justo eget magna fermentum iaculis. Volutpat diam ut venenatis tellus in metus vulputate eu scelerisque. Morbi enim nunc faucibus a pellentesque sit amet porttitor eget. Sed odio morbi quis commodo.'
'\n\nEu mi bibendum neque egestas congue quisque egestas. Libero id faucibus nisl tincidunt. Nunc mi ipsum faucibus vitae aliquet nec ullamcorper. Tristique nulla aliquet enim tortor. Risus nec feugiat in fermentum posuere. Eu non diam phasellus vestibulum. Sit amet venenatis urna cursus. Amet venenatis urna cursus eget nunc scelerisque viverra mauris in. A arcu cursus vitae congue mauris rhoncus aenean. Maecenas sed enim ut sem viverra aliquet eget. Scelerisque purus semper eget duis at tellus at. Aliquam malesuada bibendum arcu vitae. Sed augue lacus viverra vitae congue eu. Sit amet est placerat in egestas erat imperdiet sed euismod. Venenatis tellus in metus vulputate eu scelerisque felis imperdiet proin. Volutpat consequat mauris nunc congue. Nec dui nunc mattis enim ut tellus elementum. Amet purus gravida quis blandit turpis cursus. Nisl suscipit adipiscing bibendum est ultricies integer quis auctor elit.'
'\n\nNunc vel risus commodo viverra maecenas accumsan. Felis donec et odio pellentesque diam volutpat commodo. Sodales ut etiam sit amet nisl purus in mollis. Et netus et malesuada fames ac. Pretium aenean pharetra magna ac placerat vestibulum lectus mauris. Pulvinar pellentesque habitant morbi tristique. Nisl purus in mollis nunc sed id semper risus in. Elit ut aliquam purus sit amet luctus venenatis. Nulla aliquet enim tortor at. Amet luctus venenatis lectus magna fringilla urna porttitor rhoncus. Aliquam malesuada bibendum arcu vitae. Urna nec tincidunt praesent semper feugiat nibh. Vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare. Vel turpis nunc eget lorem dolor sed viverra. Bibendum neque egestas congue quisque egestas. Leo a diam sollicitudin tempor id eu. Consectetur lorem donec massa sapien. Consequat ac felis donec et odio. Sed velit dignissim sodales ut eu sem integer vitae justo.'
'\n\nInteger vitae justo eget magna fermentum iaculis. Lorem ipsum dolor sit amet consectetur adipiscing elit ut. Id porta nibh venenatis cras sed felis eget velit aliquet. Non sodales neque sodales ut etiam. Nunc faucibus a pellentesque sit amet porttitor. Ultricies tristique nulla aliquet enim tortor. Cursus metus aliquam eleifend mi. Arcu non odio euismod lacinia at quis. Sed lectus vestibulum mattis ullamcorper velit sed. Tortor aliquam nulla facilisi cras. Quam vulputate dignissim suspendisse in est ante in nibh mauris. Pretium nibh ipsum consequat nisl vel pretium lectus. Eget lorem dolor sed viverra. Neque ornare aenean euismod elementum nisi quis eleifend.'
'\n\nDiam vel quam elementum pulvinar etiam non quam lacus suspendisse. Dui vivamus arcu felis bibendum ut tristique et. Gravida neque convallis a cras semper. Nisl nunc mi ipsum faucibus vitae aliquet. Vitae justo eget magna fermentum. Odio morbi quis commodo odio aenean sed adipiscing. Est ullamcorper eget nulla facilisi etiam dignissim. Dictum sit amet justo donec enim diam vulputate ut pharetra. Consequat id porta nibh venenatis cras sed felis eget. Ut porttitor leo a diam. Ipsum dolor sit amet consectetur adipiscing elit duis tristique sollicitudin. Vulputate enim nulla aliquet porttitor lacus luctus accumsan tortor posuere. Sit amet consectetur adipiscing elit duis tristique sollicitudin nibh sit. Phasellus faucibus scelerisque eleifend donec.'
'\n\nAc feugiat sed lectus vestibulum mattis ullamcorper velit. Placerat duis ultricies lacus sed turpis tincidunt. Faucibus a pellentesque sit amet. Sagittis vitae et leo duis ut diam. Augue interdum velit euismod in pellentesque massa. At urna condimentum mattis pellentesque. Potenti nullam ac tortor vitae purus. Cursus mattis molestie a iaculis at erat pellentesque adipiscing. Tortor consequat id porta nibh venenatis cras. Sagittis nisl rhoncus mattis rhoncus urna. Elit eget gravida cum sociis natoque penatibus et. Vitae et leo duis ut diam quam. Eu turpis egestas pretium aenean pharetra. Morbi tincidunt ornare massa eget egestas purus. Eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque.'
'\n\nFaucibus ornare suspendisse sed nisi lacus sed viverra tellus. Dignissim diam quis enim lobortis scelerisque fermentum. Turpis tincidunt id aliquet risus. Quam adipiscing vitae proin sagittis nisl rhoncus mattis rhoncus urna. Dolor magna eget est lorem ipsum dolor. Nam aliquam sem et tortor consequat id porta nibh venenatis. At augue eget arcu dictum varius duis at consectetur. Felis eget velit aliquet sagittis id. At elementum eu facilisis sed odio. Habitant morbi tristique senectus et netus et malesuada fames ac. Vitae congue eu consequat ac felis donec et odio. Ipsum dolor sit amet consectetur adipiscing elit ut aliquam purus. Non arcu risus quis varius quam quisque id diam. Rhoncus urna neque viverra justo nec ultrices dui sapien eget. Accumsan in nisl nisi scelerisque eu ultrices vitae auctor.'
'\n\nSed adipiscing diam donec adipiscing tristique risus. Massa vitae tortor condimentum lacinia quis vel eros. Non enim praesent elementum facilisis leo vel fringilla est ullamcorper. Rhoncus dolor purus non enim praesent elementum. Praesent semper feugiat nibh sed pulvinar proin. Nisl condimentum id venenatis a condimentum vitae sapien pellentesque habitant. Sit amet dictum sit amet justo donec enim diam. Consectetur adipiscing elit pellentesque habitant morbi tristique senectus. Et netus et malesuada fames ac turpis. Viverra aliquet eget sit amet tellus cras adipiscing enim. Tristique senectus et netus et. Sed lectus vestibulum mattis ullamcorper velit sed.'
'\n\nIaculis urna id volutpat lacus. Imperdiet massa tincidunt nunc pulvinar sapien et. Posuere sollicitudin aliquam ultrices sagittis orci a. Eu volutpat odio facilisis mauris sit amet. Scelerisque eleifend donec pretium vulputate sapien nec sagittis aliquam. Sit amet nisl purus in mollis nunc sed id. Maecenas accumsan lacus vel facilisis volutpat est velit. Tortor at risus viverra adipiscing at in tellus. Arcu ac tortor dignissim convallis. Nisi scelerisque eu ultrices vitae auctor eu augue ut lectus. Consectetur adipiscing elit pellentesque habitant morbi tristique senectus. Quis lectus nulla at volutpat. Diam in arcu cursus euismod quis viverra nibh cras pulvinar. Libero id faucibus nisl tincidunt eget. Tellus in hac habitasse platea dictumst vestibulum rhoncus est pellentesque. Odio morbi quis commodo odio aenean sed. Vitae suscipit tellus mauris a diam maecenas. Non pulvinar neque laoreet suspendisse interdum consectetur. Libero nunc consequat interdum varius sit amet. Tincidunt id aliquet risus feugiat in ante.';
}
演示