整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Flutter中富文件标签的解决方案

Flutter中富文件标签的解决方案


—— 执剑天涯,从你的点滴积累开始,所及之处,必精益求精。


在实际业务开发中,时常会有这种一段Html格式的标签,看下图的情况 :


在 Flutter 中,有点发愁,因为 Flutter 提供的 Text 与 RichText 还解析不了这种格式的,但是你也不能使用 WebView 插件,如果使用了,你会在每一个Item中嵌入一个浏览器内核,再强的手机,也会卡,当然肯定不能这样做,因为这样就是错误的做法。

小编经过大量的尝试与思考,终于写出来了一个插件可以来解析了,现分享给大家。

1 基本使用实现

1.2 添加依赖

小编依旧,来个pub方式:

dependencies:
  flutter_html_rich_text: ^1.0.0

1.3 加载解析 HTML 片段标签

核心方法如下:

///htmlText 就是你的 HTML 片段了
 HtmlRichText(
  htmlText: txt,
 ),

如下代码清单 1-3-1 就是上述图中的效果:

/// 代码清单 1-3-1
class TestHtmlPage extends StatefulWidget {
  @override
  _TestPageState createState()=> _TestPageState();
}

class _TestPageState extends State<TestHtmlPage> {

  String txt="<p>长途轮 <h4>高速驱动</h4><span style='background-color:#ff3333'>"
      "<span style='color:#ffffff;padding:10px'> 3条立减 购胎抽奖</span></span></p>"
      "<p>长途高速驱动轮<span ><span style='color:#cc00ff;'> 3条立减 购胎抽奖</span></span></p>";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ///一个标题
      appBar: AppBar(title: Text('A页面'),),
      body: Center(
        ///一个列表
        child: ListView.builder(
          itemBuilder: (BuildContext context, int postiont) {
            return buildItemWidget(postiont);
          },
          itemCount: 100,
        ),
      ),
    );
  }

  ///ListView的条目
  Widget buildItemWidget(int postiont) {
    return Container(
      ///内容边距
      padding: EdgeInsets.all(8),
      child: Column(
        ///子Widget左对齐
        crossAxisAlignment: CrossAxisAlignment.start,

        ///内容包裹
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            "测试标题 $postiont",
            style: TextStyle(fontWeight: FontWeight.w500),
          ),

          ///html富文本标签
          Container(
            margin: EdgeInsets.only(top: 8),
            child: HtmlRichText(
              htmlText: txt,
            ),
          )
        ],
      ),
    );
  }
}

以下是解析思考 烧脑的实践


2 烧脑思考实践一

Flutter 应用程序被 Android iOS平台加载,在原生 Android 中,使用TextView就可轻松实现解析(如下代码清单2-1),当然在iOS中使用UILabel也可轻松实现(如下代码清单2-2)。

// Android 原生 TextView加载Html的核心方法
//代码清单2-1
// MxgsaTagHandler 定义的一个 TagHandler 用来处理点击事件
lTextView.setText(Html.fromHtml(myContent, null, new MxgsaTagHandler(context)));
lTextView.setClickable(true);
lTextView.setMovementMethod(LinkMovementMethod.getInstance());

iOS UILable

// iOS 原生 UILabel加载Html的核心方法
//代码清单2-2
//返回的HTML文本 如 <font color='red'></font>
NSString *str=@"htmlText";
NSString *HTMLString=[NSString stringWithFormat:@"<html><body>%@</body></html>", str ];


NSDictionary *options=@{NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType,
                          NSCharacterEncodingDocumentAttribute : @(NSUTF8StringEncoding)
                          };
NSData *data=[HTMLString dataUsingEncoding:NSUTF8StringEncoding];

NSMutableAttributedString * attributedString=[[NSMutableAttributedString alloc] initWithData:data options:options documentAttributes:nil error:nil];

NSMutableParagraphStyle *paragraphStyle=[[NSMutableParagraphStyle alloc] init];   // 调整行间距
paragraphStyle.lineSpacing=8.0;
paragraphStyle.alignment=NSTextAlignmentJustified;
[attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, attributedString.length)];

[attributedString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:15] range:NSMakeRange(0, attributedString.length)];


