feat: update RichText render style

This commit is contained in:
Lucas.Xu 2022-07-28 11:41:39 +08:00
parent c5560caf3c
commit 985fe14a8b
9 changed files with 346 additions and 36 deletions

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6" y="6" width="4" height="4" rx="2" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 166 B

View File

@ -0,0 +1,3 @@
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="160" x="80" y="20" fill="#00BCF0"/>
</svg>

After

Width:  |  Height:  |  Size: 135 B

View File

@ -0,0 +1,207 @@
{
"document": {
"type": "editor",
"attributes": {},
"children": [
{
"type": "image",
"attributes": {
"image_src": "https://images.pexels.com/photos/2253275/pexels-photo-2253275.jpeg?cs=srgb&dl=pexels-helena-lopes-2253275.jpg&fm=jpg"
}
},
{
"type": "text",
"delta": [
{
"insert": "🌶 Read Me",
"attributes": {
"heading": "h1"
}
}
],
"attributes": {
"heading": "h1"
}
},
{
"type": "text",
"delta": [
{
"insert": "👋 Welcome to Appflowy",
"attributes": {
"heading": "h2"
}
}
],
"attributes": {
"heading": "h2"
}
},
{
"type": "text",
"delta": [
{
"insert": "Here are the basics:",
"attributes": {
"heading": "h3"
}
}
],
"attributes": {
"heading": "h3"
}
},
{
"type": "text",
"delta": [
{ "insert": "Click " },
{ "insert": "anywhere", "attributes": { "underline": true } },
{ "insert": " and just typing." }
],
"attributes": {
"list": "todo",
"todo": true
}
},
{
"type": "text",
"delta": [
{
"insert": "Hit"
},
{
"insert": " / ",
"attributes": { "highlightColor": "0xFFFFFF00" }
},
{
"insert": "to see all the types of content you can add - entity, headers, videos, sub pages, etc."
}
],
"attributes": {
"list": "todo",
"todo": true
}
},
{
"type": "text",
"delta": [
{
"insert": "Highlight any text, and use the menu that pops up to "
},
{ "insert": "style", "attributes": { "bold": true } },
{ "insert": " your ", "attributes": { "italic": true } },
{ "insert": "writing", "attributes": { "strikethrough": true } },
{ "insert": "." }
],
"attributes": {
"list": "todo",
"todo": true
}
},
{
"type": "text",
"delta": [
{
"insert": "Here are the examples:",
"attributes": {
"heading": "h3"
}
}
],
"attributes": {
"heading": "h3"
}
},
{
"type": "text",
"delta": [
{
"insert": "Hello world"
}
],
"attributes": {
"list": "bullet"
}
},
{
"type": "text",
"delta": [
{
"insert": "Hello world"
}
],
"attributes": {
"list": "bullet"
}
},
{
"type": "text",
"delta": [
{
"insert": "Hello world"
}
],
"attributes": {
"list": "bullet"
}
},
{
"type": "text",
"delta": [
{
"insert": "Hello world",
"attributes": { "quote": true }
}
],
"attributes": {
"quote": true
}
},
{
"type": "text",
"delta": [
{
"insert": "Hello world",
"attributes": { "quote": true }
}
],
"attributes": {
"quote": true
}
},
{
"type": "text",
"delta": [
{
"insert": "Hello world"
}
],
"attributes": {
"number": 1
}
},
{
"type": "text",
"delta": [
{
"insert": "Hello world"
}
],
"attributes": {
"number": 2
}
},
{
"type": "text",
"delta": [
{
"insert": "Hello world"
}
],
"attributes": {
"number": 3
}
}
]
}
}

View File

@ -112,14 +112,14 @@ class _MyHomePageState extends State<MyHomePage> {
if (page == 0) {
return _buildFlowyEditor();
} else if (page == 1) {
return _buildTextfield();
return _buildTextField();
}
return Container();
}
Widget _buildFlowyEditor() {
return FutureBuilder<String>(
future: rootBundle.loadString('assets/document.json'),
future: rootBundle.loadString('assets/example.json'),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
@ -167,7 +167,7 @@ class _MyHomePageState extends State<MyHomePage> {
);
}
Widget _buildTextfield() {
Widget _buildTextField() {
return const Center(
child: TextField(),
);

View File

@ -83,7 +83,10 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable {
Widget _build(BuildContext context) {
return Column(
children: [
Image.network(src),
Image.network(
src,
height: 150.0,
),
if (node.children.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -64,6 +64,7 @@ flutter:
# To add assets to your application, add an assets section, like this:
assets:
- document.json
- example.json
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see

View File

@ -4,24 +4,36 @@ import 'package:flutter_svg/svg.dart';
class FlowySvg extends StatelessWidget {
const FlowySvg({
Key? key,
required this.name,
required this.size,
this.name,
this.size = const Size(20, 20),
this.color,
this.number,
}) : super(key: key);
final String name;
final String? name;
final Size size;
final Color? color;
final int? number;
@override
Widget build(BuildContext context) {
return SizedBox.fromSize(
size: size,
child: SvgPicture.asset(
'assets/images/$name.svg',
color: color,
package: 'flowy_editor',
),
);
if (name != null) {
return SizedBox.fromSize(
size: size,
child: SvgPicture.asset(
'assets/images/$name.svg',
color: color,
package: 'flowy_editor',
),
);
} else if (number != null) {
final numberText =
'<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"><text x="30" y="150" fill="black" font-size="160">$number.</text></svg>';
return SizedBox.fromSize(
size: size,
child: SvgPicture.string(numberText),
);
}
return Container();
}
}

View File

@ -1,8 +1,19 @@
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/position.dart';
import 'package:flowy_editor/document/selection.dart';
import 'package:flowy_editor/document/text_delta.dart';
import 'package:flowy_editor/editor_state.dart';
import 'package:flowy_editor/document/path.dart';
import 'package:flowy_editor/operation/transaction_builder.dart';
import 'package:flowy_editor/render/node_widget_builder.dart';
import 'package:flowy_editor/render/render_plugins.dart';
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flowy_editor/infra/flowy_svg.dart';
import 'package:flowy_editor/extensions/object_extensions.dart';
import 'package:flowy_editor/render/selection/selectable.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flowy_editor/infra/flowy_svg.dart';
class RichTextNodeWidgetBuilder extends NodeWidgetBuilder {
RichTextNodeWidgetBuilder.create({
@ -56,8 +67,12 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
return _buildTodoListRichText(context);
} else if (attributes.list == 'bullet') {
return _buildBulletedListRichText(context);
} else if (attributes.quotes == true) {
} else if (attributes.quote == true) {
return _buildQuotedRichText(context);
} else if (attributes.heading != null) {
return _buildHeadingRichText(context);
} else if (attributes.number != null) {
return _buildNumberListRichText(context);
}
return _buildRichText(context);
}
@ -151,7 +166,11 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
}
Widget _buildSingleRichText(BuildContext context) {
return Expanded(child: RichText(key: _textKey, text: _textSpan));
return SizedBox(
width:
MediaQuery.of(context).size.width - 20, // FIXME: use the const value
child: RichText(key: _textKey, text: _textSpan),
);
}
Widget _buildTodoListRichText(BuildContext context) {
@ -161,9 +180,8 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
children: [
GestureDetector(
child: FlowySvg(
name: name,
key: _decorationKey,
size: const Size.square(20),
name: name,
),
onTap: () => TransactionBuilder(_editorState)
..updateNode(_textNode, {
@ -178,9 +196,25 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
Widget _buildBulletedListRichText(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(key: _decorationKey, Icons.circle),
FlowySvg(
key: _decorationKey,
name: 'point',
),
_buildRichText(context),
],
);
}
Widget _buildNumberListRichText(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FlowySvg(
key: _decorationKey,
number: _textNode.attributes.number,
),
_buildRichText(context),
],
);
@ -190,17 +224,32 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(key: _decorationKey, Icons.format_quote),
FlowySvg(
key: _decorationKey,
name: 'quote',
),
_buildRichText(context),
],
);
}
Widget _buildHeadingRichText(BuildContext context) {
// TODO: customize
return Column(
children: [
const Padding(padding: EdgeInsets.only(top: 5)),
_buildRichText(context),
const Padding(padding: EdgeInsets.only(top: 5)),
],
);
}
Rect frontWidgetRect() {
// FIXME: find a more elegant way to solve this situation.
if (_textNode.attributes.list != null) {
final renderBox =
_decorationKey.currentContext?.findRenderObject() as RenderBox;
final renderBox = _decorationKey.currentContext
?.findRenderObject()
?.unwrapOrNull<RenderBox>();
if (renderBox != null) {
return renderBox.localToGlobal(Offset.zero) & renderBox.size;
}
return Rect.zero;

View File

@ -8,11 +8,13 @@ class StyleKey {
static String underline = 'underline';
static String strikethrough = 'strikethrough';
static String color = 'color';
static String highlightColor = 'highlightColor';
static String font = 'font';
static String href = 'href';
static String heading = 'heading';
static String quotes = 'quotes';
static String quote = 'quote';
static String list = 'list';
static String number = 'number';
static String todo = 'todo';
static String code = 'code';
}
@ -45,6 +47,16 @@ extension AttributesExtensions on Attributes {
return null;
}
Color? get hightlightColor {
if (containsKey(StyleKey.highlightColor) &&
this[StyleKey.highlightColor] is String) {
return Color(
int.parse(this[StyleKey.highlightColor]),
);
}
return null;
}
String? get font {
// TODO: unspport now.
return null;
@ -64,9 +76,9 @@ extension AttributesExtensions on Attributes {
return null;
}
bool get quotes {
if (containsKey(StyleKey.quotes) && this[StyleKey.quotes] == true) {
return this[StyleKey.quotes];
bool get quote {
if (containsKey(StyleKey.quote) && this[StyleKey.quote] == true) {
return this[StyleKey.quote];
}
return false;
}
@ -78,6 +90,13 @@ extension AttributesExtensions on Attributes {
return null;
}
int? get number {
if (containsKey(StyleKey.number) && this[StyleKey.number] is int) {
return this[StyleKey.number];
}
return null;
}
bool get todo {
if (containsKey(StyleKey.todo) && this[StyleKey.todo] is bool) {
return this[StyleKey.todo];
@ -102,7 +121,7 @@ extension AttributesExtensions on Attributes {
///
/// Supported global rendering types:
/// heading: h1, h2, h3, h4, h5, h6,
/// block quotes,
/// block quote,
/// list: ordered list, bulleted list,
/// code block
///
@ -124,6 +143,7 @@ class RichTextStyle {
fontStyle: fontStyle,
fontSize: fontSize,
color: textColor,
backgroundColor: backgroundColor,
decoration: textDecoration,
),
recognizer: recognizer,
@ -131,8 +151,14 @@ class RichTextStyle {
}
// bold
FontWeight get fontWeight =>
attributes.bold ? FontWeight.bold : FontWeight.normal;
FontWeight get fontWeight {
if (attributes.bold) {
return FontWeight.bold;
} else if (attributes.heading != null) {
return FontWeight.bold;
}
return FontWeight.normal;
}
// underline or strikethrough
TextDecoration get textDecoration {
@ -152,19 +178,25 @@ class RichTextStyle {
Color get textColor {
if (attributes.href != null) {
return Colors.lightBlue;
} else if (attributes.quote) {
return Colors.grey;
}
return attributes.color ?? Colors.black;
}
Color get backgroundColor {
return attributes.hightlightColor ?? Colors.transparent;
}
// font size
double get fontSize {
final heading = attributes.heading;
if (heading != null) {
final headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
final fontSizes = [30.0, 28.0, 26.0, 24.0, 22.0, 20.0];
final fontSizes = [30.0, 25.0, 20.0, 20.0, 20.0, 20.0];
return fontSizes[headings.indexOf(heading)];
} else {
return 18.0;
return 16.0;
}
}