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
相关产品推荐
相关产品推荐

