Flutter拖拉拽实战,高仿飞书会议室详情页

415 阅读3分钟

实现效果

schedule.gif

主要功能

  • 📉可视化事件列表,会议室状态一览无余,清晰直观
  • 💥支持拖拽选择预订时间,交互自然,纵享丝滑
  • 🌈提供丰富的样式配置选项,满足不同场景的视觉需求

技术实现

实现主界面

采用模块化设计思想,核心控件为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,
        ),
      ),
    );

  • 当前时间指示器:通过RepaintBoundaryCustomPaint实现实时更新的当前时间线
/// 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);
  }
}

ui.png

  • 事件列表渲染:基于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;               // 结束时间
   // 其他属性...
}

events.png

实现拖拽调整功能

通过GestureDetector组件实现事件的垂直拖拽调整,支持修改事件的开始和结束时间:

  1. 拖拽手势监听:为新创建事件添加两个GestureDetector,分别处理上下拖拽操作
  2. 位置计算:根据拖拽偏移量计算新的时间位置
  3. 边界处理:实现最小事件时长限制,确保用户体验
  4. 状态更新:拖拽结束后更新事件数据并刷新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),
    ),
  );
}

gestureDetector.png

实现拖放功能

利用Flutter中的DraggableDragTarget组件实现事件的自由拖放:

Draggable可以让组件在界面上任意拖动,同时携带一个泛型T的数据,DragTarget用于定义一个拖动的目标区域,可以接收Draggable组件的信息。

  1. 事件包装:将事件卡片包装在Draggable组件中,支持长按或点击触发拖拽
  2. 目标区域:使用DragTarget包裹整个日视图,接收拖放事件
  3. 位置计算:在拖拽过程中实时计算新位置对应的时间,刷新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;
    }
  },
);

drag_drop.png

应用场景与价值

该技术方案适用于多种需要时间可视化的场景:

  • 会议室预订系统:直观展示会议室使用情况
  • 个人日程管理:清晰管理每日任务和活动
  • 项目时间线:可视化项目进度和里程碑

Github

本文相关的代码基于SkyostFlutterWeekView修改而来,地址如下: github.com/kongpf8848/…