2022-11-01 09:46:23 +01:00
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
2022-09-01 12:05:37 +02:00
import ' dart:math ' ;
2022-08-26 09:04:08 +02:00
import ' package:flutter/material.dart ' ;
import ' package:flutter/scheduler.dart ' ;
2023-08-17 10:47:08 +02:00
import ' package:flutter_timetable/src/block_service.dart ' ;
import ' package:flutter_timetable/src/models/table_theme.dart ' ;
import ' package:flutter_timetable/src/models/time_block.dart ' ;
import ' package:flutter_timetable/src/widgets/block.dart ' ;
import ' package:flutter_timetable/src/widgets/table.dart ' as table ;
2022-08-24 09:39:36 +02:00
class Timetable extends StatefulWidget {
2022-08-26 09:04:08 +02:00
/// [Timetable] widget that displays a timetable with [TimeBlock]s.
/// The timetable automatically scrolls to the first item.
/// A [TableTheme] can be provided to customize the look of the timetable.
/// [mergeBlocks] and [combineBlocks] can be used to combine blocks
/// and merge columns of blocks when possible.
2022-08-24 11:01:50 +02:00
const Timetable ( {
2022-11-08 10:33:48 +01:00
this . tableDirection = Axis . vertical ,
2022-08-24 12:03:32 +02:00
this . timeBlocks = const [ ] ,
2022-11-18 14:22:05 +01:00
this . size ,
2023-08-17 10:36:16 +02:00
this . initialScrollTime ,
2022-08-24 11:01:50 +02:00
this . scrollController ,
2022-08-24 14:48:09 +02:00
this . scrollPhysics ,
2022-08-24 11:01:50 +02:00
this . startHour = 0 ,
this . endHour = 24 ,
2022-11-08 10:33:48 +01:00
this . blockDimension = 50 ,
2022-11-22 16:06:34 +01:00
this . blockColor = Colors . blue ,
2022-11-08 10:33:48 +01:00
this . hourDimension = 80 ,
2022-08-24 13:32:35 +02:00
this . theme = const TableTheme ( ) ,
2022-08-24 14:48:09 +02:00
this . mergeBlocks = false ,
2022-08-26 09:04:08 +02:00
this . combineBlocks = true ,
2022-08-24 11:01:50 +02:00
Key ? key ,
} ) : super ( key: key ) ;
2022-11-08 10:33:48 +01:00
/// The Axis in which the table is layed out.
final Axis tableDirection ;
2022-11-18 14:22:05 +01:00
/// The [Size] of the timetable.
final Size ? size ;
2022-08-24 11:01:50 +02:00
/// Hour at which the timetable starts.
final int startHour ;
/// Hour at which the timetable ends.
final int endHour ;
2022-08-24 12:03:32 +02:00
/// The time blocks that will be displayed in the timetable.
final List < TimeBlock > timeBlocks ;
2022-11-08 10:33:48 +01:00
/// The dimension in pixels of the block if there is no child
/// for the direction that the table expands in
final double blockDimension ;
2022-08-24 12:03:32 +02:00
2022-08-24 13:32:35 +02:00
/// The color of the block if there is no child
final Color blockColor ;
2022-11-08 10:33:48 +01:00
/// The dimension in pixels of one hour in the timetable.
final double hourDimension ;
2022-08-24 13:32:35 +02:00
/// The theme of the timetable.
final TableTheme theme ;
2023-08-17 10:36:16 +02:00
/// The initial time to scroll to if there are no timeblocks. If nothing is provided it will scroll to the current time or to the first block if there is one.
final TimeOfDay ? initialScrollTime ;
2022-08-24 11:01:50 +02:00
/// The scroll controller to control the scrolling of the timetable.
final ScrollController ? scrollController ;
2022-08-24 09:39:36 +02:00
2022-08-24 14:48:09 +02:00
/// 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.
2022-08-24 17:14:50 +02:00
/// If blocks have the same id and time they will be combined into one block.
2022-08-26 09:04:08 +02:00
final bool combineBlocks ;
2022-08-24 14:48:09 +02:00
2022-08-24 09:39:36 +02:00
@ override
State < Timetable > createState ( ) = > _TimetableState ( ) ;
}
class _TimetableState extends State < Timetable > {
2022-08-24 11:01:50 +02:00
late ScrollController _scrollController ;
@ override
void initState ( ) {
super . initState ( ) ;
2022-11-08 10:33:48 +01:00
_scrollController =
widget . scrollController ? ? ScrollController ( initialScrollOffset: 0 ) ;
2022-09-01 12:05:37 +02:00
if ( widget . timeBlocks . isNotEmpty ) {
_scrollToFirstBlock ( ) ;
2023-08-17 10:36:16 +02:00
} else {
_scrollToInitialTime ( ) ;
2022-09-01 12:05:37 +02:00
}
2022-08-24 11:01:50 +02:00
}
@ override
void dispose ( ) {
if ( widget . scrollController = = null ) {
_scrollController . dispose ( ) ;
}
super . dispose ( ) ;
}
2022-08-24 09:39:36 +02:00
@ override
Widget build ( BuildContext context ) {
2022-08-24 17:14:50 +02:00
List < TimeBlock > blocks ;
2022-08-26 09:04:08 +02:00
if ( widget . combineBlocks ) {
blocks = combineBlocksWithId ( widget . timeBlocks ) ;
2022-08-24 17:14:50 +02:00
} else {
blocks = widget . timeBlocks ;
}
2022-11-08 10:33:48 +01:00
var linePadding = _calculateTableTextSize ( ) . width ;
2022-11-18 14:22:05 +01:00
return SizedBox (
width: widget . size ? . width ,
height: widget . size ? . height ,
child: SingleChildScrollView (
physics: widget . scrollPhysics ? ? const BouncingScrollPhysics ( ) ,
controller: _scrollController ,
scrollDirection: widget . tableDirection ,
child: Stack (
alignment: Alignment . topLeft ,
children: [
table . Table (
tableDirection: widget . tableDirection ,
startHour: widget . startHour ,
endHour: widget . endHour ,
hourDimension: widget . hourDimension ,
tableOffset: _calculateTableStart ( widget . tableDirection ) . width ,
theme: widget . theme ,
size: widget . size ,
2022-08-24 13:32:35 +02:00
) ,
2022-11-18 14:22:05 +01:00
Container (
margin: EdgeInsets . only (
top: _calculateTableStart ( widget . tableDirection ) . height ,
left: _calculateTableStart ( widget . tableDirection ) . width ,
) ,
child: SingleChildScrollView (
physics: widget . scrollPhysics ? ? const BouncingScrollPhysics ( ) ,
scrollDirection: widget . tableDirection = = Axis . horizontal
? Axis . vertical
: Axis . horizontal ,
child: ( widget . tableDirection = = Axis . horizontal )
? Column (
2022-11-08 10:33:48 +01:00
crossAxisAlignment: CrossAxisAlignment . start ,
2022-11-18 14:22:05 +01:00
mainAxisAlignment: MainAxisAlignment . start ,
2022-11-08 10:33:48 +01:00
children: [
2022-11-18 14:22:05 +01:00
SizedBox ( height: widget . theme . tableTextOffset ) ,
2022-11-08 10:33:48 +01:00
if ( widget . mergeBlocks | | widget . combineBlocks ) . . . [
for ( var orderedBlocks in ( widget . mergeBlocks )
? mergeBlocksInColumns ( blocks )
: groupBlocksById ( blocks ) ) . . . [
Stack (
children: [
for ( var block in orderedBlocks ) . . . [
_showBlock ( block ) ,
] ,
] ,
) ,
SizedBox (
2022-11-18 14:22:05 +01:00
height: widget . theme . blockPaddingBetween ,
2022-11-08 10:33:48 +01:00
) ,
] ,
] else . . . [
for ( var block in blocks ) . . . [
2022-11-18 14:22:05 +01:00
_showBlock ( block , linePadding: linePadding ) ,
SizedBox (
height: widget . theme . blockPaddingBetween ,
) ,
2022-11-08 10:33:48 +01:00
] ,
] ,
2022-11-18 14:22:05 +01:00
// emtpy block at the end
2022-11-08 10:33:48 +01:00
SizedBox (
2022-11-18 14:22:05 +01:00
height: max (
2022-11-08 10:33:48 +01:00
widget . theme . tablePaddingEnd -
widget . theme . blockPaddingBetween ,
0 ,
) ,
) ,
] ,
2022-11-18 14:22:05 +01:00
)
: IntrinsicHeight (
child: Row (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
if ( widget . mergeBlocks | | widget . combineBlocks ) . . . [
for ( var orderedBlocks in ( widget . mergeBlocks )
? mergeBlocksInColumns ( blocks )
: groupBlocksById ( blocks ) ) . . . [
Stack (
children: [
for ( var block in orderedBlocks ) . . . [
_showBlock ( block ) ,
] ,
] ,
) ,
SizedBox (
width: widget . theme . blockPaddingBetween ,
) ,
] ,
] else . . . [
for ( var block in blocks ) . . . [
_showBlock ( block ) ,
] ,
] ,
SizedBox (
width: max (
widget . theme . tablePaddingEnd -
widget . theme . blockPaddingBetween ,
0 ,
) ,
height: widget . hourDimension *
( widget . endHour -
widget . startHour +
0.5 ) , // empty halfhour at the end
) ,
] ,
) ,
2022-09-01 12:05:37 +02:00
) ,
2022-11-18 14:22:05 +01:00
) ,
2022-08-24 12:03:32 +02:00
) ,
2022-11-18 14:22:05 +01:00
] ,
) ,
2022-08-24 11:01:50 +02:00
) ,
) ;
2022-08-24 09:39:36 +02:00
}
2022-08-24 12:13:24 +02:00
2023-08-17 10:36:16 +02:00
Size _calculateTableStart ( Axis axis ) = > Size (
( axis = = Axis . horizontal )
? _calculateTableTextSize ( ) . width / 2
: _calculateTableTextSize ( ) . width +
widget . theme . tablePaddingStart +
widget . theme . tableTextOffset ,
( axis = = Axis . vertical )
? _calculateTableTextSize ( ) . height / 2
: _calculateTableTextSize ( ) . height ,
) ;
2022-08-26 09:04:08 +02:00
2023-08-17 10:36:16 +02:00
Widget _showBlock ( TimeBlock block , { double linePadding = 0 } ) = > Block (
blockDirection: widget . tableDirection ,
linePadding: linePadding ,
start: block . start ,
end: block . end ,
startHour: widget . startHour ,
hourDimension: widget . hourDimension ,
blockDimension: widget . blockDimension ,
blockColor: widget . blockColor ,
child: block . child ,
) ;
2022-08-24 14:48:09 +02:00
2022-08-24 12:13:24 +02:00
void _scrollToFirstBlock ( ) {
SchedulerBinding . instance . addPostFrameCallback ( ( _ ) {
var earliestStart = widget . timeBlocks . map ( ( block ) = > block . start ) . reduce (
( a , b ) = >
a . hour < b . hour | | ( a . hour = = b . hour & & a . minute < b . minute )
? a
: b ,
) ;
2022-08-24 13:32:35 +02:00
var initialOffset =
2022-11-08 10:33:48 +01:00
( widget . hourDimension * ( widget . endHour - widget . startHour ) ) *
( ( earliestStart . hour - widget . startHour ) /
( widget . endHour - widget . startHour ) ) +
_calculateTableTextSize ( ) . width / 2 ;
2022-08-24 12:13:24 +02:00
_scrollController . jumpTo (
initialOffset ,
) ;
2022-11-08 10:33:48 +01:00
// TODO(freek): stop the controller from resetting
2022-08-24 12:13:24 +02:00
} ) ;
}
2022-08-24 13:32:35 +02:00
2023-08-17 10:36:16 +02:00
void _scrollToInitialTime ( ) {
SchedulerBinding . instance . addPostFrameCallback ( ( _ ) {
var startingTime = widget . initialScrollTime ? ? TimeOfDay . now ( ) ;
var initialOffset =
( widget . hourDimension * ( widget . endHour - widget . startHour ) ) *
( ( startingTime . hour - widget . startHour ) /
( widget . endHour - widget . startHour ) ) +
_calculateTableTextSize ( ) . width / 2 ;
_scrollController . jumpTo (
initialOffset ,
) ;
} ) ;
2022-08-24 13:32:35 +02:00
}
2022-11-08 10:33:48 +01:00
2023-08-17 10:36:16 +02:00
Size _calculateTableTextSize ( ) = > ( TextPainter (
text: TextSpan (
text: ' 22:22 ' ,
style:
widget . theme . timeStyle ? ? Theme . of ( context ) . textTheme . bodyLarge ,
) ,
maxLines: 1 ,
textScaleFactor: MediaQuery . of ( context ) . textScaleFactor ,
textDirection: TextDirection . ltr ,
) . . layout ( ) )
. size ;
2022-11-18 15:26:26 +01:00
double calculateTableHeight ( ) {
2022-11-08 10:33:48 +01:00
var sum = 0.0 ;
if ( widget . mergeBlocks | | widget . combineBlocks ) {
for ( var orderedBlocks in ( widget . mergeBlocks )
? mergeBlocksInColumns ( widget . timeBlocks )
: groupBlocksById ( widget . timeBlocks ) ) {
// check if any orderedBlock collides with another orderedBlock
if ( orderedBlocks . map ( ( block ) = > block . id ) . toSet ( ) . isNotEmpty & &
orderedBlocks . any (
( block ) = > orderedBlocks . any (
( block2 ) = > block ! = block2 & & block . collidesWith ( block2 ) ,
) ,
) ) {
// the sum is the combination of all the blocks
sum + = orderedBlocks
. map ( ( block ) = > block . childDimension ? ? widget . blockDimension )
. reduce ( ( a , b ) = > a + b ) ;
} else {
sum + =
// get the highest block within the group
orderedBlocks
. map (
( block ) = > max (
block . childDimension ? ? 0 ,
widget . blockDimension ,
) ,
)
. reduce ( max ) +
widget . theme . blockPaddingBetween ;
}
}
} else {
// sum of all the widget heights
sum = widget . timeBlocks
. map ( ( block ) = > block . childDimension ? ? widget . blockDimension )
. reduce ( ( a , b ) = > a + b ) ;
sum + = widget . timeBlocks . length * widget . theme . blockPaddingBetween ;
}
return sum ;
}
2022-08-24 09:39:36 +02:00
}