From 7d6cca3b6b80857da4e5dad95feb34750221d525 Mon Sep 17 00:00:00 2001 From: Shawn Date: Thu, 1 Dec 2022 20:09:28 -0700 Subject: [PATCH] Polish artifacts carousel --- lib/logic/app_logic.dart | 2 +- lib/ui/common/controls/simple_header.dart | 2 - .../artifact_carousel_screen.dart | 483 +++--------------- .../widgets/_bottom_text_content.dart | 88 ++++ .../widgets/_carousel_item.dart | 136 ----- .../widgets/_collapsing_carousel_item.dart | 74 +++ .../screens/editorial/editorial_screen.dart | 2 +- .../screens/editorial/widgets/_app_bar.dart | 2 +- lib/ui/screens/intro/intro_screen.dart | 35 ++ 9 files changed, 267 insertions(+), 557 deletions(-) create mode 100644 lib/ui/screens/artifact/artifact_carousel/widgets/_bottom_text_content.dart delete mode 100644 lib/ui/screens/artifact/artifact_carousel/widgets/_carousel_item.dart create mode 100644 lib/ui/screens/artifact/artifact_carousel/widgets/_collapsing_carousel_item.dart diff --git a/lib/logic/app_logic.dart b/lib/logic/app_logic.dart index 48900654..a75119c6 100644 --- a/lib/logic/app_logic.dart +++ b/lib/logic/app_logic.dart @@ -64,7 +64,7 @@ class AppLogic { // Load initial view (replace empty initial view which is covered by a native splash screen) bool showIntro = settingsLogic.hasCompletedOnboarding.value == false; - if (showIntro) { + if (true) { appRouter.go(ScreenPaths.intro); } else { appRouter.go(ScreenPaths.home); diff --git a/lib/ui/common/controls/simple_header.dart b/lib/ui/common/controls/simple_header.dart index bf1cfde6..3b56101e 100644 --- a/lib/ui/common/controls/simple_header.dart +++ b/lib/ui/common/controls/simple_header.dart @@ -40,7 +40,6 @@ class SimpleHeader extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (!showBackBtn) Gap($styles.insets.xs), Text( title.toUpperCase(), textHeightBehavior: TextHeightBehavior(applyHeightToFirstAscent: false), @@ -52,7 +51,6 @@ class SimpleHeader extends StatelessWidget { textHeightBehavior: TextHeightBehavior(applyHeightToFirstAscent: false), style: $styles.text.title1.copyWith(color: $styles.colors.accent1), ), - if (!showBackBtn) Gap($styles.insets.md), ], ), ), 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 49f18366..2d6525c7 100644 --- a/lib/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart +++ b/lib/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart @@ -9,21 +9,8 @@ import 'package:wonders/ui/common/controls/simple_header.dart'; 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)), - ), - ); -} +part 'widgets/_collapsing_carousel_item.dart'; +part 'widgets/_bottom_text_content.dart'; class ArtifactCarouselScreen extends StatefulWidget { final WonderType type; @@ -38,27 +25,38 @@ class _ArtifactScreenState extends State { final _currentPage = ValueNotifier(9999); late final List _artifacts = HighlightData.forWonder(widget.type); - late final _currentArtifactIndex = ValueNotifier(0); + late final _currentArtifactIndex = ValueNotifier(_wrappedPageIndex); - @override - void initState() { - super.initState(); - _handlePageChanged(); - } + int get _wrappedPageIndex => _currentPage.value.round() % _artifacts.length; void _handlePageChanged() { _currentPage.value = _pageController?.page ?? 0; - _currentArtifactIndex.value = _currentPage.value.round() % _artifacts.length; + _currentArtifactIndex.value = _wrappedPageIndex; + } + + void _handleSearchTap() => context.push(ScreenPaths.search(widget.type)); + + void _handleArtifactTap(int index) { + int delta = index - _currentPage.value.round(); + if (delta == 0) { + HighlightData data = _artifacts[index % _artifacts.length]; + context.push(ScreenPaths.artifact(data.artifactId)); + } else { + _pageController?.animateToPage( + _currentPage.value.round() + delta, + duration: $styles.times.fast, + curve: Curves.easeOut, + ); + } } @override Widget build(BuildContext context) { bool shortMode = context.heightPx < 800; - final double bottomHeight = shortMode ? 190 : 310; + final double bottomHeight = shortMode ? 240 : 340; // 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 itemHeight = (context.heightPx - 200 - bottomHeight).clamp(250, 400); double itemWidth = itemHeight * .666; // TODO: This could be optimized to only run if the size has changed...is it worth it? _pageController?.dispose(); @@ -67,17 +65,16 @@ class _ArtifactScreenState extends State { 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(); + final pages = _artifacts.map((e) { + return Padding( + padding: EdgeInsets.all(10), + child: _DoubleBorderImage(e), + ); + }).toList(); return Stack( children: [ - /// Background + /// Blurred Bg Positioned.fill( child: ValueListenableBuilder( valueListenable: _currentArtifactIndex, @@ -86,11 +83,37 @@ class _ArtifactScreenState extends State { }), ), - /// Bottom Text w/ Circle + /// BgCircle + _buildBgCircle(bottomHeight), + + /// Carousel Items + PageView.builder( + controller: _pageController, + itemBuilder: (_, index) { + final wrappedIndex = index % pages.length; + final child = pages[wrappedIndex]; + return ValueListenableBuilder( + valueListenable: _currentPage, + builder: (_, value, __) { + final int offset = (value.round() - index).abs(); + return _CollapsingCarouselItem( + width: itemWidth, + bottom: bottomHeight - 20, + indexOffset: min(3, offset), + onPressed: () => _handleArtifactTap(index), + title: _artifacts[wrappedIndex].title, + child: child, + ); + }, + ); + }, + ), + + /// Bottom Text BottomCenter( child: ValueListenableBuilder( valueListenable: _currentArtifactIndex, - builder: (_, value, __) => _CarouselBottomTextWithCircle( + builder: (_, value, __) => _BottomTextContent( artifact: _artifacts[value], height: bottomHeight, shortMode: shortMode, @@ -99,404 +122,32 @@ class _ArtifactScreenState extends State { ), ), - /// 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 + /// Header SimpleHeader( $strings.artifactsTitleArtifacts, - showBackBtn: true, + showBackBtn: false, isTransparent: true, trailing: (context) => CircleBtn( semanticLabel: $strings.artifactsButtonBrowse, - onPressed: () {}, + onPressed: _handleSearchTap, 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(context), - ), - Gap($styles.insets.lg), - ], - ), - ], - ), - ); - } - - void _handleSearchTap(BuildContext context) { - //context.push(ScreenPaths.search(widget.type)); - } - - OverflowBox _buildBgCircle() { + OverflowBox _buildBgCircle(double height) { + const double size = 1500; return OverflowBox( - maxWidth: 2000, - maxHeight: 2000, + maxWidth: size, + maxHeight: size, child: Transform.translate( - offset: Offset(0, 1000 - height * .7), + offset: Offset(0, size / 2), 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; - static const double _maxElementHeight = 640; - - // Locally store loaded artifacts. - late List _artifacts; - - late PageController _controller; - final _currentIndex = ValueNotifier(0); - - double get _currentOffset { - bool hasOffset = _controller.hasClients && _controller.position.haveDimensions; - return hasOffset ? _controller.page! : _controller.initialPage * 1.0; - } - - HighlightData get _currentArtifact => _artifacts[_currentIndex.value]; - - double get _backdropWidth { - final w = context.widthPx; - return w <= _maxElementWidth ? w : min(w * _partialElementWidth, _maxElementWidth); - } - - double get _backdropHeight => math.min(context.heightPx * 0.65, _maxElementHeight); - bool get _small => _backdropHeight / _maxElementHeight < 0.7; - - @override - void initState() { - super.initState(); - _artifacts = HighlightData.forWonder(widget.type); - - _controller = PageController( - // start at a high offset so we can scroll backwards: - initialPage: _artifacts.length * 9999, - viewportFraction: 0.5, - )..addListener(_handleCarouselScroll); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - void _handleCarouselScroll() => _currentIndex.value = _currentOffset.round() % _artifacts.length; - - void _handleArtifactTap(int index) { - int delta = index - _currentOffset.round(); - if (delta == 0) { - HighlightData data = _artifacts[index % _artifacts.length]; - context.push(ScreenPaths.artifact(data.artifactId)); - } else { - _controller.animateToPage( - _currentOffset.round() + delta, - duration: $styles.times.fast, - curve: Curves.easeOut, - ); - } - } - - void _handleSearchTap() { - context.push(ScreenPaths.search(widget.type)); - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: _currentIndex, - builder: (context, index, _) { - return Container( - color: $styles.colors.greyStrong, - child: Stack( - children: [ - /// Background image - Positioned.fill( - child: _BlurredImageBg(url: _currentArtifact.imageUrl), - ), - - /// Content - Column( - children: [ - SimpleHeader($strings.artifactsTitleArtifacts, showBackBtn: false, isTransparent: true), - Gap($styles.insets.xs), - Expanded( - child: Stack(children: [ - // White arch, covering bottom half: - _buildWhiteArch(), - - // Carousel - _buildCarouselPageView(), - - // Text content - _buildBottomTextContent(), - ]), - ), - ], - ), - ], - ), - ); - }, - ); - } - - Widget _buildWhiteArch() { - return BottomCenter( - child: Container( - width: _backdropWidth, - height: _backdropHeight, - decoration: BoxDecoration( - color: $styles.colors.offWhite.withOpacity(0.8), - borderRadius: BorderRadius.vertical(top: Radius.circular(999)), - ), - ), - ); - } - - Widget _buildBottomTextContent() { - return BottomCenter( - child: Container( - width: _backdropWidth, - padding: EdgeInsets.symmetric(horizontal: $styles.insets.md), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Gap($styles.insets.md), - Container( - width: _backdropWidth, - padding: EdgeInsets.symmetric(horizontal: $styles.insets.sm), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IgnorePointer( - ignoringSemantics: false, - child: Semantics( - button: true, - onIncrease: () => _handleArtifactTap(_currentOffset.round() + 1), - onDecrease: () => _handleArtifactTap(_currentOffset.round() - 1), - onTap: () => _handleArtifactTap(_currentOffset.round()), - liveRegion: true, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: _small ? 90 : 110, - alignment: Alignment.center, - child: StaticTextScale( - child: Text( - _currentArtifact.title, - overflow: TextOverflow.ellipsis, - style: $styles.text.h2.copyWith(color: $styles.colors.black, height: 1.2), - textAlign: TextAlign.center, - maxLines: 2, - ), - ), - ), - if (!_small) Gap($styles.insets.xxs), - Text( - _currentArtifact.date.isEmpty ? '--' : _currentArtifact.date, - style: $styles.text.body, - textAlign: TextAlign.center, - ), - ], - ).animate(key: ValueKey(_currentArtifact.artifactId)).fadeIn(), - ), - ), - Gap(_small ? $styles.insets.xs : $styles.insets.sm), - AppPageIndicator( - count: _artifacts.length, - controller: _controller, - semanticPageTitle: $strings.artifactsSemanticArtifact, - ), - ], - ), - ), - Gap(_small ? $styles.insets.sm : $styles.insets.md), - AppBtn.from( - text: $strings.artifactsButtonBrowse, - icon: AppIcons.search, - expand: true, - onPressed: _handleSearchTap, - ), - Gap(_small ? $styles.insets.md : $styles.insets.lg), - ], - ), - ), - ); - } - - Widget _buildCarouselPageView() { - return Center( - child: SizedBox( - width: _backdropWidth, - child: ExcludeSemantics( - child: PageView.builder( - controller: _controller, - clipBehavior: Clip.none, - itemBuilder: (context, index) => ListenableBuilder( - listenable: _controller, - builder: (_, __) { - return _CarouselItem( - index: index, - currentPage: _currentOffset, - artifact: _artifacts[index % _artifacts.length], - bottomPadding: _backdropHeight, - maxWidth: _backdropWidth, - maxHeight: _backdropHeight, - onPressed: () => _handleArtifactTap(index), - ); - }, - ), + borderRadius: BorderRadius.vertical(top: Radius.circular(size * .45)), ), ), ), diff --git a/lib/ui/screens/artifact/artifact_carousel/widgets/_bottom_text_content.dart b/lib/ui/screens/artifact/artifact_carousel/widgets/_bottom_text_content.dart new file mode 100644 index 00000000..cb42903e --- /dev/null +++ b/lib/ui/screens/artifact/artifact_carousel/widgets/_bottom_text_content.dart @@ -0,0 +1,88 @@ +part of '../artifact_carousel_screen.dart'; + +class _BottomTextContent extends StatelessWidget { + const _BottomTextContent( + {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: [ + /// Text + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Gap($styles.insets.xl), + Column( + children: [ + IgnorePointer( + ignoringSemantics: false, + child: Semantics( + button: true, + onIncrease: () => state._handleArtifactTap(state._currentPage.value.round() + 1), + onDecrease: () => state._handleArtifactTap(state._currentPage.value.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, + ), + ], + ), + if (!shortMode) Gap($styles.insets.md), + Spacer(), + AppBtn.from( + text: $strings.artifactsButtonBrowse, + icon: AppIcons.search, + expand: true, + onPressed: state._handleSearchTap, + ), + Gap($styles.insets.lg), + ], + ), + ], + ), + ); + } +} diff --git a/lib/ui/screens/artifact/artifact_carousel/widgets/_carousel_item.dart b/lib/ui/screens/artifact/artifact_carousel/widgets/_carousel_item.dart deleted file mode 100644 index 0241bde7..00000000 --- a/lib/ui/screens/artifact/artifact_carousel/widgets/_carousel_item.dart +++ /dev/null @@ -1,136 +0,0 @@ -part of '../artifact_carousel_screen.dart'; - -class _CarouselItem extends StatelessWidget { - const _CarouselItem({ - Key? key, - required this.index, - required this.currentPage, - required this.artifact, - required this.bottomPadding, - required this.maxWidth, - required this.maxHeight, - required this.onPressed, - }) : super(key: key); - final HighlightData artifact; - final int index; - final double currentPage; - final double bottomPadding; - final double maxWidth; - final double maxHeight; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return AppBtn.basic( - semanticLabel: 'Explore artifact details', - onPressed: onPressed, - pressEffect: false, - child: _ImagePreview( - image: NetworkImage(artifact.imageUrlSmall), - bottomPadding: bottomPadding, - maxWidth: maxWidth, - maxHeight: maxHeight, - offsetAmt: currentPage - index.toDouble(), - ), - ); - } -} - -class _ImagePreview extends StatelessWidget { - const _ImagePreview( - {Key? key, - required this.image, - required this.offsetAmt, - required this.bottomPadding, - required this.maxWidth, - required this.maxHeight}) - : super(key: key); - final ImageProvider image; - final double offsetAmt; - final double bottomPadding; - final double maxWidth; - final double maxHeight; - - @override - Widget build(BuildContext context) { - // Additional scale of the main page, making it larger than the others. - const double mainPageScaleFactor = 0.40; - - // Additional Y scale of the main page, making it elongated. - const double mainPageScaleFactorY = -0.13; - - // Border variables - const double borderPadding = 4.0; - const double borderWidth = 1.0; - - // Base scale units that we can treat like pixels. - double baseWidthScale = 1 / maxWidth; - double baseHeightScale = 1 / context.heightPx; - - // Size of the pages themselves. - double pageWidth = baseWidthScale * maxWidth * 0.65; - double pageHeight = baseHeightScale * maxWidth * 0.45; - - // Get the current page offset value compared to other pages. -1 is left, 0 is middle, 1 is right, etc. - // Note: This value can be in between whole numbers, like 0.25 and -0.75. - double pageOffset = math.max(-2, math.min(2, offsetAmt)); - - // Add a scale-up to the main page. - double midPageScaleUp = (1 - math.min(1, pageOffset.abs())) * mainPageScaleFactor; - double midPageScaleUpY = (1 - math.min(1, pageOffset.abs())) * mainPageScaleFactorY; - - // Use absolute value of offset so images always move down. - double yOffsetFactor = pageOffset.abs(); - - if (pageOffset >= -1 && pageOffset <= 1) { - // Create an offset factor using sin/cos to ease. - yOffsetFactor = 1 - math.cos(pageOffset * math.pi / 2.0).abs(); - } - - // Multiply the offset factors with the width/height scale to convert them to fractionals. - double yOffset = yOffsetFactor * ((baseHeightScale / 2) * (maxWidth / 2)); - - // Apply a vertical offset based on the bottom padding provided. This includes half the element width. - double bottomPadding = (baseHeightScale / 2) * (maxHeight * 2 - (maxWidth / 2)); - - double widthFactor = pageWidth + midPageScaleUp; - double heightFactor = pageHeight + midPageScaleUp + midPageScaleUpY; - - double opacity = max(0, 1 - max(0, pageOffset.abs() - 1) * 2); - - // Scale box for sizing. Uses both the element scale and the element Y scale. - return FractionalTranslation( - // Move the pages around before scaling, as scaling will directly affect their translation. - translation: Offset(0, yOffset - bottomPadding), - child: FractionallySizedBox( - // Scale the elements according to whether they are on the sides or middle. - alignment: Alignment.bottomCenter, - widthFactor: widthFactor, - heightFactor: heightFactor, - // Translation box for positioning. - child: Opacity( - opacity: opacity, - child: Container( - // Add an outer border with the rounded ends. - decoration: BoxDecoration( - shape: BoxShape.rectangle, - border: Border.all(color: $styles.colors.offWhite, width: borderWidth), - borderRadius: BorderRadius.all(Radius.circular(999)), - ), - - child: Padding( - padding: EdgeInsets.all(borderPadding), - child: ClipRRect( - borderRadius: BorderRadius.circular(999), - child: ColoredBox( - color: $styles.colors.greyMedium, - child: AppImage(image: image, fit: BoxFit.cover, scale: 0.5), - ), - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/ui/screens/artifact/artifact_carousel/widgets/_collapsing_carousel_item.dart b/lib/ui/screens/artifact/artifact_carousel/widgets/_collapsing_carousel_item.dart new file mode 100644 index 00000000..408f60b9 --- /dev/null +++ b/lib/ui/screens/artifact/artifact_carousel/widgets/_collapsing_carousel_item.dart @@ -0,0 +1,74 @@ +part of '../artifact_carousel_screen.dart'; + +/// 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 _CollapsingCarouselItem extends StatelessWidget { + const _CollapsingCarouselItem( + {Key? key, + required this.child, + required this.indexOffset, + required this.width, + required this.bottom, + required this.onPressed, + required this.title}) + : super(key: key); + final Widget child; + final int indexOffset; + final double width; + final double bottom; + final VoidCallback onPressed; + final String title; + @override + Widget build(BuildContext context) { + // Calculate offset, this will be subtracted from the bottom padding moving the element downwards + double vtOffset = 0; + if (indexOffset == 1) vtOffset = width * .1; + if (indexOffset == 2) vtOffset = width * .4; + if (indexOffset > 2) vtOffset = width; + + final content = AnimatedOpacity( + duration: $styles.times.fast, + opacity: indexOffset.abs() <= 2 ? 1 : 0, + child: AnimatedPadding( + duration: $styles.times.fast, + padding: EdgeInsets.only(bottom: max(bottom - vtOffset, 0)), + child: BottomCenter( + child: AnimatedContainer( + duration: $styles.times.fast, + // Center item is portrait, the others are square + height: indexOffset == 0 ? width * 1.5 : width, + width: width, + child: child, + ), + ), + ), + ); + if (indexOffset > 2) return content; + return AppBtn.basic(onPressed: onPressed, semanticLabel: title, child: content); + } +} + +class _DoubleBorderImage extends StatelessWidget { + const _DoubleBorderImage(this.data, {Key? key}) : super(key: key); + final HighlightData data; + @override + Widget build(BuildContext context) => Container( + // Add an outer border with the rounded ends. + decoration: BoxDecoration( + shape: BoxShape.rectangle, + border: Border.all(color: $styles.colors.offWhite, width: 1), + borderRadius: BorderRadius.all(Radius.circular(999)), + ), + + child: Padding( + padding: EdgeInsets.all($styles.insets.xs), + child: ClipRRect( + borderRadius: BorderRadius.circular(999), + child: ColoredBox( + color: $styles.colors.greyMedium, + child: AppImage(image: NetworkImage(data.imageUrlSmall), fit: BoxFit.cover, scale: 0.5), + ), + ), + ), + ); +} diff --git a/lib/ui/screens/editorial/editorial_screen.dart b/lib/ui/screens/editorial/editorial_screen.dart index 6a11f2e7..be22eddd 100644 --- a/lib/ui/screens/editorial/editorial_screen.dart +++ b/lib/ui/screens/editorial/editorial_screen.dart @@ -72,7 +72,7 @@ class _WonderEditorialScreenState extends State { return LayoutBuilder(builder: (_, constraints) { bool shortMode = constraints.biggest.height < 700; double illustrationHeight = shortMode ? 250 : 280; - double minAppBarHeight = shortMode ? 80 : 120; + double minAppBarHeight = shortMode ? 80 : 150; /// Attempt to maintain a similar aspect ratio for the image within the app-bar double maxAppBarHeight = min(context.widthPx, $styles.sizes.maxContentWidth1) * 1.5; diff --git a/lib/ui/screens/editorial/widgets/_app_bar.dart b/lib/ui/screens/editorial/widgets/_app_bar.dart index 7306d49a..f20740e3 100644 --- a/lib/ui/screens/editorial/widgets/_app_bar.dart +++ b/lib/ui/screens/editorial/widgets/_app_bar.dart @@ -80,7 +80,7 @@ class _AppBar extends StatelessWidget { /// Colored overlay if (showOverlay) ...[ AnimatedContainer( - duration: $styles.times.slow, + duration: $styles.times.med, color: wonderType.bgColor.withOpacity(showOverlay ? .8 : 0), ), ], diff --git a/lib/ui/screens/intro/intro_screen.dart b/lib/ui/screens/intro/intro_screen.dart index 266a5b70..5d79d0c9 100644 --- a/lib/ui/screens/intro/intro_screen.dart +++ b/lib/ui/screens/intro/intro_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:wonders/common_libs.dart'; import 'package:wonders/ui/common/app_icons.dart'; import 'package:wonders/ui/common/controls/app_page_indicator.dart'; +import 'package:wonders/ui/common/gradient_container.dart'; import 'package:wonders/ui/common/static_text_scale.dart'; import 'package:wonders/ui/common/themed_text.dart'; import 'package:wonders/ui/common/utils/app_haptics.dart'; @@ -139,6 +140,10 @@ class _IntroScreenState extends State { child: _buildNavText(context), ), ), + + // Build a cpl overlays to hide the content when swiping on very wide screens + _buildHzGradientOverlay(left: true), + _buildHzGradientOverlay(), ]); return DefaultTextColor( @@ -150,6 +155,36 @@ class _IntroScreenState extends State { ); } + Widget _buildHzGradientOverlay({bool left = false}) { + return Align( + alignment: Alignment(left ? -1 : 1, 0), + child: FractionallySizedBox( + widthFactor: .5, + child: Padding( + padding: EdgeInsets.only(left: left ? 0 : 200, right: left ? 200 : 0), + child: Transform.scale( + scaleX: left ? -1 : 1, + child: HzGradient([ + $styles.colors.black.withOpacity(0), + $styles.colors.black, + ], const [ + 0, + .2 + ])), + ), + ), + ); + // CenterLeft( + // child: FractionallySizedBox( + // widthFactor: .5, + // child: Padding( + // padding: const EdgeInsets.only(right: 200), + // child: HzGradient([$styles.colors.black, $styles.colors.black.withOpacity(0)], const [.8, 1]), + // ), + // ), + // ); + } + Widget _buildFinishBtn(BuildContext context) { return ValueListenableBuilder( valueListenable: _currentPage,