带有 WebView iOS 的 Flutter showModalBottomSheet 无法滚动

Flutter showModalBottomSheet with WebView iOS can't scroll

提问人:Tour4x 提问时间:11/10/2023 最后编辑:Tour4x 更新时间:11/17/2023 访问量:52

问:

我用 Webview 显示 url 显示 ModalBottomSheet,https://www.youtube.com 它显示时无法滚动,必须等待片刻才能滚动,如果我在 web 末尾添加任何字符,它可以随时滚动,或者我没有设置插图,使 webview 的宽度等于屏幕,它始终可以随时滚动,我尝试使用 webview_flutter 或flutter_inappwebview, 有同样的问题,

设备为iPhone 12 iOS 16.1。

代码

// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs

import 'package:flutter_inappwebview/flutter_inappwebview.dart';

import 'package:flutter/material.dart';
import 'package:web_test/widget_show_card_rule.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() => runApp(const MaterialApp(home: WebViewExample()));

class WebViewExample extends StatefulWidget {
  const WebViewExample({super.key});

  @override
  State<WebViewExample> createState() => _WebViewExampleState();
}

class _WebViewExampleState extends State<WebViewExample> {
  late final WebViewController _controller;

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.green,
      appBar: AppBar(
        title: const Text('Flutter WebView example'),
      ),
      body: Container(
        height: 300,
        padding: const EdgeInsets.only(left: 15, right: 15),
        child: InAppWebView(
          initialUrlRequest: URLRequest(
              url: Uri.parse(
                  'https://www.youtube.com')),
        ),
      ),
      floatingActionButton: favoriteButton(),
    );
  }

  Widget favoriteButton() {
    return FloatingActionButton(
      onPressed: () async {
        _checkRule();
      },
      child: const Icon(Icons.favorite),
    );
  }

  _checkRule() {
    showModalBottomSheet(
        enableDrag: false,
        isScrollControlled: true,
        context: context,
        backgroundColor: Colors.transparent,
        builder: (context) {
          return const ShowCardRule();
        });
  }
}

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:hexcolor/hexcolor.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';

class ShowCardRule extends StatefulWidget {

  const ShowCardRule({super.key});

  @override
  ShowCardRuleState createState() => ShowCardRuleState();
}

class ShowCardRuleState extends State<ShowCardRule> {

  late final WebViewController _controller;

  final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers = {
    Factory(() => EagerGestureRecognizer())
  };

  UniqueKey _key = UniqueKey();

  @override
  void initState() {
    super.initState();



    // #docregion platform_features
    late final PlatformWebViewControllerCreationParams params;
    if (WebViewPlatform.instance is WebKitWebViewPlatform) {
      params = WebKitWebViewControllerCreationParams(
        allowsInlineMediaPlayback: true,
        mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
      );
    } else {
      params = const PlatformWebViewControllerCreationParams();
    }

    final WebViewController controller =
    WebViewController.fromPlatformCreationParams(params);
    // #enddocregion platform_features

    controller
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(const Color(0x00000000))
      ..setNavigationDelegate(
        NavigationDelegate(
          onProgress: (int progress) {
            debugPrint('WebView is loading (progress : $progress%)');
          },
          onPageStarted: (String url) {
            debugPrint('Page started loading: $url');
          },
          onPageFinished: (String url) {
            debugPrint('Page finished loading: $url');
          },
          onWebResourceError: (WebResourceError error) {
            debugPrint('''
Page resource error:
  code: ${error.errorCode}
  description: ${error.description}
  errorType: ${error.errorType}
  isForMainFrame: ${error.isForMainFrame}
          ''');
          },
          onNavigationRequest: (NavigationRequest request) {
            if (request.url.startsWith('https://www.youtube.com/')) {
              debugPrint('blocking navigation to ${request.url}');
              return NavigationDecision.prevent;
            }
            debugPrint('allowing navigation to ${request.url}');
            return NavigationDecision.navigate;
          },
          onUrlChange: (UrlChange change) {
            debugPrint('url change to ${change.url}');
          },
        ),
      )
      ..addJavaScriptChannel(
        'Toaster',
        onMessageReceived: (JavaScriptMessage message) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(message.message)),
          );
        },
      )
      ..loadRequest(Uri.parse('https://www.youtube.com'));

    // #docregion platform_features
    if (controller.platform is AndroidWebViewController) {
      AndroidWebViewController.enableDebugging(true);
      (controller.platform as AndroidWebViewController)
          .setMediaPlaybackRequiresUserGesture(false);
    }
    // #enddocregion platform_features

    _controller = controller;
  }

  @override
  Widget build(BuildContext context) {
    double webHeight = 400;
    double contentTop = 24;
    double titleHeight = 25;
    double topBlankHeight = MediaQuery.of(context).size.height - webHeight - titleHeight - contentTop;

    return Align(
      alignment: Alignment.topCenter,
      child: Column(children: [
        GestureDetector(
          onTap: () {
            Navigator.pop(context);
          },
          child: Container(height: topBlankHeight, color: Colors.transparent),
        ),
        Container(
          decoration: const BoxDecoration(
            borderRadius: BorderRadius.vertical(top: Radius.circular(8)),
            color: Colors.white,
          ),
          padding: EdgeInsets.only(top: contentTop, left: 18, right: 18),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Container(
                  height: titleHeight,
                  child: Row(
                    children: [
                      Text('使用须知',
                          style: TextStyle(
                              color: HexColor('#333333'),
                              fontSize: 18,
                              fontWeight: FontWeight.w600)),
                      Expanded(child: SizedBox()),
                      Container(
                        alignment: Alignment.centerRight,
                        width: 60,
                        child: GestureDetector(
                            onTap: () {
                              Navigator.pop(context);
                            },
                              ),
                      )
                    ],
                  )),
              Container(
                height: webHeight,
                padding: EdgeInsets.only(top: 15),
                child: WebViewWidget(key: _key,controller: _controller, gestureRecognizers: gestureRecognizers,))
            ],
          ),
        )
      ]),
    );
  }
}

