diff --git a/lib/ui/common/controls/app_page_indicator.dart b/lib/ui/common/controls/app_page_indicator.dart index ba5a19f2..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,11 +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/controls/buttons.dart b/lib/ui/common/controls/buttons.dart index 2cfd61eb..1b071d08 100644 --- a/lib/ui/common/controls/buttons.dart +++ b/lib/ui/common/controls/buttons.dart @@ -138,19 +138,14 @@ class AppBtn extends StatelessWidget { // add press effect: if (pressEffect) button = _ButtonPressEffect(button); - // add semantics: - if (semanticLabel.isEmpty) { - button = ExcludeSemantics(child: button); - } else { - button = Semantics( - label: semanticLabel, - button: true, - container: true, - child: button, - ); - } - - return button; + // add semantics? + if (semanticLabel.isEmpty) return button; + return Semantics( + label: semanticLabel, + button: true, + container: true, + child: ExcludeSemantics(child: button), + ); } } 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 1bdc5426..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,14 +26,24 @@ class _ArtifactScreenState extends State { // Locally store loaded artifacts. late List _artifacts; + late PageController _controller; + final _currentIndex = ValueNotifier(0); double get _currentOffset { - bool inited = _controller.hasClients && _controller.position.haveDimensions; - return inited ? _controller.page! : _controller.initialPage * 1.0; + bool hasOffset = _controller.hasClients && _controller.position.haveDimensions; + return hasOffset ? _controller.page! : _controller.initialPage * 1.0; } - int get _currentIndex => _currentOffset.round() % _artifacts.length; + 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() { @@ -43,7 +54,7 @@ class _ArtifactScreenState extends State { // start at a high offset so we can scroll backwards: initialPage: _artifacts.length * 9999, viewportFraction: 0.5, - ); + )..addListener(_handleCarouselScroll); } @override @@ -52,6 +63,8 @@ class _ArtifactScreenState extends State { super.dispose(); } + void _handleCarouselScroll() => _currentIndex.value = _currentOffset.round() % _artifacts.length; + void _handleArtifactTap(int index) { int delta = index - _currentOffset.round(); if (delta == 0) { @@ -72,146 +85,155 @@ class _ArtifactScreenState extends State { @override Widget build(BuildContext context) { - return Container( - color: $styles.colors.greyStrong, - child: AnimatedBuilder(animation: _controller, builder: _buildScreen), + 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 _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; - final HighlightData artifact = _artifacts[_currentIndex]; + 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)), + ), + ), + ); + } - return Stack( - children: [ - Positioned.fill(child: _BlurredImageBg(url: artifact.imageUrl)), - Column( + Widget _buildBottomTextContent() { + return BottomCenter( + child: Container( + width: _backdropWidth, + padding: EdgeInsets.symmetric(horizontal: $styles.insets.md), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, 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) { - bool isCurrentIndex = index % _artifacts.length == _currentIndex; - return ExcludeSemantics( - excluding: isCurrentIndex == false, - child: MergeSemantics( - child: Semantics( - onIncrease: () => _handleArtifactTap(_currentIndex + 1), - onDecrease: () => _handleArtifactTap(_currentIndex - 1), - child: _CarouselItem( - index: index, - currentPage: _currentOffset, - artifact: _artifacts[index % _artifacts.length], - bottomPadding: backdropHeight, - maxWidth: backdropWidth, - maxHeight: backdropHeight, - onPressed: () => _handleArtifactTap(index), + 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(), ), ), - ), - - // 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, artifact, 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), - ], - ), + 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 _buildContent(BuildContext context, HighlightData artifact, double width, bool small) { - return Container( - width: width, - padding: EdgeInsets.symmetric(horizontal: $styles.insets.sm), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IgnorePointer( - child: ExcludeSemantics( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: small ? 90 : 110, - alignment: Alignment.center, - child: StaticTextScale( - child: Text( - artifact.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( - artifact.date.isEmpty ? '--' : artifact.date, - style: $styles.text.body, - textAlign: TextAlign.center, - ), - ], - ).animate(key: ValueKey(artifact.artifactId)).fadeIn(), + 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), + ); + }, ), ), - Gap(small ? $styles.insets.xs : $styles.insets.sm), - AppPageIndicator( - count: _artifacts.length, - controller: _controller, - semanticPageTitle: $strings.artifactsSemanticArtifact, - ), - ], + ), ), ); } diff --git a/lib/ui/screens/artifact/artifact_carousel/widgets/_carousel_item.dart b/lib/ui/screens/artifact/artifact_carousel/widgets/_carousel_item.dart index 35e5e999..0241bde7 100644 --- a/lib/ui/screens/artifact/artifact_carousel/widgets/_carousel_item.dart +++ b/lib/ui/screens/artifact/artifact_carousel/widgets/_carousel_item.dart @@ -22,7 +22,7 @@ class _CarouselItem extends StatelessWidget { @override Widget build(BuildContext context) { return AppBtn.basic( - semanticLabel: '${artifact.title} ${artifact.date}', + semanticLabel: 'Explore artifact details', onPressed: onPressed, pressEffect: false, child: _ImagePreview( 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 d7d5dcf5..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( @@ -116,7 +106,7 @@ class _WonderEditorialScreenState extends State { CustomScrollView( primary: false, controller: _scroller, - cacheExtent: 500, + cacheExtent: 1000, slivers: [ /// Invisible padding at the top of the list, so the illustration shows through the btm SliverToBoxAdapter( diff --git a/lib/ui/screens/editorial/widgets/_scrolling_content.dart b/lib/ui/screens/editorial/widgets/_scrolling_content.dart index de320a02..92e6cf69 100644 --- a/lib/ui/screens/editorial/widgets/_scrolling_content.dart +++ b/lib/ui/screens/editorial/widgets/_scrolling_content.dart @@ -28,35 +28,37 @@ class _ScrollingContent extends StatelessWidget { final String dropChar = value.substring(0, 1); final textScale = MediaQuery.of(context).textScaleFactor; final double dropCapWidth = StringUtils.measure(dropChar, dropStyle).width * textScale; - final bool skipCaps = !localeLogic.isEnglish || MediaQuery.of(context).accessibleNavigation; + final bool skipCaps = !localeLogic.isEnglish; return Semantics( label: value, - child: !skipCaps - ? DropCapText( - _fixNewlines(value).substring(1), - dropCap: DropCap( - width: dropCapWidth, - height: $styles.text.body.fontSize! * $styles.text.body.height! * 2, - child: Transform.translate( - offset: Offset(0, bodyStyle.fontSize! * (bodyStyle.height! - 1) - 2), - child: Text( - dropChar, - overflow: TextOverflow.visible, - style: $styles.text.dropCase.copyWith( - color: $styles.colors.accent1, - height: 1, + child: ExcludeSemantics( + child: !skipCaps + ? DropCapText( + _fixNewlines(value).substring(1), + dropCap: DropCap( + width: dropCapWidth, + height: $styles.text.body.fontSize! * $styles.text.body.height! * 2, + child: Transform.translate( + offset: Offset(0, bodyStyle.fontSize! * (bodyStyle.height! - 1) - 2), + child: Text( + dropChar, + overflow: TextOverflow.visible, + style: $styles.text.dropCase.copyWith( + color: $styles.colors.accent1, + height: 1, + ), ), ), ), - ), - style: $styles.text.body, - dropCapPadding: EdgeInsets.only(right: 6), - dropCapStyle: $styles.text.dropCase.copyWith( - color: $styles.colors.accent1, - height: 1, - ), - ) - : Text(value, style: bodyStyle), + style: $styles.text.body, + dropCapPadding: EdgeInsets.only(right: 6), + dropCapStyle: $styles.text.dropCase.copyWith( + color: $styles.colors.accent1, + height: 1, + ), + ) + : Text(value, style: bodyStyle), + ), ); } diff --git a/lib/ui/screens/home/_vertical_swipe_controller.dart b/lib/ui/screens/home/_vertical_swipe_controller.dart index d513e8ac..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); } @@ -53,18 +51,14 @@ class _VerticalSwipeController { } /// Utility method to wrap a gesture detector and wire up the required handlers. - Widget wrapGestureDetector(Widget child, {Key? key}) => Semantics( - button: false, - child: GestureDetector( - key: key, - onTapDown: (_) { - handleTapDown(); - }, - onTapUp: (_) => handleTapCancelled(), - onVerticalDragUpdate: handleVerticalSwipeUpdate, - onVerticalDragEnd: (_) => handleVerticalSwipeCancelled(), - onVerticalDragCancel: handleVerticalSwipeCancelled, - behavior: HitTestBehavior.translucent, - child: child), - ); + Widget wrapGestureDetector(Widget child, {Key? key}) => GestureDetector( + key: key, + excludeFromSemantics: true, + onTapDown: (_) => handleTapDown(), + onTapUp: (_) => handleTapCancelled(), + onVerticalDragUpdate: handleVerticalSwipeUpdate, + onVerticalDragEnd: (_) => handleVerticalSwipeCancelled(), + onVerticalDragCancel: handleVerticalSwipeCancelled, + behavior: HitTestBehavior.translucent, + child: child); } diff --git a/lib/ui/screens/home/wonders_home_screen.dart b/lib/ui/screens/home/wonders_home_screen.dart index d78d8041..756c3ebc 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,227 +112,227 @@ 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( - header: true, - image: true, - liveRegion: true, - onIncrease: () => _setPageIndex(_wonderIndex + 1), - onDecrease: () => _setPageIndex(_wonderIndex - 1), - 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: MergeSemantics( - child: Transform.translate( - offset: Offset(0, 30), - child: Column( - children: [ - // Hide the title when the menu is open for visual polish - 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 - MergeSemantics( - child: 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: Semantics( - sortKey: OrdinalSortKey(0), - 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), + 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.yaml b/pubspec.yaml index 383a81ae..f6547b2b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: wonders description: Explore the famous wonders of the world. publish_to: "none" -version: 1.9.4 +version: 1.9.5 environment: sdk: ">=2.16.0 <3.0.0" diff --git a/release_notes.txt b/release_notes.txt index 25763d25..55414a07 100644 --- a/release_notes.txt +++ b/release_notes.txt @@ -1,3 +1,8 @@ +# 1.9.5 +- Improved a11y for Home Screen and Artifact Carousel +- Polished collection icons +- Added hero support to fullscreen Artifact Details + # 1.9.4 - Fix tappable arrow on home page - Reduce text size for collapsing pull quote