이번 포스트는 플러터에서 키보드 위에 떠있는 버튼 만드는 방법을 정리하겠습니다.

위와 같은 화면에서 TextField를 클릭해서 Keyboard가 올라가면 아래와 같은 화면과 에러를 만나실 수 있습니다.

======== Exception caught by rendering library =====================================================
The following assertion was thrown during layout:
A RenderFlex overflowed by 276 pixels on the bottom.
에러가 발생한 이유는 resizeToAvoidBottomInset 때문입니다.
resizeToAvoidBottomInset는 Scaffold의 body나 floating button이 가려지는 것을 막기위해 스스로 크기를 조절하고 모두 보이게 할지를 결정하는 프로퍼티입니다.
resizeToAvoidBottomInset의 Default값이 true이고, Scaffold의 body가 더 이상 화면을 밀어서 조절할 공간이 없기 때문에 overflow에러가 발생하는 것입니다.
그러면 resizeToAvoidBottomInset의 속성을 false로 주어보겠습니다.

에러는 없어졌지만 키보드가 버튼을 가려서 UX적으로 매우 좋지않습니다.
버튼을 키보드가 가리지 않도록 하기위해, 항상 키보드 위에 떠있도록 만들겠습니다.
MediaQuery.of(context).viewInsets은 SystemUI에 의해 가려진 부분의 크기를 받아옵니다. 결국, Keyboard에 의해 가려진 부분의 크기를 구하려면 MediaQuery.of(context).viewInsets.bottom을 통해 구할 수 있습니다.
즉, Scaffold의 bottomsheet 속성의 패딩값에 MediaQuery.of(context).viewInsets.bottom 을 넣어주면 됩니다.
이렇게 하면, 키보드가 떠있지 않다면 padding값이 0이 return되어 항상 맨아래에 위치할 것이고 키보드가 떠있다면 키보드의 크기가 return되어 키보드 위에 위치할 것입니다.
실행결과
수정한 코드는 아래와 같습니다.
bottomSheet: SafeArea(
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
width: double.infinity,
color: Colors.white,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 14),
primary: AppColors.azul,
onSurface: AppColors.blueGrey,
),
child: Text('확인'),
),
),
),
),
전체 코드
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_widgets/colors.dart';
import '../text_style.dart';
class ButtonAboveKeyboardScreen extends StatefulWidget {
static String routeName = '/button_above_keyboard_page';
@override
_ButtonAboveKeyboardPageState createState() => _ButtonAboveKeyboardPageState();
}
class _ButtonAboveKeyboardPageState extends State<ButtonAboveKeyboardScreen> {
TextEditingController _idController = TextEditingController();
TextEditingController _passWordController = TextEditingController();
bool _isValid = false;
bool _isNewObscure = false;
bool _isPasswordValid = false;
bool _isPasswordObscure = false;
@override
void initState() {
super.initState();
_idController.addListener(() {});
_passWordController.addListener(() {});
}
@override
void dispose() {
_idController.dispose();
_passWordController.dispose();
super.dispose();
}
void _passwordObscureToggle() {
setState(() {
_isPasswordObscure = !_isPasswordObscure;
});
}
@override
Widget build(BuildContext context) {
final node = FocusScope.of(context);
return GestureDetector(
onTap: () {
node.requestFocus(FocusNode());
},
child: Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0.0,
centerTitle: true,
title: Text(
'Login Screen',
style: kNotoSansMedium16.copyWith(color: AppColors.dark),
),
leading: IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: Image.asset('images/ic_back.png')),
),
bottomSheet: SafeArea(
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
width: double.infinity,
color: Colors.white,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 14),
primary: AppColors.azul,
onSurface: AppColors.blueGrey,
),
onPressed: () { },
child: Text('Login'),
),
),
),
),
body: SafeArea(
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(children: [
TextSpan(text: 'ButtonAboveKeyboard Example', style: kNotoSansBold24.copyWith(color: AppColors.dark)),
]),
),
SizedBox(
height: 36.0,
),
TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
controller: _idController,
keyboardType: TextInputType.name,
obscureText: _isNewObscure,
style: kNotoSansMedium16.copyWith(color: AppColors.dark),
textInputAction: TextInputAction.next,
onEditingComplete: _isValid
? () {
node.nextFocus();
}
: null,
autofocus: false,
cursorColor: AppColors.azul,
decoration: InputDecoration(
counterText: ' ',
labelText: "ID",
labelStyle: kNotoSansMedium12.copyWith(
color: AppColors.lightBlueGrey,
),
hintText: 'ID',
hintStyle: kNotoSansMedium16.copyWith(
color: AppColors.lightBlueGrey,
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: AppColors.azul,
width: 1.5,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: AppColors.lightBlueGrey,
width: 1.0,
),
),
alignLabelWithHint: true,
isDense: true,
),
),
SizedBox(
height: 10,
),
TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
controller: _passWordController,
keyboardType: TextInputType.visiblePassword,
// validator: (String value) => passwordConfirmValidator(value, _newPasswordController.text),
obscureText: _isPasswordObscure,
style: kNotoSansMedium16.copyWith(color: AppColors.dark),
textInputAction: TextInputAction.done,
onEditingComplete: _isPasswordValid
? () {
node.unfocus();
}
: null,
autofocus: false,
cursorColor: AppColors.azul,
decoration: InputDecoration(
labelText: "Password",
labelStyle: kNotoSansMedium12.copyWith(
color: AppColors.lightBlueGrey,
),
hintText: 'Password',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: AppColors.azul,
width: 1.5,
),
),
border: UnderlineInputBorder(
borderSide: BorderSide(
color: AppColors.lightBlueGrey,
width: 1.0,
),
),
alignLabelWithHint: true,
isDense: true,
suffixIcon: _isPasswordObscure
? IconButton(
icon: Image.asset('images/invisible_icon.png'),
onPressed: () => _passwordObscureToggle(),
)
: IconButton(
icon: Image.asset('images/visible_icon.png'),
onPressed: () => _passwordObscureToggle(),
),
),
),
],
),
),
],
),
),
),
),
);
}
}