iOS iPhone WebView Flutter-ShowModalBottomsheet

评论


答:

1赞 Mathis Fouques 11/10/2023 #1

我看了一下你的问题,滚动最初不起作用的原因是你的图像在webview上的加载时间。

即使触发了回调 onPageFinished,它似乎也不会加载所有图像,这就是为什么您可以查看 webview 中的大部分文本,但不能查看图像,因此您不能滚动太多。

问题实际上不是来自 webview_flutter 或 inappwebview 插件,而是来自网页本身。 如果你能设法在所有图像加载后触发一些操作,你将能够通过从你的网页向 flutter webview 控制器发送一条消息,让用户知道图像正在加载。

我使用了这篇文章的组合 https://stackoverflow.com/a/11071687/14227800;使用名为 imageLoadCompleter 的 Completer 和来自 webview 的 onConsoleMessage 回调,以: (0). 定义 imageLoad 完成器。

  1. 检测 webview 上所有图像的加载。
  2. 发送控制台消息(更喜欢使用 javascript 通道,我只是无法让它与你一起工作)
  3. 使用完成器完成
  4. 使用 FutureBuilder 和堆栈,在顶部显示 webview + 加载器,直到加载图像。

以下是使用加载器更新的“ShowCardRule”小部件:

class ShowCardRule extends StatefulWidget {
  const ShowCardRule({super.key});

  @override
  ShowCardRuleState createState() => ShowCardRuleState();
}

class ShowCardRuleState extends State<ShowCardRule> {
  late final WebViewController _controller;
  final Completer imageLoadCompleter = Completer();
  // COMMENT_MATHIS_FOUQUES : Completer that will complete once all images have loaded.

  final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers = {
    Factory(() => EagerGestureRecognizer())
  };

  final UniqueKey _key = UniqueKey();

