flip_calendar 0.1.1 copy "flip_calendar: ^0.1.1" to clipboard
flip_calendar: ^0.1.1 copied to clipboard

A customizable calendar widget with page-turn animations and swipe gesture navigation.

Flip Calendar #

A customizable Flutter month calendar widget with realistic page-turn animations and swipe gesture navigation, powered by page_turn_animation.

pub package License: MIT Publisher

Top edge demo Bottom edge demo Left edge demo Right edge demo
Top Bottom Left Right

Features #

  • Realistic 3D page curl effect when navigating between months
  • Swipe gesture navigation with flick detection
  • Fully customizable day cell rendering via builder
  • Programmatic navigation with CalendarController
  • Date constraints (min/max) with support for fixed, relative, and dynamic dates
  • Configurable bound edge (top, bottom, left, right)
  • Sequential or direct-jump multi-month animation modes
  • Built-in light and dark theme presets
  • Haptic feedback hooks for navigation events
  • Works with any first day of week (Monday, Sunday, etc.)

Installation #

Add the package to your pubspec.yaml:

dependencies:
  flip_calendar: ^0.1.0
  page_turn_animation: ^0.1.4

Then run:

flutter pub get

Quick Start #

import 'package:flip_calendar/flip_calendar.dart';
import 'package:page_turn_animation/page_turn_animation.dart';

class MyCalendar extends StatefulWidget {
  @override
  State<MyCalendar> createState() => _MyCalendarState();
}

class _MyCalendarState extends State<MyCalendar> {
  final _controller = CalendarController(initialMonth: DateTime.now());
  DateTime? _selectedDate;

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return FlipCalendar(
      controller: _controller,
      selectedDate: _selectedDate,
      onDayTap: (date) => setState(() => _selectedDate = date),
      dayBuilder: (context, data) {
        return Center(
          child: Text(
            data.date.day.toString(),
            style: TextStyle(
              color: data.isCurrentMonth ? Colors.black : Colors.grey[400],
              fontWeight: data.isToday ? FontWeight.bold : FontWeight.normal,
            ),
          ),
        );
      },
    );
  }
}

Usage Guide #

CalendarController #

The CalendarController manages the displayed month and exposes animation state. Create it once and pass it to FlipCalendar:

final controller = CalendarController(initialMonth: DateTime(2025, 6, 1));

// Listen for changes
controller.addListener(() {
  print('Month: ${controller.currentMonth}');
  print('Animating: ${controller.isAnimating}');
});

// Programmatic navigation
controller.nextMonth();
controller.previousMonth();
controller.goToMonth(DateTime(2025, 12, 1));
controller.goToToday();
controller.goToYearMonth(2026, 3);

The isAnimating property is useful for disabling external UI (such as header navigation buttons) while a page-turn animation is in progress.

Day Builder #

The dayBuilder callback receives a CalendarDayData object with everything you need to render each cell:

FlipCalendar(
  controller: controller,
  dayBuilder: (context, data) {
    // data.date           — the date this cell represents
    // data.isCurrentMonth — false for overflow days from adjacent months
    // data.isToday        — whether this date is today
    // data.isSelected     — whether this date matches selectedDate
    // data.isFutureDate   — whether this date is after today
    // data.isEnabled      — whether this cell is within date bounds
    // data.row / data.column — grid position (0-based)

    return Center(
      child: Text(
        data.date.day.toString(),
        style: TextStyle(
          color: data.isEnabled ? Colors.black : Colors.grey,
        ),
      ),
    );
  },
)

Date Constraints #

Control the navigable and selectable date range using DateConstraint:

// Fixed date
FlipCalendar(
  controller: controller,
  minDate: DateConstraint.fixed(DateTime(2020, 1, 1)),
  maxDate: DateConstraint.fixed(DateTime(2030, 12, 31)),
  // ...
)

// Dynamic: today recomputes each build
FlipCalendar(
  controller: controller,
  maxDate: DateConstraint.today(),
  // ...
)

// Relative: 2 years back, 1 year ahead
FlipCalendar(
  controller: controller,
  minDate: DateConstraint.relative(years: -2),
  maxDate: DateConstraint.relative(years: 1),
  // ...
)

When a user swipes toward a restricted month, the gesture is clamped to a small bounce and the onHapticFeedback callback fires with CalendarHapticType.navigationRestricted.

Choosing the Bound Edge #

The boundEdge parameter controls which edge the page curls over, and determines whether swipe gestures are vertical or horizontal:

// Top-bound (default): swipe up/down to navigate
FlipCalendar(
  controller: controller,
  boundEdge: PageTurnEdge.top,
  // ...
)

// Right-bound: swipe left/right like a book
FlipCalendar(
  controller: controller,
  boundEdge: PageTurnEdge.right,
  // ...
)
Edge Gesture Use Case
top Vertical (swipe up/down) Top-bound notepad (default)
bottom Vertical (swipe up/down) Wall calendar, bottom-bound pad
left Horizontal (swipe left/right) Right-to-left book, manga
right Horizontal (swipe left/right) Left-to-right book, standard Western reading

Multi-Month Animation Modes #

When navigating multiple months at once (e.g., jumping from January to June programmatically), you can choose how the animation behaves:

// Sequential: flips through each intermediate month (default)
FlipCalendar(
  controller: controller,
  multiMonthAnimationMode: MultiMonthAnimationMode.sequential,
  maxAnimatedMonthJump: 6, // Skip animation beyond 6 months
  // ...
)

// Direct jump: single flip from start to end month
FlipCalendar(
  controller: controller,
  multiMonthAnimationMode: MultiMonthAnimationMode.directJump,
  // ...
)
Mode Behavior
sequential Flips through each month in sequence. Skips animation if jump exceeds maxAnimatedMonthJump.
directJump Single page flip directly from origin to destination, regardless of distance.

First Day of Week #

// Start weeks on Monday
FlipCalendar(
  controller: controller,
  firstDayOfWeek: DateTime.monday,
  // ...
)

// Start weeks on Sunday (default)
FlipCalendar(
  controller: controller,
  firstDayOfWeek: DateTime.sunday,
  // ...
)

The weekday header row automatically rotates to match.

Haptic Feedback #

The calendar exposes haptic events via a callback — you implement the actual feedback:

import 'package:flutter/services.dart';

FlipCalendar(
  controller: controller,
  onHapticFeedback: (type) {
    switch (type) {
      case CalendarHapticType.navigationRestricted:
        HapticFeedback.heavyImpact();
    }
  },
  // ...
)

Disabling Animation and Gestures #

// Static calendar with no animations or gestures
FlipCalendar(
  controller: controller,
  animationsEnabled: false,
  gesturesEnabled: false,
  // ...
)

// Animated programmatic navigation, but no swipe gestures
FlipCalendar(
  controller: controller,
  animationsEnabled: true,
  gesturesEnabled: false,
  // ...
)

Customization #

CalendarStyle #

Customize the visual appearance with CalendarStyle:

FlipCalendar(
  controller: controller,
  style: CalendarStyle(
    // Background
    calendarBackground: Colors.white,
    padding: EdgeInsets.all(8),
    borderRadius: BorderRadius.circular(12),

    // Grid
    gridLineColor: Colors.grey[300]!,
    gridLineWidth: 1.0,

    // Weekday header
    weekdayHeaderBackground: Colors.grey[100]!,
    weekdayHeaderTextColor: Colors.black87,
    weekdayHeaderHeight: 40.0,
    weekdayNames: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],

    // Today indicator
    todayBorderColor: Colors.blue,
    todayBorderWidth: 2.0,

    // Selection
    selectedDayBackground: Colors.blue.withValues(alpha: 0.1),

    // Animation
    animationDuration: Duration(milliseconds: 650),
    animationCurve: Curves.decelerate,
    pageTurnStyle: PageTurnStyle(
      shadowOpacity: 0.8,
      curlIntensity: 1.2,
    ),

    // Gestures
    flickDistanceThreshold: 0.05,
    dragProgressThreshold: 0.3,
  ),
  // ...
)

