Flutter组件封装:标签拖拽排序 NDragSortWrap
一、需求来源
最近需要实现一个可拖拽标签需求,实现之后顺手封装一下,效果如下:
二、使用示例
//
// DraggableDemo.dart
// flutter_templet_project
//
// Created by shang on 6/2/21 5:37 PM.
// Copyright © 6/2/21 shang. All rights reserved.
//
import 'package:flutter/material.dart';
import 'package:flutter_templet_project/basicWidget/n_drag_sort_wrap.dart';
import 'package:flutter_templet_project/extension/extension_local.dart';
class DraggableDemo extends StatefulWidget {
final String? title;
const DraggableDemo({Key? key, this.title}) : super(key: key);
@override
_DraggableDemoState createState() => _DraggableDemoState();
}
class _DraggableDemoState extends State<DraggableDemo> with TickerProviderStateMixin {
final scrollController = ScrollController();
List<String> tags = List.generate(20, (i) => "标签$i");
late List<String> others = List.generate(10, (i) => "其他${i + tags.length}");
late var tabController = TabController(length: tags.length, vsync: this);
bool canEdit = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title ?? "$widget"),
),
body: buildBody(),
);
}
Widget buildBody() {
return Scrollbar(
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
buildDragSortWrap(),
],
),
),
);
}
Widget buildDragSortWrap() {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
tabController = TabController(length: tags.length, vsync: this);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Material(
child: Row(
children: [
Expanded(
child: TabBar(
controller: tabController,
isScrollable: true,
tabs: tags.map((e) => Tab(text: e)).toList(),
labelColor: Colors.black87,
unselectedLabelColor: Colors.black38,
indicatorColor: Colors.red,
indicatorSize: TabBarIndicatorSize.label,
indicatorPadding: EdgeInsets.symmetric(horizontal: 16),
),
),
GestureDetector(
onTap: () {
DLog.d("more");
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Icon(Icons.keyboard_arrow_down),
),
)
],
),
),
buildTagBar(
onEdit: () {
canEdit = !canEdit;
setState(() {});
},
),
NDragSortWrap<String>(
spacing: 12,
runSpacing: 8,
items: tags,
itemBuilder: (context, item, isDragging) {
return buildItem(
isDragging: isDragging,
item: item,
isTopRightVisible: canEdit,
topRight: GestureDetector(
onTap: () {
DLog.d(item);
tags.remove(item);
setState(() {});
},
child: Icon(Icons.remove, size: 14, color: Colors.white),
),
);
},
onChanged: (newList) {
tags = newList;
setState(() {});
},
),
Divider(height: 16),
Wrap(
spacing: 12,
runSpacing: 8,
children: [
...others.map(
(item) {
return buildItem(
isDragging: false,
item: item,
isTopRightVisible: canEdit,
topRight: GestureDetector(
onTap: () {
DLog.d(item);
others.remove(item);
tags.add(item);
setState(() {});
},
child: Icon(Icons.add, size: 14, color: Colors.white),
),
);
},
),
],
)
],
);
},
);
}
Widget buildItem({
required bool isDragging,
required String item,
bool isTopRightVisible = true,
required Widget topRight,
}) {
return Badge(
backgroundColor: Colors.red,
textColor: Colors.white,
offset: Offset(4, -4),
isLabelVisible: isTopRightVisible,
label: topRight,
child: AnimatedContainer(
duration: Duration(milliseconds: 150),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isDragging ? Colors.green.withOpacity(0.6) : Colors.green,
borderRadius: BorderRadius.circular(8),
),
child: Text(
item,
style: TextStyle(color: Colors.white),
),
),
);
}
Widget buildTagBar({required VoidCallback onEdit}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'我的频道',
textAlign: TextAlign.center,
style: TextStyle(
color: const Color(0xFF303034),
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
Text(
' (点击编辑可排序)',
textAlign: TextAlign.center,
style: TextStyle(
color: const Color(0xFF7C7C85),
fontSize: 12,
),
),
],
),
),
GestureDetector(
onTap: onEdit,
child: Text(
'编辑',
textAlign: TextAlign.center,
style: TextStyle(
color: const Color(0xFF303034),
fontSize: 14,
),
),
),
],
),
);
}
}
三、源码
组件 NDragSortWrap
import 'package:flutter/material.dart';
import 'package:flutter_templet_project/extension/extension_local.dart';
class NDragSortWrap<T extends Object> extends StatefulWidget {
const NDragSortWrap({
super.key,
required this.items,
required this.itemBuilder,
this.onChanged,
this.spacing = 8,
this.runSpacing = 8,
});
final List<T> items;
final Widget Function(BuildContext context, T item, bool isDragging) itemBuilder;
final void Function(List<T> newList)? onChanged;
final double spacing;
final double runSpacing;
@override
State<NDragSortWrap<T>> createState() => _NDragSortWrapState<T>();
}
class _NDragSortWrapState<T extends Object> extends State<NDragSortWrap<T>> {
late List<T> _list;
@override
void initState() {
super.initState();
_list = List<T>.from(widget.items);
}
@override
void didUpdateWidget(covariant NDragSortWrap<T> oldWidget) {
super.didUpdateWidget(oldWidget);
_list = List<T>.from(widget.items);
setState(() {});
}
@override
Widget build(BuildContext context) {
return Wrap(
spacing: widget.spacing,
runSpacing: widget.runSpacing,
children: [
for (int i = 0; i < _list.length; i++) _buildDraggableItem(context, i),
],
);
}
Widget _buildDraggableItem(BuildContext context, int index) {
final item = _list[index];
return LongPressDraggable<T>(
data: item,
feedback: Material(
color: Colors.transparent,
child: widget.itemBuilder(context, item, true),
),
childWhenDragging: Opacity(
opacity: 0.3,
child: widget.itemBuilder(context, item, false),
),
onDragCompleted: () {},
onDraggableCanceled: (_, __) {},
child: DragTarget<T>(
onAcceptWithDetails: (details) {
final draggedItem = details.data;
final oldIndex = _list.indexOf(draggedItem);
final newIndex = index;
final item = _list.removeAt(oldIndex);
_list.insert(newIndex, item);
setState(() {});
widget.onChanged?.call(_list);
},
builder: (context, _, __) {
return widget.itemBuilder(context, item, false);
},
),
);
}
}
最后、总结
实现起来并不复杂,就是依赖官方组件 LongPressDraggable 提供的各种状态做数据处理,然后刷新页面即可。