Flutter 解决App登录页面软键盘遮挡住登录按钮或顶起底部控件的问题


1 软键盘问题点对比效果图

问题点

在这里插入图片描述

最终效果图

在这里插入图片描述


2 解决软键盘将底部布局顶上去的问题

2.1 方式一:修改resizeToAvoidBottomInset属性

问题点: 当前使用的是Column布局,弹窗软键盘后页面超出范围。

A RenderFlex overflowed by 0.533 pixels on the bottom.

解决方式

Scaffold或者CupertinoPageScaffold中设置resizeToAvoidBottomInset为false

  @override
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      backgroundColor: Colors.white,
      resizeToAvoidBottomInset:false,
      body: ...,
    );
  }

2.2 方式二:使用可滑动布局

不修改resizeToAvoidBottomInset属性的话,可以使用ListViewSingleChildScrollViewCustomScrollView等布局构建页面。

3 弹出软键盘时滚动布局到指定位置(登录按钮下方)

3.1 问题点描述

在此登录页面布局中使用上述2种方式都会存在问题。

  • 小屏幕手机中,弹出软键盘会将登录按钮挡住
  • 直接使用ListView时,无法将第三方登录布局至于底部

在这里插入图片描述


3.2 实现方式

  1. 在Column布局中使用 ListView + 底部第三方登录
  2. 在ListView中底部加一个可控制高度的SizeBox
  3. 设置resizeToAvoidBottomInset属性为false
  4. 监听软键盘弹出并获取其高度
  5. 改变ListView中底部SizeBox的高度
  6. 滑动ListView到指定位置(使用GlobalKey来确定)

简要代码

class _LoginPageState extends State<LoginPage> with WidgetsBindingObserver {
    
    

  // 软键盘高度
  double _keyboardHeight = 0;

  // 可控制ListView滑动
  final _scrollController = ScrollController();

  // 用于获取目标Widget的位置坐标
  final _targetWidgetKey = GlobalKey();

  @override
  void initState() {
    
    
    super.initState();
    // 添加监听,didChangeMetrics
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    
    
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  // 当应用程序的尺寸发生变化时会调用
  @override
  void didChangeMetrics() {
    
    
    // 获取页面高度
    var pageHeight = MediaQuery.of(context).size.height;
    if (pageHeight <= 0) {
    
    
      return;
    }

    // 软键盘顶部  px
    final keyboardTopPixels =
        window.physicalSize.height - window.viewInsets.bottom;
    // 转换为 dp
    final keyboardTopPoints = keyboardTopPixels / window.devicePixelRatio;
    // 软键盘高度
    final keyboardHeight = pageHeight - keyboardTopPoints;

    setState(() {
    
    
      _keyboardHeight = keyboardHeight;
    });

    if (keyboardHeight <= 0) {
    
    
      return;
    }
    // 获取目标位置的坐标
    RenderBox? renderBox =
        _targetWidgetKey.currentContext?.findRenderObject() as RenderBox?;
    if (renderBox == null) {
    
    
      return;
    }
    // 转换为全局坐标
    final bottomOffset =
        renderBox.localToGlobal(Offset(0, renderBox.size.height));
    final targetDy = bottomOffset.dy;
    // 获取要滚动的距离
    // 即被软键盘挡住的那段距离 加上 _scrollController.offset 已经滑动过的距离
    final offsetY =
        keyboardHeight - (pageHeight - targetDy) + _scrollController.offset;
    // 滑动到指定位置
    if (offsetY > 0) {
    
    
      _scrollController.animateTo(
        offsetY,
        duration: kTabScrollDuration,
        curve: Curves.ease,
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      backgroundColor: Colors.white,
      // 避免底部布局被软键盘顶上来
      resizeToAvoidBottomInset: false,
      body: GestureDetector(
        behavior: HitTestBehavior.opaque,
        // 点击空白位置关闭软键盘
        onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
        child: Column(
          children: [
            Expanded(
              child: ListView(
                controller: _scrollController,
                children: [
                  ...
                  // 一系列输入框Widget
                  ...
                  // 弹出的软键盘位于此Widget之下
                  Row(
                    key: _targetWidgetKey,
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                     ...
                    ],
                  ),
                  // 动态变换高度,保证ListView可滑动
                  SizedBox(height: _keyboardHeight)
                ],
              ),
            ),
            /// 底部的布局
            Row(
              children: const [
              ...
              ],
            ),
          ],
        ),
      ),
    );
  }
}

效果图
在这里插入图片描述


4 登录页面完整代码

带删除和眼睛按钮的输入框控件

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class UserTextField extends StatefulWidget {
    
    
  final TextEditingController controller;
  final TextInputType? keyboardType;
  final String? placeholder;
  final bool usedInPassword;
  final Widget? suffixWidget;
  final int? maxLength;

  const UserTextField({
    
    
    Key? key,
    required this.controller,
    this.keyboardType,
    this.placeholder,
    this.usedInPassword = false,
    this.suffixWidget,
    this.maxLength,
  }) : super(key: key);

  @override
  State<StatefulWidget> createState() => _UserTextFieldState();
}

class _UserTextFieldState extends State<UserTextField> {
    
    
  var _showClearIcon = false;
  var _showEyeIcon = false;
  late bool _obscurePassword;

