Flutter實(shí)現(xiàn)webview與原生組件組合滑動(dòng)的示例代碼
最近在用Flutter寫(xiě)一個(gè)新聞客戶(hù)端, 新聞詳情頁(yè)中的內(nèi)容 需要用Flutter的本地Widget和WebView共同展示 . 比如標(biāo)題/上方的視頻播放器是用本地Widget展示, 新聞內(nèi)容的富文本文字使用webview展示html, 這樣就要求標(biāo)題/視頻播放器與webview可以 組合滑動(dòng) .
ps: 如果把新聞詳情頁(yè)都用html畫(huà)出, 就不用考慮組合滑動(dòng)的問(wèn)題.
找到支持與本地組件共存的webview控件
找一個(gè)可以與本地組件共存的webview控件是首要任務(wù), 以下是我測(cè)試過(guò)的幾個(gè)庫(kù):
- flutter_WebView_plugin : 不可以inline;
- webView_flutter : 可能支持, 但是還沒(méi)有發(fā)布;
- flutter_inappbrowser : 可以實(shí)現(xiàn)組合布局, 所以選用了此庫(kù), 鏈接 https://github.com/pichillilorenzo/flutter_inappbrowser
另外, 如果僅是展示html靜態(tài)頁(yè)面, 可以嘗試以下幾個(gè)庫(kù), 不用看我這個(gè)麻煩的解決辦法了:
html
flutter_html
flutter_html_view
初步實(shí)現(xiàn)組合布局
選定 flutter_inappbrowser 后開(kāi)始實(shí)現(xiàn), 初步代碼如下:
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Column( children: <Widget>[ Text('Title'), Expanded( // 注意必須加這個(gè), 否則webview沒(méi)有高度 child: InAppWebView(initialUrl: 'https://juejin.im/timeline'), ), ], ), ); }
這樣會(huì)構(gòu)建一個(gè)text和webview組合的界面, 不過(guò)這里webview自帶滾動(dòng)條, 滾動(dòng)時(shí)是不帶著title一塊的. 嘗試以下兩種辦法
包裹 SingleChildScrollView : 界面會(huì)消失不見(jiàn), 因?yàn)镾crollview根據(jù)子布局處理高度, 而Expanded又要根據(jù)父布局處理高度, 所以互相依賴(lài)導(dǎo)致整個(gè)頁(yè)面無(wú)法繪制.
body: SingleChildScrollView( child: Column( children: <Widget>[ Text('Title'), Expanded( child: InAppWebView(initialUrl: 'https://juejin.im/timeline'), ), ], ), ),
包裹 SingleChildScrollView , 去掉 Expanded : AppBar可以顯示了, 但是 InAppWebView 沒(méi)有高度了.
body: SingleChildScrollView( child: Column( children: <Widget>[ Text('Title'), InAppWebView(initialUrl: 'https://juejin.im/timeline'), ], ), ),
這兩種方式都不行, 歸根到底是不知道 InAppWebView 的高度, 所以才需要使用與 SingleChildScrollView 相沖突的 Expanded , 所以這個(gè)問(wèn)題變?yōu)榱?如何獲取WebView的高度 .
獲取WebView的高度
在android中不會(huì)有這個(gè)破問(wèn)題, 給 webview 設(shè)置 wrap_content 就可以了, 但是在Flutter中我沒(méi)有找到類(lèi)似布局方式. (有大哥知道的話(huà)麻煩告訴我一下下啊)
其他嘗試的方法就不說(shuō)了, 最后我采用的辦法是: 通過(guò)JS注入拿到html內(nèi)容的高度回調(diào) . 實(shí)現(xiàn)方法如下:
class TestState extends State<Test> { InAppWebViewController _controller; double _htmlHeight = 200; // 目的是在回調(diào)完成直接先展示出200高度的內(nèi)容, 提高用戶(hù)體驗(yàn) 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, 設(shè)置為回調(diào)的高度 height: _htmlHeight, child: InAppWebView( initialUrl: 'https://juejin.im/timeline', onWebViewCreated: (InAppWebViewController controller) { _controller = controller; _setJSHandler(_controller); // 設(shè)置js方法回掉, 拿到高度 }, onLoadStop: (InAppWebViewController controller, String url) { // 頁(yè)面加載完成后注入js方法, 獲取頁(yè)面總高度 controller.injectScriptCode(""" window.flutter_inappbrowser.callHandler('InAppWebView', document.body.scrollHeight)); """); }, ), ) ], ), ), ); } void _setJSHandler(InAppWebViewController controller) { JavaScriptHandlerCallback callback = (List<dynamic> arguments) async { // 解析argument, 獲取到高度, 直接設(shè)置即可(iphone手機(jī)需要+20高度) double height = HtmlUtils.getHeight(arguments); if (height > 0) { setState(() { _htmlHeight = height; }); } }; controller.addJavaScriptHandler(HANDLER_NAME, callback); } }
以上方法可以精確獲取到webview高度, 實(shí)現(xiàn)webview與本地Widget組合滑動(dòng)的要求.
Android端一個(gè)問(wèn)題
以上方法實(shí)現(xiàn)后我是一陣竊喜, 趕忙測(cè)試了一下, 結(jié)果發(fā)現(xiàn)一個(gè)嚴(yán)重問(wèn)題: Android端給webview設(shè)置超出5500左右的高度時(shí), App會(huì)閃退 . 閃退時(shí)AndroidStudio不會(huì)展示錯(cuò)誤日志, 通過(guò) flutter run --verbose
命令運(yùn)行可以獲取到錯(cuò)誤信息, 大體看了下是Flutter渲染的問(wèn)題, 先反饋給官方以及 flutter_inappbrowser
作者了.
然后自己簡(jiǎn)單測(cè)試發(fā)現(xiàn), 給Column的child添加了多個(gè)webview沒(méi)什么問(wèn)題, 哪怕這幾個(gè)webview的內(nèi)容相加絕對(duì)超出了5500高度. 所以有了思路: 切分html, 分為多個(gè)webview共同展示, 然后分別注入JS獲取高度 .
注意!注意! 我們的使用場(chǎng)景是: 要展示的內(nèi)容 = assets存儲(chǔ)的html外殼 + 接口獲取到的新聞內(nèi)容段落, 而不是一個(gè)url . 以上解決思路僅適用于加載html的場(chǎng)景, 而不是url.
這個(gè)思路的核心在于如何切分html內(nèi)容, 需要保證切分后的html是標(biāo)簽閉合的, 即不是切在了某標(biāo)簽內(nèi)部. 使用此切分方案的前提是: body內(nèi)部的html標(biāo)簽不會(huì)有超大范圍的div包裹, 否則單個(gè)標(biāo)簽內(nèi)容就超過(guò)高度了. 可用的html示例:
<html> <head></head> <body> <!-- 并列小組合, 沒(méi)有超大范圍的div等標(biāo)簽的包裹 --> <p style.. > asdasdasd </p> <div style.. > <img ... /> <p> ... </p> </div> <p> asdasdas </p> </body> </html>
下面是我實(shí)現(xiàn)的切分html的算法:
// 剪切過(guò)長(zhǎng)的html, 考慮到較差機(jī)型以及其他誤差, 定為4000 // @params htmlString 待切分的html // @params totalHeight 前面webview回調(diào)出的總高度 // @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的長(zhǎng)度 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個(gè)標(biāo)簽尾之前的內(nèi)容剪切到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是標(biāo)簽閉合的 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內(nèi)的字符串開(kāi)頭 static String _startWithLabel(String resultHtml) { for (String label in _labelsHead) { if (resultHtml.startsWith(label)) { return label; } } return null; } // 去除body及以外的標(biāo)簽, 露出并列的子標(biāo)簽 // <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; } // 待檢測(cè)的標(biāo)簽 static final _labelsHead = {'<div', '<img', '<p', '<strong', '<span'}; static final _labelsTail = '</';
通過(guò)以上算法, 拿到了切分好的htmlList, 然后在PageState中使用多個(gè)webview分別加載, 分別注入js即可解決此問(wèn)題.
大功告成!
附:
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; }
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android實(shí)現(xiàn)Unity3D下RTMP推送的示例
像Unity3D下的RTMP或RTSP播放器一樣,好多開(kāi)發(fā)者苦于在Unity環(huán)境下,如何高效率低延遲的把數(shù)據(jù)采集并編碼實(shí)時(shí)推送到流媒體服務(wù)器,實(shí)現(xiàn)Unity場(chǎng)景下的低延遲推拉流方案。本文介紹幾種RTMP推送的方案2021-06-06Android通過(guò)原生APi獲取所在位置的經(jīng)緯度
本篇文章主要介紹了Android通過(guò)原生APi獲取所在位置的經(jīng)緯度,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07Android實(shí)現(xiàn)多線(xiàn)程下載文件的方法
這篇文章主要介紹了Android實(shí)現(xiàn)多線(xiàn)程下載文件的方法,以實(shí)例形式較為詳細(xì)的分析了Android多線(xiàn)程文件傳輸及合并等操作的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10Android軟鍵盤(pán)彈出時(shí)的界面控制方法
這篇文章主要介紹了Android軟鍵盤(pán)彈出時(shí)的界面控制方法,結(jié)合實(shí)例形式分析了Android軟鍵盤(pán)彈出后的三種模式,涉及Android針對(duì)AndroidManifet.xml的修改技巧,需要的朋友可以參考下2016-08-08Android如何動(dòng)態(tài)改變App桌面圖標(biāo)
這篇文章主要介紹了 Android動(dòng)態(tài)改變App桌面圖標(biāo)的方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下2017-01-01Android webview與js交換JSON對(duì)象數(shù)據(jù)示例
js主動(dòng)調(diào)用android的對(duì)象方式,android也無(wú)法返回給js一個(gè)jsonobject,需要js做一下轉(zhuǎn)換,具體代碼如下,感興趣的朋友可以參考下哈2013-06-06Android SQLite數(shù)據(jù)庫(kù)加密的操作方法
因?yàn)锳ndroid自帶的SQLite數(shù)據(jù)庫(kù)本身是沒(méi)有實(shí)現(xiàn)加密的,那我們?nèi)绾螌?shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)的加密呢?今天通過(guò)本文給大家介紹下Android SQLite數(shù)據(jù)庫(kù)加密的操作方法,一起看看吧2021-09-09android使用ViewPager組件實(shí)現(xiàn)app引導(dǎo)查看頁(yè)面
這篇文章主要為大家詳細(xì)介紹了android使用ViewPager組件實(shí)現(xiàn)app引導(dǎo)查看頁(yè)面,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07