首页 > 系统 > Android > 正文

Flutter实现webview与原生组件组合滑动的示例代码

2019-12-12 00:15:45
字体:
来源:转载
供稿:网友

最近在用Flutter写一个新闻客户端, 新闻详情页中的内容 需要用Flutter的本地Widget和WebView共同展示 . 比如标题/上方的视频播放器是用本地Widget展示, 新闻内容的富文本文字使用webview展示html, 这样就要求标题/视频播放器与webview可以 组合滑动 .

ps: 如果把新闻详情页都用html画出, 就不用考虑组合滑动的问题.

找到支持与本地组件共存的webview控件

找一个可以与本地组件共存的webview控件是首要任务, 以下是我测试过的几个库:

  1. flutter_WebView_plugin : 不可以inline;
  2. webView_flutter : 可能支持, 但是还没有发布;
  3. flutter_inappbrowser : 可以实现组合布局, 所以选用了此库, 链接 https://github.com/pichillilorenzo/flutter_inappbrowser

另外, 如果仅是展示html静态页面, 可以尝试以下几个库, 不用看我这个麻烦的解决办法了:

html
flutter_html
flutter_html_view

初步实现组合布局

选定 flutter_inappbrowser 后开始实现, 初步代码如下:

@override Widget build(BuildContext context) {  return Scaffold(   appBar: AppBar(),   body: Column(    children: <Widget>[     Text('Title'),     Expanded( // 注意必须加这个, 否则webview没有高度      child: InAppWebView(initialUrl: 'https://juejin.im/timeline'),     ),    ],   ),  ); }

这样会构建一个text和webview组合的界面, 不过这里webview自带滚动条, 滚动时是不带着title一块的. 尝试以下两种办法

包裹 SingleChildScrollView : 界面会消失不见, 因为Scrollview根据子布局处理高度, 而Expanded又要根据父布局处理高度, 所以互相依赖导致整个页面无法绘制.

body: SingleChildScrollView(    child: Column(     children: <Widget>[      Text('Title'),      Expanded(       child: InAppWebView(initialUrl: 'https://juejin.im/timeline'),      ),     ],    ),   ),

包裹 SingleChildScrollView , 去掉 Expanded : AppBar可以显示了, 但是 InAppWebView 没有高度了.

body: SingleChildScrollView(    child: Column(     children: <Widget>[      Text('Title'),      InAppWebView(initialUrl: 'https://juejin.im/timeline'),     ],    ),   ),

这两种方式都不行, 归根到底是不知道 InAppWebView 的高度, 所以才需要使用与 SingleChildScrollView 相冲突的 Expanded , 所以这个问题变为了 如何获取WebView的高度 .

获取WebView的高度

在android中不会有这个破问题, 给 webview 设置 wrap_content 就可以了, 但是在Flutter中我没有找到类似布局方式. (有大哥知道的话麻烦告诉我一下下啊)

其他尝试的方法就不说了, 最后我采用的办法是: 通过JS注入拿到html内容的高度回调 . 实现方法如下:

class TestState extends State<Test> { InAppWebViewController _controller; double _htmlHeight = 200; // 目的是在回调完成直接先展示出200高度的内容, 提高用户体验 static const String HANDLER_NAME = 'InAppWebView'; @override void dispose() {  super.dispose();  _controller?.removeJavaScriptHandler(HANDLER_NAME, 0);  _controller = null; } @override Widget build(BuildContext context) {  return Scaffold(   appBar: AppBar(),   body: SingleChildScrollView(    child: Column(     children: <Widget>[      Text('Title'),      Container( // 使用可提供高度的Container包裹WebView, 设置为回调的高度       height: _htmlHeight,       child: InAppWebView(        initialUrl: 'https://juejin.im/timeline',        onWebViewCreated: (InAppWebViewController controller) {         _controller = controller;         _setJSHandler(_controller); // 设置js方法回掉, 拿到高度        },        onLoadStop: (InAppWebViewController controller, String url) {         // 页面加载完成后注入js方法, 获取页面总高度           controller.injectScriptCode("""         window.flutter_inappbrowser.callHandler('InAppWebView', document.body.scrollHeight));        """);        },       ),      )     ],    ),   ),  ); } void _setJSHandler(InAppWebViewController controller) {  JavaScriptHandlerCallback callback = (List<dynamic> arguments) async {   // 解析argument, 获取到高度, 直接设置即可(iphone手机需要+20高度)   double height = HtmlUtils.getHeight(arguments);   if (height > 0) {    setState(() {     _htmlHeight = height;    });   }  };  controller.addJavaScriptHandler(HANDLER_NAME, callback); }}

以上方法可以精确获取到webview高度, 实现webview与本地Widget组合滑动的要求.

Android端一个问题

以上方法实现后我是一阵窃喜, 赶忙测试了一下, 结果发现一个严重问题: Android端给webview设置超出5500左右的高度时, App会闪退 . 闪退时AndroidStudio不会展示错误日志, 通过 flutter run --verbose 命令运行可以获取到错误信息, 大体看了下是Flutter渲染的问题, 先反馈给官方以及 flutter_inappbrowser 作者了.

然后自己简单测试发现, 给Column的child添加了多个webview没什么问题, 哪怕这几个webview的内容相加绝对超出了5500高度. 所以有了思路: 切分html, 分为多个webview共同展示, 然后分别注入JS获取高度 .

注意!注意! 我们的使用场景是: 要展示的内容 = assets存储的html外壳 + 接口获取到的新闻内容段落, 而不是一个url . 以上解决思路仅适用于加载html的场景, 而不是url.

这个思路的核心在于如何切分html内容, 需要保证切分后的html是标签闭合的, 即不是切在了某标签内部. 使用此切分方案的前提是: body内部的html标签不会有超大范围的div包裹, 否则单个标签内容就超过高度了. 可用的html示例:

<html> <head></head>  <body>    <!-- 并列小组合, 没有超大范围的div等标签的包裹 -->    <p style.. > asdasdasd </p>    <div style.. >       <img ... />      <p> ... </p>    </div>     <p> asdasdas </p>  </body></html>

下面是我实现的切分html的算法:

// 剪切过长的html, 考虑到较差机型以及其他误差, 定为4000 // @params htmlString 待切分的html // @params totalHeight 前面webview回调出的总高度 // @return String 剪切后的html static List<String> cutHtml(String htmlString, double totalHeight) {  htmlString = _getBody(htmlString);  List<String> htmlList = List();  if (Platform.isAndroid && totalHeight > 4000) {   // 切为几段('~/'整除, /.toInt)   int childNum = totalHeight ~/ 4000 + (totalHeight % 4000 == 0 ? 0 : 1);   // 每段html的长度   int childLength = htmlString.length ~/ childNum;   // 切一刀后的两段html   String resultHtml = '', remainHtml = htmlString;   int labelStack = 0;   while (childNum > 0 && remainHtml.length > 0) {    if (childLength < remainHtml.length) {     resultHtml = remainHtml.substring(0, childLength);     remainHtml = remainHtml.substring(childLength);    } else {     resultHtml = remainHtml;     remainHtml = '';    }    if (_checkComplete(resultHtml, labelStack)) {     htmlList.add(resultHtml);     childNum--;    } else {     // 如果不是闭合的, 把remain里的n个标签尾之前的内容剪切到result中     while (labelStack != 0) {      int tailPosition = remainHtml.indexOf(_labelsTail);      if (tailPosition != -1) {       resultHtml = resultHtml + remainHtml.substring(0, tailPosition + 2);       remainHtml = remainHtml.substring(tailPosition + 2);       labelStack--;      }     }     htmlList.add(resultHtml);     childNum--;    }   }  } else {   htmlList.add(htmlString);  }  return htmlList; } // true if resultHtml是标签闭合的 static bool _checkComplete(String resultHtml, int labelStack) {  labelStack = 0;  for (int i = 0; i < resultHtml.length; i++) {   if (resultHtml.startsWith('<', i)) {    String label = _startWithLabel(resultHtml.substring(i));    if (label != null) {     labelStack++;     i += label.length - 1;    }   }   if (resultHtml.startsWith(_labelsTail, i)) {    labelStack--;    i += _labelsTail.length - 1;   }  }  return labelStack == 0; } // 以_labelsHead内的字符串开头 static String _startWithLabel(String resultHtml) {  for (String label in _labelsHead) {   if (resultHtml.startsWith(label)) {    return label;   }  }  return null; } // 去除body及以外的标签, 露出并列的子标签 // <html> //  <head></head> //   <body> //   ... //   </body> // </html> static String _getBody(String htmlString) {  if (htmlString.contains('<body>')) {   htmlString = htmlString.substring(htmlString.indexOf('<body>') + 6);   htmlString = htmlString.substring(0, htmlString.indexOf('</body>'));  }  return htmlString; } // 待检测的标签 static final _labelsHead = {'<div', '<img', '<p', '<strong', '<span'}; static final _labelsTail = '</';

通过以上算法, 拿到了切分好的htmlList, 然后在PageState中使用多个webview分别加载, 分别注入js即可解决此问题.

大功告成!

附:

flutter_inappbrowser 如何加载html字符串:

InAppWebView( initialData: InAppWebViewInitialData(' htmlContent '))

解析asset文件为字符串:

static Future<String> decodeStringFromAssets(String path) async {  ByteData byteData = await PlatformAssetBundle().load(path);  String htmlString = String.fromCharCodes(byteData.buffer.asUint8List());  return htmlString;}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持武林网。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表