不多说,我们肯定遇到过这个需求:
把一个控件从当前位置移动到另一个位置。可能需求最多的就像是支付宝应用页面的编辑:
比如,我想把最近使用的 红包 添加到 我的应用 当中,支付宝这里是用的 + 号。
那如果产品说我就想让它拖过去,这可咋整?
不慌,Flutter 也为我们提供了相关的 Widget。
Draggable
Flutter 如果要实现这种效果,那么非 Draggable 不可。
照例我们查看官方文档。
A widget that can be dragged from to a DragTarget.
可拖动到 DragTarget 的小部件。
复制代码
那也就是说,除了 Draggable ,还有一个 DragTarget。
DragTarget 是用来接收我们拖过去的 Widget 的,我们后面再说。
先说Draggable。
在官方文档找了一圈没发现Demo,那没办法了,直接开撸。
先看构造函数:
class Draggable<T> extends StatefulWidget {
/// Creates a widget that can be dragged to a [DragTarget].
///
/// The [child] and [feedback] arguments must not be null. If
/// [maxSimultaneousDrags] is non-null, it must be non-negative.
const Draggable({
Key key,
@required this.child,
@required this.feedback,
this.data,
this.axis,
this.childWhenDragging,
this.feedbackOffset = Offset.zero,
this.dragAnchor = DragAnchor.child,
this.affinity,
this.maxSimultaneousDrags,
this.onDragStarted,
this.onDraggableCanceled,
this.onDragEnd,
this.onDragCompleted,
this.ignoringFeedbackSemantics = true,
}) : assert(child != null),
assert(feedback != null),
assert(ignoringFeedbackSemantics != null),
assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0),
super(key: key);
}
复制代码
可以看到该类还支持泛型,这个泛型是来界定 data
的,后面再说。
先来看看必须的参数,child 和 feedback。
child 应该不比多说,说一下 feedback。
点击查看feedback 参数,上面的注释这样写着:
当拖动正在进行时在指针下显示的小部件。
既然搞懂了必传参数,那就开撸:
Widget _createGridView(List<String> _items) {
return GridView.builder(
itemCount: _items.length,
shrinkWrap: true, // 相当于Android的 wrap_content ,也就是包裹住该 widget的高度
physics: NeverScrollableScrollPhysics(), // 不允许滑动
padding: EdgeInsets.all(10),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5, // 设置 gridview 每一行的数量
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
itemBuilder: (context, index) {
return Draggable( // 返回一个Draggable
// 必须要一个Material,不然拖动时Text会有双下划线
feedback: Material(
child: Container(
height: 100,
width: 100,
color: Colors.blueAccent,
alignment: Alignment.center,
child: Text(
_items[index],
style: TextStyle(color: Colors.white),
),
),
),
child: Container(
color: Colors.blueAccent,
alignment: Alignment.center,
child: Text(
_items[index],
style: TextStyle(color: Colors.white),
),
),
);
},
);
}
复制代码
我们定义一个 GridView,里面每一个 item 都是一个 Draggable。
来运行一下看效果:
可以看到我们确实是可以拖动了,大功已经告成一半了。
那么我们下面开始定义接收的部件 DragTarget。
DragTarget
废话也不多说,直接看构造函数,看看什么是必填:
class DragTarget<T> extends StatefulWidget {
/// Creates a widget that receives drags.
///
/// The [builder] argument must not be null.
const DragTarget({
Key key,
@required this.builder,
this.onWillAccept,
this.onAccept,
this.onLeave,
}) : super(key: key);
}
复制代码
可以看到必传的参数也就是一个builder,用来构建我们的页面使用。
其他参数看名字也都能明白:
- onWillAccept 拖到该控件上时调用
- onAccept 放到该控件时调用
- onLeave 没有放到该控件时调用
那我们这里只需要一个确认已经放到该控件时的回调,来接收我们传过来的值。
撸码:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('DraggablePage'),
),
body: Column(
children: <Widget>[
_createGridView(_items1),
SizedBox(height: 100,),// 占位
DragTarget<String>( // 用来接收数据的 Widget
builder: (
BuildContext context,
List<dynamic> accepted,
List<dynamic> rejected,
) {
return _createGridView(_items2);
},
// 用来接收数据
onAccept: (String data) {
setState(() {
_items2.add(data);
});
},
)
],
),
);
}
复制代码
这是完整的 build 代码,但是还需要改造一下我们的 Draggable 。
再看一下Draggable的构造函数:
class Draggable<T> extends StatefulWidget {
/// Creates a widget that can be dragged to a [DragTarget].
///
/// The [child] and [feedback] arguments must not be null. If
/// [maxSimultaneousDrags] is non-null, it must be non-negative.
const Draggable({
Key key,
@required this.child,
@required this.feedback,
this.data,
this.axis,
this.childWhenDragging,
this.feedbackOffset = Offset.zero,
this.dragAnchor = DragAnchor.child,
this.affinity,
this.maxSimultaneousDrags,
this.onDragStarted,
this.onDraggableCanceled,
this.onDragEnd,
this.onDragCompleted,
this.ignoringFeedbackSemantics = true,
}) : assert(child != null),
assert(feedback != null),
assert(ignoringFeedbackSemantics != null),
assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0),
super(key: key);
}
复制代码
因为如果想要传入数据,那也就必须要有数据可以传,也就是我们构造函数里看到的 data
字段。
还需要删除我们的源数据,那也就是要监听拖动结束的回调,这里就是 onDragCompleted
。
我们来看改造后的Draggable:
Widget _createGridView(List<String> _items) {
return GridView.builder(
itemCount: _items.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(10),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
itemBuilder: (context, index) {
return Draggable<String>(
onDragCompleted: (){
// 在拖动结束后删除数据
setState(() {
_items.removeAt(index);
});
},
feedback: Material(
child: Container(
height: 100,
width: 100,
color: Colors.blueAccent,
alignment: Alignment.center,
child: Text(
_items[index],
style: TextStyle(color: Colors.white),
),
),
),
// 当前组件的数据
data: _items[index],
child: Container(
color: Colors.blueAccent,
alignment: Alignment.center,
child: Text(
_items[index],
style: TextStyle(color: Colors.white),
),
),
);
},
);
}
复制代码
运行看一下效果:
可以看到这样就基本完成我们的需求了,但是有人说,能不能把我拖着的源控件加个特效?
没问题,我们通过 childWhenDragging
参数来控制。
如,加个蒙层:
childWhenDragging: Container(
color: Colors.blueAccent,
alignment: Alignment.center,
foregroundDecoration: BoxDecoration(color: Colors.white30),
child: Text(
_items[index],
style: TextStyle(color: Colors.white),
),
),
复制代码
添加一个foregroundDecoration
就ok了,效果如下:
总结
通过这个小例子我们可以实现特别多的效果。
而且默认拖动的控件时可以多指触控的,也就是说我们可以同时拖动N个控件。
可以通过 Draggable 的 maxSimultaneousDrags
来控制。
构造函数里其他的参数大家可以自行了解一下。
不得不说 Flutter 是真的好用。
关注我,每天更新 Flutter & Dart 知识。
完整代码已经传至GitHub:github.com/wanglu1209/…