Solutions for rich file tags in Flutter

Inscription
- Holding the sword in the world, starting from your accumulation, you will strive for perfection wherever you go.
【x1】Click to view the tips

[x2] Various series of tutorials

A programmer's practice diary


In actual business development, there is often such a tag in Html format, see the following picture:

insert image description here
In Flutter , I'm a little worried, because the Text and RichText provided by Flutter can't parse this format, but you can't use the WebView plug-in. If you use it, you will embed a browser kernel in each Item, no matter how strong it is. The mobile phone will also be stuck, of course, it must not be done, because this is the wrong way.

After a lot of attempts and thinking, the editor finally wrote a plug-in that can be analyzed, and now I will share it with you.

1 Basic use implementation

1.2 Add dependencies

The editor is still the same, here is a pub method: [Needless to say, the shortcut entry is here] [Of course there is also github] [There is also video support for exaggeration]

dependencies:
  flutter_html_rich_text: ^1.0.0
  • 1
  • 2

insert image description here

1.3 Load and parse HTML fragment tags

The core method is as follows:

///htmlText is your HTML fragment 
 HtmlRichText ( 
  htmlText : txt , 
 ) ,
  • 1
  • 2
  • 3
  • 4

The following code listing 1-3-1 is the effect of the above figure:

/// Code Listing 1-3-1 
class  TestHtmlPage  extends  StatefulWidget  {
  @override
  _TestPageState createState() => _TestPageState();
}

class _TestPageState extends State<TestHtmlPage> {

  String txt = 
      "<p>long-distance wheel<h4>high-speed drive</h4><span style='background-color:#ff3333'>" 
      "<span style='color:#ffffff;padding:10px'> 3 Immediately Reduced Tire Purchase Raffle</span></span></p>" 
      "<p>Long-distance high-speed drive wheels<span><span style='color:#cc00ff;'> 3 Immediately Reduced Tire Purchase Raffle Draw</span> span></span></p>" ;

  @override
  Widget build ( BuildContext context )  { 
    return  Scaffold ( 
      /// a title 
      appBar :  AppBar ( title :  Text ( 'A Page' ) , ) , 
      body :  Center ( 
        /// a list 
        child : ListView . builder ( 
          itemBuilder :  ( BuildContext context , int postiont )  { 
            return  buildItemWidget ( postiont);
          },
          itemCount: 100,
        ),
      ),
    );
  }

  ///ListView's item 
  Widget buildItemWidget ( int postiont )  { 
    return  Container ( 
      /// Content margin 
      padding : EdgeInsets . all ( 8 ) , 
      child :  Column ( 
        /// Left-aligned child Widget 
        crossAxisAlignment : CrossAxisAlignment . start ,

        ///Content wrapping 
        mainAxisSize : MainAxisSize .min , children 
        : [ Text ( " Test Title $postiont " , 
            style : TextStyle ( fontWeight : FontWeight . w500 ) , ) , 
          
             
          

          ///html Rich Text Label 
          Container ( 
            margin : EdgeInsets . only ( top :  8 ) , 
            child :  HtmlRichText ( 
              htmlText : txt , 
            ) , 
          ) 
        ] , 
      ) , 
    ) ; 
  } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

The following is the practice of analysing thinking and burning the brain


2 Brain-burning thinking practice one

The Flutter application is loaded by the Android iOS platform. In native Android, the parsing can be easily implemented using TextView (Listing 2-1 below), and of course UILabel can be easily implemented in iOS (Listing 2-2 below).

// Core method of loading Html in Android native TextView 
// Code Listing 2-1 
// A TagHandler defined by MxgsaTagHandler is used to handle click events 
lTextView . setText ( Html . fromHtml ( myContent , null ,  new  MxgsaTagHandler ( context ) ) ) ; 
lTextView .setClickable ( true ) ; 
lTextView.setMovementMethod ( LinkMovementMethod.getInstance ( ) ) ; _ _ _ _ _
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
// The core method of loading Html in iOS native UILabel 
// Code Listing 2-2 
// The returned HTML text is such as <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];

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

Then for Flutter, the native View can be loaded smoothly [ there is a description here ], as shown in the following code listing 2-3, which is implemented in Flutter through AndroidView and UiKitView.

  //The core method of loading native View in Flutter 
  //Code Listing 2-3 
  buildAndroidView ( )  { 
    return  AndroidView ( 
      //Set the logo 
      viewType :  "com.studyon./text_html" , 
      //The encoding method of parameters 
      creationParamsCodec :  const  StandardMessageCodec ( ) , 
    ) ; 
  }

