preface

Welcome to Github and CSDN.

Recently, I wrote a news client with Flutter. The content of the news detail page needs to be displayed with the local widgets and WebView of Flutter. For example, the video player above the title/video is displayed as a native Widget, and the rich text of the news content is displayed as HTML using the WebView. This requires that the title/video player and the WebView can be swiped together.

Ps: If the news details page is drawn in HTML, there is no need to worry about the combined sliding problem.

Reprint please indicate the source: juejin.cn/post/684490…

Find webView controls that support coexistence with native components

Finding a WebView control that can coexist with native components is a top priority. Here are a few libraries I tested:

  • flutter_WebView_plugin: cannot be inline;
  • webView_flutter: Possible, but not yet released;
  • flutter_inappbrowser: can achieve combined layout, so choose this library, linkGithub.com/pichillilor…

Alternatively, if you just want to display static HTML pages, you can try the following libraries without seeing my cumbersome solution:

  • html
  • flutter_html
  • flutter_html_view

Preliminary realization of the combined layout

After selecting Flutter_inappBrowser, the initial code is as follows:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        children: <Widget>[
          Text('Title'),
          Expanded( // Note that this must be added, otherwise the WebView has no height
            child: InAppWebView(initialUrl: 'https://juejin.im/timeline'() [() [() [() }Copy the code

This will create a text and WebView interface, but in this case the WebView has its own scrollbar, so it doesn’t scroll with its title. Try two things

  1. The parcelSingleChildScrollView: The interface disappears because Scrollview handles height based on the child layout and Expanded handles height based on the parent layout, so the interdependence prevents the entire page from being drawn.
    body: SingleChildScrollView(
            child: Column(
              children: <Widget>[
                Text('Title'),
                Expanded(
                  child: InAppWebView(initialUrl: 'https://juejin.im/timeline'(), [(), [(),Copy the code
  2. The parcelSingleChildScrollViewTo remove theExpanded: AppBar can be displayed, howeverInAppWebViewThere is no height.
    body: SingleChildScrollView(
            child: Column(
              children: <Widget>[
                Text('Title'),
                InAppWebView(initialUrl: 'https://juejin.im/timeline'),,),),Copy the code

In the end, you don’t know the height of the InAppWebView, so you need to use Expanded that conflicts with the SingleChildScrollView, so the problem becomes how to get the height of the WebView.

Gets the WebView height

Android doesn’t have this problem. Just set the webview to wrap_content, but I can’t find a similar layout for Flutter.

The other way to try it is to get the height callback of the HTML content through JS injection. The implementation method is as follows:

class TestState extends State<Test> {
  InAppWebViewController _controller;
  double _htmlHeight = 200; // The purpose is to improve the user experience by displaying the 200-height content before the callback is complete

  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( // Wrap the WebView in a Container that provides the height, setting it to the height of the callback
              height: _htmlHeight,
              child: InAppWebView(
                initialUrl: 'https://juejin.im/timeline',
                onWebViewCreated: (InAppWebViewController controller) {
                  _controller = controller;
                  _setJSHandler(_controller); // set the js method back to get the height
                },
                onLoadStop: (InAppWebViewController controller, String url) {
                  // After the page is loaded, inject js method to get the total height of the page
                  controller.injectScriptCode(""" window.flutter_inappbrowser.callHandler('InAppWebView', document.body.scrollHeight)); "" ");
                },
              ),
            )
          ],
        ),
      ),
    );
  }

  void _setJSHandler(InAppWebViewController controller) {
    JavaScriptHandlerCallback callback = (List<dynamic> arguments) async {
      // Set the argument to the height (+20 for iPhone)
      double height = HtmlUtils.getHeight(arguments);
      if (height > 0) { setState(() { _htmlHeight = height; }); }}; controller.addJavaScriptHandler(HANDLER_NAME, callback); }}Copy the code

The above methods can accurately obtain the height of the WebView to achieve the requirements of the combination of the WebView and local Widget sliding.

A problem on Android

When the above method was implemented, I was so pleased that I rushed to test it and found a serious problem: when the Height of WebView on Android was set beyond 5500, the App would flash back. AndroidStudio will not display error logs when the flutter returns. Run the flutter run –verbose command to get error messages. The error message was reported to officials and the author of flutter_inappbrowser.

Then I did a simple test and found that adding multiple WebViews to Column’s child was fine, even though the total content of the webViews definitely exceeded 5500 height. So there is an idea: split HTML, divided into multiple WebViews for common display, and then inject JS respectively to get the height.

Attention! Attention! Our usage scenario is: the content to be displayed = the HTML shell of assets stored + the news content paragraph obtained by the interface, rather than a URL. The above solution only applies to scenarios where HTML is loaded, not urls.

The core of this idea is how to slice HTML content. You need to make sure that the HTML is closed, that is, not cut inside a tag. The premise for using this shard scheme is that the HTML tag inside the body does not have an oversized DIV wrapped around it, otherwise the individual tag content would be too tall. Examples of HTML available:

<html>
  <head></head>
    <body>
        <! -- Juxtaposed small groups, without large div tags -->
        <p style.. > asdasdasd </p>
       	<div style.. > 
       	    <img . />
       	    <p>.</p>
       	</div> 
       	<p> asdasdas </p>
    </body>
</html>
Copy the code

Here is the algorithm I implemented to slice HTML:

  // Cut too long HTML, take into account poor models and other errors, set to 4000
  // @return String Clipped HTML
  static List<String> cutHtml(String htmlString) {
    htmlString = _getBody(htmlString);

    List<String> htmlList = List(a);if (Platform.isAndroid && _calculateHeightOfHtml(htmlString) > 4000) {
      // total height of HTML
      double totalHeight = _calculateHeightOfHtml(htmlString);
      // Cut into segments ('~/' divisible, /.toint)
      int childNum = totalHeight ~/ 4000 + (totalHeight % 4000= =0 ? 0 : 1);
      // The length of each HTML section
      int childLength = htmlString.length ~/ childNum;
      // Two pieces of HTML after a single cut
      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 = ' ';
        }

        labelStack = _checkComplete(resultHtml);
        if (labelStack == 0) {
          htmlList.add(resultHtml);
          childNum--;
        } else {
          // If not closed, cut the remaining n tags into result
          int tailPosition = 0;
          do {
            tailPosition = _getTailPositionOfTail(remainHtml, tailPosition);
            if (tailPosition == - 1) {
              throw Exception('html style error: no label tail');
            }
            labelStack--;
          } while(labelStack ! =0);

          resultHtml = resultHtml + remainHtml.substring(0, tailPosition); remainHtml = remainHtml.substring(tailPosition); htmlList.add(resultHtml); childNum--; }}}else {
      htmlList.add(htmlString);
    }

    return htmlList;
  }

  // Find the first tail label back from startPosition and return the next bit of the tail label for substring
  static int _getTailPositionOfTail(String remainHtml, int startPosition) {
    int frontTailPosition = remainHtml.length;
    String frontTailName;
    for (String tailLabel in _tailLabels) {
      int current = remainHtml.indexOf(tailLabel, startPosition);
      if(current ! =- 1&& current < frontTailPosition) { frontTailPosition = current; frontTailName = tailLabel; }}return frontTailPosition + frontTailName.length;
  }

  // Number of unclosed labels --> Time complexity is too high, O(11n)
  static int _checkComplete(String resultHtml) {
    // Instead of using stack, simply count, default correct HTML format, and only _headLabels within the label type
    int labelStack = 0;
    for (int i = 0; i < resultHtml.length; i++) {
      String label = _startWithLabelHead(resultHtml, i);
      if(label ! =null) {
        labelStack++;
        i += label.length - 1;
      } else {
        label = _startWithLabelTail(resultHtml, i);
        if(label ! =null) {
          labelStack--;
          i += label.length - 1; }}}return labelStack;
  }

  // Start with a string inside _labelsHead
  static String _startWithLabelHead(String resultHtml, int startPosition) {
    for (String label in _headLabels) {
      if (resultHtml.startsWith(label, startPosition)) {
        returnlabel; }}return null;
  }

  // Start with a string inside _labelsTail
  static String _startWithLabelTail(String resultHtml, int startPosition) {
    for (String label in _tailLabels) {
      if (resultHtml.startsWith(label, startPosition)) {
        returnlabel; }}return null;
  }

  // Remove the body and other tags to reveal the parallel child tags
  // <html>
  // 
  // 
  / /...
  // 
  // </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;
  }

  // The label to be detected
  static final _headLabels = {'<div'.'<img'.'<p'.'<strong'.'<span'};
  static final _tailLabels = {'</div>'.'</img>'.'</p>'.'</strong>'.'</span>'.'/ >'};
Copy the code

Through the above algorithm, we got the segmented htmlList, and then load it in PageState using multiple WebViews and inject JS to solve this problem.

And you’re done!

Ps. The 4000 height used here is only about, and it will be adjusted when later models are adapted.

The attached:

  1. flutter_inappbrowserHow to load AN HTML string:
    InAppWebView( initialData: InAppWebViewInitialData(' htmlContent '))
    Copy the code
  2. Parse the asset file as a string:
    static Future<String> decodeStringFromAssets(String path) async {
        ByteData byteData = await PlatformAssetBundle().load(path);
        String htmlString = String.fromCharCodes(byteData.buffer.asUint8List());
        return htmlString;
    }
    Copy the code