From 45e1a95df0b5d363adc8c8f8c1291cf17481dce8 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:35:47 +0900 Subject: [PATCH] =?UTF-8?q?WIP=20204=20Android=20=EB=B8=8C=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=EC=A0=80:=20=EC=95=84=EB=9E=98=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EB=A7=89=EB=8C=80=20(=EC=A0=9C=EC=8A=A4=EC=B2=98?= =?UTF-8?q?=20=C2=B7=20=EC=9E=91=EC=97=85=20=ED=91=9C=EC=8B=9C=EC=A4=84)?= =?UTF-8?q?=20=EA=B0=80=20=EC=9E=A0=EA=B9=90=20=EC=BB=A4=EC=A7=88=20?= =?UTF-8?q?=EB=95=8C=20=EC=9B=B9=20=EB=B7=B0=20=ED=81=AC=EA=B8=B0=EB=A5=BC?= =?UTF-8?q?=20=EB=B0=94=EA=BE=B8=EC=A7=80=20=EC=95=8A=EA=B3=A0=20=EA=B7=B8?= =?UTF-8?q?=20=EB=9D=A0=20=EC=9C=84=EB=A1=9C=20=EA=B7=B8=EB=A6=BC=20-=20?= =?UTF-8?q?=EC=95=84=EB=9E=98=EC=97=90=EC=84=9C=20=EA=B2=80=EC=9D=80=20?= =?UTF-8?q?=EB=9D=A0=EA=B0=80=20=EC=98=AC=EB=9D=BC=EC=98=A4=EB=8D=98=20?= =?UTF-8?q?=EA=B2=83=20(=EA=B8=B0=EA=B8=B0=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EC=A0=84,=20=EC=9B=90=EC=9D=B8=20=EC=B6=94=EC=A0=95)=20(?= =?UTF-8?q?=EA=B2=80=ED=86=A0=20=EB=8C=80=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- lib/ui/bottom_inset_hold.dart | 64 ++++++++++++++++++++++++++++++++ lib/ui/browser_page.dart | 9 ++++- test/bottom_inset_hold_test.dart | 63 +++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 lib/ui/bottom_inset_hold.dart create mode 100644 test/bottom_inset_hold_test.dart diff --git a/lib/ui/bottom_inset_hold.dart b/lib/ui/bottom_inset_hold.dart new file mode 100644 index 0000000..753d4eb --- /dev/null +++ b/lib/ui/bottom_inset_hold.dart @@ -0,0 +1,64 @@ +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); + }); +} diff --git a/lib/ui/browser_page.dart b/lib/ui/browser_page.dart index e7eda26..e16241c 100644 --- a/lib/ui/browser_page.dart +++ b/lib/ui/browser_page.dart @@ -33,6 +33,7 @@ import 'key_hint.dart'; import 'settings_page.dart'; import 'tor_page.dart'; import 'theme.dart'; +import 'bottom_inset_hold.dart'; import 'work_panel.dart'; import '../l10n/tr.dart'; @@ -1012,6 +1013,9 @@ class _SplitHandle extends StatelessWidget { // ───────── 앱 안 Edge 웹뷰 ───────── +/// 204: Android 만 (Windows 창에는 아래 시스템 막대가 없음) +Widget _holdBottom(Widget webView) => Platform.isAndroid ? BottomInsetHold(child: webView) : webView; + /// Edge · Android 웹뷰 설정 (처음 만들 때와 JavaScript 켜기 · 끄기에 같이 쓴다) InAppWebViewSettings _webSettings({required bool javaScript}) => _JjWebSettings(javaScript: javaScript); @@ -1125,7 +1129,8 @@ class _EdgeViewState extends State<_EdgeView> with WebProxyVisibility { } catch (_) {} } - return InAppWebView( + // 204: 아래 시스템 막대가 잠깐 커질 때 (제스처 · 작업 표시줄) 웹 뷰 크기를 바꾸지 않게 (검은 띠가 올라오던 것) + return _holdBottom(InAppWebView( keepAlive: _keepAlive, webViewEnvironment: snap.data, initialUrlRequest: URLRequest(url: WebUri(normalizeAddress(widget.initialUrl))), @@ -1170,7 +1175,7 @@ class _EdgeViewState extends State<_EdgeView> with WebProxyVisibility { onReceivedError: (c, req, err) { if (req.isForMainFrame ?? true) h.onMainFrameError?.call(req.url.toString(), err.description); }, - ); + )); }, ); } diff --git a/test/bottom_inset_hold_test.dart b/test/bottom_inset_hold_test.dart new file mode 100644 index 0000000..7ed2516 --- /dev/null +++ b/test/bottom_inset_hold_test.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jj_mkvmaker/ui/bottom_inset_hold.dart'; + +/// 204: 아래 시스템 막대 (제스처 · 작업 표시줄) 가 잠깐 커져도 웹 뷰 자리의 크기는 그대로 (검은 띠가 올라오지 않게) +void main() { + const box = Key('web'); + Future app(WidgetTester t) async { + t.view.physicalSize = const Size(800, 1000); + t.view.devicePixelRatio = 1; + t.view.padding = const FakeViewPadding(bottom: 20); + t.view.viewPadding = const FakeViewPadding(bottom: 20); + addTearDown(t.view.reset); + // 앱처럼: 맨 위 SafeArea (아래 막대만큼 앱을 줄임) 안에 웹 뷰 자리 + await t.pumpWidget(const MaterialApp( + home: SafeArea( + child: Column(children: [ + SizedBox(height: 100), + Expanded(child: BottomInsetHold(child: ColoredBox(key: box, color: Colors.white))), + ]), + ), + )); + } + + double h(WidgetTester t) => t.getSize(find.byKey(box)).height; + + testWidgets('아래 막대가 잠깐 커지면 크기 그대로 (넘쳐서 그 띠 위에) · 들어가면 그대로 원래', (t) async { + await app(t); + expect(h(t), 1000 - 20 - 100); + // 작업 표시줄이 나타남 (아래 막대 20 → 76) + t.view.padding = const FakeViewPadding(bottom: 76); + t.view.viewPadding = const FakeViewPadding(bottom: 76); + await t.pump(); + expect(h(t), 880, reason: '웹 뷰 크기를 바꾸지 않음'); + expect(t.getRect(find.byKey(box)).bottom, 980, reason: '줄어든 자리 아래로 넘쳐 그 띠 위에 그림'); + // 들어감 + t.view.padding = const FakeViewPadding(bottom: 20); + t.view.viewPadding = const FakeViewPadding(bottom: 20); + await t.pump(const Duration(seconds: 5)); + expect(h(t), 880); + }); + + testWidgets('커진 막대가 3초 넘게 그대로면 (작업 표시줄 고정 등) 새 크기로', (t) async { + await app(t); + t.view.padding = const FakeViewPadding(bottom: 76); + t.view.viewPadding = const FakeViewPadding(bottom: 76); + await t.pump(); + expect(h(t), 880); + await t.pump(const Duration(seconds: 3)); + await t.pump(); + expect(h(t), 1000 - 76 - 100); + }); + + testWidgets('키보드가 열리면 지금처럼 줄인다 (입력 칸이 가리지 않게)', (t) async { + await app(t); + t.view.padding = const FakeViewPadding(bottom: 0); + t.view.viewInsets = const FakeViewPadding(bottom: 300); + await t.pump(); + // SafeArea 는 키보드만큼 줄이지 않으므로 (Scaffold 가 한다) 여기서는 아래 여백이 0 이 된 만큼만 - 넘치지 않음 + expect(h(t), 1000 - 100); + expect(t.getRect(find.byKey(box)).bottom, 1000); + }); +}