From e9ae67bff5d84fcc6231db8d1e85df9e41b38b16 Mon Sep 17 00:00:00 2001 From: Shawn Date: Tue, 22 Nov 2022 10:56:53 -0700 Subject: [PATCH] carousel WIP, added `trailing` param to SimpleHeader --- lib/ui/common/controls/buttons.dart | 7 +- lib/ui/common/controls/simple_header.dart | 71 +++-- .../artifact_carousel_screen.dart | 263 +++++++++++++++++- .../expanding_time_range_selector.dart | 3 +- .../screens/editorial/editorial_screen.dart | 8 +- .../screens/editorial/widgets/_app_bar.dart | 7 +- .../widgets/_collapsing_pull_quote_image.dart | 9 +- .../common/animated_clouds.dart | 3 +- 8 files changed, 326 insertions(+), 45 deletions(-) diff --git a/lib/ui/common/controls/buttons.dart b/lib/ui/common/controls/buttons.dart index 1b071d08..2abb5aa9 100644 --- a/lib/ui/common/controls/buttons.dart +++ b/lib/ui/common/controls/buttons.dart @@ -1,8 +1,9 @@ import 'package:wonders/common_libs.dart'; +import 'package:wonders/ui/common/app_icons.dart'; /// Shared methods across button types -Widget _buildIcon(BuildContext context, IconData icon, {required bool isSecondary, required double? size}) => - Icon(icon, color: isSecondary ? $styles.colors.black : $styles.colors.offWhite, size: size ?? 18); +Widget _buildIcon(BuildContext context, AppIcons icon, {required bool isSecondary, required double? size}) => + AppIcon(icon, color: isSecondary ? $styles.colors.black : $styles.colors.offWhite, size: size ?? 18); /// The core button that drives all other buttons. class AppBtn extends StatelessWidget { @@ -37,7 +38,7 @@ class AppBtn extends StatelessWidget { this.border, String? semanticLabel, String? text, - IconData? icon, + AppIcons? icon, double? iconSize, }) : child = null, circular = false, diff --git a/lib/ui/common/controls/simple_header.dart b/lib/ui/common/controls/simple_header.dart index f2cf7e41..bf1cfde6 100644 --- a/lib/ui/common/controls/simple_header.dart +++ b/lib/ui/common/controls/simple_header.dart @@ -2,14 +2,14 @@ import 'package:wonders/common_libs.dart'; class SimpleHeader extends StatelessWidget { const SimpleHeader(this.title, - {Key? key, this.subtitle, this.child, this.showBackBtn = true, this.isTransparent = false, this.onBack}) + {Key? key, this.subtitle, this.showBackBtn = true, this.isTransparent = false, this.onBack, this.trailing}) : super(key: key); final String title; final String? subtitle; - final Widget? child; final bool showBackBtn; final bool isTransparent; final VoidCallback? onBack; + final Widget Function(BuildContext context)? trailing; @override Widget build(BuildContext context) { @@ -17,35 +17,50 @@ class SimpleHeader extends StatelessWidget { color: isTransparent ? Colors.transparent : $styles.colors.black, child: SafeArea( bottom: false, - child: Row(children: [ - if (showBackBtn) BackBtn(onPressed: onBack).safe(), - Flexible( - fit: FlexFit.tight, - child: MergeSemantics( - child: Semantics( - header: true, - child: Column( - children: [ - if (!showBackBtn) Gap($styles.insets.xs), - Text( - title.toUpperCase(), - textHeightBehavior: TextHeightBehavior(applyHeightToFirstAscent: false), - style: $styles.text.h4.copyWith(color: $styles.colors.offWhite, fontWeight: FontWeight.w500), - ), - if (subtitle != null) - Text( - subtitle!.toUpperCase(), - textHeightBehavior: TextHeightBehavior(applyHeightToFirstAscent: false), - style: $styles.text.title1.copyWith(color: $styles.colors.accent1), - ), - if (!showBackBtn) Gap($styles.insets.md), - ], + child: SizedBox( + height: 64, + child: Stack( + children: [ + Positioned.fill( + child: Center( + child: Row(children: [ + Gap($styles.insets.sm), + if (showBackBtn) BackBtn(onPressed: onBack), + Spacer(), + if (trailing != null) trailing!.call(context), + Gap($styles.insets.sm), + //if (showBackBtn) Container(width: $styles.insets.lg * 2, alignment: Alignment.centerLeft, child: child), + ]), ), ), - ), + MergeSemantics( + child: Semantics( + header: true, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!showBackBtn) Gap($styles.insets.xs), + Text( + title.toUpperCase(), + textHeightBehavior: TextHeightBehavior(applyHeightToFirstAscent: false), + style: $styles.text.h4.copyWith(color: $styles.colors.offWhite, fontWeight: FontWeight.w500), + ), + if (subtitle != null) + Text( + subtitle!.toUpperCase(), + textHeightBehavior: TextHeightBehavior(applyHeightToFirstAscent: false), + style: $styles.text.title1.copyWith(color: $styles.colors.accent1), + ), + if (!showBackBtn) Gap($styles.insets.md), + ], + ), + ), + ), + ) + ], ), - if (showBackBtn) Container(width: $styles.insets.lg * 2, alignment: Alignment.centerLeft, child: child), - ]), + ), ), ); } diff --git a/lib/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart b/lib/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart index f9decc27..65329463 100644 --- a/lib/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart +++ b/lib/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:wonders/common_libs.dart'; import 'package:wonders/logic/data/highlight_data.dart'; +import 'package:wonders/ui/common/app_icons.dart'; import 'package:wonders/ui/common/controls/app_page_indicator.dart'; import 'package:wonders/ui/common/controls/simple_header.dart'; import 'package:wonders/ui/common/static_text_scale.dart'; @@ -10,6 +11,20 @@ import 'package:wonders/ui/common/static_text_scale.dart'; part 'widgets/_blurred_image_bg.dart'; part 'widgets/_carousel_item.dart'; +class GreyBox extends StatelessWidget { + const GreyBox(this.lbl, {Key? key, this.strength = .5}) : super(key: key); + final String lbl; + final double strength; + @override + Widget build(BuildContext context) => ColoredBox( + color: Colors.white, + child: Container( + color: Colors.grey.shade800.withOpacity(strength), + child: Center(child: Text(lbl, style: $styles.text.h3)), + ), + ); +} + class ArtifactCarouselScreen extends StatefulWidget { final WonderType type; const ArtifactCarouselScreen({Key? key, required this.type}) : super(key: key); @@ -19,6 +34,252 @@ class ArtifactCarouselScreen extends StatefulWidget { } class _ArtifactScreenState extends State { + PageController? _pageController; + final _currentPage = ValueNotifier(9999); + + late final List _artifacts = HighlightData.forWonder(widget.type); + late final _currentArtifactIndex = ValueNotifier(0); + + @override + void initState() { + super.initState(); + _handlePageChanged(); + } + + void _handlePageChanged() { + _currentPage.value = _pageController?.page ?? 0; + _currentArtifactIndex.value = _currentPage.value.round() % _artifacts.length; + } + + @override + Widget build(BuildContext context) { + bool shortMode = context.heightPx < 800; + final double bottomHeight = shortMode ? 190 : 310; + // Allow objects to become wider as the screen becomes tall, this allows + // them to grow taller as well, filling the available space better. + //double itemWidth = 150 + max(context.heightPx - 600, 0) / 600 * 150; + double itemHeight = context.heightPx - 150 - bottomHeight; + double itemWidth = itemHeight * .666; + // TODO: This could be optimized to only run if the size has changed...is it worth it? + _pageController?.dispose(); + _pageController = PageController( + viewportFraction: itemWidth / context.widthPx, + initialPage: _currentPage.value.round(), + ); + _pageController?.addListener(_handlePageChanged); + final pages = [ + GreyBox('item1', strength: .5), + GreyBox('item2', strength: .5), + GreyBox('item3', strength: .5), + GreyBox('item4', strength: .5), + GreyBox('item5', strength: .5), + ].map((e) => Padding(padding: EdgeInsets.all(10), child: e)).toList(); + + return Stack( + children: [ + /// Background + Positioned.fill( + child: ValueListenableBuilder( + valueListenable: _currentArtifactIndex, + builder: (_, value, __) { + return _BlurredImageBg(url: _artifacts[value].imageUrl); + }), + ), + + /// Bottom Text w/ Circle + BottomCenter( + child: ValueListenableBuilder( + valueListenable: _currentArtifactIndex, + builder: (_, value, __) => _CarouselBottomTextWithCircle( + artifact: _artifacts[value], + height: bottomHeight, + shortMode: shortMode, + state: this, + ), + ), + ), + + /// Carousel Items + PageView.builder( + controller: _pageController, + itemBuilder: (_, index) { + final child = pages[index % pages.length]; + return ValueListenableBuilder( + valueListenable: _currentPage, + builder: (_, value, __) { + final int offset = (value.round() - index).abs(); + return CollapsingCarouselListItem( + width: itemWidth, + bottom: bottomHeight, + indexOffset: min(3, offset), + child: child, + ); + }, + ); + }, + ), + + /// header + SimpleHeader( + $strings.artifactsTitleArtifacts, + showBackBtn: true, + isTransparent: true, + trailing: (context) => CircleBtn( + semanticLabel: $strings.artifactsButtonBrowse, + onPressed: () {}, + child: AppIcon(AppIcons.search), + ), + ), + ], + ); + } +} + +/// Handles the carousel specific logic, like setting the height and vertical alignment of each item. +/// This lets the child simply render it's contents +class CollapsingCarouselListItem extends StatelessWidget { + const CollapsingCarouselListItem( + {Key? key, required this.child, required this.indexOffset, required this.width, required this.bottom}) + : super(key: key); + final Widget child; + final int indexOffset; + final double width; + final double bottom; + @override + Widget build(BuildContext context) { + // Calculate offset, this will be subtracted from the bottom padding moving the element downwards + double vtOffset = indexOffset * 30; + return AppBtn.basic( + onPressed: () => print('todo'), + semanticLabel: 'TODO', + child: AnimatedOpacity( + duration: $styles.times.fast, + opacity: indexOffset.abs() <= 2 ? 1 : 0, + child: AnimatedPadding( + duration: $styles.times.fast, + padding: EdgeInsets.only(bottom: bottom), + child: BottomCenter( + child: AnimatedContainer( + duration: $styles.times.fast, + // Center item is portrait, the others are square + height: indexOffset == 0 ? width * 1.3 : width, + child: Placeholder(), + ), + ), + ), + ), + ); + } +} + +class _CarouselBottomTextWithCircle extends StatelessWidget { + const _CarouselBottomTextWithCircle( + {Key? key, required this.artifact, required this.height, required this.state, required this.shortMode}) + : super(key: key); + + final HighlightData artifact; + final double height; + final _ArtifactScreenState state; + final bool shortMode; + + @override + Widget build(BuildContext context) { + return Container( + width: $styles.sizes.maxContentWidth2, + height: height, + padding: EdgeInsets.symmetric(horizontal: $styles.insets.md), + alignment: Alignment.center, + child: Stack( + children: [ + /// BgCircle + _buildBgCircle(), + + /// Text + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Gap($styles.insets.md), + Column( + children: [ + IgnorePointer( + ignoringSemantics: false, + child: Semantics( + button: true, + // TODO: FIX + // onIncrease: () => _handleArtifactTap(_currentOffset.round() + 1), + // onDecrease: () => _handleArtifactTap(_currentOffset.round() - 1), + // onTap: () => _handleArtifactTap(_currentOffset.round()), + liveRegion: true, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // Force column to stretch horizontally so text is centered + SizedBox(width: double.infinity), + // Stop text from scaling to make layout a little easier, it's already quite large + StaticTextScale( + child: Text( + artifact.title, + overflow: TextOverflow.ellipsis, + style: $styles.text.h2.copyWith(color: $styles.colors.black, height: 1.2, fontSize: 32), + textAlign: TextAlign.center, + maxLines: 2, + ), + ), + if (!shortMode) ...[ + Gap($styles.insets.xxs), + Text( + artifact.date.isEmpty ? '--' : artifact.date, + style: $styles.text.body, + textAlign: TextAlign.center, + ), + ] + ], + ).animate(key: ValueKey(artifact.artifactId)).fadeIn(), + ), + ), + Gap($styles.insets.sm), + if (!shortMode) + AppPageIndicator( + count: state._artifacts.length, + controller: state._pageController!, + semanticPageTitle: $strings.artifactsSemanticArtifact, + ), + ], + ), + Gap($styles.insets.md), + Spacer(), + AppBtn.from( + text: $strings.artifactsButtonBrowse, + icon: AppIcons.search, + expand: true, + onPressed: () {}, //_handleSearchTap, + ), + Gap($styles.insets.lg), + ], + ), + ], + ), + ); + } + + OverflowBox _buildBgCircle() { + return OverflowBox( + maxWidth: 2000, + maxHeight: 2000, + child: Transform.translate( + offset: Offset(0, 1000 - height * .7), + child: Container( + decoration: BoxDecoration( + color: $styles.colors.offWhite.withOpacity(0.8), + borderRadius: BorderRadius.vertical(top: Radius.circular(999)), + ), + ), + ), + ); + } +} + +class _ArtifactScreenState2 extends State { // Used to cap white background dimensions. static const double _maxElementWidth = 440; static const double _partialElementWidth = 0.9; @@ -199,7 +460,7 @@ class _ArtifactScreenState extends State { Gap(_small ? $styles.insets.sm : $styles.insets.md), AppBtn.from( text: $strings.artifactsButtonBrowse, - icon: Icons.search, + icon: AppIcons.search, expand: true, onPressed: _handleSearchTap, ), diff --git a/lib/ui/screens/artifact/artifact_search/time_range_selector/expanding_time_range_selector.dart b/lib/ui/screens/artifact/artifact_search/time_range_selector/expanding_time_range_selector.dart index 4b290127..af25aa8c 100644 --- a/lib/ui/screens/artifact/artifact_search/time_range_selector/expanding_time_range_selector.dart +++ b/lib/ui/screens/artifact/artifact_search/time_range_selector/expanding_time_range_selector.dart @@ -1,6 +1,7 @@ import 'package:wonders/common_libs.dart'; import 'package:wonders/logic/common/string_utils.dart'; import 'package:wonders/logic/data/wonder_data.dart'; +import 'package:wonders/ui/common/app_icons.dart'; import 'package:wonders/ui/common/cards/opening_card.dart'; import 'package:wonders/ui/common/wonders_timeline_builder.dart'; import 'package:wonders/ui/screens/artifact/artifact_search/artifact_search_screen.dart'; @@ -193,7 +194,7 @@ class _OpenedTimeRange extends StatelessWidget { onPressed: onClose, semanticLabel: $strings.expandingTimeSelectorSemanticSelector, enableFeedback: false, // handled when panelController changes. - icon: Icons.close, + icon: AppIcons.close, iconSize: 20, padding: EdgeInsets.symmetric(vertical: $styles.insets.xxs), bgColor: Colors.transparent, diff --git a/lib/ui/screens/editorial/editorial_screen.dart b/lib/ui/screens/editorial/editorial_screen.dart index 6a9e8820..71e03557 100644 --- a/lib/ui/screens/editorial/editorial_screen.dart +++ b/lib/ui/screens/editorial/editorial_screen.dart @@ -78,11 +78,13 @@ class _WonderEditorialScreenState extends State { children: [ /// Background Positioned.fill( - child: ValueListenableBuilder( + child: ValueListenableBuilder( valueListenable: _scrollPos, builder: (_, value, __) { - return Container( - color: widget.data.type.bgColor.withOpacity(1), + bool showBg = value < 700; + return AnimatedContainer( + duration: $styles.times.fast, + color: widget.data.type.bgColor.withOpacity(showBg ? 1 : 0), ); }, ), diff --git a/lib/ui/screens/editorial/widgets/_app_bar.dart b/lib/ui/screens/editorial/widgets/_app_bar.dart index b84e102f..365ec25e 100644 --- a/lib/ui/screens/editorial/widgets/_app_bar.dart +++ b/lib/ui/screens/editorial/widgets/_app_bar.dart @@ -74,10 +74,9 @@ class _AppBar extends StatelessWidget { /// Colored overlay if (showOverlay) ...[ - ClipRect( - child: ColoredBox( - color: wonderType.bgColor.withOpacity(.8), - ).animate().fade(duration: $styles.times.fast), + AnimatedContainer( + duration: $styles.times.slow, + color: wonderType.bgColor.withOpacity(showOverlay ? .8 : 0), ), ], ], diff --git a/lib/ui/screens/editorial/widgets/_collapsing_pull_quote_image.dart b/lib/ui/screens/editorial/widgets/_collapsing_pull_quote_image.dart index 43b60c95..d1caa237 100644 --- a/lib/ui/screens/editorial/widgets/_collapsing_pull_quote_image.dart +++ b/lib/ui/screens/editorial/widgets/_collapsing_pull_quote_image.dart @@ -10,15 +10,16 @@ class _CollapsingPullQuoteImage extends StatelessWidget { // Start transitioning when we are halfway up the screen final collapseStartPx = context.heightPx * 1; final collapseEndPx = context.heightPx * .15; - const double imgHeight = 430; + const double imgHeight = 500; const double outerPadding = 100; /// A single piece of quote text, this widget has one on top, and one on bottom Widget buildText(String value, double collapseAmt, {required bool top, bool isAuthor = false}) { - var quoteStyle = $styles.text.quote1; + /// Use a fixed font-size for this for consistent scaling + var quoteStyle = $styles.text.quote1.copyWith(fontSize: 32); quoteStyle = quoteStyle.copyWith(color: $styles.colors.caption); if (isAuthor) { - quoteStyle = quoteStyle.copyWith(fontSize: 20 * $styles.scale, fontWeight: FontWeight.w600); + quoteStyle = quoteStyle.copyWith(fontSize: 20, fontWeight: FontWeight.w600); } double offsetY = (imgHeight / 2 + outerPadding * .25) * (1 - collapseAmt); if (top) offsetY *= -1; // flip? @@ -44,7 +45,7 @@ class _CollapsingPullQuoteImage extends StatelessWidget { return MergeSemantics( child: CenteredBox( padding: EdgeInsets.symmetric(vertical: outerPadding), - width: 450, + width: imgHeight * .66, child: Stack( children: [ Container( diff --git a/lib/ui/wonder_illustrations/common/animated_clouds.dart b/lib/ui/wonder_illustrations/common/animated_clouds.dart index 121bcbee..3749e1ac 100644 --- a/lib/ui/wonder_illustrations/common/animated_clouds.dart +++ b/lib/ui/wonder_illustrations/common/animated_clouds.dart @@ -112,6 +112,7 @@ class _AnimatedCloudsState extends State with SingleTickerProvid List<_Cloud> _getClouds() { Size size = ContextUtils.getSize(context) ?? Size(context.widthPx, 400); + rndSeed = _getCloudSeed(widget.wonderType); return List<_Cloud>.generate(3, (index) { return _Cloud( @@ -141,7 +142,7 @@ class _Cloud extends StatelessWidget { child: Image.asset( ImagePaths.cloud, opacity: AlwaysStoppedAnimation(.4 * opacity), - width: context.widthPx * .65 * scale, + width: 400 * scale, fit: BoxFit.fitWidth, ), );