From 6a31c38dab5daa416f222043818143efe20e341b Mon Sep 17 00:00:00 2001 From: Shawn Date: Tue, 20 Sep 2022 19:01:23 -0600 Subject: [PATCH] Improve a11y on home-screen and carousel --- .../common/controls/app_page_indicator.dart | 28 +- lib/ui/common/pop_router_on_over_scroll.dart | 50 ++ .../artifact_carousel_screen.dart | 271 +++++------ .../artifact_details_screen.dart | 23 +- .../screens/editorial/editorial_screen.dart | 16 +- .../home/_vertical_swipe_controller.dart | 18 +- .../home/widgets/_animated_arrow_button.dart | 2 +- lib/ui/screens/home/wonders_home_screen.dart | 429 +++++++++--------- .../wonder_events/widgets/_events_list.dart | 18 +- .../screens/wonder_events/wonder_events.dart | 1 + pubspec.lock | 16 +- 11 files changed, 467 insertions(+), 405 deletions(-) create mode 100644 lib/ui/common/pop_router_on_over_scroll.dart diff --git a/lib/ui/common/controls/app_page_indicator.dart b/lib/ui/common/controls/app_page_indicator.dart index b0167482..cb9b337d 100644 --- a/lib/ui/common/controls/app_page_indicator.dart +++ b/lib/ui/common/controls/app_page_indicator.dart @@ -3,15 +3,15 @@ import 'package:wonders/common_libs.dart'; import 'package:wonders/logic/common/string_utils.dart'; class AppPageIndicator extends StatefulWidget { - AppPageIndicator( - {Key? key, - required this.count, - required this.controller, - this.onDotPressed, - this.color, - this.dotSize, - String? semanticPageTitle}) - : semanticPageTitle = semanticPageTitle ?? $strings.appPageDefaultTitlePage, + AppPageIndicator({ + Key? key, + required this.count, + required this.controller, + this.onDotPressed, + this.color, + this.dotSize, + String? semanticPageTitle, + }) : semanticPageTitle = semanticPageTitle ?? $strings.appPageDefaultTitlePage, super(key: key); final int count; final PageController controller; @@ -33,9 +33,11 @@ class _AppPageIndicatorState extends State { widget.controller.addListener(_handlePageChanged); } - int get _controllerPage => widget.controller.page?.round() ?? 0; + int get _controllerPage => _currentPage.value; - void _handlePageChanged() => _currentPage.value = _controllerPage; + void _handlePageChanged() { + _currentPage.value = widget.controller.page!.round(); + } @override Widget build(BuildContext context) { @@ -48,12 +50,12 @@ class _AppPageIndicatorState extends State { valueListenable: _currentPage, builder: (_, value, child) { return Semantics( - container: true, liveRegion: true, + focusable: false, readOnly: true, label: StringUtils.supplant($strings.appPageSemanticSwipe, { '{pageTitle}': widget.semanticPageTitle, - '{count}': (value % (widget.count) + 1).toString(), + '{count}': (_controllerPage % (widget.count) + 1).toString(), '{total}': widget.count.toString(), }), child: Container()); diff --git a/lib/ui/common/pop_router_on_over_scroll.dart b/lib/ui/common/pop_router_on_over_scroll.dart new file mode 100644 index 00000000..6830b7f3 --- /dev/null +++ b/lib/ui/common/pop_router_on_over_scroll.dart @@ -0,0 +1,50 @@ +import 'package:wonders/common_libs.dart'; + +class PopRouterOnOverScroll extends StatefulWidget { + const PopRouterOnOverScroll({Key? key, required this.child, required this.controller}) : super(key: key); + final ScrollController controller; + final Widget child; + + @override + State createState() => _PopRouterOnOverScrollState(); +} + +class _PopRouterOnOverScrollState extends State { + final _scrollToPopThreshold = 70; + bool _isPointerDown = false; + + @override + void initState() { + super.initState(); + widget.controller.addListener(_handleScrollChanged); + } + + @override + void didUpdateWidget(covariant PopRouterOnOverScroll oldWidget) { + if (widget.controller != oldWidget.controller) { + widget.controller.addListener(_handleScrollChanged); + } + super.didUpdateWidget(oldWidget); + } + + bool _checkPointerIsDown(d) => _isPointerDown = d.dragDetails != null; + + @override + Widget build(BuildContext context) { + return NotificationListener( + onNotification: _checkPointerIsDown, + child: widget.child, + ); + } + + void _handleScrollChanged() { + // If user pulls far down on the elastic list, pop back to + final px = widget.controller.position.pixels; + if (px < -_scrollToPopThreshold) { + if (_isPointerDown) { + context.pop(); + widget.controller.removeListener(_handleScrollChanged); + } + } + } +} 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 4f5cb9ee..bde57027 100644 --- a/lib/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart +++ b/lib/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart @@ -6,6 +6,7 @@ import 'package:wonders/logic/data/highlight_data.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'; + part 'widgets/_blurred_image_bg.dart'; part 'widgets/_carousel_item.dart'; @@ -25,17 +26,25 @@ class _ArtifactScreenState extends State { // 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; } - final _currentIndex = ValueNotifier(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(); @@ -48,14 +57,14 @@ class _ArtifactScreenState extends State { )..addListener(_handleCarouselScroll); } - void _handleCarouselScroll() => _currentIndex.value = _currentOffset.round() % _artifacts.length; - @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) { @@ -76,152 +85,156 @@ class _ArtifactScreenState extends State { @override Widget build(BuildContext context) { - return Container( - color: $styles.colors.greyStrong, - child: AnimatedBuilder( - animation: _currentIndex, - builder: (context, __) => _buildScreen(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 _buildScreen(BuildContext context) { - final double w = context.widthPx; - final double backdropWidth = w <= _maxElementWidth ? w : min(w * _partialElementWidth, _maxElementWidth); - final double backdropHeight = math.min(context.heightPx * 0.65, _maxElementHeight); - final bool small = backdropHeight / _maxElementHeight < 0.7; - - return Stack( - children: [ - Positioned.fill( - child: _BlurredImageBg(url: _currentArtifact.imageUrl), - ), - Column( - children: [ - SimpleHeader($strings.artifactsTitleArtifacts, showBackBtn: false, isTransparent: true), - Gap($styles.insets.xs), - Expanded( - child: Stack(children: [ - // White arch, covering bottom half: - BottomCenter( - child: Container( - width: backdropWidth, - height: backdropHeight, - decoration: BoxDecoration( - color: $styles.colors.offWhite.withOpacity(0.8), - borderRadius: BorderRadius.vertical(top: Radius.circular(999)), - ), - ), - ), - - // Carousel: - Center( - child: SizedBox( - width: backdropWidth, - child: PageView.builder( - controller: _controller, - clipBehavior: Clip.none, - itemBuilder: (context, index) => AnimatedBuilder( - animation: _controller, - builder: (_, __) { - bool isCurrentIndex = index % _artifacts.length == _currentIndex.value; - return ExcludeSemantics( - excluding: !isCurrentIndex, - child: _CarouselItem( - index: index, - currentPage: _currentOffset, - artifact: _artifacts[index % _artifacts.length], - bottomPadding: backdropHeight, - maxWidth: backdropWidth, - maxHeight: backdropHeight, - onPressed: () => _handleArtifactTap(index), - ), - ); - }, - ), - ), - ), - ), - - // Text content - 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), - _buildContent(context, backdropWidth, small), - Gap(small ? $styles.insets.sm : $styles.insets.md), - AppBtn.from( - text: $strings.artifactsButtonBrowse, - icon: Icons.search, - expand: true, - onPressed: _handleSearchTap, - ), - Gap(small ? $styles.insets.md : $styles.insets.lg), - ], - ), - ), - ), - ]), - ), - ], - ), - ], - ); - } - - Widget _buildContent(BuildContext context, double width, bool small) { - return Semantics( - onIncrease: () => _handleArtifactTap(_currentOffset.round() + 1), - onDecrease: () => _handleArtifactTap(_currentOffset.round() - 1), - liveRegion: true, + Widget _buildBottomTextContent() { + return BottomCenter( child: Container( - width: width, - padding: EdgeInsets.symmetric(horizontal: $styles.insets.sm), + width: _backdropWidth, + padding: EdgeInsets.symmetric(horizontal: $styles.insets.md), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - IgnorePointer( - ignoringSemantics: false, + Gap($styles.insets.md), + Container( + width: _backdropWidth, + padding: EdgeInsets.symmetric(horizontal: $styles.insets.sm), child: Column( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, 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, - ), + 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(), ), ), - if (!small) Gap($styles.insets.xxs), - Text( - _currentArtifact.date.isEmpty ? '--' : _currentArtifact.date, - style: $styles.text.body, - textAlign: TextAlign.center, + Gap(_small ? $styles.insets.xs : $styles.insets.sm), + AppPageIndicator( + count: _artifacts.length, + controller: _controller, + semanticPageTitle: $strings.artifactsSemanticArtifact, ), ], - ).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: Icons.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) => AnimatedBuilder( + animation: _controller, + builder: (_, __) { + return _CarouselItem( + index: index, + currentPage: _currentOffset, + artifact: _artifacts[index % _artifacts.length], + bottomPadding: _backdropHeight, + maxWidth: _backdropWidth, + maxHeight: _backdropHeight, + onPressed: () => _handleArtifactTap(index), + ); + }, + ), + ), + ), + ), + ); + } } diff --git a/lib/ui/screens/artifact/artifact_details/artifact_details_screen.dart b/lib/ui/screens/artifact/artifact_details/artifact_details_screen.dart index 7de55b15..7682a1c3 100644 --- a/lib/ui/screens/artifact/artifact_details/artifact_details_screen.dart +++ b/lib/ui/screens/artifact/artifact_details/artifact_details_screen.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:wonders/common_libs.dart'; import 'package:wonders/logic/common/string_utils.dart'; import 'package:wonders/logic/data/artifact_data.dart'; @@ -7,8 +6,8 @@ import 'package:wonders/ui/common/controls/app_loading_indicator.dart'; import 'package:wonders/ui/common/gradient_container.dart'; import 'package:wonders/ui/common/modals/fullscreen_url_img_viewer.dart'; -part 'widgets/_header.dart'; part 'widgets/_content.dart'; +part 'widgets/_header.dart'; class ArtifactDetailsScreen extends StatefulWidget { const ArtifactDetailsScreen({Key? key, required this.artifactId}) : super(key: key); @@ -35,13 +34,21 @@ class _ArtifactDetailsScreenState extends State { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - Center(child: Icon(Icons.warning_amber_outlined, color: $styles.colors.accent1, size: $styles.insets.lg,)), + Center( + child: Icon( + Icons.warning_amber_outlined, + color: $styles.colors.accent1, + size: $styles.insets.lg, + )), Gap($styles.insets.xs), - SizedBox(width: $styles.insets.xxl * 3, child: Text( - StringUtils.supplant($strings.artifactDetailsErrorNotFound, {'{artifactId}': widget.artifactId}), - style: $styles.text.body.copyWith(color: $styles.colors.offWhite), - textAlign: TextAlign.center, - ),), + SizedBox( + width: $styles.insets.xxl * 3, + child: Text( + StringUtils.supplant($strings.artifactDetailsErrorNotFound, {'{artifactId}': widget.artifactId}), + style: $styles.text.body.copyWith(color: $styles.colors.offWhite), + textAlign: TextAlign.center, + ), + ), ], ).animate().fadeIn(); } else if (!snapshot.hasData) { diff --git a/lib/ui/screens/editorial/editorial_screen.dart b/lib/ui/screens/editorial/editorial_screen.dart index c86f5451..d3f3a717 100644 --- a/lib/ui/screens/editorial/editorial_screen.dart +++ b/lib/ui/screens/editorial/editorial_screen.dart @@ -14,6 +14,7 @@ import 'package:wonders/ui/common/curved_clippers.dart'; import 'package:wonders/ui/common/google_maps_marker.dart'; import 'package:wonders/ui/common/gradient_container.dart'; import 'package:wonders/ui/common/hidden_collectible.dart'; +import 'package:wonders/ui/common/pop_router_on_over_scroll.dart'; import 'package:wonders/ui/common/scaling_list_item.dart'; import 'package:wonders/ui/common/static_text_scale.dart'; import 'package:wonders/ui/common/themed_text.dart'; @@ -47,8 +48,6 @@ class _WonderEditorialScreenState extends State { late final ScrollController _scroller = ScrollController()..addListener(_handleScrollChanged); final _scrollPos = ValueNotifier(0.0); final _sectionIndex = ValueNotifier(0); - final _scrollToPopThreshold = 50; - bool _isPointerDown = false; @override void dispose() { @@ -60,17 +59,8 @@ class _WonderEditorialScreenState extends State { void _handleScrollChanged() { _scrollPos.value = _scroller.position.pixels; widget.onScroll.call(_scrollPos.value); - // If user pulls far down on the elastic list, pop back to - if (_scrollPos.value < -_scrollToPopThreshold) { - if (_isPointerDown) { - context.pop(); - _scroller.removeListener(_handleScrollChanged); - } - } } - bool _checkPointerIsDown(d) => _isPointerDown = d.dragDetails != null; - @override Widget build(BuildContext context) { return LayoutBuilder(builder: (_, constraints) { @@ -79,8 +69,8 @@ class _WonderEditorialScreenState extends State { double minAppBarHeight = shortMode ? 80 : 120; double maxAppBarHeight = shortMode ? 400 : 500; - return NotificationListener( - onNotification: _checkPointerIsDown, + return PopRouterOnOverScroll( + controller: _scroller, child: ColoredBox( color: $styles.colors.offWhite, child: Stack( diff --git a/lib/ui/screens/home/_vertical_swipe_controller.dart b/lib/ui/screens/home/_vertical_swipe_controller.dart index 7dc405ae..e080f0b8 100644 --- a/lib/ui/screens/home/_vertical_swipe_controller.dart +++ b/lib/ui/screens/home/_vertical_swipe_controller.dart @@ -22,18 +22,16 @@ class _VerticalSwipeController { void handleVerticalSwipeUpdate(DragUpdateDetails details) { if (swipeReleaseAnim.isAnimating) swipeReleaseAnim.stop(); - if (details.delta.dy > 0) { - swipeAmt.value = 0; - } else { - isPointerDown.value = true; - double value = (swipeAmt.value - details.delta.dy / _pullToViewDetailsThreshold).clamp(0, 1); - if (value != swipeAmt.value) { - swipeAmt.value = value; - if (swipeAmt.value == 1) { - onSwipeComplete(); - } + + isPointerDown.value = true; + double value = (swipeAmt.value - details.delta.dy / _pullToViewDetailsThreshold).clamp(0, 1); + if (value != swipeAmt.value) { + swipeAmt.value = value; + if (swipeAmt.value == 1) { + onSwipeComplete(); } } + //print(_swipeUpAmt.value); } diff --git a/lib/ui/screens/home/widgets/_animated_arrow_button.dart b/lib/ui/screens/home/widgets/_animated_arrow_button.dart index 95e86309..54de2f42 100644 --- a/lib/ui/screens/home/widgets/_animated_arrow_button.dart +++ b/lib/ui/screens/home/widgets/_animated_arrow_button.dart @@ -28,7 +28,7 @@ class _AnimatedArrowButton extends StatelessWidget { onPressed: onTap, child: SizedBox( height: 80, - width: 250, + width: 50, child: Animate( effects: [ CustomEffect(builder: _buildOpacityTween, duration: duration, curve: Curves.easeOut), diff --git a/lib/ui/screens/home/wonders_home_screen.dart b/lib/ui/screens/home/wonders_home_screen.dart index 41c8843d..bfde3876 100644 --- a/lib/ui/screens/home/wonders_home_screen.dart +++ b/lib/ui/screens/home/wonders_home_screen.dart @@ -1,4 +1,3 @@ -import 'package:flutter/rendering.dart'; import 'package:wonders/common_libs.dart'; import 'package:wonders/logic/common/string_utils.dart'; import 'package:wonders/logic/data/wonder_data.dart'; @@ -94,12 +93,16 @@ class _HomeScreenState extends State with SingleTickerProviderStateM } void _startDelayedFgFade() async { - for (var a in _fadeAnims) { - a.value = 0; - } - await Future.delayed(300.ms); - for (var a in _fadeAnims) { - a.forward(); + try { + for (var a in _fadeAnims) { + a.value = 0; + } + await Future.delayed(300.ms); + for (var a in _fadeAnims) { + a.forward(); + } + } on Exception catch (e) { + debugPrint(e.toString()); } } @@ -109,221 +112,231 @@ class _HomeScreenState extends State with SingleTickerProviderStateM _startDelayedFgFade(); _fadeInOnNextBuild = false; } - return _swipeController.wrapGestureDetector( - Container( - color: $styles.colors.black, - child: Stack( - children: [ - /// Background - ..._buildBgChildren(), - /// Clouds - FractionallySizedBox( - widthFactor: 1, - heightFactor: .5, - child: AnimatedClouds(wonderType: currentWonder.type, opacity: 1), - ), + return _swipeController.wrapGestureDetector(Container( + color: $styles.colors.black, + child: Stack( + children: [ + Stack( + children: [ + /// Background + ..._buildBgAndClouds(), - /// Wonders Illustrations - MergeSemantics( - child: Semantics( - liveRegion: true, - onIncrease: () => _setPageIndex(_wonderIndex + 1), - onDecrease: () => _setPageIndex(_wonderIndex - 1), - onTap: () => _showDetailsPage(), - child: PageView.builder( - controller: _pageController, - onPageChanged: _handlePageViewChanged, - itemBuilder: _buildMgChild, - ), - ), - ), + /// Wonders Illustrations (main content) + _buildMgPageView(), - Stack(children: [ - /// Foreground gradient-1, gets darker when swiping up - BottomCenter( - child: _buildSwipeableBgGradient(currentWonder.type.bgColor.withOpacity(.65)), - ), + /// Foreground illustrations and gradients + _buildFgAndGradients(), - /// Foreground decorators - ..._buildFgChildren(), + /// Controls that float on top of the various illustrations + _buildFloatingUi(), + ], + ).animate().fadeIn(), + ], + ), + )); + } - /// Foreground gradient-2, gets darker when swiping up - BottomCenter( - child: _buildSwipeableBgGradient(currentWonder.type.bgColor.withOpacity(1)), - ), - - /// Floating controls / UI - AnimatedSwitcher( - duration: $styles.times.fast, - child: RepaintBoundary( - key: ObjectKey(currentWonder), - child: OverflowBox( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(width: double.infinity), - const Spacer(), - - /// Title Content - LightText( - child: IgnorePointer( - ignoringSemantics: false, - child: Transform.translate( - offset: Offset(0, 30), - child: Column( - children: [ - ExcludeSemantics( - // Hide the title when the menu is open for visual polish - child: AnimatedOpacity( - opacity: _isMenuOpen ? 0 : 1, - duration: $styles.times.fast, - child: WonderTitleText(currentWonder, enableShadows: true), - ), - ), - Gap($styles.insets.md), - AppPageIndicator( - count: _numWonders, - controller: _pageController, - color: $styles.colors.white, - dotSize: 8, - onDotPressed: _handlePageIndicatorDotPressed, - semanticPageTitle: $strings.homeSemanticWonder, - ), - Gap($styles.insets.md), - ], - ), - ), - ), - ), - - /// Animated arrow and background - /// Wrap in a container that is full-width to make it easier to find for screen readers - Container( - width: double.infinity, - alignment: Alignment.center, - - /// Lose state of child objects when index changes, this will re-run all the animated switcher and the arrow anim - key: ValueKey(_wonderIndex), - child: Stack( - children: [ - /// Expanding rounded rect that grows in height as user swipes up - Positioned.fill( - child: _buildSwipeableArrowBg(), - ), - - /// Arrow Btn that fades in and out - _AnimatedArrowButton(onTap: _showDetailsPage, semanticTitle: currentWonder.title), - ], - ), - ), - Gap($styles.insets.md), - ], - ), - ), - ), - ), - - /// Menu Btn - TopLeft( - child: AnimatedOpacity( - duration: $styles.times.fast, - opacity: _isMenuOpen ? 0 : 1, - child: MergeSemantics( - child: CircleIconBtn( - icon: AppIcons.menu, - onPressed: _handleOpenMenuPressed, - semanticLabel: $strings.homeSemanticOpenMain, - ).safe(), - ), - ), - ), - ]), - ], - ).animate().fadeIn(), + Widget _buildMgPageView() { + return ExcludeSemantics( + child: PageView.builder( + controller: _pageController, + onPageChanged: _handlePageViewChanged, + itemBuilder: (_, index) { + final wonder = _wonders[index % _wonders.length]; + final wonderType = wonder.type; + bool isShowing = _isSelected(wonderType); + return _swipeController.buildListener( + builder: (swipeAmt, _, child) { + final config = WonderIllustrationConfig.mg( + isShowing: isShowing, + zoom: .05 * swipeAmt, + ); + return WonderIllustration(wonderType, config: config); + }, + ); + }, ), ); } - Widget _buildMgChild(_, index) { - final wonder = _wonders[index % _wonders.length]; - final wonderType = wonder.type; - bool isShowing = _isSelected(wonderType); - return _swipeController.buildListener(builder: (swipeAmt, _, child) { - final config = WonderIllustrationConfig.mg( - isShowing: isShowing, - zoom: .05 * swipeAmt, - ); - return ExcludeSemantics( - excluding: !isShowing, - child: Semantics( - label: wonder.title, - child: WonderIllustration(wonderType, config: config), - ), - ); - }); + List _buildBgAndClouds() { + return [ + // Background + ..._wonders.map((e) { + final config = WonderIllustrationConfig.bg(isShowing: _isSelected(e.type)); + return WonderIllustration(e.type, config: config); + }).toList(), + // Clouds + FractionallySizedBox( + widthFactor: 1, + heightFactor: .5, + child: AnimatedClouds(wonderType: currentWonder.type, opacity: 1), + ) + ]; } - List _buildBgChildren() { - return _wonders.map((e) { - final config = WonderIllustrationConfig.bg(isShowing: _isSelected(e.type)); - return WonderIllustration(e.type, config: config); - }).toList(); - } - - List _buildFgChildren() { - return _wonders.map((e) { - return _swipeController.buildListener(builder: (swipeAmt, _, child) { - final config = WonderIllustrationConfig.fg( - isShowing: _isSelected(e.type), - zoom: .4 * (_swipeOverride ?? swipeAmt), - ); - return Animate( - effects: const [FadeEffect()], - onPlay: _handleFadeAnimInit, - child: IgnorePointer(child: WonderIllustration(e.type, config: config))); - }); - }).toList(); - } - - Widget _buildSwipeableArrowBg() { - return _swipeController.buildListener( - builder: (swipeAmt, _, child) { - double heightFactor = .5 + .5 * (1 + swipeAmt * 4); - return FractionallySizedBox( - alignment: Alignment.bottomCenter, - heightFactor: heightFactor, - child: Opacity(opacity: swipeAmt * .5, child: child), - ); - }, - child: VtGradient( - [$styles.colors.white.withOpacity(0), $styles.colors.white.withOpacity(1)], - const [.3, 1], - borderRadius: BorderRadius.circular(99), - ), - ); - } - - Widget _buildSwipeableBgGradient(Color fgColor) { - return _swipeController.buildListener(builder: (swipeAmt, isPointerDown, _) { - return IgnorePointer( - child: FractionallySizedBox( - heightFactor: .6, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - fgColor.withOpacity(0), - fgColor.withOpacity(.5 + fgColor.opacity * .25 + (isPointerDown ? .05 : 0) + swipeAmt * .20), - ], - stops: const [0, 1], + Widget _buildFgAndGradients() { + Widget buildSwipeableBgGradient(Color fgColor) { + return _swipeController.buildListener(builder: (swipeAmt, isPointerDown, _) { + return IgnorePointer( + child: FractionallySizedBox( + heightFactor: .6, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + fgColor.withOpacity(0), + fgColor.withOpacity(.5 + fgColor.opacity * .25 + (isPointerDown ? .05 : 0) + swipeAmt * .20), + ], + stops: const [0, 1], + ), ), ), ), + ); + }); + } + + final gradientColor = currentWonder.type.bgColor; + return Stack(children: [ + /// Foreground gradient-1, gets darker when swiping up + BottomCenter( + child: buildSwipeableBgGradient(gradientColor.withOpacity(.65)), + ), + + /// Foreground decorators + ..._wonders.map((e) { + return _swipeController.buildListener(builder: (swipeAmt, _, child) { + final config = WonderIllustrationConfig.fg( + isShowing: _isSelected(e.type), + zoom: .4 * (_swipeOverride ?? swipeAmt), + ); + return Animate( + effects: const [FadeEffect()], + onPlay: _handleFadeAnimInit, + child: IgnorePointer(child: WonderIllustration(e.type, config: config))); + }); + }).toList(), + + /// Foreground gradient-2, gets darker when swiping up + BottomCenter( + child: buildSwipeableBgGradient(gradientColor), + ), + ]); + } + + Widget _buildFloatingUi() { + return Stack(children: [ + /// Floating controls / UI + AnimatedSwitcher( + duration: $styles.times.fast, + child: RepaintBoundary( + child: OverflowBox( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: double.infinity), + const Spacer(), + + /// Title Content + LightText( + child: IgnorePointer( + ignoringSemantics: false, + child: Transform.translate( + offset: Offset(0, 30), + child: Column( + children: [ + Semantics( + liveRegion: true, + button: true, + header: true, + onIncrease: () => _setPageIndex(_wonderIndex + 1), + onDecrease: () => _setPageIndex(_wonderIndex - 1), + onTap: () => _showDetailsPage(), + // Hide the title when the menu is open for visual polish + child: AnimatedOpacity( + opacity: _isMenuOpen ? 0 : 1, + duration: $styles.times.fast, + child: WonderTitleText(currentWonder, enableShadows: true), + ), + ), + Gap($styles.insets.md), + Semantics( + onIncrease: () => _setPageIndex(_wonderIndex + 1), + onDecrease: () => _setPageIndex(_wonderIndex - 1), + child: AppPageIndicator( + count: _numWonders, + controller: _pageController, + color: $styles.colors.white, + dotSize: 8, + onDotPressed: _handlePageIndicatorDotPressed, + semanticPageTitle: $strings.homeSemanticWonder, + ), + ), + Gap($styles.insets.md), + ], + ), + ), + ), + ), + + /// Animated arrow and background + /// Wrap in a container that is full-width to make it easier to find for screen readers + Container( + width: double.infinity, + alignment: Alignment.center, + + /// Lose state of child objects when index changes, this will re-run all the animated switcher and the arrow anim + key: ValueKey(_wonderIndex), + child: Stack( + children: [ + /// Expanding rounded rect that grows in height as user swipes up + Positioned.fill( + child: _swipeController.buildListener( + builder: (swipeAmt, _, child) { + double heightFactor = .5 + .5 * (1 + swipeAmt * 4); + return FractionallySizedBox( + alignment: Alignment.bottomCenter, + heightFactor: heightFactor, + child: Opacity(opacity: swipeAmt * .5, child: child), + ); + }, + child: VtGradient( + [$styles.colors.white.withOpacity(0), $styles.colors.white.withOpacity(1)], + const [.3, 1], + borderRadius: BorderRadius.circular(99), + ), + )), + + /// Arrow Btn that fades in and out + _AnimatedArrowButton(onTap: _showDetailsPage, semanticTitle: currentWonder.title), + ], + ), + ), + Gap($styles.insets.md), + ], + ), + ), ), - ); - }); + ), + + /// Menu Btn + TopLeft( + child: AnimatedOpacity( + duration: $styles.times.fast, + opacity: _isMenuOpen ? 0 : 1, + child: MergeSemantics( + child: CircleIconBtn( + icon: AppIcons.menu, + onPressed: _handleOpenMenuPressed, + semanticLabel: $strings.homeSemanticOpenMain, + ).safe(), + ), + ), + ), + ]); } } diff --git a/lib/ui/screens/wonder_events/widgets/_events_list.dart b/lib/ui/screens/wonder_events/widgets/_events_list.dart index d2edeb20..5af97acf 100644 --- a/lib/ui/screens/wonder_events/widgets/_events_list.dart +++ b/lib/ui/screens/wonder_events/widgets/_events_list.dart @@ -9,31 +9,19 @@ class _EventsList extends StatefulWidget { } class _EventsListState extends State<_EventsList> { - late final ScrollController _scroller = ScrollController()..addListener(_handleScrollChanged); - bool _hasPopped = false; - bool _isPointerDown = false; + final ScrollController _scroller = ScrollController(); @override void dispose() { _scroller.dispose(); super.dispose(); } - void _handleScrollChanged() { - if (!_isPointerDown) return; - if (_scroller.position.pixels < -100 && !_hasPopped) { - _hasPopped = true; - context.pop(); - } - } - - bool _checkPointerIsDown(d) => _isPointerDown = d.dragDetails != null; - void _handleGlobalTimelinePressed() => context.push(ScreenPaths.timeline(widget.data.type)); @override Widget build(BuildContext context) { - return NotificationListener( - onNotification: _checkPointerIsDown, + return PopRouterOnOverScroll( + controller: _scroller, child: LayoutBuilder(builder: (_, constraints) { return Stack( children: [ diff --git a/lib/ui/screens/wonder_events/wonder_events.dart b/lib/ui/screens/wonder_events/wonder_events.dart index 192d0343..a899f2e4 100644 --- a/lib/ui/screens/wonder_events/wonder_events.dart +++ b/lib/ui/screens/wonder_events/wonder_events.dart @@ -6,6 +6,7 @@ import 'package:wonders/ui/common/compass_divider.dart'; import 'package:wonders/ui/common/curved_clippers.dart'; import 'package:wonders/ui/common/hidden_collectible.dart'; import 'package:wonders/ui/common/list_gradient.dart'; +import 'package:wonders/ui/common/pop_router_on_over_scroll.dart'; import 'package:wonders/ui/common/themed_text.dart'; import 'package:wonders/ui/common/timeline_event_card.dart'; import 'package:wonders/ui/common/wonders_timeline_builder.dart'; diff --git a/pubspec.lock b/pubspec.lock index 41a4e88b..90158145 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -35,7 +35,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "3.3.1" args: dependency: transitive description: @@ -238,7 +238,7 @@ packages: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.1.2" + version: "6.1.4" fixnum: dependency: transitive description: @@ -588,7 +588,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: @@ -978,7 +978,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.9.1" stack_trace: dependency: transitive description: @@ -1027,7 +1027,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.12" + version: "0.4.13" timing: dependency: transitive description: @@ -1125,14 +1125,14 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" vm_service: dependency: transitive description: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "9.0.0" + version: "9.3.0" watcher: dependency: transitive description: @@ -1190,5 +1190,5 @@ packages: source: hosted version: "2.3.0" sdks: - dart: ">=2.17.1 <3.0.0" + dart: ">=2.18.0-146.0.dev <3.0.0" flutter: ">=3.0.0"