南阳网站建设制作价格,asp.net 网站开发项目,wordpress积分插件,装网要多少钱Flutter 代码如何实现一个带有富文本显示和交互的页面。 前置知识点学习
RealRichText
RealRichText 和 ImageSpan 不是 Flutter 框架中内置的组件#xff0c;而是自定义的组件或来自第三方库。这些组件的实现可以提供比标准 RichText 更丰富的功能#xff0c;比如在富文本…Flutter 代码如何实现一个带有富文本显示和交互的页面。 前置知识点学习
RealRichText
RealRichText 和 ImageSpan 不是 Flutter 框架中内置的组件而是自定义的组件或来自第三方库。这些组件的实现可以提供比标准 RichText 更丰富的功能比如在富文本中插入图片、处理点击事件等。由于这些组件在标准 Flutter 中不存在我将解释如何可能实现类似的功能。 TextSpan
TextSpan 是 Flutter 中用于构建富文本的基本组件之一。它允许你在同一个文本组件中混合和匹配不同的文本样式和手势识别功能。TextSpan 通常与 RichText 组件一起使用以显示复杂的文本布局。 TextSpan 的基本结构
TextSpan 是一个不可变的、递归的数据结构可以包含其他 TextSpan从而允许嵌套不同的文本样式。
TextSpan({TextStyle? style,String? text,ListInlineSpan? children,GestureRecognizer? recognizer,String? semanticsLabel,
})
关键属性
style:
指定文本的样式例如字体大小、颜色、粗细等。
类型为 TextStyle。
text:
要显示的文本字符串。
text 和 children 之间是互斥的如果需要在一个 TextSpan 中显示多个文本片段通常使用 children。
children:
一个 InlineSpan通常为 TextSpan的列表用于嵌套子文本。
允许创建复杂的文本结构如不同样式的文本段落。
recognizer:
用于处理文本的手势识别例如点击事件。
通常使用 TapGestureRecognizer 来处理点击事件。
semanticsLabel:
为文本提供一个可供屏幕阅读器使用的标签帮助无障碍访问。 使用示例
下面是一个简单的例子展示如何使用 TextSpan 来创建一个包含多种样式和交互的文本。
import package:flutter/gestures.dart;
import package:flutter/material.dart;class RichTextExample extends StatelessWidget {const RichTextExample({super.key});overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text(Rich Text Example)),body: Center(child: RichText(text: TextSpan(text: Hello ,style: const TextStyle(color: Colors.black, fontSize: 18),children: TextSpan[const TextSpan(text: bold, style: TextStyle(fontWeight: FontWeight.bold)),const TextSpan(text: and ,),const TextSpan(text: italic,style: TextStyle(fontStyle: FontStyle.italic),),TextSpan(text: clickable ,style: const TextStyle(color: Colors.blue),recognizer: TapGestureRecognizer()..onTap () {ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text(Clickable text clicked!)),);},),const TextSpan(text: text.,),],),),),);}
}ImageSpan
ImageSpan 是一种在富文本中嵌入图片的技术。在 Flutter 中虽然没有直接提供一个名为 ImageSpan 的组件但你可以通过使用 WidgetSpan 来实现类似的功能。WidgetSpan 允许你在 RichText 中插入任意的 Flutter 小部件包括图片。 使用 WidgetSpan 实现 ImageSpan 的功能
WidgetSpan 是 InlineSpan 的子类可以在 TextSpan 列表中使用来嵌入小部件。通过这种方式你可以在文本段落中插入图片或者其他小部件。 示例代码
import package:flutter/material.dart;class RichTextWithImageExample extends StatelessWidget {const RichTextWithImageExample({super.key});overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text(Rich Text with Image Example)),body: Center(child: RichText(text: TextSpan(style: const TextStyle(color: Colors.black, fontSize: 18),children: InlineSpan[const TextSpan(text: This is an example of ),WidgetSpan(child: Image.asset(static/demo.png,width: 24,height: 24,)),const TextSpan(text: in a rich text widget.),]),),),);}
}解释
RichText 和 TextSpan:
RichText 用于显示复杂的文本布局TextSpan 用于定义文本的样式和内容。
WidgetSpan:
WidgetSpan 可以嵌入任何小部件。在这个例子中它用于插入一个图片。
child 属性接受一个 Widget这里使用 Image.asset 来加载本地图片。
图片资源:
确保图片路径正确并在 pubspec.yaml 中声明图片资源。
例如
flutter:assets:- assets/demo.png 使用场景
图文混排: 当需要在同一个文本段落中展示图片和文本比如图标、表情符号或其他装饰性元素。
动态内容: 在文章或聊天应用中以富文本形式展示内容图片作为内嵌元素。
自定义格式: 创建带有图片的复杂格式文本比如新闻应用中的插图或注释。
通过 WidgetSpan你可以在 Flutter 的文本组件中灵活地插入图片和其他小部件实现更复杂的文本布局和丰富的用户界面体验。 TapGestureRecognizer
TapGestureRecognizer 是 Flutter 中用于检测点击手势的一个手势识别器。它通常用于处理文本中的点击事件特别是在 TextSpan 中使得文本中的某些部分可以响应用户的点击操作。 基本使用
TapGestureRecognizer 是 GestureRecognizer 的一个子类它专门用于处理点击tap事件。通常情况下它与 RichText 和 TextSpan 结合使用以实现文本的可点击功能。 如何使用 TapGestureRecognizer
以下是一个简单示例展示如何在 TextSpan 中使用 TapGestureRecognizer使得文本的一部分可以被点击并响应事件
import package:flutter/gestures.dart;
import package:flutter/material.dart;
class TapGestureExample extends StatelessWidget {const TapGestureExample({super.key});overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text(Tap Gesture Recognizer Example)),body: Center(child: RichText(text: TextSpan(text: Click ,style: const TextStyle(color: Colors.black, fontSize: 18),children: TextSpan[TextSpan(text: here,style: const TextStyle(color: Colors.blue, decoration: TextDecoration.underline),recognizer: TapGestureRecognizer()..onTap () {ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text(Text clicked!)),);},),const TextSpan(text: to see the effect.,),],),),),);}
} 解释
RichText 和 TextSpan:
RichText 用于显示富文本。通过 TextSpan你可以定义不同的文本样式和内容。
TapGestureRecognizer:
创建一个 TapGestureRecognizer 实例并将其分配给 TextSpan 的 recognizer 属性。
使用 ..onTap级联操作符来定义 onTap 回调函数当用户点击该文本时调用。
响应点击:
在 onTap 回调中使用 ScaffoldMessenger 显示一个 SnackBar以反馈用户的点击操作。 注意事项
释放资源:
如果在有状态的小部件中使用 TapGestureRecognizer请确保在 dispose 方法中调用 dispose 方法来释放手势识别器以避免内存泄漏。
多手势识别:
TapGestureRecognizer 可以与其他类型的手势识别器一起使用。使用 GestureDetector 可以创建更复杂的手势识别场景。
用户体验:
确保可点击的文本有视觉上的提示如颜色变化或下划线以便用户能够清楚地识别哪些文本是可交互的。
通过 TapGestureRecognizer你可以为文本添加交互功能使得应用的用户界面更加动态和响应用户操作。 富文本代码实现学习
import package:flutter/gestures.dart;
import package:flutter/material.dart;
import package:gsy_flutter_demo/widget/rich/real_rich_text.dart;class RichTextDemoPage extends StatefulWidget {const RichTextDemoPage({super.key});override_RichTextDemoState createState() _RichTextDemoState();
}class _RichTextDemoState extends StateRichTextDemoPage {overrideWidget build(BuildContext mainContext) {return Scaffold(appBar: AppBar(title: const Text(RichTextDemoPage),),body: Container(margin: const EdgeInsets.all(10),child: Builder(builder: (context) {return Center(child: RealRichText([TextSpan(text: A Text Link,style: const TextStyle(color: Colors.red, fontSize: 14),recognizer: TapGestureRecognizer()..onTap () {show(context, Link Clicked.);},),ImageSpan(const AssetImage(static/demo.png),imageWidth: 24,imageHeight: 24,),ImageSpan(const AssetImage(static/demo.png),imageWidth: 24,imageHeight: 24,margin: const EdgeInsets.symmetric(horizontal: 10)),const TextSpan(text: 哈哈哈,style: TextStyle(color: Colors.yellow, fontSize: 14),),TextSpan(text: Somebody,style: const TextStyle(color: Colors.black,fontSize: 14,fontWeight: FontWeight.bold),recognizer: TapGestureRecognizer()..onTap () {show(context, Link Clicked.);},),TextSpan(text: #RealRichText# ,style: const TextStyle(color: Colors.blue, fontSize: 14),recognizer: TapGestureRecognizer()..onTap () {show(context, Link Clicked.);},),const TextSpan(text: showing a bigger image,style: TextStyle(color: Colors.black, fontSize: 14),),ImageSpan(const AssetImage(static/demo.png),imageWidth: 24,imageHeight: 24,margin: const EdgeInsets.symmetric(horizontal: 5)),const TextSpan(text: and seems working perfect……,style: TextStyle(color: Colors.black, fontSize: 14),),]),);}),),);}show(context, text) {ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text),action: SnackBarAction(label: ACTION,onPressed: () {ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text(You pressed snackbar\s action.),));},),));}
}三方flutter库中的富文本实现源码
import dart:ui as ui show Image;import package:flutter/material.dart;
import package:flutter/rendering.dart;/// According to the related Flutter Issues(#2022) ,
/// Inline-Image-In-Text is a long-time(2 years) missing feature since RichText(or the underlying Paragraph) does only support pure text.
/// But we can solve this problem in a simple/tricky way:
///
/// 1. Regarde the images as a particular blank TextSpan,
/// convert images width and height to textspans letterSpacing and fontSize.
/// the origin paragraph will do the layout operation and leave the desired image space for us.
/// 2. Override the paint function
/// calculate the right offset via the getOffsetForCaret() api to draw the image over the space.
///
/// The only thing you have to do is converting your origin text to a TextSpan/ImageSpan List first.
///
/// {tool sample}
///
/// dart
/// RealRichText([
/// TextSpan(
/// text: showing a bigger image,
/// style: TextStyle(color: Colors.black, fontSize: 14),
/// ),
/// ImageSpan(
/// AssetImage(packages/real_rich_text/images/emoji_10.png),
/// width: 40,
/// height: 40,
/// ),
/// TextSpan(
/// text: and seems working perfect……,
/// style: TextStyle(color: Colors.black, fontSize: 14),
/// ),
/// ])
///
/// {end-tool}
///
class RealRichText extends Text {final ListTextSpan textSpanList;const RealRichText(this.textSpanList, {super.key,TextStyle? style,TextAlign textAlign TextAlign.start,TextDirection? textDirection,bool softWrap true,TextOverflow overflow TextOverflow.clip,TextScaler? textScaler,int? maxLines,Locale? locale,}) : super(,style: style,textAlign: textAlign,textDirection: textDirection,softWrap: softWrap,overflow: overflow,textScaler: textScaler,maxLines: maxLines,locale: locale);ListTextSpan extractAllNestedChildren(TextSpan textSpan) {if (textSpan.children null || textSpan.children!.isEmpty) {return [textSpan];}ListTextSpan childrenSpan [];for (var child in textSpan.children!) {childrenSpan.addAll(extractAllNestedChildren(child as TextSpan));}return childrenSpan;}overrideWidget build(BuildContext context) {final DefaultTextStyle defaultTextStyle DefaultTextStyle.of(context);TextStyle? effectiveTextStyle style;if (style null || style!.inherit) {effectiveTextStyle defaultTextStyle.style.merge(style);}if (MediaQuery.boldTextOf(context)) {effectiveTextStyle effectiveTextStyle!.merge(const TextStyle(fontWeight: FontWeight.bold));}TextSpan textSpan TextSpan(style: effectiveTextStyle,text: ,children: extractAllNestedChildren(TextSpan(style: effectiveTextStyle,text: ,children: textSpanList,)));// pass the context to ImageSpan to create a ImageConfigurationfor (var f in textSpan.children!) {if (f is ImageSpan) {f.updateImageConfiguration(context);}}Widget result _RichTextWrapper(textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,textDirection: textDirection,// RichText uses Directionality.of to obtain a default if this is null.locale: locale,// RichText uses Localizations.localeOf to obtain a default if this is nullsoftWrap: softWrap ?? defaultTextStyle.softWrap,overflow: overflow ?? defaultTextStyle.overflow,textScaler: textScaler ?? MediaQuery.textScalerOf(context),maxLines: maxLines ?? defaultTextStyle.maxLines,text: textSpan);if (semanticsLabel ! null) {result Semantics(textDirection: textDirection,label: semanticsLabel,child: ExcludeSemantics(child: result,));}return result;}
}/// Since flutter engine does not support inline-image for now, we have to support this feature via a tricky solution:
/// convert image to a particular TextSpan whose text always be \u200B(a zero-width-space).
/// set letterSpacing by the required image width
/// set fontSize by the required image height
class ImageSpan extends TextSpan {final double imageWidth;final double imageHeight;final EdgeInsets? margin;final ImageProvider imageProvider;final ImageResolver imageResolver;ImageSpan(this.imageProvider, {this.imageWidth 14.0,this.imageHeight 14.0,this.margin,super.recognizer,}) : imageResolver ImageResolver(imageProvider),super(style: TextStyle(color: Colors.transparent,letterSpacing:imageWidth (margin null ? 0 : margin.horizontal),height: 1,fontSize: (imageHeight / 1.15) (margin null ? 0 : margin.vertical)),text: \u200B,children: []);void updateImageConfiguration(BuildContext context) {imageResolver.updateImageConfiguration(context, imageWidth, imageHeight);}double get width imageWidth (margin null ? 0 : margin!.horizontal);double get height imageHeight (margin null ? 0 : margin!.vertical);
}typedef ImageResolverListener void Function(ImageInfo imageInfo, bool synchronousCall);class ImageResolver {final ImageProvider imageProvider;ImageStream? _imageStream;ImageConfiguration? _imageConfiguration;ui.Image? image;ImageResolverListener? _listener;ImageResolver(this.imageProvider);/// set the ImageConfiguration from outsidevoid updateImageConfiguration(BuildContext context, double width, double height) {_imageConfiguration createLocalImageConfiguration(context,size: Size(width, height),);}ImageStreamListener? imageStreamListener;void resolve(ImageResolverListener listener) {assert(_imageConfiguration ! null);final ImageStream? oldImageStream _imageStream;_imageStream imageProvider.resolve(_imageConfiguration!);assert(_imageStream ! null);_listener listener;if (_imageStream!.key ! oldImageStream?.key) {if (imageStreamListener ! null) {oldImageStream?.removeListener(imageStreamListener!);}imageStreamListener ?? ImageStreamListener(_handleImageChanged);_imageStream!.addListener(imageStreamListener!);}}void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {image imageInfo.image;_listener?.call(imageInfo, synchronousCall);}void addListening() {if (_listener ! null) {imageStreamListener ?? ImageStreamListener(_handleImageChanged);_imageStream?.addListener(imageStreamListener!);}}void stopListening() {if (imageStreamListener ! null) {_imageStream?.removeListener(imageStreamListener!);}}
}/// Just a subclass of RichText for overriding createRenderObject
/// to return a [_RealRichRenderParagraph] object
///
/// No more special purpose.
class _RichTextWrapper extends RichText {_RichTextWrapper({required TextSpan super.text,super.textAlign,super.textDirection,super.softWrap,super.overflow,super.textScaler,super.maxLines,super.locale,}) : assert(maxLines null || maxLines 0);overrideRenderParagraph createRenderObject(BuildContext context) {assert(textDirection ! null || debugCheckHasDirectionality(context));return _RealRichRenderParagraph(text as TextSpan,textAlign: textAlign,textDirection: textDirection ?? Directionality.of(context),softWrap: softWrap,overflow: overflow,textScaler: textScaler,maxLines: maxLines,locale: locale ?? Localizations.localeOf(context),);}
}/// paint the image on the top of those ImageSpans blank space
class _RealRichRenderParagraph extends RenderParagraph {_RealRichRenderParagraph(TextSpan super.text,{required super.textAlign,required super.textDirection,required super.softWrap,required super.overflow,required super.textScaler,super.maxLines,super.locale});overridevoid paint(PaintingContext context, Offset offset) {super.paint(context, offset);// Here it is!paintImageSpan(context, offset);}overridevoid attach(covariant Object owner) {super.attach(owner as PipelineOwner);for (var textSpan in (text as TextSpan).children!) {if (textSpan is ImageSpan) {textSpan.imageResolver.addListening();}}}overridevoid detach() {super.detach();for (var textSpan in (text as TextSpan).children!) {if (textSpan is ImageSpan) {textSpan.imageResolver.stopListening();}}}overridevoid performLayout() {super.performLayout();debugPrint(size $size);}/// this method draws inline-image over blank text space.void paintImageSpan(PaintingContext context, Offset offset) {final Canvas canvas context.canvas;final Rect bounds offset size;debugPrint(_RealRichRenderParagraph offset$offset bounds$bounds);canvas.save();int textOffset 0;for (TextSpan textSpanin (text as TextSpan).children as IterableTextSpan) {if (textSpan is ImageSpan) {// this is the top-center point of the ImageSpanOffset offsetForCaret getOffsetForCaret(TextPosition(offset: textOffset),bounds,);// found this is a overflowed image. ignore itif (textOffset ! 0 offsetForCaret.dx 0 offsetForCaret.dy 0) {return;}// this is the top-left point of the ImageSpan.// Usually, offsetForCaret indicates the top-center offset// except the first text which is always (0, 0)Offset topLeftOffset Offset(offset.dx offsetForCaret.dx -(textOffset 0 ? 0 : textSpan.width / 2),offset.dy offsetForCaret.dy);debugPrint(_RealRichRenderParagraph ImageSpan, textOffset $textOffset, offsetForCaret$offsetForCaret, topLeftOffset$topLeftOffset);// if image is not ready: wait for async ImageInfoif (textSpan.imageResolver.image null) {textSpan.imageResolver.resolve((imageInfo, synchronousCall) {if (synchronousCall) {paintImage(canvas: canvas,rect: topLeftOffset Size(textSpan.width, textSpan.height),image: textSpan.imageResolver.image!,fit: BoxFit.scaleDown,alignment: Alignment.center);} else {if (owner null || !owner!.debugDoingPaint) {markNeedsPaint();}}});textOffset textSpan.toPlainText().length;continue;}// else: just paint it. bottomCenter Alignment seems better...paintImage(canvas: canvas,rect: topLeftOffset Size(textSpan.width, textSpan.height),image: textSpan.imageResolver.image!,fit: BoxFit.scaleDown,alignment: Alignment.center);}textOffset textSpan.toPlainText().length;}canvas.restore();}
}