предисловие
Добро пожаловать, чтобы следовать за мнойGithubа такжеCSDN.
Недавно я пишу новостной клиент во Flutter, контент на странице сведений о новостяхНеобходимо использовать локальный виджет Flutter и WebView для совместного отображения., Например, видеоплеер над заголовком/видео отображается с помощью локального виджета, а форматированный текст новостного контента отображается в формате html с использованием веб-представления, для чего требуется, чтобы заголовок/видеопроигрыватель и веб-представление могликомбинированный слайд.
ps: Если страницы с подробностями новостей нарисованы в html, то проблему комбинированного скольжения рассматривать не нужно.
При перепечатке просьба указывать источник:nuggets.capable/post/684490…
Найдите элементы управления веб-просмотром, которые поддерживают сосуществование с собственными компонентами.
Поиск элемента управления веб-представления, который может сосуществовать с нативными компонентами, является приоритетом, вот несколько библиотек, которые я тестировал:
-
flutter_WebView_plugin
: не может быть встроенным; -
webView_flutter
: может поддерживаться, но еще не выпущен; -
flutter_inappbrowser
: можно добиться комбинированного макета, поэтому выбрана эта библиотека, ссылкаGitHub.com/Теория рулетки или…
Кроме того, если вы отображаете только статические страницы html, вы можете попробовать следующие библиотеки вместо того, чтобы смотреть на мое проблемное решение:
Предварительная реализация комбинированной компоновки
выбран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'),
),
],
),
);
}
Это создаст интерфейс, который сочетает в себе текст и веб-просмотр, но здесь у веб-просмотра есть собственная полоса прокрутки, а заголовок не включается при прокрутке. Попробуйте следующие два метода
- пакет
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
Не будет этой сломанной проблемы в андроиде, дайтеwebview
настраиватьwrap_content
Вот и все, но похожего макета во Flutter я не нашел (если кто знает, сообщите, пожалуйста)
Я не буду упоминать другие проверенные методы, и последний метод, который я выбрал, это:Высоко обратного вызова к содержимому HTML через инъекцию JSМетод реализации следующий:
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);
}
}
Вышеупомянутый метод может точно получить высоту веб-просмотра и реализовать требования комбинированного скольжения веб-просмотра и локального виджета.
Проблема на стороне Android
После реализации вышеописанного метода я какое-то время обрадовался, и быстро протестировал его, и обнаружил серьезную проблему:Когда сторона Android устанавливает высоту веб-просмотра более 5500, приложение будет мигать.
AndroidStudio не будет отображать журнал ошибок при сбое, передатьflutter run --verbose
Информацию об ошибке можно получить, выполнив команду.Вообще говоря, это проблема рендеринга Flutter.Отзыв официала иflutter_inappbrowser
Автор.
Затем я просто проверил и обнаружил, что добавить несколько веб-просмотров в дочерний элемент столбца не проблема, даже если содержимое этих веб-просмотров определенно превышает высоту 5500. Итак, у меня есть идея:Разделите html, разделите его на несколько веб-просмотров для отображения вместе, а затем введите JS соответственно, чтобы получить высоту.
Внимание!Внимание!Наши сценарии использования:Отображаемый контент = html-оболочка, хранящаяся в активах + абзац новостного контента, полученный интерфейсом, а не URL. Приведенное выше решение применимо только к сцене загрузки html, а не URL-адреса.
Суть этой идеи в том, как сегментировать html контент, нужно сделать так, чтобы сегментированный html закрывался тегом, то есть не обрезался внутри тега. Предпосылка использования этой схемы сегментации заключается в том, что 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
// @return String 剪切后的html
static List<String> cutHtml(String htmlString) {
htmlString = _getBody(htmlString);
List<String> htmlList = List();
if (Platform.isAndroid && _calculateHeightOfHtml(htmlString) > 4000) {
// html总高度
double totalHeight = _calculateHeightOfHtml(htmlString);
// 切为几段('~/'整除, /.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 = '';
}
labelStack = _checkComplete(resultHtml);
if (labelStack == 0) {
htmlList.add(resultHtml);
childNum--;
} else {
// 如果不是闭合的, 把remain里的n个标签尾之前的内容剪切到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;
}
// 自startPosition开始向后找到第一个尾标签, 返回该尾标签的下一位位置, 以便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;
}
// 未闭合的标签数目 --> 时间复杂度过高, O(11n)
static int _checkComplete(String resultHtml) {
// 这里没有使用stack, 而是简单的计数, 是默认正确的html格式, 而且只有_headLabels内的标签类型
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;
}
// 以_labelsHead内的字符串开头
static String _startWithLabelHead(String resultHtml, int startPosition) {
for (String label in _headLabels) {
if (resultHtml.startsWith(label, startPosition)) {
return label;
}
}
return null;
}
// 以_labelsTail内的字符串开头
static String _startWithLabelTail(String resultHtml, int startPosition) {
for (String label in _tailLabels) {
if (resultHtml.startsWith(label, startPosition)) {
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 _headLabels = {'<div', '<img', '<p', '<strong', '<span'};
static final _tailLabels = {'</div>', '</img>', '</p>', '</strong>', '</span>', '/>'};
С помощью приведенного выше алгоритма получите сегментированный htmlList, а затем используйте несколько веб-просмотров в PageState, чтобы загрузить их по отдельности и, соответственно, внедрить js для решения этой проблемы.
Готово!
ps Высота 4000, используемая здесь, является лишь приблизительной, и будут корректировки, когда будут адаптированы более поздние модели.
Прикрепил:
-
flutter_inappbrowser
Как загрузить строку html:InAppWebView( initialData: InAppWebViewInitialData(' htmlContent '))
- Разберите файл актива как строку:
static Future<String> decodeStringFromAssets(String path) async { ByteData byteData = await PlatformAssetBundle().load(path); String htmlString = String.fromCharCodes(byteData.buffer.asUint8List()); return htmlString; }