我的表单中有文本表单字段。当我单击提交按钮时,验证错误显示在文本表单字段下。我想将焦点添加到该特定字段,以便当用户单击“保存”按钮时会弹出该字段。用户不需要向上滚动并输入信息。用户应该能够直接开始打字。在当前情况下,用户会感到困惑并且不知道为什么保存按钮不起作用。 Bcoz 直到向上滚动,错误才未知。
TextFormField(
controller: NoteController,
validator: (String value){
if(value.isEmpty)
{
return "Note can't be null";
}
else
return null;
},
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: const BorderSide(width: 2.0),)),
keyboardType: TextInputType.multiline,
minLines: 5,
maxLines: 5,
onChanged: (value) {
this.note.note = value;
},
),
bool validateAndSave() {
final form = globalFormKey.currentState;
if (form.validate()) {
form.save();
return true;
}
return false;
}
void _save() async {
if (validateAndSave()) {
}
}
您可以使用 FocusNode 和 TextField 来实现这一点。你可以在这里读更多关于它的内容 https://flutter.dev/docs/cookbook/forms/focus
让我向您展示一个示例(尝试过并且有效):
构建方法之前:
final FocusNode noteFocus = FocusNode();
在构建方法中:
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
// Add FocusNode to TextFieldForm
focusNode: noteFocus,
validator: (value) {
if (value == null || value.isEmpty) {
// Request focus in case of error
noteFocus.requestFocus();
return "Note can't be null";
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
print('Validated');
}
},
child: Text('Submit'),
)
],
),
),
如果您还需要什么,请告诉我:)
像这样使用 FocusNode。
FocusNode focusNode = FocusNode();
TextFormField(
controller: NoteController,
focusNode: focusNode,
validator: (String value){
if(value.isEmpty)
{
Focus.of(context).requestFocus(focusNode);
return "Note can't be null";
}
else
return null;
},
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: const BorderSide(width: 2.0),)),
keyboardType: TextInputType.multiline,
minLines: 5,
maxLines: 5,
onChanged: (value) {
this.note.note = value;
},
),
这就是我的整个实现,正是这样做的。最好的部分是它在第一个输入(TextFormField)中设置焦点always,该输入在执行验证器后包含错误。
验证屏幕
import 'package:flutter_267_chat/flutter_resources.dart';
import 'package:flutter_267_chat/external_resources.dart';
import 'package:flutter_267_chat/custom_resources.dart';
final _firebase = FirebaseAuth.instance;
class AuthScreen extends StatefulWidget {
final String title;
final bool isSignup;
const AuthScreen({super.key, this.title = 'Chatty', this.isSignup = true});
@override
State<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
bool _isLogin = true;
final _formKey = GlobalKey<FormState>();
var _isAuthenticating = false;
var _enteredEmail = '';
var _enteredUsername = '';
var _enteredPassword = '';
File? _selectedImageFile;
String imageError = '';
FocusNode focusNodeEmail = FocusNode();
FocusNode focusNodeUsername = FocusNode();
FocusNode focusNodePassword = FocusNode();
List<Map<String, dynamic>> inputsWithErrorsFocusNodes = [];
void onPickImage(File? aImage) => _selectedImageFile = aImage;
void toggleAuth() => setState(() => _isLogin = !_isLogin);
void _logIn() async {
setState(() => _isAuthenticating = true);
try {
final UserCredential userCredential = await _firebase.signInWithEmailAndPassword(email: _enteredEmail, password: _enteredPassword);
} on FirebaseAuthException catch (error) {
if (mounted) {
String? errorMessage;
if (error.code == 'INVALID_LOGIN_CREDENTIALS') {
errorMessage = 'Invalid login credentials';
}
NotificationsHelper.showSnackBarMessage(context, errorMessage ?? error.message ?? 'Authentication failed');
}
}
if (mounted) setState(() => _isAuthenticating = false);
}
void _signUp() async {
setState(() => _isAuthenticating = true);
try {
final UserCredential userCredential = await _firebase.createUserWithEmailAndPassword(email: _enteredEmail, password: _enteredPassword);
final String userImageFileName = '${userCredential.user!.uid}.jpg';
final Reference storageReference = FirebaseStorage.instance.ref().child('user_images').child(userImageFileName);
await storageReference.putFile(_selectedImageFile!);
final String uploadedImageUrl = await storageReference.getDownloadURL();
final DocumentReference<Map<String, dynamic>> userReference = FirebaseFirestore.instance.collection('users').doc(userCredential.user!.uid);
final Map<String, dynamic> userData = {
'username': _enteredUsername,
'email': userCredential.user!.email,
'image_url': uploadedImageUrl,
};
await userReference.set(userData);
} on FirebaseAuthException catch (error) {
if (mounted) {
if (error.code == 'email-already-in-use') {
//
}
NotificationsHelper.showSnackBarMessage(context, error.message ?? 'Authentication failed');
}
}
if (mounted) setState(() => _isAuthenticating = false);
}
void focusInputsWithErrors() {
if (inputsWithErrorsFocusNodes.isNotEmpty) {
inputsWithErrorsFocusNodes.sort((a, b) => (b['index']).compareTo(a['index']));
inputsWithErrorsFocusNodes = inputsWithErrorsFocusNodes.reversed.toList();
final Map<String, dynamic> firstInputFocusNodeMap = inputsWithErrorsFocusNodes.first;
final FocusNode firstInputFocusNode = firstInputFocusNodeMap['node'];
firstInputFocusNode.requestFocus();
}
}
void _submit() async {
inputsWithErrorsFocusNodes.clear();
setState(() => imageError = _selectedImageFile == null ? 'Must pick a picture' : '');
final bool formIsValid = _formKey.currentState!.validate();
final bool signUpImageIsNull = !_isLogin && _selectedImageFile == null;
if (!formIsValid || signUpImageIsNull) {
focusInputsWithErrors();
return;
}
_formKey.currentState!.save();
_isLogin ? _logIn() : _signUp();
if (!context.mounted) return;
}
void _resetForm() {
setState(() => imageError = '');
_formKey.currentState!.reset();
inputsWithErrorsFocusNodes.clear();
}
@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final TextTheme textTheme = themeData.textTheme;
final ColorScheme colorScheme = themeData.colorScheme;
final emailFocusNodeMap = {'node': focusNodeEmail, 'index': 1};
final usernameFocusNodeMap = {'node': focusNodeUsername, 'index': 2};
final passwordFocusNodeMap = {'node': focusNodePassword, 'index': 3};
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
backgroundColor: colorScheme.primary,
body: Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo
Container(
margin: EdgeInsets.only(top: 30, bottom: 20, left: 20, right: 20),
width: 140,
child: Image.asset('assets/images/chat.png'),
),
// Auth Card with the form
Card(
margin: EdgeInsets.all(20),
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// User Image Picker
// if (!_isLogin) UserImagePicker(onPickImage: (d) {}),
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
// reverseDuration: const Duration(milliseconds: 500),
child: !_isLogin
? Column(
children: [
UserImagePicker(onPickImage: onPickImage),
SizedBox(height: 8),
// Image Error
Text(imageError, style: TextStyle(color: Color(0xFFBA1A1A))),
],
)
: null,
),
// Email Input
TextFormField(
decoration: InputDecoration(labelText: 'Email address', helperText: ' '),
focusNode: focusNodeEmail,
keyboardType: TextInputType.emailAddress,
autocorrect: false,
textCapitalization: TextCapitalization.none,
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
inputsWithErrorsFocusNodes.add(emailFocusNodeMap);
return 'The email can not be blank';
}
final bool isValidEmail = EmailValidator.validate(value);
if (!isValidEmail) {
inputsWithErrorsFocusNodes.add(emailFocusNodeMap);
return 'The email must be valid';
}
return null;
},
onSaved: (String? value) {
_enteredEmail = value!.trim();
},
onFieldSubmitted: (String? value) => _submit(),
),
// Username Input
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
// reverseDuration: const Duration(milliseconds: 900),
child: !_isLogin
? TextFormField(
decoration: InputDecoration(labelText: 'Username', helperText: ' '),
enableSuggestions: false,
focusNode: focusNodeUsername,
keyboardType: TextInputType.name,
// autocorrect: false,
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
inputsWithErrorsFocusNodes.add(usernameFocusNodeMap);
return 'The username can not be blank';
}
if (value.trim().length < 3) {
inputsWithErrorsFocusNodes.add(usernameFocusNodeMap);
return 'The username must be at least 3 characters long';
}
return null;
},
onSaved: (String? value) {
_enteredUsername = value!;
},
onFieldSubmitted: (String? value) => _submit(),
)
: null,
),
// Password Input
TextFormField(
decoration: InputDecoration(labelText: 'Password', helperText: ' '),
focusNode: focusNodePassword,
// autocorrect: false,
obscureText: true,
obscuringCharacter: '*',
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
inputsWithErrorsFocusNodes.add(passwordFocusNodeMap);
return 'The password can not be blank';
}
if (value.trim().length < 6) {
inputsWithErrorsFocusNodes.add(passwordFocusNodeMap);
return 'The password must be at least 6 characters long';
}
return null;
},
onSaved: (String? value) {
_enteredPassword = value!;
},
onFieldSubmitted: (String? value) => _submit(),
),
// SizedBox(height: 12),
Stack(
children: [
// Submit & Reset buttons
Opacity(
opacity: _isAuthenticating ? 0 : 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Submit button
ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(backgroundColor: colorScheme.primaryContainer),
child: Text(_isLogin ? 'Log in' : 'Create account'),
),
SizedBox(width: 12),
// Reset button
ElevatedButton(
onPressed: _resetForm,
style: ElevatedButton.styleFrom(backgroundColor: colorScheme.primaryContainer),
child: Text('Reset'),
),
],
),
),
if (_isAuthenticating)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
],
),
],
),
// SizedBox(height: 12),
// Auth Toggling button
Opacity(
opacity: _isAuthenticating ? 0 : 1,
child: TextButton(
onPressed: toggleAuth,
child: Text(
_isLogin ? 'No account? Sign up' : 'Have an account? Log in',
style: textTheme.labelSmall,
),
),
),
],
)),
),
),
),
],
),
),
),
);
}
}
在此示例中,我们有三个 TextFormField(电子邮件、用户名和密码)。重要步骤如下。
第 1 步: 在 StatefulWidget State 类中声明变量。
FocusNode focusNodeEmail = FocusNode();
FocusNode focusNodeUsername = FocusNode();
FocusNode focusNodePassword = FocusNode();
List<Map<String, dynamic>> inputsWithErrorsFocusNodes = [];
第2步:将每个FocusNode与其各自的TextFormField链接起来并触发填写验证。
// Email Input
TextFormField(
decoration: InputDecoration(labelText: 'Email address', helperText: ' '),
focusNode: focusNodeEmail,
keyboardType: TextInputType.emailAddress,
autocorrect: false,
textCapitalization: TextCapitalization.none,
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
inputsWithErrorsFocusNodes.add(emailFocusNodeMap);
return 'The email can not be blank';
}
final bool isValidEmail = EmailValidator.validate(value);
if (!isValidEmail) {
inputsWithErrorsFocusNodes.add(emailFocusNodeMap);
return 'The email must be valid';
}
return null;
},
onSaved: (String? value) {
_enteredEmail = value!.trim();
},
onFieldSubmitted: (String? value) => _submit(),
),
// Username Input
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
// reverseDuration: const Duration(milliseconds: 900),
child: !_isLogin
? TextFormField(
decoration: InputDecoration(labelText: 'Username', helperText: ' '),
enableSuggestions: false,
focusNode: focusNodeUsername,
keyboardType: TextInputType.name,
// autocorrect: false,
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
inputsWithErrorsFocusNodes.add(usernameFocusNodeMap);
return 'The username can not be blank';
}
if (value.trim().length < 3) {
inputsWithErrorsFocusNodes.add(usernameFocusNodeMap);
return 'The username must be at least 3 characters long';
}
return null;
},
onSaved: (String? value) {
_enteredUsername = value!;
},
onFieldSubmitted: (String? value) => _submit(),
)
: null,
),
// Password Input
TextFormField(
decoration: InputDecoration(labelText: 'Password', helperText: ' '),
focusNode: focusNodePassword,
// autocorrect: false,
obscureText: true,
obscuringCharacter: '*',
validator: (String? value) {
if (value == null || value.trim().isEmpty) {
inputsWithErrorsFocusNodes.add(passwordFocusNodeMap);
return 'The password can not be blank';
}
if (value.trim().length < 6) {
inputsWithErrorsFocusNodes.add(passwordFocusNodeMap);
return 'The password must be at least 6 characters long';
}
return null;
},
onSaved: (String? value) {
_enteredPassword = value!;
},
onFieldSubmitted: (String? value) => _submit(),
),
第三步:调用submit时,必须先清空已有的List,然后才能验证。
void _submit() async {
inputsWithErrorsFocusNodes.clear();
setState(() => imageError = _selectedImageFile == null ? 'Must pick a picture' : '');
final bool formIsValid = _formKey.currentState!.validate();
final bool signUpImageIsNull = !_isLogin && _selectedImageFile == null;
if (!formIsValid || signUpImageIsNull) {
focusInputsWithErrors();
return;
}
_formKey.currentState!.save();
_isLogin ? _logIn() : _signUp();
// final newPlace = PlaceModel(title: _enteredTitle, image: selectedImage!, location: selectedPlaceLocation!);
// Navigator.of(context).pop(newPlace);
if (!context.mounted) return;
}
第 4 步: 这是排序、反转然后聚焦的地方。
void focusInputsWithErrors() {
if (inputsWithErrorsFocusNodes.isNotEmpty) {
inputsWithErrorsFocusNodes.sort((a, b) => (b['index']).compareTo(a['index']));
inputsWithErrorsFocusNodes = inputsWithErrorsFocusNodes.reversed.toList();
final Map<String, dynamic> firstInputFocusNodeMap = inputsWithErrorsFocusNodes.first;
final FocusNode firstInputFocusNode = firstInputFocusNodeMap['node'];
firstInputFocusNode.requestFocus();
}
}
第 5 步: 当然,我们还必须清除重置中的地图列表。
void _resetForm() {
setState(() => imageError = '');
_formKey.currentState!.reset();
inputsWithErrorsFocusNodes.clear();
}
视觉效果: