当前位置: 移动技术网 > IT编程>移动开发>Android > Flutter实现webview与原生组件组合滑动的示例代码

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

2019年07月23日  | 移动技术网IT编程  | 我要评论

风景图片素材,千纤草官网,彭春平博客

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

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

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

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

  1. flutter_webview_plugin : 不可以inline;
  2. webview_flutter : 可能支持, 但是还没有发布;
  3. flutter_inappbrowser : 可以实现组合布局, 所以选用了此库, 链接

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




初步实现组合布局

选定 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;
}

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

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网