Flutter中同时显示两个Alert Dialog时焦点异常问题求助
Flutter 多弹窗焦点异常问题
我在Flutter中尝试同时显示两个AlertDialog,打开第二个Dialog时,焦点没有落在第二个Dialog内的Widget上,反而停留在第一个已打开的Dialog的Widget上。想请教这是框架问题,还是Flutter本就不允许同时打开两个Dialog?
问题演示

相关代码
第一个Dialog代码
showDialog( context: context, builder: (_) => AlertDialog( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( width: 16, height: 32, decoration: BoxDecoration( color: AppColors.colorFFBC99, borderRadius: BorderRadius.circular(4), ), ), const SizedBox(width: 16), Text( S.addaRecipe, style: Theme.of(context).textTheme.headlineSmall!.copyWith( fontWeight: FontWeight.w600, color: Colors.black, ), ), ], ), SizedBox( width: 300, child: Row( children: [ Text( S.recipeBy, style: Theme.of(context) .textTheme .labelLarge! .copyWith(fontWeight: FontWeight.w500), ), const SizedBox(width: 8), Expanded(child: CustomMyDropDownButton(label: 'dsfsdsf')), ], ), ) ], ), content: Form( child: Column( mainAxisSize: MainAxisSize.min, children: [ Expanded( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 16), Text( S.addPhotos, style: Theme.of(context) .textTheme .labelLarge! .copyWith(fontWeight: FontWeight.w500), ), const SizedBox(height: 8), Container( width: double.infinity, color: Colors.white, child: Wrap( children: [ for (int i = 0; i < 10; i++) Padding( padding: const EdgeInsets.all(8.0), child: SizedBox( width: 200, height: 200, child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( S.recipeNetworkImage, fit: BoxFit.cover, ), ), ), ), Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () {}, child: Container( decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8), ), width: 200, height: 200, child: const Center(child: Icon(Icons.add, size: 32)), ), ), ), ], ), ), const SizedBox(height: 16), CustomTextField( showLabel: true, label: S.recipeName, validator: (v) { if (v!.isEmpty) return 'This field is required'; return null; }, ), const SizedBox(height: 16), CustomTextField( showLabel: true, label: S.description, validator: (v) { if (v!.isEmpty) return 'This field is required'; return null; }, ), const SizedBox(height: 32), Row( children: [ Text( S.nutritionalInfo, style: Theme.of(context) .textTheme .labelLarge! .copyWith(fontWeight: FontWeight.w500), ), IconButton( onPressed: () { showDialog( context: context, builder: (_) => AlertDialog( title: const Text(S.addNutrition), content: Column( mainAxisSize: MainAxisSize.min, children: const [ CustomTextField(showLabel: true, label: S.name), CustomTextField(showLabel: true, label: S.value), ], ), actions: [ CustomOutlinedButton( name: S.close, onPress: () => context.pop()), CustomElevatedButton(name: S.add, onPress: () {}), ], ), ); }, icon: const Icon(Icons.add), ) ], ), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), color: Colors.grey.shade100, child: Text("Nutrition Info...."), ), const SizedBox(height: 32), Text( S.steps, style: Theme.of(context) .textTheme .labelLarge! .copyWith(fontWeight: FontWeight.w500), ), const SizedBox(height: 8), Container( color: AppColors.colorF4F4F4, height: 300, width: MediaQuery.of(context).size.width * .8, child: HtmlEditor( controller: HtmlEditorController(), htmlEditorOptions: const HtmlEditorOptions(hint: "Steps...."), otherOptions: const OtherOptions( height: 400, decoration: BoxDecoration(color: Colors.red)), ), ), const SizedBox(height: 32), ], ), ), ), const SizedBox(height: 32), Stack( children: [ Text( '', maxLines: 3, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: Colors.red), ), Text( '', maxLines: 3, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: Colors.green), ), ], ), ], ), ), actions: [ CustomOutlinedButton(name: S.close, onPress: () => context.pop()), CustomElevatedButton(name: S.add, onPress: () {}), ], ), );
第二个Dialog代码
showDialog( context: context, builder: (_) => AlertDialog( title: const Text(S.addNutrition), content: Column( mainAxisSize: MainAxisSize.min, children: const [ CustomTextField(showLabel: true, label: S.name), CustomTextField(showLabel: true, label: S.value), ], ), actions: [ CustomOutlinedButton(name: S.close, onPress: () => context.pop()), CustomElevatedButton(name: S.add, onPress: () {}), ], ), );
CustomTextField代码
import 'package:flutter/material.dart'; class CustomTextField extends StatefulWidget { final String? label; final TextEditingController? controller; final String? Function(String?)? validator; final Function(String?)? onSaved; final bool showLabel; final bool enableSuggestions; final bool autocorrect; final TextInputType? keyBoardType; final Function(String)? onChange; final bool showSuffixIcon; const CustomTextField({ Key? key, this.label, this.controller, this.validator, this.onSaved, this.enableSuggestions = true, this.autocorrect = true, this.keyBoardType, this.onChange, this.showSuffixIcon = false, this.showLabel = false, }) : super(key: key); @override State<CustomTextField> createState() => _CustomTextFieldState(); } class _CustomTextFieldState extends State<CustomTextField> { final textFieldFocusNode = FocusNode(); bool _obscured = false; @override void initState() { if (widget.showSuffixIcon) { _obscured = true; } super.initState(); } void _toggleObscured() { setState(() { _obscured = !_obscured; if (textFieldFocusNode.hasPrimaryFocus) return; textFieldFocusNode.canRequestFocus = false; }); } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ widget.showLabel ? Text( widget.label!, style: Theme.of(context) .textTheme .labelLarge! .copyWith(fontWeight: FontWeight.w500), ) : const SizedBox(), SizedBox(height: widget.showLabel ? 8 : 0), TextFormField( controller: widget.controller, validator: widget.validator, onSaved: widget.onSaved, onChanged: widget.onChange, focusNode: textFieldFocusNode, obscureText: _obscured, enableSuggestions: widget.enableSuggestions, autocorrect: widget.autocorrect, keyboardType: widget.keyBoardType, decoration: InputDecoration( isDense: true, enabledBorder: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(8)), borderSide: BorderSide(color: Colors.black12), ), focusedBorder: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(8)), borderSide: BorderSide(color: Colors.black12), ), errorBorder: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(8)), borderSide: BorderSide(color: Colors.red), ), border: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(8)), borderSide: BorderSide(color: Colors.black12), ), labelText: widget.showLabel ? null : widget.label, suffixIcon: widget.showSuffixIcon ? Padding( padding: const EdgeInsets.fromLTRB(0, 0, 4, 0), child: GestureDetector( onTap: _toggleObscured, child: Icon( _obscured ? Icons.visibility_rounded : Icons.visibility_off_rounded, size: 24, color: Colors.grey, ), ), ) : null, ), ), ], ); } }
问题解答
Flutter 默认允许叠加显示多个Dialog,但焦点管理逻辑是焦点会保留在最先获取焦点的元素上,除非手动处理焦点转移。你遇到的问题并非框架Bug,而是未对第二个Dialog的焦点进行主动控制导致的。
解决方法
1. 给第二个Dialog的输入框添加自动聚焦
修改CustomTextField,新增autofocus参数并传递给内部的TextFormField:
// CustomTextField参数新增 final bool autofocus; const CustomTextField({ // ...其他参数 this.autofocus = false, }) : super(key: key); // TextFormField中添加 TextFormField( // ...其他属性 autofocus: widget.autofocus, )
然后在第二个Dialog中给第一个输入框设置autofocus: true:
CustomTextField( showLabel: true, label: S.name, autofocus: true, ),
2. 打开第二个Dialog前清除旧焦点
在触发第二个Dialog的按钮点击事件中,先清除当前焦点:
IconButton( onPressed: () { FocusManager.instance.primaryFocus?.unfocus(); showDialog(/* 第二个Dialog代码 */); }, icon: const Icon(Icons.add), )
3. 手动请求焦点
在CustomTextField的initState中延迟请求焦点:
@override void initState() { super.initState(); if (widget.showSuffixIcon) { _obscured = true; } WidgetsBinding.instance.addPostFrameCallback((_) { FocusScope.of(context).requestFocus(textFieldFocusNode); }); }
额外说明
虽然Flutter支持多Dialog叠加,但从用户体验角度,不建议同时展示多个弹窗,易造成界面混乱。更优方案是改用底部Sheet、侧边栏,或把子弹窗的功能整合到父弹窗的内容区域中。
内容的提问来源于stack exchange,提问作者Konark Shivam
相关产品推荐
相关产品推荐

