/// 약속: 모든 화면의 위쪽 막대 ([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 createState() => _AppTopBarState(); } class _AppTopBarState extends State { 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; }