From e09aa3d1b883e2d8504432480ada391fd20487cb Mon Sep 17 00:00:00 2001 From: Shawn Date: Tue, 18 Apr 2023 10:43:21 -0600 Subject: [PATCH] Initial pass at vertical menu for smaller landscape views --- .../wonder_details_tab_menu.dart | 116 ++++++++++++------ .../wonders_details_screen.dart | 47 +++++-- .../screens/wonder_events/wonder_events.dart | 75 +++++------ 3 files changed, 152 insertions(+), 86 deletions(-) diff --git a/lib/ui/screens/wonder_details/wonder_details_tab_menu.dart b/lib/ui/screens/wonder_details/wonder_details_tab_menu.dart index 4b147dcb..537a8319 100644 --- a/lib/ui/screens/wonder_details/wonder_details_tab_menu.dart +++ b/lib/ui/screens/wonder_details/wonder_details_tab_menu.dart @@ -4,12 +4,19 @@ class WonderDetailsTabMenu extends StatelessWidget { static double bottomPadding = 0; static double buttonInset = 12; - const WonderDetailsTabMenu({Key? key, required this.tabController, this.showBg = false, required this.wonderType}) + const WonderDetailsTabMenu( + {Key? key, + required this.tabController, + this.showBg = false, + required this.wonderType, + this.axis = Axis.horizontal}) : super(key: key); final TabController tabController; final bool showBg; final WonderType wonderType; + final Axis axis; + bool get isVertical => axis == Axis.vertical; @override Widget build(BuildContext context) { @@ -19,53 +26,79 @@ class WonderDetailsTabMenu extends StatelessWidget { bottomPadding = max(context.mq.padding.bottom, $styles.insets.xs * 1.5); return Stack( children: [ - //Background + /// Background, animates in and out based on `showBg` Positioned.fill( child: AnimatedOpacity( duration: $styles.times.fast, opacity: showBg ? 1 : 0, child: Padding( - padding: EdgeInsets.only(top: buttonInset), + padding: EdgeInsets.only( + top: isVertical ? context.mq.viewPadding.top : buttonInset, + right: isVertical ? buttonInset : 0, + ), child: ColoredBox(color: $styles.colors.offWhite), ), ), ), - // Buttons + + /// Buttons + /// A Stack with a row or column of tabButtons, and an illustrated homeButton sitting on top. Padding( padding: EdgeInsets.only(left: $styles.insets.sm, right: $styles.insets.xxs, bottom: bottomPadding), - // TabButtons are a Stack with a row of icon buttons, and an illustrated home button sitting on top. - // The home buttons shows / hides itself based on `showHomeBtn` - // The row contains an animated placeholder gap which makes room for the icon as it transitions in. - child: IntrinsicHeight( - child: Stack( - children: [ - // Main tab btns + animated gap - Center( - child: FocusTraversalGroup( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // Home btn - _WonderHomeBtn( - size: homeBtnSize, - wonderType: wonderType, - borderSize: showBg ? 6 : 2, - ), - _TabBtn(0, tabController, - iconImg: 'editorial', label: $strings.wonderDetailsTabLabelInformation, color: iconColor), - _TabBtn(1, tabController, - iconImg: 'photos', label: $strings.wonderDetailsTabLabelImages, color: iconColor), - _TabBtn(2, tabController, - iconImg: 'artifacts', label: $strings.wonderDetailsTabLabelArtifacts, color: iconColor), - _TabBtn(3, tabController, - iconImg: 'timeline', label: $strings.wonderDetailsTabLabelEvents, color: iconColor), - ], - ), + child: Stack( + children: [ + SizedBox( + width: isVertical ? null : double.infinity, + height: isVertical ? double.infinity : null, + child: FocusTraversalGroup( + child: Flex( + direction: axis, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: isVertical ? CrossAxisAlignment.start : CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + /// Home btn + _WonderHomeBtn( + size: homeBtnSize, + wonderType: wonderType, + borderSize: showBg ? 6 : 2, + ), + + /// Tabs + _TabBtn( + 0, + tabController, + iconImg: 'editorial', + label: $strings.wonderDetailsTabLabelInformation, + color: iconColor, + axis: axis, + ), + _TabBtn( + 1, + tabController, + iconImg: 'photos', + label: $strings.wonderDetailsTabLabelImages, + color: iconColor, + axis: axis, + ), + _TabBtn( + 2, + tabController, + iconImg: 'artifacts', + label: $strings.wonderDetailsTabLabelArtifacts, + color: iconColor, + axis: axis, + ), + _TabBtn(3, tabController, + iconImg: 'timeline', + label: $strings.wonderDetailsTabLabelEvents, + color: iconColor, + axis: axis), + ], ), ), - ], - ), + ), + ], ), ), ], @@ -111,16 +144,21 @@ class _TabBtn extends StatelessWidget { required this.iconImg, required this.color, required this.label, + required this.axis, }) : super(key: key); - static const double _minWidth = 50; - static const double _maxWidth = 120; + static const double minSize = 50; + static const double maxSize = 100; + static const double crossBtnSize = 60; final int index; final TabController tabController; final String iconImg; final Color color; final String label; + final Axis axis; + + bool get _isVertical => axis == Axis.vertical; @override Widget build(BuildContext context) { @@ -129,7 +167,7 @@ class _TabBtn extends StatelessWidget { final iconImgPath = '${ImagePaths.common}/tab-$iconImg${selected ? '-active' : ''}.png'; String tabLabel = localizations.tabLabel(tabIndex: index + 1, tabCount: tabController.length); tabLabel = '$label: $tabLabel'; - final double btnWidth = (context.widthPx / 6).clamp(_minWidth, _maxWidth); + final double mainBtnSize = ((_isVertical ? context.heightPx : context.widthPx) / 6).clamp(minSize, maxSize); return MergeSemantics( child: Semantics( selected: selected, @@ -139,7 +177,7 @@ class _TabBtn extends StatelessWidget { padding: EdgeInsets.only(top: $styles.insets.md + $styles.insets.xs, bottom: $styles.insets.sm), onPressed: () => tabController.index = index, semanticLabel: label, - minimumSize: Size(btnWidth, 0), + minimumSize: _isVertical ? Size(crossBtnSize, mainBtnSize) : Size(mainBtnSize, crossBtnSize), // Image icon child: Image.asset(iconImgPath, height: 32, width: 32, color: selected ? null : color), ), diff --git a/lib/ui/screens/wonder_details/wonders_details_screen.dart b/lib/ui/screens/wonder_details/wonders_details_screen.dart index 38efce6f..1294ed5e 100644 --- a/lib/ui/screens/wonder_details/wonders_details_screen.dart +++ b/lib/ui/screens/wonder_details/wonders_details_screen.dart @@ -26,7 +26,8 @@ class _WonderDetailsScreenState extends State AnimationController? _fade; final _detailsHasScrolled = ValueNotifier(false); - double? _tabBarHeight; + double? _tabBarSize; + bool _useNavRail = false; @override void dispose() { @@ -42,16 +43,20 @@ class _WonderDetailsScreenState extends State void _handleDetailsScrolled(double scrollPos) => _detailsHasScrolled.value = scrollPos > 0; void _handleTabMenuSized(Size size) { - setState(() => _tabBarHeight = size.height - WonderDetailsTabMenu.buttonInset); + setState(() { + _tabBarSize = (_useNavRail ? size.width : size.height) - WonderDetailsTabMenu.buttonInset; + }); } @override Widget build(BuildContext context) { + _useNavRail = context.isLandscape && context.heightPx < 900; + final wonder = wondersLogic.getData(widget.type); int tabIndex = _tabController.index; bool showTabBarBg = tabIndex != 1; - final tabBarHeight = _tabBarHeight ?? 0; - //final double tabBarHeight = WonderDetailsTabMenu.bottomPadding + 60; + final tabBarSize = _tabBarSize ?? 0; + final menuPadding = _useNavRail ? EdgeInsets.only(left: tabBarSize) : EdgeInsets.only(bottom: tabBarSize); return ColoredBox( color: Colors.black, child: Stack( @@ -62,21 +67,41 @@ class _WonderDetailsScreenState extends State children: [ WonderEditorialScreen(wonder, onScroll: _handleDetailsScrolled), PhotoGallery(collectionId: wonder.unsplashCollectionId, wonderType: wonder.type), - Padding(padding: EdgeInsets.only(bottom: tabBarHeight), child: ArtifactCarouselScreen(type: wonder.type)), - Padding(padding: EdgeInsets.only(bottom: tabBarHeight), child: WonderEvents(type: widget.type)), + AnimatedPadding( + duration: $styles.times.fast, + curve: Curves.easeOut, + padding: menuPadding, + child: ArtifactCarouselScreen(type: wonder.type), + ), + AnimatedPadding( + duration: $styles.times.fast, + curve: Curves.easeOut, + padding: menuPadding, + child: WonderEvents(type: widget.type), + ), ], ), /// Tab menu - BottomCenter( + Align( + alignment: _useNavRail ? Alignment.centerLeft : Alignment.bottomCenter, child: ValueListenableBuilder( valueListenable: _detailsHasScrolled, builder: (_, value, ___) => MeasurableWidget( onChange: _handleTabMenuSized, - child: WonderDetailsTabMenu( - tabController: _tabController, - wonderType: wonder.type, - showBg: showTabBarBg, + + /// Animate the menu in when the axis changes + child: Animate( + key: ValueKey(_useNavRail), + effects: [ + FadeEffect(begin: 0, delay: $styles.times.fast), + SlideEffect(begin: _useNavRail ? Offset(-.2, 0) : Offset(0, .2)), + ], + child: WonderDetailsTabMenu( + tabController: _tabController, + wonderType: wonder.type, + showBg: showTabBarBg, + axis: _useNavRail ? Axis.vertical : Axis.horizontal), ), ), ), diff --git a/lib/ui/screens/wonder_events/wonder_events.dart b/lib/ui/screens/wonder_events/wonder_events.dart index a85e9d84..1c01904b 100644 --- a/lib/ui/screens/wonder_events/wonder_events.dart +++ b/lib/ui/screens/wonder_events/wonder_events.dart @@ -68,49 +68,52 @@ class WonderEvents extends StatelessWidget { /// Landscape layout is a row, with the WonderImage on left and EventsList on the right Widget _buildTwoColumn(BuildContext context) { - return Row( - children: [ - /// WonderImage w/ Timeline btn - Expanded( - child: CenteredBox( - width: $styles.sizes.maxContentWidth3, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Gap($styles.insets.lg), - Expanded( - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _WonderImageWithTimeline(data: _data, height: min(500, context.heightPx - 300)), - Gap($styles.insets.lg), - SizedBox(width: 400, child: _TimelineBtn(type: type)), - ], + return Padding( + padding: EdgeInsets.all($styles.insets.lg), + child: Row( + children: [ + /// WonderImage w/ Timeline btn + Expanded( + child: CenteredBox( + width: $styles.sizes.maxContentWidth3, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Gap($styles.insets.lg), + Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _WonderImageWithTimeline(data: _data, height: min(500, context.heightPx - 300)), + Gap($styles.insets.lg), + SizedBox(width: 400, child: _TimelineBtn(type: type)), + ], + ), ), ), - ), - Gap($styles.insets.lg), - ], + Gap($styles.insets.lg), + ], + ), ), ), - ), - /// EventsList - Expanded( - child: CenteredBox( - width: $styles.sizes.maxContentWidth2, - child: _EventsList( - data: _data, - topHeight: 100, - blurOnScroll: false, - onScroll: _handleScroll, - initialScrollOffset: _scrollPos, + /// EventsList + Expanded( + child: CenteredBox( + width: $styles.sizes.maxContentWidth2, + child: _EventsList( + data: _data, + topHeight: 100, + blurOnScroll: false, + onScroll: _handleScroll, + initialScrollOffset: _scrollPos, + ), ), ), - ), - ], + ], + ), ); }