You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter实现跨训练组拖拽练习:当前方案可行性及替代方案咨询

跨训练组拖拽练习的实现方案

当前代码的问题

  • 嵌套ReoderableListView会触发手势冲突,内外层列表的拖拽逻辑互相干扰,无法正常完成跨组拖拽。
  • Draggable未传递拖拽数据(比如练习的ID或实体对象),DragTarget也没有实现接收数据并更新列表的逻辑,核心的跨组拖拽交互完全缺失。
  • 存在语法错误:direction未指定方向、confirmDismiss是空实现且缺少括号、map方法未闭合、Draggable的child后错误使用分号等。

最优替代方案

我们用Draggable+DragTarget配合状态管理(比如setState或状态管理库)来实现,外层用普通列表展示训练组,每个训练组内部用列表展示练习,同时给每个练习加Draggable,每个训练组的容器加DragTarget。

代码示例

假设我们有以下数据结构:

// 练习实体
class Exercise {
  final String id;
  final String name;

  Exercise({required this.id, required this.name});
}

// 训练组实体
class Workout {
  final String id;
  final String name;
  List<Exercise> exercises;

  Workout({required this.id, required this.name, required this.exercises});
}

然后是UI实现:

class WorkoutDragPage extends StatefulWidget {
  @override
  _WorkoutDragPageState createState() => _WorkoutDragPageState();
}

class _WorkoutDragPageState extends State<WorkoutDragPage> {
  List<Workout> workouts = [
    Workout(
      id: 'w1',
      name: '训练组1',
      exercises: [Exercise(id: 'e1', name: '俯卧撑'), Exercise(id: 'e2', name: '引体向上')],
    ),
    Workout(
      id: 'w2',
      name: '训练组2',
      exercises: [Exercise(id: 'e3', name: '深蹲'), Exercise(id: 'e4', name: '平板支撑')],
    ),
  ];

  // 从原训练组移除练习并返回
  Exercise _removeExercise(String workoutId, String exerciseId) {
    final workout = workouts.firstWhere((w) => w.id == workoutId);
    final exercise = workout.exercises.firstWhere((e) => e.id == exerciseId);
    workout.exercises.remove(exercise);
    return exercise;
  }

  // 添加练习到目标训练组
  void _addExercise(String targetWorkoutId, Exercise exercise) {
    final targetWorkout = workouts.firstWhere((w) => w.id == targetWorkoutId);
    targetWorkout.exercises.add(exercise);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('跨组拖拽练习')),
      body: ListView.builder(
        itemCount: workouts.length,
        itemBuilder: (context, index) {
          final workout = workouts[index];
          return DragTarget<Exercise>(
            onAccept: (receivedExercise) {
              // 避免将当前组的练习重复添加
              final isFromCurrentWorkout = workout.exercises.any((e) => e.id == receivedExercise.id);
              if (!isFromCurrentWorkout) {
                setState(() {
                  _addExercise(workout.id, receivedExercise);
                });
              }
            },
            builder: (context, candidateData, rejectedData) {
              // 拖拽进入时改变容器样式,提供视觉反馈
              final isDraggingOver = candidateData.isNotEmpty;
              return Container(
                margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                padding: EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: isDraggingOver ? Colors.blue[50] : Colors.white,
                  border: Border.all(color: isDraggingOver ? Colors.blue : Colors.grey.shade200),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(workout.name, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                    SizedBox(height: 8),
                    ListView.builder(
                      shrinkWrap: true,
                      physics: NeverScrollableScrollPhysics(),
                      itemCount: workout.exercises.length,
                      itemBuilder: (context, exerciseIndex) {
                        final exercise = workout.exercises[exerciseIndex];
                        return Draggable<Exercise>(
                          data: exercise,
                          feedback: Material(
                            child: Container(
                              padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
                              decoration: BoxDecoration(
                                color: Colors.white,
                                borderRadius: BorderRadius.circular(8),
                                boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4)],
                              ),
                              child: Text(exercise.name, style: TextStyle(fontSize: 16)),
                            ),
                          ),
                          childWhenDragging: Opacity(
                            opacity: 0.5,
                            child: _ExerciseItem(exercise: exercise),
                          ),
                          child: _ExerciseItem(exercise: exercise),
                          onDragEnd: (details) {
                            // 拖拽成功放置后,从原训练组移除练习
                            if (details.wasAccepted) {
                              setState(() {
                                _removeExercise(workout.id, exercise.id);
                              });
                            }
                          },
                        );
                      },
                    ),
                  ],
                ),
              );
            },
          );
        },
      ),
    );
  }
}

// 练习项组件
class _ExerciseItem extends StatelessWidget {
  final Exercise exercise;

  const _ExerciseItem({required this.exercise});

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: 4),
      padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      decoration: BoxDecoration(
        color: Colors.grey.shade50,
        borderRadius: BorderRadius.circular(6),
      ),
      child: Text(exercise.name, style: TextStyle(fontSize: 16)),
    );
  }
}

核心逻辑说明

  • Draggable:每个练习项包装成Draggable,传递练习实体作为data;feedback是拖拽时跟随光标显示的组件;childWhenDragging设置拖拽时原位置的半透明效果;onDragEnd中处理从原训练组移除练习的逻辑。
  • DragTarget:每个训练组的容器包装成DragTarget,指定接收Exercise类型的数据;onAccept中处理将练习添加到目标训练组的逻辑;builder中根据拖拽状态改变容器样式,给用户明确的视觉反馈。
  • 状态更新:通过setState更新训练组列表,触发UI重绘,完成练习的跨组移动。

额外优化点

  • 如果需要支持训练组内的练习排序,可以在练习列表中使用ReorderableListView,但要注意和跨组拖拽的手势冲突,建议给排序和跨组拖拽设置不同的触发方式(比如长按触发跨组拖拽,短按触发排序)。
  • 如果数据量较大,建议使用Provider、Riverpod等状态管理工具来管理训练组数据,避免在StatefulWidget中处理复杂的状态逻辑。

内容的提问来源于stack exchange,提问作者Andrew .M.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 08:36:11