在点击外部或在颤动中滚动页面时无法在移动屏幕上保留键盘

问题描述 投票:0回答:1

在我的PWA(正在从移动浏览器中查看应用程序)中,我不想关闭键盘。 每次我点击外部或滚动屏幕以查看页面上表单中的其他详细信息时,它都会消失。 即使输入字段失去焦点 - 我想保持键盘在屏幕上可见。 我尝试添加

GestureDetector
NotificationListener<ScrollNotification>
TapRegion

但是我只能再次

request focus
并且无法保留它。

尝试时

FocusScope.of(context).requestFocus(_model.textFieldFocusNode)
->键盘关闭然后再次打开 - 这会带来糟糕的用户体验。

除非满足特定条件,否则如何防止键盘关闭。

这可能吗?

我尝试过的事情(但没有成功):

  1. 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
},
  1. 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
    },
  1. 此外,包装了小部件 TapRegion 以防止输入字段失去焦点

这是我的文本输入字段的代码

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();
    },
),
flutter keyboard focus progressive-web-apps
1个回答
0
投票

亲爱的使用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),
      ),
    );
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.