From 5a54309a5d1e3a820097782302eea6db45d336f4 Mon Sep 17 00:00:00 2001 From: Freek van de Ven Date: Wed, 24 Aug 2022 14:48:09 +0200 Subject: [PATCH] feat: added timetable merge column option --- example/lib/main.dart | 2 + lib/src/models/time_block.dart | 14 ++++++ lib/src/timetable.dart | 78 +++++++++++++++++++++++++++++----- 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index b2a2737..3848196 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -53,6 +53,8 @@ class _TimetableDemoState extends State { endHour: 22, timeBlocks: blocks, scrollController: _scrollController, + tablePaddingStart: 0, + mergeBlocks: true, ), ), ); diff --git a/lib/src/models/time_block.dart b/lib/src/models/time_block.dart index bda25d0..5bea5dc 100644 --- a/lib/src/models/time_block.dart +++ b/lib/src/models/time_block.dart @@ -4,6 +4,7 @@ class TimeBlock { TimeBlock({ required this.start, required this.end, + this.id = 0, this.child, }) : assert( start.hour <= end.hour || @@ -22,4 +23,17 @@ class TimeBlock { /// The child widget that will be displayed within the block. /// The size of the parent widget should dictate the size of the child. final Widget? child; + + /// The identifier of the block that is used to collapse blocks in 1 column. + /// Leave empty or 0 if you do not want to collapse blocks. + final int id; + + /// Check if two blocks overlap each other + bool collidesWith(TimeBlock block) { + var startMinute = start.hour * 60 + start.minute; + var endMinute = end.hour * 60 + end.minute; + var otherStartMinute = block.start.hour * 60 + block.start.minute; + var otherEndMinute = block.end.hour * 60 + block.end.minute; + return startMinute < otherEndMinute && endMinute > otherStartMinute; + } } diff --git a/lib/src/timetable.dart b/lib/src/timetable.dart index ac9a11f..f66c56c 100644 --- a/lib/src/timetable.dart +++ b/lib/src/timetable.dart @@ -4,6 +4,7 @@ class Timetable extends StatefulWidget { const Timetable({ this.timeBlocks = const [], this.scrollController, + this.scrollPhysics, this.startHour = 0, this.endHour = 24, this.blockWidth = 50, @@ -12,6 +13,8 @@ class Timetable extends StatefulWidget { this.tablePaddingStart = 10, this.tablePaddingEnd = 15, this.theme = const TableTheme(), + this.mergeBlocks = false, + this.collapseBlocks = false, Key? key, }) : super(key: key); @@ -45,6 +48,15 @@ class Timetable extends StatefulWidget { /// The scroll controller to control the scrolling of the timetable. final ScrollController? scrollController; + /// The scroll physics used for the SinglechildScrollView. + final ScrollPhysics? scrollPhysics; + + /// Whether or not to merge blocks in 1 column that fit below eachother. + final bool mergeBlocks; + + /// Whether or not to collapse blocks in 1 column if they have the same id. + final bool collapseBlocks; + @override State createState() => _TimetableState(); } @@ -70,13 +82,14 @@ class _TimetableState extends State { @override Widget build(BuildContext context) { return SingleChildScrollView( - physics: const BouncingScrollPhysics(), + physics: widget.scrollPhysics ?? const BouncingScrollPhysics(), controller: _scrollController, child: Stack( children: [ Table( startHour: widget.startHour, endHour: widget.endHour, + columnHeight: widget.hourHeight, theme: widget.theme, ), Container( @@ -86,20 +99,43 @@ class _TimetableState extends State { 5, ), child: SingleChildScrollView( + physics: widget.scrollPhysics ?? const BouncingScrollPhysics(), scrollDirection: Axis.horizontal, child: IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - for (var block in widget.timeBlocks) ...[ - Block( - start: block.start, - end: block.end, - startHour: widget.startHour, - hourHeight: widget.hourHeight, - blockWidth: widget.blockWidth, - child: block.child, - ), + if (!widget.mergeBlocks && !widget.collapseBlocks) ...[ + for (var block in widget.timeBlocks) ...[ + Block( + start: block.start, + end: block.end, + startHour: widget.startHour, + hourHeight: widget.hourHeight, + blockWidth: widget.blockWidth, + blockColor: widget.blockColor, + child: block.child, + ), + ], + ] else if (widget.mergeBlocks) ...[ + for (var blocks + in _mergeBlocksInColumns(widget.timeBlocks)) ...[ + Stack( + children: [ + for (var block in blocks) ...[ + Block( + start: block.start, + end: block.end, + startHour: widget.startHour, + hourHeight: widget.hourHeight, + blockWidth: widget.blockWidth, + blockColor: widget.blockColor, + child: block.child, + ), + ], + ], + ), + ] ], SizedBox( width: widget.tablePaddingEnd, @@ -116,6 +152,27 @@ class _TimetableState extends State { ); } + List> _mergeBlocksInColumns(List blocks) { + var mergedBlocks = >[]; + // try to put blocks in the same column if the time doesn´t collide + for (var block in blocks) { + var mergeIndex = 0; + + for (var mergedBlock in mergedBlocks) { + if (!mergedBlock.any((b) => b.collidesWith(block))) { + mergedBlock.add(block); + break; + } else { + mergeIndex++; + } + } + if (mergedBlocks.length == mergeIndex) { + mergedBlocks.add([block]); + } + } + return mergedBlocks; + } + void _scrollToFirstBlock() { SchedulerBinding.instance.addPostFrameCallback((_) { var earliestStart = widget.timeBlocks.map((block) => block.start).reduce( @@ -134,7 +191,6 @@ class _TimetableState extends State { }); } - /// Calculates the width of 22:22 Size _calculateTableTextSize() { return (TextPainter( text: TextSpan(text: '22:22', style: widget.theme.timeStyle),