Theme Presets #

// Light theme (same as default)
FlipCalendar(
  controller: controller,
  style: CalendarStyle.light(),
  // ...
)

// Dark theme
FlipCalendar(
  controller: controller,
  style: CalendarStyle.dark(),
  // ...
)

Using copyWith #

final customStyle = CalendarStyle.dark().copyWith(
  borderRadius: BorderRadius.circular(16),
  animationDuration: Duration(milliseconds: 800),
  todayBorderColor: Colors.amber,
);

Style Properties #

Background & Layout

Property Type Default Description
calendarBackground Color Colors.white Background color of the calendar widget
padding EdgeInsets EdgeInsets.zero Padding inside the calendar background
borderRadius BorderRadius BorderRadius.zero Border radius for the calendar's outer edges

Grid

Property Type Default Description
gridLineColor Color Color(0xFFE0E0E0) Color of grid lines between cells
gridLineWidth double 1.0 Width of grid lines

Weekday Header

Property Type Default Description
weekdayHeaderBackground Color Color(0xFFF5F5F5) Background color of the header row
weekdayHeaderTextColor Color Colors.black87 Text color for weekday names
weekdayHeaderHeight double 40.0 Height of the header row
weekdayTextStyle TextStyle? null Custom text style (overrides color/size)
weekdayNames List<String> ['Sun', 'Mon', ...] Weekday names starting from Sunday

Day Cells

Property Type Default Description
dayTextColor Color Colors.black87 Text color for day numbers
disabledDayTextColor Color Color(0xFF9E9E9E) Text color for disabled days
dayTextSize double 16.0 Font size for day numbers

Today Indicator

Property Type Default Description
todayBorderColor Color Colors.blue Border color for today's cell
todayBorderWidth double 2.0 Border width for today's cell
todayBorderRadius BorderRadius Radius.circular(4) Border radius for today's cell
todayMargin EdgeInsets EdgeInsets.all(2) Margin around today's cell

Selection & Disabled

Property Type Default Description
selectedDayBackground Color Colors.blue (10%) Background for the selected day
disabledDateBackground Color Colors.grey (10%) Background for disabled dates

Animation

Property Type Default Description
animationDuration Duration 650ms Duration of the page-turn animation
animationCurve Curve Curves.decelerate Easing curve for animation
pageTurnStyle PageTurnStyle PageTurnStyle() Style for the page curl effect

Gestures

Property Type Default Description
flickDistanceThreshold double 0.05 Min distance fraction for a flick
flickMaxDuration Duration 500ms Max duration for a flick gesture
dragBoxSizePercentage double 0.7 Fraction of dimension = 100% progress
dragProgressThreshold double 0.3 Min progress to complete a transition

Building a Header #

The calendar widget renders only the grid — headers, titles, and navigation buttons are your responsibility. Here's a typical pattern:

class CalendarScreen extends StatefulWidget {
  @override
  State<CalendarScreen> createState() => _CalendarScreenState();
}

class _CalendarScreenState extends State<CalendarScreen> {
  final _controller = CalendarController(initialMonth: DateTime.now());
  DateTime? _selectedDate;

  @override
  void initState() {
    super.initState();
    _controller.addListener(() => setState(() {}));
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final month = _controller.currentMonth;
    final monthName = _monthNames[month.month - 1];

    return Column(
      children: [
        // Custom header
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            IconButton(
              onPressed: _controller.isAnimating
                  ? null
                  : _controller.previousMonth,
              icon: Icon(Icons.chevron_left),
            ),
            Text(
              '$monthName ${month.year}',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            IconButton(
              onPressed: _controller.isAnimating
                  ? null
                  : _controller.nextMonth,
              icon: Icon(Icons.chevron_right),
            ),
          ],
        ),

        // Calendar
        Expanded(
          child: FlipCalendar(
            controller: _controller,
            selectedDate: _selectedDate,
            onDayTap: (date) => setState(() => _selectedDate = date),
            dayBuilder: (context, data) {
              return Center(
                child: Text(data.date.day.toString()),
              );
            },
          ),
        ),
      ],
    );
  }

  static const _monthNames = [
    'January', 'February', 'March', 'April', 'May', 'June',
    'July', 'August', 'September', 'October', 'November', 'December',
  ];
}

