Flutter增强课程表管理插件flutter_enhanced_timetable的使用

Flutter增强课程表管理插件flutter_enhanced_timetable的使用

flutter_enhanced_timetable 是一个定制化的、带有动画效果的日历小部件,包括日视图、周视图和月视图。

注意事项

此包 flutter_enhanced_timetable 是原始 timetable 包的分叉版本。我们创建了这个分叉来维护并增强该包以适应生产应用的需求。更多关于原始包的详细信息,请参阅其仓库:timetable

特性

  • 定制化、带有动画效果的日历小部件
  • 日视图、周视图和月视图
导航 动画
回调 更改可见日期范围

可用布局

多日期课程表 <MultiDateTimetable>

这是一个显示多个连续天数的课程表小部件。

明亮模式 暗黑模式
循环多日期课程表 <RecurringMultiDateTimetable>

这是一个显示多个连续天数但不显示日期且无周指示器的课程表小部件。

明亮模式 暗黑模式
紧凑月课程表 <CompactMonthTimetable>

这是一个以页面视图形式显示 <MonthWidget> 的课程表小部件。

明亮模式 暗黑模式

开始使用

0. 通用信息

课程表不关心任何与时区相关的事情。所有提供的 <DateTime> 必须将 isUtc 设置为 true,但在显示事件时会忽略实际时区。

一些与日期/时间相关的参数也有一些特殊后缀:

  • date: 一个时间戳为零的 <DateTime>
  • month: 一个时间戳为零且日期为一天的 <DateTime>
  • timeOfDay: 一个在零到24小时之间的 <Duration>
  • dayOfWeek: 一个从1到7的 <int>(从 <DateTime.monday><DateTime.sunday>

课程表目前支持中文、英语、法语、德语、匈牙利语、意大利语、日语、葡萄牙语和西班牙语的本地化。即使您的应用程序仅支持英语,也必须将课程表的本地化代理添加到您的 <MaterialApp>/<CupertinoApp>/<WidgetsApp> 中:

MaterialApp(
  localizationsDelegates: [
    TimetableLocalizationsDelegate(),
    // 其他代理,例如 `GlobalMaterialLocalizations.delegate`
  ],
  // ...
);

提示: 如果你想贡献新的本地化,请遵循 <TimetableLocalizationsDelegate> 的文档注释中的步骤。

1. 定义你的事件 <Event>

事件作为 <Event> 类型的实例提供。为了方便起见,有一个子类 <BasicEvent>,你可以直接实例化它。如果你想要更具体,也可以实现自己的类扩展 <Event>

警告: 大多数课程表类接受类型参数 <E extends Event>。请将其设置为你选择的 <Event> 子类(例如 <BasicEvent>),以避免运行时异常。

此外,你还需要一个 <Widget> 来显示你的事件。当使用 <BasicEvent> 时,这可以简单地是 <BasicEventWidget>

2. 创建一个日期控制器(可选)

类似于 <ScrollController><TabController><DateController> 负责与课程表的小部件交互并管理它们的状态。顾名思义,你可以使用 <DateController> 访问当前可见的日期,并且可以动画或跳转到不同的日期。通过提供一个 <VisibleDateRange>,你还可以自定义一次可见多少天以及它们是否,例如,对齐到星期。

final myDateController = DateController(
  // 所有参数都是可选的,默认值如下。
  initialDate: DateTimeTimetable.today(),
  visibleRange: VisibleDateRange.week(startOfWeek: DateTime.monday),
);

注意: 不要忘记在 <State.dispose> 中释放你的控制器!

这里有一些可用的 <VisibleDateRange>

  • <VisibleDateRange.days>:显示 visibleDayCount 连续天数,在 swipeRange 天(对齐到 alignmentDate)范围内从 minDatemaxDate
  • <VisibleDateRange.week>:显示并对齐整个星期,具有可配置的 startOfWeek,范围从 minDatemaxDate
  • <VisibleDateRange.weekAligned>:显示 visibleDayCount 连续天数,同时对齐到整个星期,具有可配置的 firstDay,范围从 minDatemaxDate —— 可用于显示五天工作周
3. 创建一个时间控制器(可选)

类似于上面的 <DateController><TimeController> 同样负责与课程表的小部件交互并管理它们的状态。更具体地说,它控制 <MultiDateTimetable><RecurringMultiDateTimetable> 中的可见时间范围和缩放因子。你还可以编程方式更改这些,例如,动画显示全天。

final myTimeController = TimeController(
  // 所有参数都是可选的。默认情况下,最初显示一整天,你可以缩放到查看仅仅一分钟。
  minDuration: 15.minutes, // 最近的缩放
  maxDuration: 23.hours, // 最远的缩放
  initialRange: TimeRange(9.hours, 17.hours),
  maxRange: TimeRange(0.hours, 24.hours),
);

注意: 这个例子使用了一些 <time> 库的扩展方法来更简洁地创建 <Duration>

注意: 不要忘记在 <State.dispose> 中释放你的控制器!

4. 创建你的课程表小部件

课程表的小部件配置通过继承小部件提供。你可以使用 <TimetableConfig<E>> 一次性提供所有配置:

TimetableConfig<BasicEvent>(
  // 必须:
  dateController: _dateController,
  timeController: _timeController,
  eventBuilder: (context, event) => BasicEventWidget(event),
  child: MultiDateTimetable<BasicEvent>(),
  // 可选:
  eventProvider: (date) => someListOfEvents,
  allDayEventBuilder: (context, event, info) =>
      BasicAllDayEventWidget(event, info: info),
  allDayOverflowBuilder: (date, overflowedEvents) => /* … */,
  callbacks: TimetableCallbacks(
    // onWeekTap, onDateTap, onDateBackgroundTap, onDateTimeBackgroundTap, 和
    // onMultiDateHeaderOverflowTap
  ),
  theme: TimetableThemeData(
    context,
    // startOfWeek: DateTime.monday,
    // 查看“主题”部分以获取更多信息。
  ),
)

你已经完成了🎉

主题

课程表已经支持自动适应环境 <ThemeData> 的亮色和暗色主题。但是,你可以通过提供自定义的 <TimetableThemeData> 来自定义几乎所有的组件样式。

要应用你自己的主题,请在 <TimetableConfig<E>>(或直接在 <TimetableTheme>)中指定它:

TimetableConfig<BasicEvent>(
  theme: TimetableThemeData(
    context,
    startOfWeek: DateTime.monday,
    dateDividersStyle: DateDividersStyle(
      context,
      color: Colors.blue.withOpacity(.3),
      width: 2,
    ),
    dateHeaderStyleProvider: (date) =>
        DateHeaderStyle(context, date, tooltip: 'My custom tooltip'),
    nowIndicatorStyle: NowIndicatorStyle(
      context,
      lineColor: Colors.green,
      shape: TriangleNowIndicatorShape(color: Colors.green),
    ),
    // 查看“主题”部分以获取更多信息。
  ),
  // 其他属性...
)

注意: <TimetableThemeData> 和所有组件样式提供了两个构造函数:

  • 默认构造函数接受 <BuildContext> 和有时是一个天或一个月,使用环境主题和本地化的信息生成默认值。你仍然可以通过可选的命名参数覆盖所有选项。
  • 命名的 raw 构造函数通常是 const 的,并且需要所有选项的必填参数。

高级功能

拖放
Drag and Drop demo

你可以轻松地使 <MultiDateTimetable><RecurringMultiDateTimetable> 内容区域中的事件可拖动,只需将它们包装在 <PartDayDraggableEvent> 中:

PartDayDraggableEvent(
  // 用户开始拖动此事件。
  onDragStart: () {},
  // 事件被拖动到给定的 `[DateTime]`。
  onDragUpdate: (dateTime) {},
  // 用户完成拖动事件并落在给定的 `[DateTime]`。
  onDragEnd: (dateTime) {},
  child: MyEventWidget(),
  // 默认情况下,当拖动时,子项将以降低的透明度显示。当然,你可以自定义这一点:
  childWhileDragging: OptionalChildWhileDragging(),
)

课程表不会自动显示一个移动反馈小部件在当前位置。相反,你可以自定义这一点,例如,使事件的开始时间对齐到15分钟的倍数。请参阅包含的示例应用,我们在其中实现了这一点,通过显示拖动反馈作为一个时间叠加层。

如果你有其他小部件可以从课程表外部拖入课程表,你必须给你的 <MultiDateContent> 和每个 <PartDayDraggableEvent> 一个 <geometryKey><geometryKey> 是一个 <GlobalKey<MultiDateContentGeometry>>,它允许将当前拖动偏移量转换为 <DateTime>

final geometryKey = GlobalKey<MultiDateContentGeometry>();

final timetable = MultiDateTimetable(contentGeometryKey: geometryKey);
// 或者 `MultiDateContent(geometryKey: geometryKey)` 如果你从提供的模块化小部件构建你的课程表。

final draggableEvent = PartDayDraggableEvent.forGeometryKeys(
  {geometryKey},
  // `child`, `childWhileDragging` 和回调在这里也是可用的。
);

// 或者,你可以手动将偏移量转换为 `DateTime`:
final dateTime = geometryKey.currentState!.resolveOffset(globalOffset);

你甚至可以提供将事件拖入多个课程表:给每个课程表一个自己的 <geometryKey> 并传递它们到 <PartDayDraggableEvent.forGeometryKeys>。在回调中,你会收到当前正在拖动事件的课程表的 <geometryKey>。请参阅 <PartDayDraggableEvent.geometryKeys> 了解确切的行为。

时间叠加层
Drag and Drop demo

除了显示事件,<MultiDateTimetable><RecurringMultiDateTimetable> 还可以在每一天显示时间范围的叠加层。在上面的截图中,工作日的早上8点之前和晚上8点之后,周末全天都有浅灰色叠加层。时间叠加层的提供方式与事件类似:只需向你的 <TimetableConfig<E>> 添加一个 <timeOverlayProvider>(或直接使用 <DefaultTimeOverlayProvider>)。

TimetableConfig<MyEvent>(
  timeOverlayProvider: (context, date) => <TimeOverlay>[
    TimeOverlay(
      start: 0.hours,
      end: 8.hours,
      widget: ColoredBox(color: Colors.black12),
      position: TimeOverlayPosition.behindEvents, // 默认,也可选择 `inFrontOfEvents`
    ),
    TimeOverlay(
      start: 20.hours,
      end: 24.hours,
      widget: ColoredBox(color: Colors.black12),
    ),
  ],
  // 其他属性...
)

提供者只是一个接收日期并返回该日期的 <TimeOverlay> 列表的函数。因此,上述示例会在每天早上8点之前和晚上8点之后绘制一个浅灰色背景。


示例代码

以下是完整的示例代码,展示了如何使用 flutter_enhanced_timetable 插件创建一个课程表。

import 'package:black_hole_flutter/black_hole_flutter.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:time/time.dart';
import 'package:flutter_enhanced_timetable/flutter_enhanced_timetable.dart';

// ignore: unused_import
import 'positioning_demo.dart';
import 'utils.dart';

Future<void> main() async {
  initDebugOverlay();
  runApp(ExampleApp(child: TimetableExample()));
}

class TimetableExample extends StatefulWidget {
  [@override](/user/override)
  State<TimetableExample> createState() => _TimetableExampleState();
}

class _TimetableExampleState extends State<TimetableExample>
    with TickerProviderStateMixin {
  var _visibleDateRange = PredefinedVisibleDateRange.week;
  void _updateVisibleDateRange(PredefinedVisibleDateRange newValue) {
    setState(() {
      _visibleDateRange = newValue;
      _dateController.visibleRange = newValue.visibleDateRange;
    });
  }

  bool get _isRecurringLayout =>
      _visibleDateRange == PredefinedVisibleDateRange.fixed;

  late final _dateController = DateController(
    // 所有参数都是可选的。
    // initialDate: DateTimeTimetable.today(),
    visibleRange: _visibleDateRange.visibleDateRange,
  );

  final _timeController = TimeController(
    // 所有参数都是可选的。
    // minDuration: 1.hours,
    // maxDuration: 10.hours,
    // initialRange: TimeRange(8.hours, 20.hours),
    maxRange: TimeRange(0.hours, 24.hours),
  );

  final _draggedEvents = <BasicEvent>[];

  [@override](/user/override)
  void dispose() {
    _timeController.dispose();
    _dateController.dispose();
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return TimetableConfig<BasicEvent>(
      // 必须:
      dateController: _dateController,
      timeController: _timeController,
      eventBuilder: (context, event) => _buildPartDayEvent(event),
      // ignore: sort_child_properties_last
      child: Column(children: [
        _buildAppBar(),
        Expanded(
          child: _isRecurringLayout
              ? RecurringMultiDateTimetable<BasicEvent>()
              : MultiDateTimetable<BasicEvent>(),
        ),
      ]),
      // 可选:
      eventProvider: eventProviderFromFixedList(positioningDemoEvents),
      allDayEventBuilder: (context, event, info) => BasicAllDayEventWidget(
        event,
        info: info,
        onTap: () => _showSnackBar('All-day event $event tapped'),
      ),
      timeOverlayProvider: mergeTimeOverlayProviders([
        positioningDemoOverlayProvider,
        (context, date) => _draggedEvents
            .map(
              (it) =>
                  it.toTimeOverlay(date: date, widget: BasicEventWidget(it)),
            )
            .whereNotNull()
            .toList(),
      ]),
      callbacks: TimetableCallbacks(
        onWeekTap: (week) {
          _showSnackBar('Tapped on week $week.');
          _updateVisibleDateRange(PredefinedVisibleDateRange.week);
          _dateController.animateTo(
            week.getDayOfWeek(DateTime.monday),
            vsync: this,
          );
        },
        onDateTap: (date) {
          _showSnackBar('Tapped on date $date.');
          _dateController.animateTo(date, vsync: this);
        },
        onDateBackgroundTap: (date) =>
            _showSnackBar('Tapped on date background at $date.'),
        onDateTimeBackgroundTap: (dateTime) =>
            _showSnackBar('Tapped on date-time background at $dateTime.'),
        onMultiDateHeaderOverflowTap: (date) =>
            _showSnackBar('Tapped on the overflow of $date.'),
      ),
      theme: TimetableThemeData(
        context,
        // startOfWeek: DateTime.monday,
        // dateDividersStyle: DateDividersStyle(
        //   context,
        //   color: Colors.blue.withOpacity(.3),
        //   width: 2,
        // ),
        // nowIndicatorStyle: NowIndicatorStyle(
        //   context,
        //   lineColor: Colors.green,
        //   shape: TriangleNowIndicatorShape(color: Colors.green),
        // ),
        // timeIndicatorStyleProvider: (time) => TimeIndicatorStyle(
        //   context,
        //   time,
        //   alwaysUse24HourFormat: false,
        // ),
      ),
    );
  }

  Widget _buildPartDayEvent(BasicEvent event) {
    final roundedTo = 15.minutes;

    return PartDayDraggableEvent(
      onDragStart: () => setState(() => _draggedEvents.add(event)),
      onDragUpdate: (dateTime) => setState(() {
        dateTime = dateTime.roundTimeToMultipleOf(roundedTo);
        final index = _draggedEvents.indexWhere((it) => it.id == event.id);
        final oldEvent = _draggedEvents[index];
        _draggedEvents[index] = oldEvent.copyWith(
          start: dateTime,
          end: dateTime + oldEvent.duration,
        );
      }),
      onDragEnd: (dateTime) {
        dateTime = (dateTime ?? event.start).roundTimeToMultipleOf(roundedTo);
        setState(() => _draggedEvents.removeWhere((it) => it.id == event.id));
        _showSnackBar('Dragged event to $dateTime.');
      },
      onDragCanceled: (isMoved) => _showSnackBar('Your finger moved: $isMoved'),
      child: BasicEventWidget(
        event,
        onTap: () => _showSnackBar('Part-day event $event tapped'),
      ),
    );
  }

  Widget _buildAppBar() {
    final colorScheme = context.theme.colorScheme;
    Widget child = AppBar(
      elevation: 0,
      titleTextStyle: TextStyle(color: colorScheme.onSurface),
      iconTheme: IconThemeData(color: colorScheme.onSurface),
      systemOverlayStyle: SystemUiOverlayStyle.light,
      backgroundColor: Colors.transparent,
      title: _isRecurringLayout
          ? null
          : MonthIndicator.forController(_dateController),
      actions: [
        IconButton(
          icon: const Icon(Icons.today),
          onPressed: () {
            _dateController.animateToToday(vsync: this);
            _timeController.animateToShowFullDay(vsync: this);
          },
          tooltip: 'Go to today',
        ),
        const SizedBox(width: 8),
        DropdownButton(
          onChanged: (visibleRange) => _updateVisibleDateRange(visibleRange!),
          value: _visibleDateRange,
          items: [
            for (final visibleRange in PredefinedVisibleDateRange.values)
              DropdownMenuItem(
                value: visibleRange,
                child: Text(visibleRange.title),
              ),
          ],
        ),
        const SizedBox(width: 16),
      ],
    );

    if (!_isRecurringLayout) {
      child = Column(children: [
        child,
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: Builder(builder: (context) {
            return DefaultTimetableCallbacks(
              callbacks: DefaultTimetableCallbacks.of(context)!.copyWith(
                onDateTap: (date) {
                  _showSnackBar('Tapped on date $date.');
                  _updateVisibleDateRange(PredefinedVisibleDateRange.day);
                  _dateController.animateTo(date, vsync: this);
                },
              ),
              child: CompactMonthTimetable(),
            );
          }),
        ),
      ]);
    }

    return Material(color: colorScheme.surface, elevation: 4, child: child);
  }

  void _showSnackBar(String content) =>
      context.scaffoldMessenger.showSnackBar(SnackBar(content: Text(content)));
}

enum PredefinedVisibleDateRange { day, threeDays, workWeek, week, fixed }

extension on PredefinedVisibleDateRange {
  VisibleDateRange get visibleDateRange {
    switch (this) {
      case PredefinedVisibleDateRange.day:
        return VisibleDateRange.days(1);
      case PredefinedVisibleDateRange.threeDays:
        return VisibleDateRange.days(3);
      case PredefinedVisibleDateRange.workWeek:
        return VisibleDateRange.weekAligned(5);
      case PredefinedVisibleDateRange.week:
        return VisibleDateRange.week();
      case PredefinedVisibleDateRange.fixed:
        return VisibleDateRange.fixed(
          DateTimeTimetable.today(),
          DateTime.daysPerWeek,
        );
    }
  }

  String get title {
    switch (this) {
      case PredefinedVisibleDateRange.day:
        return 'Day';
      case PredefinedVisibleDateRange.threeDays:
        return '3 Days';
      case PredefinedVisibleDateRange.workWeek:
        return 'Work Week';
      case PredefinedVisibleDateRange.week:
        return 'Week';
      case PredefinedVisibleDateRange.fixed:
        return '7 Days (fixed)';
    }
  }
}

// ignore_for_file: avoid_print, unused_element

更多关于Flutter增强课程表管理插件flutter_enhanced_timetable的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter增强课程表管理插件flutter_enhanced_timetable的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何使用 flutter_enhanced_timetable 插件的示例代码,用于在 Flutter 应用中增强课程表管理功能。假设你已经将 flutter_enhanced_timetable 添加到你的 pubspec.yaml 文件中并运行了 flutter pub get

