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

Flutter实现带统一滚动条的嵌套ReorderableListView(支持分类/帖子重排及跨分类移动)

Flutter实现带统一滚动条的嵌套ReorderableListView(支持分类/帖子重排及跨分类移动)

嘿,我完全get你的需求了——要一个带统一滚动条的分类+帖子层级,既能拖放重排分类顺序、调整分类内的帖子顺序,还能把帖子跨分类移动对吧?我帮你整理了一个完整的可运行实现,直接就能用:

import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: TmpScreen()));

class Category {
  final String id;
  String name;
  List<Post> posts;

  Category({required this.id, required this.name, required this.posts});
}

class Post {
  final String id;
  String title;

  Post({required this.id, required this.title});
}

class TmpScreen extends StatefulWidget {
  const TmpScreen({super.key});

  @override
  State<TmpScreen> createState() => _TmpScreenState();
}

class _TmpScreenState extends State<TmpScreen> {
  // 初始化测试数据
  List<Category> categories = [
    Category(
      id: 'cat1',
      name: '技术分享',
      posts: [
        Post(id: 'post1', title: 'Flutter状态管理最佳实践'),
        Post(id: 'post2', title: 'Dart异步编程详解'),
      ],
    ),
    Category(
      id: 'cat2',
      name: '日常记录',
      posts: [
        Post(id: 'post3', title: '周末爬山的一天'),
        Post(id: 'post4', title: '新手咖啡制作指南'),
      ],
    ),
    Category(
      id: 'cat3',
      name: '资源推荐',
      posts: [
        Post(id: 'post5', title: '免费的Flutter组件库'),
        Post(id: 'post6', title: '高效的代码编辑器插件'),
      ],
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('分类帖子重排')),
      body: Scrollbar(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(16),
          child: ReorderableListView.builder(
            physics: const NeverScrollableScrollPhysics(),
            shrinkWrap: true,
            itemCount: categories.length,
            itemBuilder: (context, index) {
              final category = categories[index];
              return CategoryItem(
                key: ValueKey(category.id),
                category: category,
                onPostReorder: (oldIndex, newIndex) {
                  setState(() {
                    if (oldIndex < newIndex) newIndex--;
                    final post = category.posts.removeAt(oldIndex);
                    category.posts.insert(newIndex, post);
                  });
                },
                onPostMoved: (Post post, Category targetCategory) {
                  setState(() {
                    // 从原分类移除帖子
                    categories.firstWhere((c) => c.posts.contains(post)).posts.remove(post);
                    // 添加到目标分类
                    targetCategory.posts.add(post);
                  });
                },
              );
            },
            onReorder: (oldIndex, newIndex) {
              setState(() {
                if (oldIndex < newIndex) newIndex--;
                final category = categories.removeAt(oldIndex);
                categories.insert(newIndex, category);
              });
            },
          ),
        ),
      ),
    );
  }
}

class CategoryItem extends StatelessWidget {
  final Category category;
  final void Function(int oldIndex, int newIndex) onPostReorder;
  final void Function(Post post, Category targetCategory) onPostMoved;

  const CategoryItem({
    super.key,
    required this.category,
    required this.onPostReorder,
    required this.onPostMoved,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.only(bottom: 16),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.grey.shade300),
        borderRadius: BorderRadius.circular(8),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 分类标题栏(用于拖拽调整分类顺序)
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
            color: Colors.grey.shade50,
            child: Row(
              children: [
                const Icon(Icons.drag_indicator, size: 20),
                const SizedBox(width: 8),
                Text(
                  category.name,
                  style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
              ],
            ),
          ),
          // 帖子列表区域(支持内部重排和接收外部拖动的帖子)
          DragTarget<Post>(
            onAccept: (post) {
              onPostMoved(post, category);
            },
            builder: (context, candidateData, rejectedData) {
              return Container(
                padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                child: ReorderableListView.builder(
                  physics: const NeverScrollableScrollPhysics(),
                  shrinkWrap: true,
                  itemCount: category.posts.length,
                  itemBuilder: (context, index) {
                    final post = category.posts[index];
                    return PostItem(
                      key: ValueKey(post.id),
                      post: post,
                    );
                  },
                  onReorder: onPostReorder,
                ),
              );
            },
          ),
        ],
      ),
    );
  }
}

class PostItem extends StatelessWidget {
  final Post post;

  const PostItem({super.key, required this.post});

  @override
  Widget build(BuildContext context) {
    return LongPressDraggable<Post>(
      data: post,
      feedback: Material(
        child: Container(
          width: MediaQuery.of(context).size.width - 64,
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(8),
            boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)],
          ),
          child: Text(post.title, style: const TextStyle(fontSize: 14)),
        ),
      ),
      child: Container(
        margin: const EdgeInsets.only(bottom: 8),
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(8),
          border: Border.all(color: Colors.grey.shade200),
        ),
        child: Row(
          children: [
            const Icon(Icons.drag_indicator, size: 18, color: Colors.grey),
            const SizedBox(width: 8),
            Expanded(child: Text(post.title, style: const TextStyle(fontSize: 14))),
          ],
        ),
      ),
    );
  }
}

关键实现细节说明

  • 统一滚动处理:整个页面用SingleChildScrollView包裹并搭配Scrollbar,所有嵌套的ReorderableListView都设置physics: NeverScrollableScrollPhysics(),彻底解决嵌套滚动冲突,实现全局唯一的滚动条
  • 分类重排逻辑:外层ReorderableListView.builder负责分类的拖拽排序,通过onReorder回调直接调整categories列表的元素顺序,状态更新后UI自动刷新
  • 帖子内部分类重排:每个分类内部的ReorderableListView.builder处理帖子的重排,回调中更新对应分类的posts列表顺序,逻辑和分类重排类似
  • 跨分类移动实现:用LongPressDraggable包裹帖子item,长按即可拖动;分类的帖子区域用DragTarget包裹,当帖子拖入时触发onAccept回调,完成帖子在分类间的转移
  • 唯一Key保障:所有Reorderable的item都使用ValueKey绑定唯一id,这是ReorderableListView正常工作的核心要求,避免重排时出现元素识别错误

备注:内容来源于stack exchange,提问作者Kristi Jorgji

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.15 14:19:28