Initial pass at vertical menu for smaller landscape views

This commit is contained in:
Shawn 2023-04-18 10:43:21 -06:00
parent f786e3f5dc
commit e09aa3d1b8
3 changed files with 152 additions and 86 deletions

View File

@ -4,12 +4,19 @@ class WonderDetailsTabMenu extends StatelessWidget {
static double bottomPadding = 0; static double bottomPadding = 0;
static double buttonInset = 12; 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); : super(key: key);
final TabController tabController; final TabController tabController;
final bool showBg; final bool showBg;
final WonderType wonderType; final WonderType wonderType;
final Axis axis;
bool get isVertical => axis == Axis.vertical;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -19,53 +26,79 @@ class WonderDetailsTabMenu extends StatelessWidget {
bottomPadding = max(context.mq.padding.bottom, $styles.insets.xs * 1.5); bottomPadding = max(context.mq.padding.bottom, $styles.insets.xs * 1.5);
return Stack( return Stack(
children: [ children: [
//Background /// Background, animates in and out based on `showBg`
Positioned.fill( Positioned.fill(
child: AnimatedOpacity( child: AnimatedOpacity(
duration: $styles.times.fast, duration: $styles.times.fast,
opacity: showBg ? 1 : 0, opacity: showBg ? 1 : 0,
child: Padding( 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), 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(
padding: EdgeInsets.only(left: $styles.insets.sm, right: $styles.insets.xxs, bottom: bottomPadding), 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. child: Stack(
// The home buttons shows / hides itself based on `showHomeBtn` children: [
// The row contains an animated placeholder gap which makes room for the icon as it transitions in. SizedBox(
child: IntrinsicHeight( width: isVertical ? null : double.infinity,
child: Stack( height: isVertical ? double.infinity : null,
children: [ child: FocusTraversalGroup(
// Main tab btns + animated gap child: Flex(
Center( direction: axis,
child: FocusTraversalGroup( mainAxisAlignment: MainAxisAlignment.center,
child: Row( crossAxisAlignment: isVertical ? CrossAxisAlignment.start : CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ /// Home btn
// Home btn _WonderHomeBtn(
_WonderHomeBtn( size: homeBtnSize,
size: homeBtnSize, wonderType: wonderType,
wonderType: wonderType, borderSize: showBg ? 6 : 2,
borderSize: showBg ? 6 : 2, ),
),
_TabBtn(0, tabController, /// Tabs
iconImg: 'editorial', label: $strings.wonderDetailsTabLabelInformation, color: iconColor), _TabBtn(
_TabBtn(1, tabController, 0,
iconImg: 'photos', label: $strings.wonderDetailsTabLabelImages, color: iconColor), tabController,
_TabBtn(2, tabController, iconImg: 'editorial',
iconImg: 'artifacts', label: $strings.wonderDetailsTabLabelArtifacts, color: iconColor), label: $strings.wonderDetailsTabLabelInformation,
_TabBtn(3, tabController, color: iconColor,
iconImg: 'timeline', label: $strings.wonderDetailsTabLabelEvents, 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.iconImg,
required this.color, required this.color,
required this.label, required this.label,
required this.axis,
}) : super(key: key); }) : super(key: key);
static const double _minWidth = 50; static const double minSize = 50;
static const double _maxWidth = 120; static const double maxSize = 100;
static const double crossBtnSize = 60;
final int index; final int index;
final TabController tabController; final TabController tabController;
final String iconImg; final String iconImg;
final Color color; final Color color;
final String label; final String label;
final Axis axis;
bool get _isVertical => axis == Axis.vertical;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -129,7 +167,7 @@ class _TabBtn extends StatelessWidget {
final iconImgPath = '${ImagePaths.common}/tab-$iconImg${selected ? '-active' : ''}.png'; final iconImgPath = '${ImagePaths.common}/tab-$iconImg${selected ? '-active' : ''}.png';
String tabLabel = localizations.tabLabel(tabIndex: index + 1, tabCount: tabController.length); String tabLabel = localizations.tabLabel(tabIndex: index + 1, tabCount: tabController.length);
tabLabel = '$label: $tabLabel'; 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( return MergeSemantics(
child: Semantics( child: Semantics(
selected: selected, selected: selected,
@ -139,7 +177,7 @@ class _TabBtn extends StatelessWidget {
padding: EdgeInsets.only(top: $styles.insets.md + $styles.insets.xs, bottom: $styles.insets.sm), padding: EdgeInsets.only(top: $styles.insets.md + $styles.insets.xs, bottom: $styles.insets.sm),
onPressed: () => tabController.index = index, onPressed: () => tabController.index = index,
semanticLabel: label, semanticLabel: label,
minimumSize: Size(btnWidth, 0), minimumSize: _isVertical ? Size(crossBtnSize, mainBtnSize) : Size(mainBtnSize, crossBtnSize),
// Image icon // Image icon
child: Image.asset(iconImgPath, height: 32, width: 32, color: selected ? null : color), child: Image.asset(iconImgPath, height: 32, width: 32, color: selected ? null : color),
), ),

View File

@ -26,7 +26,8 @@ class _WonderDetailsScreenState extends State<WonderDetailsScreen>
AnimationController? _fade; AnimationController? _fade;
final _detailsHasScrolled = ValueNotifier(false); final _detailsHasScrolled = ValueNotifier(false);
double? _tabBarHeight; double? _tabBarSize;
bool _useNavRail = false;
@override @override
void dispose() { void dispose() {
@ -42,16 +43,20 @@ class _WonderDetailsScreenState extends State<WonderDetailsScreen>
void _handleDetailsScrolled(double scrollPos) => _detailsHasScrolled.value = scrollPos > 0; void _handleDetailsScrolled(double scrollPos) => _detailsHasScrolled.value = scrollPos > 0;
void _handleTabMenuSized(Size size) { void _handleTabMenuSized(Size size) {
setState(() => _tabBarHeight = size.height - WonderDetailsTabMenu.buttonInset); setState(() {
_tabBarSize = (_useNavRail ? size.width : size.height) - WonderDetailsTabMenu.buttonInset;
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_useNavRail = context.isLandscape && context.heightPx < 900;
final wonder = wondersLogic.getData(widget.type); final wonder = wondersLogic.getData(widget.type);
int tabIndex = _tabController.index; int tabIndex = _tabController.index;
bool showTabBarBg = tabIndex != 1; bool showTabBarBg = tabIndex != 1;
final tabBarHeight = _tabBarHeight ?? 0; final tabBarSize = _tabBarSize ?? 0;
//final double tabBarHeight = WonderDetailsTabMenu.bottomPadding + 60; final menuPadding = _useNavRail ? EdgeInsets.only(left: tabBarSize) : EdgeInsets.only(bottom: tabBarSize);
return ColoredBox( return ColoredBox(
color: Colors.black, color: Colors.black,
child: Stack( child: Stack(
@ -62,21 +67,41 @@ class _WonderDetailsScreenState extends State<WonderDetailsScreen>
children: [ children: [
WonderEditorialScreen(wonder, onScroll: _handleDetailsScrolled), WonderEditorialScreen(wonder, onScroll: _handleDetailsScrolled),
PhotoGallery(collectionId: wonder.unsplashCollectionId, wonderType: wonder.type), PhotoGallery(collectionId: wonder.unsplashCollectionId, wonderType: wonder.type),
Padding(padding: EdgeInsets.only(bottom: tabBarHeight), child: ArtifactCarouselScreen(type: wonder.type)), AnimatedPadding(
Padding(padding: EdgeInsets.only(bottom: tabBarHeight), child: WonderEvents(type: widget.type)), 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 /// Tab menu
BottomCenter( Align(
alignment: _useNavRail ? Alignment.centerLeft : Alignment.bottomCenter,
child: ValueListenableBuilder<bool>( child: ValueListenableBuilder<bool>(
valueListenable: _detailsHasScrolled, valueListenable: _detailsHasScrolled,
builder: (_, value, ___) => MeasurableWidget( builder: (_, value, ___) => MeasurableWidget(
onChange: _handleTabMenuSized, onChange: _handleTabMenuSized,
child: WonderDetailsTabMenu(
tabController: _tabController, /// Animate the menu in when the axis changes
wonderType: wonder.type, child: Animate(
showBg: showTabBarBg, 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),
), ),
), ),
), ),

View File

@ -68,49 +68,52 @@ class WonderEvents extends StatelessWidget {
/// Landscape layout is a row, with the WonderImage on left and EventsList on the right /// Landscape layout is a row, with the WonderImage on left and EventsList on the right
Widget _buildTwoColumn(BuildContext context) { Widget _buildTwoColumn(BuildContext context) {
return Row( return Padding(
children: [ padding: EdgeInsets.all($styles.insets.lg),
/// WonderImage w/ Timeline btn child: Row(
Expanded( children: [
child: CenteredBox( /// WonderImage w/ Timeline btn
width: $styles.sizes.maxContentWidth3, Expanded(
child: Column( child: CenteredBox(
mainAxisAlignment: MainAxisAlignment.center, width: $styles.sizes.maxContentWidth3,
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisAlignment: MainAxisAlignment.center,
Gap($styles.insets.lg), mainAxisSize: MainAxisSize.min,
Expanded( children: [
child: Center( Gap($styles.insets.lg),
child: Column( Expanded(
mainAxisSize: MainAxisSize.min, child: Center(
children: [ child: Column(
_WonderImageWithTimeline(data: _data, height: min(500, context.heightPx - 300)), mainAxisSize: MainAxisSize.min,
Gap($styles.insets.lg), children: [
SizedBox(width: 400, child: _TimelineBtn(type: type)), _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 /// EventsList
Expanded( Expanded(
child: CenteredBox( child: CenteredBox(
width: $styles.sizes.maxContentWidth2, width: $styles.sizes.maxContentWidth2,
child: _EventsList( child: _EventsList(
data: _data, data: _data,
topHeight: 100, topHeight: 100,
blurOnScroll: false, blurOnScroll: false,
onScroll: _handleScroll, onScroll: _handleScroll,
initialScrollOffset: _scrollPos, initialScrollOffset: _scrollPos,
),
), ),
), ),
), ],
], ),
); );
} }