在我的PWA(正在从移动浏览器中查看应用程序)中,我不想关闭键盘。 每次我点击外部或滚动屏幕以查看页面上表单中的其他详细信息时,它都会消失。 即使输入字段失去焦点 - 我想保持键盘在屏幕上可见。 我尝试添加
GestureDetector
、NotificationListener<ScrollNotification>
、TapRegion
但是我只能再次
request focus
并且无法保留它。
尝试时
FocusScope.of(context).requestFocus(_model.textFieldFocusNode)
->键盘关闭然后再次打开 - 这会带来糟糕的用户体验。
除非满足特定条件,否则如何防止键盘关闭。
这可能吗?
我尝试过的事情(但没有成功):
GestureDetector
behavior: HitTestBehavior.translucent,
onTap: () {
// Either requesting focus again
if (!canLoseFocus) {
FocusScope.of(context).requestFocus(_model.textFieldFocusNode);
}
// Or leaving the onTap function empty to not dismiss the keyboard
},
NotificationListener
NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification is ScrollUpdateNotification) {
// Requesting focus again if it is lost
if (!canLoseFocus) {
FocusScope.of(context).requestFocus(_model.textFieldFocusNode);
}
}
return true; // Consume scroll notifications to prevent focus loss
},
这是我的文本输入字段的代码
TextFormField(
controller: _model.textController,
focusNode: _model.textFieldFocusNode,
autofocus: false,
obscureText: false,
decoration: InputDecoration(
labelStyle: FlutterFlowTheme.of(context).labelMedium.override(
fontSize: AppFontSizes.medium/textScaleFac,
color: AppColors.textLightLabel,
fontFamily: 'Poppins',
letterSpacing: 0,
),
hintText: 'Enter Answer Here',
hintStyle: FlutterFlowTheme.of(context).labelMedium.override(
color: AppColors.textLightLabel,
fontSize: AppFontSizes.medium,
fontFamily: 'Poppins',
letterSpacing: 0,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: AppColors.inputDarkBorder,
width: 1,
),
borderRadius: BorderRadius.circular(4),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: AppColors.inputDarkBorder,
width: 1,
),
borderRadius: BorderRadius.circular(4),
),
),
style: FlutterFlowTheme.of(context).bodyMedium.override(
fontFamily: 'Poppins',
fontSize: 14,
color: Colors.white,
letterSpacing: 0,
),
textAlign: TextAlign.start,
maxLines: 6,
validator: _model.textControllerValidator.asValidator(context),
textInputAction: TextInputAction.done, // Shows the "Done" button on the keyboard
onFieldSubmitted: (value) {
setState(() {
canLoseFocus = true;
});
FocusScope.of(context).unfocus();
_model.textFieldFocusNode?.unfocus();
},
),
亲爱的使用FocusNode让下一个字段聚焦于上一个字段的提交 你可以像这样练习
class SignUpScreen extends StatefulWidget {
const SignUpScreen({super.key});
@override
State<SignUpScreen> createState() => _SignUpScreenState();
}
class _SignUpScreenState extends State<SignUpScreen> {
final _formKey = GlobalKey<FormState>();
///first name controller & Focus node
TextEditingController firstNameController = TextEditingController();
late FocusNode firstNameFocusNode;
///last name controller & Focus node
TextEditingController lastNameController = TextEditingController();
late FocusNode lastNameFocusNode;
///email controller & Focus Node
TextEditingController emailController = TextEditingController();
late FocusNode emailFocusNode;
///Phone number Controller & Focus Node
final TextEditingController phoneNumberController = TextEditingController();
late FocusNode phoneNumberFocusNode;
///password Controller & Focus Node
TextEditingController passwordController = TextEditingController();
late FocusNode passwordFocusNode;
///confirm Password Controller & Focus Node
TextEditingController confirmPasswordController = TextEditingController();
late FocusNode confirmPasswordFocusNode;
bool _isObscureText_1 = true;
bool _isObscureText_2 = true;
late AuthProvider authProvider;
late SocialAuthProvider socialAuthProvider;
bool readOnly = false;
File? _image;
final ImagePicker _picker = ImagePicker();
bool ageAgrementChecked = false;
bool textAgrementChecked = false;
bool isPasswordMatch = true;
final maskFormatter = MaskTextInputFormatter(
mask: '+# (###) ###-####',
filter: {"#": RegExp(r'[0-9]')},
type: MaskAutoCompletionType.lazy,
);
@override
void initState() {
firstNameFocusNode = FocusNode();
lastNameFocusNode = FocusNode();
emailFocusNode = FocusNode();
phoneNumberFocusNode = FocusNode();
passwordFocusNode = FocusNode();
confirmPasswordFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
firstNameController.dispose();
lastNameController.dispose();
emailController.dispose();
phoneNumberController.dispose();
passwordController.dispose();
confirmPasswordController.dispose();
firstNameFocusNode.dispose();
lastNameFocusNode.dispose();
emailFocusNode.dispose();
phoneNumberFocusNode.dispose();
passwordFocusNode.dispose();
confirmPasswordFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
authProvider = Provider.of<AuthProvider>(context);
socialAuthProvider = Provider.of<SocialAuthProvider>(context);
return LoadingOverlay(
isLoading: authProvider.registerLoading || socialAuthProvider.isLoading,
opacity: 0.5,
progressIndicator: const SpinKitPulsingGrid(
color: AppColors.primary,
size: 50.0,
),
child: Scaffold(
backgroundColor: AppColors.white,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(Amount.screenMargin),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CusTextField(
isValidator: true,
focusNode: firstNameFocusNode,
controller: firstNameController,
textInputType: TextInputType.name,
readOnly: readOnly,
labelText:
getTranslated(context, LangConst.firstName).toString(),
validatorMessage: "Please enter user first name",
nextNode: lastNameFocusNode,
),
const HeightBox(15),
CusTextField(
isValidator: true,
focusNode: lastNameFocusNode,
controller: lastNameController,
textInputType: TextInputType.name,
readOnly: readOnly,
labelText:
getTranslated(context, LangConst.lastName).toString(),
validatorMessage: 'Please enter user last name',
nextNode: emailFocusNode,
),
const HeightBox(15),
CusTextField(
isValidator: true,
focusNode: emailFocusNode,
controller: emailController,
textInputType: TextInputType.emailAddress,
readOnly: readOnly,
labelText:
getTranslated(context, LangConst.email).toString(),
validatorMessage: 'Please enter a valid email',
customValidator: (input) {
if (input == null || input.isEmpty) {
return 'This field cannot be empty.';
}
final emailRegex = RegExp(
r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$");
if (emailRegex.hasMatch(input)) {
return null;
} else {
return 'Please enter a valid email address.';
}
},
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r"\s")),
],
nextNode: phoneNumberFocusNode,
),
const HeightBox(15),
CusTextField(
isValidator: true,
focusNode: phoneNumberFocusNode,
controller: phoneNumberController,
textInputType: TextInputType.phone,
readOnly: readOnly,
labelText:
getTranslated(context, LangConst.labelPhoneNumber)
.toString(),
validatorMessage: 'Please enter a valid phone number',
customValidator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a valid phone number';
}
if (value.length < 10) {
return 'Please enter a valid Mobile number.';
}
// final phonePattern = RegExp(
// r'^\+\d{1,3}\s\(\d{3}\)\s\d{3}-\d{4}$',
// );
// if (!phonePattern.hasMatch(value)) {
// return 'Please enter a valid phone number';
// }
return null;
},
isPhoneNumber: true,
maskTextInputFormatter: maskFormatter,
),
const HeightBox(15),
CusTextField(
isValidator: true,
focusNode: passwordFocusNode,
controller: passwordController,
obscureText: _isObscureText_1,
readOnly: readOnly,
validatorMessage: 'Please enter your password',
labelText:
getTranslated(context, LangConst.password).toString(),
suffixIcon: InkWell(
onTap: () {
setState(() {
_isObscureText_1 = !_isObscureText_1;
});
},
child: Icon(
_isObscureText_1
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
),
),
nextNode: confirmPasswordFocusNode,
onChanged: (p0) => _matchPassword(),
),
const HeightBox(15),
CusTextField(
focusNode: confirmPasswordFocusNode,
controller: confirmPasswordController,
obscureText: _isObscureText_2,
readOnly: readOnly,
validatorMessage: 'Please enter your password again',
labelText: getTranslated(context, LangConst.confirmPassword)
.toString(),
suffixIcon: InkWell(
onTap: () {
setState(() {
_isObscureText_2 = !_isObscureText_2;
});
},
child: Icon(
_isObscureText_2
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: isPasswordMatch
? null
: Colors.red.withOpacity(.65),
),
),
onChanged: (p0) => _matchPassword(),
borderColor:
isPasswordMatch ? null : Colors.red.withOpacity(.65),
focusBorderColor:
isPasswordMatch ? null : Colors.red.withOpacity(.65),
lableColors:
isPasswordMatch ? null : Colors.red.withOpacity(.65),
floatingLableColors:
isPasswordMatch ? null : Colors.red.withOpacity(.65),
),
!isPasswordMatch
? Container(
width: double.infinity,
child: Text(
"please confirm your password",
style: TextStyle(
fontSize: 10,
color: Colors.red.withOpacity(.65)),
textAlign: TextAlign.left,
),
)
: SizedBox(),
const HeightBox(35),
ElevatedButton(
onPressed: ageAgrementChecked &&
textAgrementChecked &&
isPasswordMatch
? () async {
try {
if (DevelopmentHelper.bypass) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) =>
const OtpVerificationScreen(
phoneNumber: "",
action: 'signup',
),
));
} else {
if (_formKey.currentState!.validate()) {
Map<String, dynamic> body = {
'first_name': firstNameController.text,
'last_name': lastNameController.text,
'email': emailController.text,
'password': passwordController.text,
'phone_number': phoneNumberController.text,
};
authProvider
.getRegistered(context, body)
.then((value) {
if (value.data?.success == true) {
Navigator.of(context)
.pushReplacement(MaterialPageRoute(
builder: (context) =>
OtpVerificationScreen(
phoneNumber: emailController.text,
action: "signup",
),
));
}
});
}
}
} catch (e) {
print("Error: $e");
}
}
: null,
style: AppButtonStyle.filledMedium.copyWith(
minimumSize: MaterialStatePropertyAll(
Size(MediaQuery.of(context).size.width, 50),
),
backgroundColor: (ageAgrementChecked &&
textAgrementChecked &&
isPasswordMatch)
? AppButtonStyle.filledMedium.backgroundColor
: MaterialStateProperty.all<Color>(
AppColors.disabledbutton),
),
child: authProvider.registerLoading
? const Center(
child: CircularProgressIndicator(
backgroundColor: AppColors.white,
)) // Show loading indicator
: Text(
getTranslated(context, LangConst.proceed)
.toString(),
style: Theme.of(context)
.textTheme
.headlineSmall!
.copyWith(color: AppColors.white),
),
),
HeightBox(20),
],
),
),
),
),
),
);
}
_matchPassword() => setState(() => isPasswordMatch =
passwordController.text == confirmPasswordController.text);
}
这是我的自定义文本字段组件,是我经过大量努力和经验制作的,现在我认为它已经成熟了: 作为初学者可能对你有很大帮助
extension EmailValidator on String {
bool isValidEmail() {
return RegExp(
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')
.hasMatch(this);
}
}
class CusTextField extends StatelessWidget {
final TextEditingController? controller;
final String? hintText;
final TextInputType? textInputType;
final int? maxLine;
final FocusNode? focusNode;
final FocusNode? nextNode;
final bool? autoFocus;
final TextInputAction? textInputAction;
final bool isPhoneNumber;
final bool isValidator;
final String? validatorMessage;
final Color? fillColor;
final TextCapitalization capitalization;
final bool isBorder;
final TextAlign? textAlign;
final bool isEnable;
final double cursorWidth;
final String? labelText;
final Function(String?)? onChanged;
final bool? readOnly;
final List<FilteringTextInputFormatter>? inputFormatters;
final bool? obscureText;
final Color? borderColor;
final Color? focusBorderColor;
final Widget? suffixIcon;
final Widget? prefixIcon;
final int? maxLength;
final TextStyle? textStyle;
final TextStyle? hintStyle;
final Color? cursorColor;
final Color? lableColors;
final Color? floatingLableColors;
final MaskTextInputFormatter? maskTextInputFormatter;
final FormFieldValidator<String>? customValidator;
final bool? alignLabelWithHint;
final TextStyle? labelStyle;
final Function(String)? onFieldSubmitted;
const CusTextField(
{Key? key,
this.controller,
this.hintText,
this.textInputType,
this.maxLine,
this.focusNode,
this.nextNode,
this.autoFocus = false,
this.textInputAction,
this.isPhoneNumber = false,
this.isValidator = false,
this.validatorMessage,
this.capitalization = TextCapitalization.none,
this.fillColor,
this.isBorder = false,
this.textAlign,
this.isEnable = true,
this.cursorWidth = 1.5,
this.labelText,
this.onChanged,
this.readOnly,
this.inputFormatters,
this.obscureText,
this.borderColor,
this.focusBorderColor,
this.suffixIcon,
this.prefixIcon,
this.maxLength,
this.textStyle,
this.hintStyle,
this.cursorColor,
this.lableColors,
this.floatingLableColors,
this.maskTextInputFormatter,
this.customValidator,
this.alignLabelWithHint,
this.labelStyle,
this.onFieldSubmitted})
: super(key: key);
@override
Widget build(context) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
// border: isBorder
// ? (Border.all(width: 1, color: borderColor ?? AppColors.primary))
// : null,
// color: Theme.of(context).highlightColor,
borderRadius: BorderRadius.circular(6),
// boxShadow: [
// BoxShadow(
// color: Colors.grey.withOpacity(0.1),
// spreadRadius: 1,
// blurRadius: 3,
// offset: const Offset(0, 1)) // changes position of shadow
// ],
),
child: TextFormField(
obscureText: obscureText ?? false,
style: textStyle,
onChanged: onChanged,
cursorWidth: cursorWidth,
autofocus: autoFocus!,
textAlign: textAlign != null ? textAlign! : TextAlign.start,
controller: controller,
maxLines: maxLine ?? 1,
textCapitalization: capitalization,
maxLength: maxLength ?? (isPhoneNumber ? 18 : null),
focusNode: focusNode,
keyboardType: textInputType ?? TextInputType.text,
enabled: isEnable,
initialValue: null,
textInputAction: textInputAction ?? TextInputAction.next,
onFieldSubmitted: onFieldSubmitted ??
(v) {
FocusScope.of(context).requestFocus(nextNode);
},
inputFormatters: inputFormatters ??
[
isPhoneNumber
? FilteringTextInputFormatter.digitsOnly
: FilteringTextInputFormatter.singleLineFormatter,
if (maskTextInputFormatter != null) maskTextInputFormatter!
],
validator: customValidator ??
(input) {
if (input == null || input.isEmpty) {
if (isValidator) {
return validatorMessage ?? "";
}
}
return null;
},
readOnly: readOnly ?? false,
cursorColor: AppColors.subText,
decoration: InputDecoration(
floatingLabelStyle:
TextStyle(color: lableColors ?? AppColors.surface),
labelStyle: labelStyle ??
TextStyle(color: floatingLableColors ?? AppColors.surface),
labelText: labelText,
alignLabelWithHint: alignLabelWithHint ?? false,
hintText: hintText ?? null,
filled: false,
fillColor: AppColors.transparent,
contentPadding:
const EdgeInsets.symmetric(vertical: 12.0, horizontal: 15),
isDense: true,
counterText: '',
focusedBorder: OutlineInputBorder(
borderSide:
BorderSide(color: focusBorderColor ?? AppColors.subText)),
hintStyle: hintStyle ?? TextStyle(color: AppColors.subText),
errorStyle: const TextStyle(height: 1.5),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide:
BorderSide(width: 1, color: borderColor ?? AppColors.subText),
),
suffixIcon: suffixIcon,
prefixIcon: prefixIcon),
),
);
}
}