  /// Load iOS native View through 
  UiKitView buildUIKitView ( )  { 
    return  UiKitView ( 
      //identify 
      viewType :  "com.studyon./text_html" , 
      //parameter encoding 
      creationParamsCodec :  const  StandardMessageCodec ( ) , 
    ) ; 
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

So the editor developed the first wave of operations, and developed such a plug-in to call the native View to render rich text labels [source code here] . The use of this plug-in is very simple, as shown below:

HTMLTextWidet ( 
  htmlText :  "test it" , 
 )
  • 1
  • 2
  • 3

This step is really a so-called show operation. In fact, I thought it was inappropriate before the development. However, due to the personality of the editor, I had to try to verify it. Now the result is out. HTMLTextWidet will have a short black screen effect, and the memory is too much, as shown in the following figure:
insert image description here
Why is the screen black, the Xianyu technical team has discussed the correct posture of embedding Native components in Flutter and the article In -depth understanding of Flutter interface development is discussed in detail .

So the result is: not feasible.


3 Brain-burning thinking practice 2

Use the idea of ​​Java to parse String to process HTML strings, process them into small fragments, and then use Text combined with the flow layout Wrap to combine. The core code is shown in the following list 3-1 for parsing:

  /*
   Parse tags
   */
  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 )  { 
            // / Parsing exceptions do not have to be handled 
          } 
        } 
      }
    }
    LogUtil.e("${tagColorModelList.length} \n\n ");
    return tagColorModelList;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

Then TagColorModel is defined as shown in Listing 3-2:

///Code Listing 3-2 
class  TagColorModel  { 
  ///Background 
  Color backGroundColor ; 
 ///Text Color 
  Color textColor ; 
 ///Text 
  String text ;

  TagColorModel(
      {this.text = "",
      this.backGroundColor = Colors.transparent,
      this.textColor = Colors.white});
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

Then use Wrap to use the parsed content, as shown in Listing 3-3 below:

///Code Listing 3-3 
///Get Background Color 
  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),
      ),
    ));
  }

  /// Then use Wrap to wrap 
  Wrap ( 
	alignment : WrapAlignment . spaceBetween , 
 	 children : tagList , 
  ) ,
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

Practical result: feasible, but has poor compatibility and low efficiency.

Of course, the Xianyu team has an article on how to implement rich text in Flutter at low cost. It is enough to read this article! is also discussed in detail.

4 Brain-burning thinking practice III

When Dart extracts data from a website in Flutter, the html dependency library is a good choice. html is an open source Dart package, which is mainly used to extract data from HTML, obtain node attributes, text and HTML, and various The content of the node. Html pub repository

dependencies:
  html: ^0.14.0+3
  • 1
  • 2

So Xiaobian also started to try, first of all, use the Html library to parse the HTML text block, and traverse all the node nodes of the parsed Document recursively, as shown in the following code listing 4-1:

Code Listing 4-1 
import  'package:html/parser.dart'  as parser ; 
import  'package:html/dom.dart'  as dom ;

List < Widget >  parse ( String originHtmlString )  { 
  // Space replacement removes all br tags and replaces them with \n, 
  originHtmlString = originHtmlString . replaceAll ( '<br/>' ,  '\n' ) ; 
  originHtmlString = originHtmlString . replaceAll ( '< br>' ,  '\n' ) ; 
  originHtmlString = originHtmlString . replaceAll ( '<br />' ,  '\n' ) ;

  ///html Dependency library parsing 
  dom . Document document = parser . parse ( originHtmlString ) ; 
  ///Get the node node 
  dom in DOM . Node cloneNode = document . body . clone ( true ) ;

 // Note: Pre-order traversal finds all key nodes (because it is passed by reference, so you need to get the hashCode again) 
  List < dom . Node > keyNodeList =  new  List < dom . Node > ( ) ; 
  int nodeIndex =  0 ; 
  // / Recursively traverse 
  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 ) ; 
      // Note: images that occupy the entire row are also treated as key nodes 
    }  else  if  ( childNode is dom . Element && 
        childNode . localName ==  'img'  && 
        checkImageNeedNewLine ( childNode ) )  { 
      print ('TEST: one line image nodeIndex = ${nodeIndex++}');
      keyNodeList.add(childNode);
    }
  });

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
/// Recursively traverse 
void  parseNodesTree ( dom . Node node , 
    { NodeTreeCallBack callBack = printNodeName } )  { 
  /// Traverse Node nodes 
  for  ( var i =  0 ; i < node . nodes . length ;  ++ i )  { 
    dom . Node item = node . nodes [ i ] ; 
    callBack ( item ) ;
    parseNodesTree(item, callBack: callBack);
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

Then it is to map the obtained node node with the Flutter component, use TextSpan for text, Image for image, and then use TextStyle to map the style, and finally wrap the parsed result component with Wrap to achieve the current plugin flutter_html_rich_text

The comprehensive implementation idea is to use the HTML library to improve the analysis in [Brain-burning Thinking Practice II].

The analysis is long, and if you are interested, you can look at the github source code.


At present, Xiaobian publishes a series of Flutter tutorials for free on the watermelon video, which is updated daily. Welcome to pay attention to receiving reminders and click to view various series of tutorials.

2020.09.12 Development Notes

Related: Solutions for rich file tags in Flutter