Flutter Webview 사용시 키보드가림 현상 수정
문제 상황
앱 내에 웹뷰를 이용하다보면 만나는 문제 중 하나는 웹뷰 내에서 Input Tag를 사용했을 때 해당 요소가 키보드에 가리는 현상입니다. 이 문제는 사용자 경험을 크게 저해할 수 있으며, 특히 모바일 환경에서 자주 발생합니다.
기존 해결 방법의 한계
이 문제를 해결하기 위해 인터넷에서 검색한 결과, 다양한 해결 방법이 있었습니다:
Scaffold
의resizeToAvoidBottomInset
속성 활용SingleChildScrollView
로 감싸기MediaQuery.viewInsets.bottom
을 활용한 패딩 추가
하지만 이러한 방법들은 현재 앱의 웹뷰 환경과 맞지 않았습니다. 각 해결책은 특정 상황에서는 효과적이지만, 모든 환경에 적용하기에는 한계가 있었습니다.
JavaScript를 활용한 해결 방법
이러한 한계를 극복하기 위해 JavaScript를 활용하여 웹뷰 내부의 입력 필드 위치를 동적으로 조정하는 방법을 구현했습니다. 이 방법의 핵심은 다음과 같습니다:
- 웹뷰 내에서 Input Tag 요소의 위치를 확인
- 키보드 높이와 Input 요소의 bottom 값을 비교
- 필요한 경우 패딩을 추가하여 키보드에 가리지 않도록 조정
구현 결과
좌(변경 전) / 우(변경 후)
구현 원리
- WebView 내 Focus 된 Input 요소 확인 (사용자 클릭 시, 키보드에서 '다음' Input 요소로 이동)
- Input 요소의 bottom 값 확인
- Keyboard Height 확인 후 Input bottom 값을 비교
- Input bottom > Keyboard Height 일 경우 스크롤 X
- Input bottom < Keyboard Height 일 경우
- Keyboard Height - Input bottom 값 만큼 Padding 추가
코드 구현
키보드 높이는 디바이스마다 다르고, build 내에서 확인을 수행하고 있기 때문에 RxDart의 Stream을 이용하여 중복값과 JavaScript를 이용한 계산을 최적화했습니다.
Flutter WebView 플러그인으로는 flutter_inappwebview를 사용했습니다.
기본적으로 WebView의 경우 Height가 infinite 처리되어 있어 다른 위젯과 사용하기 위해서 LayoutBuilder로 constraints를 계산하여 사용하고 있습니다. 단일로 WebView 사용 시에는 굳이 필요 없긴 합니다.
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
InAppWebViewController? controller;
bool isKeyboardUp = false;
double webviewHeight = 0.0;
double webviewPadding = 0.0;
final BehaviorSubject<double> keyboardSizeStream = BehaviorSubject<double>();
@override
void initState() {
super.initState();
keyboardSizeStream.stream
.distinct()
.debounceTime(const Duration(milliseconds: 100))
.listen(
(double keyboardSize) {
if (keyboardSize > 10) {
// 웹뷰 내 키보드 위치만큼 패딩을 준다.
keyboardPaddingUpdate(keyboardSize: keyboardSize);
} else {
setState(() {
webviewPadding = 0.0;
});
}
},
);
}
@override
void dispose() {
keyboardSizeStream.close();
controller = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
final keyboardBottom = MediaQuery.of(context).viewInsets.bottom;
if (keyboardBottom > 10) {
isKeyboardUp = true;
keyboardSizeStream.add(keyboardBottom);
} else if (keyboardBottom < 10 && isKeyboardUp) {
isKeyboardUp = false;
keyboardSizeStream.add(0.0);
}
return Scaffold(
// 키보드가 올라오면 화면이 올라가는 것을 방지
resizeToAvoidBottomInset: false,
body: SafeArea(
// 다른 요소가 존재할 수도 있으니 LayoutBuilder로 constraints를 받아서 사용
child: LayoutBuilder(builder: (context, constraints) {
return SingleChildScrollView(
reverse: true,
child: Column(
children: [
SizedBox(
width: constraints.maxWidth,
height: constraints.maxHeight,
child: InAppWebView(
initialUrlRequest: URLRequest(
url: Uri.parse("https://dashboard.branch.io/"),
),
onWebViewCreated: (controller) {
this.controller = controller;
webviewHeight = constraints.maxHeight;
},
onLoadStart: (controller, url) async {
await controller.evaluateJavascript(source: """
window.addEventListener('focus', function(event) {
if(event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
window.flutter_inappwebview.callHandler('focusChange');
}
}, true);
""");
},
onLoadStop: (controller, url) async {
controller.addJavaScriptHandler(
handlerName: "focusChange",
callback: (_) {
if (kDebugMode) {
print("focusChange");
}
keyboardPaddingUpdate(
keyboardSize: MediaQuery.of(context).viewInsets.bottom,
);
},
);
},
),
),
SizedBox(height: webviewPadding),
],
),
);
}),
),
);
}
// 키보드가 올라오면 WebView 내 Focus Input의 위치 계산 및 Padding 추가
keyboardPaddingUpdate({
double keyboardSize = 0.0,
}) async {
if (controller == null) return;
final double webViewInputPadding = await controller!.evaluateJavascript(
source: "document.activeElement.getBoundingClientRect().bottom;")
as double;
final double padding = webviewHeight - webViewInputPadding;
if (keyboardSize < padding) {
setState(() {
webviewPadding = 0.0;
});
} else {
setState(() {
webviewPadding = keyboardSize - padding;
});
}
}
}
해결 방법의 장점
- 유연성: 다양한 웹 페이지와 레이아웃에 적용 가능합니다.
- 동적 대응: 키보드 높이 변경에 실시간으로 대응합니다.
- 사용자 경험 향상: 입력 필드가 항상 가시적으로 유지됩니다.
- 성능 최적화: RxDart를 활용한 중복 처리 방지로 성능을 개선했습니다.
주의사항 및 한계
- 웹 페이지 구조 의존성: 일부 복잡한 웹 페이지에서는 예상대로 작동하지 않을 수 있습니다.
- 브라우저 호환성: 모든 브라우저에서 동일하게 작동하지 않을 수 있습니다.
- 성능 고려: JavaScript 실행으로 인한 약간의 성능 저하 가능성이 있습니다.
결론
JavaScript를 활용한 키보드 가림 현상 해결 방법은 WebView 내 입력 필드의 가시성을 보장하는 효과적인 접근법입니다. 이 방법은 다양한 디바이스와 웹 페이지에 적용 가능하며, 사용자 경험을 크게 향상시킬 수 있습니다.
모든 환경에 대응할 수는 없지만, 현재 프로덕트에 잘 어울리는 결과물을 얻을 수 있었습니다. 필요에 따라 프로젝트의 요구사항에 맞게 코드를 조정하여 사용하시기 바랍니다.