Files
flutter-wonderous-app/lib/ui/screens/timeline/widgets/_scrolling_viewport.dart

170 lines
5.5 KiB
Dart

part of '../timeline_screen.dart';
class _ScrollingViewport extends StatefulWidget {
const _ScrollingViewport({
super.key,
// ignore: unused_element
this.onInit,
required this.scroller,
required this.minSize,
required this.maxSize,
required this.selectedWonder,
this.onYearChanged,
});
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
State<_ScrollingViewport> createState() => _ScalingViewportState();
}
class _ScalingViewportState extends State<_ScrollingViewport> {
late final _ScrollingViewportController controller = _ScrollingViewportController(this);
static const double _minTimelineSize = 100;
final _currentEventMarker = ValueNotifier<TimelineEvent?>(null);
Size? _prevSize;
@override
void initState() {
super.initState();
controller.init();
widget.onInit?.call(controller);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
void _handleEventMarkerChanged(TimelineEvent? event) {
_currentEventMarker.value = event;
AppHaptics.selectionClick();
}
void _handleMarkerPressed(event) {
final pos = controller.calculateScrollPosFromYear(event.year);
controller.scroller.animateTo(pos, duration: $styles.times.med, curve: Curves.easeOutBack);
}
@override
Widget build(BuildContext context) {
if (_prevSize != null && _prevSize != context.mq.size) {
scheduleMicrotask(controller._handleResize);
}
_prevSize = context.mq.size;
return GestureDetector(
// Handle pinch to zoom
onScaleUpdate: controller._handleScaleUpdate,
onScaleStart: controller._handleScaleStart,
behavior: HitTestBehavior.translucent,
// Fade in entire view when first shown
child: Stack(
children: [
// Main content area
_buildScrollingArea(context).animate().fadeIn(),
// Dashed line with a year that changes as we scroll
IgnorePointerWithSemantics(
child: AnimatedBuilder(
animation: controller.scroller,
builder: (_, __) {
return _DashedDividerWithYear(controller.calculateYearFromScrollPos());
},
),
),
],
),
);
}
Widget _buildScrollingArea(BuildContext context) {
// Builds a TimelineSection, and passes it the currently selected yr based on scroll position.
// Rebuilds when timeline is scrolled.
Widget buildTimelineSection(WonderData data) {
return ClipRRect(
borderRadius: BorderRadius.circular(99),
child: AnimatedBuilder(
animation: controller.scroller,
builder: (_, __) => TimelineSection(
data,
controller.calculateYearFromScrollPos(),
selectedWonder: widget.selectedWonder,
),
),
);
}
return LayoutBuilder(
builder: (_, constraints) {
// cache constraints, so they can be used to maintain the selected year while zooming
controller._constraints = constraints;
double vtPadding = constraints.maxHeight / 2;
double height = controller.calculateContentHeight();
final width = min($styles.sizes.maxContentWidth2, constraints.maxWidth);
return Stack(
children: [
SingleChildScrollView(
controller: controller.scroller,
padding: EdgeInsets.symmetric(vertical: vtPadding),
// A stack inside a SizedBox which sets its overall height
child: CenteredBox(
height: height,
width: width,
child: Stack(
children: [
/// Year Markers
_YearMarkers(),
/// individual timeline sections
Positioned.fill(
left: 100,
right: $styles.insets.sm,
child: FocusTraversalGroup(
//child: Placeholder(),
child: WondersTimelineBuilder(
axis: Axis.vertical,
crossAxisGap: max(6, (width - (120 * 3)) / 2),
minSize: _minTimelineSize,
timelineBuilder: (_, data, __) => buildTimelineSection(data),
),
),
),
/// Event Markers, rebuilds on scroll
AnimatedBuilder(
animation: controller.scroller,
builder: (_, __) => _EventMarkers(
controller.calculateYearFromScrollPos(),
onEventChanged: _handleEventMarkerChanged,
onMarkerPressed: _handleMarkerPressed,
),
),
],
),
),
),
/// Top and bottom gradients for visual style
ListOverscollGradient(),
BottomCenter(
child: ListOverscollGradient(bottomUp: true),
),
/// Event Popups, rebuilds when [_currentEventMarker] changes
ValueListenableBuilder<TimelineEvent?>(
valueListenable: _currentEventMarker,
builder: (_, data, __) {
return _EventPopups(currentEvent: data);
})
],
);
},
);
}
}