_uiLabel.backgroundColor=[UIColor cyanColor];
_uiLabel.numberOfLines=0;
_uiLabel.attributedText=attributedString;
[_uiLabel sizeToFit];

然后对于 Flutter 来讲是可以顺利的加载原生 View的 ,如下代码清单 2-3所示就是在Flutter中通过 AndroidView 与 UiKitView来实现。

  //Flutter中加载原生View核心方法
  //代码清单2-3
  buildAndroidView() {
    return AndroidView(
      //设置标识
      viewType: "com.studyon./text_html",
      //参数的编码方式
      creationParamsCodec: const StandardMessageCodec(),
    );
  }

  /// 通过 UiKitView 来加载 iOS原生View
  buildUIKitView() {
    return UiKitView(
      //标识
      viewType: "com.studyon./text_html",
      //参数的编码方式
      creationParamsCodec: const StandardMessageCodec(),
    );
  }



于是小编开发了第一波操作,开发了这样的一个插件来调用原生 View 实现渲染富文本标签,这个插件使用方式很简单,如下所示:

HTMLTextWidet(
  htmlText: "测试一下",
 )

这一步操作真是所谓的骚操作,其实小编在开发前就觉得不太合适,不过以小编的个性,非得尝试验证一下,现结果出来了,就是在加载时,由于应用在列表中,使用 HTMLTextWidet 会有短暂的黑屏效果,而且内存出吃不消,如下图所示:


为什么会黑屏,闲鱼技术团队有过论述在《Flutter中嵌入Native组件的正确姿势》 以及 文章 《深入了解Flutter界面开发中有详细论述》 。

所以结果是 :不可行。


3 烧脑思考实践二

用 Java 的思想来解析 String 的方式来处理 HTML 字符串,处理成小片段,然后使用Text结合 流式布局 Wrap 来组合,核心代码如下清单 3-1 所示为解析:

/*
   解析标签
   */
List<TagColorModel> findBackGroundColor(String htmlStr) {
  List<TagColorModel> tagColorModelList=[];
  List<String> colorSpiltList=[];
  String driverAdvertisement=htmlStr;
  if (driverAdvertisement !=null) {

    colorSpiltList=driverAdvertisement.split("background-color");

    for (var i=0; i < colorSpiltList.length; i++) {
      TagColorModel itemColorModel=TagColorModel();
      String colorsStr=colorSpiltList[i];
      List<String> itemSpiltList=colorsStr.split(":#");
      for (var j=0; j < itemSpiltList.length; ++j) {
        String item=itemSpiltList[j];
        String itemColor="";
        String itemText="";
        try {
          if (item.length >=6) {
            itemColor=item.toString().substring(0, 6);
            if (itemColor.trim().toUpperCase()=="FFFFFF") {
              itemColorModel.backGroundColor=ColorUtils.getRandomColor();
            } else {
              itemColorModel.backGroundColor=new Color(
                int.parse(itemColor.trim(), radix: 16) + 0xFF000000);
            }
            int startIndex=item.indexOf("\">");
            int endIndex=item.indexOf("</");
            if (startIndex !=-1 && endIndex >=startIndex) {
              LogUtil.e("startIndex  $startIndex  endIndex $endIndex ");
              itemText=item.substring(startIndex + 2, endIndex);
              LogUtil.e("itemColor  $itemColor  itemText $itemText ");
              itemColorModel.text=itemText;
              tagColorModelList.add(itemColorModel);
            }
          }
        } catch (e) {
          ///解析异常的 不必处理
        }
      }
    }
  }
  LogUtil.e("${tagColorModelList.length} \n\n ");
  return tagColorModelList;
}

然后 TagColorModel 的定义如下代码清单 3-2所示:

///代码清单 3-2 
class TagColorModel {
  ///背景
  Color backGroundColor;
 ///文本颜色
  Color textColor;
 ///文本
  String text;

  TagColorModel(
      {this.text="",
      this.backGroundColor=Colors.transparent,
      this.textColor=Colors.white});
}

然后就是使用 Wrap 来使用解析的内容,如下代码清单3-3所示:

