Flutter實現(xiàn)webview與原生組件組合滑動的示例代碼
最近在用Flutter寫一個新聞客戶端, 新聞詳情頁中的內容 需要用Flutter的本地Widget和WebView共同展示 . 比如標題/上方的視頻播放器是用本地Widget展示, 新聞內容的富文本文字使用webview展示html, 這樣就要求標題/視頻播放器與webview可以 組合滑動 .
ps: 如果把新聞詳情頁都用html畫出, 就不用考慮組合滑動的問題.
找到支持與本地組件共存的webview控件
找一個可以與本地組件共存的webview控件是首要任務, 以下是我測試過的幾個庫:
- flutter_WebView_plugin : 不可以inline;
- webView_flutter : 可能支持, 但是還沒有發(fā)布;
- flutter_inappbrowser : 可以實現(xiàn)組合布局, 所以選用了此庫, 鏈接 https://github.com/pichillilorenzo/flutter_inappbrowser
另外, 如果僅是展示html靜態(tài)頁面, 可以嘗試以下幾個庫, 不用看我這個麻煩的解決辦法了:
html
flutter_html
flutter_html_view
初步實現(xiàn)組合布局
選定 flutter_inappbrowser 后開始實現(xiàn), 初步代碼如下:
@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根據(jù)子布局處理高度, 而Expanded又要根據(jù)父布局處理高度, 所以互相依賴導致整個頁面無法繪制.
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 , 所以這個問題變?yōu)榱?如何獲取WebView的高度 .
獲取WebView的高度
在android中不會有這個破問題, 給 webview 設置 wrap_content 就可以了, 但是在Flutter中我沒有找到類似布局方式. (有大哥知道的話麻煩告訴我一下下啊)
其他嘗試的方法就不說了, 最后我采用的辦法是: 通過JS注入拿到html內容的高度回調 . 實現(xiàn)方法如下:
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高度, 實現(xiàn)webview與本地Widget組合滑動的要求.
Android端一個問題
以上方法實現(xiàn)后我是一陣竊喜, 趕忙測試了一下, 結果發(fā)現(xiàn)一個嚴重問題: Android端給webview設置超出5500左右的高度時, App會閃退 . 閃退時AndroidStudio不會展示錯誤日志, 通過 flutter run --verbose 命令運行可以獲取到錯誤信息, 大體看了下是Flutter渲染的問題, 先反饋給官方以及 flutter_inappbrowser 作者了.
然后自己簡單測試發(fā)現(xiàn), 給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>
下面是我實現(xiàn)的切分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;
}
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
Android實現(xiàn)Unity3D下RTMP推送的示例
像Unity3D下的RTMP或RTSP播放器一樣,好多開發(fā)者苦于在Unity環(huán)境下,如何高效率低延遲的把數(shù)據(jù)采集并編碼實時推送到流媒體服務器,實現(xiàn)Unity場景下的低延遲推拉流方案。本文介紹幾種RTMP推送的方案2021-06-06
Android通過原生APi獲取所在位置的經(jīng)緯度
本篇文章主要介紹了Android通過原生APi獲取所在位置的經(jīng)緯度,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-07-07
Android webview與js交換JSON對象數(shù)據(jù)示例
js主動調用android的對象方式,android也無法返回給js一個jsonobject,需要js做一下轉換,具體代碼如下,感興趣的朋友可以參考下哈2013-06-06
Android SQLite數(shù)據(jù)庫加密的操作方法
因為Android自帶的SQLite數(shù)據(jù)庫本身是沒有實現(xiàn)加密的,那我們如何實現(xiàn)對數(shù)據(jù)庫的加密呢?今天通過本文給大家介紹下Android SQLite數(shù)據(jù)庫加密的操作方法,一起看看吧2021-09-09
android使用ViewPager組件實現(xiàn)app引導查看頁面
這篇文章主要為大家詳細介紹了android使用ViewPager組件實現(xiàn)app引導查看頁面,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-07-07

