Sample code for Flutter to realize the combination sliding of webview and native components

  • 2021-11-02 02:46:35
  • OfStack

Recently, I am writing a news client with Flutter, and the content in the news details page needs to be displayed with Flutter's local Widget and WebView. For example, the video player above the title/is displayed with local Widget, and the rich text text of news content is displayed with webview and html, which requires that the title/video player and webview can be combined and slid.

ps: If you draw all the news details pages with html, you don't have to consider the problem of combination sliding.

Find webview controls that support coexistence with local components

Finding an webview control that can coexist with native components is the first priority. Here are a few libraries I have tested:

flutter_WebView_plugin: inline is not allowed; webView_flutter: Possibly supported, but not yet released; flutter_inappbrowser: Composite layout can be realized, so this library is selected. Link https://github.com/pichillilorenzo/flutter_inappbrowser

In addition, if you only show html static pages, you can try the following libraries without looking at my troublesome solution:

html
flutter_html
flutter_html_view

Initial realization of combined layout

After selecting flutter_inappbrowser, the implementation begins. The preliminary 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 webview No height 
      child: InAppWebView(initialUrl: 'https://juejin.im/timeline'),
     ),
    ],
   ),
  );
 }

This will create a combination of text and webview, but here webview comes with scroll bars and scrolls without title1 blocks. Try the following two methods

Wrapping SingleChildScrollView: The interface disappears because Scrollview processes heights based on child layouts and Expanded processes heights based on parent layouts, 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'),
      ),
     ],
    ),
   ),

Wrap SingleChildScrollView and remove Expanded: AppBar can be displayed, but InAppWebView has no height.


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

Neither of these methods works. In the final analysis, I don't know the height of InAppWebView, so I need to use Expanded which conflicts with SingleChildScrollView, so the problem becomes how to obtain the height of WebView.

Get the height of WebView

In android will not have this broken problem, to webview set wrap_content can be, but in Flutter I did not find a similar layout

I don't want to talk about other ways to try. Finally, I use the method: Get the high callback of 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 show directly before the callback is completed 200 High content ,  Improve the user experience 

 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( //  Object that provides height Container Package WebView,  Set to the height of the callback 
       height: _htmlHeight,
       child: InAppWebView(
        initialUrl: 'https://juejin.im/timeline',
        onWebViewCreated: (InAppWebViewController controller) {
         _controller = controller;
         _setJSHandler(_controller); //  Settings js Method rollback ,  Get to altitude 
        },
        onLoadStop: (InAppWebViewController controller, String url) {
         //  Inject after the page is loaded js Method ,  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 {
   //  Analyse argument,  Obtain height ,  You can set it directly (iphone Mobile phone needs +20 Height )
   double height = HtmlUtils.getHeight(arguments);
   if (height > 0) {
    setState(() {
     _htmlHeight = height;
    });
   }
  };
  controller.addJavaScriptHandler(HANDLER_NAME, callback);
 }
}

The above method can accurately obtain the height of webview, and realize the combined sliding requirement of webview and local Widget.

Android End 1 Problem

After the implementation of the above method, I was secretly pleased. I quickly tested it for 1 time, and found a serious problem: When Android sets the height of webview beyond 5500, App will flash back. When flashing back, AndroidStudio will not show error log, through flutter run --verbose Command running can get error information, generally look at the problem of Flutter rendering, first feedback to the official and flutter_inappbrowser The author.

Then, after simple test, I found that it is no problem to add multiple webview to child of Column, even if the contents of these webview definitely exceed 5500 height. Therefore, I have an idea: divide html, divide it into multiple webview for joint display, and then inject JS to obtain height.

Attention! Attention! Our usage scenario is: Content to be displayed = html shell stored by assets + news content paragraph obtained by interface, instead of an url. The above solution is only applicable to the scenario where html is loaded, not url.

The core of this idea lies in how to segment the content of html, and it is necessary to ensure that the segmented html is closed, that is, it is not cut inside a certain tag. The premise of using this segmentation scheme is that the html tag inside body will not have a large range of div packages, otherwise the content of a single tag will exceed the height. Available html examples:


<html>
 <head></head>
  <body>
    <!--  Parallel small combination ,  There is no super-large range div Packages with equal labels  -->
    <p style.. > asdasdasd </p>
    <div style.. > 
      <img ... />
      <p> ... </p>
    </div> 
    <p> asdasdas </p>
  </body>
</html>

The following is the algorithm I implemented to segment html:


//  Cut too long html,  Considering the poor model and other errors, ,  Determine to 4000
 // @params htmlString  To be segmented html
 // @params totalHeight  Front webview Total height of callback 
 // @return String  After cutting html
 static List<String> cutHtml(String htmlString, double totalHeight) {
  htmlString = _getBody(htmlString);

  List<String> htmlList = List();
  if (Platform.isAndroid && totalHeight > 4000) {
   //  Cut into several sections ('~/' Divisive , /.toInt)
   int childNum = totalHeight ~/ 4000 + (totalHeight % 4000 == 0 ? 0 : 1);
   //  Each paragraph html The length of 
   int childLength = htmlString.length ~/ childNum;
   //  Cut 1 Two sections behind the knife 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 {
     //  If it's not closed ,  Put remain In n Cut the contents before the end of the tag to result Medium 
     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 The label is closed 
 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;
 }

 //  With _labelsHead The beginning of the string in the 
 static String _startWithLabel(String resultHtml) {
  for (String label in _labelsHead) {
   if (resultHtml.startsWith(label)) {
    return label;
   }
  }
  return null;
 }

 //  Removal body And other labels ,  Expose juxtaposed sub-labels 
 // <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;
 }

 //  Label to be detected 
 static final _labelsHead = {'<div', '<img', '<p', '<strong', '<span'};
 static final _labelsTail = '</';

Through the above algorithm, the segmented htmlList is obtained, and then several webview are loaded and injected into js in PageState to solve this problem.

Be crowned with success!

Attachment:

flutter_inappbrowser How to Load an html String:


InAppWebView( initialData: InAppWebViewInitialData(' htmlContent '))

Parsing an asset file to a string:


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

Related articles: