实现效果
主要功能
- 📉可视化事件列表,会议室状态一览无余,清晰直观
- 💥支持拖拽选择预订时间,交互自然,纵享丝滑
- 🌈提供丰富的样式配置选项,满足不同场景的视觉需求
技术实现
实现主界面
采用模块化设计思想,核心控件为DayView,用于展示单日会议室预订事件。整体架构采用SingleChildScrollView作为外层容器实现垂直滚动,内部使用Stack布局管理多个视觉元素。
- 左侧时间轴组件:负责绘制00:00-24:00的时间文本,为用户提供时间参考
/// widgets/day_view.dart
if (widget.hoursColumnStyle.width > 0) {
children.add(Positioned(
top: 0,
left: widget.isRTL ? null : 0,
child: HoursColumn.fromHeadersWidgetState(parent: this),
));
}
- 右侧背景时间线网格:自定义
CustomPaint实现每小时的时间分割线
/// widgets/day_view.dart
Widget createBackground() => Positioned.fill(
child: CustomPaint(
painter: widget.style.createBackgroundPainter(
dayView: widget,
topOffsetCalculator: calculateTopOffset,
),
),
);
- 当前时间指示器:通过
RepaintBoundary和CustomPaint实现实时更新的当前时间线
/// widgets/day_view.dart
if (_showCurrentTimeIndicator()) {
Widget? currentTimeIndicator =
(widget.currentTimeIndicatorBuilder ?? DefaultBuilders.defaultCurrentTimeIndicatorBuilder)(widget.style, calculateTopOffset, widget.hoursColumnStyle, widget.isRTL,_currentTimeNotifier);
if (currentTimeIndicator != null) {
children.add(currentTimeIndicator);
}
}
- 事件列表渲染:基于
FlutterWeekViewEvent数据模型,动态生成会议室预订事件卡片
/// widgets/day_view.dart
children.addAll(eventsDrawProperties.entries.map((entry) => entry.value.createWidget(
context,
widget,
buildResizeUpGestureDetector(entry.key),
buildResizeDownGestureDetector(entry.key),
entry.key,
)));
事件数据结构:
/// event.dart
class FlutterWeekViewEvent implements Comparable<FlutterWeekViewEvent> {
final String title; // 事件标题
final String description; // 事件描述
DateTime start; // 开始时间
DateTime end; // 结束时间
// 其他属性...
}
实现拖拽调整功能
通过GestureDetector组件实现事件的垂直拖拽调整,支持修改事件的开始和结束时间:
- 拖拽手势监听:为新创建事件添加两个
GestureDetector,分别处理上下拖拽操作 - 位置计算:根据拖拽偏移量计算新的时间位置
- 边界处理:实现最小事件时长限制,确保用户体验
- 状态更新:拖拽结束后更新事件数据并刷新UI
/// widgets/day_view.dart
/// 向上拖拽调整开始时间
Widget? buildResizeUpGestureDetector(FlutterWeekViewEvent event,) {
if (widget.resizeEventOptions == null) {
return null;
}
return GestureDetector(
onVerticalDragStart: (_) {
accumulatedResizeOffset = 0;
originalResizeEventStart = event.start;
},
onVerticalDragEnd: (_) {
DateTime newEventStart = event.start;
event.start = originalResizeEventStart;
widget.resizeEventOptions!.onEventResizedUp(event, newEventStart);
setState(() {
reset();
createEventsDrawProperties();
});
},
onVerticalDragUpdate: (details) => onEventResizeUpUpdate(event, details.primaryDelta ?? 0),
child: MouseRegion(
cursor: SystemMouseCursors.resizeUpDown,
child: Container(color: Colors.transparent),
),
);
}
实现拖放功能
利用Flutter中的Draggable和DragTarget组件实现事件的自由拖放:
Draggable可以让组件在界面上任意拖动,同时携带一个泛型T的数据,DragTarget用于定义一个拖动的目标区域,可以接收Draggable组件的信息。
- 事件包装:将事件卡片包装在
Draggable组件中,支持长按或点击触发拖拽 - 目标区域:使用
DragTarget包裹整个日视图,接收拖放事件 - 位置计算:在拖拽过程中实时计算新位置对应的时间,刷新UI
/// utils/event_grid.dart
/// 事件拖拽包装
child = _getDraggableOrLongPressDraggable(
isLongPress: options.startingGesture == DragStartingGesture.longPress,
data: event,
axis: options.allowOnlyVerticalDrag ? Axis.vertical : null,
feedback: SizedBox(
height: height!,
width: width!,
child: child,
),
childWhenDragging: Opacity(opacity: 0.5, child: child),
child: child,
);
/// widgets/day_view.dart
/// 拖放目标区域
mainWidget = DragTarget<FlutterWeekViewEvent>(
builder: (_, __, ___) => createMainWidget(),
onMove: (details){
/// 计算新位置对应的时间
RenderBox renderBox = context.findRenderObject() as RenderBox;
Offset localOffset = renderBox.globalToLocal(details.offset);
Offset correctedOffset = Offset(localOffset.dx, localOffset.dy + (verticalScrollController?.offset ?? 0) - widget.padding.top);
DateTime newStartTime = widget.date.add(calculateOffsetHourMinute(correctedOffset).asDuration);
/// 网格对齐处理
if(widget.resizeEventOptions!=null) {
newStartTime= roundTimeToFitGrid(newStartTime,
gridGranularity: widget.resizeEventOptions!.snapToGridGranularity);
}
/// 更新预览位置
widget.timeRangeStartNotifier?.value = newStartTime;
widget.timeRangeEndNotifier?.value = details.data.end.add(newStartTime.difference(details.data.start));
},
onAcceptWithDetails: (details) {
/// 处理拖放完成逻辑
RenderBox renderBox = context.findRenderObject() as RenderBox;
Offset localOffset = renderBox.globalToLocal(details.offset);
Offset correctedOffset = Offset(localOffset.dx, localOffset.dy + (verticalScrollController?.offset ?? 0) - widget.padding.top);
DateTime newStartTime = widget.date.add(calculateOffsetHourMinute(correctedOffset).asDuration);
widget.dragAndDropOptions!.onEventDragged(details.data, newStartTime);
widget.timeRangeStartNotifier?.value = details.data.start;
widget.timeRangeEndNotifier?.value = details.data.end;
/// 更新UI
setState(() {
reset();
createEventsDrawProperties();
});
},
onLeave: (event){
/// 处理拖放离开逻辑
if(event!=null) {
widget.timeRangeStartNotifier?.value = event.start;
widget.timeRangeEndNotifier?.value = event.end;
}
},
);
应用场景与价值
该技术方案适用于多种需要时间可视化的场景:
- 会议室预订系统:直观展示会议室使用情况
- 个人日程管理:清晰管理每日任务和活动
- 项目时间线:可视化项目进度和里程碑
Github
本文相关的代码基于Skyost的FlutterWeekView修改而来,地址如下: github.com/kongpf8848/…