首先,确保你的 pubspec.yaml 文件包含以下依赖项:

dependencies:
  flutter:
    sdk: flutter
  flutter_enhanced_timetable: ^最新版本号  # 请替换为实际版本号

然后,你可以在你的 Dart 文件中使用 flutter_enhanced_timetable 来创建一个课程表。以下是一个简单的示例代码:

import 'package:flutter/material.dart';
import 'package:flutter_enhanced_timetable/flutter_enhanced_timetable.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Enhanced Timetable Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // 定义课程数据
  List<TimetableLesson> lessons = [
    TimetableLesson(
      title: 'Math',
      teacher: 'Mr. Smith',
      startTime: TimeOfDay(hour: 8, minute: 0),
      endTime: TimeOfDay(hour: 9, minute: 0),
      dayOfWeek: Day.monday,
      location: 'Room 101',
      color: Colors.blue,
    ),
    TimetableLesson(
      title: 'English',
      teacher: 'Mrs. Johnson',
      startTime: TimeOfDay(hour: 9, minute: 30),
      endTime: TimeOfDay(hour: 10, minute: 30),
      dayOfWeek: Day.monday,
      location: 'Room 202',
      color: Colors.red,
    ),
    // 可以继续添加更多课程
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Enhanced Timetable Demo'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: EnhancedTimetable(
          lessons: lessons,
          startHour: 8,
          endHour: 18,
          intervalMinutes: 30,
          onLessonTap: (TimetableLesson lesson) {
            // 点击课程时的回调
            showDialog(
              context: context,
              builder: (context) => AlertDialog(
                title: Text('Lesson Details'),
                content: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    Text('Title: ${lesson.title}'),
                    Text('Teacher: ${lesson.teacher}'),
                    Text('Time: ${lesson.startTime.format(context)} - ${lesson.endTime.format(context)}'),
                    Text('Location: ${lesson.location}'),
                  ],
                ),
                actions: <Widget>[
                  TextButton(
                    onPressed: () => Navigator.of(context).pop(),
                    child: Text('OK'),
                  ),
                ],
              ),
            );
          },
        ),
      ),
    );
  }
}

// 格式化时间的小工具函数(可以根据需要自定义)
extension TimeOfDayFormat on TimeOfDay {
  String format(BuildContext context) {
    final DateFormat formatter = DateFormat.Hm();
    return formatter.format(DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day, hour, minute));
  }
}

在这个示例中,我们创建了一个简单的 Flutter 应用,其中包含一个使用 flutter_enhanced_timetable 插件的课程表。我们定义了一些课程数据,并将它们传递给 EnhancedTimetable 小部件。此外,我们还添加了一个点击课程时的回调,用于显示课程的详细信息。

请注意,flutter_enhanced_timetable 插件的具体 API 和功能可能会随着版本更新而变化,因此请查阅最新的官方文档以获取最新的使用指南和 API 参考。

回到顶部