이번 포스트는 플러터에서 키보드 위에 떠있는 버튼 만드는 방법을 정리하겠습니다.
위와 같은 화면에서 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(),
),
),
),
],
),
),
],
),
),
),
),
);
}
}