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
属性的话,可以使用ListView
、SingleChildScrollView
、CustomScrollView
等布局构建页面。
3 弹出软键盘时滚动布局到指定位置(登录按钮下方)
3.1 问题点描述
在此登录页面布局中使用上述2种方式都会存在问题。
- 小屏幕手机中,弹出软键盘会将登录按钮挡住
- 直接使用ListView时,无法将第三方登录布局至于底部
3.2 实现方式
- 在Column布局中使用 ListView + 底部第三方登录
- 在ListView中底部加一个可控制高度的SizeBox
- 设置resizeToAvoidBottomInset属性为false
- 监听软键盘弹出并获取其高度
- 改变ListView中底部SizeBox的高度
- 滑动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 登录页面完整代码
带删除和眼睛按钮的输入框控件文章来源:https://www.toymoban.com/news/detail-440473.html
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);
}
}
登录页面文章来源地址https://www.toymoban.com/news/detail-440473.html
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),
],
),
),
);
}
}
到了这里,关于Flutter 解决App登录页面软键盘遮挡住登录按钮或顶起底部控件的问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!