  @override
  void initState() {
    
    
    super.initState();
    _obscurePassword = widget.usedInPassword;

    widget.controller.addListener(() {
    
    
      var isNotEmpty = widget.controller.text.isNotEmpty;
      setState(() {
    
    
        _showEyeIcon = isNotEmpty;
        _showClearIcon = isNotEmpty;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    
    
    return CupertinoTextField(
      controller: widget.controller,
      keyboardType: widget.keyboardType,
      onChanged: (_) {
    
    },
      placeholder: widget.placeholder,
      style: const TextStyle(color: Colors.black),
      placeholderStyle: const TextStyle(color: Colors.grey),
      maxLength: widget.maxLength,
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
      decoration: BoxDecoration(
        color: Colors.white,
        border: Border.all(color: Colors.grey, width: 0.5),
        borderRadius: BorderRadius.circular(26),
      ),
      obscureText: _obscurePassword,
      obscuringCharacter: "*",
      suffix: widget.suffixWidget ??
          (widget.usedInPassword ? _buildPasswordEyeIcon() : _buildClearIcon()),
    );
  }

  Widget _buildClearIcon() {
    
    
    return _showClearIcon
        ? CupertinoButton(
            padding: const EdgeInsets.fromLTRB(0, 0, 8, 0),
            child: const Icon(Icons.clear, size: 18),
            onPressed: () => widget.controller.clear(),
          )
        : const SizedBox(width: 8.0);
  }

  Widget _buildPasswordEyeIcon() {
    
    
    return _showEyeIcon
        ? CupertinoButton(
            padding: const EdgeInsets.fromLTRB(0, 0, 8, 0),
            child: Icon(
              _obscurePassword ? Icons.visibility_off : Icons.visibility,
              size: 18,
            ),
            onPressed: () {
    
    
              setState(() => _obscurePassword = !_obscurePassword);
            },
          )
        : const SizedBox(width: 8.0);
  }
}

登录页面

import 'dart:ui';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'user_text_field.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
    
    
  const MyApp({
    
    Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    
    
    return const CupertinoApp(
      theme: CupertinoThemeData(
        primaryColor: Colors.red,
        scaffoldBackgroundColor: Colors.white,
      ),
      debugShowCheckedModeBanner: false,
      home: LoginPage(),
    );
  }
}

class LoginPage extends StatefulWidget {
    
    
  const LoginPage({
    
    Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> with WidgetsBindingObserver {
    
    
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _codeController = TextEditingController();

  // 软键盘高度
  double _keyboardHeight = 0;

  // 可控制ListView滑动
  final _scrollController = ScrollController();

  // 用于获取目标Widget的位置坐标
  final _targetWidgetKey = GlobalKey();

  @override
  void initState() {
    
    
    super.initState();
    // 添加监听,didChangeMetrics
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    
    
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  // 当应用程序的尺寸发生变化时会调用
  @override
  void didChangeMetrics() {
    
    
    // 获取页面高度
    var pageHeight = MediaQuery.of(context).size.height;
    if (pageHeight <= 0) {
    
    
      return;
    }

    // 软键盘顶部  px
    final keyboardTopPixels =
        window.physicalSize.height - window.viewInsets.bottom;
    // 转换为 dp
    final keyboardTopPoints = keyboardTopPixels / window.devicePixelRatio;
    // 软键盘高度
    final keyboardHeight = pageHeight - keyboardTopPoints;

    setState(() {
    
    
      _keyboardHeight = keyboardHeight;
    });
    if (keyboardHeight <= 0) {
    
    
      return;
    }
    // 获取目标位置的坐标
    RenderBox? renderBox =
        _targetWidgetKey.currentContext?.findRenderObject() as RenderBox?;
    if (renderBox == null) {
    
    
      return;
    }
    // 转换为全局坐标
    final bottomOffset =
        renderBox.localToGlobal(Offset(0, renderBox.size.height));
    final targetDy = bottomOffset.dy;
    // 获取要滚动的距离
    // 即被软键盘挡住的那段距离 加上 _scrollController.offset 已经滑动过的距离
    final offsetY =
        keyboardHeight - (pageHeight - targetDy) + _scrollController.offset;
    // 滑动到指定位置
    if (offsetY > 0) {
    
    
      _scrollController.animateTo(
        offsetY,
        duration: kTabScrollDuration,
        curve: Curves.ease,
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      backgroundColor: Colors.white,
      resizeToAvoidBottomInset: false,
      body: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
        child: Column(
          children: [
            Expanded(
              child: ListView(
                controller: _scrollController,
                children: [
                  SafeArea(
                    child: Align(
                      alignment: Alignment.centerRight,
                      child: CupertinoButton(
                        onPressed: () {
    
    },
                        child: const Icon(CupertinoIcons.clear, size: 24),
                      ),
                    ),
                  ),
                  const Padding(
                    padding: EdgeInsets.fromLTRB(16, 16, 16, 16),
                    child: Text(
                      '你好,\n欢迎使用Flutter App',
                      style: TextStyle(
                        fontSize: 24,
                        color: Colors.black,
                        fontWeight: FontWeight.w500,
                      ),
                    ),
                  ),
                  const SizedBox(height: 40),
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16),
                    child: UserTextField(
                      controller: _emailController,
                      keyboardType: TextInputType.emailAddress,
                      placeholder: '请输入邮箱',
                    ),
                  ),
                  const SizedBox(height: 16.0),
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16),
                    child: UserTextField(
                      controller: _passwordController,
                      keyboardType: TextInputType.visiblePassword,
                      usedInPassword: true,
                      placeholder: '请输入密码',
                    ),
                  ),
                  const SizedBox(height: 16.0),
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16),
                    child: UserTextField(
                      controller: _codeController,
                      keyboardType: TextInputType.number,
                      placeholder: '请输入6位验证码',
                    ),
                  ),
                  const SizedBox(height: 16.0),
                  CupertinoButton(
                    padding: const EdgeInsets.all(16),
                    child: Container(
                      height: 44,
                      width: double.infinity,
                      alignment: Alignment.center,
                      decoration: const BoxDecoration(
                        color: Colors.red,
                        borderRadius: BorderRadius.all(Radius.circular(22)),
                      ),
                      child: const Text(
                        '登录',
                        style: TextStyle(color: Colors.white),
                      ),
                    ),
                    onPressed: () {
    
    },
                  ),
                  Row(
                    key: _targetWidgetKey,
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      CupertinoButton(
                        minSize: 24,
                        alignment: Alignment.topCenter,
                        padding: const EdgeInsets.symmetric(
                            horizontal: 18, vertical: 0),
                        onPressed: () {
    
    },
                        child: const Text(
                          '忘记密码?',
                          style: TextStyle(fontSize: 14),
                        ),
                      ),
                      CupertinoButton(
                        minSize: 24,
                        alignment: Alignment.topCenter,
                        padding: const EdgeInsets.symmetric(
                            horizontal: 18, vertical: 0),
                        onPressed: () {
    
    },
                        child:
                            const Text('立即注册', style: TextStyle(fontSize: 14)),
                      ),
                    ],
                  ),
                  SizedBox(height: _keyboardHeight)
                ],
              ),
            ),
            Row(
              children: const [
                SizedBox(width: 16),
                Expanded(child: Divider()),
                SizedBox(width: 8),
                Text(
                  '其它登录方式',
                  style: TextStyle(fontSize: 13, color: Colors.grey),
                ),
                SizedBox(width: 8),
                Expanded(child: Divider()),
                SizedBox(width: 16),
              ],
            ),
            Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                CupertinoButton(
                  onPressed: () {
    
    },
                  child: const Icon(Icons.facebook, size: 44),
                ),
                const SizedBox(width: 32),
                CupertinoButton(
                  onPressed: () {
    
    },
                  child: const Icon(Icons.apple, size: 44),
                ),
              ],
            ),
            const SizedBox(height: 12),
          ],
        ),
      ),
    );
  }
}

猜你喜欢

转载自blog.csdn.net/ww897532167/article/details/125633146