import 'dart:async'; import 'package:flutter/widgets.dart'; /// 204 (사용자 지적 10/11): Android 브라우저에서 화면 아래를 위로 밀면 아래에서 검은 화면이 올라왔다. /// 제스처 내비 · 삼성 태블릿의 작업 표시줄은 아래에서 밀면 잠깐 나타나고, 그동안 아래 시스템 막대 높이 (viewPadding) 가 커진다. /// 앱 맨 위의 SafeArea 가 그만큼 앱 전체를 줄여 그 자리에 어두운 바탕이 보이고, 텍스처로 그리는 웹 뷰는 /// 새 크기로 다시 그릴 때까지 늦어 더 검게 보였다. /// /// 이 위젯 안의 [child] (웹 뷰) 는 그렇게 잠깐 커진 아래 막대 때문에는 크기를 바꾸지 않는다: 원래 크기 그대로 두고 /// 줄어든 만큼 아래로 넘쳐 (자르지 않고) 그 띠 위에 그린다. 막대가 들어가면 그대로 원래 자리. /// - 키보드가 열리면 (viewInsets) 지금처럼 줄인다 - 입력 칸이 키보드에 가리지 않게. /// - 커진 막대가 [settle] 넘게 그대로면 (작업 표시줄을 고정하는 등) 그 크기를 새 기준으로 받아들인다. /// - 폭이 바뀌면 (회전 · 창 크기) 기준을 다시 잡는다. class BottomInsetHold extends StatefulWidget { final Widget child; final Duration settle; const BottomInsetHold({super.key, required this.child, this.settle = const Duration(seconds: 3)}); @override State createState() => _BottomInsetHoldState(); } class _BottomInsetHoldState extends State { /// 이 폭에서 기준으로 삼는 아래 시스템 막대 높이 (논리 픽셀) double? _base; double _width = -1; Timer? _accept; @override void dispose() { _accept?.cancel(); super.dispose(); } @override Widget build(BuildContext context) => LayoutBuilder(builder: (context, box) { // 앱 위 SafeArea 가 MediaQuery 의 여백을 지우므로 창 값을 직접 본다. 막대가 바뀌면 SafeArea 가 크기를 바꿔 // 이 builder 가 다시 불리므로 여기서 읽는다 (밖에서 읽으면 예전 값) final view = View.of(context); final dpr = view.devicePixelRatio; final pad = view.viewPadding.bottom / dpr; final keyboard = view.viewInsets.bottom / dpr; if (box.maxWidth != _width) { _width = box.maxWidth; _base = pad; } if (pad <= _base!) { _base = pad; _accept?.cancel(); _accept = null; } final extra = keyboard > 0 ? 0.0 : pad - _base!; if (extra <= 0 || !box.hasBoundedHeight) return widget.child; // 오래 그대로면 새 기준으로 (한 번 다시 배치) _accept ??= Timer(widget.settle, () { if (!mounted) return; setState(() => _base = View.of(context).viewPadding.bottom / View.of(context).devicePixelRatio); _accept = null; }); final h = box.maxHeight + extra; return OverflowBox(alignment: Alignment.topCenter, minHeight: h, maxHeight: h, child: widget.child); }); }