///代码清单 3-3
///获取背景颜色
  List<TagColorModel> colorList=findBackGroundColor(htmlStr);

  List<Widget> tagList=[];

  for (var i=0; i < colorList.length; ++i) {
    TagColorModel model=colorList[i];

    tagList.add(Container(
      margin: EdgeInsets.only(right: 2, left: 4, top: 4),
      padding: EdgeInsets.only(left: 6, right: 6),
      decoration: BoxDecoration(
        color: model.backGroundColor,
        borderRadius: BorderRadius.all(Radius.circular(2)),
      ),
      child: Text(
        "${model.text}",
        style: TextStyle(fontSize: 12, color: model.textColor),
      ),
    ));
  }

  ///然后再使用 Wrap 包裹
  Wrap(
	alignment: WrapAlignment.spaceBetween,
 	 children: tagList,
  ),

实践结果:可行,但是有兼容性差,效率低。

当然闲鱼团队在文章《如何低成本实现Flutter富文本,看这一篇就够了!》 中也有详细论述过,与上述的思路差不多。

4 烧脑思考实践三

当在Flutter中 Dart 从网站中提取数据时,html依赖库是一个不错的选择,html 是一个开源的 Dart 包,主要用于从 HTML 中提取数据,从中获取节点的属性、文本和 HTML以及各种节点的内容。


dependencies:
  html: ^0.14.0+3

于是乎小编也开始尝试,首先是使用 Html 库解析 HTML文本块,将解析的 Document 通过递归方式遍历出来所有的 node 节点,如下代码清单4-1所示:

代码清单4-1
import 'package:html/parser.dart' as parser;
import 'package:html/dom.dart' as dom;

List<Widget> parse(String originHtmlString) {
  // 空格替换 去除所有 br 标签用 \n 代替,
  originHtmlString=originHtmlString.replaceAll('<br/>', '\n');
  originHtmlString=originHtmlString.replaceAll('<br>', '\n');
  originHtmlString=originHtmlString.replaceAll('<br />', '\n');

  ///html 依赖库解析
  dom.Document document=parser.parse(originHtmlString);
  ///获取 DOM 中的 node 节点
  dom.Node cloneNode=document.body.clone(true);

 // 注意: 先序遍历找到所有关键节点(由于是引用传值,所以需要重新获取一遍 hashCode)
  List<dom.Node> keyNodeList=new List<dom.Node>();
  int nodeIndex=0;
  ///递归遍历
  parseNodesTree(cloneNode, callBack: (dom.Node childNode) {
    if (childNode is dom.Element &&
        truncateTagList.indexOf(childNode.localName) !=-1) {
      print('TEST: truncate tag nodeIndex=${nodeIndex++}');
      keyNodeList.add(childNode);
      // 注意: 对于占据整行的图片也作为关键节点处理
    } else if (childNode is dom.Element &&
        childNode.localName=='img' &&
        checkImageNeedNewLine(childNode)) {
      print('TEST: one line image nodeIndex=${nodeIndex++}');
      keyNodeList.add(childNode);
    }
  });

}
///递归遍历
void parseNodesTree(dom.Node node,
    {NodeTreeCallBack callBack=printNodeName}) {
  ///遍历 Node 节点
  for (var i=0; i < node.nodes.length; ++i) {
    dom.Node item=node.nodes[i];
    callBack(item);
    parseNodesTree(item, callBack: callBack);
  }
}

然后就是将 得出的 node 节点 与 Flutter 组件映射,文本使用 TextSpan ,图片使用 Image ,然后将 样式使用 TextStyle 映射,然后最后将解析的结果组件使用 Wrap 来包裹,就达到了现在的插件 flutter_html_rich_text 。


综合实现思路就是 使用 HTML 库完善了【烧脑思考实践二】中的解析。

解析篇幅较长,大家有兴趣可以看下 github 源码。



完毕

者博客

http://www.jianshu.com/u/0fa6f5d09040

文章目录

  • 前言

  • 场景

  • 实现方式

  • drawable属性

  • Spannable使用

  • HTML显示

  • 总结


0

前言

在使用TextView的时候,我们经常需要在TextView中进行图文混排,比如在QQ中聊天的消息中的表情,底部tab图标等。

1

场景

2

实现方式