  @override
  void initState() {
    super.initState();

    // #docregion platform_features
    late final PlatformWebViewControllerCreationParams params;
    if (WebViewPlatform.instance is WebKitWebViewPlatform) {
      params = WebKitWebViewControllerCreationParams(
        allowsInlineMediaPlayback: true,
        mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
      );
    } else {
      params = const PlatformWebViewControllerCreationParams();
    }

    final WebViewController controller =
        WebViewController.fromPlatformCreationParams(params);
    // #enddocregion platform_features

    controller
      ..clearCache() // COMMENT_MATHIS_FOUQUES : /!\ TO remove ! Put there for testing purposes.
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(const Color(0x00000000))
      ..setNavigationDelegate(
        NavigationDelegate(
          onProgress: (int progress) {
            debugPrint('WebView is loading (progress : $progress%)');
          },
          onPageStarted: (String url) {
            debugPrint('Page started loading: $url');
          },
          onPageFinished: (String url) {
            _controller.runJavaScript(
              """ var imgs = document.images,
                  len = imgs.length,
                  counter = 0;
                  console.log(imgs);

                  [].forEach.call( imgs, function( img ) {
                      if(img.complete) {
                        incrementCounter();
                      } else {
                        img.addEventListener( 'load', incrementCounter, false );
                      }
                  } );

                  function incrementCounter() {
                      counter++;
                      if ( counter === len ) {
                          console.log( 'LOADED' );
                      }
                  }""",
            );
            // COMMENT_MATHIS_FOUQUES : This will run on pageFinished, on the _controller, that will have been initialised.

            debugPrint('Page finished loading: $url');
          },
          onWebResourceError: (WebResourceError error) {
            debugPrint('''
Page resource error:
  code: ${error.errorCode}
  description: ${error.description}
  errorType: ${error.errorType}
  isForMainFrame: ${error.isForMainFrame}
          ''');
          },
          onNavigationRequest: (NavigationRequest request) {
            if (request.url.startsWith('https://www.youtube.com/')) {
              debugPrint('blocking navigation to ${request.url}');
              return NavigationDecision.prevent;
            }
            debugPrint('allowing navigation to ${request.url}');
            return NavigationDecision.navigate;
          },
          onUrlChange: (UrlChange change) {
            debugPrint('url change to ${change.url}');
          },
        ),
      )
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..addJavaScriptChannel(
        'Toaster',
        onMessageReceived: (JavaScriptMessage message) {
          print(message);
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(message.message)),
          );
        },
      )
      ..setOnConsoleMessage((message) {
        if (message.message == "LOADED") {
          imageLoadCompleter.complete();
        }

        print(message.message);
      }) // COMMENT_MATHIS_FOUQUES : This console message callback can be replaced by proper use of javascriptChannels, it's just that I couldn't make it work quick enough with js channels.
      ..loadRequest(
        Uri.parse(
            'https://image.fangte.com/TestPro/UploadFiles/H5/RichText/index.html?id=BDB1A583206FED5F975D6D89D9CCB8E1'),
      );

    // #docregion platform_features
    if (controller.platform is AndroidWebViewController) {
      AndroidWebViewController.enableDebugging(true);
      (controller.platform as AndroidWebViewController)
          .setMediaPlaybackRequiresUserGesture(false);
    }
    // #enddocregion platform_features

    _controller = controller;
  }

  @override
  Widget build(BuildContext context) {
    double webHeight = 400;
    double contentTop = 24;
    double titleHeight = 25;
    double topBlankHeight = MediaQuery.of(context).size.height -
        webHeight -
        titleHeight -
        contentTop;

    return Align(
      alignment: Alignment.topCenter,
      child: Column(children: [
        GestureDetector(
          onTap: () {
            Navigator.pop(context);
          },
          child: Container(height: topBlankHeight, color: Colors.transparent),
        ),
        Container(
          decoration: const BoxDecoration(
            borderRadius: BorderRadius.vertical(top: Radius.circular(8)),
            color: Colors.white,
          ),
          padding: EdgeInsets.only(top: contentTop, left: 18, right: 18),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              SizedBox(
                  height: titleHeight,
                  child: Row(
                    children: [
                      Text('使用须知',
                          style: TextStyle(
                              color: HexColor('#333333'),
                              fontSize: 18,
                              fontWeight: FontWeight.w600)),
                      const Expanded(child: SizedBox()),
                      Container(
                        alignment: Alignment.centerRight,
                        width: 60,
                        child: GestureDetector(
                          onTap: () {
                            Navigator.pop(context);
                          },
                        ),
                      )
                    ],
                  )),
              Container(
                height: webHeight,
                padding: const EdgeInsets.only(top: 15),
                child: FutureBuilder(
                    future: imageLoadCompleter
                        .future, // COMMENT_MATHIS_FOUQUES : This will complete when we receive the js message that images have loaded.
                    builder: (context, snapshot) {
                      return Stack(
                        children: [
                          WebViewWidget(
                            key: _key,
                            controller: _controller,
                            gestureRecognizers: gestureRecognizers,
                          ),
                          if (snapshot.connectionState != ConnectionState.done)
                            const Center(
                              child: CircularProgressIndicator
                                  .adaptive(), // COMMENT_MATHIS_FOUQUES : Shows a loader until images are ok, but we can do anything we want.
                              // COMMENT_MATHIS_FOUQUES : The only thing is that the webview still has to be showed, even if images are not loaded.
                            )
                        ],
                      );
                    }),
              )
            ],
          ),
        )
      ]),
    );
  }
}

我确实用适当的标签放置了评论来解释这一点。

希望这会有所帮助

评论

0赞 Tour4x 11/10/2023
嗨,马蒂斯 感谢您的帮助。我已经尝试了解决方案,它仍然有同样的问题,我不明白为什么我不设置插图(现在插图是 EdgeInsets.only(top: contentTop, left: 18, right: 18)) 它会起作用,另一个问题是我可以编辑 Web 内容,如果我在末尾添加空格或任何字符(现在内容的结尾是图片), 它总是没有滚动问题。
0赞 Mathis Fouques 11/11/2023
是的,如果您在图像后添加一个字符,滚动问题就会消失,因为阻止您滚动的是图像的加载时间。
0赞 Mathis Fouques 11/11/2023
因此,如果您想让滚动一直工作,只需在带有图像的 div 后面添加“<p> </p>”,您的问题就解决了。问题并不真正出在 flutter webview 上,你可以用一个小窗口在你的电脑上测试,也会有同样的问题。
0赞 Tour4x 11/13/2023
谢谢马蒂斯,问题与网络内容格式有关,我已经编辑了它,网络中的滚动问题消失了。