本例通過繼承StatefulWidget搅吁,使用Draggable和GridView使GridView的Item實(shí)現(xiàn)可拖拽排序冕茅。
最終效果如下:
實(shí)現(xiàn)原理:
不管是Flutter還是Android應(yīng)用中GridView這類列表的展示通常都是基于數(shù)據(jù)源,在Flutter中店茶,我們?nèi)绻胍oGridView進(jìn)行排序蜕便,只需要修改其數(shù)據(jù)源List的順序就能實(shí)現(xiàn)排序的效果。
由于每次重新排序后 都需要更新UI贩幻,因此我們選擇使用StatefulWidget作為父控件玩裙,當(dāng)需要更新UI,我們?cè)趕etState函數(shù)中修改數(shù)據(jù)源List即可段直。
按照慣例,先來個(gè)空白頁用于展示我們的UI溶诞。
void main() => runApp(new MyApp());
///用于展示Demo的界面鸯檬,其中的MaterialApp、ThemeData螺垢、AppBar都是不必要的喧务,只是稍微美觀一點(diǎn)。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new Scaffold(
appBar: new AppBar(
title: new Text("DraggableDemo"),
),
// body: MyDraggable()),
// body: Drag2TargetPage()),
// body: DraggableItemDemo()),
body: DraggableGridViewDemo()),//此處為本例將要展示的頁面
);
}
}
有了空白頁枉圃,就可以開始封裝我們想要的可拖拽GridView了功茴,先看一下GridView該怎么用
///The most commonly used grid layouts are [GridView.count]
根據(jù)GridView的文檔,最常用的是GridView.count孽亲,我們就從這個(gè)創(chuàng)建方法開始看看怎么創(chuàng)建一個(gè)簡(jiǎn)單的GridView坎穿。
以下是GridView.count方法的部分參數(shù),對(duì)于參數(shù)的說明我知道的都寫上了注釋,有些參數(shù)現(xiàn)在還不了解玲昧。
GridView.count({
Key key,//一般不需要傳栖茉,用于區(qū)分Item是否為同一個(gè),大多數(shù)時(shí)候是在remove某個(gè)Item的時(shí)候系統(tǒng)通過這個(gè)key來執(zhí)行remove的動(dòng)畫孵延。
Axis scrollDirection = Axis.vertical,//滾動(dòng)的方向
bool reverse = false,//是否反向
ScrollController controller,//主要用于控制GridView的滾動(dòng)和設(shè)置滾動(dòng)監(jiān)聽吕漂。當(dāng)item數(shù)量超出屏幕 拖動(dòng)Item到底部或頂部 可使用ScrollController滾動(dòng)GridView 實(shí)現(xiàn)自動(dòng)滾動(dòng)的效果。
@required int crossAxisCount,//列或者行數(shù)尘应,取決于滾動(dòng)方向惶凝,即非主軸方向上的item個(gè)數(shù)
double childAspectRatio = 1.0,//item的寬高比
List<Widget> children = const <Widget>[],//itemList
})
class GridViewPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView.count(
childAspectRatio: 3.0, //item寬高比
scrollDirection: Axis.vertical, //默認(rèn)vertical
crossAxisCount: 3, //列數(shù)
children: _buildGridChildren(context),
);
}
//生成widget列表
List<Widget> _buildGridChildren(BuildContext context) {
final List list = List<Widget>();
for (int x = 0; x < 12; x++) {
list.add(Card(
child: Center(
child: Text('x = $x'),
),
));
}
return list;
}
}
以上是一個(gè)簡(jiǎn)單的GridView實(shí)現(xiàn),效果如下:
此時(shí)犬钢,如果我們使用上一篇中的Draggable和DragTarget的組合Item苍鲜,是不是就可以實(shí)現(xiàn)可拖拽了呢?試一試
將 dart _buildGridChildren 方法稍作修改
//生成widget列表
List<Widget> _buildGridChildren(BuildContext context) {
final List list = List<Widget>();
for (int x = 0; x < 12; x++) {
list.add(MyDraggableTarget(data: 'x = $x'));
}
return list;
}
效果如下:可以看到娜饵,現(xiàn)在確實(shí)已經(jīng)可以拖動(dòng)坡贺,并且數(shù)據(jù)也成功接收了。接下來想要實(shí)現(xiàn)排序的功能就是對(duì)數(shù)據(jù)做處理然后setState啦箱舞。
如果能在DragTarget的onAccept方法中直接獲取到數(shù)據(jù)源List遍坟,那么我們只需要把拖拽的item從他原來的位置remove,再insert到目標(biāo)位置晴股,就可以實(shí)現(xiàn)一個(gè)粗糙的拖拽排序了愿伴。
核心代碼如下:
onAccept: (fromIndex) {
setState(() {
final temp = widget._dataList[fromIndex];
widget._dataList.remove(temp);
widget._dataList.insert(index, temp);
});
},
順便了解一下LongPressDraggable,是Draggable的子類电湘,區(qū)別就是手勢(shì)識(shí)別需要長(zhǎng)按才會(huì)觸發(fā)拖動(dòng)隔节,不詳細(xì)說明了,用起來是一樣的寂呛。
那么最終實(shí)現(xiàn)的可拖拽的GridView會(huì)是這樣的:
長(zhǎng)按后Item變?yōu)榭赏献顟B(tài)怎诫,拖拽后松手,會(huì)將Item插入到對(duì)應(yīng)位置贷痪。
代碼如下:
class GridViewPage3 extends StatefulWidget {
@override
State<StatefulWidget> createState() => _GridViewPage3State();
final List _dataList = <String>[
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'10',
'11',
].toList();
}
class _GridViewPage3State extends State<GridViewPage3> {
@override
Widget build(BuildContext context) {
return GridView.count(
childAspectRatio: 3.0, //item寬高比
scrollDirection: Axis.vertical, //默認(rèn)vertical
crossAxisCount: 3, //列數(shù)
children: _buildGridChildren(context),
);
}
//生成widget列表
List<Widget> _buildGridChildren(BuildContext context) {
final List list = List<Widget>();
for (int x = 0; x < widget._dataList.length; x++) {
list.add(_buildItemWidget(context, x));
}
return list;
}
Widget _buildItemWidget(BuildContext context, int index) {
return LongPressDraggable(
data: index, //這里data使用list的索引幻妓,方便交換數(shù)據(jù)
child: DragTarget<int>(
//松手時(shí) 如果onWillAccept返回true 那么久會(huì)調(diào)用
onAccept: (data) {
setState(() {
final temp = widget._dataList[data];
widget._dataList.remove(temp);
widget._dataList.insert(index, temp);
});
},
//繪制widget
builder: (context, data, rejects) {
return Card(
child: Center(
child: Text('x = ${widget._dataList[index]}'),
),
);
},
//手指拖著一個(gè)widget從另一個(gè)widget頭上滑走時(shí)會(huì)調(diào)用
onLeave: (data) {
print('$data is Leaving item $index');
},
//接下來松手 是否需要將數(shù)據(jù)給這個(gè)widget? 因?yàn)樾枰谕蟿?dòng)時(shí)改變UI劫拢,所以在這里直接修改數(shù)據(jù)源
onWillAccept: (data) {
print('$index will accept item $data');
return true;
},
),
onDragStarted: () {
//開始拖動(dòng)肉津,備份數(shù)據(jù)源
print('item $index ---------------------------onDragStarted');
},
onDraggableCanceled: (Velocity velocity, Offset offset) {
print(
'item $index ---------------------------onDraggableCanceled,velocity = $velocity,offset = $offset');
//拖動(dòng)取消,還原數(shù)據(jù)源
},
onDragCompleted: () {
//拖動(dòng)完成舱沧,刷新狀態(tài)妹沙,重置willAcceptIndex
print("item $index ---------------------------onDragCompleted");
},
//用戶拖動(dòng)item時(shí),那個(gè)給用戶看起來被拖動(dòng)的widget熟吏,(就是會(huì)跟著用戶走的那個(gè)widget)
feedback: SizedBox(
child: Center(
child: Icon(Icons.feedback),
),
),
//這個(gè)是當(dāng)item被拖動(dòng)時(shí)距糖,item原來位置用來占位的widget玄窝,(用戶把item拖走后原來的地方該顯示啥?就是這個(gè))
childWhenDragging: Container(
child: Center(
child: Icon(Icons.child_care),
),
),
);
}
}
至此肾筐,一個(gè)簡(jiǎn)單的可以拖拽GridView就完成了哆料。
接下來思考如何讓item在拖拽時(shí)讓其余Item給他讓位置,
我在 onWillAccept 中添加了Log
print('$index will accept item $fromIndex');
當(dāng)拖動(dòng)第一個(gè)Item到第3個(gè)Item上方時(shí)吗铐,將會(huì)打印
flutter: 2 will accept item 0
- 可以看到在onWillAccept方法中可以獲得拖動(dòng)的item的起點(diǎn)與終點(diǎn)东亦。
- 如果我們?cè)趏nWillAccept方法中調(diào)用setState改變數(shù)據(jù)集的順序,應(yīng)該就可以在拖動(dòng)時(shí)讓UI跟隨手指移動(dòng)而變化唬渗。
- 考慮到如果用戶最后又放棄了拖動(dòng)典阵,需要還原UI,我們應(yīng)該創(chuàng)建另一個(gè)List用來備份當(dāng)前數(shù)據(jù)集镊逝。
- 與onWillAccept方法對(duì)應(yīng)的方法為onLeave壮啊,在拖動(dòng)的Item離開時(shí)將會(huì)調(diào)用,我們?cè)贒raggable的onDragStarted的時(shí)候記錄當(dāng)前數(shù)據(jù)集到備份集合中撑蒜,每次onLeave的時(shí)候還原數(shù)據(jù)集歹啼,當(dāng)取消拖動(dòng)時(shí)也取消數(shù)據(jù)集,這樣一來座菠,我們可以把onAccept中的代碼移動(dòng)到onWillAccept狸眼。
完整代碼如下:
typedef bool CanAccept(int oldIndex, int newIndex);
typedef Widget DataWidgetBuilder<T>(BuildContext context, T data);
class SortableGridView<T> extends StatefulWidget {
final DataWidgetBuilder<T>
itemBuilder; //用于生成GridView的Item Widget的函數(shù),接收一個(gè)context參數(shù)和一個(gè)數(shù)據(jù)源參數(shù)浴滴,返回一個(gè)Widget
final CanAccept canAccept; //是否接受拖拽過來的數(shù)據(jù)的回調(diào)函數(shù)
final List<T> dataList; //數(shù)據(jù)源List
final Axis scrollDirection; //GridView的滾動(dòng)方向
final int
crossAxisCount; //非主軸方向的item數(shù)量拓萌,即 如果GridView的滾動(dòng)方向是垂直方向,那么這個(gè)字段的意思就是有多少列升略;如果為水平方向微王,則此字段代表有多少行。
final double
childAspectRatio; //每個(gè)Item的寬高比品嚣,由于GridView的Item默認(rèn)是正方形的炕倘,可以通過這個(gè)比例稍作調(diào)整『渤牛可能會(huì)有我不知道的別的辦法罩旋。
SortableGridView(
this.dataList, {
Key key,
this.scrollDirection = Axis.vertical,
this.crossAxisCount = 3,
this.childAspectRatio = 1.0,
@required this.itemBuilder,
@required this.canAccept,
}) : assert(itemBuilder != null),
assert(canAccept != null),
assert(dataList != null && dataList.length >= 0),
super(key: key);
@override
State<StatefulWidget> createState() => _SortableGridViewState<T>();
}
class _SortableGridViewState<T> extends State<SortableGridView> {
List<T> _dataList; //數(shù)據(jù)源
List<T> _dataListBackup; //數(shù)據(jù)源備份,在拖動(dòng)時(shí) 會(huì)直接在數(shù)據(jù)源上修改 來影響UI變化额嘿,當(dāng)拖動(dòng)取消等情況,需要通過備份還原
bool _showItemWhenCovered = false; //手指覆蓋的地方劣挫,即item被拖動(dòng)時(shí) 底部的那個(gè)widget是否可見册养;
int _willAcceptIndex = -1; //當(dāng)拖動(dòng)覆蓋到某個(gè)item上的時(shí)候,記錄這個(gè)item的坐標(biāo)
// int _draggingItemIndex = -1; //當(dāng)前被拖動(dòng)的item坐標(biāo)
// ScrollController _scrollController;//當(dāng)item數(shù)量超出屏幕 拖動(dòng)Item到底部或頂部 可使用ScrollController滾動(dòng)GridView 實(shí)現(xiàn)自動(dòng)滾動(dòng)的效果压固。
@override
void initState() {
super.initState();
_dataList = widget.dataList;
_dataListBackup = _dataList.sublist(0);
// _scrollController = ScrollController();
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
// _scrollController?.dispose();
}
@override
Widget build(BuildContext context) {
return GridView.count(
// controller: _scrollController,
childAspectRatio: widget.childAspectRatio, //item寬高比
scrollDirection: widget.scrollDirection, //默認(rèn)vertical
crossAxisCount: widget.crossAxisCount, //列數(shù)
children: _buildGridChildren(context),
);
}
//生成widget列表
List<Widget> _buildGridChildren(BuildContext context) {
final List list = List<Widget>();
for (int x = 0; x < _dataList.length; x++) {
list.add(_buildDraggable(context, x));
}
return list;
}
//繪制一個(gè)可拖拽的控件球拦。
Widget _buildDraggable(BuildContext context, int index) {
return LayoutBuilder(
builder: (context, constraint) {
return LongPressDraggable(
data: index,
child: DragTarget<int>(
//松手時(shí) 如果onWillAccept返回true 那么久會(huì)調(diào)用,本案例不使用。
onAccept: (int data) {},
//繪制widget
builder: (context, data, rejects) {
return _willAcceptIndex >= 0 && _willAcceptIndex == index
? null
: widget.itemBuilder(context, _dataList[index]);
},
//手指拖著一個(gè)widget從另一個(gè)widget頭上滑走時(shí)會(huì)調(diào)用
onLeave: (int data) {
//TODO 這里應(yīng)該還可以優(yōu)化坎炼,當(dāng)用戶滑出而又沒有滑入某個(gè)item的時(shí)候 可以重新排列 讓當(dāng)前被拖走的item的空白被填滿
print('$data is Leaving item $index');
_willAcceptIndex = -1;
setState(() {
_showItemWhenCovered = false;
_dataList = _dataListBackup.sublist(0);
});
},
//接下來松手 是否需要將數(shù)據(jù)給這個(gè)widget愧膀? 因?yàn)樾枰谕蟿?dòng)時(shí)改變UI,所以在這里直接修改數(shù)據(jù)源
onWillAccept: (int fromIndex) {
print('$index will accept item $fromIndex');
final accept = fromIndex != index;
if (accept) {
_willAcceptIndex = index;
_showItemWhenCovered = true;
_dataList = _dataListBackup.sublist(0);
final fromData = _dataList[fromIndex];
setState(() {
_dataList.removeAt(fromIndex);
_dataList.insert(index, fromData);
});
}
return accept;
},
),
onDragStarted: () {
//開始拖動(dòng)谣光,備份數(shù)據(jù)源
// _draggingItemIndex = index;
_dataListBackup = _dataList.sublist(0);
print('item $index ---------------------------onDragStarted');
},
onDraggableCanceled: (Velocity velocity, Offset offset) {
print(
'item $index ---------------------------onDraggableCanceled,velocity = $velocity,offset = $offset');
//拖動(dòng)取消檩淋,還原數(shù)據(jù)源
setState(() {
_willAcceptIndex = -1;
_showItemWhenCovered = false;
_dataList = _dataListBackup.sublist(0);
});
},
onDragCompleted: () {
//拖動(dòng)完成,刷新狀態(tài)萄金,重置willAcceptIndex
print("item $index ---------------------------onDragCompleted");
setState(() {
_showItemWhenCovered = false;
_willAcceptIndex = -1;
});
},
//用戶拖動(dòng)item時(shí)蟀悦,那個(gè)給用戶看起來被拖動(dòng)的widget,(就是會(huì)跟著用戶走的那個(gè)widget)
feedback: SizedBox(
width: constraint.maxWidth,
height: constraint.maxHeight,
child: widget.itemBuilder(context, _dataList[index]),
),
//這個(gè)是當(dāng)item被拖動(dòng)時(shí)氧敢,item原來位置用來占位的widget日戈,(用戶把item拖走后原來的地方該顯示啥?就是這個(gè))
childWhenDragging: Container(
child: SizedBox(
child: _showItemWhenCovered
? widget.itemBuilder(context, _dataList[index])
: null,
),
),
);
},
);
}
}
然后將這個(gè)自定義的SortableGridView創(chuàng)建出來孙乖,填充到最開始的空白頁中浙炼。即可實(shí)現(xiàn)最終的效果。
使用方式如下:
class DraggableGridViewDemo extends StatelessWidget {
final List<String> channelItems = List<String>();
@override
Widget build(BuildContext context) {
for (int x = 0; x < 20; x++) {
channelItems.add("x = $x");
}
return SortableGridView(
channelItems,
childAspectRatio: 3.0, //寬高3比1
crossAxisCount: 3, //3列
scrollDirection: Axis.vertical, //豎向滑動(dòng)
canAccept: (oldIndex, newIndex) {
return true;
},
itemBuilder: (context, data) {
return Card(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
child: Center(
child: Text(data),
),
));
},
);
}
}
至此唯袄,一個(gè)可拖拽的還算能看的GridView就算完成了弯屈。
最近在github上看到了一個(gè)DragAndDropList。
地址:https://github.com/Norbert515/flutter_list_drag_and_drop
是一個(gè)可拖拽的ListView越妈。通過修改Draggable的源碼實(shí)現(xiàn)季俩,感覺這個(gè)思路比我的好,先研究一下梅掠,可能下一篇會(huì)按照他的方式優(yōu)化一下SortableGridView酌住。