From 72e727898aaffc109a6a586d198d27bb1e508abc Mon Sep 17 00:00:00 2001 From: Shawn Date: Sat, 5 Nov 2022 02:10:11 -0600 Subject: [PATCH] Fix timeline issues with event markers alignment --- lib/ui/screens/timeline/timeline_screen.dart | 43 +++++++----- .../timeline/widgets/_animated_era_text.dart | 16 +++++ .../timeline/widgets/_bottom_scrubber.dart | 8 ++- .../widgets/_dashed_divider_with_year.dart | 1 + .../timeline/widgets/_event_markers.dart | 67 +++++++++++++------ .../timeline/widgets/_scrolling_viewport.dart | 36 ++-------- .../_scrolling_viewport_controller.dart | 7 ++ .../timeline/widgets/_year_markers.dart | 17 ++++- 8 files changed, 124 insertions(+), 71 deletions(-) create mode 100644 lib/ui/screens/timeline/widgets/_animated_era_text.dart diff --git a/lib/ui/screens/timeline/timeline_screen.dart b/lib/ui/screens/timeline/timeline_screen.dart index 94cfee85..b1379fef 100644 --- a/lib/ui/screens/timeline/timeline_screen.dart +++ b/lib/ui/screens/timeline/timeline_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:ui'; +import 'package:flutter/foundation.dart'; import 'package:wonders/common_libs.dart'; import 'package:wonders/logic/common/debouncer.dart'; import 'package:wonders/logic/common/string_utils.dart'; @@ -22,6 +23,7 @@ part 'widgets/_scrolling_viewport.dart'; part 'widgets/_scrolling_viewport_controller.dart'; part 'widgets/_timeline_section.dart'; part 'widgets/_year_markers.dart'; +part 'widgets/_animated_era_text.dart'; class TimelineScreen extends StatefulWidget { final WonderType? type; @@ -35,12 +37,15 @@ class TimelineScreen extends StatefulWidget { class _TimelineScreenState extends State { /// Create a scroll controller that the top and bottom timelines can share final ScrollController _scroller = ScrollController(); + final _year = ValueNotifier(0); + + void _handleViewportYearChanged(int value) => _year.value = value; @override Widget build(BuildContext context) { return LayoutBuilder(builder: (_, constraints) { // Determine min and max size of the timeline based on the size available to this widget - const double scrubberSize = 70; + const double scrubberSize = 80; const double minSize = 1200; const double maxSize = 5500; return Container( @@ -53,26 +58,32 @@ class _TimelineScreenState extends State { /// Vertically scrolling timeline, manages a ScrollController. Expanded( - child: Stack( - children: [ - /// The timeline content itself - _ScrollingViewport( - scroller: _scroller, - minSize: minSize, - maxSize: maxSize, - selectedWonder: widget.type, - ), - ], + child: _ScrollingViewport( + scroller: _scroller, + minSize: minSize, + maxSize: maxSize, + selectedWonder: widget.type, + onYearChanged: _handleViewportYearChanged, ), ), + /// Era Text (classical, modern etc) + ValueListenableBuilder( + valueListenable: _year, + builder: (_, value, __) => _AnimatedEraText(value), + ), + Gap($styles.insets.xs), + /// Mini Horizontal timeline, reacts to the state of the larger scrolling timeline, /// and changes the timelines scroll position on Hz drag - _BottomScrubber( - _scroller, - size: scrubberSize, - timelineMinSize: minSize, - selectedWonder: widget.type, + Padding( + padding: EdgeInsets.symmetric(horizontal: $styles.insets.lg), + child: _BottomScrubber( + _scroller, + size: scrubberSize, + timelineMinSize: minSize, + selectedWonder: widget.type, + ), ), Gap($styles.insets.lg), ], diff --git a/lib/ui/screens/timeline/widgets/_animated_era_text.dart b/lib/ui/screens/timeline/widgets/_animated_era_text.dart new file mode 100644 index 00000000..2563305b --- /dev/null +++ b/lib/ui/screens/timeline/widgets/_animated_era_text.dart @@ -0,0 +1,16 @@ +part of '../timeline_screen.dart'; + +class _AnimatedEraText extends StatelessWidget { + const _AnimatedEraText(this.year); + final int year; + + @override + Widget build(BuildContext context) { + String era = StringUtils.getEra(year); + final style = $styles.text.body.copyWith(color: $styles.colors.offWhite); + return Semantics( + liveRegion: true, + child: Text(era, style: style), + ).animate(key: ValueKey(era)).fadeIn().slide(begin: Offset(0, .2)); + } +} diff --git a/lib/ui/screens/timeline/widgets/_bottom_scrubber.dart b/lib/ui/screens/timeline/widgets/_bottom_scrubber.dart index cc0d1c5c..cb5a1a41 100644 --- a/lib/ui/screens/timeline/widgets/_bottom_scrubber.dart +++ b/lib/ui/screens/timeline/widgets/_bottom_scrubber.dart @@ -41,8 +41,12 @@ class _BottomScrubber extends StatelessWidget { child: Stack( children: [ /// Timeline background - Padding( - padding: EdgeInsets.all($styles.insets.sm), + Container( + padding: EdgeInsets.all($styles.insets.md), + decoration: BoxDecoration( + color: $styles.colors.greyStrong, + borderRadius: BorderRadius.circular($styles.corners.md), + ), child: WondersTimelineBuilder( crossAxisGap: 4, selectedWonders: selectedWonder != null ? [selectedWonder!] : [], diff --git a/lib/ui/screens/timeline/widgets/_dashed_divider_with_year.dart b/lib/ui/screens/timeline/widgets/_dashed_divider_with_year.dart index 4aec9fa8..36ac305c 100644 --- a/lib/ui/screens/timeline/widgets/_dashed_divider_with_year.dart +++ b/lib/ui/screens/timeline/widgets/_dashed_divider_with_year.dart @@ -30,6 +30,7 @@ class _DashedDividerWithYear extends StatelessWidget { shadows: $styles.shadows.textStrong, ), ), + Gap($styles.insets.xs), ], ), ), diff --git a/lib/ui/screens/timeline/widgets/_event_markers.dart b/lib/ui/screens/timeline/widgets/_event_markers.dart index e7675730..00b2721d 100644 --- a/lib/ui/screens/timeline/widgets/_event_markers.dart +++ b/lib/ui/screens/timeline/widgets/_event_markers.dart @@ -13,6 +13,7 @@ class _EventMarkers extends StatefulWidget { } class _EventMarkersState extends State<_EventMarkers> { + bool get showReferenceMarkers => kDebugMode; int get startYr => wondersLogic.timelineStartYear; int get endYr => wondersLogic.timelineEndYear; @@ -60,7 +61,7 @@ class _EventMarkersState extends State<_EventMarkers> { /// Create a marker for each event List markers = timelineLogic.events.map((event) { - final offsetY = _calculateOffsetY(event.year); + double offsetY = _calculateOffsetY(event.year); return _EventMarker(offsetY, isSelected: event == selectedEvent, semanticLabel: '${event.year}: ${event.description}'); }).toList(); @@ -73,13 +74,33 @@ class _EventMarkersState extends State<_EventMarkers> { padding: EdgeInsets.only(left: 75), child: SizedBox( width: 20, - child: Stack(children: markers), + child: Stack( + children: [ + ...markers, + if (showReferenceMarkers) ..._buildReferenceMarkers(), + ], + ), ), ), ); }), ); } + + List _buildReferenceMarkers() { + final marker = Container(color: Colors.red.withOpacity(.4), width: 10, height: 10); + return [ + Align( + alignment: Alignment.topCenter, + child: FractionalTranslation(translation: Offset(0, -.5), child: marker), + ), + Align(alignment: Alignment.center, child: marker), + Align( + alignment: Alignment.bottomCenter, + child: FractionalTranslation(translation: Offset(0, .5), child: marker), + ), + ]; + } } /// A dot that represents a single global event. @@ -99,23 +120,31 @@ class _EventMarker extends StatelessWidget { Widget build(BuildContext context) { return Align( alignment: Alignment(0, -1 + offset * 2), - child: Semantics( - label: semanticLabel, - child: Container( - alignment: Alignment.center, - height: 30, - child: AnimatedContainer( - width: isSelected ? 6 : 2, - height: isSelected ? 6 : 2, - curve: Curves.easeOutBack, - duration: $styles.times.med, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(99), - color: $styles.colors.accent1, - boxShadow: [ - BoxShadow( - color: $styles.colors.accent1.withOpacity(isSelected ? .5 : 0), spreadRadius: 3, blurRadius: 3), - ], + // Use an OverflowBox wrapped in a zero-height sized box so that al + // This allows alignment-based positioning to be accurate even at the edges of the parent. + child: SizedBox( + height: 0, + child: OverflowBox( + maxHeight: 30, + child: Semantics( + label: semanticLabel, + child: Container( + alignment: Alignment.center, + height: 30, + child: AnimatedContainer( + width: isSelected ? 6 : 2, + height: isSelected ? 6 : 2, + curve: Curves.easeOutBack, + duration: $styles.times.med, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(99), + color: $styles.colors.accent1, + boxShadow: [ + BoxShadow( + color: $styles.colors.accent1.withOpacity(isSelected ? .5 : 0), spreadRadius: 3, blurRadius: 3), + ], + ), + ), ), ), ), diff --git a/lib/ui/screens/timeline/widgets/_scrolling_viewport.dart b/lib/ui/screens/timeline/widgets/_scrolling_viewport.dart index a7fde53f..27476ac9 100644 --- a/lib/ui/screens/timeline/widgets/_scrolling_viewport.dart +++ b/lib/ui/screens/timeline/widgets/_scrolling_viewport.dart @@ -9,11 +9,13 @@ class _ScrollingViewport extends StatefulWidget { required this.minSize, required this.maxSize, required this.selectedWonder, + this.onYearChanged, }) : super(key: key); final double minSize; final double maxSize; final ScrollController scroller; final WonderType? selectedWonder; + final void Function(int year)? onYearChanged; final void Function(_ScrollingViewportController controller)? onInit; @override @@ -53,21 +55,10 @@ class _ScalingViewportState extends State<_ScrollingViewport> { // Fade in entire view when first shown child: Stack( children: [ - Column( - children: [ - /// Main scrolling area, holds the year markers, and the [WondersTimelineBuilder] - Expanded( - child: _buildScrollingArea(context), - ), - Gap($styles.insets.xs), + // Main content area + _buildScrollingArea(context).animate().fadeIn(), - /// Era Text (classical, modern etc) - _buildAnimatedEraText(context), - Gap($styles.insets.xs), - ], - ).animate().fadeIn(), - - /// Dashed line with a year that changes as we scroll + // Dashed line with a year that changes as we scroll IgnorePointer( ignoringSemantics: false, child: AnimatedBuilder( @@ -82,23 +73,6 @@ class _ScalingViewportState extends State<_ScrollingViewport> { ); } - AnimatedBuilder _buildAnimatedEraText(BuildContext context) { - return AnimatedBuilder( - animation: controller.scroller, - builder: (_, __) { - String era = StringUtils.getEra(controller.calculateYearFromScrollPos()); - final style = $styles.text.body.copyWith(color: $styles.colors.offWhite); - return AnimatedSwitcher( - duration: $styles.times.fast, - child: Semantics( - liveRegion: true, - child: Text(era, key: ValueKey(era), style: style) - .animate(key: ValueKey(era)) - .slide(begin: Offset(0, .2))), - ); - }); - } - Widget _buildScrollingArea(BuildContext context) { // Builds a TimelineSection, and passes it the currently selected yr based on scroll position. // Rebuilds when timeline is scrolled. diff --git a/lib/ui/screens/timeline/widgets/_scrolling_viewport_controller.dart b/lib/ui/screens/timeline/widgets/_scrolling_viewport_controller.dart index 1b21a1ba..bb546063 100644 --- a/lib/ui/screens/timeline/widgets/_scrolling_viewport_controller.dart +++ b/lib/ui/screens/timeline/widgets/_scrolling_viewport_controller.dart @@ -12,6 +12,10 @@ class _ScrollingViewportController extends ChangeNotifier { late BoxConstraints _constraints; _ScrollingViewport get widget => state.widget; ScrollController get scroller => widget.scroller; + late final ValueNotifier _currentYr = ValueNotifier(startYr) + ..addListener( + () => state.widget.onYearChanged?.call(_currentYr.value), + ); void init() { scheduleMicrotask(() { @@ -22,10 +26,13 @@ class _ScrollingViewportController extends ChangeNotifier { final pos = calculateScrollPosFromYear(data.startYr); scroller.jumpTo(pos - 200); scroller.animateTo(pos, duration: 1.35.seconds, curve: Curves.easeOutCubic); + scroller.addListener(_updateCurrentYear); } }); } + void _updateCurrentYear() => _currentYr.value = calculateYearFromScrollPos(); + /// Allows ancestors to set zoom directly void setZoom(double d) { // ignore: invalid_use_of_protected_member diff --git a/lib/ui/screens/timeline/widgets/_year_markers.dart b/lib/ui/screens/timeline/widgets/_year_markers.dart index e0c96c5d..f1796841 100644 --- a/lib/ui/screens/timeline/widgets/_year_markers.dart +++ b/lib/ui/screens/timeline/widgets/_year_markers.dart @@ -57,10 +57,21 @@ class _YearMarker extends StatelessWidget { return ExcludeSemantics( child: Align( alignment: Alignment(0, -1 + offset * 2), - child: FractionalTranslation( - translation: Offset(0, 0), - child: Text('${yr.abs()}', style: $styles.text.body.copyWith(color: Colors.white, height: 1)), + + // Use an OverflowBox wrapped in a zero-height sized box so that al + // This allows alignment-based positioning to be accurate even at the edges of the parent. + child: SizedBox( + height: 0, + child: OverflowBox( + alignment: Alignment.topCenter, + maxHeight: 100, + maxWidth: 100, + child: FractionalTranslation( + translation: Offset(0, -.5), + child: Text('${yr.abs()}', style: $styles.text.body.copyWith(color: Colors.white, height: 1))), + ), ), + //child:, ), ); }