Layout topic

Layout

This is part of the kalender documentation.

Where event tiles are placed and sized. This is advanced material. The built-in strategies cover most apps, and you only need this to write your own.

For what a tile looks like once placed, see Appearance.


Vertical layout (Day / MultiDay body)

The package uses CustomMultiChildLayout with an EventLayoutDelegate to position tiles.

Built-in strategies (pass via MultiDayBodyConfiguration.eventLayoutStrategy):

Strategy Behavior
EventLayoutStrategy.overlap() Tiles stack on top of each other (default)
EventLayoutStrategy.sideBySide() Tiles placed side by side

To create a custom strategy, subclass EventLayoutDelegate. See CustomSideBySideLayoutDelegate in the advanced example.

performLayout must guard layoutChild and positionChild with hasChild. The day view builds only the tiles inside the visible scroll window, while the delegate receives every event of the day, so an event outside that window has no child. Calculate from every event, since a tile's width depends on partners that may be off screen, and lay out only the ones that were built.

Here is a minimal implementation:

class MyLayoutDelegate extends EventLayoutDelegate {
  MyLayoutDelegate({
    required super.events,
    required super.heightPerMinute,
    required super.date,
    required super.location,
    required super.timeOfDayRange,
    required super.minimumTileHeight,
    required super.layoutCache,
  });

  @override
  List<KalenderEvent> sortEvents(Iterable<KalenderEvent> events) =>
      events.toList()..sort((a, b) => a.start.compareTo(b.start));

  @override
  List<VerticalLayoutData> sortVerticalLayoutData(
      List<VerticalLayoutData> layoutData) => layoutData;

  @override
  void performLayout(Size size) {
    final verticalLayoutData = calculateVerticalLayoutData(size);
    for (final data in verticalLayoutData) {
      // Events scrolled out of view are culled and have no child to lay out.
      if (!hasChild(data.id)) continue;
      layoutChild(data.id, BoxConstraints.tightFor(
        width: size.width,
        height: data.height,
      ));
      positionChild(data.id, Offset(0, data.top));
    }
  }
}

Then wrap it in a strategy. Give the class value equality, comparing on runtimeType so a subclass of it does not compare equal. The field is included in the body configuration's equality, and a strategy that compares unequal on every build makes every rebuild look like a change.

class MyLayoutStrategy extends EventLayoutStrategy {
  const MyLayoutStrategy();

  @override
  EventLayoutDelegate createDelegate({
    required Iterable<KalenderEvent> events,
    required FloatingDateTime date,
    required KalenderTimeRange timeOfDayRange,
    required double heightPerMinute,
    required double? minimumTileHeight,
    required EventLayoutDelegateCache? cache,
    required Location? location,
  }) {
    return MyLayoutDelegate(
      events: events,
      date: date,
      heightPerMinute: heightPerMinute,
      timeOfDayRange: timeOfDayRange,
      minimumTileHeight: minimumTileHeight,
      layoutCache: cache ?? EventLayoutDelegateCache(),
      location: location,
    );
  }

  @override
  bool operator ==(Object other) => other.runtimeType == runtimeType;

  @override
  int get hashCode => (MyLayoutStrategy).hashCode;
}
final body = KalenderBody(
  multiDayBodyConfiguration: const MultiDayBodyConfiguration(
    eventLayoutStrategy: MyLayoutStrategy(),
  ),
);

Horizontal layout (Month view / MultiDay header)

Events are placed in a grid of rows × columns (rows = concurrent events, columns = days). A MultiDayLayoutStrategy produces a layout frame (MultiDayLayoutFrame) that determines each event's row and column span.

MultiDayLayoutStrategy.byDuration(), the default, sorts events by duration then start date.

Write your own by extending MultiDayLayoutStrategy. Call defaultMultiDayFrameGenerator to keep the built-in row assignment and change only the order, by supplying an eventComparator.

Give the class value equality, comparing on runtimeType for the same reason as above. This field is included in the body configuration's equality, and a strategy that compares unequal on every build clears the layout frame cache and regenerates every row each time.

class FrameSortedByEnd extends MultiDayLayoutStrategy {
  const FrameSortedByEnd();

  @override
  MultiDayLayoutFrame generateFrame({
    required FloatingDateTimeRange visibleRange,
    required List<KalenderEvent> events,
    required TextDirection textDirection,
    required Location? location,
    required MultiDayLayoutFrameCache? cache,
  }) {
    return defaultMultiDayFrameGenerator(
      visibleRange: visibleRange,
      events: events,
      textDirection: textDirection,
      location: location,
      cache: cache,
      eventComparator: (a, b) => a.end.compareTo(b.end),
    );
  }

  @override
  bool operator ==(Object other) => other.runtimeType == runtimeType;

  @override
  int get hashCode => (FrameSortedByEnd).hashCode;
}
final monthBody = KalenderBody(
  monthBodyConfiguration: MonthBodyConfiguration(
    multiDayLayoutStrategy: const FrameSortedByEnd(),
  ),
);

Classes

DurationMultiDayLayoutStrategy Layout
Places the longest events first, breaking ties by start time.
EventLayoutData Layout
This stores the final layout data of a single KalenderEvent.
EventLayoutDelegate Layout
The base MultiChildLayoutDelegate class for laying out KalenderEvents.
EventLayoutDelegateCache Layout
A cache for EventLayoutDelegates.
EventLayoutInformation Layout
Contains all the data needed to layout a single event with the MultiDayLayout.
EventLayoutStrategy Layout
Decides how tiles that overlap in time share a day column.
HorizontalGroupData Layout
This stores horizontal data top and bottom for a group of VerticalLayoutData.
MultiDayLayout Layout
A custom layout delegate for arranging multi-day events in a calendar view.
MultiDayLayoutFrame Layout
Frame containing all the data to layout the KalenderEvents with MultiDayLayout.
MultiDayLayoutFrameCache Layout
A cache for MultiDayLayoutFrames.
MultiDayLayoutStrategy Layout
Assigns each event in the multi-day lane a row and a span of columns.
OverlapLayoutDelegate Layout
The OverlapLayoutDelegate lays out KalenderEvent's, by stacking them on top of one another.
OverlapLayoutStrategy Layout
Lays out the tiles on top of each other.
SideBySideLayoutDelegate Layout
The SideBySideLayoutDelegate lays out KalenderEvent's next to one another.
SideBySideLayoutStrategy Layout
Lays out the tiles side by side.
VerticalLayoutData Layout
This stores the vertical layout data of a single KalenderEvent.

Functions

defaultMultiDayFrameGenerator({required FloatingDateTimeRange visibleRange, required List<KalenderEvent> events, required TextDirection textDirection, required Location? location, MultiDayLayoutFrameCache? cache, int eventComparator(KalenderEvent, KalenderEvent)?}) MultiDayLayoutFrame Layout
The row assignment behind MultiDayLayoutStrategy.byDuration.

Typedefs

RenderBoxCallback = RenderBox Function() Layout
A function that returns a RenderBox for the multi-day event layout.