You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中showModalBottomSheet嵌套WebView在iOS无法滚动的问题

Flutter ModalBottomSheet中WebView加载YouTube初始无法滚动的iOS问题

在iPhone 12(iOS 16.1)设备上,通过showModalBottomSheet展示加载https://www.youtube.com的WebView时,会出现初始无法滚动的问题,必须等待一段时间后才能正常滚动。

经过测试发现两种可规避该问题的场景:

  • 在YouTube URL末尾添加任意字符(如https://www.youtube.com/?foo)
  • 移除WebView容器的左右内边距,让WebView宽度与屏幕一致

且webview_flutter和flutter_inappwebview两个主流WebView库均存在此问题。


问题复现代码

主页面代码

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

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. 调整WebView容器布局(推荐)

避免直接给WebView的父容器设置左右内边距,改为给外层容器或子组件单独设置padding,让WebView宽度与父容器完全一致,消除布局偏移导致的手势识别异常:

// 修改ShowCardRule中的外层白色容器和WebView部分
Container(
  decoration: const BoxDecoration(
    borderRadius: BorderRadius.vertical(top: Radius.circular(8)),
    color: Colors.white,
  ),
  padding: EdgeInsets.only(top: contentTop), // 移除左右padding
  child: Column(
    mainAxisSize: MainAxisSize.min,
    children: [
      // 给标题栏单独设置左右padding
      Container(
        padding: EdgeInsets.symmetric(horizontal: 18),
        height: titleHeight,
        child: Row(/* 原标题代码 */),
      ),
      // WebView容器仅保留顶部padding
      Container(
        height: webHeight,
        padding: EdgeInsets.only(top: 15),
        child: WebViewWidget(
          key: _key,
          controller: _controller,
          gestureRecognizers: gestureRecognizers,
        ),
      )
    ],
  ),
)

2. 临时URL Workaround

给YouTube URL添加无意义的查询参数,绕过其初始加载逻辑对WebView的影响:

// 修改loadRequest的URL
..loadRequest(Uri.parse('https://www.youtube.com/?t=0'))

3. 强制触发WebView布局刷新

在WebView页面加载完成后,通过重建WebView的key触发布局更新:

// 在NavigationDelegate的onPageFinished中添加:
onPageFinished: (String url) {
  debugPrint('Page finished loading: $url');
  setState(() {
    _key = UniqueKey(); // 生成新key,触发WebView重建
  });
},

4. 针对WKWebView手动启用滚动

通过平台通道直接设置WKWebView的滚动属性(需依赖webview_flutter_wkwebview):

// 初始化controller后添加:
if (controller.platform is WebKitWebViewController) {
  final webKitController = controller.platform as WebKitWebViewController;
  webKitController.getScrollView().then((scrollView) {
    scrollView.isScrollEnabled = true;
    scrollView.bounces = true;
  });
}

内容的提问来源于stack exchange,提问作者Tour4x

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 19:31:00