Skip to content

Commit

Permalink
Merge pull request #8 from JonasWanke/issue/5-all-day-events
Browse files Browse the repository at this point in the history
All-day events
  • Loading branch information
JonasWanke authored May 18, 2020
2 parents 478962c + 7ade86a commit e37eafc
Show file tree
Hide file tree
Showing 16 changed files with 592 additions and 103 deletions.
4 changes: 4 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ analyzer:
exclude:
- build/**
- lib/**.g.dart
- local/**
language:
strict-inference: true
strict-raw-types: true

linter:
rules:
Expand Down
3 changes: 3 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ class _TimetableExampleState extends State<TimetableExample> {
body: Timetable<BasicEvent>(
controller: _controller,
eventBuilder: (event) => BasicEventWidget(event),
allDayEventBuilder: (context, event, info) {
return BasicAllDayEventWidget(event, info: info);
},
),
),
);
Expand Down
10 changes: 10 additions & 0 deletions example/lib/positioning_demo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ final _events = <BasicEvent>[
_DemoEvent(7, 2, LocalTime(3, 0, 0), LocalTime(4, 0, 0)),
_DemoEvent(8, 0, LocalTime(20, 0, 0), LocalTime(4, 0, 0), endDateOffset: 1),
_DemoEvent.allDay(0, 0, 1),
_DemoEvent.allDay(1, 1, 1),
_DemoEvent.allDay(2, 0, 2),
_DemoEvent.allDay(3, 2, 2),
_DemoEvent.allDay(4, 2, 2),
_DemoEvent.allDay(5, 1, 2),
_DemoEvent.allDay(6, 3, 2),
_DemoEvent.allDay(7, 4, 4),
_DemoEvent.allDay(8, -1, 2),
_DemoEvent.allDay(9, -2, 2),
_DemoEvent.allDay(10, -3, 2),
];

class _DemoEvent extends BasicEvent {
Expand Down
196 changes: 196 additions & 0 deletions lib/src/all_day.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import 'dart:math' as math;

import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';

/// Information about how an all-day event was laid out.
@immutable
class AllDayEventLayoutInfo {
const AllDayEventLayoutInfo({
@required this.hiddenStartDays,
@required this.hiddenEndDays,
}) : assert(hiddenStartDays != null),
assert(hiddenStartDays >= 0),
assert(hiddenEndDays != null),
assert(hiddenEndDays >= 0);

final double hiddenStartDays;
final double hiddenEndDays;

@override
bool operator ==(dynamic other) {
return other is AllDayEventLayoutInfo &&
hiddenStartDays == other.hiddenStartDays &&
hiddenEndDays == other.hiddenEndDays;
}

@override
int get hashCode => hashList([hiddenStartDays, hiddenEndDays]);
}

class AllDayEventBackgroundPainter extends CustomPainter {
const AllDayEventBackgroundPainter({
@required this.info,
@required this.color,
this.borderRadius = 0,
}) : assert(info != null),
assert(color != null),
assert(borderRadius != null);

final AllDayEventLayoutInfo info;
final Color color;
final double borderRadius;

@override
void paint(Canvas canvas, Size size) {
canvas.drawPath(
_getPath(size, info, borderRadius),
Paint()..color = color,
);
}

@override
bool shouldRepaint(covariant AllDayEventBackgroundPainter oldDelegate) {
return info != oldDelegate.info ||
color != oldDelegate.color ||
borderRadius != oldDelegate.borderRadius;
}
}

/// A modified [RoundedRectangleBorder] that morphs to triangular left and/or
/// right borders if not all of the event is currently visible.
class AllDayEventBorder extends ShapeBorder {
const AllDayEventBorder({
@required this.info,
this.side = BorderSide.none,
this.borderRadius = 0,
}) : assert(info != null),
assert(side != null),
assert(borderRadius != null);

final AllDayEventLayoutInfo info;
final BorderSide side;
final double borderRadius;

@override
EdgeInsetsGeometry get dimensions => EdgeInsets.all(side.width);

@override
ShapeBorder scale(double t) {
return AllDayEventBorder(
info: info,
side: side.scale(t),
borderRadius: borderRadius * t,
);
}

@override
Path getInnerPath(Rect rect, {TextDirection textDirection}) {
return null;
}

@override
Path getOuterPath(Rect rect, {TextDirection textDirection}) {
return _getPath(rect.size, info, borderRadius);
}

@override
void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
// For some reason, when we paint the background in this shape directly, it
// lags while scrolling. Hence, we only use it to provide the outer path
// used for clipping.
}

@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is AllDayEventBorder &&
other.info == info &&
other.side == side &&
other.borderRadius == borderRadius;
}

@override
int get hashCode => hashValues(info, side, borderRadius);

@override
String toString() =>
'${objectRuntimeType(this, 'RoundedRectangleBorder')}($side, $borderRadius)';
}

Path _getPath(Size size, AllDayEventLayoutInfo info, double radius) {
final height = size.height;
// final radius = borderRadius.coerceAtMost(width / 2);

final maxTipWidth = height / 4;
final leftTipWidth = info.hiddenStartDays.coerceAtMost(1) * maxTipWidth;
final rightTipWidth = info.hiddenEndDays.coerceAtMost(1) * maxTipWidth;

final width = size.width;
// final leftTipBase = math.min(leftTipWidth + radius, width - radius);
// final rightTipBase = math.max(width - rightTipWidth - radius, radius);
final leftTipBase = info.hiddenStartDays > 0
? math.min(leftTipWidth + radius, width - radius)
: leftTipWidth + radius;
final rightTipBase = info.hiddenEndDays > 0
? math.max(width - rightTipWidth - radius, radius)
: width - rightTipWidth - radius;

final tipSize = Size.square(radius * 2);

// no tip: 0 ≈ 0°
// full tip: PI / 4 ≈ 45°
final leftTipAngle = math.pi / 2 - math.atan2(height / 2, leftTipWidth);
final rightTipAngle = math.pi / 2 - math.atan2(height / 2, rightTipWidth);

return Path()
..moveTo(leftTipBase, 0)
// Right top
..arcTo(
Offset(rightTipBase - radius, 0) & tipSize,
math.pi * 3 / 2,
math.pi / 2 - rightTipAngle,
false,
)
// Right tip
..arcTo(
Offset(rightTipBase + rightTipWidth - radius, height / 2 - radius) &
tipSize,
-rightTipAngle,
2 * rightTipAngle,
false,
)
// Right bottom
..arcTo(
Offset(rightTipBase - radius, height - radius * 2) & tipSize,
rightTipAngle,
math.pi / 2 - rightTipAngle,
false,
)
// Left bottom
..arcTo(
Offset(leftTipBase - radius, height - radius * 2) & tipSize,
math.pi / 2,
math.pi / 2 - leftTipAngle,
false,
)
// Left tip
..arcTo(
Offset(leftTipBase - leftTipWidth - radius, height / 2 - radius) &
tipSize,
math.pi - leftTipAngle,
2 * leftTipAngle,
false,
)
// Left top
..arcTo(
Offset(leftTipBase - radius, 0) & tipSize,
math.pi + leftTipAngle,
math.pi / 2 - leftTipAngle,
false,
);
}
49 changes: 48 additions & 1 deletion lib/src/basic.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'package:black_hole_flutter/black_hole_flutter.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:time_machine/time_machine.dart';
import 'package:time_machine/time_machine.dart' hide Offset;

import 'all_day.dart';
import 'event.dart';

/// A basic implementation of [Event] to get you started.
Expand Down Expand Up @@ -67,3 +69,48 @@ class BasicEventWidget extends StatelessWidget {
);
}
}

/// A simple [Widget] for displaying a [BasicEvent] as an all-day event.
class BasicAllDayEventWidget extends StatelessWidget {
const BasicAllDayEventWidget(
this.event, {
Key key,
@required this.info,
this.borderRadius = 4,
}) : assert(event != null),
assert(info != null),
assert(borderRadius != null),
super(key: key);

/// The [BasicEvent] to be displayed.
final BasicEvent event;
final AllDayEventLayoutInfo info;
final double borderRadius;

@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(2),
child: CustomPaint(
painter: AllDayEventBackgroundPainter(
info: info,
color: event.color,
borderRadius: borderRadius,
),
child: Padding(
padding: EdgeInsets.fromLTRB(4, 2, 0, 2),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: DefaultTextStyle(
style: context.textTheme.bodyText2.copyWith(
fontSize: 14,
color: event.color.highEmphasisOnColor,
),
child: Text(event.title, maxLines: 1),
),
),
),
),
);
}
}
20 changes: 15 additions & 5 deletions lib/src/event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ abstract class Event {
@required this.end,
}) : assert(id != null),
assert(start != null),
assert(end != null);
assert(end != null),
assert(start <= end);

/// A unique ID, used e.g. for animating events.
final Object id;
Expand All @@ -41,18 +42,27 @@ abstract class Event {

@override
int get hashCode => hashList([runtimeType, id, start, end]);

@override
String toString() => id.toString();
}

extension TimetableEvent on Event {
bool intersectsDate(LocalDate date) =>
start <= date.at(LocalTime.maxValue) && end > date.at(LocalTime.minValue);
intersectsInterval(DateInterval(date, date));

bool intersectsInterval(DateInterval interval) {
return start <= interval.end.at(LocalTime.maxValue) &&
end > interval.start.at(LocalTime.minValue);
return start.calendarDate <= interval.end &&
endDateInclusive >= interval.start;
}

LocalDate get endDateInclusive => (end - Period(nanoseconds: 1)).calendarDate;
LocalDate get endDateInclusive {
if (start.calendarDate == end.calendarDate) {
return end.calendarDate;
}

return (end - Period(nanoseconds: 1)).calendarDate;
}

DateInterval get intersectingDates =>
DateInterval(start.calendarDate, endDateInclusive);
Expand Down
2 changes: 1 addition & 1 deletion lib/src/event_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class StreamEventProvider<E extends Event> extends EventProvider<E>

final StreamedEventGetter<E> eventGetter;
ValueConnectableStream<Iterable<E>> _events;
StreamSubscription _eventsSubscription;
StreamSubscription<Iterable<E>> _eventsSubscription;

@override
Stream<Iterable<E>> getAllDayEventsIntersecting(DateInterval interval) {
Expand Down
Loading

0 comments on commit e37eafc

Please sign in to comment.