Flutter showModalBottomSheet with WebView iOS can't scroll

159 views Asked by At

I showModalBottomSheet with Webview show url https://www.youtube.com it can't scroll when it show, must wait moment then can scroll, if I add any characters in the end of web, it can scroll anytime, or I don't set the inset, make the webview's width equal to the screen, it always can scroll anytime, I try use webview_flutter or flutter_inappwebview, have the same problem,

The device is iPhone 12 iOS 16.1。

the code

// 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,))
            ],
          ),
        )
      ]),
    );
  }
}

1

There are 1 answers

4
Mathis Fouques On

I took a look at your problem, and the reason of the scroll not working initially is because of the loading time of your images on the webview.

Even if the callback onPageFinished is triggered, it doesn't appear to load all the images, and that's why you can view most of the text in the webview, but not images, thus you can't scroll much.

The problem does not actually come from the webview_flutter or inappwebview plugin, but from the web page itself. If you can manage to trigger some action once all images have loaded, you'll be able, by sending a message from your webpage to the flutter webview controller, to make the user know that images are loading.

I used a combination of this post https://stackoverflow.com/a/11071687/14227800 ; the use of a Completer called imageLoadCompleter, and the onConsoleMessage callback from the webview, to : (0). Define the imageLoad completer.

  1. Detect load of all images on webview.
  2. Send a console message (prefer to use javascript channels, i just couldn't make it work with thoe)
  3. Use the completer to complete
  4. With a FutureBuilder and a stack, show the webview + a loader on top, until image have loaded.

Here is the "ShowCardRule" widget updated with the loader :

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.
                            )
                        ],
                      );
                    }),
              )
            ],
          ),
        )
      ]),
    );
  }
}

I did put comments with the proper tag to explain this.

Hope this helps