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
相关产品推荐
相关产品推荐

