JJ_MKVMaker/lib/ui/app_bar/app_top_bar.dart

134 lines
6.2 KiB
Dart

/// 약속: 모든 화면의 위쪽 막대 ([AppTopBar]) - 높이 · 오른쪽 여백이 어느 화면에서나 같고, 좁으면 (또는 가운데가 [AppTopBar.minMiddleWidth]
/// 보다 좁아지면) 두 줄. 좁은 화면 기준 [compactWidth] · [isCompact].
library;
import 'package:flutter/material.dart';
import '../theme.dart';
/// 모든 화면의 위쪽 막대 높이 · 오른쪽 여백 (환경 설정 · 종료 버튼이 어느 화면에서나 같은 자리에 오도록)
const double appBarHeight = 56;
const double appBarRightPadding = 12;
/// 이보다 좁으면 (접은 폴드 · 휴대폰 세로 등) 위쪽 막대를 두 줄로: 위 = 화면 이동 · 설정 · 종료, 아래 = 화면의 도구
const double compactWidth = 720;
bool isCompact(BuildContext context) => MediaQuery.sizeOf(context).width < compactWidth;
/// 모든 화면의 위쪽 막대: [nav] [middle (남는 너비)] [actions].
/// 좁은 화면에서는 두 줄 - 위: [nav] … [actions], 아래: [middle] 이 한 줄을 다 쓴다.
/// [minMiddleWidth] (201): 한 줄일 때 middle 이 이보다 좁아지면 (폰 가로처럼 넓지만 버튼이 폭을 다 차지할 때) 두 줄로.
/// 브라우저 주소 칸처럼 middle 이 꼭 보여야 하는 화면이 준다 (0 이면 폭만으로 정함).
/// [trailing] (242): middle 오른쪽에 붙는, 줄지 않는 버튼 묶음 - 이것 때문에 middle 이 [minMiddleWidth] 보다 좁아지면 두 줄
/// (어림한 폭이 실제보다 작아 넘친 버튼이 화면 크기 "100%" 를 덮던 것 - 실제로 재어 정한다)
class AppTopBar extends StatefulWidget {
final Widget nav;
final Widget middle;
final Widget? trailing;
final Widget actions;
final double minMiddleWidth;
const AppTopBar(
{super.key,
required this.nav,
required this.middle,
this.trailing,
required this.actions,
this.minMiddleWidth = 0});
@override
State<AppTopBar> createState() => _AppTopBarState();
}
class _AppTopBarState extends State<AppTopBar> {
final _mid = GlobalKey();
/// 한 줄에서 middle 이 모자랐던 화면 폭 - 이 폭 이하이면 두 줄
double? _twoRowsAtOrBelow;
void _check(double width) {
if (!mounted || widget.minMiddleWidth <= 0) return;
final box = _mid.currentContext?.findRenderObject() as RenderBox?;
if (box == null || !box.hasSize) return;
if (box.size.width < widget.minMiddleWidth) setState(() => _twoRowsAtOrBelow = width);
}
@override
Widget build(BuildContext context) {
final nav = widget.nav, middle = widget.middle, actions = widget.actions;
final width = MediaQuery.sizeOf(context).width;
final narrow = _twoRowsAtOrBelow != null && width <= _twoRowsAtOrBelow!;
if (!isCompact(context) && !narrow) {
if (widget.minMiddleWidth > 0) WidgetsBinding.instance.addPostFrameCallback((_) => _check(width));
return Container(
height: appBarHeight,
color: JjColors.panel,
padding: const EdgeInsets.only(left: 8, right: appBarRightPadding),
// 242: middle 이 넘쳐도 오른쪽 (화면 크기 "100%" · 설정 · 종료) 을 덮어 그리지 않게.
// trailing 이 있으면 제 폭대로 재고 남는 폭을 middle 에 (모자라면 [_check] 가 두 줄로 - 그사이 넘침 오류 없이 잘라 그림)
child: widget.trailing == null
? Row(children: [
nav,
const SizedBox(width: 8),
Expanded(child: ClipRect(child: KeyedSubtree(key: _mid, child: middle))),
actions,
])
: ClipRect(
child: CustomMultiChildLayout(delegate: _BarLayout(), children: [
LayoutId(id: #nav, child: nav),
LayoutId(id: #mid, child: ClipRect(child: KeyedSubtree(key: _mid, child: middle))),
LayoutId(id: #trail, child: widget.trailing!),
LayoutId(id: #act, child: actions),
]),
),
);
}
return Container(
color: JjColors.panel,
padding: const EdgeInsets.only(left: 4, right: 4),
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
// 아주 좁으면 왼쪽 이동 버튼 묶음을 줄여서라도 설정 · 종료가 밀려나지 않게
SizedBox(
height: 48,
child: Row(children: [
Expanded(child: Align(alignment: Alignment.centerLeft, child: FittedBox(fit: BoxFit.scaleDown, child: nav))),
actions,
]),
),
// F8 (10/21): 아래 줄은 48 이상 - middle 이 좁아 두 줄로 바꾸면 (다운로드 화면의 주소 칸) 그만큼 늘어난다. 한 줄이면 예전처럼 48 가운데
ConstrainedBox(
constraints: const BoxConstraints(minHeight: 48),
child: Align(
alignment: Alignment.centerLeft,
heightFactor: 1,
child: SizedBox(
width: double.infinity, // 아래 줄은 가운데가 폭을 다 쓴다 (201)
child: Padding(
padding: const EdgeInsets.only(left: 6),
child: widget.trailing == null ? middle : Row(children: [Expanded(child: middle), widget.trailing!]))))),
]),
);
}
}
/// 242: [nav] [middle (남는 폭)] [trailing] [actions] - nav · trailing · actions 는 제 폭대로, 세로 가운데
class _BarLayout extends MultiChildLayoutDelegate {
@override
void performLayout(Size size) {
final loose = BoxConstraints(maxHeight: size.height);
final nav = layoutChild(#nav, loose);
final act = layoutChild(#act, loose);
final trail = layoutChild(#trail, loose);
final midW = (size.width - nav.width - 8 - trail.width - act.width).clamp(0.0, double.infinity);
final mid = layoutChild(#mid, BoxConstraints(minWidth: midW, maxWidth: midW, maxHeight: size.height));
double y(Size s) => (size.height - s.height) / 2;
positionChild(#nav, Offset(0, y(nav)));
positionChild(#mid, Offset(nav.width + 8, y(mid)));
positionChild(#trail, Offset(nav.width + 8 + midW, y(trail)));
positionChild(#act, Offset(size.width - act.width, y(act)));
}
@override
bool shouldRelayout(_BarLayout oldDelegate) => false;
}