diff --git a/lib/common_libs.dart b/lib/common_libs.dart index 54f17731..55fc08ac 100644 --- a/lib/common_libs.dart +++ b/lib/common_libs.dart @@ -15,6 +15,7 @@ export 'package:provider/provider.dart'; export 'package:rnd/rnd.dart'; export 'package:simple_rich_text/simple_rich_text.dart'; export 'package:sized_context/sized_context.dart'; +export 'package:stateful_props/stateful_props.dart'; export 'package:wonders/assets.dart'; export 'package:wonders/logic/app_logic.dart'; export 'package:wonders/logic/data/wonder_type.dart'; diff --git a/lib/ui/common/modals/fullscreen_url_img_viewer.dart b/lib/ui/common/modals/fullscreen_url_img_viewer.dart index b1e1ea37..56effa2a 100644 --- a/lib/ui/common/modals/fullscreen_url_img_viewer.dart +++ b/lib/ui/common/modals/fullscreen_url_img_viewer.dart @@ -12,17 +12,11 @@ class FullscreenUrlImgViewer extends StatefulWidget { State createState() => _FullscreenUrlImgViewerState(); } -class _FullscreenUrlImgViewerState extends State { +class _FullscreenUrlImgViewerState extends State with StatefulPropsMixin { final _isZoomed = ValueNotifier(false); - late final _controller = PageController(initialPage: widget.index); + late final _pages = PageControllerProp(this, initialPage: widget.index); - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - void _handleBackPressed() => Navigator.pop(context, _controller.page!.round()); + void _handleBackPressed() => Navigator.pop(context, _pages.page.round()); @override Widget build(BuildContext context) { @@ -32,7 +26,7 @@ class _FullscreenUrlImgViewerState extends State { final bool enableSwipe = !_isZoomed.value && widget.urls.length > 1; return PageView.builder( physics: enableSwipe ? PageScrollPhysics() : NeverScrollableScrollPhysics(), - controller: _controller, + controller: _pages.controller, itemCount: widget.urls.length, itemBuilder: (_, index) => _Viewer(widget.urls[index], _isZoomed), onPageChanged: (_) => AppHaptics.lightImpact(), @@ -69,25 +63,19 @@ class _Viewer extends StatefulWidget { State<_Viewer> createState() => _ViewerState(); } -class _ViewerState extends State<_Viewer> with SingleTickerProviderStateMixin { - final _controller = TransformationController(); - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } +class _ViewerState extends State<_Viewer> with StatefulPropsMixin { + late final _transformation = TransformationControllerProp(this); /// Reset zoom level to 1 on double-tap - void _handleDoubleTap() => _controller.value = Matrix4.identity(); + void _handleDoubleTap() => _transformation.controller.value = Matrix4.identity(); @override Widget build(BuildContext context) { return GestureDetector( onDoubleTap: _handleDoubleTap, child: InteractiveViewer( - transformationController: _controller, - onInteractionEnd: (_) => widget.isZoomed.value = _controller.value.getMaxScaleOnAxis() > 1, + transformationController: _transformation.controller, + onInteractionEnd: (_) => widget.isZoomed.value = _transformation.matrix.getMaxScaleOnAxis() > 1, minScale: 1, maxScale: 5, child: Hero( 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 2d6525c7..901981d7 100644 --- a/lib/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart +++ b/lib/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart @@ -153,4 +153,10 @@ class _ArtifactScreenState extends State { ), ); } + + @override + void dispose() { + _pageController?.dispose(); + super.dispose(); + } } diff --git a/lib/ui/screens/editorial/editorial_screen.dart b/lib/ui/screens/editorial/editorial_screen.dart index be22eddd..926cfb30 100644 --- a/lib/ui/screens/editorial/editorial_screen.dart +++ b/lib/ui/screens/editorial/editorial_screen.dart @@ -37,10 +37,6 @@ part 'widgets/_sliding_image_stack.dart'; part 'widgets/_title_text.dart'; part 'widgets/_top_illustration.dart'; -//TODO: Try and maintain 1.5 : 1 aspect ratio on the featured image -//TODO: Try and move the scrollbar all the way to the edge of the screen -//TODO: Fix arch logic (if necessary) -// or maybe remove class WonderEditorialScreen extends StatefulWidget { const WonderEditorialScreen(this.data, {Key? key, required this.onScroll}) : super(key: key); final WonderData data; @@ -50,20 +46,14 @@ class WonderEditorialScreen extends StatefulWidget { State createState() => _WonderEditorialScreenState(); } -class _WonderEditorialScreenState extends State { - late final ScrollController _scroller = ScrollController()..addListener(_handleScrollChanged); - final _scrollPos = ValueNotifier(0.0); - final _sectionIndex = ValueNotifier(0); - - @override - void dispose() { - _scroller.dispose(); - super.dispose(); - } +class _WonderEditorialScreenState extends State with StatefulPropsMixin { + late final _scroll = ScrollControllerProp(this, onChange: _handleScrollChanged); + late final _scrollPos = ValueNotifier(0.0); + late final _sectionIndex = ValueNotifier(0); /// Various [ValueListenableBuilders] are mapped to the _scrollPos and will rebuild when it changes void _handleScrollChanged() { - _scrollPos.value = _scroller.position.pixels; + _scrollPos.value = _scroll.px; widget.onScroll.call(_scrollPos.value); } @@ -78,7 +68,7 @@ class _WonderEditorialScreenState extends State { double maxAppBarHeight = min(context.widthPx, $styles.sizes.maxContentWidth1) * 1.5; return PopRouterOnOverScroll( - controller: _scroller, + controller: _scroll.controller, child: ColoredBox( color: $styles.colors.offWhite, child: Stack( @@ -91,11 +81,11 @@ class _WonderEditorialScreenState extends State { /// Top Illustration - Sits underneath the scrolling content, fades out as it scrolls SizedBox( height: illustrationHeight, - child: ValueListenableBuilder( - valueListenable: _scrollPos, - builder: (_, value, child) { + child: ListenableBuilder( + listenable: _scrollPos, + builder: (_, child) { // get some value between 0 and 1, based on the amt scrolled - double opacity = (1 - value / 700).clamp(0, 1); + double opacity = (1 - _scrollPos.value / 700).clamp(0, 1); return Opacity(opacity: opacity, child: child); }, // This is due to a bug: https://github.com/flutter/flutter/issues/101872 @@ -109,7 +99,7 @@ class _WonderEditorialScreenState extends State { //width: $styles.sizes.maxContentWidth1, child: CustomScrollView( primary: false, - controller: _scroller, + controller: _scroll.controller, scrollBehavior: ScrollConfiguration.of(context).copyWith(), cacheExtent: 1000, slivers: [ @@ -120,17 +110,17 @@ class _WonderEditorialScreenState extends State { /// Text content, animates itself to hide behind the app bar as it scrolls up SliverToBoxAdapter( - child: ValueListenableBuilder( - valueListenable: _scrollPos, - builder: (_, value, child) { - double offsetAmt = max(0, value * .3); + child: ListenableBuilder( + listenable: _scrollPos, + builder: (_, child) { + double offsetAmt = max(0, _scrollPos.value * .3); double opacity = (1 - offsetAmt / 150).clamp(0, 1); return Transform.translate( offset: Offset(0, offsetAmt), child: Opacity(opacity: opacity, child: child), ); }, - child: _TitleText(widget.data, scroller: _scroller), + child: _TitleText(widget.data, scroller: _scroll.controller), ), ), @@ -166,7 +156,7 @@ class _WonderEditorialScreenState extends State { /// Home Btn ListenableBuilder( - listenable: _scroller, + listenable: _scroll.controller, builder: (_, child) { return AnimatedOpacity( opacity: _scrollPos.value > 0 ? 0 : 1, diff --git a/lib/ui/screens/editorial/widgets/_circular_title_bar.dart b/lib/ui/screens/editorial/widgets/_circular_title_bar.dart index 9ffca6a5..26a9652b 100644 --- a/lib/ui/screens/editorial/widgets/_circular_title_bar.dart +++ b/lib/ui/screens/editorial/widgets/_circular_title_bar.dart @@ -62,22 +62,13 @@ class _AnimatedCircleWithText extends StatefulWidget { State<_AnimatedCircleWithText> createState() => _AnimatedCircleWithTextState(); } -class _AnimatedCircleWithTextState extends State<_AnimatedCircleWithText> with SingleTickerProviderStateMixin { +class _AnimatedCircleWithTextState extends State<_AnimatedCircleWithText> with StatefulPropsMixin { int _prevIndex = -1; String get oldTitle => _prevIndex == -1 ? '' : widget.titles[_prevIndex]; String get newTitle => widget.titles[widget.index]; - late final _anim = AnimationController( - vsync: this, - duration: $styles.times.med, - )..forward(); + late final _anim = AnimationControllerProp(this, $styles.times.med); - bool get isAnimStopped => _anim.value == 0 || _anim.value == _anim.upperBound; - - @override - void dispose() { - _anim.dispose(); - super.dispose(); - } + bool get isAnimStopped => _anim.value == 0 || _anim.value == _anim.controller.upperBound; @override void didUpdateWidget(covariant _AnimatedCircleWithText oldWidget) { @@ -86,7 +77,7 @@ class _AnimatedCircleWithTextState extends State<_AnimatedCircleWithText> with S _prevIndex = oldWidget.index; // If the animation is already in motion, we don't need to interrupt it, just let the text change if (isAnimStopped) { - _anim.forward(from: 0); + _anim.controller.forward(from: 0); } } super.didUpdateWidget(oldWidget); @@ -95,9 +86,10 @@ class _AnimatedCircleWithTextState extends State<_AnimatedCircleWithText> with S @override Widget build(_) { return ListenableBuilder( - listenable: _anim, + listenable: _anim.controller, builder: (_, __) { var rot = _prevIndex > widget.index ? -pi : pi; + bool isCompleted = _anim.controller.isCompleted; return Transform.rotate( angle: Curves.easeInOut.transform(_anim.value) * rot, child: Container( @@ -114,13 +106,13 @@ class _AnimatedCircleWithTextState extends State<_AnimatedCircleWithText> with S child: Stack( children: [ Transform.rotate( - angle: _anim.isCompleted ? rot : 0, - child: _buildCircularText(_anim.isCompleted ? newTitle : oldTitle), + angle: isCompleted ? rot : 0, + child: _buildCircularText(isCompleted ? newTitle : oldTitle), ), - if (!_anim.isCompleted) ...[ + if (!isCompleted) ...[ Transform.rotate( - angle: _anim.isCompleted ? 0 : rot, - child: _buildCircularText(_anim.isCompleted ? oldTitle : newTitle), + angle: rot, + child: _buildCircularText(newTitle), ), ] ], diff --git a/lib/ui/screens/intro/intro_screen.dart b/lib/ui/screens/intro/intro_screen.dart index 9ab803ed..ed4b7d1f 100644 --- a/lib/ui/screens/intro/intro_screen.dart +++ b/lib/ui/screens/intro/intro_screen.dart @@ -14,7 +14,7 @@ class IntroScreen extends StatefulWidget { State createState() => _IntroScreenState(); } -class _IntroScreenState extends State { +class _IntroScreenState extends State with StatefulPropsMixin { static const double _imageSize = 264; static const double _logoHeight = 126; static const double _textHeight = 155; @@ -22,15 +22,9 @@ class _IntroScreenState extends State { static List<_PageData> pageData = []; - late final PageController _pageController = PageController()..addListener(_handlePageChanged); + late final _page = PageControllerProp(this, onChange: _handlePageChanged); final ValueNotifier _currentPage = ValueNotifier(0); - @override - void dispose() { - _pageController.dispose(); - super.dispose(); - } - void _handleIntroCompletePressed() { if (_currentPage.value == pageData.length - 1) { context.go(ScreenPaths.home); @@ -39,13 +33,16 @@ class _IntroScreenState extends State { } void _handlePageChanged() { - int newPage = _pageController.page?.round() ?? 0; + int newPage = _page.controller.page?.round() ?? 0; _currentPage.value = newPage; } void _handleSemanticSwipe(int dir) { - _pageController.animateToPage((_pageController.page ?? 0).round() + dir, - duration: $styles.times.fast, curve: Curves.easeOut); + _page.controller.animateToPage( + (_page.controller.page ?? 0).round() + dir, + duration: $styles.times.fast, + curve: Curves.easeOut, + ); } @override @@ -61,9 +58,7 @@ class _IntroScreenState extends State { // However, we only want the title / description to actually swipe, // so we stack a PageView with that content over top of all the other // content, and line up their layouts. - final List pages = pageData.map((e) => _Page(data: e)).toList(); - final Widget content = Stack(children: [ // page view with title & description: MergeSemantics( @@ -71,7 +66,7 @@ class _IntroScreenState extends State { onIncrease: () => _handleSemanticSwipe(1), onDecrease: () => _handleSemanticSwipe(-1), child: PageView( - controller: _pageController, + controller: _page.controller, children: pages, onPageChanged: (_) => AppHaptics.lightImpact(), ), @@ -118,8 +113,11 @@ class _IntroScreenState extends State { Container( height: _pageIndicatorHeight, alignment: Alignment(0.0, -0.75), - child: - AppPageIndicator(count: pageData.length, controller: _pageController, color: $styles.colors.offWhite), + child: AppPageIndicator( + count: pageData.length, + controller: _page.controller, + color: $styles.colors.offWhite, + ), ), Spacer(flex: 2), @@ -213,8 +211,8 @@ class _IntroScreenState extends State { child: Semantics( onTapHint: $strings.introSemanticNavigate, onTap: () { - final int current = _pageController.page!.round(); - _pageController.animateToPage(current + 1, duration: 250.ms, curve: Curves.easeIn); + final int current = _page.controller.page!.round(); + _page.controller.animateToPage(current + 1, duration: 250.ms, curve: Curves.easeIn); }, child: Text($strings.introSemanticSwipeLeft, style: $styles.text.bodySmall), ), diff --git a/lib/ui/screens/timeline/widgets/_scrolling_viewport_controller.dart b/lib/ui/screens/timeline/widgets/_scrolling_viewport_controller.dart index bb546063..30162994 100644 --- a/lib/ui/screens/timeline/widgets/_scrolling_viewport_controller.dart +++ b/lib/ui/screens/timeline/widgets/_scrolling_viewport_controller.dart @@ -1,5 +1,6 @@ part of '../timeline_screen.dart'; +/// TODO: This can just be a prop?? class _ScrollingViewportController extends ChangeNotifier { _ScrollingViewportController(this.state); final _ScalingViewportState state; diff --git a/lib/ui/screens/wallpaper_photo/wallpaper_photo_screen.dart b/lib/ui/screens/wallpaper_photo/wallpaper_photo_screen.dart index 4a8fae95..c514378a 100644 --- a/lib/ui/screens/wallpaper_photo/wallpaper_photo_screen.dart +++ b/lib/ui/screens/wallpaper_photo/wallpaper_photo_screen.dart @@ -24,13 +24,6 @@ class _WallpaperPhotoScreenState extends State { Widget? _illustration; bool _showTitleText = true; - Timer? _photoRetryTimer; - - @override - void dispose() { - _photoRetryTimer?.cancel(); - super.dispose(); - } void _handleTakePhoto(BuildContext context, String wonderName) async { final boundary = _containerKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; diff --git a/lib/ui/screens/wonder_details/wonders_details_screen.dart b/lib/ui/screens/wonder_details/wonders_details_screen.dart index 38efce6f..40179bc0 100644 --- a/lib/ui/screens/wonder_details/wonders_details_screen.dart +++ b/lib/ui/screens/wonder_details/wonders_details_screen.dart @@ -16,29 +16,17 @@ class WonderDetailsScreen extends StatefulWidget with GetItStatefulWidgetMixin { State createState() => _WonderDetailsScreenState(); } -class _WonderDetailsScreenState extends State - with GetItStateMixin, SingleTickerProviderStateMixin { - late final _tabController = TabController( +class _WonderDetailsScreenState extends State with GetItStateMixin, StatefulPropsMixin { + late final _tabs = TabControllerProp( + this, length: 4, - vsync: this, initialIndex: widget.initialTabIndex, - )..addListener(_handleTabChanged); - AnimationController? _fade; + autoBuild: true, + ); final _detailsHasScrolled = ValueNotifier(false); double? _tabBarHeight; - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - void _handleTabChanged() { - _fade?.forward(from: 0); - setState(() {}); - } - void _handleDetailsScrolled(double scrollPos) => _detailsHasScrolled.value = scrollPos > 0; void _handleTabMenuSized(Size size) { @@ -48,17 +36,16 @@ class _WonderDetailsScreenState extends State @override Widget build(BuildContext context) { final wonder = wondersLogic.getData(widget.type); - int tabIndex = _tabController.index; + int tabIndex = _tabs.controller.index; bool showTabBarBg = tabIndex != 1; final tabBarHeight = _tabBarHeight ?? 0; - //final double tabBarHeight = WonderDetailsTabMenu.bottomPadding + 60; return ColoredBox( color: Colors.black, child: Stack( children: [ /// Fullscreen tab views LazyIndexedStack( - index: _tabController.index, + index: _tabs.controller.index, children: [ WonderEditorialScreen(wonder, onScroll: _handleDetailsScrolled), PhotoGallery(collectionId: wonder.unsplashCollectionId, wonderType: wonder.type), @@ -74,7 +61,7 @@ class _WonderDetailsScreenState extends State builder: (_, value, ___) => MeasurableWidget( onChange: _handleTabMenuSized, child: WonderDetailsTabMenu( - tabController: _tabController, + tabController: _tabs.controller, wonderType: wonder.type, showBg: showTabBarBg, ), diff --git a/lib/ui/screens/wonder_events/widgets/_events_list.dart b/lib/ui/screens/wonder_events/widgets/_events_list.dart index 8d86b3a4..d89d637a 100644 --- a/lib/ui/screens/wonder_events/widgets/_events_list.dart +++ b/lib/ui/screens/wonder_events/widgets/_events_list.dart @@ -19,13 +19,8 @@ class _EventsList extends StatefulWidget { State<_EventsList> createState() => _EventsListState(); } -class _EventsListState extends State<_EventsList> { - final ScrollController _scroller = ScrollController(); - @override - void dispose() { - _scroller.dispose(); - super.dispose(); - } +class _EventsListState extends State<_EventsList> with StatefulPropsMixin { + late final _scroll = ScrollControllerProp(this); @override Widget build(BuildContext context) { @@ -58,7 +53,7 @@ class _EventsListState extends State<_EventsList> { children: [ //TODO: Remove scrollbar on portrait SingleChildScrollView( - controller: _scroller, + controller: _scroll.controller, child: Column( children: [ IgnorePointer(child: Gap(widget.topHeight)), @@ -104,17 +99,17 @@ class _EventsListState extends State<_EventsList> { /// Wraps the list in a scroll listener Widget _buildScrollingListWithBlur() { return ListenableBuilder( - listenable: _scroller, + listenable: _scroll.controller, child: _buildScrollingList(), builder: (_, child) { bool showBackdrop = true; double backdropAmt = 0; - if (_scroller.hasClients && showBackdrop) { + if (showBackdrop) { double blurStart = 50; double maxScroll = 150; - double scrollPx = _scroller.position.pixels - blurStart; + double scrollPx = _scroll.px - blurStart; // Normalize scroll position to a value between 0 and 1 - backdropAmt = (_scroller.position.pixels - blurStart).clamp(0, maxScroll) / maxScroll; + backdropAmt = (_scroll.px - blurStart).clamp(0, maxScroll) / maxScroll; // Disable backdrop once it is offscreen for an easy perf win showBackdrop = (scrollPx <= 500); } diff --git a/lib/ui/wonder_illustrations/common/illustration_piece.dart b/lib/ui/wonder_illustrations/common/illustration_piece.dart index c7ba1a01..4fd7aba2 100644 --- a/lib/ui/wonder_illustrations/common/illustration_piece.dart +++ b/lib/ui/wonder_illustrations/common/illustration_piece.dart @@ -83,9 +83,9 @@ class _IllustrationPieceState extends State { key: ValueKey(aspectRatio), builder: (_, constraints) { final anim = wonderBuilder.anim; - final curvedAnim = Curves.easeOut.transform(anim.value); + final curvedAnim = Curves.easeOut.transform(anim.controller.value); final config = wonderBuilder.widget.config; - Widget img = Image.asset(imgPath, opacity: anim, fit: BoxFit.fitHeight); + Widget img = Image.asset(imgPath, opacity: anim.controller, fit: BoxFit.fitHeight); // Add overflow box so image doesn't get clipped as we translate it around img = OverflowBox(maxWidth: 2000, child: img); diff --git a/lib/ui/wonder_illustrations/common/wonder_illustration_builder.dart b/lib/ui/wonder_illustrations/common/wonder_illustration_builder.dart index a0358851..a0485c57 100644 --- a/lib/ui/wonder_illustrations/common/wonder_illustration_builder.dart +++ b/lib/ui/wonder_illustrations/common/wonder_illustration_builder.dart @@ -25,27 +25,24 @@ class WonderIllustrationBuilder extends StatefulWidget { State createState() => WonderIllustrationBuilderState(); } -class WonderIllustrationBuilderState extends State with SingleTickerProviderStateMixin { - late final anim = AnimationController(vsync: this, duration: $styles.times.med * .75) - ..addListener(() => setState(() {})); - +class WonderIllustrationBuilderState extends State with StatefulPropsMixin { + late final anim = AnimationControllerProp( + this, + $styles.times.med * .75, + autoBuild: true, + autoPlay: false, + ); bool get isShowing => widget.config.isShowing; @override void initState() { super.initState(); - if (isShowing) anim.forward(from: 0); - } - - @override - void dispose() { - anim.dispose(); - super.dispose(); + if (isShowing) anim.controller.forward(from: 0); } @override void didUpdateWidget(covariant WonderIllustrationBuilder oldWidget) { if (isShowing != oldWidget.config.isShowing) { - isShowing ? anim.forward(from: 0) : anim.reverse(from: 1); + isShowing ? anim.controller.forward(from: 0) : anim.controller.reverse(from: 1); } super.didUpdateWidget(oldWidget); } @@ -53,8 +50,8 @@ class WonderIllustrationBuilderState extends State wi @override Widget build(BuildContext context) { // Optimization: no need to return all of these children if the widget is fully invisible. - if (anim.value == 0 && widget.config.enableAnims) return SizedBox.expand(); - Animation animation = widget.config.enableAnims ? anim : AlwaysStoppedAnimation(1); + if (anim.controller.value == 0 && widget.config.enableAnims) return SizedBox.expand(); + Animation animation = widget.config.enableAnims ? anim.controller : AlwaysStoppedAnimation(1); return Provider.value( value: this, diff --git a/pubspec.lock b/pubspec.lock index 808dbd67..17350a2f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1129,6 +1129,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.0" + stateful_props: + dependency: "direct main" + description: + name: stateful_props + sha256: "5f14e2f7c720b044e44d587ed3e7cfa6a3a2431d348416027755e5d331d0c164" + url: "https://pub.dev" + source: hosted + version: "1.2.0+1" stream_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 13c540e0..115b5ee0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ dependencies: screenshot: ^1.2.3 share_plus: ^4.0.10 shared_preferences: ^2.0.15 + stateful_props: ^1.2.0+1 simple_rich_text: ^2.0.49 sized_context: ^1.0.0+1 smooth_page_indicator: ^1.0.0+2