簡體   English   中英

在 Flutter 中將很長的文本拆分為頁面

[英]Split very long text into pages in Flutter

我有多個包含很長文本的文本文件,我想將它們分成幾頁,以便於閱讀和導航。 這是一個例子:

文本拆分成頁面示例

因此,給定頁面的容器大小和作為輸入的文本(具有特定字體樣式),結果應該是顯示該文本所需的頁面總數

我知道文本可以像@pskink 提到的那樣顯示在 ListView 中,但我想像在 Kindle 中一樣制作頁面 static 並提前顯示總頁數,以便可以通過索引切換到任何頁面。

我還發現了一個相關的 Flutter 框架問題,它可能與這個問題有關。 不確定它是否有限制。

公開足夠的 LibTxt 以使自定義文本布局實用

我通過創建一個自定義小部件MultiPageText解決了這個問題,它根據可用空間將整個文本分布在多個頁面上。

在此處輸入圖像描述

由於布置多個段落只是為了確定每個頁面的截止點是昂貴的,因此通常將昂貴的_getPageText() function 放在單獨的隔離區中。 不幸的是,這目前是不可能的,因為非主隔離不能使用dart:ui ,因此 function 在主隔離中執行。

下面是完整的代碼,包括一些您可以在 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.';
}

演示

演示

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM