现在的新闻阅读界面一般都包含两部分,新闻文章内容和用户评论列表。内容一般都是HTML富文本数据,以便实现丰富的展示风格。现在Flutter原生没有支持的HTML富文本展示,也没有提供类似的于WebView的Widget。只能使用插件方式调用原生的WebView。
详情页逻辑流程:
网络请求新闻数据------组装成标准的HTML(一般是静态的)-------浏览器加载页面
我网络请求返回数据组装后的HTML纯文本数据
<html>
<head>
<meta name="generator"
content="HTML Tidy for HTML5 (experimental) for Windows https://github.com/w3c/tidy-html5/tree/c63cc39" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<link rel="stylesheet" type="text/css" href="main.css" />
<title></title>
</head>
<body>
<section data-role="outer" label="Powered by 135editor.com" style="font-size: 16px; font-family: 微软雅黑;">
<section data-role="outer" label="Powered by 135editor.com">
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-indent: 0em; text-align: center;">
<img imageflag=""
src="https://cdnproduce.yunshicloud.com/wenshan/QMTNRK_YUNSHI/F41EF6C10FA3408DB377A13D5A01466F/5e7b22b63e73672e423375d50cb694a5.jpg"
style="width: 100%;" />
</p>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-align: justify; text-indent: 2em;">
<span style="font-size: 17px;">连日来,为维护消费者合法权益,保护人民群众猪肉食品安全,文山市市场监督管理局组织执法人员开展辖区内生猪肉市场、酒店餐桌专项检查活动,保障辖区内生猪肉市场的安全。</span>
</p>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-indent: 0em; text-align: center;">
<img alt="" height="1080" src="/sites/default/files/news/087659cfb4f94ee074a35c3bcdea9999.jpg" width="1440" />
</p>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-align: justify; text-indent: 2em;">
<span style="font-size: 17px;">此次检查的重点是农贸市场和各大酒店宾馆,在诗达酒店,作为一家四星级酒店,餐桌食品安全备受关注,检查人员先后检查了该酒店的生产操作间、储存仓库及各类食品进货台账,摸清了猪肉进货渠道和销售数量,并结合《食品安全法》《生猪屠宰管理条例》等相关法律法规,进一步加强宣传,引导广大经营业者守法经营。督促其建立健全进销货台账、进货检验、索证索票、质量承诺,落实猪肉来源溯源制度,保证猪肉合格,同时和酒店签订了文山市餐饮行业非洲猪瘟防控工作承诺书。</span>
</p>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-indent: 0em; text-align: center;">
</p>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-indent: 0em; text-align: center;">
<span style="font-size: 17px;">
<img imageflag=""
src="https://cdnproduce.yunshicloud.com/wenshan/QMTNRK_YUNSHI/F41EF6C10FA3408DB377A13D5A01466F/92fe6ad88b8b78efc8af05fe1b4d48c1.jpg"
style="width: 100%;" />
<img imageflag=""
src="https://cdnproduce.yunshicloud.com/wenshan/QMTNRK_YUNSHI/F41EF6C10FA3408DB377A13D5A01466F/3eca8da600b80b9b45db45e3d2c46146.jpg"
style="text-indent: 0em; caret-color: red; width: 100%;" /> </span>
</p>
<section class="_135editor" data-id="88403" data-tools="135编辑器" style="border: 0px none; box-sizing: border-box;">
<section>
<section style="margin-top: 0.5em; margin-bottom: 0.5em; position: static;">
<section style="border-top: 3px dashed rgb(147, 122, 122); box-sizing: border-box;"></section>
</section>
<section style="margin-top: 20px; margin-bottom: 20px; position: static;">
<p style="margin-top: 5px; margin-bottom: 5px; font-size: 15px; line-height: 1.75em; font-family: Arial, sans-serif; color: rgb(120, 114, 114); text-align: justify; text-indent: 2em;">
<strong>
<span style="font-size: 17px;">文山诗达酒店 营运总监 罗桂星:</span>
</strong>
<span style="font-size: 17px;">“我们酒店食品安全这一块也是经常有计划有步骤的进行计划性的培训。除此之外,我们餐饮部门之间也会经常在班天会上进行培训,特别是对于我们产品的进入,也是控制得很严。我们专门的采购部门,对食品的采购进行整个流程的一个控制。从采买的索证索票以及到我们酒店大门的运货口,我们验收的每一个环节,也是严控,还有我们的成品与半成品都会分开管理和分开储存。”</span></p>
<p style="margin-top: 5px; margin-bottom: 5px; font-size: 15px; line-height: 1.75em; font-family: Arial, sans-serif; color: rgb(120, 114, 114); text-indent: 0em; text-align: center;">
<span style="font-size: 17px;">
<img imageflag=""
src="https://cdnproduce.yunshicloud.com/wenshan/QMTNRK_YUNSHI/F41EF6C10FA3408DB377A13D5A01466F/4fdd642bb1e1abc5dfdcb3b95ba54207.jpg"
style="width: 100%;" />
</span>
</p>
</section>
<section style="margin-top: 0.5em; margin-bottom: 0.5em; position: static;">
<section style="border-top: 5px dashed rgb(147, 122, 122); box-sizing: border-box;"></section>
</section>
</section>
</section>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-align: justify; text-indent: 2em;">
</p>
<section class="_135editor" data-id="89227" data-tools="135编辑器" style="border: 0px none; box-sizing: border-box;">
<section data-width="100%"
style="margin: 10px auto; width: 100%; background-color: rgb(251, 251, 251); border-bottom: 1px solid rgb(246, 246, 246); overflow: hidden; box-sizing: border-box;">
<section style="display: inline-block; width: auto; background-color: rgb(255, 166, 0); border-radius: 0px 0px 10px; margin-right: 60px; margin-left: -30px; padding-left: 30px;transform: skew(-30deg);-webkit-transform: skew(-30deg);-moz-transform: skew(-30deg);-o-transform: skew(-30deg);">
<section class="135brush" data-brushtype="text"
style="padding: 8px 30px 8px 20px; color: rgb(255, 255, 255);transform: skew(30deg);-webkit-transform: skew(30deg);-moz-transform: skew(30deg);-o-transform: skew(30deg);">
<strong>
<span style="font-size: 17px;">采访中,酒店还承诺:</span>
</strong>
</section>
</section>
</section>
</section>
<section class="_135editor" data-id="91614" data-tools="135编辑器" style="border: 0px none; box-sizing: border-box;">
<section class="_135editor" style="border: 0px none; box-sizing: border-box;">
<section style="padding: 10px; box-sizing: border-box;">
<section style="color: rgb(133, 133, 133); line-height: 30px; border: 2px solid rgb(136, 136, 136); box-sizing: border-box;">
<section style="display: -webkit-flex; justify-content: center; align-items: center;">
<section class="135brush" data-brushtype="text" data-width="65%"
style="width: 65%; line-height: 30px; font-size: 22px; color: rgb(220, 54, 74); padding-left: 10px; box-sizing: border-box;">
</section>
<section data-width="35%" style="width: 35%;">
<section style="width: 115px; float: right; margin-right: -4px; margin-top: -23px;"></section>
</section>
</section>
<section class="135brush"
style="padding-right: 15px; padding-bottom: 30px; padding-left: 15px; text-align: justify; box-sizing: border-box;">
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-indent: 2em;">
<span style="font-size: 17px;">一、严格按照云南省非洲猪瘟防控工作会议要求,不使用餐厨剩余物、废弃物(潲水)饲喂生猪,不向生猪养猪场和养猪户出售餐厨剩余物、废弃物(潲水)。</span>
</p>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-indent: 2em;">
<span style="font-size: 17px;">二、采购猪肉向供货方索取《生猪检验检疫合格证明》,坚决不购买未经检验检疫的猪肉。</span>
</p>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-indent: 2em;">
<span style="font-size: 17px;">三、保持食品加工、经营场所内外的卫生清洁,确保无卫生死角,无蟑螂、老鼠、苍蝇。</span>
</p>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-indent: 2em;">
<span style="font-size: 17px;">四、认真履行食品安全主任责任,严把食品安全质量关。</span>
</p>
</section>
<section data-width="100%" style="width: 100%;">
<section style="width: 80px; height: 15px; float: right; border-bottom: 2px solid rgb(136, 136, 136); border-right: 2px solid rgb(136, 136, 136); margin-top: -19px; margin-right: 4px; box-sizing: border-box;">
</section>
</section>
</section>
</section>
</section>
</section>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-align: justify; text-indent: 2em;">
<span style="font-size: 17px;">在文山城区文新农贸市场,检查人员详细询问经营户生猪肉来源,检查是否具有相关对应的票据,所售猪肉是否经过卫生部门检疫。严禁露天经营,确保猪肉市场消费安全。并要求经营者在销售过程中做好防鼠防蝇措施,让消费者放心吃肉,吃到放心肉。</span>
</p>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-indent: 0em; text-align: center;">
<span style="font-size: 17px;">
<img imageflag=""
src="https://cdnproduce.yunshicloud.com/wenshan/QMTNRK_YUNSHI/F41EF6C10FA3408DB377A13D5A01466F/bd3b9a79d4cfb19cea065a0dd9ef5acf.jpg"
style="width: 100%;" />
</span>
</p>
<section class="_135editor" data-id="88403" data-tools="135编辑器" style="border: 0px none; box-sizing: border-box;">
<section>
<section style="margin-top: 0.5em; margin-bottom: 0.5em; position: static;">
<section style="border-top: 3px dashed rgb(147, 122, 122); box-sizing: border-box;"></section>
</section>
<section style="margin-top: 20px; margin-bottom: 20px; position: static;">
<p style="margin-top: 5px; margin-bottom: 5px; font-size: 15px; line-height: 1.75em; font-family: Arial, sans-serif; color: rgb(120, 114, 114); text-align: justify; text-indent: 2em;">
<strong>
<span style="font-size: 17px;">市民:</span>
</strong>
<span style="font-size: 17px;">“我们买菜一般还是喜欢来市场买,都不喜欢在外面买,怕买到不好的。”</span></p>
<p style="margin-top: 5px; margin-bottom: 5px; font-size: 15px; line-height: 1.75em; font-family: Arial, sans-serif; color: rgb(120, 114, 114); text-indent: 0em; text-align: center;">
<span style="font-size: 17px;">
<img imageflag=""
src="https://cdnproduce.yunshicloud.com/wenshan/QMTNRK_YUNSHI/F41EF6C10FA3408DB377A13D5A01466F/cfb5635f567d5154324a55aa9a38d868.jpg"
style="width: 100%;" />
</span>
</p>
</section>
<section style="margin-top: 0.5em; margin-bottom: 0.5em; position: static;">
<section style="border-top: 5px dashed rgb(147, 122, 122); box-sizing: border-box;"></section>
</section>
</section>
</section>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-indent: 2em; text-align: justify;">
<span style="font-size: 17px; text-indent: 2em; caret-color: red;">在检查过程中,执法人员不仅向生猪肉经营者、酒店管理者开展了食品安全法律法规宣传,引导经营者增强主体责任意识,从自我做起。同时向周边消费者宣传食品安全法,告知消费者购买生猪肉时要检查猪肉是否盖有防疫部门的检疫章,出现问题及时向食品药品监管部门反映,共同维护生猪肉消费市场,维护人民群众舌尖上的安全。检查当天未发现不合格猪肉以及其他问题。</span>
</p>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-align: justify; text-indent: 2em;">
</p>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-align: justify; text-indent: 2em;">
<span style="font-size: 17px;">本台:周 秋 张 麟 实习:杨仁洁</span>
</p>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-align: justify; text-indent: 2em;">
<span style="font-size: 17px;">编辑:张海蓝 刘虹</span>
</p>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-align: justify; text-indent: 2em;">
<span style="font-size: 17px;">制作:冯明兰 农锦庄</span>
</p>
<p style="margin-top: 5px; margin-bottom: 5px; line-height: 1.75em; font-family: Arial, sans-serif; text-align: justify; text-indent: 2em;">
<span style="font-size: 17px;">责任编辑:胡润</span>
</p>
</section>
</section>
</body>
</html>
详情页网页加载:
- 想法1:
Dart可以开发web开发,把组装的静态HTML标签转换成Flutter的对应的Widget(翻译一遍)
当前也有一些现成的库:例如:flutter_html;flutter_widget_from_html_core
我均尝试使用了一番,发现对于style和背景较多的网页不能美观的展示。
优点是全程可控,绕开了WebView加载时的滑动冲突,但缺点也十分明显,工作量大(就像自己实现了一个浏览器,兼容性还十分不好的浏览器),展示的页面效果没有WebView效果好。
- 想法2:使用浏览器加载,使用原生WebView加载网页。现在也有较好的WebView插件。主要尝试用了一下flutter_webview_plugin和flutter_inappbrowser;
flutter_webview_plugin: 主要用来打开网站,非常方便,但需要指定相对于屏幕的固定的显示区域。Rect。而在开发的时候往往不好计算得到十分精确的区域。
flutter_inappbrowser:应该算着flutter_webview_plugin的改进版。 支持内嵌入页面。也需要固定的widget宽高。
由于客户要求比较高,和大量的兼容性考虑,还是决定WebView加载网页。当前的WebView插件都是使用HTML的URL方式加载网页。因此我们还需要把数据保存为文件,在使用WebView去加载本地网页。
流程变更为:
网络请求新闻数据------组装成标准的HTML(一般是静态的)------- 写入本地设备html(包括通用的css 文件和 html文件)----- WebView加载html
文件读写
1.保存的文件的位置,使用path_provider插件,可以实现跨平台文件路劲的获取。
Future<File> _getLocalCssFile() async {
// 获取本地文档目录
String dir = (await getApplicationDocumentsDirectory()).path;
// 返回本地文件目录
return new File('$dir/$fileCssName');
}
Future<File> _getLocalHtmlFile() async {
// 获取本地文档目录
String dir = (await getApplicationDocumentsDirectory()).path;
// 返回本地文件目录
return new File('$dir/$fileName');
}
2.写入文件
先写入通用的CSS文件,通用的Css源文件使用assets方式存放。当时苦于不知道flutter怎么获取asset的URI地址。因此又使用IO操作重新另存一遍。
assets配置
assets:
- htmlsource/css/main.css
另存为代码:(注意:这里进行了防止重复IO操作代码,但要注意排除修改css文件内容情况,当前没有实现这个升级修改Css内容的维护)
void _checkCssFile() async {
File file = await _getLocalCssFile();
bool isExist = await file.exists();
int fileLength = isExist ? await file.length() : -1;
print('csss file length === $fileLength');
if (!isExist || fileLength <= 0) {
if (isExist) {
await file.delete();
}
await file.create();
String cssStr = await DefaultAssetBundle.of(context)
.loadString('htmlsource/css/main.css');
print('csss ==== $cssStr');
await file.writeAsString(cssStr);
}
}
写入Html文件:并使用URL更新WebView
void _writeDataFile(String data) async {
_checkCssFile();
File file = await _getLocalHtmlFile();
File afterFile = await file.writeAsString(data);
setState(() {
_webUrl = afterFile.uri.toString();
});
print('weburl ==== $_webUrl');
}
加载HTML完整代码:
import 'dart:io';
import 'package:demonewsapp/page/native_web_view.dart';
import 'package:demonewsapp/page/webview.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart' as html;
import 'package:path_provider/path_provider.dart';
class NewsDetailsWeb extends StatefulWidget {
String body;
List<Widget> widgets;
NewsDetailsWeb(
{Key key, @required String this.body, List<Widget> this.widgets})
: super(key: key);
@override
NewsDetailsWebState createState() {
return NewsDetailsWebState();
}
}
class NewsDetailsWebState extends State<NewsDetailsWeb> {
final String fileName = 'wenshan_details.html';
final String fileCssName = 'wenshan_details_css.css';
String _webUrl = '';
double top = 156.899;
@override
void initState() {
super.initState();
_createHtmlContent();
}
void _createHtmlContent() async {
String cssUrl = (await _getLocalCssFile()).uri.toString();
String cssHead =
'''<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"/>
<link rel="stylesheet" type="text/css" href="${cssUrl}" />''';
String newHtml = cssHead + widget.body;
dom.Document doc = html.parse(newHtml);
String htmlContent = doc.outerHtml;
print('htmlContent === $htmlContent');
_writeDataFile(htmlContent);
}
void _checkCssFile() async {
File file = await _getLocalCssFile();
bool isExist = await file.exists();
int fileLength = isExist ? await file.length() : -1;
print('csss file length === $fileLength');
if (!isExist || fileLength <= 0) {
if (isExist) {
await file.delete();
}
await file.create();
String cssStr = await DefaultAssetBundle.of(context)
.loadString('htmlsource/css/main.css');
print('csss ==== $cssStr');
await file.writeAsString(cssStr);
}
}
void _writeDataFile(String data) async {
_checkCssFile();
File file = await _getLocalHtmlFile();
File afterFile = await file.writeAsString(data);
setState(() {
_webUrl = afterFile.uri.toString();
});
print('weburl ==== $_webUrl');
}
Future<File> _getLocalCssFile() async {
// 获取本地文档目录
String dir = (await getApplicationDocumentsDirectory()).path;
// 返回本地文件目录
return new File('$dir/$fileCssName');
}
Future<File> _getLocalHtmlFile() async {
// 获取本地文档目录
String dir = (await getApplicationDocumentsDirectory()).path;
// 返回本地文件目录
return new File('$dir/$fileName');
}
@override
Widget build(BuildContext context) {
return getNativeWeb();
}
Widget getNativeWeb() {
return _webUrl.isNotEmpty
? NativeWebView(
webUrl: _webUrl,
webRect: Rect.fromLTWH(0.0, 0.0, MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height - AppBar().preferredSize.height
-MediaQuery.of(context).padding.top),
)
: new Container(
height: 300.0,
color: Colors.yellow,
);
}
Widget getRectSizeWeb() {
return _webUrl.isNotEmpty
? new WebViewWidget(
url: _webUrl,
webRect: Rect.fromLTWH(0.0, top, MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height - top),
withZoom: false,
withLocalStorage: true,
scrollBar: false,
)
: new Container(
height: 300.0,
color: Colors.yellow,
);
}
Widget getHtmlView() {
return Container(
child: Html(
data: widget.body,
padding: EdgeInsets.all(8.0),
),
);
}
}
InAppWebView 网页加载的代码:
import 'package:flutter/material.dart';
import 'package:flutter_inappbrowser/flutter_inappbrowser.dart';
class NativeWebView extends StatelessWidget {
String webUrl;
final Rect webRect;
InAppWebViewController webView;
NativeWebView({Key key, this.webUrl, this.webRect}) : super(key: key);
@override
Widget build(BuildContext context) {
InAppWebView webWidget = new InAppWebView(
initialUrl: webUrl,
initialHeaders: {},
initialOptions: {},
onWebViewCreated: (InAppWebViewController controller) {
webView = controller;
},
onLoadStart: (InAppWebViewController controller, String url) {
print("started -------------- $url");
this.webUrl = url;
},
onProgressChanged: (InAppWebViewController controller, int progress) {
double prog = progress / 100;
print('prog --------- $prog');
});
return Container(
width: webRect.width,
height: webRect.height,
child: webWidget,
);
}
}
效果: