最近在用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;
}

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

Flutter实现webview与原生组件组合滑动的示例代码的更多相关文章

  1. 详解如何通过H5(浏览器/WebView/其他)唤起本地app

    这篇文章主要介绍了详解如何通过H5(浏览器/WebView/其他)唤起本地app的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. HTML5页面无缝闪开的问题及解决方案

    这篇文章主要介绍了HTML5页面无缝闪开方案,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  3. ios – 在WKWebView中获取链接URL

    我想在WKWebView中获取tapped链接的url.链接采用自定义格式,可触发应用中的某些操作.例如HTTP://我的网站/帮助#深层链接对讲.我这样使用KVO:这在第一次点击链接时效果很好.但是,如果我连续两次点击相同的链接,它将不报告链接点击.是否有解决方法来解决这个问题,以便我可以检测每个点击并获取链接?任何关于这个的指针都会很棒!解决方法像这样更改addobserver在observeValue函数中,您可以获得两个值

  4. ios – 永远不会调用shouldStartLoadWithRequest

    我已经研究和研究,但仍然不明白为什么从未调用过StartLoadWithRequest.我的页面加载正常,并调用了一些UIWebview委托协议方法.请从以下代码中找到相关的摘要:在我的.m中合成我的webview(在头文件中定义):我成功加载了我的webview:把我的代表设置为自己我的所有协议方法都被称为EXCEPTshouldStartLoadWithRequest提前致谢.解决方法尝试在V

  5. ios – UIWebView不适合设备屏幕

    我有一个网页视图,我想填写iDevice的全屏.我把它放在视图中心设置为中心并与容器边缘齐平.然而,当我加载应用程序时,视图比它运行的模拟iPhone大.我做了一些搜索,一些建议自动布局,这已经应该是视图的中心.我发现的另一件事是通过代码设置大小.我甚至将应用程序从通用应用程序更改为iPhone,对布局没有影响.完整来源:解决方法设置缩放以适合视图边界.试试这个:希望这可以帮助..:)

  6. ios – 如何在Swift中手动为UIWebView设置Cookie

    我需要在swift中为webview设置一个cookie.我找到了一个解决方案,但它是针对objective-c的.如何在Swift中做到这一点?

  7. iOS – 防止UIAppearance设置更改UIWebView输入附件视图

    我的应用程序中有一个UIWebView,问题是我有一个UIAppearance来修改分段控件的外观,因此它修改了UIWebView文本字段的输入附件视图中的分段控件,我希望它看起来不错,或者不试图修改它.这就是它目前的样子:解决方法我只是通过使用[UIAppearanceappearanceWhenContainedIn:]而不是[UIAppearance外观]来解决这个问题.通过向选择器发送包含

  8. ios – 如何为UIWebView设置自定义键盘

    如何为iOS7及更高版本的可编辑UIWebView显示自定义键盘和输入附件视图.即:如何设置和使用[UIWebViewsetInputView:]和[UIWebViewsetInputAccessoryView:]中的值解决方法理解你想要做什么会有所帮助?您可以尝试强制使用特定类型的键盘,例如数字,电子邮件等,这些键盘将使用用户在其设备(或系统)上明确安装的键盘.为此,您可以参考thisapple

  9. xcode – 无法在iOS8beta5中使用UIWebView打开PDF文件

    如果是,请提供一些示例代码.解决方法我找到了一种在WebView中查看PDF的解决方法

  10. iOS从UIWebview内容创建pdf

    哪个是从webview内容中获取最佳质量pdf文档的最佳选择?

随机推荐

  1. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Android单选按钮RadioButton的使用详解

    今天小编就为大家分享一篇关于Android单选按钮RadioButton的使用详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  3. 解决android studio 打包发现generate signed apk 消失不见问题

    这篇文章主要介绍了解决android studio 打包发现generate signed apk 消失不见问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  4. Android 实现自定义圆形listview功能的实例代码

    这篇文章主要介绍了Android 实现自定义圆形listview功能的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 详解Android studio 动态fragment的用法

    这篇文章主要介绍了Android studio 动态fragment的用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Android用RecyclerView实现图标拖拽排序以及增删管理

    这篇文章主要介绍了Android用RecyclerView实现图标拖拽排序以及增删管理的方法,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下

  7. Android notifyDataSetChanged() 动态更新ListView案例详解

    这篇文章主要介绍了Android notifyDataSetChanged() 动态更新ListView案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下

  8. Android自定义View实现弹幕效果

    这篇文章主要为大家详细介绍了Android自定义View实现弹幕效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. Android自定义View实现跟随手指移动

    这篇文章主要为大家详细介绍了Android自定义View实现跟随手指移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android实现多点触摸操作

    这篇文章主要介绍了Android实现多点触摸操作,实现图片的放大、缩小和旋转等处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部