Flutter文本处理插件flutter_sanity_portable_text的使用

发布于 1周前 作者 gougou168 来自 Flutter

Flutter文本处理插件 flutter_sanity_portable_text 的使用

简介

flutter_sanity_portable_text 是一个用于渲染 Sanity.io 的 Portable Text 格式的 Flutter 插件。它支持多种样式和标记,并且可以高度定制。

示例截图

特性

  • 完整的 Portable Text 支持:渲染所有标准样式和标记
  • 块渲染:支持多个不同样式的块
  • 自定义
    • 自定义块和块容器
    • 自定义样式和标记,包括复杂的注释
    • 自定义所有默认样式、块和容器
  • 开发者体验
    • 显示未注册的块、标记和样式的内联错误
    • 调试时显示有用的错误信息
    • 类型安全的 API

安装

在你的 pubspec.yaml 文件中添加以下依赖:

dependencies:
  flutter_sanity_portable_text: ^1.0.0

使用示例

使用简单的 TextBlockItem

import 'package:flutter/material.dart';
import 'package:flutter_sanity_portable_text/flutter_sanity_portable_text.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: Scaffold(
        body: SafeArea(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: PortableText(
              blocks: [
                TextBlockItem(
                  children: [
                    Span(
                      text: 'Sanity Portable Text',
                    ),
                  ],
                  style: 'h1',
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

直接从 JSON 渲染

final text = PortableText(
  blocks: [
    TextBlockItem.fromJson(jsonDecode('''
{
  "_type": "block",
  "style": "h3",
  "children": [
    {
      "_type": "span",
      "text": "Rendered in "
    },
    {
      "_type": "span",
      "text": "Flutter",
      "marks": ["em", "strong", "underline"]
    }
  ]
}
''')),
  ],
);

使用多个块和不同的样式

final text = PortableText(
  blocks: [
    TextBlockItem(
      children: [
        Span(
          text: 'Sanity Portable Text',
        ),
      ],
      style: 'h1',
    ),
    // 引用块
    TextBlockItem(
      children: [
        Span(
          text: '"The best way to predict the future is to invent it."',
        ),
        Span(
          text: '\n- Steve Jobs',
        ),
      ],
      style: 'blockquote',
    ),
    TextBlockItem(
      children: [
        Span(
          text: 'Supports all standard marks and styles, including support for:',
        ),
      ],
    ),
    _listItem('Bulleted text', ListItemType.bullet),
    _listItem('Numbered text', ListItemType.number),
    _listItem('Square bullet text', ListItemType.square),
    TextBlockItem(
      children: [
        Span(text: 'Strong text, ', marks: ['strong']),
        Span(text: 'Emphasized text, ', marks: ['em']),
        Span(text: 'Underlined text, ', marks: ['underline']),
        Span(text: 'Strike through text, ', marks: ['strike-through']),
        Span(text: 'All combined', marks: ['strong', 'em', 'underline', 'strike-through']),
        Span(text: '.'),
      ],
    ),
    for (final index in [1, 2, 3, 4, 5, 6])
      _textBlock('H$index', style: 'h$index'),
  ],
);

TextBlockItem _textBlock(String text, {String? style}) {
  return TextBlockItem(
    children: [
      Span(text: text),
    ],
    style: style ?? 'normal',
  );
}

TextBlockItem _listItem(String text, ListItemType type) {
  return TextBlockItem(
    children: [
      Span(text: text),
    ],
    listItem: type,
  );
}

使用自定义块

import 'package:flutter/material.dart';
import 'package:flutter_sanity_portable_text/flutter_sanity_portable_text.dart';

// 自定义块项
final class CustomBlockItem implements PortableBlockItem {
  CustomBlockItem({
    required this.text,
    required this.foregroundColor,
    required this.backgroundColor,
  });

  @override
  String get blockType => 'custom';

  final String text;
  final Color foregroundColor;
  final Color backgroundColor;
}

void main() {
  // 注册自定义块
  PortableTextConfig.shared.blocks['custom'] = (context, item) {
    final theme = Theme.of(context);
    final custom = item as CustomBlockItem;
    final style =
        theme.textTheme.bodyMedium?.apply(color: custom.foregroundColor);

    return Container(
      decoration: BoxDecoration(
          color: custom.backgroundColor,
          borderRadius: BorderRadius.circular(8),
          border: Border.all(color: Colors.blueAccent, width: 2),
          boxShadow: const [
            BoxShadow(
              offset: Offset(0, 4),
              blurRadius: 2,
              color: Colors.black26,
            )
          ]),
      margin: const EdgeInsets.only(bottom: 8),
      padding: const EdgeInsets.all(8),
      child: Text(custom.text, style: style),
    );
  };

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: Scaffold(
        body: SafeArea(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: PortableText(
              blocks: [
                TextBlockItem(
                  children: [
                    Span(
                      text: 'Sanity Portable Text',
                    ),
                  ],
                  style: 'h1',
                ),
                CustomBlockItem(
                    text: 'We can also do custom blocks!',
                    foregroundColor: Colors.white,
                    backgroundColor: Colors.primaries[4]),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

使用未注册的块会显示错误

import 'package:flutter/material.dart';
import 'package:flutter_sanity_portable_text/flutter_sanity_portable_text.dart';

// 未注册的块项
final class UnregisteredBlockItem implements PortableBlockItem {
  @override
  String get blockType => 'unregistered';
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: Scaffold(
        body: SafeArea(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: PortableText(
              blocks: [
                TextBlockItem(
                  children: [
                    Span(
                      text: 'Sanity Portable Text',
                    ),
                  ],
                  style: 'h1',
                ),
                // 这个会显示错误
                UnregisteredBlockItem(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

使用自定义标记

final class CustomMarkDef implements MarkDef {
  CustomMarkDef({
    required this.color,
    required this.key,
  });

  @override
  final String key;

  final Color color;

  @override
  String get type => 'custom-mark';
}

void main() {
  // 注册自定义标记
  _registerCustomMark();

  runApp(const MyApp());
}

void _registerCustomMark() {
  PortableTextConfig.shared.markDefs['custom-mark'] = MarkDefDescriptor(
    schemaType: 'custom-mark',
    styleBuilder: (context, markDef, textStyle) {
      final mark = markDef as CustomMarkDef;

      final style = textStyle.apply(
        decoration: TextDecoration.underline,
        decorationColor: mark.color,
      );

      return style;
    },
    fromJson: (json) => CustomMarkDef(color: json['color'], key: json['key']),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: Scaffold(
        body: SafeArea(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: PortableText(
              blocks: [
                TextBlockItem(
                  children: [
                    Span(
                      text: 'Sanity Portable Text',
                    ),
                  ],
                  style: 'h1',
                ),
                TextBlockItem(
                  children: [
                    Span(
                      text: 'We can also do ',
                    ),
                    Span(text: 'custom marks!', marks: ['custom-key']),
                  ],
                  markDefs: [
                    CustomMarkDef(color: Colors.red, key: 'custom-key'),
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

当自定义标记未注册时显示错误

final class UnregisteredMarkDef implements MarkDef {
  UnregisteredMarkDef({
    required this.key,
  });

  @override
  final String key;

  @override
  String get type => 'unregistered-mark';
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: Scaffold(
        body: SafeArea(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: PortableText(
              blocks: [
                TextBlockItem(
                  children: [
                    Span(
                      text: 'Sanity Portable Text',
                    ),
                  ],
                  style: 'h1',
                ),
                TextBlockItem(
                  children: [
                    Span(
                        text: ' and report when a custom mark is not registered, such as:'),
                    Span(text: ' this.', marks: ['missing-key']),
                  ],
                  markDefs: [
                    UnregisteredMarkDef(key: 'missing-key')
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

使用自定义列表构建器来渲染 PortableText

Widget columnBuilder(BuildContext context, List<PortableBlockItem> blocks) {
  return Column(
    children: blocks
        .map((block) => PortableTextConfig.shared.buildBlock(context, block))
        .toList(),
  );
}

final text = PortableText(blocks: [...], listBuilder: columnBuilder);

更多探索

还有其他一些特性没有在上面的例子中展示,例如:

  • 自定义块样式
  • 自定义块容器
  • PortableText 小部件内的项目填充
  • 列表项缩进
  • 更改基本样式
  • 更改默认块和标记样式

你可以查看 PortableConfig 的属性以获得更多自定义机会。

贡献

欢迎贡献!请随意提交 Pull Request。对于重大更改,请先打开一个问题讨论你想要更改的内容。

学习更多

许可证

该项目根据 LICENSE 文件中指定的条款进行许可。


更多关于Flutter文本处理插件flutter_sanity_portable_text的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter文本处理插件flutter_sanity_portable_text的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何在Flutter项目中使用flutter_sanity_portable_text插件来处理文本的示例代码。这个插件通常用于从Sanity CMS(一个headless CMS)获取Portable Text格式的内容,并在Flutter应用中渲染它。

首先,你需要在你的pubspec.yaml文件中添加依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_sanity_portable_text: ^最新版本号  # 请替换为当前最新版本号

然后运行flutter pub get来安装依赖。

接下来是一个完整的示例代码,展示如何使用flutter_sanity_portable_text插件来渲染Portable Text内容:

import 'package:flutter/material.dart';
import 'package:flutter_sanity_portable_text/flutter_sanity_portable_text.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FlutterSanityPortableText Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // 假设这是从Sanity CMS获取的Portable Text内容
  final String portableText = `
{
  "_type": "block",
  "children": [
    {
      "_type": "heading1",
      "children": [
        {
          "_type": "span",
          "text": "Hello, World!",
          "marks": []
        }
      ]
    },
    {
      "_type": "paragraph",
      "children": [
        {
          "_type": "span",
          "text": "This is a paragraph with some ",
          "marks": []
        },
        {
          "_type": "span",
          "text": "bold",
          "marks": [
            {
              "_type": "strong"
            }
          ]
        },
        {
          "_type": "span",
          "text": " and ",
          "marks": []
        },
        {
          "_type": "span",
          "text": "italic",
          "marks": [
            {
              "_type": "em"
            }
          ]
        },
        {
          "_type": "span",
          "text": " text.",
          "marks": []
        }
      ]
    }
  ],
  "markDefs": [],
  "style": "normal"
}
  `;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('FlutterSanityPortableText Demo'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: PortableTextWidget(
          blocks: portableText,  // 这里传入Portable Text内容
          serializers: {
            'type': 'default',  // 你可以根据需要配置不同的序列化器
            'serializers': DefaultSerializers(),
          },
          styleOverrides: {},  // 你可以在这里覆盖默认样式
        ),
      ),
    );
  }
}

在这个示例中,我们创建了一个简单的Flutter应用,它包含一个从Sanity CMS获取的Portable Text内容,并使用PortableTextWidget来渲染它。portableText变量包含了一个示例Portable Text JSON字符串。

请注意,serializers参数需要配置为适当的序列化器,这里使用了DefaultSerializers()作为示例。在实际应用中,你可能需要根据你的内容结构和样式需求进行配置。

此外,styleOverrides参数允许你覆盖默认样式,如果你需要自定义渲染样式,可以在这里进行配置。

这个示例代码展示了基本的用法,你可以根据实际需求进一步扩展和自定义。

回到顶部