Android官方对TextView的图文混排提供了支持,我们可以从以下三种方式实现TextView的图文混排:

1.在TextView中使用Compound Drawable属性;

2.在TextView中使用Spannable多样式显示;

3.在TextView中显示HTML文本。

3

drawable属性

在TextView中使用Compound Drawable属性可以在文字的上下左右放置drawable,效果如下:

一共有两种方式可以实现:XML布局设置和Java代码设置。

1. xml布局

2. java代码

注意:必须setBounds测量图片边界,否则不显示。

3.缺陷

当TextView设置成固定大小时,由于文字距离边界的距离过大,会导致文字与图片之间设置的间距无效,如下图。

解决方案:

①设置TextView的内填充

通过设置paddingLeft、paddingRight、paddingTop、paddingBottom来缩写这个间距

②自定义TextView重新布局

a.先自定义属性iconPadding来设置间距,并提供方法给外部调用。

b.重写setCompoundDrawablesWithIntrinsicBounds方法来获取我们设置的drawable宽高。

c.最后重写onLayout方法。

可以先参考:Android技巧之drawablePadding的那些事(https://yuxingxin.com/2015/11/05/DrawablePadding/),该篇文章只解决了左右失效的问题。后期会整理个解决图文混排的工具库,里面会有具体方案。

4

Spannable使用

1.简介

setText(CharSequence text)中接收的是CharSequence。而SpannableString和SpannableStringBuilder是其实现类,是可以直接赋值的。并且两者的setSpan方法可以设置一些格式对象(例如字体大小、下划线、替换为图片等),这就可以实现富文本了。

Spannable实现子类:SpannableString,SpannableStringBuilder(可变,类似于StringBuilder)。

Spannable中定义了抽象方法:setSpan(Object what, int start, int end, int flags)和removeSpan(Object what)。这两个方法实现了对字符串的灵活编辑。

其中setSpan方法包含如下参数:

flags常用的有四种

通常在insert方式才生效,平时不生效,具体看:Explain the meaning of Span flags like SPAN_EXCLUSIVE_EXCLUSIVE。(https://stackoverflow.com/questions/9879233/explain-the-meaning-of-span-flags-like-span-exclusive-exclusive)

2.常用span类

3.使用方式

其中ImageSpan默认对其方式有两种:ALIGN_BOTTOM及ALIGN_BASELINE。很可惜我们平常用的居中对其的方式没有,不过可以通过自定义实现,后续会在开源出来。

4.效果

5

HTML显示

一般显示HTML内容有两种方式:

  • 使用 Android 提供的 WebView 控件。


  • 通过将 HTML 内容转化为 Spanned 格式在 TextView 中进行显示。

现在大多数都用WebView的方式。但是并不是所有的场景下都适合使用 WebView 来显示 HTML 内容,例如,如果应用要显示的内容只是一部分 HTML 片段,就可以利用 TextView 来进行显示,并且效率较高。

由于这种方式不太常用,就不深入介绍,里面可以实现的效果还是很好的。

1.简介

Android 中的 TextView 组件常用于显示文本内容,其实它也可以显示 HTML 的内容。

简单来讲,这就需要先把 HTML 的内容以字符串的形式获取后,经过 android.text.Html.fromHtml转化成 Spanned 的格式,然后将其传递到 TextView 的 setText方法中,这样就可以在 TextView 中显示 HTML 页面的内容了。

需要注意的是,并不是所有的 HTML 标签在 TextView 中都是支持的,且官方文档并没有明确的说明支持 HTML 标签列表,通过查看 Android 源代码,可以得到简单的支持列表。

下面的示例来介绍如何在 TextView 中显示一段 HTML 内容,要显示的这段 HTML 内容即包含超链接内容,也包含有图片。

2.使用

fromHtml方法

source,就是包含 HTML 内容的字符串。Html.ImageGetter 和 Html.TagHandler 是两个接口,提供给开发者继承使用。

imageGetter, 如果要显示图片是需要被继承的,重写 getDrawable(String source)方法,用于获取 HTML 里面的图片来显示在 TextView 中。

tagHandler,其作用是把 HTML 带标记的文本内容字符串转化成可以显示效果的的 Spanned 字符串 。由于并非所有的 HTML 标签都可以转化,所以在使用时,用户需要自己添加一些必要的标签和处理方法时才会继承使用的。

继承ImageGetter

继承于 ImageGetter,重写 getDrawable (String source) 方法。通过异步操作,读取本地/网络资源,获得drawable对象。

继承TagHandler

继承于 TagHandler,重写了 handleTag方法。为了支持更多的标签,例如为了支持<ul><ol><dd>和<li>标签,这四个标签是在 formHtml方法中本身是不支持。

如果开发者认为安卓 TagHandler 提供的默认标签解析已经够用,直接在 fromHtml方法中第三个参数的地方填写 既可。

最后,通过 formHtml方法将 HTML 内容转化为可供显示的 SpannableString,将 SpannableString 通过 setText 方法放入 TextView 中,就可以显示图文并茂的内容了。

用户交互

formHtml方法已经将 HTML 内容中的超链接和图片转义成为 UrlSpan 和 ImageSpan,进而在 TextView 中完成显示。但是此时是没有任何用户交互的,用户只能看到 HTML 的内容,下面介绍如何添加用户交互功能。

要完成用户交互,这里我们需要在 TextView 中还需要调用textView.setMovementMethod方法。

Android 提供了 LinkMovementMethod 类以实现了对于文本内容中超链接的遍历,并且支持对于超链接的点击事件。

所以只要在添加下面一行代码,就可以使点击 UrlSpan 能够触发打开链接的功能。

如果想要更多的用户交互效果,可以自定义LinkMovementMethod 类,重写onTouchEvent方法来实现。

3.效果

关于HTML显示这部分,没做具体实现。具体可以看:灵活高效的在 Android Native App 开发中显示 HTML 内容(https://www.ibm.com/developerworks/cn/web/1407_zhangqian_androidhtml/index.html),里面有具体源码可以下载,HTML部分内容也是参考该篇文章完成的。

开源库:html-textview

https://github.com/PrivacyApps/html-textview

6

总结

以上就是关于图文混排的一些解决方案,相信通过这些了解,对于工作中的实际场景的使用大家会有适当的解决方案。由于实际应用较少,所以认识较为浅显,可能有些地方描述不当,后期会考虑封装个解决图文混排的工具类,加深下理解。

TextView添加背景,或者是给TextView添加添加边框,以及怎么样设置TextView的形状。怎么在java代码部分设置TextView的背景,和TextView的形状及边框。

方法如下:

怎么在Java代码部分怎么设置TextView的背景颜色,其实很简单的就一句话。

tvTemp.setBackgroundColor(Color.parseColor("#00FF00"));

在xml布局文件中就可以直接调用drawable文件代码如下:

android:background="@drawable/textview"

在设置背景的时候, 我们都知道使用 setBackgroundColor()方法,但是,方法里面的参数,必须是RGB HTML格式的值,如果我们用drawable,它会提示drawable是int类型的。(其实如果可以的话我们不妨使用ImageView组件,这个组件相对TextView更好用)。

接下来就来看看怎么给让TextView显示边框,并且怎么样让其显示圆形。这里我们就需要在drawable里面,新建一个.xml文件。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
 android:shape="oval"
 android:useLevel="false">
 <solid android:color="#00FF00" />
 <stroke android:width="1dip" android:color="#000000" />
 <size
  android:width="15dp"
  android:height="15dp" />
</shape>

这只是一个圆形,其中, stroke属性,是设置的他的边框颜色和宽度,在xml布局中显示的是如图所示的样式:

xml样式

可以在xml文件中利用drawable调用。其中在xml中不仅可以设置圆形,而且还可以设置圆角,

<corners android:radius="15dp" />属性就是设置圆角

我们在介绍一些关于shape里面的知识:

gradient -- 颜色渐变
startcolor 起点颜色
endcolor 终点颜色
android:angle 角度 0是从左到右,90是从下到上
solid -- 填充
stroke -- 描边
corners -- 圆角
padding -- 内容离边界的距离
当需要在java代码中需要设置TextView时,发现通过上面的方法设置,圆角就会消失,在这里怎么让圆角不会消失,我们需要: