Flutter练习(四)— Flutter 渲染Markdown

4,661 阅读4分钟

前言

上一篇文章模仿掘金将文章列表渲染了出来,接下来是展示文章详情,在此我引用了flutter_markdown来实现markdown的渲染,在此遇到了一些问题,并将解决办法记录在文章里面,该项目已经放到github中,欢迎查看

主要实现步骤如下:

  • 引入flutter_markdown
  • 构造页面,渲染数据
    • 头像
    • 作者、等级以及创建时间
    • 文章详情渲染

引入flutter_markdown

pubspec.yaml文件引入

  flutter_markdown: 0.6.6

执行命令

flutter packages get

构造页面,渲染数据

头像

头像可以使用 CircleAvatar 处理头像

代码如下:

 // 头像
  Expanded(
    flex: 1,
    child: CircleAvatar(
      backgroundImage: NetworkImage(_articleDtlModel.avatarUrl),
      radius: 20,
    )),

作者、等级以及创建时间

这里主要是一些布局问题,还有引入插件dateFormat

  date_format: 2.0.4
            Expanded(
          flex: 5,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  Text(_articleDtlModel.author,
                      style: TextStyle(
                          fontSize: 12.0,
                          fontWeight: FontWeight.w300)),
                  // TODO  level
                ],
              ),
              Text(
                  formatDate(
                      _articleDtlModel.ctime, [yy,'-',mm,'-',dd,' ',HH,':',nn,':',ss]),
                  style: TextStyle(
                      fontSize: 12.0, fontWeight: FontWeight.w300))
            ],
          )),

文章详情渲染

这里遇到了两个问题:

  • Column 下面 不能直接用ListView
  • ListView与Markdown的嵌套使用

首先第一个问题github中提供了相关的解决办法链接,文中提到了,可以用Container或者Expanded包裹ListView处理

解决完这步的时候,我发现了代码依然报错,百度过后发现滚动类型的widget嵌套使用的时候会有以下问题,

A listview嵌套B listview,这里面有两个问题:
a. B listview不显示
b. A listview滑动监听被B listview窃取
解决问题1:要在B listview里面添加属性 shrinkWrap: true 即可
解决问题2:在B listview里面添加属性 physics: NeverScrollableScrollPhysics() 即可

参考文档:blog.csdn.net/qq_27981847…

这时候我看了下Markdown组件,果然它也是滚动类型组件。

Markdown组件源码:

/// A scrolling widget that parses and displays Markdown.
///
/// Supports all GitHub Flavored Markdown from the
/// [specification](https://github.github.com/gfm/).
///
/// See also:
///
///  * [MarkdownBody], which is a non-scrolling container of Markdown.
///  * <https://github.github.com/gfm/>
class Markdown extends MarkdownWidget {
  /// Creates a scrolling widget that parses and displays Markdown.
  const Markdown({
    Key? key,
    required String data,
    bool selectable = false,
    MarkdownStyleSheet? styleSheet,
    MarkdownStyleSheetBaseTheme? styleSheetTheme,
    SyntaxHighlighter? syntaxHighlighter,
    MarkdownTapLinkCallback? onTapLink,
    VoidCallback? onTapText,
    String? imageDirectory,
    List<md.BlockSyntax>? blockSyntaxes,
    List<md.InlineSyntax>? inlineSyntaxes,
    md.ExtensionSet? extensionSet,
    MarkdownImageBuilder? imageBuilder,
    MarkdownCheckboxBuilder? checkboxBuilder,
    MarkdownBulletBuilder? bulletBuilder,
    Map<String, MarkdownElementBuilder> builders =
        const <String, MarkdownElementBuilder>{},
    MarkdownListItemCrossAxisAlignment listItemCrossAxisAlignment =
        MarkdownListItemCrossAxisAlignment.baseline,
    this.padding = const EdgeInsets.all(16.0),
    this.controller,
    this.physics,
    this.shrinkWrap = false,
    bool softLineBreak = false,
  }) : super(
          key: key,
          data: data,
          selectable: selectable,
          styleSheet: styleSheet,
          styleSheetTheme: styleSheetTheme,
          syntaxHighlighter: syntaxHighlighter,
          onTapLink: onTapLink,
          onTapText: onTapText,
          imageDirectory: imageDirectory,
          blockSyntaxes: blockSyntaxes,
          inlineSyntaxes: inlineSyntaxes,
          extensionSet: extensionSet,
          imageBuilder: imageBuilder,
          checkboxBuilder: checkboxBuilder,
          builders: builders,
          listItemCrossAxisAlignment: listItemCrossAxisAlignment,
          bulletBuilder: bulletBuilder,
          softLineBreak: softLineBreak,
        );

最后文章详情渲染组件如下

                    // 文章详情渲染
            Expanded(
                child: ListView(
                  // physics: new NeverScrollableScrollPhysics(),
                  // shrinkWrap: true,
                  children: <Widget>[
                    Image.network(
                      _articleDtlModel.imgUrl,
                      width: 750,
                    ),
                    Markdown(
                        data: _articleDtlModel.content,
                        physics: new NeverScrollableScrollPhysics(), // 滚动失效
                        shrinkWrap: true  // markdown不显示
                    )
                  ],
                )),

整体代码如下

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_application/config/theme.dart';
import 'package:flutter_application/pages/common/bar/common_bar.dart';
import 'package:flutter_application/utils/common_utils.dart';
import 'package:flutter_markdown/flutter_markdown.dart';

class ArticleDetail extends StatefulWidget {
  // final ArticleDtlModel articleDtlModel;
  // ArticleDetail(this.articleDtlModel);

  _ArticleDetailState createState() => _ArticleDetailState();
}

class _ArticleDetailState extends State<ArticleDetail> {
  ArticleDtlModel _articleDtlModel = new ArticleDtlModel(ctime: DateTime.now());
  var tempArticleModel;
  var tempTags = [];

  Future<void> _loadData() async {
    await rootBundle
        .loadString("assets/json/article_detail.json")
        .then((value) => {tempArticleModel = json.decode(value)});
    _articleDtlModel = new ArticleDtlModel(
      avatarUrl: tempArticleModel['author_user_info']['avatar_large'],
      ctime: DateTime.parse(tempArticleModel['article_info']['ctime']),
      imgUrl: tempArticleModel['article_info']['cover_image'],
      author: tempArticleModel['author_user_info']['user_name'],
      content: tempArticleModel['article_info']['mark_content'],
      title: tempArticleModel['article_info']['title'],
      level: tempArticleModel['author_user_info']['level'],
      viewCount: tempArticleModel['article_info']['view_count'],
      commentCount: tempArticleModel['article_info']['comment_count'],
      followCount: tempArticleModel['author_user_info']['followee_count'],
      collectCount: tempArticleModel['article_info']['collect_count'],
      diggCount: tempArticleModel['article_info']['digg_count'],
      // tags: curTags
    );
    // 重构页面
    setState(() {
      _articleDtlModel = _articleDtlModel;
    });
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _loadData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: CommonBar(title: _articleDtlModel.title),
      body: Container(
        alignment: Alignment.topLeft,
        padding: EdgeInsets.fromLTRB(12, 6, 12, 12),
        color: secondBgColor,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Flex(
              direction: Axis.horizontal,
              children: [
                // 头像
                Expanded(
                    flex: 1,
                    child: CircleAvatar(
                      backgroundImage: NetworkImage(_articleDtlModel.avatarUrl),
                      radius: 20,
                    )),
                // 作者、等级以及创建时间
      Expanded(
          flex: 5,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  Text(_articleDtlModel.author,
                      style: TextStyle(
                          fontSize: 12.0,
                          fontWeight: FontWeight.w300)),
                  // TODO  level
                ],
              ),
              Text(
                  dateFormat(
                      _articleDtlModel.ctime, 'yyyy-mm-dd HH:mm:ss'),
                  style: TextStyle(
                      fontSize: 12.0, fontWeight: FontWeight.w300))
            ],
          )),
              ],
            ),
            // 文章详情渲染
            Expanded(
                child: ListView(
                  // physics: new NeverScrollableScrollPhysics(),
                  // shrinkWrap: true,
                  children: <Widget>[
                    Image.network(
                      _articleDtlModel.imgUrl,
                      width: 750,
                    ),
                    Markdown(
                        data: _articleDtlModel.content,
                        physics: new NeverScrollableScrollPhysics(),
                        shrinkWrap: true
                    )
                  ],
                )),
          ],
        ),
      ),
    );
  }
}

class ArticleDtlModel {
  final String avatarUrl;
  final String author;
  final String title;
  final String imgUrl;
  final int level;
  final DateTime ctime;
  final int viewCount;
  final int commentCount;
  final int followCount;
  final int collectCount;
  final int diggCount;
  final String content;
  final List<Map> tags;

  ArticleDtlModel(
      {this.avatarUrl = '',
      this.author = '',
      this.title = '',
      this.imgUrl = '',
      this.level = 1,
      required this.ctime,
      this.viewCount = 0,
      this.commentCount = 0,
      this.followCount = 0,
      this.collectCount = 0,
      this.diggCount = 0,
      this.content = '',
      this.tags = const []});
}

最后文章详情渲染组件如下

                    // 文章详情渲染
            Expanded(
                child: ListView(
                  // physics: new NeverScrollableScrollPhysics(),
                  // shrinkWrap: true,
                  children: <Widget>[
                    Image.network(
                      _articleDtlModel.imgUrl,
                      width: 750,
                    ),
                    Markdown(
                        data: _articleDtlModel.content,
                        physics: new NeverScrollableScrollPhysics(), // 滚动失效
                        shrinkWrap: true  // markdown不显示
                    )
                  ],
                )),

整体代码如下

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_application/config/theme.dart';
import 'package:flutter_application/pages/common/bar/common_bar.dart';
import 'package:flutter_application/utils/common_utils.dart';
import 'package:flutter_markdown/flutter_markdown.dart';

class ArticleDetail extends StatefulWidget {
  // final ArticleDtlModel articleDtlModel;
  // ArticleDetail(this.articleDtlModel);

  _ArticleDetailState createState() => _ArticleDetailState();
}

class _ArticleDetailState extends State<ArticleDetail> {
  ArticleDtlModel _articleDtlModel = new ArticleDtlModel(ctime: DateTime.now());
  var tempArticleModel;
  var tempTags = [];

  Future<void> _loadData() async {
    await rootBundle
        .loadString("assets/json/article_detail.json")
        .then((value) => {tempArticleModel = json.decode(value)});
    _articleDtlModel = new ArticleDtlModel(
      avatarUrl: tempArticleModel['author_user_info']['avatar_large'],
      ctime: DateTime.parse(tempArticleModel['article_info']['ctime']),
      imgUrl: tempArticleModel['article_info']['cover_image'],
      author: tempArticleModel['author_user_info']['user_name'],
      content: tempArticleModel['article_info']['mark_content'],
      title: tempArticleModel['article_info']['title'],
      level: tempArticleModel['author_user_info']['level'],
      viewCount: tempArticleModel['article_info']['view_count'],
      commentCount: tempArticleModel['article_info']['comment_count'],
      followCount: tempArticleModel['author_user_info']['followee_count'],
      collectCount: tempArticleModel['article_info']['collect_count'],
      diggCount: tempArticleModel['article_info']['digg_count'],
      // tags: curTags
    );
    // 重构页面
    setState(() {
      _articleDtlModel = _articleDtlModel;
    });
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _loadData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: CommonBar(title: _articleDtlModel.title),
      body: Container(
        alignment: Alignment.topLeft,
        padding: EdgeInsets.fromLTRB(12, 6, 12, 12),
        color: secondBgColor,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Flex(
              direction: Axis.horizontal,
              children: [
                // 头像
                Expanded(
                    flex: 1,
                    child: CircleAvatar(
                      backgroundImage: NetworkImage(_articleDtlModel.avatarUrl),
                      radius: 20,
                    )),
                // 作者、等级以及创建时间
      Expanded(
          flex: 5,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  Text(_articleDtlModel.author,
                      style: TextStyle(
                          fontSize: 12.0,
                          fontWeight: FontWeight.w300)),
                  // TODO  level
                ],
              ),
              Text(
                  dateFormat(
                      _articleDtlModel.ctime, 'yyyy-mm-dd HH:mm:ss'),
                  style: TextStyle(
                      fontSize: 12.0, fontWeight: FontWeight.w300))
            ],
          )),
              ],
            ),
            // 文章详情渲染
            Expanded(
                child: ListView(
                  // physics: new NeverScrollableScrollPhysics(),
                  // shrinkWrap: true,
                  children: <Widget>[
                    Image.network(
                      _articleDtlModel.imgUrl,
                      width: 750,
                    ),
                    Markdown(
                        data: _articleDtlModel.content,
                        physics: new NeverScrollableScrollPhysics(),
                        shrinkWrap: true
                    )
                  ],
                )),
          ],
        ),
      ),
    );
  }
}

class ArticleDtlModel {
  final String avatarUrl;
  final String author;
  final String title;
  final String imgUrl;
  final int level;
  final DateTime ctime;
  final int viewCount;
  final int commentCount;
  final int followCount;
  final int collectCount;
  final int diggCount;
  final String content;
  final List<Map> tags;

  ArticleDtlModel(
      {this.avatarUrl = '',
      this.author = '',
      this.title = '',
      this.imgUrl = '',
      this.level = 1,
      required this.ctime,
      this.viewCount = 0,
      this.commentCount = 0,
      this.followCount = 0,
      this.collectCount = 0,
      this.diggCount = 0,
      this.content = '',
      this.tags = const []});
}

效果图

image.png

该项目已经放到github中,欢迎查看