Note how _controller.isAnimating is used to disable the navigation buttons during animation.

API Reference #

FlipCalendar #

The main calendar widget.

const FlipCalendar({
  required CalendarController controller,
  required Widget Function(BuildContext, CalendarDayData) dayBuilder,
  DateTime? selectedDate,
  void Function(DateTime)? onDayTap,
  void Function(CalendarHapticType)? onHapticFeedback,
  CalendarStyle style = const CalendarStyle(),
  int firstDayOfWeek = DateTime.sunday,
  DateConstraint? minDate,
  DateConstraint? maxDate,
  PageTurnEdge boundEdge = PageTurnEdge.top,
  bool animationsEnabled = true,
  bool gesturesEnabled = true,
  int maxAnimatedMonthJump = 6,
  MultiMonthAnimationMode multiMonthAnimationMode = MultiMonthAnimationMode.sequential,
})

CalendarController #

Navigation state manager. Extends ChangeNotifier.

Property/Method Description
currentMonth The currently displayed month
isAnimating Whether a transition is in progress
nextMonth() Navigate to the next month
previousMonth() Navigate to the previous month
goToMonth(DateTime) Navigate to a specific month
goToToday() Navigate to the current month
goToYearMonth(int, int) Navigate to a specific year and month

CalendarDayData #

Data object passed to the day builder.

Property Type Description
date DateTime The date this cell represents
isCurrentMonth bool Whether the date is in the displayed month
isToday bool Whether the date is today
isSelected bool Whether the date matches selectedDate
isFutureDate bool Whether the date is after today
isEnabled bool Whether the date is within min/max bounds
row int Row index in the grid (0-based)
column int Column index in the grid (0-based)

DateConstraint #

Defines a date boundary. Supports static, dynamic, and relative dates.

Factory Description
DateConstraint.fixed(DateTime) A fixed date boundary
DateConstraint.today() Today's date, recomputed each build
DateConstraint.relative({years, months, days}) Offset from today, recomputed each build

MultiMonthAnimationMode #

Value Description
sequential Flips through each intermediate month
directJump Single flip from start to end month

CalendarHapticType #

Value Description
navigationRestricted User swiped toward a restricted month

CalendarStyle #

Configuration class for visual styling. See Customization for all properties.

MonthGrid #

Utility class for computing the grid layout of a month. Useful for building custom overlays or layouts on top of the calendar.

final grid = MonthGrid.forMonth(
  DateTime(2025, 6, 1),
  firstDayOfWeek: DateTime.monday,
);

print(grid.rows);            // 5 or 6
print(grid.start);           // First date shown in the grid
print(grid.dateAt(0, 0));    // Date at row 0, column 0
print(grid.totalCells);      // rows * 7

Best Practices #

Controller Lifecycle #

Always dispose the controller when the parent widget is disposed:

@override
void dispose() {
  _controller.dispose();
  super.dispose();
}

Performance Tips #

  • Use CalendarStyle.pageTurnStyle to lower segments (e.g., 50–80) on lower-end devices
  • Avoid rebuilding the parent widget during animations — use _controller.isAnimating to gate updates
  • For large month jumps, MultiMonthAnimationMode.directJump is more performant than sequential

Sizing #

The calendar expands to fill available space. Wrap it in a SizedBox or use Expanded to control dimensions:

SizedBox(
  height: 400,
  child: FlipCalendar(
    controller: controller,
    dayBuilder: (context, data) => Center(
      child: Text(data.date.day.toString()),
    ),
  ),
)

License #

MIT License — see LICENSE for details.

Contributing #

Contributions are welcome! Please feel free to submit issues and pull requests on GitHub.

0
likes
160
points
110
downloads

Publisher

verified publisherresengi.io

Weekly Downloads

A customizable calendar widget with page-turn animations and swipe gesture navigation.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, page_turn_animation

More

Packages that depend on flip_calendar