├── .flutter-plugins
├── .gitignore
├── .idea
├── encodings.xml
├── flutter_fullhtml_textview.iml
├── misc.xml
├── modules.xml
└── vcs.xml
├── .metadata
├── README.md
├── android
├── .gradle
│ ├── 4.10.2
│ │ ├── fileChanges
│ │ │ └── last-build.bin
│ │ ├── fileContent
│ │ │ └── fileContent.lock
│ │ ├── fileHashes
│ │ │ ├── fileHashes.bin
│ │ │ ├── fileHashes.lock
│ │ │ └── resourceHashesCache.bin
│ │ ├── gc.properties
│ │ ├── javaCompile
│ │ │ ├── classAnalysis.bin
│ │ │ ├── jarAnalysis.bin
│ │ │ ├── javaCompile.lock
│ │ │ └── taskHistory.bin
│ │ └── taskHistory
│ │ │ ├── taskHistory.bin
│ │ │ └── taskHistory.lock
│ ├── buildOutputCleanup
│ │ ├── buildOutputCleanup.lock
│ │ ├── cache.properties
│ │ └── outputFiles.bin
│ └── vcs-1
│ │ └── gc.properties
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ ├── com
│ │ │ │ └── example
│ │ │ │ │ └── flutter_app
│ │ │ │ │ └── MainActivity.java
│ │ │ └── io
│ │ │ │ └── flutter
│ │ │ │ └── plugins
│ │ │ │ └── GeneratedPluginRegistrant.java
│ │ └── res
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ │ └── values
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── flutter_app_android.iml
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── local.properties
└── settings.gradle
├── assets
└── images
│ ├── 3.0x
│ ├── feed_cell_photo_default_big.png
│ ├── image_error.png
│ └── long_picture_icon.png
│ ├── image_error.png
│ └── video_placeholder.png
├── flutter_app.iml
├── ios
├── Flutter
│ ├── AppFrameworkInfo.plist
│ ├── Debug.xcconfig
│ ├── Generated.xcconfig
│ └── Release.xcconfig
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ └── contents.xcworkspacedata
└── Runner
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Icon-App-1024x1024@1x.png
│ │ ├── Icon-App-20x20@1x.png
│ │ ├── Icon-App-20x20@2x.png
│ │ ├── Icon-App-20x20@3x.png
│ │ ├── Icon-App-29x29@1x.png
│ │ ├── Icon-App-29x29@2x.png
│ │ ├── Icon-App-29x29@3x.png
│ │ ├── Icon-App-40x40@1x.png
│ │ ├── Icon-App-40x40@2x.png
│ │ ├── Icon-App-40x40@3x.png
│ │ ├── Icon-App-60x60@2x.png
│ │ ├── Icon-App-60x60@3x.png
│ │ ├── Icon-App-76x76@1x.png
│ │ ├── Icon-App-76x76@2x.png
│ │ └── Icon-App-83.5x83.5@2x.png
│ └── LaunchImage.imageset
│ │ ├── Contents.json
│ │ ├── LaunchImage.png
│ │ ├── LaunchImage@2x.png
│ │ ├── LaunchImage@3x.png
│ │ └── README.md
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── GeneratedPluginRegistrant.h
│ ├── GeneratedPluginRegistrant.m
│ ├── Info.plist
│ └── main.m
├── lib
├── ImageClipper.dart
├── circle.dart
├── constants.dart
├── element_parser_to_widget_list.dart
├── flutter_html_text.dart
├── flutter_html_textview.dart
├── html_async_textview.dart
├── html_parser_to_element_list.dart
├── html_parser_to_widget_list.dart
├── html_tags.dart
├── html_text_style.dart
├── image.dart
├── main.dart
├── network_image.dart
├── on_tap_data.dart
└── web_view_page.dart
├── preview
├── Screenshot_1.jpg
├── Screenshot_2.jpg
├── Screenshot_3.jpg
├── Screenshot_4.jpg
└── Screenshot_5.jpg
└── pubspec.yaml
/.flutter-plugins:
--------------------------------------------------------------------------------
1 | fluttertoast=/Users/houzhenpu/flutter/.pub-cache/hosted/pub.flutter-io.cn/fluttertoast-3.1.3/
2 | path_provider=/Users/houzhenpu/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider-1.4.0/
3 | sqflite=/Users/houzhenpu/flutter/.pub-cache/hosted/pub.flutter-io.cn/sqflite-1.1.7+1/
4 | webview_flutter=/Users/houzhenpu/flutter/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter-0.3.15+1/
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .dart_tool/
3 |
4 | .packages
5 | .pub/
6 |
7 | .idea/
8 | .vagrant/
9 | .sconsign.dblite
10 | .svn/
11 |
12 | *.swp
13 | profile
14 |
15 | DerivedData/
16 |
17 | .generated/
18 |
19 | *.pbxuser
20 | *.mode1v3
21 | *.mode2v3
22 | *.perspectivev3
23 |
24 | !default.pbxuser
25 | !default.mode1v3
26 | !default.mode2v3
27 | !default.perspectivev3
28 |
29 | xcuserdata
30 |
31 | *.moved-aside
32 |
33 | *.pyc
34 | *sync/
35 | Icon?
36 | .tags*
37 |
38 | build/
39 | .android/
40 | .ios/
41 | .flutter-plugins
42 |
43 | android/
44 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
这样做的好处,我觉得可能是为了了统一的渲染。加入样式,会
'; 36 | 37 | HtmlTextView(html) 38 | ``` 39 | 异步解析 40 | ``` 41 | HtmlParserToWidgetList( 42 | imagePadding: 43 | EdgeInsets.only(top: 2.0, left: 12.0, right: 12.0, bottom: 0.0), 44 | videoPadding: 45 | EdgeInsets.only(top: 4.0, left: 12.0, right: 12.0, bottom: 2.0), 46 | htmlTextStyle: HtmlTextStyle( 47 | height: 1.6, 48 | fontSize: valueOfPt(17), 49 | padding: 50 | EdgeInsets.only(top: 6.0, left: 12.0, right: 12.0, bottom: 8.0), 51 | digitalFontWeight: DigitalFontWeight.strong, 52 | digitalPrefix: ' ', 53 | pointPrefix: ' ', 54 | defaultTextColor: isNightStyle ? Color(0xff898989) : Color(0xff333333), 55 | letterSpacing: 1.0, 56 | ), 57 | ) 58 | /// this.data 是需要解析的html 59 | .asyncParseHtmlToWidgetList(this.data, 60 | onTapCallback: this.onTapCallback, imageList: imageList, id: id) 61 | .then((nodes) { 62 | /// nodes 既为解析出的WidgetList 63 | }); 64 | ``` 65 | 66 | ## 预览 67 | 68 | 效果图如下: 69 | 70 | |  |  |  |  | 71 | | :--------------------------------: | :---------------------------------: | :-------------------------------: | :-------------------------------: | 72 | |  | | | | 73 | 74 | **觉得还可以的话,来个Star、Fork支持一波!有问题欢迎提Issue。** 75 | 76 | -------------------------------------------------------------------------------- /android/.gradle/4.10.2/fileChanges/last-build.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/.gradle/4.10.2/fileContent/fileContent.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/android/.gradle/4.10.2/fileContent/fileContent.lock -------------------------------------------------------------------------------- /android/.gradle/4.10.2/fileHashes/fileHashes.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/android/.gradle/4.10.2/fileHashes/fileHashes.bin -------------------------------------------------------------------------------- /android/.gradle/4.10.2/fileHashes/fileHashes.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/android/.gradle/4.10.2/fileHashes/fileHashes.lock -------------------------------------------------------------------------------- /android/.gradle/4.10.2/fileHashes/resourceHashesCache.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/android/.gradle/4.10.2/fileHashes/resourceHashesCache.bin -------------------------------------------------------------------------------- /android/.gradle/4.10.2/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/android/.gradle/4.10.2/gc.properties -------------------------------------------------------------------------------- /android/.gradle/4.10.2/javaCompile/classAnalysis.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/android/.gradle/4.10.2/javaCompile/classAnalysis.bin -------------------------------------------------------------------------------- /android/.gradle/4.10.2/javaCompile/jarAnalysis.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/android/.gradle/4.10.2/javaCompile/jarAnalysis.bin -------------------------------------------------------------------------------- /android/.gradle/4.10.2/javaCompile/javaCompile.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/android/.gradle/4.10.2/javaCompile/javaCompile.lock -------------------------------------------------------------------------------- /android/.gradle/4.10.2/javaCompile/taskHistory.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/android/.gradle/4.10.2/javaCompile/taskHistory.bin -------------------------------------------------------------------------------- /android/.gradle/4.10.2/taskHistory/taskHistory.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/android/.gradle/4.10.2/taskHistory/taskHistory.bin -------------------------------------------------------------------------------- /android/.gradle/4.10.2/taskHistory/taskHistory.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/android/.gradle/4.10.2/taskHistory/taskHistory.lock -------------------------------------------------------------------------------- /android/.gradle/buildOutputCleanup/buildOutputCleanup.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock -------------------------------------------------------------------------------- /android/.gradle/buildOutputCleanup/cache.properties: -------------------------------------------------------------------------------- 1 | #Mon Apr 29 09:50:52 CST 2019 2 | gradle.version=4.10.2 3 | -------------------------------------------------------------------------------- /android/.gradle/buildOutputCleanup/outputFiles.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/android/.gradle/buildOutputCleanup/outputFiles.bin -------------------------------------------------------------------------------- /android/.gradle/vcs-1/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/android/.gradle/vcs-1/gc.properties -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 28 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.example.flutter_app" 37 | minSdkVersion 16 38 | targetSdkVersion 28 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | implementation 'androidx.appcompat:appcompat:1.0.2' 59 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 60 | androidTestImplementation 'androidx.test:runner:1.1.1' 61 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 62 | testImplementation 'junit:junit:4.12' 63 | } 64 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 |'; 4 | 5 | const String lineBreakTag = '\n'; 6 | 7 | const String blockQuote = '
'; -------------------------------------------------------------------------------- /lib/element_parser_to_widget_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'constants.dart'; 4 | import 'flutter_html_text.dart'; 5 | import 'html_text_style.dart'; 6 | import 'network_image.dart'; 7 | import 'package:html/dom.dart' as dom; 8 | 9 | import 'on_tap_data.dart'; 10 | 11 | class ElementParserToWidgetList { 12 | static const EdgeInsetsGeometry _defaultImagePadding = 13 | EdgeInsets.only(top: 4.0, left: 0.0, right: 0.0, bottom: 8.0); 14 | 15 | EdgeInsetsGeometry imagePadding; 16 | 17 | static const EdgeInsetsGeometry _defaultVideoPadding = 18 | EdgeInsets.only(top: 4.0, left: 0.0, right: 0.0, bottom: 8.0); 19 | 20 | EdgeInsetsGeometry videoPadding; 21 | 22 | HtmlTextStyle htmlTextStyle; 23 | 24 | ElementParserToWidgetList( 25 | {this.imagePadding = _defaultImagePadding, 26 | this.videoPadding = _defaultVideoPadding, 27 | this.htmlTextStyle}); 28 | 29 | ListparseElementToWidgetList(dom.Element element, 30 | {Function onTapCallback, List imageList, String id}) { 31 | List widgetList = new List(); 32 | if (element.outerHtml.contains(" widgetList, Function onTapCallback, 71 | {List
imageList, String id}) { 72 | String outerHtml = e.outerHtml; 73 | 74 | var imgElements = e.getElementsByTagName("img"); 75 | if (e.nodes.length > 1) { 76 | imgElements.forEach((f) { 77 | outerHtml = outerHtml.replaceAll( 78 | '${f.outerHtml}', '$separator ${f.outerHtml}$separator
'); 79 | }); 80 | } 81 | int imageIndex = 0; 82 | outerHtml.split(separator).forEach((html) { 83 | if (html.contains("
$separatorwidgetList, Function onTapCallback, 95 | {String id}) { 96 | widgetList.add(new GestureDetector( 97 | onTap: () { 98 | if (onTapCallback != null) { 99 | onTapCallback(OnTapData(imageUrl, type: OnTapType.img, id: id)); 100 | } 101 | }, 102 | child: Center( 103 | child: Container( 104 | padding: imagePadding, 105 | child: NetworkImageClipper( 106 | imageUrl, 107 | id: id, 108 | ), 109 | ), 110 | ), 111 | )); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/flutter_html_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'constants.dart'; 5 | import 'html_tags.dart'; 6 | import 'html_text_style.dart'; 7 | import 'on_tap_data.dart'; 8 | 9 | // ignore: must_be_immutable 10 | class HtmlText extends StatelessWidget { 11 | String data; 12 | 13 | final Function onTapCallback; 14 | 15 | HtmlTextStyle htmlTextStyle; 16 | 17 | Container _dataContainer; 18 | 19 | HtmlText(this.data, {this.onTapCallback, this.htmlTextStyle}); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | if (_dataContainer == null) { 24 | HtmlParser parser = new HtmlParser(htmlTextStyle: this.htmlTextStyle); 25 | this.data = this 26 | .data 27 | .replaceAll(' ', ' ') 28 | .replaceAll('>', '>') 29 | .replaceAll('<', '>') 30 | .replaceAll('&', '&'); 31 | _dataContainer = Container( 32 | padding: htmlTextStyle?.padding, 33 | child: this.data.startsWith(blockQuote) 34 | ? createBlockQuote(parser, context) 35 | : _createRichText(parser, context), 36 | ); 37 | } 38 | return _dataContainer; 39 | } 40 | 41 | Stack createBlockQuote(HtmlParser parser, BuildContext context) { 42 | this.data = this.data.replaceAll(endStartPTag, lineBreakTag); 43 | return Stack( 44 | fit: StackFit.loose, 45 | children:
[ 46 | Container( 47 | margin: htmlTextStyle.blockQuotTextMargin, 48 | child: _createRichText(parser, context), 49 | ), 50 | Positioned( 51 | top: 1, 52 | bottom: 1, 53 | child: Container( 54 | color: htmlTextStyle.blockQuoteColor, 55 | width: htmlTextStyle.blockQuoteWidth, 56 | alignment: Alignment.topLeft, 57 | margin: htmlTextStyle.blockQuotMargin, 58 | ), 59 | ), 60 | ], 61 | ); 62 | } 63 | 64 | RichText _createRichText(HtmlParser parser, BuildContext context) { 65 | return RichText( 66 | text: this._stackToTextSpan(parser.parse(this.data), context), 67 | softWrap: true, 68 | textAlign: parser.textAlign, 69 | ); 70 | } 71 | 72 | TextSpan _stackToTextSpan(List nodes, BuildContext context) { 73 | List children = []; 74 | for (int i = 0; i < nodes.length; i++) { 75 | children.add(_textSpan(nodes[i])); 76 | } 77 | return new TextSpan( 78 | text: '', 79 | style: DefaultTextStyle.of(context).style, 80 | children: children); 81 | } 82 | 83 | TextSpan _textSpan(Map node) { 84 | TextSpan span = new TextSpan( 85 | text: node['text'], 86 | style: node['style'], 87 | recognizer: TapGestureRecognizer() 88 | ..onTap = () { 89 | if (onTapCallback != null && node['href'] != '') { 90 | onTapCallback(OnTapData(node['href'])); 91 | } 92 | }); 93 | return span; 94 | } 95 | } 96 | 97 | class HtmlParser { 98 | HtmlTextStyle htmlTextStyle; 99 | 100 | HtmlParser({this.htmlTextStyle}); 101 | 102 | List _stack = []; 103 | List _spanStyle = []; 104 | List _result = []; 105 | Map _tag; 106 | TextAlign textAlign = TextAlign.left; 107 | 108 | List parse(String html) { 109 | String last = html; 110 | Match match; 111 | int index; 112 | bool isTextPart; 113 | int tagIndex = 1; 114 | bool isAppendStartTag = false; 115 | htmlTextStyle = htmlTextStyle ?? HtmlTextStyle(); 116 | while (html.length > 0) { 117 | isTextPart = true; 118 | if (this._getStackLastItem() == null || 119 | !specialTags.contains(this._getStackLastItem())) { 120 | if (html.indexOf(''); 122 | if (index >= 0) { 123 | html = html.substring(index + 3); 124 | isTextPart = false; 125 | } 126 | } else if (html.indexOf('') == 0) { 127 | isAppendStartTag = false; 128 | match = endTag.firstMatch(html); 129 | if (match != null) { 130 | String tag = match[0]; 131 | html = html.substring(tag.length); 132 | isTextPart = false; 133 | this._parseEndTag(tag); 134 | } 135 | } else if (html.indexOf('<') == 0) { 136 | match = startTag.firstMatch(html); 137 | if (match != null) { 138 | String tag = match[0]; 139 | html = html.substring(tag.length); 140 | isTextPart = false; 141 | this._parseStartTag( 142 | tag, match[1], match[2], match.start, isAppendStartTag); 143 | isAppendStartTag = true; 144 | } 145 | } 146 | if (isTextPart) { 147 | index = html.indexOf('<'); 148 | String text = (index < 0) ? html : html.substring(0, index); 149 | html = (index < 0) ? '' : html.substring(index); 150 | if (html.contains(ulTag)) { 151 | text = _createPointTags(text); 152 | } else if (html.contains(olTag)) { 153 | text = _createDigitalTags(tagIndex, text); 154 | } 155 | tagIndex++; 156 | this._appendNode(text); 157 | } 158 | } else { 159 | RegExp re = 160 | new RegExp(r'(.*)<\/' + this._getStackLastItem() + r'[^>]*>'); 161 | html = html.replaceAllMapped(re, (Match match) { 162 | String text = match[0] 163 | ..replaceAll(new RegExp(''), '\$1') 164 | ..replaceAll(new RegExp(''), '\$1'); 165 | this._appendNode(text); 166 | return ''; 167 | }); 168 | this._parseEndTag(this._getStackLastItem()); 169 | } 170 | if (html == last) { 171 | //throw 'Parse Error: ' + html;出现不可解析标签直接显示 172 | _appendNode(html); 173 | html = ''; 174 | } 175 | last = html; 176 | } 177 | this._parseEndTag(); 178 | List result = this._result; 179 | this._stack = []; 180 | this._result = []; 181 | this._spanStyle = []; 182 | return result; 183 | } 184 | 185 | String _createDigitalTags(int tagIndex, String text) { 186 | this._tag = null; 187 | _appendTag('${htmlTextStyle.digitalFontWeight}', 188 | {'style': 'font-size:${htmlTextStyle.digitalFontSize}'}, false); 189 | 190 | this._appendNode( 191 | '${htmlTextStyle.pointPrefix ?? ""}' + tagIndex.toString() + '. '); 192 | text = text + lineBreakTag; 193 | return text; 194 | } 195 | 196 | String _createPointTags(String text) { 197 | this._tag = null; 198 | _appendTag( 199 | 'w900', {'style': 'font-size:${htmlTextStyle.pointFontSize}'}, false); 200 | this._appendNode('${htmlTextStyle.pointPrefix ?? ""}• '); 201 | text = text + lineBreakTag; 202 | return text; 203 | } 204 | 205 | void _parseStartTag(String tag, String tagName, String rest, int unary, 206 | bool isAppendStartTag) { 207 | tagName = tagName.toLowerCase(); 208 | if (blockTags.contains(tagName)) { 209 | while (this._getStackLastItem() != null && 210 | inlineTags.contains(this._getStackLastItem())) { 211 | this._parseEndTag(this._getStackLastItem()); 212 | } 213 | } 214 | if (closeSelfTags.contains(tagName) && 215 | this._getStackLastItem() == tagName) { 216 | this._parseEndTag(tagName); 217 | } 218 | if (emptyTags.contains(tagName)) { 219 | unary = 1; 220 | } 221 | if (unary == 0) { 222 | this._stack.add(tagName); 223 | } 224 | Map attrs = {}; 225 | Iterable matches = attrTag.allMatches(rest); 226 | 227 | if (matches != null) { 228 | for (Match match in matches) { 229 | String attribute = match[1]; 230 | String value; 231 | 232 | if (match[2] != null) { 233 | value = match[2]; 234 | } else if (match[3] != null) { 235 | value = match[3]; 236 | } else if (match[4] != null) { 237 | value = match[4]; 238 | } else if (fillAttrs.contains(attribute) != null) { 239 | value = attribute; 240 | } 241 | attrs[attribute] = value; 242 | if (attribute.endsWith('style') && tagName.endsWith('span')) { 243 | this._spanStyle.add(value); 244 | } 245 | } 246 | } 247 | this._appendTag(tagName, attrs, isAppendStartTag); 248 | } 249 | 250 | void _parseEndTag([String tagName]) { 251 | int pos; 252 | if (tagName == null) { 253 | pos = 0; 254 | } else { 255 | if (tagName.contains('span') && this._spanStyle.length > 0) { 256 | this._spanStyle?.removeLast(); 257 | } 258 | for (pos = this._stack.length - 1; pos >= 0; pos--) { 259 | if (this._stack[pos] == tagName || tagName.contains(this._stack[pos])) { 260 | this._stack.remove(tagName); 261 | break; 262 | } 263 | } 264 | } 265 | if (pos >= 0) { 266 | this._stack.removeRange(pos, this._stack.length); 267 | } 268 | } 269 | 270 | TextStyle _parseStyle(List tags, Map attrs) { 271 | Iterable matches; 272 | String spanStyle = ''; 273 | if (this._spanStyle.isNotEmpty) { 274 | spanStyle = this._spanStyle.last.toString(); 275 | } 276 | String style = '$spanStyle${attrs['style']}'; 277 | String param; 278 | String value; 279 | 280 | FontWeight fontWeight = FontWeight.normal; 281 | FontStyle fontStyle = FontStyle.normal; 282 | TextDecoration textDecoration = TextDecoration.none; 283 | Color backgroundColor; 284 | htmlTextStyle.color = htmlTextStyle.defaultTextColor; 285 | double fontSize = htmlTextStyle.fontSize; 286 | tags.forEach((tag) { 287 | switch (tag) { 288 | case 'h1': 289 | fontSize = 32.0; 290 | break; 291 | case 'h2': 292 | fontSize = 24.0; 293 | break; 294 | case 'h3': 295 | fontSize = 20.8; 296 | break; 297 | case 'h4': 298 | fontSize = 16.0; 299 | break; 300 | case 'h5': 301 | fontSize = 12.8; 302 | break; 303 | case 'h6': 304 | fontSize = 11.2; 305 | break; 306 | case 'a': 307 | textDecoration = htmlTextStyle.hrefTextDecoration; 308 | htmlTextStyle.color = htmlTextStyle.hrefTextColor; 309 | break; 310 | case 'b': 311 | case 'strong': 312 | fontWeight = FontWeight.bold; 313 | break; 314 | case 'w900': 315 | fontWeight = FontWeight.w900; 316 | break; 317 | case 'i': 318 | case 'em': 319 | fontStyle = FontStyle.italic; 320 | break; 321 | case 'u': 322 | textDecoration = TextDecoration.underline; 323 | break; 324 | } 325 | }); 326 | 327 | if (style != null) { 328 | matches = styleTag.allMatches(style); 329 | 330 | for (Match match in matches) { 331 | param = match[1].trim(); 332 | value = match[2].trim(); 333 | switch (param) { 334 | case 'color': 335 | if (colorTag.hasMatch(value)) { 336 | value = value.replaceAll('#', '').trim(); 337 | htmlTextStyle.color = new Color(int.parse('0xFF' + value)); 338 | } 339 | break; 340 | case 'font-weight': 341 | fontWeight = 342 | (value == 'bold') ? FontWeight.bold : FontWeight.normal; 343 | break; 344 | case 'font-style': 345 | fontStyle = 346 | (value == 'italic') ? FontStyle.italic : FontStyle.normal; 347 | break; 348 | case 'text-decoration': 349 | textDecoration = (value == 'underline') 350 | ? TextDecoration.underline 351 | : TextDecoration.none; 352 | break; 353 | case 'text-align': 354 | if (value.endsWith('center')) { 355 | textAlign = TextAlign.center; 356 | } else if (value.endsWith('right')) { 357 | textAlign = TextAlign.right; 358 | } else { 359 | textAlign = TextAlign.left; 360 | } 361 | break; 362 | case 'background-color': 363 | if (colorTag.hasMatch(value)) { 364 | value = value.replaceAll('#', '').trim(); 365 | backgroundColor = new Color(int.parse('0xFF' + value)); 366 | } 367 | break; 368 | case 'font-size': 369 | fontSize = double.parse( 370 | value?.replaceAll(RegExp('([^.0-9])'), '') == '' 371 | ? '${htmlTextStyle.fontSize}' 372 | : value?.replaceAll(RegExp('([^.0-9])'), '')); 373 | break; 374 | } 375 | } 376 | } 377 | return TextStyle( 378 | color: htmlTextStyle.color, 379 | fontWeight: fontWeight, 380 | fontStyle: fontStyle, 381 | decoration: textDecoration, 382 | fontSize: fontSize, 383 | height: htmlTextStyle.height, 384 | backgroundColor: backgroundColor, 385 | letterSpacing: htmlTextStyle.letterSpacing, 386 | wordSpacing: htmlTextStyle.wordSpacing, 387 | ); 388 | } 389 | 390 | void _appendTag(String tag, Map attrs, bool isAppendStartTag) { 391 | if (this._tag == null) { 392 | List tags = [tag]; 393 | this._tag = {'label': tags, 'attrs': attrs}; 394 | } 395 | if (isAppendStartTag) { 396 | (this._tag['label'] as List ).add(tag); 397 | } 398 | if (attrs.length > 0) { 399 | this._tag['attrs'] = attrs; 400 | } 401 | } 402 | 403 | void _appendNode(String text) { 404 | if (this._tag == null) { 405 | List tags = ['p']; 406 | this._tag = {'label': tags, 'attrs': {}}; 407 | } 408 | if (this._stack != null) { 409 | (this._tag['label'] as List ).addAll(this._stack); 410 | } 411 | this._tag['text'] = text; 412 | this._tag['style'] = 413 | this._parseStyle(this._tag['label'], this._tag['attrs']); 414 | this._tag['href'] = 415 | (this._tag['attrs']['href'] != null) ? this._tag['attrs']['href'] : ''; 416 | 417 | this._result.add(this._tag); 418 | this._tag.remove('attrs'); 419 | this._tag = null; 420 | } 421 | 422 | String _getStackLastItem() { 423 | if (this._stack.length <= 0) { 424 | return null; 425 | } 426 | 427 | return this._stack[this._stack.length - 1]; 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /lib/flutter_html_textview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'html_parser_to_widget_list.dart'; 4 | import 'html_text_style.dart'; 5 | import 'on_tap_data.dart'; 6 | 7 | class HtmlTextView extends StatelessWidget { 8 | final String data; 9 | 10 | HtmlTextView(this.data, {EdgeInsetsGeometry padding}); 11 | 12 | final Function onTapCallback = (data) { 13 | if (data is OnTapData) { 14 | debugPrint('data.type--->${data.type}'); 15 | if (data.type == OnTapType.href) { 16 | } else if (data.type == OnTapType.img) { 17 | } else if (data.type == OnTapType.video) { 18 | } 19 | } 20 | }; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | List nodes = HtmlParserToWidgetList( 25 | imagePadding: 26 | EdgeInsets.only(top: 12.0, left: 0.0, right: 0.0, bottom: 12.0), 27 | videoPadding: 28 | EdgeInsets.only(top: 12.0, left: 0.0, right: 0.0, bottom: 12.0), 29 | htmlTextStyle: HtmlTextStyle( 30 | hrefTextDecoration: TextDecoration.underline, 31 | hrefTextColor: Colors.amber, 32 | height: 1.4, 33 | fontSize: 15, 34 | padding: EdgeInsets.only(top: 0.0, left: 0.0, right: 0.0, bottom: 4.0), 35 | digitalFontWeight: DigitalFontWeight.strong, 36 | digitalPrefix: ' ', 37 | pointPrefix: ' ', 38 | blockQuoteColor: Colors.teal, 39 | blockQuoteWidth: 5, 40 | blockQuotMargin: EdgeInsets.only(left: 10, right: 10), 41 | blockQuotTextMargin: EdgeInsets.only(left: 25), 42 | ), 43 | ).parseHtmlToWidgetList(this.data, onTapCallback: this.onTapCallback); 44 | 45 | return new Container( 46 | padding: const EdgeInsets.all(0.0), 47 | child: new Column( 48 | crossAxisAlignment: CrossAxisAlignment.start, 49 | children: nodes, 50 | )); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/html_async_textview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fluttertoast/fluttertoast.dart'; 3 | import 'package:html_text/html_parser_to_widget_list.dart'; 4 | import 'package:html_text/html_text_style.dart'; 5 | import 'package:html_text/on_tap_data.dart'; 6 | 7 | import 'image.dart'; 8 | 9 | class AsyncHtmlTextView { 10 | final String data; 11 | 12 | AsyncHtmlTextView(this.data, {EdgeInsetsGeometry padding}); 13 | 14 | final Function onTapCallback = (data) { 15 | if (data is OnTapData) { 16 | if (data.type == OnTapType.href) { 17 | Fluttertoast.showToast( 18 | msg: data.url, 19 | gravity: ToastGravity.CENTER, 20 | ); 21 | } else if (data.type == OnTapType.img) { 22 | Fluttertoast.showToast( 23 | msg: 24 | 'currentImageUrl: ${data.url} imageList: ${getArticleImageList(data.id).toString()}', 25 | gravity: ToastGravity.CENTER, 26 | ); 27 | } else if (data.type == OnTapType.video) { 28 | Fluttertoast.showToast( 29 | msg: data.url, 30 | gravity: ToastGravity.CENTER, 31 | ); 32 | } 33 | } 34 | }; 35 | 36 | build(Function onParserCallback, {List imageList, String id}) { 37 | HtmlParserToWidgetList( 38 | imagePadding: 39 | EdgeInsets.only(top: 2.0, left: 12.0, right: 12.0, bottom: 0.0), 40 | videoPadding: 41 | EdgeInsets.only(top: 4.0, left: 12.0, right: 12.0, bottom: 2.0), 42 | htmlTextStyle: HtmlTextStyle( 43 | height: 1.6, 44 | fontSize: 17, 45 | padding: 46 | EdgeInsets.only(top: 6.0, left: 12.0, right: 12.0, bottom: 8.0), 47 | digitalFontWeight: DigitalFontWeight.strong, 48 | digitalPrefix: ' ', 49 | pointPrefix: ' ', 50 | defaultTextColor: Color(0xff333333), 51 | letterSpacing: 1.0, 52 | ), 53 | isInPackage: false, 54 | ) 55 | .asyncParseHtmlToWidgetList(this.data, 56 | onTapCallback: this.onTapCallback, imageList: imageList, id: id) 57 | .then((nodes) { 58 | if (onParserCallback != null) { 59 | onParserCallback(nodes); 60 | } 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/html_parser_to_element_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:html/dom.dart' as dom; 2 | import 'package:html/parser.dart' show parse; 3 | 4 | class HtmlParserToElementList { 5 | HtmlParserToElementList(); 6 | 7 | Future > asyncParseHtmlToElementList(String html) async { 8 | return await _getElementListFromHtml(html); 9 | } 10 | 11 | Future
> _getElementListFromHtml(String html) async { 12 | return _parseHtmlToElementList(html); 13 | } 14 | 15 | List
_parseHtmlToElementList(String html) { 16 | return parse(html).body.children; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/html_parser_to_widget_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:html/dom.dart' as dom; 3 | import 'package:html/parser.dart' show parse; 4 | 5 | import 'constants.dart'; 6 | import 'flutter_html_text.dart'; 7 | import 'html_text_style.dart'; 8 | import 'network_image.dart'; 9 | import 'on_tap_data.dart'; 10 | import 'web_view_page.dart'; 11 | 12 | class HtmlParserToWidgetList { 13 | static const EdgeInsetsGeometry _defaultImagePadding = 14 | EdgeInsets.only(top: 4.0, left: 0.0, right: 0.0, bottom: 8.0); 15 | 16 | EdgeInsetsGeometry imagePadding; 17 | 18 | static const EdgeInsetsGeometry _defaultVideoPadding = 19 | EdgeInsets.only(top: 4.0, left: 0.0, right: 0.0, bottom: 8.0); 20 | 21 | EdgeInsetsGeometry videoPadding; 22 | 23 | HtmlTextStyle htmlTextStyle; 24 | 25 | bool isInPackage = true; 26 | bool loadingOffstage; 27 | 28 | HtmlParserToWidgetList( 29 | {this.imagePadding = _defaultImagePadding, 30 | this.videoPadding = _defaultVideoPadding, 31 | this.htmlTextStyle, 32 | this.isInPackage = true, 33 | this.loadingOffstage = true}); 34 | 35 | Future > asyncParseHtmlToWidgetList(String html, 36 | {Function onTapCallback, 37 | List
imageList, 38 | String id, 39 | BuildContext context}) async { 40 | return await _getWidgetListFromHtml(html, 41 | onTapCallback: onTapCallback, 42 | imageList: imageList, 43 | id: id, 44 | context: context); 45 | } 46 | 47 | Future > _getWidgetListFromHtml(String html, 48 | {Function onTapCallback, 49 | List
imageList, 50 | String id, 51 | BuildContext context}) async { 52 | return parseHtmlToWidgetList( 53 | html, 54 | onTapCallback: onTapCallback, 55 | imageList: imageList, 56 | id: id, 57 | context: context, 58 | ); 59 | } 60 | 61 | List parseHtmlToWidgetList(String html, 62 | {Function onTapCallback, 63 | List imageList, 64 | String id, 65 | BuildContext context}) { 66 | List widgetList = new List(); 67 | if (html == null) { 68 | widgetList.add(Container()); 69 | return widgetList; 70 | } 71 | List docBodyChildren = parse(html).body.children; 72 | if (docBodyChildren.length == 0) { 73 | widgetList.add(Container( 74 | padding: htmlTextStyle.padding, 75 | child: Text(html, 76 | style: TextStyle( 77 | height: htmlTextStyle.height, 78 | letterSpacing: htmlTextStyle.letterSpacing, 79 | fontSize: htmlTextStyle.fontSize)), 80 | )); 81 | } else { 82 | docBodyChildren.forEach((e) { 83 | if (e.outerHtml.contains(" widgetList, Function onTapCallback, 114 | {List
imageList, String id, BuildContext context}) { 115 | String outerHtml = e.outerHtml; 116 | 117 | var imgElements = e.getElementsByTagName("img"); 118 | if (imgElements.length == 0) { 119 | imgElements.add(e); 120 | } 121 | if (e.nodes.length > 1) { 122 | imgElements.forEach((f) { 123 | outerHtml = outerHtml.replaceAll( 124 | '${f.outerHtml}', ' ${f.outerHtml}$separator
'); 125 | }); 126 | } 127 | int imageIndex = 0; 128 | outerHtml.split(separator).forEach((html) { 129 | if (html.contains("
'.endsWith(tempHtml)) { 137 | widgetList.add(_createHtmlText(tempHtml, onTapCallback)); 138 | } 139 | } 140 | }); 141 | } 142 | 143 | void _createImage( 144 | String imageUrl, 145 | ListwidgetList, 146 | Function onTapCallback, { 147 | String id, 148 | BuildContext context, 149 | }) { 150 | widgetList.add(new GestureDetector( 151 | onTap: () { 152 | if (onTapCallback != null) { 153 | onTapCallback(OnTapData(imageUrl, 154 | type: OnTapType.img, id: id, context: context)); 155 | } 156 | }, 157 | onLongPress: () { 158 | if (onTapCallback != null) { 159 | onTapCallback(OnTapData(imageUrl, 160 | type: OnTapType.img, 161 | id: id, 162 | isOnLongPress: true, 163 | context: context)); 164 | } 165 | }, 166 | child: Center( 167 | child: Container( 168 | padding: imagePadding, 169 | child: NetworkImageClipper( 170 | imageUrl, 171 | id: id, 172 | isInPackage: isInPackage, 173 | loadingOffstage: loadingOffstage, 174 | ), 175 | ), 176 | ), 177 | )); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /lib/html_tags.dart: -------------------------------------------------------------------------------- 1 | final List emptyTags = const [ 2 | 'area', 3 | 'base', 4 | 'basefont', 5 | 'br', 6 | 'col', 7 | 'frame', 8 | 'hr', 9 | 'img', 10 | 'input', 11 | 'isindex', 12 | 'link', 13 | 'meta', 14 | 'param', 15 | 'embed' 16 | ]; 17 | 18 | final List blockTags = const [ 19 | 'address', 20 | 'applet', 21 | 'blockquote', 22 | 'button', 23 | 'center', 24 | 'dd', 25 | 'del', 26 | 'dir', 27 | 'div', 28 | 'dl', 29 | 'dt', 30 | 'fieldset', 31 | 'form', 32 | 'frameset', 33 | 'hr', 34 | 'iframe', 35 | 'ins', 36 | 'isindex', 37 | 'li', 38 | 'map', 39 | 'menu', 40 | 'noframes', 41 | 'noscript', 42 | 'object', 43 | 'ol', 44 | 'p', 45 | 'pre', 46 | 'script', 47 | 'table', 48 | 'tbody', 49 | 'td', 50 | 'tfoot', 51 | 'th', 52 | 'thead', 53 | 'tr', 54 | 'ul' 55 | ]; 56 | final List inlineTags = const [ 57 | 'a', 58 | 'abbr', 59 | 'acronym', 60 | 'applet', 61 | 'b', 62 | 'basefont', 63 | 'bdo', 64 | 'big', 65 | 'br', 66 | 'button', 67 | 'cite', 68 | 'code', 69 | 'del', 70 | 'dfn', 71 | 'em', 72 | 'font', 73 | 'i', 74 | 'iframe', 75 | 'img', 76 | 'input', 77 | 'ins', 78 | 'kbd', 79 | 'label', 80 | 'map', 81 | 'object', 82 | 'q', 83 | 's', 84 | 'samp', 85 | 'script', 86 | 'select', 87 | 'small', 88 | 'span', 89 | 'strike', 90 | 'strong', 91 | 'sub', 92 | 'sup', 93 | 'textarea', 94 | 'tt', 95 | 'u', 96 | 'var' 97 | ]; 98 | final List closeSelfTags = const [ 99 | 'colgroup', 100 | 'dd', 101 | 'dt', 102 | 'li', 103 | 'options', 104 | 'p', 105 | 'td', 106 | 'tfoot', 107 | 'th', 108 | 'thead', 109 | 'tr' 110 | ]; 111 | final List fillAttrs = const [ 112 | 'checked', 113 | 'compact', 114 | 'declare', 115 | 'defer', 116 | 'disabled', 117 | 'ismap', 118 | 'multiple', 119 | 'nohref', 120 | 'noresize', 121 | 'noshade', 122 | 'nowrap', 123 | 'readonly', 124 | 'selected' 125 | ]; 126 | final List specialTags = const ['script', 'style']; 127 | 128 | final String ulTag = ''; 129 | final String olTag = ''; 130 | 131 | final RegExp startTag = new RegExp( 132 | r'^<([-A-Za-z0-9_]+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")' + 133 | "|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>"); 134 | final RegExp endTag = new RegExp("^<\/([-A-Za-z0-9_]+)[^>]*>"); 135 | final RegExp attrTag = new RegExp( 136 | r'([-A-Za-z0-9_]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")' + 137 | r"|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?"); 138 | final RegExp styleTag = new RegExp(r'([a-zA-Z\-]+)\s*:\s*([^;]*)'); 139 | final RegExp colorTag = new RegExp(r'^#([a-fA-F0-9]{6})$'); 140 | -------------------------------------------------------------------------------- /lib/html_text_style.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/widgets.dart'; 4 | 5 | class HtmlTextStyle { 6 | ///字体大小 7 | double fontSize; 8 | 9 | /// a 链接 A linear decoration to draw near the text. 10 | TextDecoration hrefTextDecoration; 11 | 12 | ///a 链接字体颜色 13 | Color hrefTextColor; 14 | 15 | ///a 链接字体默认颜色 16 | static const Color defaultHrefTextColor = Color(0xFF5193ad); 17 | 18 | ///行高 19 | double height; 20 | 21 | ///字体颜色 22 | Color color; 23 | 24 | ///默认字体颜色 25 | Color defaultTextColor = Color(0xFF000000); 26 | 27 | ///默认字体大小 28 | double defaultFontSize = 14.0; 29 | 30 | ///段落padding 31 | EdgeInsetsGeometry padding; 32 | 33 | ///默认段落padding 34 | static const EdgeInsetsGeometry defaultPadding = 35 | EdgeInsets.only(top: 0.0, left: 0.0, right: 0.0, bottom: 4.0); 36 | 37 | ///数字标签大小 38 | double digitalFontSize = 14.0; 39 | 40 | ///点标签大小 41 | double pointFontSize = 14.0; 42 | 43 | ///点标签字体样式,默认普通样式 可以设置为 DigitalFontWeight.strong 44 | String digitalFontWeight; 45 | 46 | ///数字标签前缀 47 | String digitalPrefix; 48 | 49 | ///点标签前缀 50 | String pointPrefix; 51 | 52 | ///引用线条颜色 53 | Color blockQuoteColor; 54 | 55 | ///引用线条默认颜色 56 | static const Color defaultBlockQuoteColor = Color(0xFF2576a5); 57 | 58 | ///引用线条宽度 59 | double blockQuoteWidth; 60 | 61 | ///引用线条Margin 62 | EdgeInsetsGeometry blockQuotMargin; 63 | 64 | ///引用线条默认Margin 65 | static const EdgeInsetsGeometry defaultBlockQuotMargin = 66 | EdgeInsets.only(left: 3, right: 12); 67 | 68 | ///引用线条文字Margin 和 blockQuotMargin,blockQuoteWidth结合绘制线条和文字的相对位置 69 | EdgeInsetsGeometry blockQuotTextMargin; 70 | 71 | ///引用线条文字默认Margin 72 | static const EdgeInsetsGeometry defaultBlockQuotTextMargin = 73 | EdgeInsets.only(left: 15); 74 | 75 | /// 字符间距 就是单个字母或者汉字之间的间隔,可以是负数 76 | double letterSpacing; 77 | 78 | /// 字间距 句字之间的间距 79 | double wordSpacing; 80 | 81 | HtmlTextStyle({ 82 | this.fontSize = 14.0, 83 | this.hrefTextDecoration = TextDecoration.none, 84 | this.hrefTextColor = defaultHrefTextColor, 85 | this.height = 1.4, 86 | this.defaultTextColor, 87 | this.digitalFontSize = 14.0, 88 | this.pointFontSize = 14.0, 89 | this.digitalFontWeight, 90 | this.digitalPrefix, 91 | this.pointPrefix, 92 | this.blockQuoteColor = defaultBlockQuoteColor, 93 | this.blockQuoteWidth = 3, 94 | this.blockQuotMargin = defaultBlockQuotMargin, 95 | this.blockQuotTextMargin = defaultBlockQuotTextMargin, 96 | this.padding = defaultPadding, 97 | this.letterSpacing, 98 | this.wordSpacing, 99 | }); 100 | } 101 | 102 | class DigitalFontWeight { 103 | static const String strong = 'strong'; 104 | } 105 | -------------------------------------------------------------------------------- /lib/image.dart: -------------------------------------------------------------------------------- 1 | var articleImageMap = Map >(); 2 | 3 | var articleImageFilePathMap = Map >(); 4 | 5 | //清空 6 | void removeImageFromArticleImageMap(String articleId) { 7 | if (articleImageMap.containsKey(articleId)) { 8 | articleImageMap.remove(articleId); 9 | } 10 | } 11 | 12 | void addImageToArticleImageMap(String articleId, List imageList) { 13 | if (articleImageMap.containsKey(articleId)) { 14 | articleImageMap.update(articleId, (value) => value..addAll(imageList)); 15 | } else { 16 | articleImageMap.putIfAbsent(articleId, () => imageList); 17 | } 18 | } 19 | 20 | void insertImageToArticleImageMap( 21 | String articleId, List imageList, int index) { 22 | if (articleImageMap.containsKey(articleId)) { 23 | articleImageMap.update( 24 | articleId, (value) => value..insertAll(index, imageList)); 25 | } else { 26 | articleImageMap.putIfAbsent(articleId, () => imageList); 27 | } 28 | } 29 | 30 | List getArticleImageList(String articleId) { 31 | return articleImageMap.putIfAbsent(articleId, () => []); 32 | } 33 | 34 | void addImageToArticleImageFilePathMap( 35 | String articleId, String imageUrl, String filePath) { 36 | if (articleImageFilePathMap.containsKey(articleId)) { 37 | articleImageFilePathMap.update( 38 | articleId, (value) => value..putIfAbsent(imageUrl, () => filePath)); 39 | } else { 40 | articleImageFilePathMap.putIfAbsent(articleId, () => {imageUrl: filePath}); 41 | } 42 | } 43 | 44 | Map getArticleImageUrlFilePathList(String articleId) { 45 | return articleImageFilePathMap[articleId]; 46 | } 47 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'flutter_html_textview.dart'; 4 | import 'html_async_textview.dart'; 5 | import 'image.dart'; 6 | 7 | void main() => runApp(MyApp()); 8 | 9 | class MyApp extends StatelessWidget { 10 | @override 11 | Widget build(BuildContext context) { 12 | return MaterialApp( 13 | home: HtmlTest(), 14 | ); 15 | } 16 | } 17 | 18 | class HtmlTest extends StatefulWidget { 19 | @override 20 | State createState() { 21 | // TODO: implement createState 22 | return SingleChildScrollViewTestRouteState(); 23 | } 24 | } 25 | 26 | // ignore: must_be_immutable 27 | class SingleChildScrollViewTestRouteState extends State { 28 | String html = 29 | ' 维生素A报价继续向上 月涨幅超20%
A股上市公司中,新和成(002001)目前公司维生素A粉产能10000吨。浙江医药(600216)目前公司维生素A粉产能5000吨。
box constraints有人也翻译有人也翻译有人也翻译有人也翻译有人也翻译
为盒约束、箱约束
,我个人还是。
这样做的好处,了统一的渲染。加入样式,会让布局复杂不少,在渲染层面会降低很多性能。因此,Flutter在大的方向上,加入不同类型的布局widget。在小的方向上,只给出很少的定制化的东西,将布局限定在有限的范围内,在完成布局的同时,让整个渲染能够统一,加快了更新和渲染居中结束 居中
但是,缺点也是同样明显,少了很多灵活性,不同的布局方式都被抽离出了widget,大家需要了解的widget非常多,增加了学习成本。 居右
1.2 约束种类
布局空间。Flutter借鉴了很多React相关的东西,包括一些布局思想'; 30 | String htmlOl = 31 | '布局空间。Flutter借鉴了很多React
widget可以按照
条件,来决定自身如何
这样做的好处,我觉得可能是为了了统一的渲染。加入样式,会
'; 32 | String imageHtml = 33 | '这样做的好处,我觉得了统一的渲染加粗正常
只给出很少的定制化的东西,将布局限
定在有限的让布局复杂不少范围内
中间的文字
,在完成布局的同时,
让整个渲染能够统一,
加入视频开始
视频结束
'; 34 | String blockQuote = 35 | '引用开始
比特币(Bitcoin)的概念最初由中本聪在2008年11月1日提出,并于2009年1月3日正式诞生 [1] 。根据中本聪的思路设计发布的开源软件以及建构其上的P2P网络。比特币是一种P2P形式的虚拟的加密数字货币。点对点的传输意味着一个去中心化的支付系统。
与所有的货币不同,比特币不依靠特定货币机构发行,它依据特定算法,通过大量的计算产生,比特币经济使用整个P2P网络中众多节点构成的分布以确保无法通过大量制造比特币来人为操控币值。基于密码学的设计可以使比特币只能被真实的拥有者转移或支付。这同样确保了货币所有权与流通交易的匿名性。比特币与其他虚拟货币最大的不同,是其总数量非常
2017年12月17日,比特币达到历史最高价19850美元。
引用结束
在渲染层面会降低很多性能。因此,Flutter
'; 36 | 37 | ListcontentListWidget = []; 38 | 39 | List contentImageList = []; 40 | 41 | @override 42 | void initState() { 43 | super.initState(); 44 | AsyncHtmlTextView(imageHtml + html + htmlOl + blockQuote).build((nodes) { 45 | contentListWidget = nodes; 46 | addImageToArticleImageMap('12', contentImageList); 47 | setState(() { 48 | contentListWidget = nodes; 49 | }); 50 | }, imageList: contentImageList, id: '12'); 51 | } 52 | 53 | @override 54 | Widget build(BuildContext context) { 55 | return Scaffold( 56 | body: Scrollbar( 57 | child: SingleChildScrollView( 58 | padding: EdgeInsets.all(12.0), 59 | child: Column( 60 | crossAxisAlignment: CrossAxisAlignment.start, 61 | children: contentListWidget, 62 | ), 63 | ), 64 | )); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/network_image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui' as ui; 3 | import 'dart:ui'; 4 | 5 | import 'package:cached_network_image/cached_network_image.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 8 | 9 | import 'ImageClipper.dart'; 10 | import 'circle.dart'; 11 | import 'image.dart'; 12 | 13 | class NetworkImageClipper extends StatefulWidget { 14 | final String id; 15 | final String imageUrl; 16 | bool isInPackage = true; 17 | bool loadingOffstage; 18 | 19 | NetworkImageClipper(this.imageUrl, 20 | {this.id, this.isInPackage, this.loadingOffstage = true}); 21 | 22 | @override 23 | State createState() { 24 | return CachedImage(); 25 | } 26 | } 27 | 28 | class CachedImage extends State { 29 | ImageClipper clipper; 30 | 31 | CachedImage(); 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | clip(widget.imageUrl, context); 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | getImageSingleFilePath(widget.id, widget.imageUrl); 42 | 43 | return ImageClipperInstance().containsUrl(widget.imageUrl) 44 | ? clipper == null 45 | ? Container( 46 | width: MediaQuery.of(context).size.width, 47 | height: MediaQuery.of(context).size.height, 48 | ) 49 | : _createCustomPaint(context) 50 | : Hero( 51 | tag: widget.imageUrl, 52 | child: buildCachedNetworkImage(), 53 | placeholderBuilder: (context, heroSize, widget) => 54 | buildCachedNetworkImage(), 55 | ); 56 | } 57 | 58 | CachedNetworkImage buildCachedNetworkImage() { 59 | return CachedNetworkImage( 60 | placeholder: (context, url) => Stack( 61 | alignment: AlignmentDirectional.center, 62 | children: [ 63 | Image( 64 | image: AssetImage("assets/images/feed_cell_photo_default_big.png", 65 | package: getPackageName()), 66 | ), 67 | Offstage( 68 | offstage: widget.loadingOffstage, 69 | child: SpinKitCircle( 70 | color: Colors.blueAccent, 71 | ), 72 | ), 73 | ], 74 | ), 75 | errorWidget: (context, url, error) => Image( 76 | image: AssetImage("assets/images/feed_cell_photo_default_big.png", 77 | package: getPackageName()), 78 | ), 79 | imageUrl: widget.imageUrl, 80 | fit: BoxFit.cover, 81 | ); 82 | } 83 | 84 | String getPackageName() => widget.isInPackage ? 'html_text' : null; 85 | 86 | Future getImageSingleFilePath(String id, String imageUrl) async { 87 | return await DefaultCacheManager().getSingleFile(imageUrl).then((file) { 88 | addImageToArticleImageFilePathMap(id, '"$imageUrl"', '"${file?.path}"'); 89 | }); 90 | } 91 | 92 | Future _loadImage(String url) async { 93 | ImageStream imageStream = NetworkImage(url).resolve(ImageConfiguration()); 94 | Completer completer = Completer (); 95 | void imageListener(ImageInfo info, bool synchronousCall) { 96 | ui.Image image = info.image; 97 | completer.complete(image); 98 | } 99 | 100 | ImageStreamListener imageStreamListener = 101 | ImageStreamListener(imageListener); 102 | imageStream.removeListener(imageStreamListener); 103 | imageStream.addListener(imageStreamListener); 104 | return completer.future; 105 | } 106 | 107 | clip(String url, BuildContext context) async { 108 | ui.Image uiImage; 109 | _loadImage(url).then((image) { 110 | uiImage = image; 111 | }).whenComplete(() { 112 | if (2 * window.physicalSize.height < uiImage.height) { 113 | clipper = ImageClipper(uiImage, context); 114 | setState(() { 115 | ImageClipperInstance() 116 | .addUrl(widget.imageUrl, _createCustomPaint(context)); 117 | }); 118 | } else { 119 | clipper = null; 120 | } 121 | }); 122 | } 123 | 124 | Widget _createCustomPaint(BuildContext context) { 125 | return Stack( 126 | children: [ 127 | CustomPaint( 128 | painter: clipper, 129 | size: Size(MediaQuery.of(context).size.width, 130 | MediaQuery.of(context).size.height), 131 | ), 132 | Positioned( 133 | right: 1, 134 | bottom: 1, 135 | child: Image.asset( 136 | "assets/images/long_picture_icon.png", 137 | package: getPackageName(), 138 | ), 139 | ), 140 | ], 141 | ); 142 | } 143 | } 144 | 145 | class ImageClipperInstance { 146 | factory ImageClipperInstance() => _getInstance(); 147 | 148 | static ImageClipperInstance _instance; 149 | 150 | List clipperImageUrl; 151 | 152 | ImageClipperInstance._() { 153 | clipperImageUrl = List(); 154 | } 155 | 156 | static ImageClipperInstance _getInstance() { 157 | if (_instance == null) { 158 | _instance = ImageClipperInstance._(); 159 | } 160 | return _instance; 161 | } 162 | 163 | void addUrl(String url, Widget imageClipperWidget) { 164 | clipperImageUrl.add(url); 165 | } 166 | 167 | bool containsUrl(String url) { 168 | return clipperImageUrl.contains(url); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /lib/on_tap_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class OnTapData { 4 | OnTapData( 5 | this.url, { 6 | this.type = OnTapType.href, 7 | this.data, 8 | this.id, 9 | this.isOnLongPress = false, 10 | this.context, 11 | }); 12 | 13 | String url; 14 | 15 | OnTapType type; 16 | 17 | dynamic data; 18 | 19 | String id; 20 | 21 | bool isOnLongPress = false; 22 | 23 | BuildContext context; 24 | } 25 | 26 | enum OnTapType { 27 | href, 28 | img, 29 | video, 30 | } 31 | -------------------------------------------------------------------------------- /lib/web_view_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:webview_flutter/webview_flutter.dart'; 3 | 4 | import 'on_tap_data.dart'; 5 | 6 | class WebViewVideo extends StatelessWidget { 7 | final String url; 8 | final Function onTapCallback; 9 | 10 | WebViewVideo(this.url, {this.onTapCallback}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Container( 15 | margin: EdgeInsets.only(left: 12, right: 12, top: 12), 16 | height: MediaQuery.of(context).size.width * 9 / 16, 17 | child: Stack( 18 | alignment: Alignment.center, 19 | fit: StackFit.expand, 20 | children: [ 21 | WebView( 22 | initialUrl: url, 23 | javascriptMode: JavascriptMode.unrestricted, 24 | ), 25 | GestureDetector( 26 | onTap: () { 27 | onTapCallback(OnTapData(url, type: OnTapType.video)); 28 | }, 29 | child: Container( 30 | color: Color(0x00FFFFFF), 31 | ), 32 | ), 33 | Positioned( 34 | right: 12, 35 | bottom: 12, 36 | child: GestureDetector( 37 | onTap: () { 38 | if (onTapCallback != null) { 39 | onTapCallback(OnTapData(url, type: OnTapType.video)); 40 | } 41 | }, 42 | child: Image.asset("assets/images/fullscreen_enter.png"), 43 | ), 44 | ), 45 | ], 46 | )); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /preview/Screenshot_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/preview/Screenshot_1.jpg -------------------------------------------------------------------------------- /preview/Screenshot_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/preview/Screenshot_2.jpg -------------------------------------------------------------------------------- /preview/Screenshot_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/preview/Screenshot_3.jpg -------------------------------------------------------------------------------- /preview/Screenshot_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/preview/Screenshot_4.jpg -------------------------------------------------------------------------------- /preview/Screenshot_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/houzhenpu/flutter_html_text/2d81625ccc3f437483bfa079f0245a9cced9d6ff/preview/Screenshot_5.jpg -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: html_text 2 | description: A new Flutter package. 3 | version: 0.0.2 4 | author: 5 | homepage: https://github.com/houzhenpu/flutter_fullhtml_textview 6 | 7 | environment: 8 | sdk: ">=2.1.0 <3.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | webview_flutter: ^0.3.10+4 14 | cached_network_image: ^2.0.0-rc 15 | html: any 16 | fluttertoast: ^3.1.0 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | 22 | # For information on the generic Dart part of this file, see the 23 | # following page: https://dart.dev/tools/pub/pubspec 24 | 25 | # The following section is specific to Flutter. 26 | flutter: 27 | assets: 28 | - assets/images/image_error.png 29 | - assets/images/long_picture_icon.png 30 | - assets/images/feed_cell_photo_default_big.png 31 | 32 | # To add assets to your package, add an assets section, like this: 33 | # assets: 34 | # - images/a_dot_burr.jpeg 35 | # - images/a_dot_ham.jpeg 36 | # 37 | # For details regarding assets in packages, see 38 | # https://flutter.dev/assets-and-images/#from-packages 39 | # 40 | # An image asset can refer to one or more resolution-specific "variants", see 41 | # https://flutter.dev/assets-and-images/#resolution-aware. 42 | 43 | # To add custom fonts to your package, add a fonts section here, 44 | # in this "flutter" section. Each entry in this list should have a 45 | # "family" key with the font family name, and a "fonts" key with a 46 | # list giving the asset and other descriptors for the font. For 47 | # example: 48 | # fonts: 49 | # - family: Schyler 50 | # fonts: 51 | # - asset: fonts/Schyler-Regular.ttf 52 | # - asset: fonts/Schyler-Italic.ttf 53 | # style: italic 54 | # - family: Trajan Pro 55 | # fonts: 56 | # - asset: fonts/TrajanPro.ttf 57 | # - asset: fonts/TrajanPro_Bold.ttf 58 | # weight: 700 59 | # 60 | # For details regarding fonts in packages, see 61 | # https://flutter.dev/custom-fonts/#from-packages 62 | --------------------------------------------------------------------------------