WIP 204 Android 브라우저: 아래 시스템 막대 (제스처 · 작업 표시줄) 가 잠깐 커질 때 웹 뷰 크기를 바꾸지 않고 그 띠 위로 그림 - 아래에서 검은 띠가 올라오던 것 (기기 확인 전, 원인 추정) (검토 대기)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
next3
RyuJongJin 2026-10-19 21:35:47 +09:00
parent 1396ba41c8
commit 45e1a95df0
3 changed files with 134 additions and 2 deletions

View File

@ -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<BottomInsetHold> createState() => _BottomInsetHoldState();
}
class _BottomInsetHoldState extends State<BottomInsetHold> {
/// 이 폭에서 기준으로 삼는 아래 시스템 막대 높이 (논리 픽셀)
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);
});
}

View File

@ -33,6 +33,7 @@ import 'key_hint.dart';
import 'settings_page.dart'; import 'settings_page.dart';
import 'tor_page.dart'; import 'tor_page.dart';
import 'theme.dart'; import 'theme.dart';
import 'bottom_inset_hold.dart';
import 'work_panel.dart'; import 'work_panel.dart';
import '../l10n/tr.dart'; import '../l10n/tr.dart';
@ -1012,6 +1013,9 @@ class _SplitHandle extends StatelessWidget {
// ───────── 앱 안 Edge 웹뷰 ───────── // ───────── 앱 안 Edge 웹뷰 ─────────
/// 204: Android 만 (Windows 창에는 아래 시스템 막대가 없음)
Widget _holdBottom(Widget webView) => Platform.isAndroid ? BottomInsetHold(child: webView) : webView;
/// Edge · Android 웹뷰 설정 (처음 만들 때와 JavaScript 켜기 · 끄기에 같이 쓴다) /// Edge · Android 웹뷰 설정 (처음 만들 때와 JavaScript 켜기 · 끄기에 같이 쓴다)
InAppWebViewSettings _webSettings({required bool javaScript}) => _JjWebSettings(javaScript: javaScript); InAppWebViewSettings _webSettings({required bool javaScript}) => _JjWebSettings(javaScript: javaScript);
@ -1125,7 +1129,8 @@ class _EdgeViewState extends State<_EdgeView> with WebProxyVisibility {
} catch (_) {} } catch (_) {}
} }
return InAppWebView( // 204: 아래 시스템 막대가 잠깐 커질 때 (제스처 · 작업 표시줄) 웹 뷰 크기를 바꾸지 않게 (검은 띠가 올라오던 것)
return _holdBottom(InAppWebView(
keepAlive: _keepAlive, keepAlive: _keepAlive,
webViewEnvironment: snap.data, webViewEnvironment: snap.data,
initialUrlRequest: URLRequest(url: WebUri(normalizeAddress(widget.initialUrl))), initialUrlRequest: URLRequest(url: WebUri(normalizeAddress(widget.initialUrl))),
@ -1170,7 +1175,7 @@ class _EdgeViewState extends State<_EdgeView> with WebProxyVisibility {
onReceivedError: (c, req, err) { onReceivedError: (c, req, err) {
if (req.isForMainFrame ?? true) h.onMainFrameError?.call(req.url.toString(), err.description); if (req.isForMainFrame ?? true) h.onMainFrameError?.call(req.url.toString(), err.description);
}, },
); ));
}, },
); );
} }

View File

@ -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<void> 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);
});
}